zoukankan      html  css  js  c++  java
  • 背包九讲学习笔记

    背包问题是一类经典的动态规划问题,它非常灵活,需要仔细琢磨体会,本文先对背包问题的几种常见类型作一个总结,给出代码模板。

    根据维基百科,背包问题(Knapsack problem)是一种组合优化的NP完全(NP-Complete,NPC)问题。问题可以描述为:给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高。NPC问题是没有多项式时间复杂度的解法的,但是利用动态规划,我们可以以伪多项式时间复杂度求解背包问题。一般来讲,背包问题有以下几种分类:

    1. 01背包问题
    2. 完全背包问题
    3. 多重背包问题

    此外,还存在一些其他考法,例如恰好装满、求方案总数、求所有的方案等。本文接下来就分别讨论一下这些问题。

    以下是学习背包九讲的学习笔记。

    1. 01背包

    1.1 题目

    最基本的背包问题就是01背包问题(01 knapsack problem):一共有N件物品,第i(i从1开始)件物品的重量为 (w[i]),价值为 $v[i] $。在总重量不超过背包承载上限 $W $ 的情况下,能够装入背包的最大价值是多少?

    1.2 分析

    如果采用暴力穷举的方式,每件物品都存在装入和不装入两种情况,所以总的时间复杂度是(O(2^N)),这是不可接受的。而使用动态规划可以将复杂度降至 $O(NW) $。我们的目标是书包内物品的总价值,而变量是物品和书包的限重,所以我们可定义状态 (dp) :

    dp[i][j]表示将前i件物品装进限重为j的背包可以获得的最大价值, 0<=i<=N, 0<=j<=W
    

    那么我们可以将dp[0][0…W]初始化为0,表示将前0个物品(即没有物品)装入书包的最大价值为0。那么当 i > 0 时dp[i][j]有两种情况:

    1. 不装入第i件物品,即dp[i−1][j]
    2. 装入第i件物品(前提是能装下),即dp[i−1][j−w[i]] + v[i]

    即状态转移方程为

    dp[i][j] = max(dp[i−1][j], dp[i−1][j−w[i]]+v[i]) // j >= w[i]
    

    由上述状态转移方程可知,dp[i][j]的值只与dp[i-1][0,...,j-1]有关,所以我们可以采用动态规划常用的方法(滚动数组)对空间进行优化(即去掉dp的第一维)。需要注意的是,为了防止上一层循环的dp[0,...,j-1]被覆盖,循环的时候 j 只能逆向枚举(空间优化前没有这个限制),伪代码为:

    // 01背包问题伪代码(空间优化版)
    dp[0,...,W] = 0
    for i = 1,...,N
        for j = W,...,w[i] // 必须逆向枚举!!!
            dp[j] = max(dp[j], dp[j−w[i]]+v[i])
    

    时间复杂度为 (O(NW)), 空间复杂度为 (O(W)) 。由于W的值是W的位数的幂,所以这个时间复杂度是伪多项式时间。

    动态规划的核心思想避免重复计算在01背包问题中体现得淋漓尽致。第 $ i$ 件物品装入或者不装入而获得的最大价值完全可以由前面 (i-1) 件物品的最大价值决定,暴力枚举忽略了这个事实。

    2. 完全背包

    2.1 题目

    完全背包(unbounded knapsack problem)与01背包不同就是每种物品可以有无限多个:一共有N种物品,每种物品有无限多个,第 (i)(i) 从1开始)种物品的重量为(w[i]),价值为 $v[i] $。在总重量不超过背包承载上限 $W $的情况下,能够装入背包的最大价值是多少?

    2.2 分析一

    我们的目标和变量和01背包没有区别,所以我们可定义与01背包问题几乎完全相同的状态 (dp):

    dp[i][j]表示将前i种物品装进限重为j的背包可以获得的最大价值, 0<=i<=N, 0<=j<=W
    

    初始状态也是一样的,我们将 (dp[0][0…W])初始化为0,表示将前0种物品(即没有物品)装入书包的最大价值为0。那么当 (i > 0)dp[i][j]也有两种情况:

    1. 不装入第 $i $种物品,即dp[i−1][j],同01背包;
    2. 装入第 (i) 种物品,此时和01背包不太一样,因为每种物品有无限个(但注意书包限重是有限的),所以此时不应该转移到dp[i−1][j−w[i]]而应该转移到dp[i][j−w[i]],即装入第 (i) 种商品后还可以再继续装入第种商品。

    所以状态转移方程为

    dp[i][j] = max(dp[i−1][j], dp[i][j−w[i]]+v[i]) // j >= w[i]
    

    这个状态转移方程与01背包问题唯一不同就是max第二项不是 (dp[i-1]而是dp[i])

    和01背包问题类似,也可进行空间优化,优化后不同点在于这里的 j 只能正向枚举而01背包只能逆向枚举,因为这里的max第二项是dp[i]而01背包是dp[i-1],即这里就是需要覆盖而01背包需要避免覆盖。所以伪代码如下:

    // 完全背包问题思路一伪代码(空间优化版)
    dp[0,...,W] = 0
    for i = 1,...,N
        for j = w[i],...,W // 必须正向枚举!!!
            dp[j] = max(dp[j], dp[j−w[i]]+v[i])
    

    由上述伪代码看出,01背包和完全背包问题此解法的空间优化版解法唯一不同就是前者的 j 只能逆向枚举而后者的 j 只能正向枚举,这是由二者的状态转移方程决定的。此解法时间复杂度为 (O(NW)) , 空间复杂度为 (O(W))

    2.3 分析二

    除了分析一的思路外,完全背包还有一种常见的思路,但是复杂度高一些。我们从装入第 i 种物品多少件出发,01背包只有两种情况即取0件和取1件,而这里是取0件、1件、2件…直到超过限重((k > j/w[i])),所以状态转移方程为:

    # k为装入第i种物品的件数, k <= j/w[i]
    dp[i][j] = max{(dp[i-1][j − k*w[i]] + k*v[i]) for every k}
    

    同理也可以进行空间优化,需要注意的是,这里max里面是dp[i-1],和01背包一样,所以 j 必须逆向枚举,优化后伪代码为

    // 完全背包问题思路二伪代码(空间优化版)
    dp[0,...,W] = 0
    for i = 1,...,N
        for j = W,...,w[i] // 必须逆向枚举!!!
            for k = [0, 1,..., j/w[i]]
                dp[j] = max(dp[j], dp[j−k*w[i]]+k*v[i])
    

    相比于分析一,此种方法不是在O(1)时间求得dp[i][j],所以总的时间复杂度就比分析一大些了。

    2.4 分析三、转换成01背包

    01背包问题是最基本的背包问题,我们可以考虑把完全背包问题转化为01背包问题来解:将一种物品转换成若干件只能装入0件或者1件的01背包中的物品。

    最简单的想法是,考虑到第 i 种物品最多装入 W/w[i] 件,于是可以把第 i 种物品转化为 W/w[i] 件费用及价值均不变的物品,然后求解这个01背包问题。

    更高效的转化方法是采用二进制的思想:把第 i 种物品拆成重量为 wi2kwi2k、价值为 vi2kvi2k 的若干件物品,其中 k 取遍满足 wi2k≤Wwi2k≤W 的非负整数。这是因为不管最优策略选几件第 i 种物品,总可以表示成若干个刚才这些物品的和(例:13 = 1 + 4 + 8)。这样就将转换后的物品数目降成了对数级别。具体代码见3.4节模板。

    3. 多重背包

    3.1 题目

    多重背包(bounded knapsack problem)与前面不同就是每种物品是有限个:一共有N种物品,第i(i从1开始)种物品的数量为n[i],重量为w[i],价值为v[i]。在总重量不超过背包承载上限W的情况下,能够装入背包的最大价值是多少?

    3.2 分析一

    此时的分析和完全背包的分析二差不多,也是从装入第 i 种物品多少件出发:装入第i种物品0件、1件、…n[i]件(还要满足不超过限重)。所以状态方程为:

    # k为装入第i种物品的件数, k <= min(n[i], j/w[i])
    dp[i][j] = max{(dp[i-1][j − k*w[i]] + k*v[i]) for every k}
    

    同理也可以进行空间优化,而且 j 也必须逆向枚举,优化后伪代码为

    // 完全背包问题思路二伪代码(空间优化版)
    dp[0,...,W] = 0
    for i = 1,...,N
        for j = W,...,w[i] // 必须逆向枚举!!!
            for k = [0, 1,..., min(n[i], j/w[i])]
                dp[j] = max(dp[j], dp[j−k*w[i]]+k*v[i])
    

    总的时间复杂度约为 (O(NWn¯)=O(W∑_in_i))级别。

    3.3 分析二、转换成01背包

    采用2.4节类似的思路可以将多重背包转换成01背包问题,采用二进制思路将第 i 种物品分成了 (O(logn_i)) 件物品,将原问题转化为了复杂度为 (O(W∑_ilogn_i))的 01 背包问题,相对于分析一是很大的改进,具体代码见3.4节。

    3.4 代码模板

    此节根据上面的讲解给出这三种背包问题的解题模板,方便解题使用。尤其注意其中二进制优化是如何实现的。

    /*
    用法:
        对每个物品调用对应的函数即可, 例如多重背包:
        for(int i = 0; i < N; i++) 
            multiple_pack_step(dp, w[i], v[i], num[i], W);
    
    参数:
        dp   : 空间优化后的一维dp数组, 即dp[i]表示最大承重为i的书包的结果
        w    : 这个物品的重量
        v    : 这个物品的价值
        n    : 这个物品的个数
        max_w: 书包的最大承重
    */
    void zero_one_pack_step(vector<int>&dp, int w, int v, int max_w){
        for(int j = max_w; j >= w; j--) // 反向枚举!!!
            dp[j] = max(dp[j], dp[j - w] + v);
    }
    
    void complete_pack_step(vector<int>&dp, int w, int v, int max_w){
        for(int j = w; j <= max_w; j++) // 正向枚举!!!
            dp[j] = max(dp[j], dp[j - w] + v);
    
        // 法二: 转换成01背包, 二进制优化
        // int n = max_w / w, k = 1;
        // while(n > 0){
        //     zero_one_pack_step(dp, w*k, v*k, max_w);
        //     n -= k;
        //     k = k*2 > n ? n : k*2;
        // }
    }
    
    void multiple_pack_step(vector<int>&dp, int w, int v, int n, int max_w){
       if(n >= max_w / w) complete_pack_step(dp, w, v, max_w);
       else{ // 转换成01背包, 二进制优化
           int k = 1;
           while(n > 0){
               zero_one_pack_step(dp, w*k, v*k, max_w);
               n -= k;
               k = k*2 > n ? n : k*2;
           }
       }
    }
    

    3.5混合三种背包问题

    3.5.1Description

    有一些物品只能取 (1) 件,有一些物品可以取无数件,其他物品可以取 (z_i) 件。

    3.5.2 Solution

    首先考虑 (01) 背包和完全背包的混合。只要判断一下当前物品的种类,看看是要顺序还是逆序循环。加上多重背包:按照上面的方法,拆开物品,将其变成 (01) 背包(听说可以使用单调队列)。

    3.5.3 Code

    for(int i = 1; i <= n; i++)
    {
        if(can[i] == INF)
            for(int j = m; j >= w[i]; j--)
                f[j] = max(f[j], f[j - w[i]] + val[i]);
        else
            for(int j = w[i]; j <= m; j++)
                f[j] = max(f[j], f[j - w[i]] + val[i]);
    }
    

    4. 二维费用背包问题

    4.1 Description

    对于每件物品,这件物品必须同时付出这两种代价,对于每种代价都有一个可付出的最大值(背包容量)。问怎样选择物品可以得到最大的价值。

    4.2 Solution

    设 $f[i][u][v] $为前 (i) 个物品,两种代价分别为 (u 和 v) 能够得到的最大价值。转移方程:

    [f[i][u][v]=max(f[i−1][u−a[i]][v−b[i]]+val[i],f[i−1][u][v]) ]

    可以按照上面优化,去掉 (i) 这一维。只要注意 (01) 背包是逆序循环,完全背包是顺序循环就行了。代码以 (01) 背包为例。

    4.3 Code

    #include <iostream>
    #include <cstdio>
    using namespace std;
    
    const int N = 506;
    int n, m1, m2, a[N], b[N], val[N], f[N][N];
    
    int main()
    {
        scanf("%d%d%d", &n, &m1, &m2);
        for(int i = 1; i <= n; i++)
            scanf("%d%d%d", &a[i], &b[i], &val[i]);
        for(int i = 1; i <= n; i++)
            for(int j = m1; j >= a[i]; j--)
                for(int k = m2; k >= a[i]; k--)
                    f[j][k] = max(f[j][k], f[j - a[i]][k - b[i]] + val[i]);
        printf("%d", f[m1][m2]);
        return 0;
    }
    

    5.分组背包问题

    5.1 Description

    (n) 个物品分为 (k) 组,每一组物品最多只能选一个。

    5.2 Solution

    问题变成了,你可以选择本组的一件,或者一件也不选。设 (f[i][j]) 为前 (k) 组物品花费 (j) 能得到的价值。转移方程: $$f[k][j]=max(f[k−1][j],f[k−1][j−w[i]]+val[i])$$

    [(i 属于第 k 组) ]

    可以按照上面的优化方法,把 (f) 数组变成一维。保证每组只能选一个物品,容量的循环要在每一组物品的循环之外。具体看代码。

    for(int i = 1; i <= k; i++)
        for(int j = m; j > 0; j--)
            for(int h = 1; h <= cnt[i]; h++)
            {
                int v = belong[i][h];
                f[j] = max(f[j], f[j - w[v]] + val[v]);
            }
    

    6. 有依赖的背包问题

    6.1 Description

    如果选物品 (i),必须选物品 $ j$。

    6.2 Solution

    如果把问题转化为一棵树,若 (j) 依赖于 (i),使 (i) 成为 (j) 的父亲。连出一虚拟根。只有选择了 (i),才能选择其子树。

    (f[i][j]) 为以 (i) 为根的子树,已经选择了 (j) 个节点的最大价值。转移方程:

    [f[i][j]=max(f[i][j],f[s][k]+f[i][j−k]) (s 是 i 的儿子,0<k<j) ]

    6.3 例题

    洛谷 P2014 选课

    此题相当于给出 (n) 个物品,背包容量为 (m),每个物品的重量为 (1),价值为 (s_i),最大化价值。物品之间存在依赖关系。这样就把问题转化成了一般形式,按上面的思路解即可。

    6.4 Code

    #include <cstdio>
    #include <iostream>
    using namespace std;
    
    const int N = 2333;
    struct edge
    {
        int nxt, to;
    } e[N];
    int n, m, fat, cnt = 0, s[N], f[N][N], head[N];
    
    void add(int x, int y)
    {
        e[++cnt] = (edge) { head[x], y };
        head[x] = cnt;
    }
    
    void dp(int x, int fa)
    {
        f[x][1] = s[x];
        for(int i = head[x]; i; i = e[i].nxt)
        {
            int v = e[i].to;
            if(v == fa) continue;
            dp(v, x);
            for(int j = m + 1; j > 0; j--)
                for(int k = 1; k < j; k++)
                    f[x][j] = max(f[x][j], f[v][k] + f[x][j - k]);
        }
    }
    
    int main()
    {
        scanf("%d%d", &n, &m);
        for(int i = 1; i <= n; i++)
        {
            scanf("%d%d", &fat, &s[i]);
            add(fat, i);
            add(i, fat);
        }
        dp(0, -1);
        printf("%d", f[0][m + 1]);
        return 0;
    }
    

    7. 泛化背包问题

    7.1 Description

    每个物品没有固定的重量和价值。给它重量 (i),就会得到价值 (h(i))

    7.2 Solution

    设有泛化物品 (h)(l),其中 (i(j)) 表示给 (i) 这个泛化物品设置 (j) 费用能得到的价值。若 (f) 满足$ f(v)=max(h(k)+l(v−k))$,则称 (f=h+l)

    实际上求最终结果的过程,就是不断求泛化物品之和的过程。


    最后附上DD大牛的 背包九讲PDF下载:

    Here

  • 相关阅读:
    LeetCode 842. Split Array into Fibonacci Sequence
    LeetCode 1087. Brace Expansion
    LeetCode 1219. Path with Maximum Gold
    LeetCode 1079. Letter Tile Possibilities
    LeetCode 1049. Last Stone Weight II
    LeetCode 1046. Last Stone Weight
    LeetCode 1139. Largest 1-Bordered Square
    LeetCode 764. Largest Plus Sign
    LeetCode 1105. Filling Bookcase Shelves
    LeetCode 1027. Longest Arithmetic Sequence
  • 原文地址:https://www.cnblogs.com/RioTian/p/13490425.html
Copyright © 2011-2022 走看看