背包九讲

概述

本文参考自bilibili上up主大雪菜的背包九讲专题:背包九讲专题1、背包九讲专题2 以及AcWing上的相关讲解。

  • 背包问题代表了一类问题,即组合类的最优化问题,就是如果给我们一堆物品(元素),我们要按照某种限从中选出若干个物品(元素),求最大/最小值。

  • 本文将将对如下的九种背包问题给出分析过程以及实现代码(提供C++和Java代码,代码链接:github),最后还给出了Leetcode上部分相关的背包问题以及解答。

    /*
    1. 01背包问题
    2. 完全背包问题
    3. 多重背包问题
    4. 混合背包问题
    5. 二维费用背包问题
    6. 分组背包问题
    7. 背包问题求方案数
    8. 求背包问题的方案
    9. 有依赖的背包问题
    */
    
  • 背包问题是一类典型的动态规划问题,一般对于动态规划问题,常规分析方式是给出状态定义状态转移,这里为了更加容易理解,采用yxc提出的闫式dp分析法

一. 01背包问题

问题描述

  • 问题链接:01背包问题

分析

代码

  • C++:
// Created by WXX on 2021/2/24 13:46
#include <iostream>using namespace std;const int N = 1010;  // 多开几个数据,防止数组下标越界int n, m;  // 物品种类数,背包容积
int v[N], w[N];  // 体积,价值。注意:v[1]存储第一件物品,索引0未使用
int f[N][N];  // dp数组int main() {// 读入数据cin >> n >> m;for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];// 算法过程,因为f[0][0~m]初始就为0,因此初始化可以省略for (int i = 1; i <= n; i++) {  // 先循环物品for (int j = 0; j <= m; j++) {  // 再循环容量// 最后循环决策f[i][j] = f[i - 1][j];  // 不选第i件物品if (j >= v[i]) f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]);  // 考虑选第i件物品}}cout << f[n][m] << endl;return 0;
}
  • Java
import java.util.Scanner;// Created by WXX on 2021/2/24 14:15
public class Main {public static final int N = 1010;  // 多开几个数据,防止数组下标越界static int n, m;  // 物品种类数,背包容积static int[] v = new int[N], w = new int[N];  // 体积,价值。注意:v[1]存储第一件物品,索引0未使用static int[][] f = new int[N][N];   // dp数组public static void main(String[] args) {// 读入数据Scanner sn = new Scanner(System.in);n = sn.nextInt(); m = sn.nextInt();for (int i = 1; i <= n; i++) {v[i] = sn.nextInt(); w[i] = sn.nextInt();}// 算法过程,因为f[0][0~m]初始就为0,因此初始化可以省略for (int i = 1; i <= n; i++) {  // 先循环物品for (int j = 0; j <= m; j++) {  // 再循环容量// 最后循环决策f[i][j] = f[i - 1][j];  // 不选第i件物品if (j >= v[i]) f[i][j] = Math.max(f[i][j], f[i - 1][j - v[i]] + w[i]);  // 考虑选第i件物品}}System.out.println(f[n][m]);}
}

代码优化(只用C++演示)

  • 考虑到在计算 f 的时候,当计算第 i 行时,只用到了第 i - 1 行的数据,因此可以用滚动数组优化
// Created by WXX on 2021/2/24 14:29
#include <iostream>using namespace std;const int N = 1010;int n, m;
int v[N], w[N];
int f[2][N];int main() {cin >> n >> m;for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];for (int i = 1; i <= n; i++) {for (int j = 0; j <= m; j++) {f[i & 1][j] = f[(i - 1) & 1][j];if (j >= v[i]) f[i & 1][j] = max(f[i & 1][j], f[(i - 1) & 1][j - v[i]] + w[i]);}}cout << f[n & 1][m] << endl;return 0;
}
  • 其实这里还可以将 f 数组优化为一维数组,但是考虑到要用到上一行数据,因此第二层循环应该从大到小进行遍历,这样每次更新用到的就是未被更新的数据
// Created by WXX on 2021/2/24 14:36
#include <iostream>using namespace std;const int N = 1010;int n, m;
int v[N], w[N];
int f[N];int main() {cin >> n >> m;for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];for (int i = 1; i <= n; i++)for (int j = m; j >= v[i]; j--)f[j] = max(f[j], f[j - v[i]] + w[i]);cout << f[m] << endl;return 0;
}

二. 完全背包问题

问题描述

  • 问题链接:完全背包问题

分析

代码

  • C++:
// Created by WXX on 2021/2/24 14:52
#include <iostream>using namespace std;const int N = 1010;int n, m;
int v[N], w[N];
int f[N][N];// TLE
int main() {cin >> n >> m;for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];for (int i = 1; i <= n; i++)  // 先循环物品for (int j = 0; j <= m; j++)  // 再循环容量for (int k = 0; k * v[i] <= j; k++)  // 最后循环决策f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);cout << f[n][m] << endl;return 0;
}
  • Java:
import java.util.Scanner;// Created by WXX on 2021/2/24 14:58
public class Main {public static final int N = 1010;static int n, m;static int[] v = new int[N], w = new int[N];static int[][] f = new int[N][N];public static void main(String[] args) {Scanner sn = new Scanner(System.in);n = sn.nextInt(); m = sn.nextInt();for (int i = 1; i <= n; i++) {v[i] = sn.nextInt(); w[i] = sn.nextInt();}for (int i = 1; i <= n; i++)  // 先循环物品for (int j = 0; j <= m; j++)  // 再循环容量for (int k = 0; k * v[i] <= j; k++)  // 最后循环决策f[i][j] = Math.max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);System.out.println(f[n][m]);}
}

代码优化(只用C++演示)

  • 我们可以将 f(i,j)=f(i−1,j−k∗v[i])+k∗w[i]f(i, j) = f(i-1, j-k*v[i]) + k*w[i]f(i,j)=f(i−1,j−k∗v[i])+k∗w[i] 展开,就可以发现如下规律:

因此代码可以根据 f(i,j)=max(f(i−1,j),f(i,j−v)+w)f(i, j) = max( f(i-1, j), f(i, j-v) + w)f(i,j)=max(f(i−1,j),f(i,j−v)+w) 进行优化:

// Created by WXX on 2021/2/24 15:21
#include <iostream>using namespace std;const int N = 1010;int n, m;
int v[N], w[N];
int f[N][N];int main() {cin >> n >> m;for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];for (int i = 1; i <= n; i++)for (int j = 0; j <= m; j++) {f[i][j] = f[i - 1][j];if (j >= v[i]) f[i][j] = max(f[i][j], f[i][j - v[i]] + w[i]);}cout << f[n][m] << endl;return 0;
}
  • 这里还可以将 f 数组优化为一维数组,因为不需要用到上一行数据,要用到本行之前计算出来的户籍,因此第二层循环应该从小到大进行遍历
// Created by WXX on 2021/2/24 15:21
#include <iostream>using namespace std;const int N = 1010;int n, m;
int v[N], w[N];
int f[N];int main() {cin >> n >> m;for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];for (int i = 1; i <= n; i++)for (int j = v[i]; j <= m; j++)f[j] = max(f[j], f[j - v[i]] + w[i]);cout << f[m] << endl;return 0;
}

三. 多重背包问题

3.1 多重背包问题 I

问题描述

  • 问题链接:多重背包问题 I

分析

代码

  • C++
// Created by WXX on 2021/2/24 16:07
#include <iostream>using namespace std;const int N = 110;int n, m;
int v[N], w[N], s[N];
int f[N][N];int main() {cin >> n >> m;for (int i = 1; i <= n; i++) cin >> v[i] >> w[i] >> s[i];for (int i = 1; i <= n; i++)  // 先循环物品for (int j = 0; j <= m; j++)  // 再循环容量for (int k = 0; k <= s[i] && k * v[i] <= j; k++)  // 最后循环决策f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);cout << f[n][m] << endl;return 0;
}
  • Java
import java.util.Scanner;// Created by WXX on 2021/2/24 16:13
public class Main {public static final int N = 110;static int n, m;static int[] v = new int[N], w = new int[N], s = new int[N];static int[][] f = new int[N][N];public static void main(String[] args) {Scanner sn = new Scanner(System.in);n = sn.nextInt(); m = sn.nextInt();for (int i = 1; i <= n; i++) {v[i] = sn.nextInt(); w[i] = sn.nextInt(); s[i] = sn.nextInt();}for (int i = 1; i <= n; i++)  // 先循环物品for (int j = 0; j <= m; j++)  // 再循环容量for (int k = 0; k <= s[i] && k * v[i] <= j; k++)  // 最后循环决策f[i][j] = Math.max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);System.out.println(f[n][m]);}
}

3.2 多重背包问题 II

问题描述

  • 问题链接:多重背包问题 II

分析

  • 这一题不能使用类似于完全背包问题的方式进行优化,可以将 f(i,j)=f(i−1,j−k∗v)+k∗w,k=0,1,...,sf(i, j) = f(i-1, j-k*v) + k*w, k=0,1,...,sf(i,j)=f(i−1,j−k∗v)+k∗w,k=0,1,...,s 展开,如下:

    我们发现 f[i,j−v]f[i, j-v]f[i,j−v] 中比 f[i,j]f[i, j]f[i,j] 最后多了一项,不能直接得到这两者的关系,所以不能使用类似于完全背包问题的方式进行优化。

  • 本次的优化方式为二进制优化,将s件物品进行拆分,然后可以转化成01背包问题

    /*
    例如 s=10
    10可以拆分为0、1、2、4、3;
    前四个数可以凑出0~7之间的任何数据,加上3可以凑出3~10之间的任何数据,因此这5个数可以凑出0~10内的任何数据;
    相当于将10个物品打包成4个物品,打包后的物品的体积和价值分别为单个物品的1、2、4、3倍;
    这四个物品都是可选可不选,因此就转化成了01背包问题。例如 s=200
    200可以拆分为0、1、2、4、8、16、32、64、73;
    一共8个物品总结:对于s,可以划分为 log(s) 上取整个单一物品,然后用01背包问题的思路解决即可,时间复杂度:O(N*V*log(s))
    */
    

代码

  • C++
// Created by WXX on 2021/2/24 16:42
#include <iostream>
#include <vector>using namespace std;const int N = 2010;int n, m;
int f[N];struct Good {int v, w;
};int main() {vector<Good> goods;cin >> n >> m;for (int i = 0; i < n; i++) {int v, w, s;cin >> v >> w >> s;for (int k = 1; k <= s; k *= 2) {s -= k;goods.push_back({k * v, k * w});}if (s > 0) goods.push_back({s * v, s * w});}for (auto good : goods)for (int j = m; j >= good.v; j--)f[j] = max(f[j], f[j - good.v] + good.w);cout << f[m] << endl;return 0;
}
  • Java
import java.util.ArrayList;
import java.util.Scanner;// Created by WXX on 2021/2/24 16:50
public class Main {public static final int N = 2010;static int n, m;static int[] f = new int[N];static class Good {int v, w;public Good(int v, int w) {this.v = v;this.w = w;}}public static void main(String[] args) {ArrayList<Good> goods = new ArrayList<>();Scanner sn = new Scanner(System.in);n = sn.nextInt(); m = sn.nextInt();for (int i = 0; i < n; i++) {int v = sn.nextInt(), w = sn.nextInt(), s = sn.nextInt();for (int k = 1; k <= s; k *= 2) {s -= k;goods.add(new Good(k * v, k * w));}if (s > 0) goods.add(new Good(s * v, s * w));}for (Good good : goods)for (int j = m; j >= good.v; j--)f[j] = Math.max(f[j], f[j - good.v] + good.w);System.out.println(f[m]);}
}

3.2 多重背包问题 III

问题描述

  • 问题链接:多重背包问题 III

分析

  • DP分析和 3.1 多重背包问题 I 完全一致,但是本题的数据量更大,需要进一步优化,这里采用滑动窗口求最值的方式优化

    还有一个问题需要解决,就是窗口每次向右滑动一格,除了滑出窗口外的元素,其余的元素在下一次比较中都需要加上一个偏移量w,这个问题可以通过如下方式解决(参考网址):

    /*
    因为每次循环都只需要用到第i-1层的结果,所以在这里:
    g:第i-1层结果
    f:第i层结果如果一共有3个物品,即s=3的话:
    取r=1,那么f[3*v+1]的最大值为 f[3*v+1] = max(g[3*v+1], g[2*v+1]+w, g[v+1]+2w, g[1]+3w);所以我们可以得到下列算式:(其中r表示余数)
    f[r]=       g[r];
    f[r+v]= max(g[r] + w,  g[r+v]);
    f[r+2v]=max(g[r] + 2w, g[r+v] + w,  g[r+2v],);
    f[r+3v]=max(g[r] + 3w, g[r+v] + 2w, g[r+2v] + w, g[r+3v]);
    ……
    f[r+sv]=max(g[r] + sw,……, g[r+(s-1)v] + w, g[r+sv]);可以发现上面的等式上下存在偏移量w,所以可以减去kw再加上kw进行转换
    f[r]=       g[r];
    f[r+v]= max(g[r], g[r+v] - w) + w;
    f[r+2v]=max(g[r], g[r+v] - w, g[r+2v] - 2w) + 2w;
    f[r+3v]=max(g[r], g[r+v] - w, g[r+2v] - 2w, g[r+3v] - 3w) + 3w;
    ……
    f[r+sv]=max(g[r], ……, g[r+(s-1)v] - (s-1)w, g[r+sv] - sw) + sw;
    每次单调队列q (q中存储的是体积,如r,r+v,...) 用队尾数据和要插入的数据进行比较时,需要减去一个数
    这里比较的其实是上面max小括号中的数据
    队尾数据:g[a] - (a-r)/v * w,其中a是队尾体积q[tt] ==> g[q[tt]] - (q[tt]-r)/v * w
    即将滑入滑动窗口的数据:g[k] - (k-r)/v * w,其中k是当前考察的同余的体积,例如k=r+3v等另外还要注意,f中真实存储的数据为:g[q[hh]] + (q[hh]-r)/v * w,其中(q[hh]-r)/v * w为当前考察物品的收益
    */
    
  • 关于滑动窗口求最值,这是一个经典的问题,在LeetCode上有对应的问题:Leetcode 0239 滑动窗口最大值;在AcWing上也有对应的练习:AcWing 0154. 滑动窗口

  • 这里给出 Leetcode 0239 滑动窗口最大值 的C++解法(注意:这里的滑动窗口大小必须是k,不能小于k;多重背包中的滑动窗口为1,然后变为2,直到到达v后不再改变):

    // 考点:单调队列
    /*** 执行用时:240 ms, 在所有 C++ 提交中击败了97.90%的用户* 内存消耗:114.1 MB, 在所有 C++ 提交中击败了88.81%的用户*/
    class Solution {public:vector<int> maxSlidingWindow(vector<int> &nums, int k) {int n = nums.size();int q[n];int hh = 0, tt = -1;vector<int> res;for (int i = 0; i < nums.size(); i++) {if (hh <= tt && i - k + 1 > q[hh]) hh++;while (hh <= tt && nums[q[tt]] <= nums[i]) tt--;q[++tt] = i;if (i >= k - 1) res.push_back(nums[q[hh]]);}return res;}
    };
    

代码

// Created by WXX on 2021/2/24 18:43
#include <iostream>
#include <cstring>using namespace std;const int N = 20010;int n, m;
int f[N], g[N], q[N];  // g存储上一行的值,f存储当前行的值,q是单调队列int main() {cin >> n >> m;for (int i = 0; i < n; i++) {  // 这里采取边读入物品,边处理;每个物品都要进行滑窗处理int v, w, s;cin >> v >> w >> s;memcpy(g, f, sizeof f);  // 将f中的数据拷贝到g中for (int r = 0; r < v; r++) {int hh = 0, tt = -1;for (int k = r; k <= m; k += v) {if (hh <= tt && q[hh] < k - s * v) hh++;  // 说明队头元素应该 滑出滑窗while (hh <= tt && g[q[tt]] - (q[tt] - r) / v * w <= g[k] - (k - r) / v * w) tt--;q[++tt] = k;  // 将当前考察体积存入滑窗if (hh <= tt) f[k] = max(f[k], g[q[hh]] + (k - q[hh]) / v * w);}}}cout << f[m] << endl;return 0;
}
  • Java
import java.util.Scanner;// Created by WXX on 2021/2/24 19:37
public class Main {public static final int N = 20010;static int n, m;static int[] f = new int[N], g = new int[N], q = new int[N];public static void main(String[] args) {Scanner sn = new Scanner(System.in);n = sn.nextInt(); m = sn.nextInt();for (int i = 0; i < n; i++) {  // 这里采取边读入物品,边处理;每个物品都要进行滑窗处理int v = sn.nextInt(), w = sn.nextInt(), s = sn.nextInt();System.arraycopy(f, 0, g, 0, N);  // 将f中的数据拷贝到g中for (int r = 0; r < v; r++) {int hh = 0, tt = -1;for (int k = r; k <= m; k += v) {if (hh <= tt && q[hh] < k - s * v) hh++;  // 说明队头元素应该 滑出滑窗while (hh <= tt && g[q[tt]] - (q[tt] - r) / v * w <= g[k] - (k - r) / v * w) tt--;q[++tt] = k;  // 将当前考察体积存入滑窗if (hh <= tt) f[k] = Math.max(f[k], g[q[hh]] + (k - q[hh]) / v * w);}}}System.out.println(f[m]);}
}

四. 混合背包问题

问题描述

  • 问题链接:混合背包问题

分析

  • 分不同情况下进行状态转移即可,如下图:

代码

  • C++
// Created by WXX on 2021/2/24 19:59
#include <iostream>using namespace std;const int N = 1010;int n, m;
int f[N];int main() {cin >> n >> m;for (int i = 0; i < n; i++) {int v, w, s;cin >> v >> w >> s;if (s == 0) {  // 完全背包for (int j = v; j <= m; j++) f[j] = max(f[j], f[j - v] + w);} else {if (s == -1) s = 1;  // 全部转为多重背包,然后使用二进制优化for (int k = 1; k <= s; k *= 2) {for (int j = m; j >= k * v; j--)f[j] = max(f[j], f[j - k * v] + k * w);s -= k;}if (s) {for (int j = m; j >= s * v; j--)f[j] = max(f[j], f[j - s * v] + s * w);}}}cout << f[m] << endl;return 0;
}
  • Java
import java.util.Scanner;// Created by WXX on 2021/2/24 20:06
public class Main {public static final int N = 1010;static int n, m;static int[] f = new int[N];public static void main(String[] args) {Scanner sn = new Scanner(System.in);n = sn.nextInt(); m = sn.nextInt();for (int i = 0; i < n; i++) {int v = sn.nextInt(), w = sn.nextInt(), s = sn.nextInt();if (s == 0) {  // 完全背包for (int j = v; j <= m; j++) f[j] = Math.max(f[j], f[j - v] + w);} else {if (s == -1) s = 1;  // 全部转为多重背包,然后使用二进制优化for (int k = 1; k <= s; k *= 2) {for (int j = m; j >= k * v; j--)f[j] = Math.max(f[j], f[j - k * v] + k * w);s -= k;}if (s != 0) {for (int j = m; j >= s * v; j--)f[j] = Math.max(f[j], f[j - s * v] + s * w);}}}System.out.println(f[m]);}
}

五. 二维费用背包问题

问题描述

  • 问题链接:二维费用背包问题

分析

  • 需要说明一点,二维费用背包为可以和 01背包、完全背包、多重背包、分组背包 这四种类型背包中的任何一种组合起来。
  • 当前题目是和 01背包 绑定起来的题目

代码

  • C++
// Created by WXX on 2021/2/24 20:25
#include <iostream>using namespace std;const int N = 110;int n, V, M;
int f[N][N];  // 第一维代表体积,第二维代表重量int main() {cin >> n >> V >> M;for (int i = 0; i < n; i++) {int v, m, w;cin >> v >> m >> w;for (int j = V; j >= v; j--)for (int k = M; k >= m; k--)f[j][k] = max(f[j][k], f[j - v][k - m] + w);}cout << f[V][M] << endl;return 0;
}
  • Java
import java.util.Scanner;// Created by WXX on 2021/2/24 20:34
public class Main {public static final int N = 110;static int n, V, M;static int[][] f = new int[N][N];  // 第一维代表体积,第二维代表重量public static void main(String[] args) {Scanner sn = new Scanner(System.in);n = sn.nextInt(); V = sn.nextInt(); M = sn.nextInt();for (int i = 0; i < n; i++) {int v = sn.nextInt(), m = sn.nextInt(), w = sn.nextInt();for (int j = V; j >= v; j--)for (int k = M; k >= m; k--)f[j][k] = Math.max(f[j][k], f[j - v][k - m] + w);}System.out.println(f[V][M]);}
}

六. 分组背包问题

问题描述

  • 问题链接:分组背包问题

分析

代码

  • C++
// Created by WXX on 2021/2/24 20:52
#include <iostream>using namespace std;const int N = 110;int n, m;
// s[i]: 代表第i组物品的数量;v[i][k],w[i][k]: 第i组中第k个物品的体积和价值
int v[N][N], w[N][N], s[N];
int f[N];int main() {cin >> n >> m;for (int i = 1; i <= n; i++) {cin >> s[i];for (int j = 0; j < s[i]; j++)cin >> v[i][j] >> w[i][j];}for (int i = 1; i <= n; i++)  // 先循环物品for (int j = m; j >= 0; j--)  // 再循环容量。因为某件物品要么选要么不选,所以递减遍历for (int k = 0; k < s[i]; k++)  // 最后循环决策if (j >= v[i][k])f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);cout << f[m] << endl;return 0;
}
  • Java
import java.util.Scanner;// Created by WXX on 2021/2/24 21:01
public class Main {public static final int N = 110;static int n, m;// s[i]: 代表第i组物品的数量;v[i][k],w[i][k]: 第i组中第k个物品的体积和价值static int[] s = new int[N];static int[][] v = new int[N][N], w = new int[N][N];static int[] f = new int[N];public static void main(String[] args) {Scanner sn = new Scanner(System.in);n = sn.nextInt(); m = sn.nextInt();for (int i = 1; i <= n; i++) {s[i] = sn.nextInt();for (int j = 0; j < s[i]; j++) {v[i][j] = sn.nextInt(); w[i][j] = sn.nextInt();}}for (int i = 1; i <= n; i++)  // 先循环物品for (int j = m; j >= 0; j--)  // 再循环容量。因为某件物品要么选要么不选,所以递减遍历for (int k = 0; k < s[i]; k++)  // 最后循环决策if (j >= v[i][k])f[j] = Math.max(f[j], f[j - v[i][k]] + w[i][k]);System.out.println(f[m]);}
}

注意点

  • 其实 多重背包问题分组背包问题 的一个特例。

    对于多重背包问题:对于某个物品来说,如果出现 s 次,则可以选择0次,(1次…s次),我们将(1次…s次)这些情况打包起来形成一组,看成不同的物品(s个),我们最多从中选1个,因此有 s+1 种可能的情况。

七. 背包问题求方案数

问题描述

  • 问题链接:背包问题求方案数

分析

  • 需要说明一点,和二维费用背包一样。背包问题求方案数可以和 01背包、完全背包、多重背包、分组背包 这四种类型背包中的任何一种组合起来。

  • 本题的本质是求最短路径的条数。其实动态规划的本质是图论问题

  • 我们可以根据背包问题的状态转移方程来分析方案数,这里状态转移方程是:f(i,j)=max(f(i−1,j),f(i−1,j−vi)+wi)f(i,j)=max(f(i-1,j), f(i-1,j-vi)+wi)f(i,j)=max(f(i−1,j),f(i−1,j−vi)+wi)。为了计算方案数,这里的 f(i,j)f(i,j)f(i,j) 定义需要做一下改变:f(i,j)f(i,j)f(i,j) 代表只从前 i 个物品中选,体积恰好是 j 的最大价值

  • 为了实现 f(i,j)f(i,j)f(i,j) 所定义的含义,我们需要在初始化是将 f(0, k),即第一行初始化为负无穷,且只有 f(0, 0) = 0。这样的话,负无穷所更新的状态仍然是负无穷,只有被 f(0, 0)更新的状态是有效的状态,因此体积恰好是 j。

  • 为了记录方案数,我们还需要另外开辟一个数组 g(i,j)g(i,j)g(i,j) ,代表的含义是: f(i,j)f(i,j)f(i,j) 取最大值时的方案数。 g(i,j)g(i,j)g(i,j) 的求解存在三种情况:

    (1)如果 f(i−1,j)f(i - 1,j)f(i−1,j) > f(i−1,j−v)+wf(i - 1,j-v) + wf(i−1,j−v)+w ,则 g(i,j)g(i,j)g(i,j) = g(i−1,j)g(i - 1,j)g(i−1,j) ;

    (2)如果 f(i−1,j)f(i - 1,j)f(i−1,j) < f(i−1,j−v)+wf(i - 1,j-v) + wf(i−1,j−v)+w ,则 g(i,j)g(i,j)g(i,j) = g(i−1,j−v)g(i - 1,j-v)g(i−1,j−v) ;

    (3)如果 f(i−1,j)f(i - 1,j)f(i−1,j) = f(i−1,j−v)+wf(i - 1,j-v) + wf(i−1,j−v)+w ,则 g(i,j)g(i,j)g(i,j) = g(i−1,j)g(i - 1,j)g(i−1,j) + g(i−1,j−v)g(i - 1,j-v)g(i−1,j−v) ;

  • 因为本题对应的是 01背包问题,所以上面的二维数组都可以被优化为一维数组,只要在遍历体积的时候从大到小进行遍历即可。

代码

  • C++
// Created by WXX on 2021/2/24 21:56
#include <iostream>
#include <cstring>using namespace std;const int N = 1010, mod = 1e9 + 7;int n, m;
// f[i]: 表示体积恰好为j时的最大价值;g[i]: f[i]取最大值时的方案数
int f[N], g[N];int main() {cin >> n >> m;memset(f, -0x3f, sizeof f);f[0] = 0;g[0] = 1;  // 什么都不选也是一种方案for (int i = 0; i < n; i++) {int v, w;cin >> v >> w;for (int j = m; j >= v; j--) {int maxv = max(f[j], f[j - v] + w);int cnt = 0;if (maxv == f[j]) cnt += g[j];if (maxv == f[j - v] + w) cnt += g[j - v];g[j] = cnt % mod;f[j] = maxv;}}int res = 0;  // 最大价值,因为取到最大价值消耗的体积不一定恰好等于背包容量for (int i = 0; i <= m; i++) res = max(res, f[i]);int cnt = 0;  // 取到最大价值的方案数for (int i = 0; i <= m; i++)if (res == f[i])cnt = (cnt + g[i]) % mod;cout << cnt << endl;return 0;
}
  • Java
import java.util.Arrays;
import java.util.Scanner;// Created by WXX on 2021/2/24 22:08
public class Main {public static final int N = 1010, mod = (int)(1e9 + 7);static int n, m;// f[i]: 表示体积恰好为j时的最大价值;g[i]: f[i]取最大值时的方案数static int[] f = new int[N], g = new int[N];public static void main(String[] args) {Scanner sn = new Scanner(System.in);n = sn.nextInt(); m = sn.nextInt();Arrays.fill(f, Integer.MIN_VALUE);f[0] = 0;g[0] = 1;  // 什么都不选也是一种方案for (int i = 0; i < n; i++) {int v = sn.nextInt(), w = sn.nextInt();for (int j = m; j >= v; j--) {int maxv = Math.max(f[j], f[j - v] + w);int cnt = 0;if (maxv == f[j]) cnt += g[j];if (maxv == f[j - v] + w) cnt += g[j - v];g[j] = cnt % mod;f[j] = maxv;}}int res = 0;  // 最大价值,因为取到最大价值消耗的体积不一定恰好等于背包容量for (int i = 0; i <= m; i++) res = Math.max(res, f[i]);int cnt = 0;  // 取到最大价值的方案数for (int i = 0; i <= m; i++)if (res == f[i])cnt = (cnt + g[i]) % mod;System.out.println(cnt);}
}

八. 求背包问题的方案

问题描述

  • 问题链接:求背包问题的方案

分析

  • 需要说明一点,和二维费用背包以及背包问题求方案数一样。求背包问题的方案可以和 01背包、完全背包、多重背包、分组背包 这四种类型背包中的任何一种组合起来。

  • 我们可以根据背包问题的状态转移方程来求背包的方案,这里状态转移方程是:f(i,j)=max(f(i−1,j),f(i−1,j−vi)+wi)f(i,j)=max(f(i-1,j), f(i-1,j-vi)+wi)f(i,j)=max(f(i−1,j),f(i−1,j−vi)+wi)。这里存在三种情况

    (1) f(i,j)f(i,j)f(i,j) == f(i−1,j)f(i-1,j)f(i−1,j) ,f(i,j)f(i,j)f(i,j) > f(i−1,j−vi)+wif(i-1,j-vi)+wif(i−1,j−vi)+wi ,则最大价值的方案中必定不包含物品 i ;

    (2) f(i,j)f(i,j)f(i,j) > f(i−1,j)f(i-1,j)f(i−1,j) ,f(i,j)f(i,j)f(i,j) == f(i−1,j−vi)+wif(i-1,j-vi)+wif(i−1,j−vi)+wi ,则最大价值的方案中必定包含物品 i ;

    (3) f(i,j)f(i,j)f(i,j) == f(i−1,j)f(i-1,j)f(i−1,j) ,f(i,j)f(i,j)f(i,j) == f(i−1,j−vi)+wif(i-1,j-vi)+wif(i−1,j−vi)+wi ,则最大价值的方案中可包含也可不包含物品 i ;

  • 另外,本题还要求输出 字典序最小的方案,因此我们应该反过来遍历物品(从物品n开始遍历到物品1),求出数组 f 后,之后就可以从物品1开始考察,能选则选,然后考虑物品2,…,这样得到的结果字典序最小。如果还是从物品1开始遍历,则之后只能从物品n开始考察,无法保证字典序最小。

  • 注意:我们此时不能将二维数组压缩为一维数组,这是因为数组 f 中间的状态还需要被使用

  • 除了上面这种解决方案外,还可以通过创建一个bool数组记录某个物品是否应该被选择。

代码

  • C++
// Created by WXX on 2021/2/24 22:27
#include <iostream>using namespace std;const int N = 1010;int n, m;
int v[N], w[N];
int f[N][N];int main() {cin >> n >> m;for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];for (int i = n; i >= 1; i--)for (int j = 0; j <= m; j++) {f[i][j] = f[i + 1][j];if (j >= v[i]) f[i][j] = max(f[i][j], f[i + 1][j - v[i]] + w[i]);}int j = m;for (int i = 1; i <= n; i++)if (j >= v[i] && f[i][j] == f[i + 1][j - v[i]] + w[i]) {cout << i << ' ';  // 如果能选物品i的话一定要选,这样可保证字典序最小j -= v[i];}return 0;
}
  • Java
import java.util.Scanner;// Created by WXX on 2021/2/24 22:33
public class Main {public static final int N = 1010;static int n, m;static int[] v = new int[N], w = new int[N];static int[][] f = new int[N][N];public static void main(String[] args) {Scanner sn = new Scanner(System.in);n = sn.nextInt(); m = sn.nextInt();for (int i = 1; i <= n; i++) {v[i] = sn.nextInt(); w[i] = sn.nextInt();}for (int i = n; i >= 1; i--)for (int j = 0; j <= m; j++) {f[i][j] = f[i + 1][j];if (j >= v[i]) f[i][j] = Math.max(f[i][j], f[i + 1][j - v[i]] + w[i]);}int j = m;for (int i = 1; i <= n; i++)if (j >= v[i] && f[i][j] == f[i + 1][j - v[i]] + w[i]) {System.out.print(i + " ");;  // 如果能选物品i的话一定要选,这样可保证字典序最小j -= v[i];}}
}

九. 有依赖的背包问题

问题描述

  • 问题链接:有依赖的背包问题

分析

  • 这一题是一道 树形DP 的问题
  • 这是一棵树,我们采取的存储方式邻接表,实现方式是数组模拟链表,这部分内容可以参照AcWing的算法基础课:网址

代码

  • C++
// Created by WXX on 2021/2/24 23:08
#include <iostream>
#include <cstring>using namespace std;const int N = 110;int n, m;
int v[N], w[N];
int h[N], e[N], ne[N], idx = 0;  // 邻接矩阵
int f[N][N];void add(int a, int b) {e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}void dfs(int u) {for (int i = h[u]; ~i; i = ne[i]) {  // 遍历u的子节点,相当于循环物品组int son = e[i];dfs(son);  // 求解完成子节点后才能求解当前节点// 分组背包for (int j = m - v[u]; j >= 0; j--)  // 循环体积for (int k = 0; k <= j; k++)  // 循环决策f[u][j] = max(f[u][j], f[u][j - k] + f[son][k]);}// 将物品u加进去for (int i = m; i >= v[u]; i--) f[u][i] = f[u][i - v[u]] + w[u];for (int i = 0; i < v[u]; i++) f[u][i] = 0;  // 不能放入物品u,则整棵子树都不能放入
}int main() {cin >> n >> m;memset(h, -1, sizeof h);int root = 1;  // 根节点并不一定是1号点for (int i = 1; i <= n; i++) {int p;cin >> v[i] >> w[i] >> p;if (p == -1) root = i;else add(p, i);  // 添加一条由p指向i的边}dfs(root);cout << f[root][m] << endl;return 0;
}
  • Java
import java.util.Arrays;
import java.util.Scanner;// Created by WXX on 2021/2/24 23:24
public class Main {public static final int N = 110;static int n, m;static int[] v = new int[N], w = new int[N];// 邻接矩阵static int[] h = new int[N], e = new int[N], ne = new int[N];static int idx = 0;static int[][] f = new int[N][N];private static void add(int a, int b) {e[idx] = b; ne[idx] = h[a]; h[a] = idx++;}private static void dfs(int u) {for (int i = h[u]; i != -1; i = ne[i]) {  // 遍历u的子节点,相当于循环物品组int son = e[i];dfs(son);  // 求解完成子节点后才能求解当前节点// 分组背包for (int j = m - v[u]; j >= 0; j--)  // 循环体积for (int k = 0; k <= j; k++)  // 循环决策f[u][j] = Math.max(f[u][j], f[u][j - k] + f[son][k]);}// 将物品u加进去for (int i = m; i >= v[u]; i--) f[u][i] = f[u][i - v[u]] + w[u];for (int i = 0; i < v[u]; i++) f[u][i] = 0;  // 不能放入物品u,则整棵子树都不能放入}public static void main(String[] args) {Scanner sn = new Scanner(System.in);n = sn.nextInt(); m = sn.nextInt();Arrays.fill(h, -1);int root = 1;  // 根节点并不一定是1号点for (int i = 1; i <= n; i++) {v[i] = sn.nextInt(); w[i] = sn.nextInt();int p = sn.nextInt();if (p == -1) root = i;else add(p, i);  // 添加一条由p指向i的边}dfs(root);System.out.println(f[root][m]);}
}

LeetCode上一些背包问题

Leetcode 0279 完全平方数

问题描述

  • 问题链接:Leetcode 0279 完全平方数

分析

  • 完全背包问题,把n当做背包容量,每个完全平方数当做物品,其对应的数值数值当做体积,每个数的价值都为1,则问题变成在恰好装满背包的情况下,总价值最少是多少。完全背包问题的时间复杂度为O(体积*物品个数),所以该题的时间复杂度为 O(n∗n)O(n* \sqrt n)O(n∗n​)。

代码

  • C++
/*** 执行用时:204 ms, 在所有 C++ 提交中击败了69.60%的用户* 内存消耗:8.7 MB, 在所有 C++ 提交中击败了83.16%的用户*/
class Solution {public:int numSquares(int n) {vector<int> f(n + 1, 1e9);  // f[i] 代表 i 最少可以由几个完全平方数表示f[0] = 0;for (int i = 1; i * i <= n; i++)  // 先循环物品 i*ifor (int j = i * i; j <= n; j++)  // 再循环体积:体积从i*i开始f[j] = min(f[j], f[j - i * i] + 1);return f[n];}
};
  • Java
/*** 执行用时:30 ms, 在所有 Java 提交中击败了86.04%的用户* 内存消耗:37.8 MB, 在所有 Java 提交中击败了34.09%的用户*/
public class Solution {public int numSquares(int n) {int[] f = new int[n + 1];Arrays.fill(f, Integer.MAX_VALUE);f[0] = 0;for (int i = 1; i * i <= n; i++)  // 先循环物品 i*ifor (int j = i * i; j <= n; j++)  // 再循环体积:体积从i*i开始f[j] = Math.min(f[j], f[j - i * i] + 1);return f[n];}
}

Leetcode 0322 零钱兑换

问题描述

  • 问题链接:Leetcode 0322 零钱兑换

分析

  • 完全背包问题
  • amout为容量;物品体积为coins[i],价值为1
  • 和上一题十分类似,这里就不详细分析了

代码

  • C++
/*** 执行用时:64 ms, 在所有 C++ 提交中击败了89.56%的用户* 内存消耗:13.7 MB, 在所有 C++ 提交中击败了66.94%的用户*/
class Solution {public:int coinChange(vector<int> &coins, int m) {vector<int> f(m + 1, 1e8);f[0] = 0;for (auto v : coins)for (int j = v; j <= m; j++)f[j] = min(f[j], f[j - v] + 1);if (f[m] == 1e8) return -1;return f[m];}
};
  • Java
/*** 执行用时:12 ms, 在所有 Java 提交中击败了94.69%的用户* 内存消耗:38.1 MB, 在所有 Java 提交中击败了22.43%的用户*/
public class Solution {public int coinChange(int[] coins, int m) {int[] dp = new int[m + 1];Arrays.fill(dp, m + 1);dp[0] = 0;for (int v : coins)for (int j = v; j <= m; j++)dp[j] = Math.min(dp[j], dp[j - v] + 1);return dp[m] == m + 1 ? -1 : dp[m];}
}

背包问题(背包九讲)相关推荐

  1. 背包问题——背包九讲

    本帖主要为转载,只是供我学习,如有侵权,请联系我删除. 前言 本篇文章是我(dd_engi)正在进行中的一个雄心勃勃的写作计划的一部分,这个计划的内容是写作一份较为完善的NOIP难度的动态规划总结,名 ...

  2. dp背包九讲(待补充,暂时前两讲)

    文章目录 背包九讲 一.01背包 二维dp 优化为一维 二.完全背包 二维dp 优化为一维 三.多重背包 数据范围很小时,变成01背包暴力 当数据范围较大时,利用二进制优化 二进制优化还不够用时,利用 ...

  3. 完全背包问题python_令人头疼的背包九讲(2)完全背包问题

    重磅干货,第一时间送达 背包问题是一个经典的动态规划模型.它既简单形象容易理解,又在某种程度上能够揭示动态规划的本质,故不少教材都把它作为动态规划部分的第一道例题. 题目 完全背包问题 题目要求 有n ...

  4. 背包九讲之一:01背包问题

    文章目录 说明: 01背包问题 题目 基本思路 初始化的细节问题 优化空间复杂度 相关题目练习 题目URL 输入格式 输出格式 数据范围 输入样例 输出样例: 题目解法 说明: 本文所讲内容摘录自崔添 ...

  5. 背包九讲之二:完全背包问题

    文章目录 说明: 完全背包问题 题目 基本思路 一个简单有效的优化 相关题目练习 题目URL 输入格式 输出格式 数据范围 输入样例 输出样例: 题目解法 说明: 本文所讲内容摘录自崔添翼:背包九讲, ...

  6. dd大牛的背包九讲-背包问题汇总

    背包九讲 目录  第一讲 01背包问题  第二讲 完全背包问题  第三讲 多重背包问题  第四讲 混合三种背包问题  第五讲 二维费用的背包问题  第六讲 分组的背包问题  第七讲 有依赖的背包问题  ...

  7. 第二讲 完全背包问题(对背包九讲的学习)

    学习自:背包九讲 题目 有N种物品和一个容量为V的背包,每种物品都有无限件可用.第i种物品的费用是c[i],价值是w[i].求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大. ...

  8. 【算法】动态规划+“背包九讲”原理超详细讲解+常见dp问题(9种)总结

    目录 一.动态规划(DP) 二.背包九讲 (1)完全背包 P1616 疯狂的采药(完全背包) (2)01背包 滚动数组 一维数组 P1048 采药(01背包) 01背包表格图示 (3)多重背包 整数拆 ...

  9. 算法--背包九讲(详细讲解+代码)

    背包九讲 目录  第一讲 01背包问题  第二讲 完全背包问题  第三讲 多重背包问题  第四讲 混合三种背包问题  第五讲 二维费用的背包问题  第六讲 分组的背包问题  第七讲 有依赖的背包问题  ...

  10. dalao的背包九讲

    背包九讲 目录  第一讲 01背包问题  第二讲 完全背包问题  第三讲 多重背包问题  第四讲 混合三种背包问题  第五讲 二维费用的背包问题  第六讲 分组的背包问题  第七讲 有依赖的背包问题  ...

最新文章

  1. 1.Socket通信
  2. 我所遇到的C++连接问题汇总
  3. android自动填充包名,debug/release 修改包名,取不同包名下的agconnect-services.json 文件...
  4. java 8时间操作_Java8 时间日期类操作
  5. 如何使用粒子氩气进行位置跟踪
  6. The Rise of Worse Is Better 论文-学习笔记
  7. Cookie工具类-cookieUtil.js
  8. 中国电信天翼云进入4.0阶段 打造一朵无处不在的分布式云
  9. fc588热敏打印机驱动_为称重仪表设计热敏打印模块 - 工业自动化称重仪表
  10. 剑指Offer之寻找链表倒数第k个节点
  11. FreeNAS 安装与用户指导页
  12. 华为/华三IS-IS单区域配置
  13. 公司为什么需要自己的SAP内部顾问
  14. 你是《未来世界的幸存者》么?
  15. 如何进行航拍全景摄影(上)
  16. Android2018年最新前沿框架和技术
  17. 马未都说收藏:陶瓷篇(6、7)宋代八大民窑
  18. 十年感悟之 python之路
  19. sqlDBX 链接 mysql 提示ODBC驱动不正确
  20. windows7电脑备份系统教程

热门文章

  1. 基于Java毕业设计优课网设计与实现源码+系统+mysql+lw文档+部署软件
  2. PCB设计布局思路分析
  3. getch方法_c语言中中getch()函数为什么不起作用?
  4. linux数组fortran,Fortran77-90-95入门(十)--数组
  5. 《云计算核心技术剖析》学习笔记
  6. SQL Server详细教程
  7. [Python从零到壹] 十.网络爬虫之Selenium爬取在线百科知识万字详解(NLP语料构造必备技能)
  8. SecureCRT下载、安装、激活
  9. oracle中avl,AVL CRUISE软件功能简介
  10. 冰点下载器手机版apk_冰点文库下载器app下载