多重背包问题2

1. 问题描述

有N种物品和一个容量为V的背包。第i种物品最多有si件,每件体积是vi,价值是wi。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。

1.1 输入格式

1
2
3
4
5
6
7
// 第一行两个整数 N,V,用空格隔开,分别表示物品种数和背包容积
4 5
// 接下来有 N 行,每行三个整数 vi,wi,si,用空格隔开,分别表示第i种物品的体积、价值和数量
1 2 3
2 4 1
3 4 3
4 5 2

1.2 数据范围

  • 0 < N ≤ 1000
  • 0 < V ≤ 2000
  • 0 < vi, wi, si ≤ 2000

2. 基本实现

2.1 朴素解法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
using namespace std;

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

int main() {
cin >> n >> m;

for(int i = 1; i <= n; i++) {
cin >> v[i] >> w[i] >> s[i];
}

for(int i = 1; i <= n; i++) {
for(int j = 0; j <= m; j++) {
for(int k = 0; k <= s[i] && k * v[i] <= j; k++) {
dp[i][j] = max(dp[i][j], dp[i-1][j - k * v[i]] + k * w[i]);
}
}
}

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

2.2 一维数组优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
using namespace std;

const int N = 2010;
int n, m;
int v[N], w[N], s[N];
int dp[N];

int main() {
cin >> n >> m;

for(int i = 1; i <= n; i++) {
cin >> v[i] >> w[i] >> s[i];
}

for(int i = 1; i <= n; i++) {
for(int j = m; j >= 0; j--) {
for(int k = 1; k <= s[i] && k * v[i] <= j; k++) {
dp[j] = max(dp[j], dp[j - k * v[i]] + k * w[i]);
}
}
}

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

3. 二进制优化

3.1 原理

将每种物品的数量si进行二进制拆分,转化为01背包问题。

3.2 实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <iostream>
using namespace std;

const int N = 12010, M = 2010;
int n, m;
int v[N], w[N];
int dp[M];
int cnt;

int main() {
cin >> n >> m;

// 二进制拆分
for(int i = 1; i <= n; i++) {
int a, b, s;
cin >> a >> b >> s;

int k = 1;
while(k <= s) {
cnt++;
v[cnt] = a * k;
w[cnt] = b * k;
s -= k;
k *= 2;
}
if(s > 0) {
cnt++;
v[cnt] = a * s;
w[cnt] = b * s;
}
}

// 01背包
n = cnt;
for(int i = 1; i <= n; i++) {
for(int j = m; j >= v[i]; j--) {
dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
}
}

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

4. 单调队列优化

4.1 原理

利用单调队列优化转移方程,将时间复杂度降低。

4.2 实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>
using namespace std;

const int N = 20010;
int n, m;
int v[N], w[N], s[N];
int dp[N], g[N], q[N];

int main() {
cin >> n >> m;

for(int i = 1; i <= n; i++) {
cin >> v[i] >> w[i] >> s[i];
}

for(int i = 1; i <= n; i++) {
for(int j = 0; j < v[i]; j++) {
int head = 0, tail = -1;
for(int k = j; k <= m; k += v[i]) {
if(head <= tail && k - q[head] > s[i] * v[i]) head++;
while(head <= tail && g[q[tail]] <= g[k]) tail--;
q[++tail] = k;
g[k] = dp[k];
dp[k] = g[q[head]] + (k - q[head]) / v[i] * w[i];
}
}
}

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

5. 性能分析

5.1 时间复杂度

  • 朴素解法: O(N * V * S)
  • 二进制优化: O(N * V * logS)
  • 单调队列优化: O(N * V)

5.2 空间复杂度

  • 二维数组: O(N * V)
  • 一维优化: O(V)

6. 实践建议

  1. 对于小规模数据,使用朴素解法即可
  2. 当物品数量较大时,使用二进制优化
  3. 当要求更高效率时,考虑单调队列优化
  4. 注意数组大小的定义,防止越界

7. 常见错误

  1. 数组开小导致越界
  2. 循环边界条件错误
  3. 二进制拆分时的处理错误
  4. 单调队列维护不当

8. 测试样例

1
2
3
4
5
6
7
8
9
// 输入样例
4 5
1 2 3
2 4 1
3 4 3
4 5 2

// 输出样例
10