背包问题
背包问题是一类经典的可以应用动态规划来解决的问题。背包问题的基本描述如下:给定一组物品,每种物品都有其重量和价格,在限定的总重量内如何选择才能使物品的总价格最高。由于问题是关于如何选择最合适的物品放置于给定的背包中,因此这类问题通常被称为背包问题。根据物品的特点,背包问题还可以进一步细分。
- 如果每种物品只有一个,可以选择将之放入或不放入背包,那么可以将这类问题称为0-1背包问题。0-1背包问题是最基本的背包问题,其他背包问题通常可以转化为0-1背包问题。
- 如果第i种物品最多有个,也就是每种物品的数量都是有限的,那么这类背包问题称为有界背包问题(也可以称为多重背包问题)。
- 如果每种物品的数量都是无限的,那么这类背包问题称为无界背包问题(也可以称为完全背包问题)
0-1背包问题
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次.第 i 件物品的体积是 vi,价值是 wi。求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。输出最大价值
// n为物品数量, m为背包体积
for (int i = 0; i < n; i++) {
for (int j = m; j >= 0 && j - v[i] >= 0; j--) {
dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
}
}
//最终结果为dp[m];
最原始的01背包问题,可以参见ACwing的01背包问题,代码实现
#include <iostream>
#include <vector>
using namespace std;
const int N = 1010;
int v[N], w[N];
int dp[N];
int main() {
// 获取输入
int n, m; // 分别表示物品数量和背包体积
cin >> n >> m;
for (int i = 0; i < n; i++) {
cin >> v[i] >> w[i];
}
// 动态规划主体
for (int i = 0; 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];
return 0;
}
练习题
在《剑指Offer专项突破版》也给出了两道相关的练习题。
剑指 Offer II 101. 分割等和子集,假设数组中所有元素的和为sum
,该问题可以转化为:如果sum
是奇数,肯定不存在想要的结果;如果sum是偶数,那么问题转化为是否可以在数组中查找到一些数的和为,问题转化成了0-1背包问题。
bool canPartition(vector<int>& nums) {
int sum = 0;
for (int i = 0; i < nums.size(); i++) sum += nums[i];
if (sum & 1) return false;
int target = sum >> 1;
vector<bool> dp(target+1);
dp[0] = true;
for (int i = 0; i < nums.size(); i++) {
for (int j = target; j >= nums[i]; j--) {
dp[j] = dp[j] | dp[j-nums[i]];
}
}
return dp[target];
}
因而上述问题转化为如果target > sum 或者sum-target为奇数
则不存在符合条件的数组,否则,在数组中寻找和为的组合数目,问题转化为0-1背包问题。
int findTargetSumWays(vector<int>& nums, int target) {
int sum = 0;
for (int i = 0; i < nums.size(); i++) sum += nums[i];
target = (sum - target);
if (target < 0 || target & 1) return 0;
target = target >> 1;
vector<int> dp(target+1);
dp[0] = 1;
for (int i = 0; i < nums.size(); i++) {
for (int j = target; j >= nums[i]; j--) {
dp[j] += dp[j-nums[i]];
}
}
return dp[target];
}
完全背包问题
有 N 种物品和一个容量是 V 的背包,每种物品都有无限件可用。第 i 种物品的体积是 vi,价值是 wi。求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。输出最大价值。
// n为物品数量, m为背包体积
for (int i = 0; i < n; i++) {
for (int j = v[i]; j <= m; j++) {
dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
}
}
// 最终结果为dp[m];
最原始的完全背包问题,参见完全背包,代码实现:
#include <iostream>
#include <vector>
using namespace std;
const int N = 1010;
int v[N], w[N];
int dp[N];
int main() {
int n, m;
cin >> n >> m;
for (int i = 0; i < n; i++) {
cin >> v[i] >> w[i];
}
// 动态规划实现主体
for (int i = 0; i < n; i++) {
for (int j = v[i]; j <= m; j++) {
dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
}
}
cout << dp[m];
return 0;
}
练习题
在《剑指Offer专项突破版》也给出了一道相关的练习题
int coinChange(vector<int>& coins, int amount) {
if (amount < 0) return -1;
vector<int> dp(amount+1, INT_MAX);
dp[0] = 0;
for (int i = 0; i < coins.size(); i++) {
for (int j = coins[i]; j <= amount; j++) {
if (dp[j-coins[i]] != INT_MAX)
dp[j] = min(dp[j], dp[j-coins[i]]+1);
}
}
return (dp[amount] == INT_MAX) ? -1 : dp[amount];
}
多重背包问题
有 N 种物品和一个容量是 V 的背包。第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
#include <iostream>
#include <vector>
using namespace std;
const int N = 110;
int v[N], w[N], s[N];
int dp[N];
int main() {
int n, m;
cin >> n >> m;
for (int i = 0; i < n; i++) {
cin >> v[i] >> w[i] >> s[i];
}
// 动态规划实现主体
for (int i = 0; i < n; i++) {
for (int j = m; j >= 0; j--) {
for (int k = 1; k <= s[i] && j - k * v[i] >= 0; k++) {
dp[j] = max(dp[j], dp[j - k * v[i]] + k * w[i]);
}
}
}
cout << dp[m];
return 0;
}
二进制优化
将这件物品拆成系数构成一组价值和体积和系数乘积的物品,便可以将高问题转化为01背包问题。这些系数分别为,其中,k是满足的最大正整数。
例如,13可以拆解为。
#include <iostream>
#include <vector>
using namespace std;
const int N = 12010, M= 2010;
int v[N], w[N];
int dp[M];
int main() {
int n, m;
cin >> n >> m;
int cnt = 0;
for (int i = 0; i < n; i++) {
int vi, wi, si;
cin >> vi >> wi >> si;
// 系数拆解
for (int k = 1; k <= si; k *= 2) {
v[++cnt] = vi * k;
w[cnt] = wi * k;
si -= k;
}
if (si > 0) {
v[++cnt] = vi * si;
w[cnt] = wi * si;
}
}
// 转化为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];
return 0;
}
单调队列优化
#include <iostream>
#include<cstring>
using namespace std;
const int N = 20010;
int n, m;
int f[N], g[N], q[N];
int main() {
cin >> n >> m;
for (int i = 0; i < n; i++) {
int v, w, s;
cin >> v >> w >> s;
memcpy(g, f, sizeof(f));
for (int j = 0; j < v; j++) {
int hh = 0, tt = -1;
for (int k = j; k <= m; k += v) { // k表示m%v的第几个数
f[k] = g[k];
if (hh <= tt && k-s*v > q[hh]) hh++;
if (hh <= tt) f[k] = max(f[k], g[q[hh]]+(k-q[hh])/v*w);
while(hh <= tt && g[q[tt]]-(q[tt]-j)/v*w <= g[k]-(k-j)/v*w) tt--;
q[++tt] = k;
}
}
}
cout << f[m] << endl;
return 0;
}
练习题
参考资料
- 《剑指Offer专项突破版》