背包问题(01背包及其优化(滚动数组和逆序枚举))

        终于是完结了AC自动机,接下来开个新坑——背包问题,背包的种类还是很多的,之前有学过,但都是这里看一点,那里看一点,导致现在都搞混了,所以重新系统看看这方面的内容。

        先从简单的入手——01背包,那么为什么叫01背包呢?我的理解是01背包其实就是有没有的问题,所以01就代表了两种状态,取或不取。01背包适用于每种物品只有一件的情况,也是最简单的,但是会碰到各种奇怪的“WA法”,所以需要我们进行优化。

        先看例题:

https://www.luogu.com.cn/problem/P2871icon-default.png?t=N7T8https://www.luogu.com.cn/problem/P2871

题目描述

Bessie has gone to the mall's jewelry store and spies a charm bracelet. Of course, she'd like to fill it with the best charms possible from the N (1 ≤ N ≤ 3,402) available charms. Each charm i in the supplied list has a weight Wi (1 ≤ Wi ≤ 400), a 'desirability' factor Di (1 ≤ Di ≤ 100), and can be used at most once. Bessie can only support a charm bracelet whose weight is no more than M (1 ≤ M ≤ 12,880).

Given that weight limit as a constraint and a list of the charms with their weights and desirability rating, deduce the maximum possible sum of ratings.

输入格式

* Line 1: Two space-separated integers: N and M

* Lines 2..N+1: Line i+1 describes charm i with two space-separated integers: Wi and Di

输出格式

* Line 1: A single integer that is the greatest sum of charm desirabilities that can be achieved given the weight constraints

输入输出样例

输入 #1

4 6
1 4
2 6
3 12
2 7

输出 #1

23

        从题目我们就不难看出需要我们进行决策,要在有限的空间中装价值最大的情况,其实就是拿还是不拿的问题,我们需要同时维护容量和价值两个约束条件,我们不妨开个二维数组,是一个关于物品数量和最大容量的数组。

        在拿物品的时候,我们需要考虑两种情况:

        一:当我们的下一个物品的重量超过了我们背包的极限时,我们无法将其带上,所以面对此物品是我们的决策应该于上一个物品是一样的,即dp[i][j] = dp[i - 1][j]。

        二:当我们能带上某件物品时我们需要决策一下这件物品值不值得我们拿,因此,我们需要比较一下拿上它的价值和不拿的价值,也就是dp[i - 1][j]和dp[i - 1][j - w[i]] + v[i]哪个大我们就用其去更新数组。

        因此,代码如下:

 

#include <bits/stdc++.h>

using namespace std;

const int N = 13000;
const int M = 3410;

int w[N], v[N];
int dp[M][N];
int n ,m;

int main()
{
	cin >> n >> m;
	
	for(int i = 1; i <= n; i ++)
		cin >> w[i] >> v[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]] + v[i]);
		}	
		
	cout << dp[n][m] << endl;
}

        当我们高高兴兴去交一发的时候,MLE了!看一下空间复杂度O(nm),确实有可能会MLE,那么如何改进呢?

        相信有些同学看似会了,但是这个dp数组究竟是怎么操作的你真的明白吗?(其实一开始我也一直没弄懂,所以感觉总是学不进去)同时,想要优化空间,必须先弄懂dp数组的功能。

        我们从i = 1开始,i代表了什么呢?i代表了行,第i行意味着当我们的背包里有i个东西时我们的决策,j是一项约束,是从1到我们背包的最大容量,一开始i = 1;j = 1;就是代表了当背包里只能有一个物品,同时容量最大为1时,我们背包里所能有的最大价值,接着j渐渐变大,也就是在受到物品限制1个的情况下容量渐渐变大,我们所能获得的最大价值,由于我们先前初始化初态均为0,我们遵循二循环的操作,不断通过dp[0]行进行更新。不断得到新的决策,保证了我们容量不断变大时记录下真正的最大价值。

        而后,i ++,当背包物品约束变为两个时,容量依然从1开始慢慢变大,那么对于第二行来说,我们的基准态就是上一行记录下的dp[1]的价值,保证了我们的决策的正确性。

        以此类推,我们不难看出,每次我们的基准态都是上一次循环的内容,而更早的内容就成了占位置的僵尸数据,因此我们想,那我们用一个一维数组存储基准态,下一次我们直接将其更新,不就不会有僵尸数据了吗?

        理论可行,实践:

#include <bits/stdc++.h>

using namespace std;

const int N = 13000;
const int M = 3410;

int w[N], v[N];
int dp[N];
int n ,m;

int main()
{
	cin >> n >> m;
	
	for(int i = 1; i <= n; i ++)
		cin >> w[i] >> v[i];
	
	for(int i = 1; i <= n; i ++)
		for(int j = 1; j <= m; j ++)
		{
			if(j < w[i])
				dp[j] = dp[j];
			else
				dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
		}	
		
	cout << dp[n][m] << endl;
}

        但是红惨了,为什么呢?

        我们发现,在更新新基准态的时候,我们是从前向后更新的,从而导致了有可能dp[j - w[i]]会被更新,但是我们后面的数据需要原来的基准数据,导致错误,既然从前往后不行,那么从后往前不就避免了这个问题了吗,因为后面的数据与前面不会有挂钩。

        实践代码:

 

#include <bits/stdc++.h>

using namespace std;

const int N = 13000;
const int M = 3410;

int w[N], v[N];
int dp[N];
int n ,m;

int main()
{
	cin >> n >> m;
	
	for(int i = 1; i <= n; i ++)
		cin >> w[i] >> v[i];
	
	for(int i = 1; i <= n; i ++)
		for(int j = m; j >= 1; j --)
		{
			if(j < w[i])
				dp[j] = dp[j];
			else
				dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
		}	
		
	cout << dp[m] << endl;
}

        这样就可以了!这就是逆序枚举的魅力。

        关于滚动数组,其实这种一维数组也就是滚动数组,但从更直观的角度,我们可有开一个dp[2][m]的数组,这样就不怕新数据覆盖旧值的情况出现了。

        其实上面的AC代码还能继续简化,我们可以将j < w[i]条件放到for中,从elsed语句的角度出发,就是防止j - w[i]出现负数的情况。

        最简代码如下:

#include <bits/stdc++.h>

using namespace std;

const int N = 13000;
const int M = 3410;

int w[N], v[N];
int dp[N];
int n ,m;

int main()
{
	cin >> n >> m;
	
	for(int i = 1; i <= n; i ++)
		cin >> w[i] >> v[i];
	
	for(int i = 1; i <= n; i ++)
		for(int j = m; j >= w[i]; j --)
				dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
		
	cout << dp[m] << endl;
}

  • 28
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,针对您的问题,我来简单介绍一下逆序求解01背包问题动态规划的方法。 01背包问题是一个经典的动态规划问题,它的基本思路是:将问题分解为子问题,然后逐步求解。具体来说,我们可以定义一个二维数组dp[i][j],其中dp[i][j]表示在前i个物品中选择若干个物品放入容量为j的背包中所能获得的最大价值。对于第i个物品,我们有两种选择:放入背包或不放入背包。如果我们选择将第i个物品放入背包中,那么此时的最大价值就是dp[i-1][j-w[i]]+v[i],其中w[i]表示第i个物品的重量,v[i]表示第i个物品的价值;如果我们选择不放入第i个物品,那么此时的最大价值就是dp[i-1][j]。因此,我们可以得到状态转移方程: dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]]+v[i]), j>=w[i] dp[i][j] = dp[i-1][j], j<w[i] 其中,max表示取较大值。 接下来,我们来看看逆序求解01背包问题动态规划的方法。逆序求解就是从dp[n][m]开始,逐步向dp[1][1]逆推。具体来说,我们可以定义一个一维数组ans[m+1],其中ans[j]表示在前n个物品中选择若干个物品放入容量为j的背包中所能获得的最大价值。初始化时,ans[j]=dp[n][j]。然后,我们从n-1开始,逐个更新数组ans。对于第i个物品,如果它不放入背包中,则ans[j]不变;如果它放入背包中,则ans[j]的值就是dp[i][j-w[i]]+v[i]和ans[j]的较大值。最终,我们的答案就是ans[m]。 这就是逆序求解01背包问题动态规划的方法。希望能对您有所帮助!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值