01背包问题及一维数组优化

代码是前几周就写好的, 但是脑子抽了, 导致我再看时不知道是为什么, 于是乎在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<iostream>
#include<vector>
using namespace std;
struct KNAP
{
	int w; //weight;
	int v; //value,or portfolio;
};
//二维的方式
void solveKPd(vector<KNAP> cknap, vector<vector<int> > &dp, int n, int Tw)
{
	for (int i = 0; i < n; i++)
	{
		for (int j = 0; j <=Tw; j++)
		{
			if (j<cknap[i].w) dp[i+1][j] = dp[i][j]; 
//j是每一个子问题的w上界,这里与整体的Tw无关
			else if (dp[i][j] < dp[i][j - cknap[i].w] + cknap[i].v)
				dp[i+1][j] = dp[i][j - cknap[i].w] + cknap[i].v;
			else
				dp[i+1][j] = dp[i][j];
		}
	}
	cout << dp[n][Tw] << endl;
}
int main()
{
	int Tw; int n;
	cin >> Tw >> n;
	vector<KNAP> cknap;
	for (int i = 0; i < n; i++)
	{
		KNAP temp;
		cin >> temp.w >> temp.v;
		cknap.push_back(temp);
	}

	vector< vector<int> > dp(n+1, vector<int>(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<iostream>
#include<vector>
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<KNAP> cknap, vector<vector<int> > &dp, int n, int Tw)
{
	for (int i =0; i <n; i++)
	{
		for (int j=0; j <=Tw; j++)
		{
			if (j<cknap[i].w) dp[i+1][j] = dp[i][j]; //j是每一个子问题的w上界,这里与整体的Tw无关
			else if (dp[i][j] < dp[i][j - cknap[i].w] + cknap[i].v)
				dp[i+1][j] = dp[i][j - cknap[i].w] + cknap[i].v;
			else
				dp[i+1][j] = dp[i][j];
		}
	}
	//cout << dp[n][Tw] << endl;
#if uds
	cout << endl;
	cout << "n: w v\t\t";
	for (int j = 0; j <= Tw; j++)
		cout << j << "\t";
	cout << endl;
	cout << endl;
	for (int i = 0; i <=n; i++)
	{
		if(i==0)  cout << i << ": " <<0<<" "<<0<< "\t\t";
		else
		cout << i << ": "<<cknap[i-1].w<<" "<<cknap[i-1].v<<"\t\t";
		for (int j = 0; j <= Tw; j++)
			cout << dp[i][j] << "\t";
		cout << endl;
	}
	cout << endl;
#else
	cout << dp[n][Tw] << endl;
#endif
	
}

//一维的方式 solveKP refine
void solveKPd_re(vector<KNAP> cknap, vector<int> &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<<" "<<cknap[i].v<< "\t  ";
		for (int j = Tw; j >= 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 << "-"<<f[j]<<"\t"; //a-b表示从a变为b
			}
			else cout << "\t";
		}
		cout << endl;
	}
	cout << "\n最终的数组" << endl;
	cout << "\t  ";
	for (int j = Tw; j >= 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<KNAP> cknap;
	for (int i = 0; i <n; i++)
	{
		KNAP temp;
		cin >> temp.w >> temp.v;
		cknap.push_back(temp);
	}

#if xy
	vector< vector<int> > dp(n + 1, vector<int>(Tw + 1, 0));
	solveKPd(cknap, dp, n, Tw);
#else 
	vector<int> f(Tw + 1, 0);
	solveKPd_re(cknap, f, n, Tw);
#endif
	system("pause");
	return 0;
}

 

 

 

 

评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值