一、背包问题
01背包问题
题解:
ACM_01背包(恰好装满)
解题思路:求01背包恰好装满时得到的最大价值,应该这样初始化:①dp[0]=0,表示背包容量为0时得到的最大价值也为0;②dp[1~W]=-inf,表示背包容量为其他状态下都为非法状态(未装满),因为我们还要求解恰好装满的情况下得到的最大价值。那么在求解过程中,由于动规的基本思想是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解,所以如果子问题状态是非法的(-inf),则当前问题的状态依然非法,即不存在恰好装满的情况;相反,如果子问题的状态是合法(不是-inf)的(恰好装满),那么当前问题求解也可得到合法状态。这样最后判断一下dp[W]是否恰好装满即dp[W]>=0,否则为未装满状态。
求恰好装满,设为负无穷
只求最大值,设为0
二维01背包
f [ i ] [ j ] f[i][j] f[i][j]代表只在前i个物品中选,总体积为j的最大价值
for(int i=1;i<=n;i++){
for(int j=0;j<=m;j++){
f[i][j]=f[i-1][j];//若j<v[i],则最大价值为在前j个物品中选总体积为j的最大价值
if(j>=v[i]) f[i][j]=max(f[i][j],f[i-1][j-v[i]]+w[i]);
}
}
一维01背包
f [ j ] f[j] f[j]代表总体积为j时的最大价值
1)为何能从一维降到二维呢?
每次 f [ i ] [ j ] f[i][j] f[i][j]的更新,只有 f [ i − 1 ] [ j ] f[i-1][j] f[i−1][j], f [ i − 1 ] [ j − v [ i ] ] + w [ i ] f[i-1][j-v[i]]+w[i] f[i−1][j−v[i]]+w[i]有关,且只有在 j > = v [ i ] j>=v[i] j>=v[i]时才由 f [ i − 1 ] [ j − v [ i ] ] + w [ i ] f[i-1][j-v[i]]+w[i] f[i−1][j−v[i]]+w[i]更新。
我们的枚举是从前往后枚举物品,就不需要记录当前是更新哪一个物品了。
2)为何第二层循环从m~0(逆序)?
假设v[1]=4,w[1]=5,显然得f[4]=5,而当遍历到f[5]时,由于f[4]已经被更新,所以会导致产生错误的答案,当逆序遍历时,不会出现多次选择同一件物品的情况。
for(int i=1;i<=n;i++){
for(int j=m;j>=v[i];j--){//只枚举到v[i]可以节省时间
f[j]=max(f[j],f[j-v[i]]+w[i]);
}
}
完全背包问题
题解:
暴力完全背包O(n^3)
for(int i = 1 ; i<=n ;i++){
for(int j = 0 ; j<=m ;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]);
}
}
1)对于完全背包 f [ i ] [ j ] f[i][j] f[i][j]时如何更新过来的?
f[i , j ] = max( f[i-1,j] , f[i-1,j-v]+w , f[i-1,j-2*v]+2*w , f[i-1,j-3*v]+3*w , .....)
f[i , j-v]= max( f[i-1,j-v] , f[i-1,j-2*v] + w , f[i-1,j-3*v]+2*w , .....)
由上两式,可得出如下递推关系:
f[i][j]=max(f[i,j-v]+w , f[i-1][j])
即:
for(int i = 1 ; i <=n ;i++){
for(int j = 0 ; j <=m ;j++){
f[i][j] = f[i-1][j];
if(j-v[i]>=0) f[i][j]=max(f[i][j],f[i][j-v[i]]+w[i]);
}
}
又根据01背包的优化方式,得出优化版的完全背包:
优化完全背包O(n^2)
f [ j ] f[j] f[j]代表总体积为j时的最大价值
为何第二层循环从0~m(正序)?
假设v[1]=4,w[1]=5,显然得f[4]=5,而当遍历到f[5]时,由于f[4]已经被更新,正好符合完全背包可以选一件物品多次的情况。
for(int i=1;i<=n;i++){
for(int j=v[i];j<=m;j++){
f[j]=max(f[j],f[j-v[i]]+w[i]);
}
}
多重背包
题解:
1)
f [ i ] [ j ] f[i][j] f[i][j]代表之选前i个物品,总体积为j的最大价值
for(int i=1;i<=n;i++){
for(int j=0;j<=m;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]]+k*w[i]);
}
}
}
2)当物品个数以及最多可用数较少时,可以用01背包的形式解决(详见多重背包II)
多重背包问题 II
题解:
一. 为何二进制优化是正确的?
1)使用二进制可以表示出来物品i的任意选法:1、2、4、8、x…(x<16)可以组成1~15+x之内的任意一个数。
2)使用二进制将物品分成 l o g n + 1 logn+1 logn+1份,就可以用01背包的形式解决多重背包问题。
int main(){
cin>>n>>m;
vector<PII>ve;
for(int i=1;i<=n;i++){
int v,w,s;
cin>>v>>w>>s;
int j=1;
while(j<=s){
ve.push_back({v*j,w*j});//将物品i拆分成logn+1个物品
s-=j;
j<<=1;
}
if(s) ve.push_back({v*s,w*s});//注意不能恰好构成的情况
}
for(int i=0;i<ve.size();i++){
for(int j=m;j>=ve[i].x;j--){
f[j]=max(f[j],f[j-ve[i].x]+ve[i].y);
}
}
cout<<f[m];
}
二. 为何不能用完全背包的方式降低复杂度?
完全背包的转移过程:
f[i , j ] = max( f[i-1,j] , f[i-1,j-v]+w , f[i-1,j-2*v]+2*w , f[i-1,j-3*v]+3*w , .....)
f[i , j-v]= max( f[i-1,j-v] , f[i-1,j-2*v] + w , f[i-1,j-3*v]+2*w , .....)
由上两式,可得出如下递推关系:
f[i][j]=max(f[i,j-v]+w , f[i-1][j])
完全背包只有j足够大,就可以一直选择第i个物品
多重背包的转移过程:
f[i , j ] = max( f[i-1,j] ,f[i-1,j-v]+w ,f[i-1,j-2v]+2w ,..... 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-Sv]+(S-1)w, f[i-1,j-(S+1)v]+Sw )
由于多重背包限制了第i个物品的使用个数,所以会导致无法由完全背包的方式推出优化方式。
分组背包问题
题解:
1)二维分组背包
f [ i ] [ j ] f[i][j] f[i][j]代表在前i组中选,总体积为j时的最大价值
注意我们要先循环枚举体积,再循环枚举当前组中的每一个物品
如果先枚举物品,则每一次枚举新的物品都会把之前求得的最大价值给覆盖,因为每一次都是从体积为0开始更新最大价值的。
#include<bits/stdc++.h>
using namespace std;
const int N=110;
int v[N][N],w[N][N];
int f[N][N];
int a[N];
int n,m;
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i];
for(int j=1;j<=a[i];j++) cin>>v[i][j]>>w[i][j];
}
for(int i=1;i<=n;i++){
for(int j=0;j<=m;j++){//要先循环枚举体积,再循环枚举当前组中的每一个物品
f[i][j]=f[i-1][j];
for(int k=1;k<=a[i];k++){
if(j>=v[i][k]) f[i][j]=max(f[i][j],f[i-1][j-v[i][k]]+w[i][k]);
}
}
}
cout<<f[n][m];
return 0;
}
2)一维分组背包
f [ j ] f[j] f[j]表示总体积为j时的最大价值
#include<bits/stdc++.h>
using namespace std;
const int N=110;
int v[N][N],w[N][N];
int f[N];
int a[N];
int n,m;
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i];
for(int j=1;j<=a[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<=a[i];k++){
if(j>=v[i][k]) f[j]=max(f[j],f[j-v[i][k]]+w[i][k]);
}
}
}
cout<<f[m];
return 0;
}
二、线性DP
数字三角形
题解:
f [ i ] [ j ] f[i][j] f[i][j]代表从上往下遍历到(i,j)的最大和
f[i][j]=g[i][j]+max(f[i-1][j],f[i-1][j-1]);//注意考虑边界
const int N=510;
int g[N][N];
int f[N][N];
int n;
void solve(){
cin>>n;
for(int i=1;i<=n;i++){
for(int j=1;j<=i;j++) cin>>g[i][j];
}
f[1][1]=g[1][1];
for(int i=2;i<=n;i++){
for(int j=1;j<=i;j++){
if(j==1) f[i][j]=f[i-1][j]+g[i][j];
else if(j==i) f[i][j]=g[i][j]+f[i-1][j-1];
else f[i][j]=g[i][j]+max(f[i-1][j],f[i-1][j-1]);//注意考虑边界
}
}
int ans=-0x3f3f3f3f;
for(int i=1;i<=n;i++) ans=max(f[n][i],ans);
cout<<ans;
}
最长上升子序列
题解:
f [ i ] f[i] f[i]代表以第i个结尾的最长上升序列长度
for(int i=1;i<=n;i++) f[i]=1;//初始化
if(g[j]>g[i]) f[j]=max(f[i]+1,f[j]);//双重循环枚举,更新每个位置的最长上升子序列的长度
int main(){
cin>>n;
for(int i=1;i<=n;i++) cin>>g[i],f[i]=1;//初始化
// for(int i=1;i<=n;i++){
// for(int j=1;j<i;j++){
// if(g[j]<g[i]) f[i]=max(f[j]+1,f[i]);
// }
// }
for(int i=1;i<=n;i++){
for(int j=i+1;j<=n;j++){
if(g[j]>g[i]) f[j]=max(f[i]+1,f[j]);
}
}
int ans=1;
for(int i=1;i<=n;i++) ans=max(ans,f[i]);
cout<<ans;
return 0;
}
最长上升子序列 II(贪心,二分)
题解:
如何使序列尽可能的长?
尽量使序列中的数小,当序列中数尽可能的小时,才能加入后面相对较大的值,如{3 1 2 1 8 5 6}最长序列为{1,2,5,6}
我们维护一个数组 f [ i ] f[i] f[i],表示最长上升子序列长度为 i i i时以 f [ i ] f[i] f[i]结尾,
当 f [ i ] > = a [ j ] f[i]>=a[j] f[i]>=a[j]时,表示可以得到一个相对更优序列,我们可以用二分更新数组 f f f
int a[N];
vector<int>q;
int n;
int main(){
cin>>n;
for(int i=0;i<n;i++) cin>>a[i];
q.push_back(a[0]);
for(int i=1;i<n;i++){
if(q.back()>=a[i]) *lower_bound(q.begin(),q.end(),a[i])=a[i];
else q.push_back(a[i]);
}
cout<<q.size();
return 0;
}
最长公共子序列
题解:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VibBNKG5-1648267818630)(C:\Users\19322\Pictures\Saved Pictures\最长公共子序列.png)]
f [ i ] [ j ] f[i][j] f[i][j]代表序列a的前i个字符和序列b前j个字符中的最长公共序列(不一定包含a[i],b[j])
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q0iCI38S-1648267818631)(C:\Users\19322\Pictures\Saved Pictures\最长公共子序列1.png)]
对于情况2)——a[i]在公共子序列中,b[i]不在,并不能由 f [ i ] [ j − 1 ] f[i][j-1] f[i][j−1]直接表示,因为 f [ i ] [ j − 1 ] f[i][j-1] f[i][j−1]所表示的公共子序列不一定包含a[i],若包含则为情况2),否则为情况4),同理, f [ i − 1 ] [ j ] f[i-1][j] f[i−1][j]包含情况3),4)
但由于本题求的是最大值,即使有集合上的重复也不会影响最大值。
const int N=1010;
int f[N][N];
char a[N],b[N];
int n,m;
int main(){
cin>>n>>m;
cin>>(a+1)>>(b+1);
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
//f[i-1][j-1]包含于f[i-1][j],f[i][j-1]
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];
return 0;
}
最短编辑距离
题解:
f [ i ] [ j ] f[i][j] f[i][j]代表字符串a的前i个字符和字符串b的前j个字符完全匹配的最小操作数
const int N=1010;
int f[N][N];
int n,m;
char a[N],b[N];
int main(){
cin>>n>>a+1;
cin>>m>>b+1;
memset(f,0x3f,sizeof f);
for(int i=0;i<=max(n,m);i++) f[i][0]=f[0][i]=i;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
if(a[i]==b[j]) f[i][j]=min(f[i][j],f[i-1][j-1]);
else f[i][j]=min(f[i][j],f[i-1][j-1]+1);//替换
f[i][j]=min(f[i-1][j]+1,f[i][j]);//删除
f[i][j]=min(f[i][j-1]+1,f[i][j]);//插入
}
}
cout<<f[n][m];
}
为何不用考虑删除或插入字符对后续的印象?
由于dp的过程是从前往后进行的,每次只是对当前位置进行判断,每一次删除a(插入a),就相当于字符串a(b)向前一个字符
最长公共上升子序列(LCIS)
题解:
f [ i ] [ j ] f[i][j] f[i][j]表示在A的前 i i i个数字中,B的前 j j j个数字中且包含 b [ j ] b[j] b[j]的公共上升子序列的最大长度
状态转移:
不选a[i]:
在A的前i-1个数字中,B的前j个数字中且包含b[j]的公共上升子序列的最大长度——>f[i-1][j]
选a[i]:(a[i]==b[j])
在A的前i-1个数字中,B的前k个数字中(0<=k<=j-1)且以b[k]结尾的子序列中的最大值+1
包含a[i]的子集,将这个子集继续划分,依据是子序列的倒数第二个元素在b[]中是哪个数:
子序列只包含b[j]一个数,长度是1;
子序列的倒数第二个数是b[1]的集合,最大长度是f[i - 1][1] + 1;
…
子序列的倒数第二个数是b[j - 1]的集合,最大长度是f[i - 1][j - 1] + 1;
1)朴素版 O ( n 3 ) O(n^3) O(n3)
#include<bits/stdc++.h>
using namespace std;
const int N=3010;
int f[N][N];
int n;
int a[N],b[N];
int main(){
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<=n;i++) cin>>b[i];
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
f[i][j]=f[i-1][j];
if (a[i]==b[j]){
int maxv=1;//k=0时只有a[i]=b[j]所以初始化为1
for(int k=1;k<j;k++){
if (a[i]>b[k]) maxv=max(maxv,f[i-1][k]+1);
}
f[i][j]=max(f[i][j],maxv);
}
}
}
int res=0;
for(int i=0;i<=n;i++) res=max(res,f[n][i]);
cout<<res;
return 0;
}
2)优化一 O ( n 2 ) O(n^2) O(n2)
然后我们发现每次循环求得的==
m
a
x
v
maxv
maxv是满足
a
[
i
]
>
b
[
k
]
a[i] > b[k]
a[i]>b[k]的
f
[
i
−
1
]
[
k
]
+
1
f[i-1][k]+1
f[i−1][k]+1的前缀最大值。==
因此可以直接将
m
a
x
v
maxv
maxv提到第一层循环外面,减少重复计算,此时只剩下两重循环。
最终答案枚举子序列结尾取最大值即可。
#include<bits/stdc++.h>
using namespace std;
const int N=3010;
int f[N][N];
int n;
int a[N],b[N];
int main(){
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<=n;i++) cin>>b[i];
for(int i=1;i<=n;i++){
int maxv=1;
for(int j=1;j<=n;j++){
f[i][j]=f[i-1][j];
if(a[i]==b[j]) f[i][j]=max(f[i][j],maxv);
if(a[i]>b[j]) maxv=max(maxv,f[i-1][j]+1);
}
}
int res=0;
for(int i=0;i<=n;i++) res=max(res,f[n][i]);
cout<<res;
return 0;
}
3)优化三
三、区间DP
石子合并
题解:
f [ i ] [ j ] f[i][j] f[i][j]表示将区间 [ i , j ] [i,j] [i,j]合并成一堆需要的最小花费
#include<bits/stdc++.h>
using namespace std;
const int N=310;
int f[N][N];
int n;
int s[N];
int main(){
cin>>n;
for(int i=1;i<=n;i++) cin>>s[i],s[i]+=s[i-1];//前缀和求区间和
for(int len=2;len<=n;len++){//枚举区间长度
for(int l=1;l+len-1<=n;l++){//枚举左右端点
int r=l+len-1;
f[l][r]=0x3f3f3f3f;
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];
return 0;
}
四、计数类DP
整数划分
题解:
1)完全背包做法(因为每个数都可以使用无限次)
(1)二维
f [ i ] [ j ] f[i][j] f[i][j]代表在1~ i i i个数中选,总体积恰好为 j j j的方案数
因为必然能构成至少一种方案,所以初始化不用负无穷
#include<bits/stdc++.h>
using namespace std;
const int N=1010,mod=1e9+7;
int n,m;
int f[N][N];
int main(){
cin>>n;
f[0][0]=1;
for(int i=1;i<=n;i++){
for(int j=0;j<=n;j++){
f[i][j]=f[i-1][j];
if(j>=i) f[i][j]=(f[i][j]+f[i][j-i])%mod;
}
}
cout<<f[n][n];
return 0;
}
(2)一维
#include<bits/stdc++.h>
using namespace std;
const int N=1010,mod=1e9+7;
int n,m;
int f[N];
int main(){
cin>>n;
f[0]=1;
for(int i=1;i<=n;i++){
for(int j=0;j<=n;j++){
if(j>=i) f[j]=(f[j]+f[j-i])%mod;
}
}
cout<<f[n];
return 0;
}
2)计数类DP
f [ i ] [ j ] f[i][j] f[i][j]表示总和为 i i i,总个数为 j j j的方案数
状态表示:
将 f [ i ] [ j ] f[i][j] f[i][j]分为所选数最小值为1、所选数最小值大于1两部分
1)当最小值是1时,该方案数等于舍弃该1后的方案数,因此此时的方案数是: f [ i − 1 ] [ j − 1 ] f[i−1][j−1] f[i−1][j−1]
2)当最小值大于1时,把每一个数都减去1后每一个数仍大于等于1,仍合法,因此此时方案数等于把当前每个数减1的方案数,即: f [ i − j ] [ j ] f[i−j][j] f[i−j][j];
因此状态转移方程是: f [ i ] [ j ] = f [ i − 1 ] [ j − 1 ] + f [ i − j ] [ j ] f[i][j]=f[i−1][j−1]+f[i−j][j] f[i][j]=f[i−1][j−1]+f[i−j][j]
#include<bits/stdc++.h>
using namespace std;
const int N=1010,mod=1e9+7;
int n,m;
int f[N][N];
int main(){
cin>>n>>m;
f[0][0]=1;
for(int i=1;i<=n;i++){
for(int j=1;j<=i;j++){
f[i][j]=(f[i-1][j-1]+f[i-j][j])%mod;
}
}
int ans=0;
for(int i=1;i<=n;i++) ans=(ans+f[n][i])%mod;
cout<<ans;
return 0;
}