背包问题详细解析

01背包问题

1.经典例题

N 件物品和一个容量是 V 的背包。每件物品只能使用一次。

i 件物品的体积是 vi,价值是 wi

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。

输出最大价值。

2.解题思路

先给出一种较为暴力的二维动态规划做法:

dp(i,j) 表示只看前 i 个物品,当前总体积为 j 时的最大总价值。

那么我们的答案就是选前 n 个物品,当前体积为 1v 中的最大值,即 res=max{dp(n,0v)}

然后我们考虑 dp(i,j) 的计算公式:对于第 i 个物品,我们有选与不选两种情况,那么分别写出公式:

  • 1.不选第 i 个物品,那么其价值和体积与上一个物品时一样,即:dp(i,j)=dp(i1,j)

  • 2.选第 i 个物品,那么其价值就是只看前 i1 个物品且当前体积为 jvi 时的价值加上当前物品的价值,即 dp(i,j)=dp(i1,jvi)+wi

那么我们的 dp(i,j) 就是以上两种情况的较大值。

但是我们想一想,如果只有这两个公式,那么 dp 最开始什么也没有,用这个数组中的元素加减乘除也全都得0,所以我们还要考虑给数组赋初始值的问题。。。

然后我们不难发现,当一个物品也不考虑,且体积为 0 时,最大价值为 0,即 dp(0,0)=0

代码如下

#include <iostream>
#include <algorithm>
#include <cstring>
//#define int long long

using namespace std;

#define N 1010
#define For(i,j,k) for(int i=j;i<=k;i++)
#define IOS ios::sync_with_stdio(0),cin.tie(),cout.tie()

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

int main () {
	IOS;
	cin >> n >> m;
	For (i, 1, n) cin >> v[i] >> w[i];

	dp[0][0] = 0;
	For (i, 1, n) {
		For (j, 0, m) {
			dp[i][j] = dp[i - 1][j];
			if (j >= v[i])
				dp[i][j] = max (dp[i][j], dp[i - 1][j - v[i]] + w[i]);
		}
	}

	int ans = 0;
	For (i, 0, m) {
		ans = max (ans, dp[n][i]);
	}
	cout << ans << endl;
	return 0;
}

然后我们再来考虑将二维改为一维去优化它:

我们发现在转移第 i 个物品时,其最大价值永远只与 i1 有关,所以我们其实不需要记录第二维,于是我们用 dpi 表示体积为 i 时能得到的最大价值,然后我们来观察两个状态转移方程:

dp[i][j] = dp[i - 1][j];
dp[i][j] = max (dp[i][j], dp[i - 1][j - v[i]] + w[i]);

因为我们是消掉了数组的第一维,所以第一个方程可以直接去掉,但是对于第二个方程,如果强行滚掉第一维,肯定是不可取的。因为我们在计算时会用到 dp(i1,jvi) 这个东西,但是如果强行消掉了,就会变成 dp(i,jvi),显然是不行的。

我们不难发现,在枚举 i 时,我们肯定是要保证 jvi 没有被用过,但是如果我们从小到大枚举,而 vi 有肯定是个正数,所以当枚举到 j 时, jvi 肯定被使用过了。

所以我们可以倒着枚举,从大枚举到小,那样就能保证 jvi 没有被使用过了。

所以我们就只需要一个状态转移方程:

dp[j] = max (dp[j], dp[j - v[i]] + w[i]);

而最后的枚举答案的循环也可以被省掉,答案就是 dpm,因为 dpi 指体积最大为 m 的最大价值,就将所有情况都包含在里面了。

代码如下

#include <iostream>
#include <algorithm>
#include <cstring>
//#define int long long

using namespace std;

#define N 1010
#define For(i,j,k) for(int i=j;i<=k;i++)
#define IOS ios::sync_with_stdio(0),cin.tie(),cout.tie()

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

int main () {
	IOS;
	cin >> n >> m;
	For (i, 1, n) cin >> v[i] >> w[i];

	For (i, 1, n) {
		for (int j = m; j >= 0; j --) {
			if (j >= v[i])
				dp[j] = max (dp[j], dp[j - v[i]] + w[i]);
		}
	}

	cout << dp[m] << endl;
	return 0;
}

2.完全背包问题

1.经典例题

N 种物品和一个容量是 V 的背包,每种物品都有无限件可用。

i 种物品的体积是 vi,价值是 wi

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。

输出最大价值。

2.解题思路

不难发现,完全背包问题其实就是在01背包问题的基础上将 最多选一次 变成了 可以选无限次

那么我们仍然可以用 f(i,j) 表示选前 i 个物品且总体积 j 的所有选法的价值的最大值。

然后对于每个 f(i,j),关于第 i 个物品我们可以不选,也可以选 1 个, 2 个 ... k 个。

然后我们来分情况讨论这两种选法:

  • 1.不选第 i 个,即 f(i1,j)

  • 2.选第 i 个且选择了 k 个,即 f(i1,jkvi)+kwi

最后答案就是两者的较大值。

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 1010;
int n, m, v[N], w[N], f[N][N];

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

但是很明显,代码最多有三层循环,时间复杂度 O(nm2),肯定会超时。。。

所以我们要想办法优化掉一层循环。

我们将状态转移方程展开,发现:

f[i][j]=max(f[i][j],f[i1][jkv[i]]+kw[i])

f[i][j]=max(f[i1][j],f[i1][jv]+w,f[i1][j2v]+2w...)

然后我们把 f[i1][jv] 展开,即:

f[i][jv]=max(f[i1][jv],f[i1][j2v]+w,f[i1][j3v]+2w...)

所以我们发现,一式后面所有项的最大值实际上就是二式的 f[i][jv] 再加上一个 w。。。

所以化简之后的方程式为:

f[i][j]=max(f[i1][j],f[i][jv]+w)

这样一来时间久被优化到两层 O(nm),可以过了。

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 1010;
int n, m, v[N], w[N], f[N][N];

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

然后和 01 背包一样,完全背包同样可以直接删掉一维空间,而且是不需要倒序模拟的。

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 1010;
int n, m, v[N], w[N], f[N];

int main () {
	cin >> n >> m;
	for (int i = 1; i <= n; i ++)
		cin >> v[i] >> w[i];
	
	for (int i = 1; i <= n; i ++)
		for (int j = v[i]; j <= m; j ++) 
		//注意这里实际上是 j 从 0~m 循环,然后判断 j - v[i] 是否大于等于0,简写成上面这样是等效的
			f[j] = max (f[j], f[j - v[i]] + w[i]);
	
	cout << f[m] << endl;
	return 0;
}

3.多重背包问题

1.经典例题

posted @   linbaicheng2022  阅读(47)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示