0/1背包问题是非常经典的算法问题. 要求是: 给定 n 个物件, 每个物件的重量(或体积)是 A[i] 那么请问一个最多装重(或体积) m 的背包最多可以装下多少这些物件.
如果用数学表示这个问题:
max(sum(j[i] * A[i]))
subject to: j[i] = {0, 1} and sum(j[i] * A[i]) <= m
where 0 =< i < n
j[i] 就是控制是否装下第 i 个 物件 的变量(0或者1)
回溯算法
把整个搜索空间从第一个物件开始当成树根, 那么这是一棵二叉树, 每层的节点就能对应到每个物品, 两个分叉分别代表是否把当前节点(物品)装入背包中.
不难想像, 时间复杂度为 O(2^n). 如果当前背包并不能装下当前物件, 那么我们需要继续尝试下一个物品, 直到所有物品尝试完并回溯.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | class Solution { public: /** * @param m: An integer m denotes the size of a backpack * @param A: Given n items with size A[i] * @return: The maximum size */ int backPack(int m, vector<int> &A) { pack(0, m, A); return curMax; } void pack(int n, int weight, vector<int> &A) { int sz = A.size(); int m = 0; for (int i = n; i < sz; i ++) { pack(n + 1, weight, A); // 不放 if (weight >= A[i]) { // 可以放 m += A[i]; if (m > curMax) { curMax = m; } weight -= A[i]; pack(n + 1, weight, A); // 放 } } } private: int curMax = 0; }; |
class Solution { public: /** * @param m: An integer m denotes the size of a backpack * @param A: Given n items with size A[i] * @return: The maximum size */ int backPack(int m, vector<int> &A) { pack(0, m, A); return curMax; } void pack(int n, int weight, vector<int> &A) { int sz = A.size(); int m = 0; for (int i = n; i < sz; i ++) { pack(n + 1, weight, A); // 不放 if (weight >= A[i]) { // 可以放 m += A[i]; if (m > curMax) { curMax = m; } weight -= A[i]; pack(n + 1, weight, A); // 放 } } } private: int curMax = 0; };
这个算法的问题就是慢. 特别是物品数量多的时候, 整个搜索空间就变得巨大.
动态规化
如果我们用 dp[i][j] 来表示是否可以用最多 前 i 件物品来装最多重量(或者体积) 为 j . 那么:
1 | dp[i][j] = dp[i - 1][j] || dp[i - 1][j - A[i - 1]]; |
dp[i][j] = dp[i - 1][j] || dp[i - 1][j - A[i - 1]];
我们可以反过来理解:
- 如果dp[i][j] 那么下一件物品我们不装的话: dp[i + 1][j]
- 如果dp[i][j] 那么装下下一件物品: dp[i + 1][j + A[i + 1]]
动态规化的优点就是可以把中间结果给缓存起来, 这样就不会像递规回溯一样有些结果(中间节点)会重复计算.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | class Solution { public: /** * @param m: An integer m denotes the size of a backpack * @param A: Given n items with size A[i] * @return: The maximum size */ int backPack(int m, vector<int> &A) { int size = A.size(); vector< vector<bool> > dp(size + 1, vector<bool>(m + 1, false)); for (int i = 0; i < size + 1; ++ i) { dp[i][0] = true; } for(int i = 1; i < A.size() + 1; i++) { for(int j = 1; j < m + 1; j++) { if(j < A[i - 1]) { // 装不下了 dp[i][j] = dp[i - 1][j]; } else { dp[i][j] = dp[i - 1][j] || dp[i - 1][j - A[i - 1]]; } } } for (int i = m; i >= 0; i--) { // 反过来从大到小检查能装下的最大重量 if (dp[size][i]) { return i; } } return 0; } }; |
class Solution { public: /** * @param m: An integer m denotes the size of a backpack * @param A: Given n items with size A[i] * @return: The maximum size */ int backPack(int m, vector<int> &A) { int size = A.size(); vector< vector<bool> > dp(size + 1, vector<bool>(m + 1, false)); for (int i = 0; i < size + 1; ++ i) { dp[i][0] = true; } for(int i = 1; i < A.size() + 1; i++) { for(int j = 1; j < m + 1; j++) { if(j < A[i - 1]) { // 装不下了 dp[i][j] = dp[i - 1][j]; } else { dp[i][j] = dp[i - 1][j] || dp[i - 1][j - A[i - 1]]; } } } for (int i = m; i >= 0; i--) { // 反过来从大到小检查能装下的最大重量 if (dp[size][i]) { return i; } } return 0; } };
时间复杂度和空间复杂度都是 O(nm). 最后面我们只要反过来检查 dp[A.size()][j] 为 true 的第一个下标 j 就是当前背包能装下的最大重量.
DP 内存优化
上面的DP公式我们不难发现, 在计算 dp[i] 的时候只依赖于 dp[i-1], 这样我们就可以只用一维来存取 dp[i-1] 的情况. 这样, 空间复杂度从 O(mn) 就降到了 O(m).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | class Solution { public: /** * @param m: An integer m denotes the size of a backpack * @param A: Given n items with size A[i] * @return: The maximum size */ int backPack(int m, vector<int> &A) { int size = A.size(); vector<bool> dp(m + 1, false); dp[0] = true; for (int i = 1; i < A.size() + 1; i++) { for (int j = m; j >= 1; --j) { // // 内循环需要从 m 到 1 的检查 if(j >= A[i - 1]) { dp[j] = dp[j] || dp[j - A[i - 1]]; } } } for (int i = m; i >= 0; i--) { if (dp[i]) { return i; } } return 0; } }; |
class Solution { public: /** * @param m: An integer m denotes the size of a backpack * @param A: Given n items with size A[i] * @return: The maximum size */ int backPack(int m, vector<int> &A) { int size = A.size(); vector<bool> dp(m + 1, false); dp[0] = true; for (int i = 1; i < A.size() + 1; i++) { for (int j = m; j >= 1; --j) { // // 内循环需要从 m 到 1 的检查 if(j >= A[i - 1]) { dp[j] = dp[j] || dp[j - A[i - 1]]; } } } for (int i = m; i >= 0; i--) { if (dp[i]) { return i; } } return 0; } };
英文: Algorithms Series: 0/1 BackPack – Dynamic Programming and BackTracking
强烈推荐
- 英国代购-畅购英伦
- TopCashBack 返现 (英国购物必备, 积少成多, 我2年来一共得了3000多英镑)
- Quidco 返现 (也是很不错的英国返现网站, 返现率高)
- 注册就送10美元, 免费使用2个月的 DigitalOcean 云主机(性价比超高, 每月只需5美元)
- 注册就送10美元, 免费使用4个月的 Vultr 云主机(性价比超高, 每月只需2.5美元)
- 注册就送10美元, 免费使用2个月的 阿里 云主机(性价比超高, 每月只需4.5美元)
- 注册就送20美元, 免费使用4个月的 Linode 云主机(性价比超高, 每月只需5美元) (折扣码: PodCastInit2022)
- PlusNet 英国光纤(超快, 超划算! 用户名 doctorlai)
- 刷了美国运通信用卡一年得到的积分 换了 485英镑
- 注册就送50英镑 – 英国最便宜最划算的电气提供商
- 能把比特币莱特币变现的银行卡! 不需要手续费就可以把虚拟货币法币兑换
微信公众号: 小赖子的英国生活和资讯 JustYYUK