背包九讲详解
来源:互联网 发布:单片机控制舵机程序 编辑:程序博客网 时间:2024/06/15 07:39
背包九讲详解
0-1背包问题
有n个重量和价值分别为
样例:
n=4 (w,v)=(2,3),(1,2),(3,4),(2,2) W=5
n个物品,每种物品只有两种选择,放进背包,或者不放进背包。n个物品对应的,最大的所有可能的总数为
最朴素的,我们可以枚举所有可能的放法,找到最大值。
还剩
1. 第
2. 第
总和两种可能,取较大值。可以给出递推式:
还需要考虑一些细节:
1. 终止条件,
0个物品可以选择,放入容量为j的背包, 得到的最大价值只能为0
2. j < w[i],背包剩余容量不足以放下第i个物品,
综上,得到递推式:
递归方法:
#include <iostream>#define MAXN 10000using namespace std;int w[MAXN] = {0, 2, 1, 3, 2};int v[MAXN] = {0, 3, 2, 4, 2};int W = 5, n = 4;int Rec(int i, int j) { int res; if (i == 0) { // 终止条件, 无物品可选 Rec(0, j) = 0 // 0个物品可以选择,放入容量为j的背包, 得到的最大价值只能为0 res = 0; } else if (j < w[i]) { // 背包剩余容量不足以放下第i个物品 res = Rec(i-1, j); } else { // 抉择,第i个物品选或者不选,都试一下,取较大值 res = max(Rec(i-1, j), Rec(i-1, j-w[i]) + v[i]); } return res;}int main() { cout << Rec(n, W) << endl; return 0;}
大概画个递归过程:
Rec(4, 5) / \ Rec(3, 3) Rec(3, 5) / \ / \ Rec(2, 0) Rec(2, 3) Rec(2, 2) Rec(2, 5) / / \ / \ / \ Rec(1, 0) Rec(1, 2) Rec(1, 3) Rec(1, 1) Rec(1, 2) Rec(1, 4) Rec(1, 5) / / \ / \ / / \ / \ / \ (0,0) (0,0) (0,2) (0,1) (0,3) (0,1) (0,0) (0,2) (0,2) (0,4) (0,3) (0,5)
从上图可以看出,我们枚举了所有可能的情况,即对于每个物品i,物品i选还是不选,我们都考虑了一遍。递归的层数是n+1层,最后一层是判定终止。
再来重申一下
在求解
记忆化搜索
我们可以利用递归求解过程中的重复计算。如果
#include <iostream>#include <cstring>#define MAXN 1000using namespace std;int w[MAXN] = {0, 2, 1, 3, 2};int v[MAXN] = {0, 3, 2, 4, 2};int dp[MAXN][MAXN]; //记录搜索过的结果int W = 5, n = 4;int Rec(int i, int j) { //Rec(i, j)计算过,直接拿来用 if (dp[i][j] != -1) return dp[i][j]; int res; if (i == 0) { res = 0; } else if (j < w[i]) { res = Rec(i-1, j); } else { res = max(Rec(i-1, j), Rec(i-1, j-w[i]) + v[i]); } return dp[i][j] = res; //记录}int main() { memset(dp, -1, sizeof(dp)); cout << Rec(n, W) << endl; return 0;}
对于任意的
动态规划
上面的递归过程,是把大问题分解成小问题,最后由小问题的解合并成大问题的解。
动态规划的方法,就是先把小问题的解计算好,存在表里,等计算大问题的时候,需要用到小问题的解,就过来查一下表。
由递推式:
转化为动态规划的写法:
代码:
#include <iostream>#include <cstring>#define MAXN 1000using namespace std;int dp[MAXN][MAXN];int w[MAXN] = {0, 2, 1, 3, 2};int v[MAXN] = {0, 3, 2, 4, 2};int W = 5, n = 4;int solve(int n, int W) { memset(dp, 0, sizeof(dp)); for (int i = 1; i <= n; i++) { //i从1开始,因为i=0的值已经确定为0 for (int j = 0; j <= W; j++) { if (j < w[i]) { dp[i][j] = dp[i-1][j]; } else { dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]] + v[i]); } } } return dp[n][W];}int main() { cout << solve(n, W) << endl; return 0;}
dp 优化空间复杂度
输出一下上面的二维数组
0 0 0 0 0 0
0 0 3 3 3 3
0 2 3 5 5 5
0 2 3 5 6 7
0 2 3 5 6 7
我们是填
所以我们可以从这里入手优化空间复杂度。没有必要存下整个二维数组,我们只需要存2行,然后不断更新这2行就可以了。
实际上,
所以,我们可以只存1行,就能完成整个
伪代码:
for i = 1 to n for j = W to w[i] dp[j] = max(dp[j], dp[j-w[i]] + v[i])
代码:
#include <iostream>#include <cstring>#define MAXN 10000using namespace std;int dp[MAXN];int w[MAXN] = {0, 2, 1, 3, 2};int v[MAXN] = {0, 3, 2, 4, 2};int W = 5, n = 4;int solve(int n, int W) { memset(dp, 0, sizeof(dp)); for (int i = 1; i <= n; i++) { // i从1开始,递增 for (int j = W; j >= 0; j--) { // j按递减顺序填表 if (j < w[i]) { dp[j] = dp[j]; } else { dp[j] = max(dp[j], dp[j-w[i]] + v[i]); } } } return dp[W];}int main() { cout << solve(n, W) << endl; return 0;}
初始化的细节问题
我们看到的求解最优解的背包问题中,事实和桑有两种不太相同的问法。
1. 要求”背包恰好装满“ 时的最优解
2. 不要求背包一定要被装满时的最优解
我们上面所讨论的就是第2种, 不要求背包一定要被装满时的最优解。
一种区别这两种问法的实现方法是在初始化的时候有所不不同。
如果是第一种问法,要求恰好装满背包,那么在初始化时除了
如果并没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该将
这是为什么呢?可以这样理解:初始化的
如果背包并非必须被装满,那么任何容量的背包都有一个合法解 “什么也不装”,这个解的价值为0,所以初始化时状态的值也就全部为0了。
完全背包问题
有
在0-1背包问题中,每种物品只有不选和选两种可能。在完全背包问题中,每个物品可以选0, 1, …
代码:
#include <iostream>#include <cstring>#define MAXN 1000using namespace std;int dp[MAXN][MAXN];int w[MAXN] = {0, 3, 4, 2};int v[MAXN] = {0, 4, 5, 3};int W = 7, n = 3;int solve(int n, int W) { memset(dp, 0, sizeof(dp)); for (int i = 1; i <= n; i++) { for (int j = 0; j <= W; j++) { for (int k = 0; k <= j/w[i]; k++) { dp[i][j] = max(dp[i][j], dp[i-1][j-k*w[i]] + k*v[i]); } } } return dp[n][W];}int main() { cout << solve(n, W) << endl; // 10 return 0;}
最坏情况下复杂度为
再重申一下,
再来看一下上面的递推公式:
*
*
注意上面红色标识的
综上,可以得出递推公式:
更正式地推导一下:
dp[i][j]=max(dp[i−1][j−k×w[i]]+k×v[i]),0≤k≤⌊j/wi⌋ =max(dp[i−1][j],max(dp[i−1][j−k×w[i]]+k×v[i])),1≤k≤⌊j/wi⌋
=max(dp[i−1][j], max(dp[i−1][(j−w[i])−(k−1)×w[i]]+(k−1)×v[i]) +v[i]), 0≤(k−1)≤⌊j/wi⌋−1
=max(dp[i−1][j], dp[i][j−w[i]] +v[i])
刚好和上面的(1)式一样。
代码:
#include <iostream>#include <cstring>#define MAXN 1000using namespace std;int dp[MAXN][MAXN];int w[MAXN] = {0, 3, 4, 2};int v[MAXN] = {0, 4, 5, 3};int W = 7, n = 3;int solve(int n, int W) { memset(dp, 0, sizeof(dp)); for (int i = 1; i <= n; i++) { for (int j = 0; j <= W; j++) { if (j < w[i]) {// 不能塞的时候也不能硬塞,要注意一下 dp[i][j] = dp[i-1][j]; } else { dp[i][j] = max(dp[i-1][j], dp[i][j-w[i]] + v[i]); } } } return dp[n][W];}int main() { cout << solve(n, W) << endl; // 10 return 0;}
类似0-1背包问题, 完全背包问题也可以在空间上进行优化。
伪代码:
for i = 1 to n for j = 0 to W dp[j] = max(dp[j], dp[j-w[i]] + v[i])
我们可以发现,完全背包的伪代码和0-1背包的伪代码非常像,只是第二层循环j的顺序不同,0-1背包是逆序循环,完全背包是正序循环。0-1背包问题为何逆序,之前已经讲得非常清楚了。完全背包问题第二层的j正序循环,因为
代码:
#include <iostream>#include <cstring>#define MAXN 1000using namespace std;int dp[MAXN];int w[MAXN] = {0, 3, 4, 2};int v[MAXN] = {0, 4, 5, 3};int W = 7, n = 3;int solve(int n, int W) { memset(dp, 0, sizeof(dp)); for (int i = 1; i <= n; i++) { for (int j = 0; j <= W; j++) { if (j < w[i]) { dp[j] = dp[j]; } else { dp[j] = max(dp[j], dp[j-w[i]] + v[i]); } } } return dp[W];}int main() { cout << solve(n, W) << endl; // 10 return 0;}
多重背包问题
有
转化成0-1背包问题
多重背包问题,最简单的解法,就是转化成0-1背包问题。第
下面给出一个二进制拆分的多重背包模板:
const int N = 100, W = 100000;int cost[N], weight[N], number[N];int dp[W + 1];int knapsack(int n, int w){ for (int i = 0; i < n; ++i) { int num = min(number[i], w / weight[i]); for (int k = 1; num > 0; k*=2) { if (k > num) k = num; num -= k; for (int j = w; j >= weight[i] * k; --j) dp[j] = max(dp[j], dp[j - weight[i] * k] + cost[i] * k); } } return dp[w];}
时间复杂度为
单调队列优化
多重背包问题的递归式为:
根据递推式,很容易能写出三重循环版本的多重背包代码。但这明显不是我们想要的。是否有方法能像完全背包问题一样,能有
是的,有。
通过巧妙的构造,我们就可以用单调队列来优化多重背包问题, 使得复杂度降为
什么是单调队列: http://blog.csdn.net/justmeh/article/details/5844650
用单调队列优化: http://blog.csdn.net/flyinghearts/article/details/5898183
多重背包 单调队列模板:
#include <iostream>#include <deque>#include <algorithm>using namespace std;struct Pack{ int sum, cost; Pack(int s, int c) : sum (s), cost(c) {}};const int Maxv = 1001;deque <Pack> Q;int N, V, F[Maxv];int main(){ cin >> N >> V; for (int i = 1, p, w, c; i <= N; i ++) { cin >> p >> w >> c; p = min(p, V / w); for (int j = 0; j < w; j ++) { Q.clear(); for (int k = 0; k <= (V - j) / w; k ++) { int y = F[k * w + j] - k * c; while (Q.size() && Q.back().cost <= y) Q.pop_back(); Q.push_back(Pack(k, y)); if (Q.front().sum < k - c) Q.pop_front(); F[k * w + j] = Q.front().cost + k * c; } } } cout << F[V] << endl; return 0;
目前第三讲刚开了个头。
未完待续,我会尽快补全9讲的。
优秀的背包问题模板:http://blog.csdn.net/libin56842/article/details/9396649;
- 背包九讲详解
- 详解--背包九讲
- 背包九讲
- 背包九讲
- 【转】背包九讲
- 背包问题九讲
- 背包九讲
- 背包九讲
- 背包九讲
- 背包九讲
- 背包九讲!!!
- 背包九讲
- 背包九讲
- 背包问题九讲
- 背包九讲
- 背包九讲
- 背包九讲
- 背包九讲
- 【哈佛商评】人工智能的创造力总会有极限
- gVim, gVim Easy, gVim Read-only 的简单区别
- Leetcode 338 Counting Bits
- Java 中继承之后同名静态变量的问题分析
- C++中 虚函数及包含多态的实现
- 背包九讲详解
- 机器学习基础——BP算法
- const 修饰函数参数,返回值,函数体
- C++中 异常处理的 实现方式
- 关于 强制类型转换的探究
- [TravelNotes] ZJOI 2017 DAY2 酱油记
- reason about thread is blocked
- 241. Different Ways to Add Parentheses
- Reflect反射的基础知识