复健计划(3)动态规划(线性DP的模型和模板)
写在前面:关于DP的内容,复习的时候正好看到了y总的DP分析法,觉得很好理解,就都尝试拿来理解DP题目,同时我对一些典型的DP模型也有总结。不过,因为做的DP题目实在太少,总结内容不免出现错误,在学习做题中也会及时改正所写内容.
同时,我也建议每一个人都可以去 ACwing学习到y总的DP分析法(.
自己的总结:DP的过程相当于在走拓扑图,每个节点就是一个状态,对于一个结点会有几条入边和出边;一般设置的f/dp[]…就是对节点状态的一个表示,考虑入边,就是划分形成该状态集合的方式;考虑出边,就是考虑该状态能递推到其他状态的规则;两种思想都有用,根据题目采用.
不过往往DP题目难是难在状态的表示,这一点还需要我们多做题来积累经验.
复健题目:acwing2~12,acwing895,896,897,902共15道模板题.
一、线性DP(包括背包)
1、01背包,完全背包,多重背包
这三个背包模型是所有背包问题的基础,同时他们时间和空间上的优化方法也是值得借鉴的.
a) 01背包(f[i][j] ——> f[j], 其中f[i][j]表示选前 i 件物品且体积不超过 j 的最大价值)
优化原理:因为每一次 f 在计算时只与上一层 i-1 有关,因此倒序体积更新,刚好就可以用 i-1 层的小体积状态更新到 i 层的大体积状态
代码:
#include <iostream>
#include <cstdio>
#include <algorithm>
const int N=1010;
int w[N],v[N],n,V;
int f[N*N];
using namespace std;
int main(){
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]);
}
int res=0;
for(int i=1;i<=V;i++) res=max(res,f[i]);
cout<<res<<endl;
return 0;
}
b) 完全背包(f[i][j] O(nm^2) ——> f[i][j] O(nm) ——> f[j], 其中f[i][j]表示选前 i 件物品且体积恰好为 j 的最大价值)
优化原理:
1、因为
f
[
i
]
[
j
]
=
m
a
x
(
f
[
i
]
[
j
−
v
]
+
w
,
f
[
i
]
[
j
−
2
v
]
+
2
w
,
.
.
.
,
f
[
i
]
[
j
−
s
v
]
+
s
w
)
=
m
a
x
(
f
[
i
]
[
j
−
v
]
,
f
[
i
]
[
j
−
2
v
]
+
w
,
.
.
.
,
f
[
i
]
[
j
−
s
v
]
+
(
s
−
1
)
w
)
+
w
=
f
[
i
]
[
j
−
v
]
+
w
f[i][j]=max(f[i][j-v]+w, f[i][j-2v]+2w, ..., f[i][j-sv]+sw)=max(f[i][j-v], f[i][j-2v]+w,..., f[i][j-sv]+(s-1)w)+w=f[i][j-v]+w
f[i][j]=max(f[i][j−v]+w,f[i][j−2v]+2w,...,f[i][j−sv]+sw)=max(f[i][j−v],f[i][j−2v]+w,...,f[i][j−sv]+(s−1)w)+w=f[i][j−v]+w
2、原理类似于01背包,不用倒序体积的原因是因为正好可以无限用物品,所以需要用 i 层的小体积状态更新同样是第 i 层的大体积状态.
代码:
#include <iostream>
#include <cstdio>
using namespace std;
const int N=1010;
int w[N],v[N],n,V,f[N*N];
int main(){
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]<<endl;
return 0;
}
c) 多重背包(朴素做法——>二进制拆分物品再用01背包——>单调队列维护做法)
优化原理:
1、二进制拆分物品个数后,来一遍01背包;
2、类似于完全背包的优化原理1,只不过这一次s不是固定的,所以不能用算过的 f 直接表示,而是一个类似“滑动窗口”求最值的问题,细节上要注意加上“价值*物品个数”这一偏移量.
二进制拆分物品代码:
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
const int N=2010;
int n,V;
int W[N],VO[N];
int f[N];
int main(){
cin>>n>>V;
for(int i=1;i<=n;i++){
int v,w,s;
int cnt=0,k=1;
scanf("%d%d%d",&v,&w,&s);
while(k<=s){
cnt++;
W[cnt]=k*w;
VO[cnt]=k*v;
s-=k;
k*=2;
}
if(s>0){
W[++cnt]=s*w;
VO[cnt]=s*v;
}
for(int k=1;k<=cnt;k++)
for(int j=V;j>=VO[k];j--){
f[j]=max(f[j],f[j-VO[k]]+W[k]);
}
}
int res=0;
for(int i=0;i<=V;i++) res=max(res,f[i]);
cout<<res<<endl;
return 0;
}
单调队列维护做法:
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
const int N=20010;
int f[N],g[N],q[N],n,V;
int main(){
cin>>n>>V;
for(int i=1;i<=n;i++){
int v,w,s;
cin>>v>>w>>s;
memcpy(g,f,sizeof(f));
for(int j=0;j<v;j++){ //mod v 的余数 *
int hh=0,tt=-1;
for(int k=j;k<=V;k+=v){ //单调队列里存储的相当于是使g[体积]单调下降的体积,因为一般情况体积剩的越多,g[]越小
if(hh<=tt && q[hh]<k-s*v) hh++;
while(hh<=tt && g[q[tt]]-q[tt]/v*w<g[k]-k/v*w) tt--;
q[++tt]=k;
f[k]=g[q[hh]]+(k-q[hh])/v*w; //*
}
}
}
cout<<f[V]<<endl;
return 0;
}
2、最长公共子串,最长上升子列,最短编辑距离
a)最长上升子序列
朴素做法代码:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N=1010;
int a[N],n;
int f[N];
int main(){
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i],f[i]=1;
for(int i=1;i<=n;i++)
for(int j=1;j<i;j++)
if(a[i]>a[j])
f[i]=max(f[i],f[j]+1);
int res=0;
for(int i=1;i<=n;i++) res=max(res,f[i]);
cout<<res<<endl;
return 0;
}
nlogn做法代码:
优化原理:其实是贪心的思想,这里的 q[i] 储存的是长度为 i 的上升子列结尾最小的数字,根据性质,也可以知道 q 本身也是一个单调的数组,因此在更新 q 的时候可以二分查找更新,提高效率.
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N=100010;
int n,a[N];
int main(){
cin>>n;
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
int len=0;
int q[N];
q[0]=-2e9;
for(int i=1;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);
q[r+1]=a[i];
}
cout<<len<<endl;
return 0;
}
b) 最长公共子串
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N=1010;
int f[N][N];
int n,m;
char a[N],b[N];
int main(){
cin>>n>>m;
cin>>a+1>>b+1;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
if(a[i]==b[j]) f[i][j]=f[i-1][j-1]+1;
else f[i][j]=max(f[i-1][j],f[i][j-1]);
cout<<f[n][m]<<endl;
return 0;
}
c) 最短编辑距离
题目的一般描述:给定两个字符串A和B,现在要将A经过若干操作变为B,可进行的操作有:
1.删除–将字符串A中的某个字符删除.
2.插入–在字符串A的某个位置插入某个字符.
3.替换–将字符串A中的某个字符替换为另一个字符。
现在请你求出,将A变为B至少需要进行多少次操作.
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N=1010;
int n,m;
char a[N],b[N];
int f[N][N];
int main(){
scanf("%d%s",&n,a+1);
scanf("%d%s",&m,b+1);
for(int i=0;i<=n;i++) f[i][0]=i;
for(int i=0;i<=m;i++) f[0][i]=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);
int t=!(a[i]==b[j]);
f[i][j]=min(f[i-1][j-1]+t,f[i][j]);
}
cout<<f[n][m]<<endl;
return 0;
}
二、背包问题的一些扩展
除了上述三种背包,背包问题的扩展应用大多也很经典,例如分组背包,有依赖的背包,背包方案数,背包具体方案,二维费用背包,还有各种背包的组合.头大
背包方案数:
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N=1010,mod=1e9+7;
int g[N],f[N],n,V;
int main(){
cin>>n>>V;
//memset(f,-0x3f,sizeof(f));
//f[0]=0;
g[0]=1;
for(int i=1;i<=n;i++){
int v,w;
cin>>v>>w;
for(int j=V;j>=v;j--){
int mx=max(f[j],f[j-v]+w);
int cnt=0;
if(f[j]==mx) cnt=g[j];
if(f[j-v]+w==mx) cnt=(cnt+g[j-v])%mod;
f[j]=mx;
g[j]=cnt;
}
}
int res=0;
for(int i=1;i<=V;i++) res=max(f[i],res);
int sum=0;
for(int i=1;i<=V;i++)
if(res==f[i]) sum=(sum+g[i])%mod;
cout<<sum<<endl;
return 0;
}
背包具体方案:
#include <iostream>
#include <algorithm>
using namespace std;
const int N=1010;
int f[N][N],v[N],w[N],n,V;
int main(){
cin>>n>>V;
for(int i=1;i<=n;i++){
cin>>v[i]>>w[i];
}
for(int i=n;i>=1;i--)
for(int j=0;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]);
}
for(int j=V,i=1;i<=n;i++){
if(j>=v[i] && f[i][j]==f[i+1][j-v[i]]+w[i]){
j-=v[i];
cout<<i<<" ";
}
}
return 0;
}
二位费用背包问题:
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
const int N=110,M=1010;
int f[N][M],n,V,mm;
int main(){
cin>>n>>V>>mm;
for(int i=1;i<=n;i++){
int v,m,w;
cin>>v>>m>>w;
for(int j=mm;j>=m;j--)
for(int k=V;k>=v;k--)
f[j][k]=max(f[j][k],f[j-m][k-v]+w);
}
int res=0;
for(int j=0;j<=mm;j++)
for(int k=0;k<=V;k++)
res=max(res,f[j][k]);
cout<<res<<endl;
return 0;
}
分组背包问题:
#include <iostream>
#include <algorithm>
using namespace std;
const int N=110;
int f[N],c[N],n,V;
int w[N][N],v[N][N];
int main(){
cin>>n>>V;
for(int i=1;i<=n;i++){
cin>>c[i];
for(int j=1;j<=c[i];j++)
cin>>v[i][j]>>w[i][j];
}
for(int i=1;i<=n;i++)
for(int j=V;j>=0;j--)
for(int k=1;k<=c[i];k++)
if(v[i][k]<=j)
f[j]=max(f[j],f[j-v[i][k]]+w[i][k]);
cout<<f[V]<<endl;
return 0;
}
至于有依赖的背包问题在树形DP复习时会复习到,到时候再说.