代码是前几周就写好的, 但是脑子抽了, 导致我再看时不知道是为什么, 于是乎在CDSN上整理一下..
我是爱C++和算法的喵线童鞋 //才没有给自己洗脑 ⁄(⁄ ⁄•⁄ω⁄•⁄ ⁄)⁄
=============================================
首先是最简单的二维版本
递归式: dp[i][w]=max{dp[i-1][w],dp[i-1][w-wi]+vi}
i表示第i件物品, w表示当前的重量.
这个递归式是怎么来的呢?
基于我们对动态规划的基本的了解, 我们知道: 动态规划问题一般具有最优子结构性质.
假设某个0/1背包问题给了n件物品, 总重量不超过Tw,
我们观察其中一个子问题: 有i件物品, 总重量不超过w (i介于第1件物品到第n件物品之间; w介于0-Tw)之间.
这里要注意,输入物品数组从0下标开始的话, i的循环也要对应从0到n-1 (这是一个基础而又细节的问题=.=)
这里多说几句.(毕竟dp是一类问题, 如果能更深入地理解, 有助于以后解决非典型问题. 当然如果已经对"递归"有了自己深入而清晰的理解,当然可以跳过啦~~)
思考一个问题: 为什么会想到"递归"?
我自己对递归的理解是: 抽象为有限个层次之间的关系(比如第i-1与第i层), 而不必关心整体的实现过程. 如果这个不好理解, 就想一想我们已经熟悉的数学归纳法, 其思想原理是非常类似的. 我们只关心n=k-1到n=k之间是怎么证明的, 而不会说把每一步都证明出来.
至于我为什么一直会这么纠结为什么递归, 是因为我曾经在青蛙跳台阶上的题目跪了 (手动微笑:::::) 暂时的不成功使人成长, that's quite right.
因此, 我们对于这个子问题dp[i][w], 只需考虑两种情况: 放第i个物品和不放第i个物品. (即假设前i-1个问题都已经求解, 只关心第i-1层与第i层之间的关系,是不是很像数学归纳法~) =.=其实数归本身也是一种递归恩
如果不放, 这把第i个物品扔掉, 考虑dp[i-1][w];
如果放, 就是dp[i-1][w-wi]+vi; 也就是,把wi的空间腾出来给第i个物品.
比较哪个大就好啦>.<
当然, 为了问题的完备性, 最后我们还要考虑几种特殊情况:
1)不放物品时, dp[i][w]=0;
2)如果对于子问题的上界w, wi已经超过了w, 那么就直接不需要考虑这个物品了, 因此此时 dp[i][w]=dp[i-1][w];
3)一般的情况, 就是上面讲的dp[i][w]=max{dp[i-1][w],dp[i-1][w-wi]+vi}
因此, 只要写个双重循环就可以了: for i=0 to n-1 {for j=0 to Tw)
代码如下:
#include
#include
using namespace std;
struct KNAP
{
int w; //weight;
int v; //value,or portfolio;
};
//二维的方式
void solveKPd(vector cknap, vector > &dp, int n, int Tw)
{
for (int i = 0; i < n; i++)
{
for (int j = 0; j <=Tw; j++)
{
if (j> Tw >> n;
vector cknap;
for (int i = 0; i < n; i++)
{
KNAP temp;
cin >> temp.w >> temp.v;
cknap.push_back(temp);
}
vector< vector > dp(n+1, vector(Tw+1,0));
solveKPd(cknap, dp, n, Tw);
system("pause");
return 0;
}
如果还不理解, 可以把整个dp表格输出来 (后面我写了一个输出程序, 之后会附完整的代码)
这是一个例子, n=5, Tw=10, 表头的w表示每件物品的重量, v表示价值.
================================================
然后是我纠结炸了的
空间优化版本
看上面的输出数据, 我们会发现其实二维表里有很多重复的. 这是因为, 从递归式的特点来看, 我们只是基于第i-1层对第i层做了更新, 而第i-1层该是什么样还是什么样.
换言之, 我们只需要知道最后一层的情况, 而不需要存储之前的结果.
看上面的表格, 其实我们最后输出的是最右下角的值.
我们这个时候可以得到一个递归式
f[w]=max{f[w], f[w-wi]+vi}
理解起来, 是和上面讲的一样的.
但是, 在具体的实现层面上, 有一个很反直觉的点:
不同于二维dp的双重循环, 空间优化版本的内层循环必须是逆序的.
如果这一点理解了, 整个程序的实现就非常容易了.
我们可以对比一下这两个式子:
dp[i][w]=max{dp[i-1][w],dp[i-1][w-wi]+vi}
f[w]=max{f[w], f[w-wi]+vi}
可以发现, 在一维递归式里, 要求f[w-wi]+vi 这部分 代替 dp[i-1][w-wi]+vi这部分
我们现在又只有一维数组. 这就要保证, 在第i次外循环时, 调用的f[w-wi]实际上是基于第i-1次循环得到的值.
而逆序保证了, 对于f[w], 它要调用的f[w-wi]一定是第i层循环还没有更新过的, 换言之, f[w-wi]只有可能是第i-1层存储的数据.
比如说, 我们上面的例子, 内层循环从Tw=10开始往下减,
第一个数就是求f[10]=max{f[10], f[10-wi]+vi}
这时我们要知道的f[10-wi]其实是二维里的dp[i-1][10-wi]
那么我这次循环才从10开始, 才第一次啊!!
根本不会去更新f[10-wi]这个数, 那么这里面存储的是什么玩意呢?
肯定是第i-1次外循环过一遍存储的结果对不~~
这就是我之前说的"代替"dp[i-1][w-wi]+vi.
假设你这个时候第一个数是求的0, 一直求到了第f[10], 那么你这个时候再去调用f[10-wi].
因为这一下就变成了最后一个数, 那么排在10前面的数肯定已经被第i层的循环动过了
要是还原成二维递归式, 就变成了dp[i][w]=max{dp[i][w],dp[i][w-wi]+vi}
这显然是有问题的.
要是看抽象的解释还是理解不能, 我们再输出一下
还是上面的例子 //其中, 0-6表示原来是0,更新为6
//表头是表示 第i个物品: 重量 价值 e.g 0:2 6
比如我们考虑要放2个物品时, 第一个数是f[10]=max{f[10],f[10-2]+3}
这时候f[10-2]肯定是从上一行来的, 也就是放1个物品时得到的.
而下面假设正序输出时, 我们会发现放第1个物品时就出了问题.
放到f[5]时, f[5]=max{f[5],f[3]+6}, 这时候第1个物品就被放了两次.
对应上面的抽象解释, f[3]已经被这一层循环更新过了, 调用的是本层循环的值dp[i][w-wi]
而不是上一层循环的值dp[i-1][w-wi]
=============================================
于是, 我们就可以写出一维部分的核心代码啦~
for (int i = 0; i < n; i++)
{
for (int j = Tw; j >= cknap[i].w; j--) //逆序
if (f[j - cknap[i].w] + cknap[i].v > f[j])
f[j] = f[j - cknap[i].w] + cknap[i].v;
}
cout << f[Tw] << endl;
==========================================
如果看到这里还不明白, 就用下面的cpp多输出几组数据试一试
再动笔手动求一求表格中某几个值, 相信你会懂哒
//毕竟我在智商躺平的状态下都搞懂了不是嘛=.=
下面这个程序, 有直接输出表格的过程, 就不用手动完成整个dp表格了.
但是, 背包问题较大时, 格式可能会很辣眼睛...如果实在有需要可以自己调整
如果只需要结果, 就把宏定义中的uds 值改为0
如果需要二维背包求解, 把xy改为1
附上我用的输入样例:
10 5
2 6
2 3
6 5
5 4
4 6
==========================================
//author: ECSoBaby, 2018/05/05, Basic Dynamic Programming
#include
#include
using namespace std;
#define uds 1 //如果需要输出动态规划的过程帮助理解, 改uds为1; 否则为0
#define xy 0 //xy=1, dp二维数组; xy=0, dp一位数组(空间优化)
struct KNAP
{
int w; //weight;
int v; //value,or portfolio;
};
//二维的方式
void solveKPd(vector cknap, vector > &dp, int n, int Tw)
{
for (int i =0; i cknap, vector &f, int n, int Tw)
{
#if uds
cout <<"\t ";
for (int j = Tw; j >= 0; j--)
cout << j << "\t ";
cout << endl << endl;
for (int i = 0; i < n; i++)
{
cout << i <<": "<= cknap[i].w; j--)
{
cout << f[j] ;
if (f[j - cknap[i].w] + cknap[i].v > f[j])
{
f[j] = f[j - cknap[i].w] + cknap[i].v;
cout << "-"<= 0; j--)
cout << f[j] << "\t ";
cout << endl << endl;
cout << "假设是正序输出:" << endl;
for (int i = 0; i <= Tw; i++) f[i] = 0;//初始化
cout << "\t ";
for (int j =0; j <=Tw; j++)
cout << j << "\t ";
cout << endl << endl;
for (int i = 0; i < n; i++)
{
cout << i << ": " << cknap[i].w << " " << cknap[i].v << "\t ";
for (int j = 0; j<=Tw; j++)
{
cout << f[j];
if (j > cknap[i].w)
{
if (f[j - cknap[i].w] + cknap[i].v > f[j])
{
f[j] = f[j - cknap[i].w] + cknap[i].v;
cout << "-" << f[j] << "\t"; //a-b表示从a变为b
}
else cout << "\t";
}
else cout << "\t";
}
cout << endl;
}
#else
for (int i = 0; i < n; i++)
{
for (int j = Tw; j >= cknap[i].w; j--) //逆序
if (f[j - cknap[i].w] + cknap[i].v > f[j])
f[j] = f[j - cknap[i].w] + cknap[i].v;
}
cout << f[Tw] << endl;
#endif
}
int main()
{
int Tw; int n;
cin >> Tw >> n;
vector cknap;
for (int i = 0; i > temp.w >> temp.v;
cknap.push_back(temp);
}
#if xy
vector< vector > dp(n + 1, vector(Tw + 1, 0));
solveKPd(cknap, dp, n, Tw);
#else
vector f(Tw + 1, 0);
solveKPd_re(cknap, f, n, Tw);
#endif
system("pause");
return 0;
}