DP模板整理—背包、线性、区间、计数类

一、背包问题

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[i1][j] f [ i − 1 ] [ j − v [ i ] ] + w [ i ] f[i-1][j-v[i]]+w[i] f[i1][jv[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[i1][jv[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][j1]直接表示,因为 f [ i ] [ j − 1 ] f[i][j-1] f[i][j1]所表示的公共子序列不一定包含a[i],若包含则为情况2),否则为情况4),同理, f [ i − 1 ] [ j ] f[i-1][j] f[i1][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[i1][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[i1][j1]
2)当最小值大于1时,把每一个数都减去1后每一个数仍大于等于1,仍合法,因此此时方案数等于把当前每个数减1的方案数,即: f [ i − j ] [ j ] f[i−j][j] f[ij][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[i1][j1]+f[ij][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;
}
  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值