DP 背包问题详解

前言

背包问题大致分为四种:01背包完全背包多重背包分组背包


01背包

n 个物品,每个物品的体积是 v_{i},价值是 w_{i},拿⾛体积之和不超过 V 的物品,最多能拿到多少价值?

做法:

定义 dp[i][j]:前 i 个物品,占⽤了不超过 j 个单位的体积。

转移⽅程: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - v[i]] + w[i])

复杂度 O(nV) 即可解决。可以使⽤滚动数组优化空间,注意内层循环要从⼤到⼩。

模版例题:P1048 [NOIP2005 普及组] 采药

参考代码

#include <bits/stdc++.h>
#define ll long long
using namespace std;

const int N = 1005;

int dp[N];

int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);
	
	int V, n;
	cin >> V >> n;
	
	for(int i = 1; i <= n; i++)
	{
		int v, w;
		cin >> v >> w;
		
		for (int j = V; j >= v; j--)
			dp[j] = max(dp[j], dp[j - v] + w);
	}
	
	cout << dp[V];
	
	return 0;
}

上面的代码是运用滚动数组来实现的背包问题,下面给大家介绍一下滚动数组的用法与定义。

滚动数组

在整个过程,可以发现每次内层循环运算时所需要的参数只有第 i 行,所以只需要将dp数组改为一维数组,是没有问题的,因为同样都有 i 和j,可以记录两个参数。也不许v数组和w数组,只需要改为变量即可。(要注意滚动数组只能优化空间,不能优化时间,如果要使用滚动数组的话,内层循环需改为从大到小,否则就会变成完全背包,即物品数量无限个)

拓展

其实,还有一种变形的01背包——依赖性01背包

题目链接:P1064 [NOIP2006 提高组] 金明的预算方案

参考代码

#include <bits/stdc++.h>
#define ll long long
using namespace std;

int dp[32005];
int v[65], w[65], c[65];
vector<int> g[65];

int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);
	
	int V, n;
	cin >> V >> n;
	
	for (int i = 1; i <= n; i++)
	{
		cin >> v[i] >> w[i] >> c[i];
		w[i] *= v[i];
		g[c[i]].emplace_back(i);
	}
	
	for(int i = 1; i <= n; i++)
	{
		if(!c[i])
		{
			for (int j = V; j >= v[i]; j--)
			{
				dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
				
				if (g[i].size() >= 1 && j >= v[i] + v[g[i][0]])
					dp[j] = max(dp[j], dp[j - v[i] - v[g[i][0]]] + w[i] + w[g[i][0]]);
				if (g[i].size() >= 2 && j >= v[i] + v[g[i][1]])
					dp[j] = max(dp[j], dp[j - v[i] - v[g[i][1]]] + w[i] + w[g[i][1]]);
				if (g[i].size() >= 2 && j >= v[i] + v[g[i][0]] + v[g[i][1]])
					dp[j] = max(dp[j], dp[j - v[i] - v[g[i][0]] - v[g[i][1]]] + w[i] + w[g[i][0]] + w[g[i][1]]);
			}
		}
	}
	
	cout << dp[V];
	return 0;
}

解析

emplace_back() 此函数的功能与 push_back() 的功能一样,但速度不同,前者快,后者慢。

(注意:emplace_back() 因为 vector 的工作原理在特殊情况下会报错,请慎用!报错了就换push_back() )

还记得01背包的决策是什么吗?

1.不选,然后去考虑下一个

2.选,背包容量减掉那个重量,总值加上那个价值。

这个题的决策是五个,分别是:

1.不选,然后去考虑下一个

2.选且只选这个主件

3.选这个主件,并且选附件1

4.选这个主件,并且选附件2

5.选这个主件,并且选附件1和附件2.

转移方程:

if (g[i].size() >= 1 && j >= v[i] + v[g[i][0]])
					dp[j] = max(dp[j], dp[j - v[i] - v[g[i][0]]] + w[i] + w[g[i][0]]);
				if (g[i].size() >= 2 && j >= v[i] + v[g[i][1]])
					dp[j] = max(dp[j], dp[j - v[i] - v[g[i][1]]] + w[i] + w[g[i][1]]);
				if (g[i].size() >= 2 && j >= v[i] + v[g[i][0]] + v[g[i][1]])
					dp[j] = max(dp[j], dp[j - v[i] - v[g[i][0]] - v[g[i][1]]] + w[i] + w[g[i][0]] + w[g[i][1]]);

完全背包

n 种物品,每种物品有⽆限个,第 i 种物品的每个物品体积是 v_{i},价值是 w_{i},拿⾛体积之和不超过 V 的物品,最多能拿到多少价值。

做法:

定义dp[i][j]:前 i 个物品,占⽤了不超过 j  个单位的体积

转移方程:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - v[i]] + w[i])

模版例题:P1616 疯狂的采药

参考代码

#include <bits/stdc++.h>
#define ll long long
using namespace std;

ll dp[10000005];

int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);
	
	int V, n;
	cin >> V >> n;
	
	for(int i = 1; i <= n; i++)
	{
		int v, w;
		cin >> v >> w;
		
		for (int j = v; j <= V; j++)
			dp[j] = max(dp[j], dp[j - v] + w);
	}
	
	cout << dp[V];
	
	return 0;
}

多重背包

n 种物品,每种物品有 c_{i} 个,第 i 种物品的每个物品体积是 v_{i},价值是 w_{i},拿⾛体积之和不
超过 V 的物品,最多能拿到多少价值。

做法1:

转化为01背包。将每种物品分成\left \lceil logc_{i} \right \rceil组,第 i 组的个数是2^{i - 1}这样任意数量的物品都可以组合出来。复杂度是O(nVlogc).

状态转移方程:

与01背包一致。

做法2:

使用单调队列优化(单调队列在此出不做介绍)

模版例题:宝物筛选

参考代码

#include <bits/stdc++.h>
#define ll long long 
using namespace std;

const int N = 40005;
int dp[N];

int main()
{	
	ios::sync_with_stdio(0);
	cin.tie(0);
	
	int n, V;
	cin >> n >> V;
	
	while(n--)
	{
		int w, v, c;
		cin >> w >> v >> c;
		
		for (int i = 1; c; i *= 2)
		{
			if(i <= c)
				c -= i;
			else
				i = c, c = 0;
			for (int j = V; j >= v * i; j--)
				dp[j] = max(dp[j], dp[j - v * i] + w * i);
		}
	}
	
	cout << dp[V];
	
	return 0;
}

分组背包

n 个物品,每个物品的体积是 v_{i},价值是 w_{i}n 个物品被分为若⼲组,每组最多只能拿⾛⼀ 个物品。拿⾛体积之和不超过 V 的物品,最多能拿到多少价值。

做法:

定义 dp[i][j] :前 i   组物品,占⽤了不超过 j  个单位的体积,接下来枚举组内物品进⾏转移即可。

模版例题:通天之分组背包

参考代码

#include <bits/stdc++.h>
#define ll long long
using namespace std;

const int N = 1005;

vector <int> v[N];
int dp[N], a[N], b[N];

int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);
	
	int V, n;
	cin >> V >> n;
	
	for(int i = 1; i <= n; i++)
	{
		int z;
		cin >> a[i] >> b[i] >> z;
		v[z].emplace_back(i);
	}
	
	for(int i = 1; i <= n; i++)
	{
		for(int j = V; j >= 0; j--)
		{
			for (vector<int>::iterator k = v[i].begin(); k != v[i].end(); k++)
			{
				if(j >= a[*k]) 
					dp[j] = max(dp[j], dp[j - a[*k]] + b[*k]);
			}
		}
	}
	
	cout << dp[V];
	return 0;
}

后续练习内容,已经放置于主页,并绑定于此文章。喜欢的点个关注+收藏+赞!

  • 26
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

学废c++

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值