温馨提示:C++一秒最多计算一亿次 10^9
唯一的思考模板
背包问题
01背包问题
从动态规划的角度思考该问题:动态规划一般包含状态表示和状态计算
状态表示
针对01背包问题 状态表示用一个二维数组存储f[i, j] 代表从前i个物品中选出总体积不超过j的方案(集合的条件) f[i, j]是集合的某种属性
一是f[i, j]的属性可以是最大值, 最小值,数量 该问题中属性为最大值
二是f[i, j]所代表的集合是什么 这里表示的是满足条件的各种方案选法
状态计算 --- 对应集合的划分 --- 推导出计算公式
左0右1 代表选择当前物品的次数 选或不选
注意右边可能是空集 因为第i件物品的体积可能>背包总体积 导致装不下
因此计算公式要特判一下
#include<iostream>
#include<cmath>
using namespace std;
const int N = 1e4;
int n, m;
int v[N], w[N];//物品的体积和价值
int f[N][N];//集合属性表示
int main(){
cin >> n >> m;
for(int i = 1; i <= n; i ++)
scanf("%d %d", &v[i], &w[i]);
f[0][1~m]均为0 因为设置为全局变量 无需初始化 i从1开始
for(int i = 1; i <= n; i ++){
for(int j = 1; j <= m; j ++){
if(v[i] > j)//若当前第i件物品体积大于当前背包容量 只取左子集
f[i][j] = f[i - 1][j];
else
f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]);
}
}
cout << f[n][m];
}
优化 二维降一维 滚动数组滚动数组(简单说明)_儒rs的博客-CSDN博客
分析:首先观察题解中的二维数组 发现每次只需要用到第i行和第i - 1行 比如当i走到4 那么f[0][1~m],f[1][1~m],f[2][1~m]都不会再被访问 因此我们只需使用一维数组存储i - 1行的属性 新数据不断更新旧数据即可
#include<iostream>
#include<cmath>
using namespace std;
const int N = 1e4;
int n, m;
int v[N], w[N];//物品的体积和价值
int f[N];//集合属性表示
int main(){
cin >> n >> m;
for(int i = 1; i <= n; i ++)
scanf("%d %d", &v[i], &w[i]);
for(int i = 1; i <= n; i ++){
for(int j = m; j >= v[i]; j --){
//用到的旧数据下标只有j和j - v[i] j是当前要更新的 保证是旧数据
//j - v[j] <= j 如果是从前往后更新 后边可能会用到前边的旧数据
//因此我们可以采用从后往前更新的次序 后边的旧数据不会被前边更新时所访问
//注意j只需枚举到v[i] 因为根据优化前的条件 若j < v[i]
//即背包中装不下当前物品 则f[i][j] = f[i - 1][j]
//换到下式意思就是不需要更新 f[j] = f[j] 旧数据继续用即可
//下式相当于f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i] + w[i])
f[j] = max(f[j], f[j - v[i]] + w[i]);//新数据覆盖旧数据
}
}
cout << f[m];
}
完全背包问题
完全背包问题其实与01背包问题解题完全一致 只是01背包是对物品选择0次或1次 而完全背包则是选择0次到n次 n * v[i] <= j 直到背包内装不下n + 1个
状态数组还是一样的含义 f[i][j] 取前i个 总体积不超过j
朴素版无优化 但acwing数据已加强 过不了了
#include<iostream>
#include<cmath>
using namespace std;
const int N = 1e4;
int n, m;
int v[N], w[N];//物品的体积和价值
int f[N][N];//集合属性表示
int main(){
cin >> n >> m;
for(int i = 1; i <= n; i ++)
scanf("%d %d", &v[i], &w[i]);
for(int i = 1; i <= n; i ++){
for(int j = 1; j <= m; j ++){
for(int k = 0; k * v[i] <= j; k ++){
//这里的f[i - 1][j - v[i] * k] 的值各不相同 因此必须都要遍历
//每个k值对应一个f[i - 1][j - v[i] * k]
f[i][j] = max(f[i][j], f[i - 1][j - v[i] * k] + k * w[i]);
}
}
}
cout << f[n][m];
}
完全背包问题的优化1:
这里的限制因素只有一个 就是背包容量 所以f[i][j]和f[i-1][j-v]最后都达到j - kv 当前背包最多是全装v[i] ---> j - kv = 0
首先分析一下公式 f[i][j] = max(f[i - 1][j], f[i - 1][j - v] + w, f[i - 1][j - 2v] + 2w,......f[i - 1][j - kv] + kw)
f[i][j - v] = max(排版仅方便对比 f[i - 1][j - v], f[i - 1][j - 2v] + w, ........f[i - 1][j - kv] + (k - 1)*w)
由于当前 i 的值相同 v,w均一致 可以推导出f[i][j] = max(f[i - 1][j], f[i][j - v] + w); 从01背包演变为on背包 右边代表选n个物品(1~k)的最大值
#include<iostream>
#include<cmath>
using namespace std;
const int N = 1e4;
int n, m;
int v[N], w[N];//物品的体积和价值
int f[N][N];//集合属性表示
int main(){
cin >> n >> m;
for(int i = 1; i <= n; i ++)
scanf("%d %d", &v[i], &w[i]);
for(int i = 1; i <= n; i ++){
for(int j = 1; j <= m; j ++){
if(v[i] > j)
f[i][j] = f[i - 1][j];
else
f[i][j] = max(f[i - 1][j], f[i][j - v[i]] + w[i]);
}
}
cout << f[n][m];
}
完全背包的完全优化 在上次的基础上实现二维降一维
#include<iostream>
#include<cmath>
using namespace std;
const int N = 1e4;
int n, m;
int v[N], w[N];//物品的体积和价值
int f[N];//集合属性表示
int main(){
cin >> n >> m;
for(int i = 1; i <= n; i ++)
scanf("%d %d", &v[i], &w[i]);
for(int i = 1; i <= n; i ++)
for(int j = v[i]; j <= m; j ++)
//若j < v[i] 则f[i][j] = f[i - 1][j]
//对于一维数组来说 无需更新旧数据 恒等式不需要写
//该优化无需逆序 要注意原来的等式 max比较的右边是第i行的 01背包是第i-1行
//01背包需要逆序在于原来一维数组存的都是i-1 它从前往后更新会导致
//后边访问前边的旧数据会被修改
//而该式max右边访问的就是第i行的新数据 也就是必须要正序
//从f[i][0]到f[i][v[i] - 1]无需更新 第i行同样适用
//逆序的话反而会出问题 可能会访问到旧数据 导致出错
//f[i][j] = max(f[i - 1][j], f[i][j - v[i]] + w[i])
f[j] = max(f[j], f[j - v[i]] + w[i]);
cout << f[m];
}
多重背包问题|
跟完全背包问题有点类似
//暴力写法
#include<iostream>
#include<cmath>
using namespace std;
const int N = 110;
int s[N], v[N], w[N];
int f[N][N];
int n, m;
int main(){
cin >> n >> m;
for(int i = 1; i <= n; i ++)
cin >> v[i] >> w[i] >> s[i];
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= m; j ++){
int count = min(s[i], j / v[i]);
for(int k = 0; k <= count; k ++)
f[i][j] = max(f[i - 1][j - k * v[i]] + k * w[i], f[i][j]);
}
cout << f[n][m];
}
一维空间优化
#include<iostream>
#include<cmath>
using namespace std;
const int N = 110;
int s[N], v[N], w[N];
int f[N];
int n, m;
int main(){
cin >> n >> m;
for(int i = 1; i <= n; i ++)
cin >> v[i] >> w[i] >> s[i];
for(int i = 1; i <= n; i ++)
for(int j = m; j >= 1; j --){
int count = min(s[i], j / v[i]);
for(int k = 0; k <= count; k ++)
f[j] = max(f[j - k * v[i]] + k * w[i], f[j]);
}
cout << f[m];
}
多重背包问题||
题意同| 但数据量加强 三重循环必爆
首先可以考虑优化问题 若采用跟完全背包问题一样的优化
限制因素:背包容量 物品件数
当j-v足够大时 可以容纳s件当前物品 会出现下列情况 该项背包还没满 我还能装
f[i][j] = max(f[i-1][j], f[i-1][j-v] + w, f[i-1][j-2v] + 2w, f[i-1][j-3v] + 3w ......f[i-1][j-sv] + sw)
f[i][j-v] = max( f[i-1][j-v], f[i-1][j-2v] + w, f[i-1][j-3v] + 2w ......f[i-1][j-sv] + (s-1)w, f[i-1][j-(s+1)] + sw
而完全背包不出现这类问题的点在于物品件数无限制 可以一直取到背包体积满了为止
达到j-kv == 0 或是没有足够的空间再容纳一个
f[i][j] = max(f[i - 1][j], f[i - 1][j - v] + w, f[i - 1][j - 2v] + 2w,......f[i - 1][j - kv] + kw)
f[i][j - v] = max( f[i - 1][j - v], f[i - 1][j - 2v] + w, ........f[i - 1][j - kv] + (k - 1)*w)
优化方法 二进制优化为01背包问题
数据分析
根据上图背包的二进制划分个数最大为2000 划分后为log2 2000 约等于11 2^11 = 2048
时间复杂度从1000*2000*2000O(10^9)降到1000*2000*12(10^7) 能跑
空间复杂度 因为我们要把每个包二进制划分 数组要从1000开到1000*12 容纳这些不同的物品种数
之后便还原到了0/1背包的问题 每个新的物品只能使用一次 找最大价值
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 15000;
int v[N], w[N];
int f[N];
int n, m, a, b, s;
int main(){
cin >> n >> m;
int cnt = 0;//建议先设为0
for(int i = 1; i <= n; i ++){
scanf("%d %d %d", &a, &b, &s);
int k = 1;
while(k <= s){//对一个物品种数为s的物品进行二进制划分1 2 4 8 .... 2^k
cnt ++;
v[cnt] = k * a;//更新二进制后新的价值和体积
w[cnt] = k * b;
s -= k;
k *= 2;
}//退出循环后cnt正好代表当前新物品个数
//若以1开始 此时cnt = 总个数 + 1
//若下式判断为真 退出循环后 n = cnt 就不好直接赋值了 还要另加判断
if(s > 0){
cnt ++;
v[cnt] = s * a;
w[cnt] = s * b;
}
}
n = cnt;
//0/1背包优化模板
for(int i = 1; i <= n; i ++){
for(int j = m; j >= v[i]; j --){
f[j] = max(f[j], f[j - v[i]] + w[i]);
}
}
cout << f[m];
}
分组背包问题
与前几个问题一致 集合属性为f[i][j] 含义稍微有点变化 每组只能选一个 类似0/1背包问题
之前是前i个物品中选总体积不超过j
现在是前i组物品中选总体积不超过j
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1e4;
int v[N][N], w[N][N];
int n, m, s[N];//s数组接收每组个数
int f[N];
int main(){
cin >> n >> m;
for(int i = 1; i <= n; i ++){
cin >> s[i];//该组存在k个
for(int j = 1; j <= s[i]; j ++)
cin >> v[i][j] >> w[i][j];
}
for(int i = 1; i <= n; i ++){
for(int j = m; j >= 0; j --){//跟0/1背包问题类似 逆序遍历 防止旧数据被覆盖
for(int k = 1; k <= s[i]; k ++){
if(v[i][k] <= j)
f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);
}
}
}
cout << f[m];
return 0;
}
二维朴素算法 跟上述几种有些许区别
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1e4;
int v[N][N], w[N][N];
int n, m, s[N];//s数组接收每组个数
int f[N][N];
int main(){
cin >> n >> m;
for(int i = 1; i <= n; i ++){
cin >> s[i];//该组存在k个
for(int j = 1; j <= s[i]; j ++)
cin >> v[i][j] >> w[i][j];
}
for(int i = 1; i <= n; i ++){
for(int j = m; j >= 0; j --){//跟0/1背包问题类似 逆序遍历 防止旧数据被覆盖
f[i][j] = f[i - 1][j];
//这组循环是要判断不选该组价值大还是选该组某个物品价值更大
//首先先不选该组物品 在循环中依次比较选某商品总价值是否会更大
for(int k = 1; k <= s[i]; k ++){
if(v[i][k] <= j)
f[i][j] = max(f[i][j], f[i - 1][j - v[i][k]] + w[i][k]);
}
}
}
cout << f[n][m];
return 0;
}
二维朴素的错误示范
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1e4;
int v[N][N], w[N][N];
int n, m, s[N];//s数组接收每组个数
int f[N][N];
int main(){
cin >> n >> m;
for(int i = 1; i <= n; i ++){
cin >> s[i];//该组存在k个
for(int j = 1; j <= s[i]; j ++)
cin >> v[i][j] >> w[i][j];
}
for(int i = 1; i <= n; i ++){
for(int j = m; j >= 0; j --){
for(int k = 1; k <= s[i]; k ++){
if(v[i][k] > j)
f[i][j] = f[i - 1][j];
else//这里就存在一个问题 我在遍历该组数据时 要比较的数值多
//首先要比较选这个物品和选那个物品谁价值更高 还要看不选这组物品价值高
//因为不选这组的价值是固定的 不会随着遍历而改变 可以放到循环外边
//先赋值为不选该组的价值 循环内再比较选哪个物品价值更高
//这里的错误在于每次都和不选该组的价值比较 没有更新循环内出现的最大值
f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i][k]] + w[i][k]);
}
}
}
cout << f[n][m];
return 0;
}
线性DP
数字三角形
题意较简单 开始的思想跟背包问题时一样 先明确状态的集合和属性
f[i][j]的集合代表所有从顶点到num[i][j]的路径 属性为所有路径中的和最大值
之后是状态计算 到达num[i][j]只有两条路径 一个是从该点的左上i-1 j-1 一个是右上i-1 j
因此 公式推导出 f[i][j] = max(f[i - 1][j - 1], f[i - 1][j]) + num[i][j]
//自顶向下的方法
#include<iostream>
using namespace std;
const int N = 510;
const int INF = -1e9;
int f[N][N], num[N][N];
int n;
int main(){
cin >> n;
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= i; j ++)
cin >> num[i][j];
//对数字三角形旁边不存在的点进行负无穷初始化
//若默认为0 会导致在更新状态属性时影响数据准确度 在三角形中可以出现负值 会导致选择不存在的点
//数字三角形各行下标从1开始
// -∞ -∞
// -∞ 1 -∞
// -∞ 2 3 -∞
for(int i = 0; i <= n; i ++)
for(int j = 0; j <= i + 1; j ++)
f[i][j] = INF;
//一定要将最顶点的状态先手动更新
//否则会导致第一个状态就不对
f[1][1] = num[1][1];
for(int i = 2; i <= n; i ++)
for(int j = 1; j <= i; j ++)
f[i][j] = max(f[i - 1][j - 1], f[i - 1][j]) + num[i][j];
int res = INF;
for(int j = 1; j <= n; j ++)
if(res < f[n][j])
res = f[n][j];
cout << res;
}
//自底向上 减少最后判定最大值 直接输出底部到顶点的状态值即可
#include<iostream>
using namespace std;
const int N = 510;
const int INF = -1e9;
int f[N][N], num[N][N];
int n;
int main(){
cin >> n;
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= i; j ++)
cin >> num[i][j];
//对数字三角形旁边不存在的点进行负无穷初始化
//若默认为0 会导致在更新状态属性时影响数据准确度 在三角形中可以出现负值 会导致选择不存在的点
//数字三角形各行下标从1开始
// -∞ -∞
// -∞ 1 -∞
// -∞ 2 3 -∞
for(int i = 1; i <= n; i ++)
for(int j = 0; j <= i + 1; j ++)
f[i][j] = INF;
//将最底层的状态先手动更新
//否则会导致第一个状态就不对
for(int i = 1; i <= n; i ++)
f[n][i] = num[n][i];
for(int i = n - 1; i >= 1; i --)
for(int j = 1; j <= i; j ++)
f[i][j] = max(f[i + 1][j], f[i + 1][j + 1]) + num[i][j];
cout << f[1][1];
}
一个小点:数组下标从0开始还是从1开始
当代码中用到下标为i - 1时 我们尽量用i 有效避免数组下标越界 减少一些if判断
最长上升子序列
这里的严格单调递增指的是非连续 不存在相同数值的子序列
同样的流程 首先确定集合 关于此题用一维数组即可
f[i] 代表以num[i]结尾的子序列的集合 属性为该子序列的长度
状态计算 以num[i]结尾的子序列可以有多个
子序列前一个元素为空 只有他自己
子序列的前一个元素为num[1],num[2],......num[i - 1] 前提是num[j] < num[i]
由于num[i]数值代表以它为结尾的最大子序列长度 由此通过遍历这些元素可推出f[i]
有点像前缀和
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1e4;
int f[N], num[N];
int n;
int main(){
cin >> n;
int res = 0;
for(int i = 1; i <= n; i ++){
scanf("%d", &num[i]);
f[i] = 1;//默认结尾为num[i]的最长子序列长度为1
for(int j = i - 1; j >= 1; j --)
if(num[j] < num[i])
f[i] = max(f[i], f[j] + 1);
if(res < f[i])
res = f[i];
}
cout << res;
}
最长公共子序列
首先分析一下集合的这几种情况
f[i, j] = 在第一个序列前i个字母中出现且第二个序列前j个字母出现的子序列
上述这个子序列可以分为对于第i个元素和第j个元素是否选择的问题 即00 01 10 11
f[i-1, j-1] = 在第一个序列前i-1个字母中出现且第二个序列前j-1个字母出现的子序列 即不选择这两个元素 00
f[i-1, j-1] + 1 = 在第一个序列前i-1个字母中出现且第二个序列前j-1个字母出现的子序列且必须包含第i个字母和第j个字母 11
值得注意的是
f[i-1, j] = 在第一个序列前i-1个字母中出现且第二个序列前j个字母出现的子序列 它会包含01这种情况 但不像00和11那样完全等价于状态表示
用f[i-1,j]来代替01这种情况 因为是求f[i,j]的最大值 因此比较过程中出现重复或f[i-1,j]的最长子序列大于01的最长子序列长度都没问题
f[i, j-1] = 在第一个序列前i个字母中出现且第二个序列前j-1个字母出现的子序列 同理也只是包含10这种情况
并且我们可以发现f[i-1][j-1]是f[i-1][j]和f[i][j-1]的子集 因此在比较的时候只需比较后三种即可
题解
#include<iostream>
#include<cmath>
using namespace std;
const int N = 1010;
int n, m;
char a[N], b[N];
int f[N][N];
int main(){
cin >> n >> m;
scanf("%s%s", a+1, b+1);
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= m; j ++){
f[i][j] = max(f[i - 1][j], f[i][j - 1]);
if(a[i] == b[j]) f[i][j] = max(f[i][j], f[i - 1][j - 1] + 1);
}
cout << f[n][m];
}
最短编辑距离
区间DP
石子合并
状态推导:f[i][j] = 合并第i堆石子到第j堆石子的集合 属性取该集合所花费的最小代价
转态转移:要合并第i堆到第j堆 最后一步一定是将剩余的两堆石子合并,因此我们可以枚举最后两堆石子的分界线位置
f[i][j] = min(f[i][j], f[i][k] + f[k+1][j] + s[j] - s[i-1])
第一层循环枚举区间长度 第二层枚举区间起点下标 第三层枚举分界线位置
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1000;
int n;
int s[N];
int f[N][N];
int main(){
cin >> n;
for(int i = 1; i <= n; i ++){
scanf("%d ", &s[i]);
s[i] += s[i - 1];
}
for(int len = 2; len <= n; len ++)
for(int i = 1; i <= n - len + 1; i ++){//枚举起点下标 从1到i + len - 1 <= n -1是去掉i本身
int l = i, r = i + len - 1;
f[l][r] = 1e8;//取最小值 全局变量初始为0 避免影响数据准确性
for(int k = l; k <= r; k ++){
f[l][r] = min(f[l][r], f[l][k] + f[k + 1][r] + s[r] - s[l - 1]);
}
}
cout << f[1][n];
}
计数类DP
整数划分
这道题可以理解为完全背包问题的变形
正整数n 可以分析为背包总容量为n 包含n件物品 价值为[1, n] 每件均可无限取 求恰好总价值为n的方案数是多少 f[i][j]表示方案数
朴素推导状态方程为f[i][j] = f[i-1][j] + f[i-1][j-i] + ... + f[i-1][j-si]
要注意的是 最一开始f[1][0]要访问f[0][0] 只选第一件物品 要求总价值为0 方案数只能是一
代码为f[1][0] = f[0][0] f[0][0]要初始化为1 方便后续状态转移 否则所有的计算结果都会是0
#include<iostream>
using namespace std;
const int N = 1e4;
const int mod = 1e9 + 7;
int n;
int f[N][N];//含义是从前 i个数中选出总和为 j的方案数
int main(){
cin >> n;
// for(int i = 0; i <= n; i ++)//初始化 前 i个数总和为 0的方案数均为 1 即都不选 同时状态转移中也要用到f[i][0] 不能默认为 0
// f[i][0] = 1;
f[0][0] = 1;
for(int i = 1; i <= n; i ++){
for(int j = 0; j <= n; j ++){
for(int k = 0; k * i <= j; k ++)
f[i][j] = (f[i][j] + f[i - 1][j - k * i]) % mod;
f[i][j] %= mod;
}
}
cout << f[n][n];
return 0;
}
或者可以将所有的f[i][0]都初始化为1 这样的话j要从1开始遍历 原因是f[i][0]已经是最终结果了 再枚举的话会执行f[i][0] += f[i-1][0] 重复累加 导致结果错误
分析:f[1][0] 本身初始化为1 循环体又执行一遍计算 f[1][0] += f[0][0] 得出f[1][0] = 2 以上均是如此
#include<iostream>
using namespace std;
const int N = 1e4;
const int mod = 1e9 + 7;
int n;
int f[N][N];//含义是从前 i个数中选出总和为 j的方案数
int main(){
cin >> n;
for(int i = 0; i <= n; i ++)//初始化 前 i个数总和为 0的方案数均为 1 即都不选 同时状态转移中也要用到f[i][0] 不能默认为 0
f[i][0] = 1;
//f[0][0] = 1;
for(int i = 1; i <= n; i ++){
for(int j = 1; j <= n; j ++){
for(int k = 0; k * i <= j; k ++)
f[i][j] = (f[i][j] + f[i - 1][j - k * i]) % mod;
f[i][j] %= mod;
}
}
cout << f[n][n];
return 0;
}
基于完全背包问题的优化
f[i][j] = f[i-1][j] + f[i-1][j-i] + ........ + f[i-1][j-si]
f[i][j-i] = f[i-1][j-i] + f[i-1][j-2i] + ....... + f[i-1][j-si]
推导得 f[i][j] = f[i-1][j] + f[i][j-i] 此行代码可代替累加循环
for(int k = 0; k * i <= j; k ++)
f[i][j] = (f[i][j] + f[i - 1][j - k * i]) % mod;
优化ac代码
#include<iostream>
using namespace std;
const int N = 1e4;
const int mod = 1e9 + 7;
int n;
int f[N][N];//含义是从前 i个数中选出总和为 j的方案数
int main(){
cin >> n;
for(int i = 0; i <= n; i ++)//初始化 前 i个数总和为 0的方案数均为 1 即都不选 同时状态转移中也要用到f[i][0] 不能默认为 0
f[i][0] = 1;
// f[0][0] = 1;
for(int i = 1; i <= n; i ++){
for(int j = 1; j <= n; j ++){
f[i][j] = f[i-1][j] % mod;
if(j >= i)
f[i][j] = (f[i-1][j] + f[i][j-i]) % mod;
}
}
cout << f[n][n];
return 0;
}
进一步优化 二维降一维
#include<iostream>
using namespace std;
const int N = 1e4;
const int mod = 1e9 + 7;
int n;
int f[N];//含义是从前 i个数中选出总和为 j的方案数
int main(){
cin >> n;
f[0] = 1;
for(int i = 1; i <= n; i ++){
//从i开始是由于f[i][j] = f[i-1][j] + f[i][j-i]
//如果j<i 在一维中相当于无需更新
for(int j = i; j <= n; j ++){
f[j] = (f[j] + f[j-i]) % mod;
}
}
cout << f[n];
return 0;
}
数位统计DP
计数问题
思考方式:不再是f[][]表示集合求状态转移方程 而是分类讨论 推导出相关结果或表达式
用函数cnt(n, i)表示从1~n中i一共出现了多少次
cnt(b, i) - cnt(a-1, i)就等于i在a到b之间出现的次数
具体的分类讨论 针对cnt函数的实现
解释一下前导零 如果一个数为abcdefg 第四位要求是0 如果此时前三位为0 则0000efg = efg 第四位相当于无效 因此这里需要特判 如果是0 0前端的数必须是从000...1开始 保证0的有效位置
同时对于x=0的情况 在(1)中要特判左边不等于0 否则就是一个不存在的数 以0开头
在(2.1/2.2)中同样要特判这种情况
#include<iostream>
#include<cmath>
using namespace std;
int a, b;
int getsize(int num){//获取数字长度
int length = 0;
while(num){
length ++;
num /= 10;
}
return length;
}
int power10(int n){
int num = 1;
while(n --){
num *= 10;
}
return num;
}
int cnt(int n, int x){//计算1~n中i出现的次数
int res = 0, length = getsize(n);
for(int i = 1; i <= length; i ++){//i可能出现在当前数字长度的任何一位 依次统计在各位上的出现次数
//l和r代表第i位左右两边的数 d代表第i位的数字
int weight = pow(10, i), l = n / weight, r = n % (weight / 10), d = (n / (weight / 10)) % 10;
if(x != 0){//如果要查找的x为 0则左边的范围就要缩小1
res += (l * power10(i - 1));
}
else if(x == 0 && l != 0){//x等于0时左边高位不能全为0 否则为非法情况
res += ((l - 1) * power10(i - 1));
}
if(d == x){//当d计算出得0时一定不会出现在首位 即l一定不为0 无需加条件 x | l
res += (r + 1);
}
//当d>x时我们要模拟的是这一位是x时出现的次数 不考虑0时 可以推理出次数为 power10(i - 1)
//如果要考虑0 则这一位不能是首位 因为d>x 所以不加条件会累加到不符合情况的次数
//比如123 找0出现的次数 1 > 0 如此计算得100 实际为0
else if(d > x && (l | x)){//x | l代表当x = 0时 l即x左边的数不能为 0 对应0xxxxx 不存在这种数
res += power10(i - 1);
}
}
return res;
}
int main(){
while(cin >> a >> b, a | b){
if(a > b) swap(a, b);
for(int i = 0; i <= 9; i ++)
printf("%d ", cnt(b, i) - cnt(a - 1, i));
printf("\n");
}
}
状态压缩DP
蒙德里安的梦想
思路分析:首先一个棋盘中可以放置横着的1*2长方形和竖着的1*2长方形 当棋盘中的横长方形固定后 竖着的长方形就只能有一种摆法 即方案数只需考虑横长方形的不同放法或竖长方形的不同放法 这里以横长方形为例
说明一下状态压缩 将状态用二进制来表示 比如在该题中 第i列中有多少由于第i-1列横放长方形导致占用了第i列位置的 用1表示 第i列空的位置可以用0表示 这样就形成了一串二进制数
状态数的范围取决于行数 即j 属于 0 ~ (2^N-1)
状态表示:f[i][j] 表示已经将前 i -1 列摆好,且从第i − 1列,伸出到第 i 列的状态是 j 的所有方案
最终的结果用f[M][0]表示 代表前M-1列均被填
考虑状态转移 一个N*M的棋盘 共M列 第i列的放法肯定会受到第i-1列的影响 因此要考虑两个问题
1.假设当前状态为j 即f[i][j] 代表伸到第i列的状态 要枚举的第i-1列状态为k 即f[i-1][k] 代表伸到第i-1列的状态 则j&k必须为0 避免出现 1 1这种不合法的情况 0 1 | 1 0 | 0 0 均为合法情况 可以留给竖长方形填补
2.第i-1列不能出现连续个奇数的空 因为竖长方形长度为2 出现奇数的话无法填充 即j和k的二进制表示并集中不能包含连续个奇数的0 这一步可以预处理出来 即st[j|k]的值表示是否合法
k表示伸到第i-1列的长方形 j表示伸到第i列要占用第i-1列的长方形 这两个想或后得到的0就是第i-1列的空位置
第i列中的位置可以被j
#include<iostream>
#include<cstring>
using namespace std;
const int N = 12, M = 1 << N;
long long f[N][M];
bool st[M];
int main(){
int n, m;
while(cin >> n >> m, n || m){
memset(f, 0, sizeof(f));
for(int i = 0; i <= 1 << n; i ++){//枚举0~2^n-1
st[i] = true;
int cnt = 0;
for(int j = 0; j < n; j ++)//枚举每一位是0还是1 长度为n
if((i >> j) & 1){//遇到1
if(cnt & 1){//奇数最后一位一定是1
st[i] = false;
break;
}
}
else
cnt ++;
if(cnt & 1)
st[i] = false;
}
f[0][0] = 1;
for(int i = 1; i <= m; i ++)//这里算到m是因为最终结果要用f[m][0]表示
for(int j = 0; j < 1 << n; j ++)
for(int k = 0; k < 1 << n; k ++)
if((j & k) == 0 && st[j | k])
//这两条都满足后说明当前这个f[i-1][k]是合法
//可以转移到f[i][j]中
//j代表到第i列的放法 k代表到第i-1列的放法
//两种放法不冲突代表可以从第i-1列的这种情况转换到第i列的这种放法
//同时继承前边的方案数
f[i][j] += f[i-1][k];
cout << f[m][0] << endl;
}
}