背包九讲,写给自己

之前学了dp没有好好看一遍背包九讲,今天把背包九讲过一遍,供之后自己看方便一些。


一. 01背包

  题目链接:https://www.acwing.com/problem/content/2/

  n,V<=1000

  这个没什么好说的,加滚动数组,复杂度O(nV),代码如下:

#include<cstdio>
#include<algorithm>
using namespace std;

const int maxn=1e3+5;
int n,V,dp[maxn],v[maxn],w[maxn];

int main(){
    scanf("%d%d",&n,&V);
    for(int i=1;i<=n;++i)
        scanf("%d%d",&v[i],&w[i]);
    for(int i=1;i<=n;++i)
        for(int j=V;j>=v[i];--j)
            dp[j]=max(dp[j],dp[j-v[i]]+w[i]);
    printf("%d\n",dp[V]);
    return 0;
}
View Code

  要注意的是,如果题目限制一定要装满,那么应将dp数组初始化为负无穷,只将dp[0]初始化0,此时dp[i]的定义是背包大小为i装满的最大价值,其余都一样,答案是dp[V]。


二. 完全背包

  题目链接:https://www.acwing.com/problem/content/3/

  n,V<=1000

  只用把01背包的遍历j的顺序从逆序改成顺序即可,复杂度O(nV)。代码如下:

#include<cstdio>
#include<algorithm>
using namespace std;

const int maxn=1e3+5;
int n,V,v[maxn],w[maxn],dp[maxn];

int main(){
    scanf("%d%d",&n,&V);
    for(int i=1;i<=n;++i)
        scanf("%d%d",&v[i],&w[i]);
    for(int i=1;i<=n;++i)
        for(int j=v[i];j<=V;++j)
            dp[j]=max(dp[j],dp[j-v[i]]+w[i]);
    printf("%d\n",dp[V]);
    return 0;
}
View Code

  同样的,如果题目限制要装满,将dp数组初始化为负无穷,仅将dp[0]=0。


三. 多重背包

1. 多重背包I(O(nVs))

题目链接:https://www.acwing.com/problem/content/4/

n种物品,v[i],w[i],s[i]分别表示物品i的体积、价值和数量,背包大小V,求最大价值。n,V,s<=100。

在01背包基础上加一层循环表示物品i选几个,即dp[j]=max(dp[j] , dp[j-k*v[i]]+k*w[i]),k=1..s[i]。

#include<cstdio>
#include<algorithm>
using namespace std;

const int maxn=105;
int n,V,v[maxn],w[maxn],s[maxn],dp[maxn];

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

2. 多重背包II--二进制优化(O(nVlogs))

题目链接:https://www.acwing.com/problem/content/5/

题意和上面的一样,只是数据范围变了,n<=1000,V,s<=2000。

如果按照上一题做法,复杂度是4e9,妥妥的T飞。

首先,想到将每种物品拆分成s[i]个物品,01背包就能解决,那么就有1000×2000=2e6个物品,乘个V,4e9的复杂度,QAQ。。

接下来利用二进制的力量,把物品的数量s[i]分成k个数20、21...2k,k=log2s[i],那么用这几个数可以表示1到s[i]中的任意数,这样复杂度就降为O(nVlogs)。但要注意的是对于7,我们分解成1、2、7是没问题的,对于10,我们不能分解为1、2、4、8,因为这样表示的数可能超过10,那么这种情况我们将最后一个数定为s[i]-前面的和,即用1、2、4、3表示10。因为s[i]<=2000<2048,故最多用11个数就能表示s[i]。复杂度为2e7。

代码:

#include<cstdio>
#include<algorithm>
using namespace std;

const int maxn=1005*11+5;
int n,V,cnt,v[maxn],w[maxn],dp[maxn];

int main(){
    scanf("%d%d",&n,&V);
    for(int i=1;i<=n;++i){
        int vv,ww,ss;
        scanf("%d%d%d",&vv,&ww,&ss);
        for(int j=1;j<=ss;j*=2){
            v[++cnt]=vv*j;
            w[cnt]=ww*j;
            ss-=j;
        }
        if(ss){
            v[++cnt]=vv*ss;
            w[cnt]=ww*ss;
        }
    }
    for(int i=1;i<=cnt;++i)
        for(int j=V;j>=v[i];--j)
            dp[j]=max(dp[j],dp[j-v[i]]+w[i]);
    printf("%d\n",dp[V]);
    return 0;
}
View Code

3. 多重背包III(O(nV))

题目链接:https://www.acwing.com/problem/content/description/6/

题意仍然一样,数据范围变为n<=1000,V,s<=20000。此时上述二进制优化的O(nVlogs)做法也不能通过。此时考虑单调队列来优化,去掉复杂度中的logs。

  多重背包的最原始的状态转移方程:

  令 c[i] = min(num[i], j / v[i]) ,f[i][j] = max(f[i-1][j-k*v[i]] + k*w[i])     (1 <= k <= c[i])  这里的 k 是指取第 i 种物品 k 件。

  如果令 a = j / v[i] , b = j % v[i] 那么 j = a * v[i] + b。这里用 k 表示的意义改变, k 表示取第 i 种物品的件数比 a 少几件。

  那么 f[i][j] = max(f[i-1][b+k*v[i]] - k*w[i]) + a*w[i]      (a-c[i] <= k <= a)

  可以发现,f[i-1][b+k*v[i]] - k*w[i] 只与 k 有关,而这个 k 是一段连续的(只要dp问题的状态方程能够表示成这样,就可以用单调队列来优化)。我们要做的就是求出 f[i-1][b+k*v[i]] - k*w[i] 在 k 取可行区间内时的最大值。所以我们可以按j模v[i]的余数来划分,分别用单调队列来求解。

AC代码:

#include<cstdio>
#include<algorithm>
using namespace std;

const int maxn=1005;
const int maxv=20005;
int n,V,dp[maxv],q1[maxv],q2[maxv];
int head,tail;

int main(){
    scanf("%d%d",&n,&V);
    for(int i=1;i<=n;++i){
        int v,w,s;
        scanf("%d%d%d",&v,&w,&s);
        for(int j=0;j<v;++j){
            head=1,tail=0;
            for(int k=j,num=0;k<=V;k+=v,++num){
                int tmp=dp[k]-num*w;
                while(head<=tail&&tmp>=q1[tail]) --tail;
                q1[++tail]=tmp;
                q2[tail]=num;
                while(num-q2[head]>s) ++head;
                dp[k]=q1[head]+num*w;
            }
        }
    }
    printf("%d\n",dp[V]);
    return 0;
}
View Code

四. 混合背包问题

题目链接:https://www.acwing.com/problem/content/7/

题意:前3类问题的综合,即一种物品可能是01背包,可能是完全背包,可能为多重背包。

只用将多重背包的O(nV)解法稍微改一下即可,即如果s=-1,令s=1,如果s=0,令s=V/v,其它情况一样。

AC代码:

#include<cstdio>
#include<algorithm>
using namespace std;

const int maxn=1005;
const int maxv=1005;
int n,V,dp[maxv],q1[maxv],q2[maxv];
int head,tail;

int main(){
    scanf("%d%d",&n,&V);
    for(int i=1;i<=n;++i){
        int v,w,s;
        scanf("%d%d%d",&v,&w,&s);
        if(s==-1) s=1;
        else if(s==0) s=V/v;
        for(int j=0;j<v;++j){
            head=1,tail=0;
            for(int k=j,num=0;k<=V;k+=v,++num){
                int tmp=dp[k]-num*w;
                while(head<=tail&&tmp>=q1[tail]) --tail;
                q1[++tail]=tmp;
                q2[tail]=num;
                while(num-q2[head]>s) ++head;
                dp[k]=q1[head]+num*w;
            }
        }
    }
    printf("%d\n",dp[V]);
    return 0;
}
View Code

五. 二维费用的背包问题

题目链接:https://www.acwing.com/problem/content/8/

题意:在01背包的基础上增加背包的最大承重M,每个物品有3个属性v,m,w,即其体积、重量和价值,每个物品只有1件。

思路:思路与01背包一样,仅需增加一维,用dp[i][j]表示空间为i、承重为j的背包的最大价值,然后加一层表示重量的循环即可,V和M均从大到小遍历。复杂度O(nVM)。

AC代码:

#include<cstdio>
#include<algorithm>
using namespace std;

const int maxv=105;
int n,V,M,dp[maxv][maxv];

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;
}
View Code

同理,二维费用的背包问题的完全背包和多重背包的求解也与前面类似。


六. 分组背包

题目链接:https://www.acwing.com/problem/content/9/

题意: n组物品,背包容积为V。每组物品有s[i]个物品,每组中最多选1个物品。问最大价值。

思路:与多重背包类似,只是每一组最多选一个物品,用3层循环即可。复杂度为O(nVs),没有优化方案。

AC代码:

#include<cstdio>
#include<algorithm>
using namespace std;

const int maxn=105;
int n,V,s[maxn],v[maxn][maxn],w[maxn][maxn],dp[maxn];

int main(){
    scanf("%d%d",&n,&V);
    for(int i=1;i<=n;++i){
        scanf("%d",&s[i]);
        for(int j=1;j<=s[i];++j)
            scanf("%d%d",&v[i][j],&w[i][j]);
    }
    for(int i=1;i<=n;++i)
        for(int j=V;j>=0;--j)
            for(int k=1;k<=s[i];++k)
                if(j>=v[i][k])
                    dp[j]=max(dp[j],dp[j-v[i][k]]+w[i][k]);
    printf("%d\n",dp[V]);
    return 0;
}
View Code

七. 有依赖的背包问题

题目链接:https://www.acwing.com/problem/content/description/10/

题意:有n个物品,背包容量为V。每个物品有3个属性,体积、价值和父结点。并规定如果选一个结点,必须选它的父结点。

思路:树上背包问题,树形dp+分组背包。dp[u][j]表示对于以u为根的子树来说,容量为j的背包的最大价值。假设v1,v2,v3是u的3个子结点,那么相当于有3组,每一组最多选择一种方案。通过递归得到所有dp[v1][j]的最大值,对v2,v3同理。那么转移方程为dp[u][j]=max(dp[u][j],dp[u][j-k]+dp[v1][k]),其中0<=k<=j,表示在子树v1中选择的总体积为k。

需要注意的是,我们选择了子节点,就必须选择当前节点,那么最后需要把父节点的位置空出来。(把所有已算完的体积更新一下,在里面加上父节点这一物品)描述有些抽象,详见代码吧。

复杂度O(nV^2)。

AC代码:

#include<cstdio>
#include<algorithm>
using namespace std;

const int maxn=105;
int n,V,cnt,root,head[maxn],v[maxn],w[maxn],dp[maxn][maxn];

struct node{
    int v,nex;
}edge[maxn];

void adde(int u,int v){
    edge[++cnt].v=v;
    edge[cnt].nex=head[u];
    head[u]=cnt;
}

void dfs(int u){
    for(int i=head[u];i;i=edge[i].nex){
        int v=edge[i].v;
        dfs(v);
        for(int j=V;j>=0;--j)
            for(int k=0;k<=j;++k)
                dp[u][j]=max(dp[u][j],dp[u][j-k]+dp[v][k]);
    }
    for(int j=V;j>=0;--j)
        if(j>=v[u]) dp[u][j]=dp[u][j-v[u]]+w[u];
        else dp[u][j]=0;
}

int main(){
    scanf("%d%d",&n,&V);
    for(int i=1;i<=n;++i){
        int u;
        scanf("%d%d%d",&v[i],&w[i],&u);
        if(u==-1) root=i;
        else adde(u,i);
    }
    dfs(root);
    printf("%d\n",dp[root][V]);
    return 0;
}
View Code

八. 背包问题求方案数

题目链接:https://www.acwing.com/problem/content/description/11/

题意:在01背包的基础上询问取得最大价值的方案数。

思路:只需要添加一个数组num[j],表示容量为j的背包取到最优解时的方案数,num数组初始化为1。复杂度O(nV)。

AC代码:

#include<cstdio>
#include<algorithm>
using namespace std;

const int maxn=1005;
const int MOD=1e9+7;
int n,V,dp[maxn],num[maxn];

int main(){
    scanf("%d%d",&n,&V);
    for(int i=0;i<=V;++i)
        num[i]=1;
    for(int i=1;i<=n;++i){
        int v,w;
        scanf("%d%d",&v,&w);
        for(int j=V;j>=v;--j){
            if(dp[j]<dp[j-v]+w){
                dp[j]=dp[j-v]+w;
                num[j]=num[j-v];
            }
            else if(dp[j]==dp[j-v]+w){
                num[j]+=num[j-v];
                num[j]%=MOD;
            }
        }
    }
    printf("%d\n",num[V]);
    return 0;
}
View Code

九. 背包问题求具体方案

题目链接:https://www.acwing.com/problem/content/12/

题意:01背包,输出字典序最小的最优方案。

思路:首先我们从n到1号物品遍历,保证序号小的优先选择,从而保证字典序最小。然后从1到n遍历确定i是否被选。复杂度O(nV)。

AC代码:

#include<cstdio>
#include<algorithm>
using namespace std;

const int maxn=1e3+5;
int n,V,v[maxn],w[maxn],dp[maxn][maxn],ans[maxn];

int main(){
    scanf("%d%d",&n,&V);
    for(int i=1;i<=n;++i)
        scanf("%d%d",&v[i],&w[i]);
    for(int i=n;i>=1;--i)
        for(int j=V;j>=0;--j)
            if(j>=v[i]) dp[i][j]=max(dp[i+1][j],dp[i+1][j-v[i]]+w[i]);
            else dp[i][j]=dp[i+1][j];
    int tmp=V;
    for(int i=1;i<=n;++i)
        if(tmp>=v[i]&&dp[i][tmp]==dp[i+1][tmp-v[i]]+w[i]){
            printf("%d ",i);
            tmp-=v[i];
        }
    printf("\n");
    return 0;
}
View Code

猜你喜欢

转载自www.cnblogs.com/FrankChen831X/p/11423350.html