• 背包九讲,写给自己 (模板)


    之前学了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
    ",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
    ",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
    ",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
    ",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
    ",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
    ",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
    ",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
    ",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
    ",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
    ",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("
    ");
        return 0;
    }
    View Code
  • 相关阅读:
    物理好题随想
    学案12:电场强度和静电现象
    vscode插件记录
    windows使用总结
    元素周期律 + 元素周期表
    酸碱理论
    氮族元素——磷
    氮族元素——氮
    碱金属元素
    SDN第一次上机实验
  • 原文地址:https://www.cnblogs.com/FrankChen831X/p/11423350.html
Copyright © 2020-2023  润新知