背包九讲详解

#背包九讲详解

##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 
#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 
#include 
#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] & (jdp[i][j]=0dp[i1][j]max(dp[i1][j],dp[i1][jw[i]]+v[i])(i=0)(j<w[i])(other)
代码:

#include 
#include 
#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 
#include 
#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 
#include 
#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 
#include 
#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 
#include 
#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 
#include 
#include 

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;

你可能感兴趣的:(DP,Nuist-ACM,动态规划)