背包九讲详解

背包九讲详解背包九讲详解0-1背包问题完全背包问题多重背包问题

大家好,又见面了,我是你们的朋友全栈君。

#背包九讲详解

##0-1背包问题
有n个重量和价值分别为 w i , v i w_i, v_i wi,vi 的物品。从这些物品中挑选出总重量不超过 W W W 的物品, 求所有挑选方案中价值总和的最大值。

样例:

n = 4 ( w , v ) = { ( 2 , 3 ) , ( 1 , 2 ) , ( 3 , 4 ) , ( 2 , 2 ) } W = 5 n = 4\\ (w, v) = \{(2,3), (1,2), (3,4), (2,2)\}\\ W = 5 n=4(w,v)={
(2,3),(1,2),(3,4),(2,2)}
W=5

n个物品,每种物品只有两种选择,放进背包,或者不放进背包。n个物品对应的,最大的所有可能的总数为 2 n 2^n 2n 种不同的放法。
最朴素的,我们可以枚举所有可能的放法,找到最大值。

R e c ( n , W ) Rec(n, W) Rec(n,W) 表示剩余容量为 W W W 的背包,还有 1 ~ n 1~n 1n n n n 个物品可以选择, 所能取得的最大的背包价值。

还剩 i i i 个物品可以选, 背包容量剩余 j j j 的时候,可以分为两种情况:

  1. i i i 个物品不选,只考虑前 i − 1 i-1 i1 个物品。则 R e c ( i , j ) = R e c ( i − 1 , j ) Rec(i, j) = Rec(i-1, j) Rec(i,j)=Rec(i1,j) ,此时背包容量j不变
  2. i i i 个物品被选择, 接下来要考虑的就是, 如何在前 i − 1 i-1 i1 个物品中选择物品放入容量为 j − w [ i ] j-w[i] jw[i] 的背包, 则 R e c ( i , j ) = R e c ( i − 1 , j − w [ i ] ) + v [ i ] Rec(i, j) = Rec(i-1, j-w[i]) + v[i] Rec(i,j)=Rec(i1,jw[i])+v[i]

总和两种可能,取较大值。可以给出递推式:
R e c ( i , j ) = m a x ( R e c ( i − 1 , j ) , R e c ( i − 1 , j − w [ i ] ) + v [ i ] ) Rec(i, j) = max(Rec(i-1, j), Rec(i-1, j-w[i])+v[i]) Rec(i,j)=max(Rec(i1,j),Rec(i1,jw[i])+v[i])
还需要考虑一些细节:

  1. 终止条件, i = 0 i = 0 i=0 , 无物品可选 R e c ( 0 , j ) = 0 Rec(0, j) = 0 Rec(0,j)=0
    0个物品可以选择,放入容量为j的背包, 得到的最大价值只能为0
  2. j < w[i],背包剩余容量不足以放下第i个物品, R e c ( i , j ) = R e c ( i − 1 , j ) Rec(i, j) = Rec(i-1, j) Rec(i,j)=Rec(i1,j)

综上,得到递推式:
R e c ( i , j ) = { 0 i = 0 R e c ( i − 1 , j ) j < w [ i ] m a x ( R e c ( i − 1 , j ) , R e c ( i − 1 , j − w [ i ] ) + v [ i ] ) o t h e r Rec(i, j) = \begin{cases} 0 & i = 0\\ Rec(i-1, j) & j < w[i]\\ max(Rec(i-1, j), Rec(i-1, j-w[i])+v[i]) & other \end{cases} Rec(i,j)=0Rec(i1,j)max(Rec(i1,j),Rec(i1,jw[i])+v[i])i=0j<w[i]other
###递归方法:

#include <iostream>
#define MAXN 10000
using namespace std;

int w[MAXN] = { 
   0, 2, 1, 3, 2};
int v[MAXN] = { 
   0, 3, 2, 4, 2};
int W = 5, n = 4;

int Rec(int i, int j) { 
   
    int res;
    if (i == 0) { 
   
        // 终止条件, 无物品可选 Rec(0, j) = 0
        // 0个物品可以选择,放入容量为j的背包, 得到的最大价值只能为0
        res = 0;
    }
    else if (j < w[i]) { 
   
        // 背包剩余容量不足以放下第i个物品
        res = Rec(i-1, j);
    }
    else { 
   
        // 抉择,第i个物品选或者不选,都试一下,取较大值
        res = max(Rec(i-1, j), Rec(i-1, j-w[i]) + v[i]);
    }
    return res;
}

int main() { 
   
    cout << Rec(n, W) << endl;
    return 0;
}

大概画个递归过程:

                                        Rec(4, 5)
                               /                        \
                      Rec(3, 3)                             Rec(3, 5)
                    /        \                            /            \
            Rec(2, 0)     Rec(2, 3)              Rec(2, 2)             Rec(2, 5)  
            /             /     \                /     \                /       \
      Rec(1, 0)     Rec(1, 2)  Rec(1, 3)   Rec(1, 1)    Rec(1, 2)   Rec(1, 4)    Rec(1, 5)
       /          /    \         /    \         /       /    \      /      \      /     \
   (0,0)     (0,0)   (0,2)   (0,1)    (0,3)  (0,1)  (0,0)   (0,2) (0,2)   (0,4) (0,3)  (0,5)

从上图可以看出,我们枚举了所有可能的情况,即对于每个物品i,物品i选还是不选,我们都考虑了一遍。递归的层数是n+1层,最后一层是判定终止。

再来重申一下 R e c ( i , j ) Rec(i, j) Rec(i,j) 的含义,表示有编号为1~ i i i 的这前 i i i 个物品可以选择,放入容量为 j j j 的背包,所能达到的最大价值。终止条件是 i = 0 i = 0 i=0,时,无物品可选, R e c ( 0 , j ) = 0 Rec(0, j) = 0 Rec(0,j)=0 ,题目要求的是 R e c ( n , W ) Rec(n, W) Rec(n,W)

在求解 R e c ( n , W ) Rec(n, W) Rec(n,W) 的过程中,有些情况可能会被重复计算。比如上图 R e c ( 1 , 2 ) Rec(1,2) Rec(1,2) 在第四行计算了2次。这种重复计算是随着 n n n 的增大,指数级增长的,所以 n , W n, W n,W 较大时,问题就是不可解的。时间和空间复杂度太高 ,为 O ( 2 n ) O(2^n) O(2n)

###记忆化搜索
我们可以利用递归求解过程中的重复计算。如果 R e c ( i , j ) Rec(i, j) Rec(i,j) 已经计算过,则记录下来,下次需要的时候直接拿来用即可。

#include <iostream>
#include <cstring>
#define MAXN 1000
using namespace std;

int w[MAXN] = { 
   0, 2, 1, 3, 2};
int v[MAXN] = { 
   0, 3, 2, 4, 2};
int dp[MAXN][MAXN]; //记录搜索过的结果
int W = 5, n = 4;

int Rec(int i, int j) { 
   
    //Rec(i, j)计算过,直接拿来用
    if (dp[i][j] != -1) return dp[i][j];

    int res;
    if (i == 0) { 
   
        res = 0;
    }
    else if (j < w[i]) { 
   
        res = Rec(i-1, j);
    }
    else { 
   
        res = max(Rec(i-1, j), Rec(i-1, j-w[i]) + v[i]);
    }
    return dp[i][j] = res; //记录
}

int main() { 
   
    memset(dp, -1, sizeof(dp));
    cout << Rec(n, W) << endl;
    return 0;
}

对于任意的 i , j , R e c ( i , j ) i, j, Rec(i,j) i,j,Rec(i,j) 只会计算一次,所以复杂度为 O ( n W ) O(nW) O(nW)

###动态规划
上面的递归过程,是把大问题分解成小问题,最后由小问题的解合并成大问题的解。

动态规划的方法,就是先把小问题的解计算好,存在表里,等计算大问题的时候,需要用到小问题的解,就过来查一下表。

由递推式:
R e c ( i , j ) = { 0 ( i = 0 ) R e c ( i − 1 , j ) ( j < w [ i ] ) m a x ( R e c ( i − 1 , j ) , R e c ( i − 1 , j − w [ i ] ) + v [ i ] ) ( o t h e r ) Rec(i, j) = \begin{cases} 0 & (i = 0)\\ Rec(i-1, j) & (j < w[i])\\ max(Rec(i-1, j), Rec(i-1, j-w[i])+v[i]) & (other) \end{cases} Rec(i,j)=0Rec(i1,j)max(Rec(i1,j),Rec(i1,jw[i])+v[i])(i=0)(j<w[i])(other)
转化为动态规划的写法:
d p [ i ] [ j ] = { 0 ( i = 0 ) d p [ i − 1 ] [ j ] ( j < w [ i ] ) m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − w [ i ] ] + v [ i ] ) ( o t h e r ) dp[i][j] = \begin{cases} 0 & (i = 0)\\ dp[i-1][j] & (j<w[i])\\ max(dp[i-1][j], dp[i-1][j-w[i]] + v[i]) & (other) \end{cases} dp[i][j]=0dp[i1][j]max(dp[i1][j],dp[i1][jw[i]]+v[i])(i=0)(j<w[i])(other)
代码:

#include <iostream>
#include <cstring>
#define MAXN 1000
using namespace std;

int dp[MAXN][MAXN];
int w[MAXN] = { 
   0, 2, 1, 3, 2};
int v[MAXN] = { 
   0, 3, 2, 4, 2};
int W = 5, n = 4;

int solve(int n, int W) { 
   
    memset(dp, 0, sizeof(dp));
    for (int i = 1; i <= n; i++) { 
    //i从1开始,因为i=0的值已经确定为0
        for (int j = 0; j <= 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][j-w[i]] + v[i]);
            }
        }
    }
    return dp[n][W];
}

int main() { 
   
  cout << solve(n, W) << endl;
  return 0;
}

dp 优化空间复杂度

输出一下上面的二维数组 d p [ ] [ ] dp[][] dp[][]

0 0 0 0 0 0

0 0 3 3 3 3

0 2 3 5 5 5

0 2 3 5 6 7

0 2 3 5 6 7

我们是填 d p dp dp 表的顺序是从上到下,从左到右,从递归式可以看出, d p [ i ] [ j ] dp[i][j] dp[i][j] 是由 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i1][j] d p [ i − 1 ] [ j − w [ i ] ] dp[i-1][j-w[i]] dp[i1][jw[i]] 的值推来的。填写第 i i i行的值,只依赖于上一行,第 i − 1 i-1 i1行的值。下次填写第 i + 1 i+1 i+1 行的时候,只会用到第 i i i 行的值,第 i − 1 i-1 i1 行的值以后都不会再用到了。

所以我们可以从这里入手优化空间复杂度。没有必要存下整个二维数组,我们只需要存2行,然后不断更新这2行就可以了。

实际上, d p [ i ] [ j ] dp[i][j] dp[i][j] 的值只依赖于第 i − 1 i-1 i1 行的 d p [ i − 1 ] [ 0… j ] dp[i-1][0…j] dp[i1][0...j] 这前 j + 1 j+1 j+1 个元素, 与 d p [ i − 1 ] [ j + 1… W ] dp[i-1][j+1 … W] dp[i1][j+1...W] 的值无关。

所以,我们可以只存1行,就能完成整个 d p dp dp过程。用 d p [ 0… W ] dp[0…W] dp[0...W] 存储当前行,更新 d p [ 0… W ] dp[0…W] dp[0...W] 的时候,我们按照 j = W . . . 0 j = W…0 j=W...0 的递减顺序计算 d p [ j ] dp[j] dp[j],这样可以保证计算 d p [ j ] dp[j] dp[j] 时用到的 d p [ j ] 和 d p [ j − w [ i ] ] dp[j]和 dp[j-w[i]] dp[j]dp[jw[i]] 的值和原本的二维数组中的第 i − 1 i-1 i1 行的值是相等的。更新完 d p [ j ] dp[j] dp[j] 的值后,对 d p [ 0… j − 1 ] dp[0…j-1] dp[0...j1] 的值不会产生影响。

伪代码:

for i = 1 to n
    for j = W to w[i]
        dp[j] = max(dp[j], dp[j-w[i]] + v[i])

代码:

#include <iostream>
#include <cstring>
#define MAXN 10000
using namespace std;

int dp[MAXN];
int w[MAXN] = { 
   0, 2, 1, 3, 2};
int v[MAXN] = { 
   0, 3, 2, 4, 2};
int W = 5, n = 4;

int solve(int n, int W) { 
   
    memset(dp, 0, sizeof(dp));
    for (int i = 1; i <= n; i++) { 
    // i从1开始,递增
        for (int j = W; j >= 0; j--) { 
    // j按递减顺序填表
            if (j < w[i]) { 
   
                dp[j] = dp[j];
            }
            else { 
   
                dp[j] = max(dp[j], dp[j-w[i]] + v[i]);
            }
        }
    }
    return dp[W];
}

int main() { 
   
    cout << solve(n, W) << endl;
    return 0;
}

####初始化的细节问题
我们看到的求解最优解的背包问题中,事实和桑有两种不太相同的问法。

  1. 要求”背包恰好装满“ 时的最优解
  2. 不要求背包一定要被装满时的最优解

我们上面所讨论的就是第2种, 不要求背包一定要被装满时的最优解。

一种区别这两种问法的实现方法是在初始化的时候有所不不同。

如果是第一种问法,要求恰好装满背包,那么在初始化时除了 d p [ 0 ] dp[0] dp[0] 为0, 其他 d p [ 1… W ] 均 设 为 − ∞ dp[1…W]均设为 -\infty dp[1...W] ,这样就可以保证最终得到 d p [ W ] dp[W] dp[W] 是一种恰好装满背包的最优解

如果并没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该将 d p [ 0… W ] dp[0…W] dp[0...W] 全部设为0。

这是为什么呢?可以这样理解:初始化的 d p dp dp 数组事实上就是在没有任何物品可以放入背包时的合法状态。如果要求背包恰好装满,那么此时只有容量为0的背包可以在什么也不装的状态下被 “恰好装满” ,此时背包价值为0。其他容量的背包均没有合法的解,属于未定义的状态,所以都应该被赋值为 − ∞ -\infty 当前的合法解,一定是从之前的合法状态推得的

如果背包并非必须被装满,那么任何容量的背包都有一个合法解 “什么也不装”,这个解的价值为0,所以初始化时状态的值也就全部为0了。


##完全背包问题
n n n 种重量和价值分别为 w i , v i w_i, v_i wi,vi 的物品。从这些物品中挑选总重量不超过 W W W 的物品,求出挑选物品价值总和的最大值。在这里,每种物品可以挑选任意多件。

在0-1背包问题中,每种物品只有不选和选两种可能。在完全背包问题中,每个物品可以选0, 1, … ⌊ W / w i ⌋ \lfloor W/w_i \rfloor W/wi 个。
令 d p [ i ] [ j ] : = 从 前 i 种 物 品 中 挑 选 总 重 量 不 超 过 j 的 物 品 的 最 大 总 价 值 。 令 dp[i][j] := 从前i种物品中挑选总重量不超过j的物品的最大总价值。 dp[i][j]:=ij 那么递推关系为:

d p [ i ] [ j ] = { 0 i = 0 m a x ( d p [ i − 1 ] [ j − k × w [ i ] ] + k × v [ i ] ) 0 ≤ k ≤ ⌊ j / w i ⌋ 1 ≤ i ≤ n dp[i][j] = \begin{cases} 0 & &i = 0\\ max(dp[i-1][j-k×w[i]] + k×v[i]) & 0 \le k \le \lfloor j/w_i \rfloor& 1 \le i \le n\\ \end{cases} dp[i][j]={
0max(dp[i1][jk×w[i]]+k×v[i])0kj/wii=01in

代码:

#include <iostream>
#include <cstring>
#define MAXN 1000
using namespace std;

int dp[MAXN][MAXN];
int w[MAXN] = { 
   0, 3, 4, 2};
int v[MAXN] = { 
   0, 4, 5, 3};
int W = 7, n = 3;

int solve(int n, int W) { 
   
    memset(dp, 0, sizeof(dp));
    for (int i = 1; i <= n; i++) { 
   
        for (int j = 0; j <= W; j++) { 
   
            for (int k = 0; k <= j/w[i]; k++) { 
   
                dp[i][j] = max(dp[i][j], dp[i-1][j-k*w[i]] + k*v[i]);
            }
        }
    }
    return dp[n][W];
}

int main() { 
   
    cout << solve(n, W) << endl; // 10
    return 0;
}

最坏情况下复杂度为 O ( n W 2 ) O(nW^2) O(nW2) , 不够好。这里面还是有很多多余的重复计算。第三层循环里,对于每种物品,都计算 ⌊ j / w i ⌋ \lfloor j/w_i \rfloor j/wi 次,这是不必要的。动态规划就是要利用已经计算过的小规模的问题的解,来求解更大规模的问题的解。让我们来探寻一下,还有什么我们没有利用的重复计算。

再重申一下, d p [ i ] [ j ] : = 从 前 i 种 物 品 中 挑 选 总 重 量 不 超 过 j 的 物 品 的 最 大 总 价 值 。 dp[i][j] := 从前i种物品中挑选总重量不超过j的物品的最大总价值。 dp[i][j]:=ij

再来看一下上面的递推公式:

d p [ i ] [ j ] = { 0 i = 0 m a x ( d p [ i − 1 ] [ j − k × w [ i ] ] + k × v [ i ] ) 0 ≤ k ≤ ⌊ j / w i ⌋ 1 ≤ i ≤ n dp[i][j] = \begin{cases} 0 & &i = 0\\ max(dp[i-1][j-k×w[i]] + k×v[i]) & 0 \le k \le \lfloor j/w_i \rfloor& 1 \le i \le n\\ \end{cases} dp[i][j]={
0max(dp[i1][jk×w[i]]+k×v[i])0kj/wii=01in

这 里 的 k 可 分 为 两 种 情 况 , k = 0 和 k ≠ 0 , 也 就 是 第 i 种 物 品 不 选 , 或 者 至 少 选 1 个 。 这里的k可分为两种情况,k = 0和k \ne 0 ,也就是第i种物品不选,或者至少选1个。 kk=0k=0,i1

  • k = 0 时 , 即 不 选 择 第 i 种 物 品 , d p [ i ] [ j ] = d p [ k = 0 时,即不选择第i种物品, dp[i][j] = dp[ k=0idp[i][j]=dp[ i − 1 i-1 i1 ] [ j ] , ][j], ][j],
  • k ≠ 0 时 , 即 至 少 选 一 个 第 i 种 物 品 , d p [ i ] [ j ] = d p [ k \ne 0 时, 即至少选一个第i种物品, dp[i][j] = dp[ k=0idp[i][j]=dp[ i i i ] [ j − w [ i ] ] + v [ i ] ][j-w[i]] + v[i] ][jw[i]]+v[i]

注意上面红色标识的 i − 1 i-1 i1 i i i

k = 0 时 , 比 较 容 易 理 解 , k ≠ 0 时 , 先 强 行 往 背 包 里 塞 一 个 第 i 种 物 品 , 然 后 把 问 题 转 化 成 更 小 规 模 的 问 题 。 k=0时,比较容易理解,k \ne 0时,先强行往背包里塞一个第i种物品,然后把问题转化成更小规模的问题。 k=0k=0i

i 和 j 都 是 按 递 增 顺 序 循 环 的 , 所 以 求 解 d p [ i ] [ j ] 时 , d p [ i ] [ j − w [ i ] ] 的 值 是 已 经 求 过 了 的 , 可 以 直 接 拿 来 用 。 i和j都是按递增顺序循环的,所以求解dp[i][j]时, dp[i][j-w[i]]的值是已经求过了的,可以直接拿来用。 ijdp[i][j]dp[i][jw[i]]

综上,可以得出递推公式:
d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i ] [ j − w [ i ] ] + v [ i ] ) ( 1 ) dp[i][j] = max(dp[i-1][j], dp[i][j-w[i]] + v[i]) (1) dp[i][j]=max(dp[i1][j],dp[i][jw[i]]+v[i])1

更正式地推导一下:

d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j − k × w [ i ] ] + k × v [ i ] ) , 0 ≤ k ≤ ⌊ j / w i ⌋ = m a x ( d p [ i − 1 ] [ j ] , m a x ( d p [ i − 1 ] [ j − k × w [ i ] ] + k × v [ i ] ) ) , 1 ≤ k ≤ ⌊ j / w i ⌋ dp[i][j] = max(dp[i-1][j-k×w[i]] + k×v[i]) ,0 \le k \le \lfloor j/w_i \rfloor \\ = max(dp[i-1][j], max(dp[i-1][j-k×w[i]] + k×v[i])), 1 \le k \le \lfloor j/w_i \rfloor dp[i][j]=max(dp[i1][jk×w[i]]+k×v[i]),0kj/wi=max(dp[i1][j],max(dp[i1][jk×w[i]]+k×v[i])),1kj/wi
= m a x ( d p [ i − 1 ] [ j ] , =max(dp[i-1][j], =max(dp[i1][j], m a x ( d p [ i − 1 ] [ ( j − w [ i ] ) − ( k − 1 ) × w [ i ] ] + ( k − 1 ) × v [ i ] ) max(dp[i-1][(j-w[i]) – (k-1)×w[i]] + (k-1)×v[i]) max(dp[i1][(jw[i])(k1)×w[i]]+(k1)×v[i]) + v [ i ] ) , +v[i]), +v[i]),$0 \le (k-1) \le \lfloor j/w_i \rfloor -1\$

= m a x ( d p [ i − 1 ] [ j ] , =max(dp[i-1][j], =max(dp[i1][j], d p [ i ] [ j − w [ i ] ] dp[i][j-w[i]] dp[i][jw[i]] + v [ i ] ) + v[i]) +v[i])

刚好和上面的(1)式一样。

代码:

#include <iostream>
#include <cstring>
#define MAXN 1000
using namespace std;

int dp[MAXN][MAXN];
int w[MAXN] = { 
   0, 3, 4, 2};
int v[MAXN] = { 
   0, 4, 5, 3};
int W = 7, n = 3;

int solve(int n, int W) { 
   
    memset(dp, 0, sizeof(dp));
    for (int i = 1; i <= n; i++) { 
   
        for (int j = 0; j <= W; j++) { 
   
            if (j < w[i]) { 
   // 不能塞的时候也不能硬塞,要注意一下
                dp[i][j] = dp[i-1][j];
            }
            else { 
   
                dp[i][j] = max(dp[i-1][j], dp[i][j-w[i]] + v[i]);
            }
        }
    }
    return dp[n][W];
}

int main() { 
   
    cout << solve(n, W) << endl; // 10
    return 0;
}

类似0-1背包问题, 完全背包问题也可以在空间上进行优化。

我 们 发 现 d p [ i ] [ j ] 的 值 只 依 赖 于 第 i 行 和 第 i − 1 行 的 值 , 可 以 只 用 一 个 一 维 数 组 来 存 所 有 需 要 的 值 我们发现dp[i][j]的值只依赖于第i行和第i-1行的值, 可以只用一个一维数组来存所有需要的值 dp[i][j]ii1,

伪代码:

for i = 1 to n
    for j = 0 to W
        dp[j] = max(dp[j], dp[j-w[i]] + v[i])

我们可以发现,完全背包的伪代码和0-1背包的伪代码非常像,只是第二层循环j的顺序不同,0-1背包是逆序循环,完全背包是正序循环。0-1背包问题为何逆序,之前已经讲得非常清楚了。完全背包问题第二层的j正序循环,因为 d p [ i ] [ j ] dp[i][j] dp[i][j] 需要用到 d p [ i ] [ j − w [ i ] ] dp[i][j-w[i]] dp[i][jw[i]] , 只用一维数组保存的话,按j正序循环,就能保证计算 d p [ j ] dp[j] dp[j] 时,旧的 d p [ j ] dp[j] dp[j] 的值就等于 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i1][j] 的值, d p [ j − w [ i ] ] dp[j-w[i]] dp[jw[i]] 已经计算过,且对应的就是二维数组中 d p [ i ] [ j − w [ i ] ] dp[i][j-w[i]] dp[i][jw[i]] 的值。

代码:

#include <iostream>
#include <cstring>
#define MAXN 1000
using namespace std;

int dp[MAXN];
int w[MAXN] = { 
   0, 3, 4, 2};
int v[MAXN] = { 
   0, 4, 5, 3};
int W = 7, n = 3;

int solve(int n, int W) { 
   
    memset(dp, 0, sizeof(dp));
    for (int i = 1; i <= n; i++) { 
   
        for (int j = 0; j <= W; j++) { 
   
            if (j < w[i]) { 
   
                dp[j] = dp[j];
            }
            else { 
   
                dp[j] = max(dp[j], dp[j-w[i]] + v[i]);
            }
        }
    }
    return dp[W];
}

int main() { 
   
    cout << solve(n, W) << endl; // 10
    return 0;
}

##多重背包问题
n n n 种物品和一个容量为 W W W 的背包。第 i i i 种物品有 m i m_i mi 个,每件重量为 w i w_i wi , 价值为 v i v_i vi ,求从这 n n n 种物品中挑选重量总和不超过 W W W 的物品的最大价值。

###转化成0-1背包问题
多重背包问题,最简单的解法,就是转化成0-1背包问题。第 i i i 个物品有 m i m_i mi 个, 等价于有 m i m_i mi 个相同的物品。但直接拆分成 m i m_i mi 件物品并不是最好的方法。我们可以利用二进制来拆分。例如 m 1 = 13 = 2 0 + 2 1 + 2 2 + 6 m_1 = 13 = 2^0 + 2^1 + 2^2 + 6 m1=13=20+21+22+6 ,我们将第一种物品共13件,拆分成 2 0 , 2 1 , 2 2 , 6 2^0, 2^1, 2^2, 6 20,21,22,6 这四件, 13以内的任何数字都可以通过这四种数字组合而成。

下面给出一个二进制拆分的多重背包模板:

const int N = 100, W = 100000;
int cost[N], weight[N], number[N];
int dp[W + 1];

int knapsack(int n, int w)
{ 
   
    for (int i = 0; i < n; ++i)
    { 
   
        int num = min(number[i], w / weight[i]);
        for (int k = 1; num > 0; k*=2)
        { 
   
            if (k > num) k = num;
            num -= k;
            for (int j = w; j >= weight[i] * k; --j)
                dp[j] = max(dp[j], dp[j - weight[i] * k] + cost[i] * k);
        }
    }
    return  dp[w];
}

时间复杂度为 O ( n l o g M × W ) O(nlogM × W) O(nlogM×W) , 实际应用已经足够好了。

###单调队列优化
多重背包问题的递归式为:
d p [ i ] [ j ] = { 0 i = 0 m a x ( d p [ i − 1 ] [ j − k × w [ i ] ] + k × v [ i ] ) 0 ≤ k ≤ m [ i ] 1 ≤ k ≤ n dp[i][j] = \begin{cases} 0 & &i = 0\\ max(dp[i-1][j-k×w[i]] + k×v[i]) & 0 \le k \le m[i] & 1 \le k \le n \end{cases} dp[i][j]={
0max(dp[i1][jk×w[i]]+k×v[i])0km[i]i=01kn

根据递推式,很容易能写出三重循环版本的多重背包代码。但这明显不是我们想要的。是否有方法能像完全背包问题一样,能有 O ( n W ) O(nW) O(nW) 的解法呢?

是的,有。

通过巧妙的构造,我们就可以用单调队列来优化多重背包问题, 使得复杂度降为 O ( n W ) O(nW) O(nW)

什么是单调队列: http://blog.csdn.net/justmeh/article/details/5844650
用单调队列优化: http://blog.csdn.net/flyinghearts/article/details/5898183

多重背包 单调队列模板:

#include <iostream>
#include <deque>
#include <algorithm>

using namespace std;

struct Pack
{ 
   
    int sum, cost;
    Pack(int s, int c) : sum (s), cost(c) { 
   }
};

const int Maxv = 1001;
deque <Pack> Q;
int N, V, F[Maxv];

int main()
{ 
   
    cin >> N >> V;
    for (int i = 1, p, w, c; i <= N; i ++)
    { 
   
        cin >> p >> w >> c; p = min(p, V / w);
        for (int j = 0; j < w; j ++)
        { 
   
            Q.clear();
            for (int k = 0; k <= (V - j) / w; k ++)
            { 
   
                int y = F[k * w + j] - k * c;
                while (Q.size() && Q.back().cost <= y) Q.pop_back();
                Q.push_back(Pack(k, y));
                if (Q.front().sum < k - c) Q.pop_front();
                F[k * w + j] = Q.front().cost + k * c;
            }
        }
    }
    cout << F[V] << endl;
    return 0;

目前第三讲刚开了个头。
未完待续,我会尽快补全9讲的。
补全随缘吧。。工作后没太多时间了(捂脸)
优秀的背包问题模板:http://blog.csdn.net/libin56842/article/details/9396649;

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/153455.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

  • arduino小车速度调节_智能小车pwm调速程序及原理图

    arduino小车速度调节_智能小车pwm调速程序及原理图Arduino小车——调速篇  在这一篇我们将对小车的行进速度进行调整,将驱动模块的作用发挥出来。首先大家要了解PWM这个概念。PWM  脉宽调制(PWM)基本原理:控制方式就是对逆变电路开关器件的通断进行控制,使输出端得到一系列幅值相等的脉冲,用这些脉冲来代替正弦波或所需要的波形。也就是在输出波形的半个周期中产生多个脉冲,使各脉冲的等值电压为正弦波形,所获得的输出平滑且低次谐波少。按一定的规则对各

  • 国产ARM核心工控主板介绍

    国产ARM核心板有哪些型号和作用?专业工控机品牌承诺,高性能,低功耗,提供专业定制。工控机箱的抗震:工控机箱在工作的时候,由于机箱内部的光驱、硬盘在高速运转的时候都会产生震动,而震动很容易导致光盘读错和硬盘磁道损坏以至丢失数据,所以工控机箱的抗震性也是我们机箱关键的一个结构设计方案。因为考虑到工控机箱的抗腐蚀、导电、导热等的内部要求,我们的工控机箱减震系统全部采用金属材料制成,这比起用橡胶材料做…

  • C++学习——虚函数与纯虚函数

    C++学习——虚函数与纯虚函数引言:若要访问派生类中相同名字的函数,必须将基类中的同名函数定义为 虚函数,这样,将不同的派生类对象的地址赋给基类的指针变量后, 就可以动态地根据这种赋值语句调用不同类中的函数。一、虚函数的定义和使用可以在程序运行时通过调用相同的函数名而实现不同功能的 函数称为虚函数。定义格式为:virtual FuncName();一旦把基类的成员函数定义为虚函数,由基类所派生出来的所 有派生类中,…

  • modelsim 10.7安装教程

    modelsim 10.7安装教程安装步骤:安装前先关闭杀毒软件和360卫士,注意安装路径不能有中文,安装包路径也不要有中文。试装系统:win1064bit以安装Modelsim10.7为例,10.X的安装基本差不多重要:安装包有10.1,10.2,10.4,10.5,10.7这几个版本,如果是安装后安装目录win32/win64文件夹里面有mgls.dll文件,则第步不需要复制mgls.dll文件。另外有的安装教程也说…

  • 2015美国闪存峰会来了!PMC将展示新一代NVMe方案

    2015美国闪存峰会来了!PMC将展示新一代NVMe方案

  • 彻底弄懂二叉树的先序,中序,后序三种遍历与做题方式_二叉树的先序,中序,后序遍历例题

    彻底弄懂二叉树的先序,中序,后序三种遍历与做题方式_二叉树的先序,中序,后序遍历例题二叉树二叉树遍历二叉树题目计算机二级先序中序后序根

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号