前言
本文主要针对01背包的解决方法以及思路。
先介绍一下dp问题的本质:通过局部最优解达到全局最优解,避免不必要的重复计算(也就是冗余),注意:动态规划不是搜索,所以可以用较短的时间解决问题。
dp问题一般具有以下特性:
- 子问题和主问题相似,具有相同的数据结构
- 通过合理规划子问题的最优解,以达到全局情况的最优解
01背包
01背包是这么个问题(如果已了解过可跳过本部分):
有 n n n件物品,每件物品都有两种属性:价值 C C C 和重量 W W W。
背包也有一个给定的载重量 M M M,并且背包内物品的总重量不可以超过 M M M。
求能获得的最大总价值。
输入
第一行:两个整数, M M M(背包容量, M < = 200 M<=200 M<=200)和 N N N(物品数量, N < = 30 ) N<=30) N<=30);
第 2 ⋯ N + 1 2\cdots N+1 2⋯N+1 行:每行二个整数 W i W_i Wi, C i C_i Ci,表示每个物品的重量和价值。
输出
仅一行,一个数,表示最大总价值。
这里是样例题目链接:01背包题目链接
思路
为了方便,我先把题目的自测数据打在这里:
输入
10 4
2 1
3 3
4 5
7 9
输出
12
解释
选择第三、第五件物品,总重量为 10 10 10,总价值为 12 12 12。
这里主要分析一下思路,暂且不提状态转移方程式。
我们可以假定一开始背包的重量是 0 0 0,然后慢慢累加到 M M M。
还假定一开始物品的数量为 0 0 0件,然后慢慢累加到 n n n件。
这就需要一个二维数组,我们姑且称它为 d p [ ] [ ] dp[][] dp[][](这里方括号内的数随便你怎么填,只要满足题目大小就行。第一个方括号代表物品数量(或者也可以理解为第 i i i 个物品),第二个方括号代表背包大小)。
二维dp数组的表格形式:
物品数量 | 背包容量 | 答案 |
---|---|---|
0 | 0 | 0 |
⋯ \cdots ⋯ | ⋯ \cdots ⋯ | ⋯ \cdots ⋯ |
n n n | m m m | 最终答案 |
数组 w w w和 c c c的表格形式:
重量 | 价值 |
---|---|
w 1 w_1 w1 | c 1 c_1 c1 |
⋯ \cdots ⋯ | ⋯ \cdots ⋯ |
w n w_n wn | c n c_n cn |
很显然 d p [ 0 ] [ 0 ] = 0 dp[0][0]=0 dp[0][0]=0,原因有两个,但重点是:背包装得下吗?(对于 d p [ 0 ] [ 0 ] dp[0][0] dp[0][0],明显装不下,因为题目保证物品重量 ≥ 0 \ge 0 ≥0)。
然后再进一步:对于任何 d p [ i ] [ 0 ] dp[i][0] dp[i][0]肯定都等于 0 0 0,因为背包容量为 0 0 0的背包肯定装不下任何东西。
初始化数据完成后,可以开始递推了。
使用 f o r for for循环遍历 d p dp dp数组:
for (int i = 1; i <= n; ++i) {
for (int j = 1/* 以后会优化 */; j <= n; ++j) {
// 递推代码
}
}
递推代码
我们来想一想:什么时候背包的价值可以增加?
就是当容量足够的时候。
所以从 d p [ i ] [ w [ i ] ] dp[i][w[i]] dp[i][w[i]]开始,总价值就发生变化:它可以增加了。但是真的只要加就好了吗?NO! 问题肯定不会这么简单! (要不然我还写这篇东西干啥!?)。
在这里我们就要引进状态转移方程式了,我们暂时先不讨论它在01背包中是什么形态。
但大致意思就是:选择当前子问题中最好的决策。
在本问题中,分两种情况讨论:
- 不可以装下物品时, d p [ i ] [ j ] = d p [ i − 1 ] [ j ] dp[i][j]=dp[i-1][j] dp[i][j]=dp[i−1][j]。
因为装不下等于没有,所以就是没有这件物品时的最优解。 - 当可以装下物品( j ≥ w [ i ] j\ge w[i] j≥w[i])时,又有了两种选择:
选还是不选?
2.1 选:那么退回 d p [ i − 1 ] [ j − w [ i ] ] + c [ i ] dp[i-1][j-w[i]]+c[i] dp[i−1][j−w[i]]+c[i],也就是没装此物品前(注意这里背包的载重量要减去该物品的重量,因为得腾出来空间装这件物品嘛)然后加上该物品的价值(要不然你就亏了,而且代码在一般情况下不会通过)。这就是选的最大值。
2.2 不选:那么还是 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i−1][j]
最后轻描淡写地做个比较:谁大选谁喽!
这就是 d p [ i ] [ j ] dp[i][j] dp[i][j]的推法。
最后输出答案就行了,这一步可千万不要出错!
正确输出是 d p [ n ] [ m ] dp[n][m] dp[n][m]
代码
#include <iostream>
using namespace std;
int main()
{
int n, m;
cin >> m >> n;
int w[35], c[35], dp[35][205]; // 请根据需求而定
for (int i = 1; i <= n; ++i) cin >> w[i] >> c[i];
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++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]] + c[i]); // 装得下,做最优决策(这其实就是:
//状态转移·未完成版·方程式)
}
}
cout << dp[n][m];
}
优化
呵呵,差点就忘了优化o(* ̄︶ ̄*)o
具体怎么做呢?首先让我们再深入一层了解一下dp:
它还具有一个关键性质:后无效性原则。
说白了就是 前面的各种各样的一系列操作都不会影响这一步的最优决策。
这也是为什么无论你按照怎么样的顺序去输入这些物品它总能够给你最优答案的原因之一。
我们可以利用这个性质把 d p dp dp 数组压缩成一维。
既然前面的(不包括第 i − 1 i-1 i−1 行)数据对目前这个格子的数据没有影响,那么完全不需要去储存,储存前面无影响力的数据将会造成空间上的巨大浪费。
但是如果将它压缩,数据就会重叠,所以我们不能这么干?
其实解决这个问题可以使用 “逆向思维”,也就是逆推( j = m , m − 1 , ⋯ , 0 j=m,m-1,\cdots,0 j=m,m−1,⋯,0)。
而且这么做还给我们提供了别的方便:剪枝。
之前不是有个判断装不装得下的 i f if if 语句吗?现在我们可以抛弃它了,因为现在的情况与之前有所不同的是:现在那个格子已经自动 “继承” 了上一行,现在可以把 j j j改为 j = m , m − 1 , ⋯ , w [ i ] j=m,m-1,\cdots,w[i] j=m,m−1,⋯,w[i],并压缩状态转移方程式。
状态转移·完全版·方程式:
d p [ j ] = m a x ( d p [ j ] , d p [ j − w [ i ] + c [ i ] ) dp[j] = max(dp[j], dp[j-w[i]+c[i]) dp[j]=max(dp[j],dp[j−w[i]+c[i])
新代码(优化版)
#include <iostream>
using namespace std;
int main()
{
int n, m;
cin >> m >> n;
const int N = n + 1, M = m + 1;
int w[N], c[N], dp[M];
for (int i = 1; i < N; ++i) cin >> w[i] >> c[i];
for (int i = 0; i < M; ++i) dp[i] = 0;
for (int i = 0; i < N; ++i) {
for (int j = m; j >= w[i]; --j) {
dp[j] = max(dp[j], dp[j - w[i]] + c[i]);
}
}
cout << dp[m];
}
有问题或者建议欢迎在评论区分享!
(p≧w≦q)