zoukankan      html  css  js  c++  java
  • 背包九讲

    《背包九讲》

    前言:

           背包问题:有n件物品,每件物品有一定的价值,获取每件物品都需要一定的代价,背包问题就是在遵守一定的规则的情况下,获取最高的价值。

    1,01背包

           最基本的背包问题,其规则为每件物品要么选,要么不选。

           定义状状态数组dp[i][j]表示前i个物品,当背包的容量为j时,背包可以容纳的最大价值。第i件物品可以不选可以选,如果不选的话,就相当于从当背包容量为j的时候,前i-1件物品里价值最大的物品。也就是说dp[i][j]=dp[i-1][j]。如果选择的话,(设物品i所占的容量为w[i])从背包中拿出w[i]的容量用来装物品i,那么前i-1件物品所占用的容量就为j-w[i]。那么dp[i][j]=dp[i-1][j-w[i]]+v[i](v[i]表示的是物品i的价值)

          

    关于初始化。这里的初始化有一些讲究。和我们如何定义数组dp[i][j]的含义有关。假如说我们定义dp[i][j]为钱i件物品,当背包容量为j时可获得的价值,那么开始时候dp[i][j]全部初始化为0就可以了,但是如果问的是前i件物品,容量为j时,恰好可以装多少,也就是说我们必须装满,如果不能装满的话,价值再大也没有用。 这两种定义有什么区别呢?看个例子,设有n件物品, 并且可获得的最大价值为x,需要的背包容量为k,而你的背包总容量为k+1。对于第一种情况dp[n][k+1]=dp[n][k]=x,但是对于第二种情况就不一定了,因为我们要求必须装满才行。

          

           Code:

    1.    for(int i=1;i<=n;i++){  
    2.        for(int j=0;j<=m;j++){  
    3.            if(j>=v[i]) dp[i][j]=max(dp[i][j],dp[i-1][j-v[i]]+w[i]);  
    4.            dp[i][j]=max(dp[i][j],dp[i-1][j]);  
    5.        }  
    6.    }  
    7.    return dp[n][m]; 

    01背包的空间优化

    01背包可以从二维优化为一维的背包,当然可以直接通过滚动数组来降维,但还有一更优的优化方式,定义dp[i]为背包容量为i时可获取的最大的价值。如果不选的话,还是考虑到第i个物品。当容量为j时,此时的最大价值为dp[j],注意我们这里是考虑到第i个物品,所以这里的dp[j]的最大价值应该是前i-1个物品,背包容量为j时的最大价值。所以如果我们不选第i个物品的话dp[j]直接不用做任何操作,如果选的话dp[j-weight[i]]+value[i]

    但是这里就出现了一个问题,该怎么保证dp[j-weight[i]]是前i-1个物品的最大价值呢?可以倒着从m到weight[i]进行遍历。(挺难想的)

    1.        for(int i=1;i<=n;i++){  
    2.        for(int j=m;j>=weight[i];j--)  
    3.            dp[j]=max(dp[j],dp[j-weight[i]]+value[i]);  
    4.    }  
    5.    return dp[m]; 

    2,完全背包

           规则:每件物品可以选择任意次数。

            完全背包和01背包的区别就是完全背包没件物品可以选择任意次,在代码上和01背包的区别也是比较小的,只需要再加一个for,看当前物品选择多少个才是最合适的。这种做法比较简单直观,但是时间复杂度是O(n^3),空间复杂度是O(n^2)

    1.        for(int i=1;i<=n;i++){  
    2.        for(int j=0;j<=m;j++)  
    3.            for(int k=0;k*v[i]<=j;k++){  
    4.                dp[i][j]=max(dp[i][j],dp[i-1][j-k*v[i]]+w[i]*k);  
    5.            }  
    6.    }
    7.    return dp[n][m];

     显然这种复杂度并不好,首先优化空间复杂度,可以把通过类似于01背包的方法把空间复杂度优化成一维的。再优化时间复杂度,假设考虑到第i件物品的容量为j时的状态,即dp[j],这里的dp[ j ]保存的值是前i-1件物品的最大价值。该怎么转移呢?因为这里对物品选择的个数是没有限制的,所以转移的时候,如果选择1个,那么就从i-1个物品进行转移,选择两个就从再转移一次。。。所以说转移的dp[j]可以是上一层的也可以是这一层的。,所以遍历的时候,体积正序遍历就可以了。

    1.        for(int i=1;i<=n;i++){  
    2.        for(int j=v[i];j<=m;j++)  
    3.            dp[j]=max(dp[j],dp[j-v[i]]+w[i]);  
    4.    }  

    3,多重背包

           规则:每件物品最多选k次

           多重背包也是以01背包为基础的背包问题,按照朴素的做法就是枚举该物品的个数,思路和完全背包的朴素枚举基本上是完全一样的,只需要加一个物品个数限制就可以了。

    1.        for(int i=1;i<=n;i++){  
    2.        for(int j=0;j<=m;j++)  
    3.            for(int k=0;k*v[i]<=j&&k<=num[i];k++){  
    4.                dp[i][j]=max(dp[i][j],dp[i-1][j-k*v[i]]+w[i]*k);  
    5.            }  
    6.    }
    7.    return dp[n][m];

    两种优化方法

    第一种是我比较喜欢的优化方法, 叫做二进制优化,二进制真的是一个特别神奇的东西。假如说num=10.通过二进制拆分,可以拆分为1+2+4+3。并且这四个数可以组成小于等于10的任意一个数字。

    因此我们可以把10个物品拆成四份,每一份为的数目为1,2,4,3。接下来就按照01背包处理就可以了。时间复杂度大约为o(m ) (m是容量,n是物品的个数,c[i]指的是物品i的个数)

    Code

    1.    int solve1(){  
    2.        int n,m;  
    3.        cin>>n>>m;  
    4.        int pos=0,x,y,z;   
    5.        for(int i=1;i<=n;i++){  
    6.            cin>>x>>y>>z;  
    7.            for(int j=1;z>=j;j<<=1){  
    8.                z-=j;  
    9.                volume[pos]=j*x;  
    10.                value[pos++]=j*y;  
    11.            }  
    12.            if(z>0) {  
    13.                value[pos]=z*y;   
    14.                volume[pos++]=z*x;  
    15.            }  
    16.        }  
    17.        for(int i=0;i<pos;i++){  
    18.            for(int j=m;j>=volume[i];j--){  
    19.                dp[j]=max(dp[j],dp[j-volume[i]]+value[i]);  
    20.            }  
    21.        }  
    22.        return dp[m];     
    23.    }  

    第二种优化是借用一种数据结构—单调队列优化,这种优化是比较难的,《男人八题》中就有一个单调队列优化的裸多重背包问题。

      待补…

    4,混合背包

                  将01背包,完全背包,多重背包三类背包放在一起。

           这类问题的解决方法就是属于哪种背包就按照哪种背包的方式算,这类问题算不上是新问题

           例题 https://www.acwing.com/problem/content/7/

           Code

    1.    int dp[N]; //dp[i]表示当容量为i时,可获取的最大价值   
    2.    int volume[N],value[N];//体积和价值   
    3.    bool mark[N];//标记哪个物品属于哪类背包问题 mark[i]=1 表示物品i为完全背包问题,0表示01背包问题   
    4.    int solve(){  
    5.        int n,m;  
    6.        cin>>n>>m;//n件物品,容量限制为m   
    7.        int x,y,z;  
    8.        int pos=0;  
    9.        for(int i=1;i<=n;i++){  
    10.            cin>>x>>y>>z;  
    11.            if(z==0){   
    12.                mark[pos]=1;  
    13.                volume[pos]=x;  
    14.                value[pos++]=y;  
    15.            }  
    16.            else if(z==-1){  
    17.                volume[pos]=x;  
    18.                value[pos++]=y;  
    19.            }  
    20.            else {//如果是多重背包问题,通过二进制拆分成01背包   
    21.                for(int j=1;z>=j;j<<=1){  
    22.                    z-=j;  
    23.                    volume[pos]=j*x;  
    24.                    value[pos++]=j*y;  
    25.                }  
    26.                if(z>0){  
    27.                    volume[pos]=z*x;  
    28.                    value[pos++]=z*y;  
    29.                }  
    30.            }  
    31.        }  
    32.        for(int i=0;i<pos;i++){  
    33.            if(mark[i]) {//完全背包正序,01背包逆序   
    34.                for(int j=volume[i];j<=m;j++)  
    35.                    dp[j]=max(dp[j],dp[j-volume[i]] + value[i]);  
    36.            }  
    37.            else{  
    38.                for(int j=m;j>=volume[i];j--){  
    39.                    dp[j]=max(dp[j],dp[j-volume[i]] + value[i]);  
    40.                }  
    41.            }  
    42.        }  
    43.        return dp[m];  
    44.    } 

    5,二维费用背包

           例题:https://www.acwing.com/problem/content/8/

                  这里的代价是二维的,比如说同时有重量和体积的限制。

                  定义状态数组dp[i][j][k]表示考虑到第i个物品,当背包的容量为j,体积为k时可以获得的最大价值。

           状态转移:考虑到第i个物品,如果选的话:

    dp[i][j][k]=dp[i-1][j-volume[i]][k-weight[i]]+value[i];

           如果不选的话直接就dp[i][j][k]=dp[i-1][j][k];

           我们可以对空间进行优化,类似于01背包的优化,可以优化到二维.

    1.    int dp[N][N];  
    2.    int volume[N],weight[N],value[N];  
    3.    int solve(){  
    4.        int n,m,v;  
    5.        cin>>n>>v>>m;  
    6.        for(int i=1;i<=n;i++)  
    7.            cin>>volume[i]>>weight[i]>>value[i];  
    8.        for(int i=1;i<=n;i++){  
    9.            for(int j=m;j>=weight[i];j--){  
    10.                for(int k=v;k>=volume[i];k--){  
    11.                    dp[j][k]=max(dp[j][k],dp[j-weight[i]][k-volume[i]]+value[i]);  
    12.                }  
    13.            }  
    14.        }  
    15.        return dp[m][v];  
    16.    }  

    6,分组背包

    在一个组内,只能选择一类物品,该类物品的选择可以满足01背包,完全背包或者多重背包的原则。

    例题:https://www.acwing.com/problem/content/9/

    在01背包的基础上,对物品进行分组,每个组最多选择一个物品。定义状态dp[i][j]为考虑到第i组,当前背包容量为j时的最大价值。状态转移也比较简单,如果选择,直接枚举第i组的所有状态,dp[i][j]=dp[i-1][j-volume[i]]+value[i],如果不选的dp[i][j]=dp[i-1][j]。所以该类问题较01背包问题只是增加了一个for,用来枚举每个组的物品。

    空间优化后的Code:

    1.    int s[N], v[N][N], w[N][N];//分别表示每一组的个数,v[i][j]表示第i组第j个的volume,w[i][j]表示的是....value   
    2.    int dp[N];  
    3.    int solve(){  
    4.        int n,m;  
    5.        cin >> n >> m;  
    6.        for (int i = 1; i <= n; ++i) {  
    7.            cin >> s[i];  
    8.            for (int j = 1; j <= s[i]; ++j) {  
    9.                cin >> v[i][j] >> w[i][j];  
    10.            }  
    11.        }  
    12.    
    13.        for (int i = 1; i <= n; ++i) {  
    14.            for (int j = m; j >= 0; --j) {  
    15.                for (int k = 1; k <= s[i]; ++k) {  
    16.                    if (j >= v[i][k])  
    17.                      dp[j] = max(dp[j], dp[j - v[i][k]] + w[i][k]);  
    18.                }  
    19.            }  
    20.        }  
    21.        return dp[m];  
    22.    }  

    7,背包问题的具体方案

           该类问题可以归结为对背包问题的路径的记录。  

           对于这类问题有两种方法来解决。  

    1   我们可以用一个数组path[i][j]来记录当第i个物品当容量为j时的选择。以01背包为例子。考虑到第i个物品,当背包容量为j的时候,第i个物品如果选的话,

    dp[j]<dp[j-volume[i]]+value[i], path[i][j]=1

    否则的话,第i个物品可以不选,

    假设有n个物品,背包的总容量为m,那么最终的状态一定dp[n][m],如果第n个物品选了的话,path[n][m]=1,那么上一个状态就是path[n-1][m-volume[n]]…如果没选的话, 上一个状态为path[n-1][m].

    如果是完全背包的话,如果第n个物品选了的话,上一个状态就是path[n][m-volume[i]],这里是n,不是n-1,因为第n个物品可能选了多次….

    完全背包---code:

    for (int i = 1; i <= n; i++)  
        for (int j = w[i]; j <= m; j++)  
           if (dp[j] < dp[j - w[i]] + v[i]){   
               dp[j] = dp[j - w[i]] + v[i];  
               path[i][j]=1;//表示当容量为j的时候选择了第i个物品。   
            }  
      
    int i=n,j=m;  
    while(i>=1&&j){  
        if(path[i][j]){  
            cout<<i<<" ";  
            j-=w[i];  
        }  
        else i--;  
    }  

    多重背包---code:

    for (int i = 1; i <= n; i++)  
        for (int k = 1; k <= cnt[i]; k++)  
            for (int j = 10; j >= w[i]; j--)  
                if (dp[j] < dp[j - w[i]] + v[i]){  
                    dp[j] = dp[j - w[i]] + v[i];  
                    path[i][j] = 1;  
                }  
    int i=n,j=m;  
    while(i>=1&&j){  
        if(path[i][j]&&cnt[i]){  
            cnt[i]--;  
            j-=w[i];  
            cout<<i<<" ";  
        }  
        else i--;  
    }  

     2 如果第i个物品可以被选,那么它一定满足两个条件,首先是背包要有足够多的容量,然后是满足dp[i-1][j]<=dp[i-1][j-weight[i]]+value[i],即选第i个物品可以获得更高的利益,我们可以根据这一原则来寻找符合条件的路径。

    Code :

    ACwing上的一个例题 :https://www.acwing.com/problem/content/12/

    for(int i=n;i>=1;i--){  
        for(int j=m;j>=0;j--){  
            if(j>=weight[i]) dp[i][j]=max(dp[i+1][j],dp[i+1][j-weight[i]]+value[i]);  
            else dp[i][j]=dp[i+1][j];  
        }  
    }  
      
    for(int i=1;i<=n&&m>=0;i++){  
        if(weight[i]<=m&&dp[i+1][m]<=dp[i+1][m-weight[i]]+value[i]){  
            m-=weight[i];  
            cout<<i<<" ";  
        }  
    }

    8,背包问题的方案数。

    这类问题又可以分为两种,第一种是当获得最大价值时的方案数目,第二种是当装有一定体积时的方案数目。

    1、定义状态数组dp[i]表示当容量为i时可以获得的最大价值,cnt[i]当容量为i时且获得最大价值的方案数。

    首先是初始化,dp[i](0àm)=0,和01背包初始化是一样的,cnt[i]的初始化全部为1,表示不装任何物品。然后状态转移,当dp[j]=dp[j-weight[i]]+value[i]的时候,此时选与不选获得的价值都是一样的,所有cnt[j]=cnt[j]+cnt[j-weight[i]],当后者大的时候,cnt[j]=cnt[j-weight[i]],否则cnt[j]=cnt[j].

    Code (01背包为例)

    for(int i=1;i<=n;i++) cin>>weight[i]>>value[i];  
    for(int i=0;i<=m;i++) cnt[i]=1;  
      
    for(int i=1;i<=n;i++){  
        for(int j=m;j>=weight[i];j--){  
            if(dp[j]==dp[j-weight[i]]+value[i]){  
                cnt[j]=(cnt[j]+cnt[j-weight[i]])%mod;  
            }  
            else if(dp[j]>dp[j-weight[i]]+value[i]){  
                cnt[j]=cnt[j]%mod;  
            }  //这一步可以省略
            else {  
                dp[j]=dp[j-weight[i]]+value[i];  
                cnt[j]=cnt[j-weight[i]]%mod;  
            }  
        }  
    }  
    cout<<cnt[m]%mod<<endl;  

    2、定义状态数组dp[i]表示当装有容量为i的物品时的方案数目,初始化的时候,dp[0]=1.其他的都是0.

    状态转移方程dp[j]=dp[j]+dp[j-weight[i]].

    Code :(01背包为例)

    for(int i=1;i<=n;i++){  
        for(int j=m;j>=weight[i];j--){  
            dp[j]=dp[j]+dp[j-weight[i]];  
        }  
    } 

    9,有依赖性的背包问题

           当选择一件物品的时候,必须执行另外一种规则。

    有依赖的背包问题,也可以说是树形背包问题,这类问题是比较难的,我学的也不扎实,唉,算了,放俩例题吧~。

    推荐几个例题:https://www.luogu.com.cn/problem/P2014  (洛谷 选课)

    Code :

    #include<bits/stdc++.h>  
    using namespace std;  
    const int N=300+7;  
    struct stu  
    {  
        int nxt,to;  
    }edge[N];  
    int n,m;  
    int head[N],tol=1;  
    int value[N];  
    int volume[N];  
    void add(int u,int v){  
        edge[tol].to=v;  
        edge[tol].nxt=head[u];  
        head[u]=tol++;  
    }  
    int dp[N][N];  
    void dfs(int u){  
        // cout<<u<<endl;  
        for(int i=head[u];i;i=edge[i].nxt){  
            int y=edge[i].to;  
            dfs(y);  
            for(int j=m-volume[u];j>=0;j--){  
                for(int i=0;i<=j;i++){  
                    dp[u][j]=max(dp[u][j],dp[u][j-i]+dp[y][i]);  
                }  
            }  
        }  
        for(int i=m;i>=volume[u];i--){  
            dp[u][i]=dp[u][i-volume[u]]+value[u];  
        }  
        dp[u][0]=0;  
    }  
    int main(int argc, char const *argv[]){  
          
        cin>>n>>m;  
        int s;  
        for(int i=1;i<=n;i++){  
            cin>>s>>value[i];  
            add(s,i);  
            volume[i]=1;  
        }  
        dfs(0);  
        cout<<dp[0][m]<<endl;  
      
        return 0;  
    }  

     

    ACwing10. 有依赖的背包问题https://www.acwing.com/problem/content/10/

    #include<bits/stdc++.h>  
    using namespace std;  
    const int N=1e2+7;  
    struct stu{  
        int to,nxt;  
    }edge[N];  
    int head[N],tol=1,n,m;  
    void add(int u,int v){  
        edge[tol].to=v;   
        edge[tol].nxt=head[u];  
        head[u]=tol++;  
    }  
    int volume[N],value[N];  
    int dp[N][N];  
    int mark[N];   
    void dfs(int x){   
        for(int i=head[x];i;i=edge[i].nxt){  
            int y=edge[i].to;   
            dfs(y);  
            for(int j=m-volume[x];j>=0;j--){  
                for(int k=0;k<=j;k++){  
                    dp[x][j]=max(dp[x][j],dp[x][j-k]+dp[y][k]);  
                }  
            }  
        }  
        for(int i=m;i>=volume[x];i--)  
            dp[x][i]=dp[x][i-volume[x]]+value[x];  
        for (int i = 0; i < volume[x]; i++) dp[x][i] = 0;  
    }  
    int main(int argc, char const *argv[]){  
        cin>>n>>m;  
        int z,root;  
        for(int i=1;i<=n;i++){  
            cin>>volume[i]>>value[i]>>z;  
            if(z==-1) root=i;  
            else add(z,i);    
        }  
        dfs(root);  
        cout<<dp[root][m]<<endl;  
        return 0;  
    } 

     

  • 相关阅读:
    实数的构造
    实数的构造
    某曲线上的点到两点距离和最小的问题都可以用做椭圆解决
    Java 性能优化之 String 篇
    使用 Spring Data JPA 简化 JPA 开发
    使用 Sonar 进行代码质量管理
    Servlet运行周期与原理流程
    使用 Java 配置进行 Spring bean 管理
    通过日志监控并收集 Java 应用程序性能数据
    基于 JUnit 的全局单元测试程序
  • 原文地址:https://www.cnblogs.com/Accepting/p/13539104.html
Copyright © 2011-2022 走看看