[算法系列]动态规划01-背包问题

161 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第5天,点击查看活动详情

动态规划

动态规划是一类非常重要的算法,包含线性DP、树形DP、博弈DP、数位DP、状压DP等多种子类别,该算法通过寻找集合之间的递推关系求解问题,对应的算法题目难度普遍较大

背包问题是DP的基础,对于锻炼我们分析DP的能力有很大的帮助

01背包问题

特点: N种物品装背包,每个物品都有价值,背包容量为V,每种物品只能用一次,求最大可装入的价值
状态表示: 用f[i] [j]表示前i个物品,总容量为j的情况下的最大价值
初始条件: f[0] [0]=0
状态转移方程: 如果已经装不下,则:f[i] [j]=f[i-1] [j] ;如果可以装下,那么在选第i个和不选之间取最大值即可,方程如下:

f[i][j]=max(f[i1][j],f[i1][jv[i]]+w[i])f[i][j]=max(f[i-1][j],f[i-1][j-v[i]]+w[i])

代码实现:

#include<iostream>
using namespace std;
const int N=1100;
int v[N],w[N];//分别存入体积和价值
int n,m;  //分别表示物品个数和背包总容量
int dp[N][N];
int main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        cin>>v[i]>>w[i];        
    }
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            if(j<v[i]){
                dp[i][j]=dp[i-1][j];
            }
            else{
                dp[i][j]=max(dp[i-1][j],dp[i-1][j-v[i]]+w[i]);
            }
        }
    }
    cout<<dp[n][m]<<endl;
    
    return 0;
}

空间优化: 代码在空间复杂度上还有优化的空间,由于我们知道新的状态仅与上一次的状态有关,我们可以把代码优化成下面的样子:

#include<iostream>
using namespace std;
const int N=1100;
int n,m;  //分别表示物品个数和背包总容量
int dp[N];
int main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        int v,w;
        cin>>v>>w;
        for(int j=m;j>=v;j--){
            dp[j]=max(/*由于上一次的状态未更新,相当于dp[i-1][j]*/dp[j],dp[j-v]+w);
        }
    }
    cout<<dp[m]<<endl;
    
    return 0;
}

完全背包问题

特点: 还是N种物品,装容量为V的背包,但是每种物品可以用无限次,仍然求最大价值

状态表示: 用f[i] [j]表示前i个物品,总容量为j的情况下的最大价值

状态转移方程: 我们此时考虑第i个物品选k个对应的情况,从中取最大即可,则方程如下:

f[i][j]=max(f[i][j],f[i1][jkv[i]]+kw[i])f[i][j]=max(f[i][j],f[i-1][j-k*v[i]]+k*w[i])

代码实现:

此方法可能会TLE

#include<iostream>
using namespace std;
const int N=1010;
int n,m;
int v[N],w[N];
int dp[N][N];
int main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        cin>>v[i]>>w[i];
    }
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            for(int k=0;k*v[i]<=j;k++){
                dp[i][j]=max(dp[i][j],dp[i-1][j-k*v[i]]+k*w[i]);
            }
        }
    }
    cout<<dp[n][m]<<endl;
    return 0;
}

优化: 我们关注下面的式子

f[i,j]=max(f[i1,j],f[i1,jv]+w,f[i1,j2v]+2w,)f[i,jv]=max(f[i1,jv],f[i1,j2v]+w,)f[i , j ] = max( f[i-1,j] , f[i-1,j-v]+w , f[i-1,j-2v]+2w , …)\\ f[i , j-v]= max( f[i-1,j-v] , f[i-1,j-2v] + w , …)

由上两式,可得出如下递推关系: f [ i ] [ j ]=max(f[i] [j-v]+w , f [ i -1 ] [ j ])

于是我们的代码就可以优化成下面这样:

#include<iostream>
using namespace std;
const int N=1010;
int n,m;
int v[N],w[N];
int dp[N][N];
int main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        cin>>v[i]>>w[i];
    }
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            dp[i][j] = dp[i-1][j];
            if(j-v[i]>=0){
               dp[i][j]=max(dp[i][j],dp[i][j-v[i]]+w[i]);
            }
        }
    }
    cout<<dp[n][m]<<endl;
    return 0;
}

空间优化: 仿照01背包的思路,我们可以将代码优化成下面这样

#include<iostream>
using namespace std;
const int N=1010;
int n,m;
int dp[N];
int main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        int v,w;
        cin>>v>>w;
        for(int j=v;j<=m;j++){
            dp[j]=max(dp[j],dp[j-v]+w);
        }
    }
    cout<<dp[m]<<endl;
    return 0;
}

多重背包问题(暴力)

特点: 还是N种物品,V的总容量,但是每个物品个数有上限

状态表示: 用f[i] [j]表示前i个物品,总容量为j的情况下的最大价值

状态转移方程: 我们自然就想到,完全可以沿用完全背包的第一种思路,则状态转移方程不变

代码实现:

#include<iostream>
using namespace std;
const int N=110;
int n,m;
int v[N],w[N],s[N];
int dp[N][N];
int main(){
    cin>>n>>m;
    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<=m;j++){
            for(int k=0;k<=s[i];k++){
                if(k*v[i]<=j){
                   dp[i][j]=max(dp[i][j],dp[i-1][j-k*v[i]]+k*w[i]);
                }
            }
        }
    }
    cout<<dp[n][m]<<endl;
    return 0;
}

空间优化:

#include<iostream>
using namespace std;
const int N=110;
int n,m;
int dp[N];
int main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        int v,w,s;
        cin>>v>>w>>s;
        for(int j=m;j>=v;j--){
            for(int k=0;k<=s;k++){
                if(k*v<=j){
                   dp[j]=max(dp[j],dp[j-k*v]+k*w);
                }
            }
        }
    }
    cout<<dp[m]<<endl;
    return 0;
}

注意:这里仍然是从大到小枚举j,如果换个顺序的话,那么在算 dp[j]之前,dp[j-k*v]就已经被更新了,这就与原方程不符了

多重背包问题(优化)

经过优化,我们可以解决数据范围更大的多重背包问题

优化策略: 我们可以试图将多重背包问题转化为01背包问题,那么就需要对原本有s个的物品进行拆分,拆分后的数应该能表示0到s的所有情况,我们这里的策略是按s的二进制进行拆分,比如把7,拆为1,2,4,把3拆成1,1,以此类推,如果无法继续(到边界或者再拆就变成负数了),我们就停止拆分 ,随后按分好的份数,变成新的物品加进新物品组,我们可以通过下面的代码具体来感受一下操作过程,并且看看是如何转换为01背包的

#include<iostream>
#include<vector>
using namespace std;
const int N=2100;
int n,m;
int dp[N];
struct node{
    int v,w;
};
int main(){
    cin>>n>>m;
    vector<node>goods;
    for(int i=1;i<=n;i++){
        int v,w,s;
        cin>>v>>w>>s;
        for(int k=1;k<=s;k*=2){
            s-=k;
            goods.push_back({v*k,w*k});
        }
        if(s>0)goods.push_back({v*s,w*s});
    }
    for(auto good:goods){
        for(int j=m;j>=good.v;j--){
            dp[j]=max(dp[j],dp[j-good.v]+good.w);
        }
    }
    cout<<dp[m]<<endl;
    return 0;
}

总结: 01背包问题的选或者不选也就与二进制的01对应,我们这样进行这样的拆分,还是拿7举例子,相当于用3个数的选或者不选表示了0到7所有的情况,降低了时间复杂度

分组背包问题

特点: 给N组物品,容量为V的背包,选的时候,一组里面的物品,你最多选一个,仍然求最大价值

状态表示: 用f[i] [j]表示前i组物品,总容量为j的情况下的最大价值

状态转移方程: 暴力思路,可以写出如下的转移方程:

f[i][j]=max(f[i1][j],f[i1][jvi[0]]+wi[0],...)f[i][j]=max(f[i-1][j],f[i-1][j-v_{i}[0]]+w_{i}[0],...)

代码实现:

#include<iostream>
#include<vector>
using namespace std;
const int N=110;
struct node{
    int v,w;
};
int n,m;
vector<node>g[N];
int dp[N];
int main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        int s,v,w;
        cin>>s;
        while(s--){
            cin>>v>>w;
            g[i].push_back({v,w});
        }
    }
    for(int i=1;i<=n;i++){
        for(int j=m;j>=0;j--){
            for(int k=0;k<g[i].size();k++){
                if(g[i][k].v<=j){
                    dp[j]=max(dp[j],dp[j-g[i][k].v]+g[i][k].w);
                }
            }
        }
    }
    cout<<dp[m]<<endl;
    return 0;
}