背包九讲的简单讲解与实现

简介

动态规划的思想是非常容易理解的,但是对于某个问题要想到对应的递推式往往不那么容易。一看就会,一做就懵。学习过01背包、完全背包,可是在我脑中的印象似乎并不那么深刻,为了巩固与提升,看了B站某大佬讲解,讲得细致、透彻,收获颇丰,原来背包问题也没有那么的困难。为了巩固与提升,写一篇博客记录一下。
以下题目链接ACwing

01背包

首先来看最简单的背包问题
背包九讲的简单讲解与实现_第1张图片

思路

设dp[i][j]为前i件物品共选中体积为j物品的最大价值。
对于每一个物品都有选和不选两种情况,那么就有递推式dp[i][j] = max(dp[i-1][j], dp[i-1][j-v[i]]+w[i])

代码1

#include 
#include 
#define MAX 1010
using namespace std;
int N,M;
int dp[MAX][MAX];
int main()
{
    scanf("%d%d",&N,&M);
    for(int i=1;i<=N;i++)
    {
        int v,w;
        scanf("%d%d",&v,&w);
        for(int j=0;j<=M;j++)
        {
            dp[i][j] = dp[i-1][j];
            if(j-v>=0)
                dp[i][j] = max(dp[i][j],dp[i-1][j-v]+w);
        }
    }
    printf("%d\n",dp[N][M]);
    
}

思考

上述算法的时间复杂度为O(NM),已经不可以再优化了。但是空间复杂度还可优化。
dp[i][j]只与dp[i-1][j]与dp[i][j-v[i]]+w[i]有关。而当前dp[i][j]就是之后的dp[i-1][j]了,那么我们就可以把数组弄成一维。即dp[j]表示选中体积j的物品的最大价值。此外我们还需要将j变为由大到小遍历。因为如果j由小到大遍历,那么遍历时内层循环中的dp[j-v[i]]实际上等同于上面的的dp[i][j-v[i]],而不是dp[i-1][j-v[i]],而由大到小的遍历保证了在当前的dp[j-v[i]]是前一个外层循环的值。
于是:

代码2

#include 
#include 
#define MAX 1010
using namespace std;
int N,M;
int dp[MAX];
int main()
{
    scanf("%d%d",&N,&M);
    for(int i=1;i<=N;i++)
    {
        int v,w;
        scanf("%d%d",&v,&w);
        for(int j=M;j>=v;j--)
        {
            dp[j] = max(dp[j],dp[j-v]+w);
        }
    }
    printf("%d\n",dp[M]);
    
}

完全背包

思路

将01背包的代码的j改为由小到大遍历即可。

#include 
#include 
#define MAX 1010
using namespace std;
int N,M;
int dp[MAX];
int main()
{
    scanf("%d%d",&N,&M);
    for(int i=1;i<=N;i++)
    {
        int v,w;
        scanf("%d%d",&v,&w);
        for(int j=v;j<=M;j++)
        {
            dp[j] = max(dp[j],dp[j-v]+w);
        }
    }
    printf("%d\n",dp[M]);
    
}

多重背包

背包九讲的简单讲解与实现_第2张图片

思路

dp[j] = max{dp[j-k*v[i]]+w[i]}, 0<=k<=s[i]
于是有以下代码

代码1

#include 
#include 
#define MAX 110
using namespace std;

int N,M;
int dp[MAX];
int main()
{
    scanf("%d%d",&N,&M);
    for(int i=1;i<=N;i++)
    {
        int v,w,s;
        scanf("%d%d%d",&v,&w,&s);
        for(int j=M;j>=v;j--)
        {
            for(int k=0;k<=s&&k*v<=j;k++)
            {
                dp[j] = max(dp[j],dp[j-k*v]+k*w);
            }
        }
    }
    printf("%d\n",dp[M]);
}

二进制优化

代码1的复杂度为O(NMS),实际上我们可以把它优化为O(NMlog(S))。对于每一件物品,最多可能取s件,而0~s可以由log(s+1)个不同的数字表示。 例如0-7可以由1,2,4三个数表示。而0-10可以由1,2,4,10-1-2-4=3完全表示。然后就可以转换成01背包问题。
于是有:

#include 
#include 
#include 
#define MAX 2010
using namespace std;
int N,M;
int dp[MAX];
struct Good
{
    int v;
    int w;
};
int main()
{
    scanf("%d%d",&N,&M);
    vector<Good> goods;
    for(int i=1;i<=N;i++)
    {
        int v,w,s;
        scanf("%d%d%d",&v,&w,&s);
        for(int k=1;k<=s;k*=2)
        {
            s-=k;
            goods.push_back(Good{k*v,k*w});
        }
        if(s>0)
            goods.push_back(Good{s*v,s*w});
    }
    for(auto good : goods)
    {
        for(int j=M;j>=good.v;j--)
        {
            dp[j] = max(dp[j],dp[j-good.v]+good.w);
        }
    }
    printf("%d\n",dp[M]);
    return 0;
}

优先队列优化

有点复杂,之后再更

混合背包

背包九讲的简单讲解与实现_第3张图片

思路

显然,该问题可以转换为01背包和完全背包,代码如下:

代码

#include 
#include 
#include 
#define MAX 1010
using namespace std;
struct Good
{
    int v;
    int w;
    int s;
};
int N,M;
int dp[MAX];
int main()
{
    scanf("%d%d",&N,&M);
    vector<Good> goods;
    for(int i=1;i<=N;i++)
    {
        int v,w,s;
        scanf("%d%d%d",&v,&w,&s);
        if(s<=0)
            goods.push_back(Good{v,w,s});
        else{
            for(int k=1;k<=s;k*=2)
            {
                s-=k;
                goods.push_back(Good{k*v,k*w,-1});
            }
            if(s>0)
                goods.push_back(Good{s*v,s*w,-1});
        }
    }
    for(auto good : goods)
    {
        if(good.s<0)
        {
            for(int j=M;j>=good.v;j--)
                dp[j] = max(dp[j],dp[j-good.v]+good.w);
        }
        else{
            for(int j=good.v;j<=M;j++)
                dp[j] = max(dp[j],dp[j-good.v]+good.w);
        }
    }
    printf("%d\n",dp[M]);
}

二维费用的背包问题

背包九讲的简单讲解与实现_第4张图片

思路

没什么好讲的,实际上还是一个01背包。

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

分组背包问题

背包九讲的简单讲解与实现_第5张图片

思路

dp[j]=max{dp[j],dp[j-v[i]]+w[i]|物品i属于第k组}

#include 
#include 
#include 
#define MAX 1010
using namespace std;
int N,M;
int dp[MAX];
int main()
{
    scanf("%d%d",&N,&M);
    for(int i=1;i<=N;i++)
    {
        int S;
        scanf("%d",&S);
        vector<int> v,w;
        v.resize(S);
        w.resize(S);
        for(int k=0;k<S;k++)
            scanf("%d%d",&v[k],&w[k]);
        for(int j=M;j>=0;j--)
            for(int k=0;k<S;k++)
                if(j-v[k]>=0)
                    dp[j] = max(dp[j],dp[j-v[k]]+w[k]);
                
    }
    printf("%d\n",dp[M]);
}

有依赖的背包问题

背包九讲的简单讲解与实现_第6张图片
背包九讲的简单讲解与实现_第7张图片

背包问题求方案数

背包九讲的简单讲解与实现_第8张图片

思路

建一个数组g,g[i]表示体积恰为i的方案数

代码

#include 
#include 
#define MAX 1010
using namespace std;
const int MOD = 1e9+7;
int dp[MAX], g[MAX];
int N,M;
int main()
{
    scanf("%d%d",&N,&M);
    dp[0] = 0;
    g[0] = 1;
    for(int i=1;i<=M;i++)
        dp[i] = -0x3f3f3f3f;
    for(int i=0;i<N;i++)
    {
        int v,w;
        scanf("%d%d",&v,&w);
        for(int j=M;j>=v;j--)
        {
            int maxi = max(dp[j],dp[j-v]+w);
            int sum = 0;
            if(maxi==dp[j])
                sum+=g[j];
            if(maxi==dp[j-v]+w)
                sum+=g[j-v];
            dp[j] = maxi;
            sum%=MOD;
            g[j] = sum;
        }
    }
    int maxians = 0;
    for(int i=0;i<=M;i++)
        maxians = max(maxians,dp[i]);
    int res = 0;
    for(int i=0;i<=M;i++)
    {
        if(dp[i]==maxians)
        {
            res+=g[i];
            res%=MOD;
        }
    }
    printf("%d\n",res);
}

背包问题求具体方案数

背包九讲的简单讲解与实现_第9张图片

思路

为了字典序最小,应该由序号高的向低的遍历

代码

#include 
#include 
#define MAX 1010
using namespace std;
int N,M;
int dp[MAX][MAX];
int v[MAX],w[MAX];
int main()
{
    scanf("%d%d",&N,&M);
    for(int i=1;i<=N;i++)
        scanf("%d%d",&v[i],&w[i]);
    for(int i=N;i>=1;i--)
    {
        for(int j=1;j<=M;j++)
        {
            dp[i][j] = dp[i+1][j];
            if(j>=v[i])
                dp[i][j] = max(dp[i][j],dp[i+1][j-v[i]]+w[i]);
        }
    }
    int vol = M;
    for(int i=1;i<=N;i++)
    {
        if(vol-v[i]>=0)
        {
            //此条件为true意味着选中了i
            if(dp[i][vol] == dp[i+1][vol-v[i]]+w[i])
            {
                if(vol!=M)
                    printf(" ");
                vol-=v[i];
                printf("%d",i);
            }
        }
        
    }
    printf("\n");
}

你可能感兴趣的:(动态规划)