背包问题详细解析

01背包问题

1.经典例题

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

\(i\) 件物品的体积是 \(v_i\),价值是 \(w_i\)

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

输出最大价值。

2.解题思路

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

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

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

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

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

  • 2.选第 \(i\) 个物品,那么其价值就是只看前 \(i-1\) 个物品且当前体积为 \(j-v_i\) 时的价值加上当前物品的价值,即 \(dp_{(i,j)} = dp_{(i-1,j-v_i)} + w_i\)

那么我们的 \(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\) 个物品时,其最大价值永远只与 \(i - 1\) 有关,所以我们其实不需要记录第二维,于是我们用 \(dp_i\) 表示体积为 \(i\) 时能得到的最大价值,然后我们来观察两个状态转移方程:

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

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

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

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

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

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

而最后的枚举答案的循环也可以被省掉,答案就是 \(dp_m\),因为 \(dp_i\) 指体积最大为 \(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\) 种物品的体积是 \(v_i\),价值是 \(w_i\)

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

输出最大价值。

2.解题思路

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

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

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

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

  • 1.不选第 \(i\) 个,即 \(f_{(i - 1, j)}\)

  • 2.选第 \(i\) 个且选择了 \(k\) 个,即 \(f_{(i - 1, j - k * v_i)} + k * w_i\)

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

#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 (n * m ^ 2)\),肯定会超时。。。

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

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

\[f[i][j] = max (f[i][j], f[i - 1][j - k * v[i]] + k * w[i]) \]

\[一式:f[i][j] = max (f[i - 1][j], f[i - 1][j - v] + w, f[i - 1][j - 2v] + 2w ...) \]

然后我们把 \(f[i - 1][j - v]\) 展开,即:

\[二式:f[i][j - v] = max (f[i - 1][j - v], f[i - 1][j - 2v] + w, f[i - 1][j - 3v] + 2w ...) \]

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

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

\[f[i][j] = max (f[i - 1][j], f[i][j - v] + 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 @ 2023-08-06 13:21  linbaicheng2022  阅读(77)  评论(0)    收藏  举报