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
loading...
上一篇: 《Steem 指南》之 justyy 在线工具与 API 系列 - 同时给多个帐号发送SBD或者STEEM
下一篇: 屌丝与美女的硬币游戏(概率和期望值)