蓝桥杯备考---动态规划

温馨提示: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;
        
    }
}

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值