【蒟蒻の笔记】背包DP

背包DP

本人的第一篇blog,可能问题比较多,权当是个人整理的笔记吧


从一道经典题目入手:01背包问题

现有一个容量大小为 m m m的背包和 n n n件物品,每件物品有两个属性,体积和价值,请问这个背包最多能装价值为多少的物品?

我们先看一些最基本的做法

二进制枚举

这种方法就不用说了吧,来学DP的肯定会枚举

时间复杂度 O ( 2 n ) O(2^n) O(2n),一般情况下必然TLE

DFS+剪枝优化

这种方法比上一种好一点,仍然是一种暴力的方法,常数上有所优化,但仍然是 O ( 2 n ) O(2^n) O(2n),一般情况下一定会TLE

这里可以注意到,DFS中出现了重复计算某种情况的问题,而时间复杂度正是堆积在这一点上,那么我们就可以对此进行解决,这衍生出了:

记忆化搜索

这种方法记录状态的答案,可以达到每种状态仅遍历一次,也就是时间复杂度减到了 O ( n m ) O(nm) O(nm)

这里对状态的记录,以及通过计算来转移至其他状态,就可以算是一种动态规划(DP)

同时这个方法也不难理解,下面给出代码:

#include
using namespace std;
int n,m,sum,g,ans;
bool f;
int w[105],v[105];
int dp[105][2005];
int dfs(int a,int b){
    if(dp[a][b]){
        return dp[a][b];
    }
    if(a==0){
        return 0;
    }
    dp[a][b]=max(dfs(a-1,b-w[a-1])+v[a-1],dfs(a-1,b));
    return dp[a][b];
}
signed main(){
    scanf("%d%d",&m,&n);
    for(int i=1;i<=n;i++){
        scanf("%d",w+i);
        scanf("%d",v+i);
    }
    printf("%d",dfs(n,m));
    return 0;
}

01背包

下面我们正式开始动态规划,令dp[i][j]为前i个物品用容量为j的物品(你品,你细品)
然后我们就得到了以下的状态转移方程:

d p i , j = m a x ( d p i − 1 , j , d p i − 1 , j − w i + c i ) dp_{i,j}=max(dp_{i-1,j},dp_{i-1,j-w_i}+c_i) dpi,j=max(dpi1,j,dpi1,jwi+ci)

就是两种情况:取或不取

然后,我们直接通过循环算出所有范围内的dp的值就可以得到 d p n , m dp_{n,m} dpn,m也就是当前问题的解。

程序如下:

#include
using namespace std;
int n,m,sum,g,ans;
bool f;
int w[105],v[105];
int dp[105][2005];
signed main(){
    scanf("%d%d",&m,&n);
    for(int i=1;i<=n;i++){
        scanf("%d",w+i);
        scanf("%d",v+i);
    }
    for(int i=1;i<=n;i++){
        for(int j=w[i];j<=m;j++){
            dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);
        }
    }
    printf("%d",dp[n][m]);
    return 0;
}

时间复杂度 O ( n m ) O(nm) O(nm),空间复杂度也是 O ( n m ) O(nm) O(nm)

滚动数组优化

这里的时间复杂度无法继续优化,但是空间可以。

我们注意到方程中第一维一直是 i − 1 i-1 i1 i i i,也就是说我们可以直接省去它,反正我们只需要最终的 d p n , m dp_{n,m} dpn,m,于是,我们需要保证计算到 d p j dp_{j} dpj时, d p j − w [ i ] dp_{j-w[i]} dpjw[i]是未被计算的,那么我们就需要逆序循环。

代码如下:

#include
using namespace std;
int n,m,sum,g,ans;
bool f;
int w[105],v[105];
int dp[2005];
signed main(){
    scanf("%d%d",&m,&n);
    for(int i=1;i<=n;i++){
        scanf("%d",w+i);
        scanf("%d",v+i);
    }
    for(int i=1;i<=n;i++){
        for(int j=m;j>=w[i];j--){
            dp[i][j]=max(dp[j],dp[j-w[i]]+v[i]);
        }
    }
    printf("%d",dp[m]);
    return 0;
}

时间复杂度 O ( n m ) O(nm) O(nm),空间复杂度 O ( n + m ) O(n+m) O(n+m)

时间复杂度

继续:完全背包问题

现有一个容量大小为 m m m的背包和 n n n种物品,每种物品有两个属性,体积和价值,且都有无数件,请问这个背包最多能装价值为多少的物品?

可以直接沿用01背包的逻辑

二维DP

dp[i][j]为前i个物品用容量为j的物品

由题意可得,

d p i , j = m a x ( d p i − 1 , j , d p i , j − w i + c i ) dp_{i,j}=max(dp_{i-1,j},dp_{i,j-w_i}+c_i) dpi,j=max(dpi1,j,dpi,jwi+ci)

也是两种情况,一种是进入下一个物品,一种是再选一种当前物品

代码如下:

#include
using namespace std;
int n,m,sum,g,ans;
bool f;
int w[105],v[105];
int dp[105][2005];
signed main(){
    scanf("%d%d",&m,&n);
    for(int i=1;i<=n;i++){
        scanf("%d",w+i);
        scanf("%d",v+i);
    }
    for(int i=1;i<=n;i++){
        for(int j=w[i];j<=m;j++){
            dp[i][j]=max(dp[i-1][j],dp[i][j-w[i]]+v[i]);
        }
    }
    printf("%d",dp[n][m]);
    return 0;
}

时间复杂度 O ( n m ) O(nm) O(nm),空间复杂度 O ( n m ) O(nm) O(nm)

滚动数组优化

参照上文,状态转移方程变为

d p j = m a x ( d p j , d p j − w i + c i ) dp_{j}=max(dp_{j},dp_{j-w_i}+c_i) dpj=max(dpj,dpjwi+ci)

也是两种情况,一种是进入下一个物品,一种是再选一种当前物品

跟之前的01背包完全一样?这里要注意正序遍历,看看之前的状态转移方程就清楚了

代码如下:

#include
using namespace std;
int n,m,sum,g,ans;
bool f;
int w[105],v[105];
int dp[2005];
signed main(){
    scanf("%d%d",&m,&n);
    for(int i=1;i<=n;i++){
        scanf("%d",w+i);
        scanf("%d",v+i);
    }
    for(int i=1;i<=n;i++){
        for(int j=w[i];j<=m;j++){
            dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
        }
    }
    printf("%d",dp[m]);
    return 0;
}

时间复杂度 O ( n m ) O(nm) O(nm),空间复杂度 O ( n + m ) O(n+m) O(n+m)

继续拓展:多重背包问题

现有一个容量大小为 m m m的背包和 n n n种物品,每种物品有三个属性,体积和价值和数量,请问这个背包最多能装价值为多少的物品?

显然可以转化为01背包

朴素做法

直接把每个物品展开,然后直接01背包

十分暴力,下面是代码:

#include
#define int long long
using namespace std;
int n,m,sum,g,ans;
int w[100005],v[100005],a,b,c;
int dp[200005];
signed main(){
    scanf("%lld%lld",&m,&sum);
    for(int i=1;i<=sum;i++){
        scanf("%lld",&c);
        scanf("%lld",&a);
        scanf("%lld",&b);
        for(int j=1;j<=c;j++){
            n++;
            w[n]=a*1;
            v[n]=b*1;
        }
    }
    for(int i=1;i<=n;i++){
        for(int j=m;j>=w[i];j--){
            dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
        }
    }
    printf("%lld",dp[m]);
    return 0;
}

时间复杂度 O ( m ∑ i = 0 n c i ) O(m\sum_{i=0}^n c_i) O(mi=0nci),空间复杂度 O ( m + ∑ i = 0 n c i ) O(m+\sum_{i=0}^n c_i) O(m+i=0nci)

二进制优化

显然之前那种直接展开的方式极为不必要

我们只是需要凑出0至c[i]的所有组合就行了,那么,自然会想到伟大的二进制,设有 2 x ≤ c [ i ] ≤ 2 x + 1 2^x\leq c[i]\leq 2^{x+1} 2xc[i]2x+1,那么把c[i]拆成 2 0 , 2 1 , 2 2 … … 2 x − 1 2^0,2^1,2^2……2^{x-1} 20,21,22……2x1,可以凑出0至 2 x − 1 2^x-1 2x1的所有数,加上剩下的那一部分,正好够凑出0至c[i]的所有数。

拆完之后仍然是直接01背包,下面是代码:

#include
#define int long long
using namespace std;
int n,m,sum,g,ans;
int w[100005],v[100005],a,b,c;
int dp[200005];
signed main(){
    scanf("%lld%lld",&m,&sum);
    for(int i=1;i<=sum;i++){
        scanf("%lld",&c);
        scanf("%lld",&a);
        scanf("%lld",&b);
        for(int j=0;(1<<j)<=c;j++){
            c-=(1<<j);
            n++;
            w[n]=a*(1<<j);
            v[n]=b*(1<<j);
        }
        if(c){
            n++;
            w[n]=a*c;
            v[n]=b*c;
        }
    }
    for(int i=1;i<=n;i++){
        for(int j=m;j>=w[i];j--){
            dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
        }
    }
    printf("%lld",dp[m]);
    return 0;
}

时间复杂度 O ( m ∑ i = 0 n log ⁡ 2 c i ) O(m\sum_{i=0}^n \log_{2}c_i) O(mi=0nlog2ci),空间复杂度 O ( m + ∑ i = 0 n log ⁡ 2 c i ) O(m+\sum_{i=0}^n \log_{2}c_i) O(m+i=0nlog2ci)

分组背包

现有一个容量大小为 m m m的背包和 n n n件物品,这 n n n件物品分为 k k k组,每组物品有c[i]件,每件物品有对应的体积和价值,每组物品至多选择一件,请问这个背包最多能装价值为多少的物品?

直接DP走起

朴素做法

同样是01背包的思路,但是原先的“每个物品”变成了现在的“每组物品”,相应的,要考虑的情况也从“选和不选”变成了“不选,选第一个,选第二个……选第 c i c_i ci个”,再加一层循环即可。

和01背包相差不大,以下是最为直接的做法:

#include
using namespace std;
int n,m;
int dp[105][105],w[105][105],v[105][105],c[105];
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++){
        scanf("%d",c+i);
        for(int j=1;j<=c[i];j++){
            scanf("%d%d",&w[i][j],&v[i][j]);
        }
    }
    for(int i=1;i<=n;i++){
        for(int j=0;j<=m;j++){
            dp[i][j]=dp[i-1][j];
            for(int k=1;k<=c[i];k++){
                if(j>=w[i][k]){
                    dp[i][j]=max(dp[i][j],dp[i-1][j-w[i][k]]+v[i][k]);
                }
            }
        }
    }
    printf("%d\n",dp[n][m]);
    return 0;
}

时间复杂度 O ( m ∑ i = 0 n c i ) O(m\sum_{i=0}^n c_i) O(mi=0nci),空间复杂度 O ( m ∑ i = 0 n c i ) O(m \sum_{i=0}^n c_i) O(mi=0nci)

滚动数组优化

这个已经讲过很多次了,不用再一次描述了吧

直接放出代码:

#include
using namespace std;
int n,m;
int dp[105],w[105][105],v[105][105],c[105];
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++){
        scanf("%d",c+i);
        for(int j=1;j<=c[i];j++){
            scanf("%d%d",&w[i][j],&v[i][j]);
        }
    }
    for(int i=1;i<=n;i++){
        for(int j=0;j<=m;j++){
            dp[j]=dp[j];
            for(int k=1;k<=c[i];k++){
                if(j>=w[k]){
                    dp[j]=max(dp[j],dp[j-w[i][k]]+v[i][k]);
                }
            }
        }
    }
    printf("%d\n",dp[m]);
    return 0;
}

时间复杂度 O ( m ∑ i = 0 n c i ) O(m\sum_{i=0}^n c_i) O(mi=0nci),空间复杂度 O ( m + ∑ i = 0 n c i ) O(m+\sum_{i=0}^n c_i) O(m+i=0nci),仍然是非常有效的优化

有依赖的背包问题

咕咕咕!@#¥%……&*()

你可能感兴趣的:(C++学习笔记,漫漫OI路,算法)