DP
一点心得:
将一个问题拆成几个子问题,分别求解这些子问题,即可推断出大问题的解
当f[i]需要用到f[i-1]等更小的dp方程时,不要多虑,此时的f[i-1]一定已经根据之前的求出来了,大胆去用!!!此处类似于递归 dfs…
dp也是我学习算法路上的一只强劲的拦路虎,我也只是初窥门径,还需不断学习并积累。以下为备战icpc省赛时所学习并整理的典题,包含题目思路及多种ac代码,后续也会不断学习和补充dp相关的笔记思路和代码,希望能帮助到刚刚入门dp的同学。
目录:
- 背包问题
- 线性DP
- 区间DP
- 树形DP
背包问题
物品属性(vi,wi,个数限制) 和 背包容量(V)
1. 01背包
每件物品选0or1次
理解:
f(i,j)表示所有只从前i个物品中选 且总体积<=j的选法的集合
存的数是里面每个集合里面每个选法的w的最大值
f[i][j]有两种情况:
第一种不包含第i个物品 此时 f[i][j]=f[i-1][j]; 一定存在
第二种包含第i个物品 此时f[i][j]=f[i-1][j-v[i]]+w[i];当w[i]<=j时存在
二维做法:
#include<bits/stdc++.h>
using namespace std;
const int N = 1010;
int v[N],w[N];
int f[N][N];//f[i][j] 表示 所有满足条件在前i件物品中选择且总体积不超过j的选法 中的Wmax
int main()
{
int n,V;
cin>>n>>V;
for(int i = 1;i <= n;i ++) cin>>v[i]>>w[i];
for(int i=1;i <= n;i ++)
{
for(int j=1;j <= V;j ++)
{
f[i][j]=f[i-1][j];
if(j>=v[i])
f[i][j]=max(f[i][j],f[i-1][j-v[i]]+w[i]);
}
}
cout<<f[n][V];//所有满足条件在前n件物品中选择且总体积不超过V的选法 中的Wmax
return 0;
}
一维做法:
#include<bits/stdc++.h>
using namespace std;
const int N = 1010;
int v[N],w[N];
int f[N];
int main()
{
int n,V;
cin>>n>>V;
for(int i = 1;i <= n;i ++) cin>>v[i]>>w[i];
for(int i=1;i <= n;i ++)
{
for(int j=V;j >= v[i];j --)
{
f[j]=max(f[j],f[j-v[i]]+w[i]);
}
}
cout<<f[V];
return 0;
}
2. 完全背包
每件物品可以选无限个
三维:
#include<bits/stdc++.h>
using namespace std;
const int N = 1010;
int v[N],w[N];
int f[N][N];
int main()
{
int n,V;
cin>>n>>V;
for(int i=1;i<=n;i++) cin>>v[i]>>w[i];
for(int i=1;i<=n;i++)
for(int j=1;j<=V;j++)
for(int k=0;k*v[i]<=j;k++)
{
f[i][j]=max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);
}
cout<<f[n][V];
return 0;
}
二维:
#include<bits/stdc++.h>
using namespace std;
const int N = 1010;
int f[N][N];
int v[N],w[N];
int main()
{
int n,V;
cin>>n>>V;
for(int i=1;i<=n;i++) cin>>v[i]>>w[i];
for(int i=1;i<=n;i++)
for(int j=1;j<=V;j++)
{
f[i][j]=f[i-1][j];
if(v[i]<=j)
f[i][j]=max(f[i][j],f[i][j-v[i]]+w[i]);
}
cout<<f[n][V];
return 0;
}
截止到此处,不难发现与01背包二维相似,故类比可优化成一维
一维:
#include<bits/stdc++.h>
using namespace std;
const int N = 1010;
int f[N];
int v[N],w[N];
int main()
{
int n,V;
cin>>n>>V;
for(int i=1;i<=n;i++) cin>>v[i]>>w[i];
for(int i=1;i<=n;i++)
for(int j=v[i];j<=V;j++)
{
f[j]=max(f[j],f[j-v[i]]+w[i]);
}
cout<<f[V];
return 0;
}
```
3. 多重背包(朴素版&&优化版)
每件物品最多有Si个
朴素版(形似于完全背包三维):
#include<bits/stdc++.h>
using namespace std;
const int N =110;
int f[N][N];
int v[N],s[N],w[N];
int main()
{
int n,V;
cin>>n>>V;
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<=V;j++)
{
for(int k=0;k<=s[i]&&k*v[i]<=j;k++)
{
f[i][j]=max(f[i][j],f[i-1][j-k*v[i]]+w[i]*k);
}
}
}
cout<<f[n][V];
return 0;
}
格式虽形似完全背包优化方式
但在多重背包问题中受到背包大小及物品数量si所限制,故f[i] [j-v] 比f[i] [j] 后s项可能要多一项 故无法代换为f[i] [j-v]+w。
而完全背包只受背包大小因素限制,故f[i] [j-v]的末项一定等同于f[i] [j]后k项,故可通过状态转移方程f[i] [j]=max(f[i-1] [j],f[i] [j-v]+w)所代换
二进制优化:
//一种物品(s份)分为log2s份 每份看作一个新物品 可转化成01背包问题
//原本需要枚举s次 现在通过二进制分割新物品只需要枚举log2s次即可
#include<bits/stdc++.h>
using namespace std;
const int N=12*1010;//按照二进制分组后的新种类数
const int M=2010;//种类数
int v[N],w[N],s[N];
int f[M];//状态方程
int n,m;
int main()
{
cin>>n>>m;
int cnt=0;
for(int i=1;i<=n;i++)//n种
{
int a,b,x;
cin>>a>>b>>x;
int k=1;
while(k<=x)
{
cnt++;
v[cnt]=k*a;
w[cnt]=k*b;
x-=k;
k*=2;
}
if(x>0)
{
cnt++;
v[cnt]=x*a;
w[cnt]=x*b;
}
}
n=cnt;//更新物品数n
//此时优化成01背包问题
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];
return 0;
}
4. 分组背包
N组物品,每组有若干个物品,每一组最多选一个物品(水果:香蕉 苹果 葡萄...)
二维:
#include<bits/stdc++.h>
using namespace std;
const int N = 110;
int f[N][N];
int v[N][N],s[N],w[N][N];
int main()
{
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++)
{
cin>>s[i];
for(int j=1;j<=s[i];j++)
cin>>v[i][j]>>w[i][j];//第i组的第j个物品的数据
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
//f[i][j]=f[i-1][j];
for(int k=0;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];
}
优化:
#include<bits/stdc++.h>
using namespace std;
const int N = 110;
int f[N];
int v[N][N],s[N],w[N][N];
int main()
{
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++)
{
cin>>s[i];
for(int j=1;j<=s[i];j++)
cin>>v[i][j]>>w[i][j];//第i组的第j个物品的数据
}
for(int i=1;i<=n;i++)
{
for(int j=m;j>=0;j--)
{
for(int k=0;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];
}
线性DP
1. 数字三角形
#include<bits/stdc++.h>
using namespace std;
const int N = 510;
int a[N][N];//存储数据
int f[N][N];//f[i][j]表示所有最后到达(i,j)的路径 的集合 的数字和 的max
const int INF = 0x3f3f3f3f;
int main()
{
int n;
cin>>n;
for(int i=1;i<=n;i++)
for(int j=1;j<=i;j++)
cin>>a[i][j];
for(int i=0;i<=n;i++)
for(int j=0;j<=i+1;j++)
f[i][j]=-INF;//初始化 此处仔细思考一下边界问题
f[1][1]=a[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]+a[i][j],f[i-1][j]+a[i][j]);//由左上角或者右上角转化而来 取max作为f[i][j]的值
int res=-INF;
for(int i=1;i<=n;i++) res=max(res,f[n][i]);//依次遍历f[n][i] 取最大值则为最大的数字和
cout<<res;
}
2. 最长上升子序列
#include<bits/stdc++.h>
using namespace std;
const int N = 1010;
int a[N];
int f[N];//以第i个数结尾的所有上升子序列的集合 的长度 的max
int mem[N];//存储以i结尾的上升子序列倒数第二位的数字的位置 即由谁转化而来
int main()
{
int n;
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<=n;i++)
{
f[i]=1;//默认上升子序列只有第i个数本身
mem[i]=0;//此时没有上一位数字
for(int j=1;j<i;j++)
{
if(a[j]<a[i])
if(f[i]<f[j]+1)
{
f[i]=f[j]+1;
mem[i]=j;//f[i]由j转移过来的 记录一下
}
}
}
int idx=1;
for(int i=1;i<=n;i++)
if(f[idx]<f[i]) idx=i;//比较f[]
cout<<f[idx]<<endl;
//倒序输出最长上升子序列
for(int i=0,len=f[idx];i<len;i++)
{
cout<<a[idx]<<' ';
idx=mem[idx];
}
return 0;
}
优化版:
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+10;
int n;
int a[N];
int q[N];//q[i]存储长度为i的上升子序列的最小结尾数字
int main()
{
scanf("%d",&n);
for(int i=0;i<n;i++) scanf("%d",&a[i]);
int len=0;//用于记录最长上升子序列的长度
q[0]=-2e9;
for(int i=0;i<n;i++)
{
int l=0,r=len;
while(l<r)
{
int mid=l+r+1>>1;
if(q[mid]<a[i]) l=mid;
else r=mid-1;
}
len=max(len,r+1);//r是找到的小于a[i]的数里面的最大值 将a[i]排在第r+1位 故以a[i]结尾的最长上升子序列长度为r+1 取max
//因为q[r]为小于a[i]的数里面的最大值,故q[r+1]>=a[i] 用a[i]替换q[r+1];
q[r+1]=a[i];
}
printf("%d",len);
return 0;
}
3. 最长公共子序列
/*划分四个区间 00 10 01 11
可用dp[i-1][j-1] dp[i][j-1] dp[i-1][j] dp[i-1][j-1]+1
其中不含a[i],b[j]即dp[i-1][j-1]可以被dp[i][j-1] dp[i-1][j]代替
而10 01情况无法直接表示 但是dp[i][j-1] dp[i-1][j]包含10 01情况 且为dp[i][j]的子集 所以可以代替
*/
#include<bits/stdc++.h>
using namespace std;
const int N = 1010;
int dp[N][N];
int main()
{
string a,b;
int n,m;
cin>>n>>m>>a>>b;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
if(a[i-1]==b[j-1]) dp[i][j]=max(dp[i][j],dp[i-1][j-1]+1);
}
cout<<dp[n][m];
return 0;
}
4. 最短编辑距离
#include<bits/stdc++.h>
using namespace std;
const int N = 1010;
char a[N],b[N];
int f[N][N];//所有将a[1-i]变为b[1-j]的操作方式 的 操作次数 的 min
//可分为 删:f[i-1][j]+1 增:f[i][j-1]+1 改:f[i-1][j-1]+1|0
int main()
{
int n,m;
cin>>n>>a+1>>m>>b+1;
int p=0;
for(int i=0;i<=m;i++) f[0][i]=i;
for(int i=0;i<=n;i++) f[i][0]=i;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
f[i][j] = min(f[i-1][j]+1,f[i][j-1]+1);
if(a[i]==b[j]) p=0;
else p=1;
f[i][j] = min(f[i][j],f[i-1][j-1]+p);
}
}
cout<<f[n][m];
return 0;
}
5. 编辑距离
#include<bits/stdc++.h>
using namespace std;
const int N = 1010;
char a[N][15],b[15];
int f[15][15];//所有将a[1-i]变为b[1-j]的操作方式 的 操作次数 的 min
//可分为 删:f[i-1][j]+1 增:f[i][j-1]+1 改:f[i-1][j-1]+1|0
int main()
{
int n,m;
cin>>n>>m;
for(int i=0;i<n;i++) cin>>a[i]+1;
//for(int i=0;i<=15;i++) f[0][i]=f[i][0]=i;
while(m--)
{
int p;
cin>>b+1>>p;
int cnt=0;
for(int i=0;i<n;i++)
{
int lena=strlen(a[i]+1),lenb=strlen(b+1);
for(int i=0;i<=lenb;i++) f[0][i]=i;
for(int i=0;i<=lena;i++) f[i][0]=i;
//cout<<"lena = "<<lena<<' '<<"lenb = "<<lenb<<endl;
for(int j=1;j<=lena;j++)
for(int k=1;k<=lenb;k++)
{
f[j][k] = min(f[j-1][k]+1,f[j][k-1]+1);
if(a[i][j]==b[k]) f[j][k] = min(f[j][k],f[j-1][k-1]);
else f[j][k] = min(f[j][k],f[j-1][k-1]+1);
//printf("f[%d][%d] = %d\n",j,k,f[j][k]);
}
if(f[lena][lenb]<=p)cnt++;
//cout<<f[lena][lenb]<<endl;
}
cout<<cnt<<endl;
}
return 0;
}
区间DP
1. 石子合并
#include<bits/stdc++.h>
using namespace std;
const int N = 310;
int s[N];
int f[N][N];//第i堆到第j堆石子合并的方式 min代价
int main()
{
int n;
cin>>n;
for(int i=1;i<=n;i++) cin>>s[i];
for(int i=1;i<=n;i++) s[i]+=s[i-1];
for(int len=2;len<=n;len++)//len==1时无须合并
for(int i=1;i+len-1<=n;i++)
{
int l=i,r=i+len-1;
f[l][r]=1e9;
for(int k=i;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];
return 0;
}
树形DP
1.没有上司的舞会
#include<bits/stdc++.h>
using namespace std;
int f[5][6000];
int n,a,b,root;
int ne[12000],po[12000],ru[12000];
void dp(int x)
{
for(int i = po[x];i;i = ne[i])
{
dp(i);
f[1][x] = max(max(f[1][x],f[0][i]+f[1][x]),f[0][i]);//这一个点选的时候转移:可以不选子节点,可以是子节点不选时最大值+自己的值,可以是只是子节点不选时的最大值
f[0][x] = max(max(f[0][x],f[1][i]+f[0][x]),max(f[1][i],f[0][i]));//这一个点不选的时候转移:可以是自己,可以是子节点也不选,可以是子节点选时+自己,可以是仅仅子节点选时最大
}
}
int main(){
scanf("%d",&n);
for(int i = 1;i <= n;i ++)
scanf("%d",&f[1][i]);
for(int i = 1;i <= n;i ++)
{
scanf("%d%d",&b,&a);
ru[b] ++;
ne[b] = po[a]; //原理与邻接表类似 只是里面存储的都是点 可手动模拟一下
po[a] = b;
}
for(int i = 1;i <= n;i ++)
if(ru[i] == 0)
{
root = i;//找树的根
break;
}
dp(root);
printf("%d",max(f[1][root],f[0][root]));
return 0;
}