动态规划入门——背包问题(01 完全 多重)

一 背包问题

(1)01 背包 :

给定 n 种物品和一个容量为 C 的背包,物品 i 的重量是 wi,其价值为 vi 。

问:应该如何选择装入背包的物品,使得装入背包中的物品的总价值最大?

分析:对于每个物品,我们都有两种选择,取和不取。

我们可以定义一个二维数组dp[i][j],表示有i件物品,背包容量为j时获得的最大价值。

对于dp[i][j],当w[i]>j时,dp[i][j] = dp[i-1][j];否者,dp[i][j] = max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);再放与不放中选着价值        最大的。得状态转移方程:

       

if (背包体积j小于物品i的体积)
    f[i][j] = f[i-1][j] //背包装不下第i个物体,目前只能靠前i-1个物体装包
else
    f[i][j] = max(f[i-1][j], f[i-1][j-Wi] + Vi)

我们可以把这个过程看成填一个表

例如:

价值数组v = {8, 10, 6, 3, 7, 2},

重量数组w = {4, 6, 2, 2, 5, 1},

背包容量C = 12时对应的dp[i][j]数组。

0 1 2 3 4 5 6 7 8 9 10 11 12
1 0 0 0 8 8 8 8 8 8 8 8 8
2 0 0 0 8 8 10 10 10 10 18 18 18
3 0 6 6 8 8 14 14 16 16 18 18 24
4 0 6 6 9 9 14 14 17 17 19 19 24
5 0 6 6 9 9 14 14 17 17 19 21 24
6 2 6 8 9 11 14 16 17 19 19 21 24
(第一行和第一列为序号,其数值为0)
如m[2][6],在面对第二件物品,背包容量为6时我们可以选择不拿,那么获得价值仅为第一件物品的价值8,如果拿,就要把第一件物品拿出来,放第二件物品,价值10,那我们当然是选择拿。m[2][6]=m[1][0]+10=0+10=10;依次类推,得到m[6][12]就是考虑所有物品,背包容量为C时的最大价值。

伪代码:

     int w[maxn] = {0,1,2,3,4,5...};
     int v[maxn] = {0,5,4,3,2,1...};
     memset(dp,0,sizeof(dp));
     for(int i = 1 ; i <= n ;i ++ ){
        for(int j = 1 ; j < = sum_w ; j ++ ){
            if(j<w[i]) dp[i][j] = dp[i-1][j];
            else dp[i][j] = max(dp[i-1][j],dp[i-1][dp[i-1][j-w[i]]]+v[i]);
        } 
     }
     cout<<dp[n][sum_w]<<endl;

当数据量大时,这种二维数组可能就不适用了,我们可以采用滚动数组的方法。

先给出代码:

    int w[maxn] = {0,1,2,3,4,5...};
    int v[maxn] = {0,5,4,3,2,1...};
    memset(dp,0,sizeof(dp));  
    for (int i = 1;i <= n;i++) 
    {  
        for (int j = sum_w;j >= wi;j--)  
        {  
            dp[j] = max(dp[j],dp[j - w[i]] + v[i]);  
        }  
    }  
    cout<<dp[sum_w]<<endl; 

代码分析:

dp数组是从上到下,从右到左计算的,再计算(i,j)的时候,dp[j]里保存的就是dp(i-1,j)的值,而dp[j-w]里保存的是

dp(i-1,j-w)而不是dp(i,j-w)——因为dp是逆序枚举的,此时的dp(i,j-w)还没有算出来。这样,

dp[j] = max(dp[j],dp[j - w[i]] + v[i])实际上是把max(dp(i-1,j),dp(i-1,j-w))保存在dp[j]中,覆盖掉dp[j]原来的dp(i-1,j);


(2)完全背包

有N种物品和一个容量为V的背包。第i种物品有若干件可用,每件费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。

分析:这个问题是01背包的升级版,不同的地方是一种物品可以放很多件,所以在01背包两重循环的前提下,在增加一重循环用来表示取的件数

状态转移方程:

dp[i][j]=max(dp[i][j],dp[i-1][j-k*w[i]]+k*v[i]);

伪代码:

     int w[maxn] = {0,1,2,3,4,5...};
     int v[maxn] = {0,5,4,3,2,1...};
     memset(dp,0,sizeof(dp));
     for(int i = 1 ; i <= n ;i ++ ){
        for(int j = 1 ; j < = sum_w ; j ++ ){
            if(c[i]<=j)
                for(int k = 0 ; k*w[i]<=j ;k ++ )
                dp[i] = max(dp[i-1][j],dp[i-1][j-k*w[i]]+k*v[i]);
            else
                dp[i] = dp[i-1][j];
        } 
     }
     cout<<dp[n][sum_w]<<endl;

对于完全背包的滚动数组写法,影响dp[j]的是当前i种,而不是前i-1种,所以需要正序

    int w[maxn] = {0,1,2,3,4,5...};
    int v[maxn] = {0,5,4,3,2,1...};
    memset(dp,0,sizeof(dp));  
    for (int i = 1;i <= n;i++) 
    {  
        for (int j = w[i];j <= sum_w ; j++)  
        {  
            dp[j] = max(dp[j],dp[j - w[i]] + v[i]);  
        }  
    }  
    cout<<dp[sum_w]<<endl; 

多重背包

分析:多重背包可以成01背包和完全背包的结合,我们可以将相同的物品看不不同的物品,然后看作是01背包进行求解

代码:

 for(int i=1; i<=n; i++)//每种物品  
        for(int k=0; k<num[i]; k++)//其实就是把这类物品展开,调用num[i]次01背包代码  
            for(int j=m; j>=weight[i]; j--)//正常的01背包代码  
                dp[j]=max(dp[j],dp[j-weight[i]]+value[i]);  

这其实和完全背包的代码一样。。。。真神奇。。

over!

猜你喜欢

转载自blog.csdn.net/insist_77/article/details/80260852
今日推荐