核心:状态表示+状态转移
一: 线性dp
线性DP:强调是在线性状态的转移变化,一般其递推方程都有一个线性的状态转移,可能是一维也可能是二维等。
核心: 状态转移 f[ i ] [ j ]构建其与其他f[ ][ ]之间关系
图片解析:
关键代码:
1.线性dp: 通常从左至右,从上至下按顺序遍历即可
for(int i;i<n;i++){
for(int j;j<n;j++){
**关键**: 找到f[][]之间的递推关系
f[i][j]=solve(f[][],f[][],f[][]);
}
}
二: 区间dp
区间DP: 在区间上进行动态规划,求解一段区间上的最优解.主要是通过合并小区间的最优解进而得出整个大区间上最优解的dp算法.
核心: 外层枚举区间长度+内层枚举区间起点
图片解析:
关键代码:
1.通常外层len是区间长度,一般从1开始(先求小区间)
2.内层一般从i开始代表区间的起始位置,j=i+len-1表示区间结束位置.
for(int len;len<=n;len++){
for(int i;i+len-1<=n;i++){
int j=i+len-1;
**关键**: 利用小区间之间关系更新大区间
f[i][j]=solve(f[][],f[][],f[][]);
}
}
三. 背包dp
背包DP: 通过按照一定规则选择一些物品,去获取最大的价值或者方案数的求解算法。
核心: 通常外层循环物品数,中层循环空间大小,内层看情况处理一些数量要求.
图片解析:
关键代码:
*** 背包问题:
核心就是外层循环所有的物品,然后在中层循环空间大小,内层根据具体问题更改相应数量限制要求即可.
*** 优化部分:
1. 正常我们选择下一个物品得到最优解的时候往往只需要上一层的结果,所以我们并不需要dp[N][N],而
可以优化成dp[N]保留上一层的最优解即可.
2. 正常我们递归空间时,都是要从后面往前面,这是因为我们的dp[N]保留的是上一层的最优解,我们要使用上
层最优解来更新这一层的最优解,必须要从后向前更新,例如dp[j]=dp[j-v[i]]+w[i],这样子才能保证
dp[j-w[i]]只选择了上层的物品还未选择这层的物品。这样子才能保证使用上层结果来更新本层。
特例: 完全背包要从前面向后面(因为其数量不受限制,你可以使用本层更小的dp[j-v[i]]来更新dp[j],
就可以当作即使dp[j-v[i]]选了本层物品,dp[j]依旧可以再多选几个加入都是可以的),你也可以使用数学
公式进行证明。
****代码部分:
V:表示总背包大小(空间大小)
v[i]:表示第i个物品的空间大小
w[i]:表示第i个物品的价值
1.01背包:选择拿或者不拿
for(int i=1;i<=n;i++){
for(int j=V;j>=v[i];j--){ //空间从后往前
dp[j]=max(dp[j],dp[j-v[i]]+w[i]);
}
}
2.完全背包:可以无数量限制的拿
for(int i=1;i<=n;i++){
for(int j=v[i];j<=V;j++){ //空间从前面往后面
dp[j]=max(dp[j],dp[j-v[i]]+w[i]);
}
}
3.多重背包: 有数量限制
for(int i=0;i<n;i++){
for(int j=V;j>=v[i];j--){ //空间从后往前面
for(int k=1;k<=s[i]&&k*v[i]<=j;k++){// 判断数量限制为s[]
dp[j]=max(dp[j],dp[j-k*v[i]]+k*w[i]);
}
}
}
4.多重背包优化:拆分成二进制01背包问题
for(int i=0;i<n;i++){
int a,b,s;
cin>>a>>b>>s;
//二进制数拆分
for(int k=1;k<=s;k<<=1){
s-=k;
v[++cnt]=k*a;
w[cnt]=k*b;
}
if(s){
v[++cnt]=s*a;
w[cnt]=s*b;
}
}
5.分组背包问题:每组背包里面最多选一个(组内单独判断一次)
for(int i=0;i<n;i++){
for(int j=m;j>=0;j--){
for(int k=0;k<s[i];k++){
if(j>=v[i][k])// 第i组内的第k个物品
dp[j]=max(dp[j],dp[j-v[i][k]]+w[i][k]);
}
}
}
四. 数位dp
数位DP: 实际上就是在数位上进行dp,例如 ABCDEFG这个数字,从最高位A开始分解 0—A-1 和 A,然后再逐层向低位进行动态规划。
核心: 在遍历的过程当中根据题目的要求进行判断所满足的答案数量或者方案数.
图片解析:
关键代码:
1. 求解具体符合要求的数字个数/方案数
// pos当前位置 last上一位数 limit是否有最高位限制
int dfs(int pos, int last, bool limit){
if(pos < 0) return 1;
// 已经有保存的最优解
if(!limit && dp[pos][last] != -1) return dp[pos][last];
// 最高位有限制
int x = limit ? bit[pos] : 9;
int res = 0;
// **核心** 遍历根据是否满足具体要求进行求解答案
for(int i = 0; i <= x; i++){
solve(...) ;
res += dfs(pos - 1, ..., limit && i == x);
}
if(!limit) f[pos][last] = res; //记忆化保存
return res;
}
2.求解某位数字出现次数(通常计数问题)
//数字分解保存
int get(int n){
int len = 0;
while(n){
bit[len++] = n % 10;
n /= 10;
}
}
//求解该位上数字出现的次数——前面分位000-abc-1和abc讨论,再对数字合法性保证
int count(int x,int num){ //数字num出现的次数
get(x);
int res=0;
for(int i=0;i<len;i++){
//l :左边数字 r:右边数字 m:当前数字 p:位置具体10的几次方大小
int l,m,r,p;
m=bit[i],p=pow(10,i),l=x/p/10,r=x%p;
if(num) res+=l*p;
if(!num&&l) res+=p*(l-1);
if(m>num&&(num||l)) res+=p;
if(m==num&&(num||l)) res+=r+1;
}
return res;
}
五. 状态压缩dp
状态压缩DP: 往往需要借用一个二进制数来表示某种状态,然后借助之前的状态逐步递推得到最终的一个结果状态。
例如:往往借助 0,1,10,11,100,101,110,111,…这些前面的小的状态的结果去得到最终的结果11111…这样子.
核心: 找到二进制状态中间的一个状态转移方式去构建方程,然后逐步转移得到答案即可。
通常会有一个较小的数字N=10 / 20,我们往往用 M=1<<N,然后使用f[M] [N] 或者 f[N][M]这个样子去进行动态规划
图片解析:
关键代码:
1.N往往表示有多少种不同01状态可以选择,M来表示选择完总状态
const int N = 20, M = 1 << N;
int f[N][M];
for(int i=0;i<1<<n;i++){
for(int j=0;j<n;j++){
**核心**:找到不同二进制状态的一个转移过程
f[i][j]=solve(.....)
}
}
六. 树形dp
树形DP: 在树结构上进行搜寻遍历得到结果
核心:
- 树的最长路径:dfs_d() ,再比较d1[i]+d2[i]最值即可
- 树的中心:一个结点,对于和其他点的距离的最大值最小
解:dfs_d()+dfs_u()即可 - 树的重心:重心是指树中的一个结点,如果将这个点删除后,剩余各个连通块中点数的最大值最小,那么这个节点被称为树的重心。
图片解析:
关键代码:
1.树的连接代码
void add(int a,int b){
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
2.树的传统dfs_d遍历
void dfs_d(int u){
for(int i=h[u];~i;i=ne[i]){
int j=e[i];
dfs(j);
solve();
}
}
3.树的dfs_up遍历(向上最长路径)
void dfs_u(){
d1[u],d2[u],f[u],up[u]; //up向上走到最大值,f[]记录向下搜索时最长路径的起点.
dfs_d();
for(int i=h[u];~i;i=ne[i]){
int j=e[i];
if(j==fa) continue;
if(j==f[u]) up[j]=max(up[u],d2[u])+e[i].w;
else up[j]=max(up[u],d1[u])+e[i].w;
}
}