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

  • 相关阅读:
    Matlab矩阵操作函数的使用(reshape,imresize,remat,permute)
    归一化数据的好处
    博客园中用markdown编辑器编辑带下标的符号
    反向传播
    稀疏表示和字典学习
    先验概率和后验概率
    Local Generic Representation for Face Recognition with Single Sample per Person (ACCV, 2014)
    删除数组
    数组扩容(指定位置)+插入最后
    创建数组的几种方式,遍历+泛型合并
  • 原文地址:https://www.cnblogs.com/RioTian/p/13490425.html
Copyright © 2011-2022 走看看