一、潜水员
与之前的背包问题要求体积恰好是或不超过某一定值都不同,本题要求体积不小于某一定值,需要重新分析。
闫氏 DP 分析法:
- 状态表示:
f
[
i
,
j
,
k
]
f[i,j,k]
f[i,j,k]
(1) 集合:所有只从前 i i i 个物品中选,且氧气含量至少是 j j j,氮气含量至少是 k k k 的选法的集合
(2) 属性:Min - 状态计算:
(1) 所有不含物品 i i i 的选法: f [ i − 1 , j , k ] f[i-1,j,k] f[i−1,j,k]
(2) 所有包含物品 i i i 的选法: f [ i − 1 , j − v 1 , k − v 2 ] f[i-1,j-v_1,k-v_2] f[i−1,j−v1,k−v2]
因此有 f [ i , j , k ] = m i n { f [ i − 1 , j , k ] , f [ i − 1 , j − v 1 , k − v 2 ] + w } f[i,j,k]=min\{f[i-1,j,k],f[i-1,j-v_1,k-v_2]+w\} f[i,j,k]=min{f[i−1,j,k],f[i−1,j−v1,k−v2]+w}
注意到当 j < v 1 j<v_1 j<v1 或 k < v 2 k<v_2 k<v2 时,表示所有包含物品 i i i 的选法的状态中至少有一维体积是负数,无法在数组中表示。
但是,所有物品 (气缸) 中氧气和氮气含量一定是正整数,这使得当 j < v 1 j<v_1 j<v1 时, f [ i − 1 , j − v 1 , k − v 2 ] = f [ i − 1 , 0 , k − v 2 ] f[i-1,j-v_1,k-v_2]=f[i-1,0,k-v_2] f[i−1,j−v1,k−v2]=f[i−1,0,k−v2] (因为只从前 i i i 个物品中选,且氧气含量不少于 j − v 1 ( ≤ − 1 ) j-v_1(\le -1) j−v1(≤−1) 且小于 0 0 0,氮气含量不少于 k k k 的选法不存在);当 k < v 2 k<v_2 k<v2 时同理。
状态初始化: f [ 0 , 0 , 0 ] = 0 , f [ 0 , i , j ] = + ∞ ( i , j ≠ 0 ) f[0,0,0]=0,f[0,i,j]=+\infty\ (i,j\ne 0) f[0,0,0]=0,f[0,i,j]=+∞ (i,j=0)
代码实现:
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 22, M = 80;
int V1, V2, n;
int f[N][M];
int main(){
scanf("%d %d %d", &V1, &V2, &n);
memset(f, 0x3f, sizeof f);
f[0][0] = 0;
for (int i = 1; i <= n; i ++){
int a, b, c;
scanf("%d %d %d", &a, &b, &c);
for (int j = V1; j >= 0; j --)
for (int k = V2; k >= 0; k --)
f[j][k] = min(f[j][k], f[max(j - a, 0)][max(k - b, 0)] + c);
}
printf("%d", f[V1][V2]);
return 0;
}
至此,背包模型中关于体积的三种要求 (不超过、恰好是、不少于) 均已出现,在此总结一下三种要求的不同之处:
- 不超过:全部初始化为 0 0 0,枚举体积时要求 j − v ≥ 0 j-v\ge 0 j−v≥0;
- 恰好是: f [ 0 ] = 0 f[0]=0 f[0]=0,其余均初始化为 + / − ∞ +/-\infty +/−∞,枚举体积时要求 j − v ≥ 0 j-v\ge 0 j−v≥0;
- 不少于:
f
[
0
]
=
0
f[0]=0
f[0]=0,其余均初始化为
+
/
−
∞
+/-\infty
+/−∞,体积可以小于零。
其中正/负无穷视题目所求为最小值还是最大值而定。
二、背包问题求具体方案
以01背包问题求具体方案为例。
12.背包问题求具体方案 题目链接
求具体方案时不能将二维空间优化成一维。
在求解后逆序比较 f [ i , j ] f[i,j] f[i,j] 与 f [ i − 1 , j ] f[i-1,j] f[i−1,j] 和 f [ i − 1 , j − v [ i ] ] f[i-1,j-v[i]] f[i−1,j−v[i]],若等于前者,则不选第 i i i 个物品;若等于后者,则选第 i i i 个物品。 j j j 初始值为背包总体积,当等于后者时,下次比较时 j = j − v [ i ] j=j-v[i] j=j−v[i]。
此外,题目还要求输出字典序最小的答案,从前往后对于每一个物品,若可以选则一定选。可以从后往前进行求解,然后再从前往后判断是否选物品。
代码实现:
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N][N];
int main(){
scanf("%d %d", &n, &m);
for (int i = 1; i <= n; i ++)
scanf("%d %d", &v[i], &w[i]);
for (int i = n; i; i --) //状态递推时从后往前
for (int j = 0; j <= m; j ++){
f[i][j] = f[i + 1][j];
if (j >= v[i])
f[i][j] = max(f[i][j], f[i + 1][j - v[i]] + w[i]);
}
int k = m;
for (int i = 1; i <= n; i ++) //找具体方案时就可从前往后,保证字典序最小
if (k >= v[i] && f[i][k] == f[i + 1][k - v[i]] + w[i])
k -= v[i], printf("%d ", i); //如果能选编号小的物品,就一定选
return 0;
}
三、回顾分组背包问题
闫氏 DP 分析法:
- 状态表示:
f
[
i
,
j
]
f[i,j]
f[i,j]
(1) 集合:所有只从前 i i i 组物品中选,且总体积不超过 j j j 的选法的集合
(2) 属性:Max - 状态计算:
(1) 第 i i i 组物品中一个都不选: f [ i − 1 , j ] f[i-1,j] f[i−1,j]
(2) 第 i i i 组物品中选第 1 1 1 个: f [ i − 1 , j − v [ i , 1 ] ] + w [ i , 1 ] f[i-1,j-v[i,1]]+w[i,1] f[i−1,j−v[i,1]]+w[i,1]
(3) 第 i i i 组物品中选第 2 2 2 个: f [ i − 1 , j − v [ i , 2 ] ] + w [ i , 2 ] f[i-1,j-v[i,2]]+w[i,2] f[i−1,j−v[i,2]]+w[i,2]
…
(s[i]+1) 第 i i i 组物品中选第 s [ i ] s[i] s[i] 个: f [ i − 1 , j − v [ i , s [ i ] ] ] + w [ i , s [ i ] ] f[i-1,j-v[i,s[i]]]+w[i,s[i]] f[i−1,j−v[i,s[i]]]+w[i,s[i]]
上述取最大值即是 f [ i , j ] f[i,j] f[i,j]。
可以发现,相比于01背包、完全背包问题等两重循环,分组背包问题还需要额外一重循环来枚举组内的决策。
多重背包问题可以看作一种特殊的分组背包问题。
四、机器分配
可以把每个分公司看作一个物品组,每个物品组内均有 M M M 个物品,分别是 1 1 1 台机器、 2 2 2 台机器、…、 M M M台机器,且每一个物品组中至多选一个物品,背包体积是机器数量,价值即盈利,这样题目就转化为了分组背包问题。
同时本题还需输出任意一种方案,按照上面背包问题求具体方案,倒叙求每个物品组选第几个物品即可。
代码实现:
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 12, M = 17;
int n, m;
int f[N][M];
int w[N][M];
int path[N];
int main(){
scanf("%d %d", &n, &m);
for (int i = 1; i <= n; i ++)
for (int j = 1; j <= m; j ++)
scanf("%d", &w[i][j]);
for (int i = 1; i <= n; i ++)
for (int j = 0; j <= m; j ++)
for (int k = 0; k <= j; k ++)
f[i][j] = max(f[i][j], f[i - 1][j - k] + w[i][k]);
printf("%d\n", f[n][m]);
int k = m;
for (int i = n; i; i --)
for (int j = 0; j <= m; j ++)
if (k >= j && f[i][k] == f[i - 1][k - j] + w[i][j]){
path[i] = j;
k -= j;
break;
}
for (int i = 1; i <= n; i ++)
printf("%d %d\n", i, path[i]);
return 0;
}
五、金明的预算方案
可以将每个主件看作一个物品组,若第 i i i 个主件有 s [ i ] s[i] s[i] 个附件,则该物品组下有 2 s [ i ] 2^{s[i]} 2s[i] 种选择,用二进制数中的位代表相应附件的选择与否 (决策),这样就转化为分组背包问题。
代码实现:
#include <cstdio>
#include <algorithm>
#include <vector>
using namespace std;
#define v first
#define w second
const int N = 70, M = 32010;
typedef pair <int, int> PII;
int n, m;
PII a[N];
vector <PII> b[N];
int f[M];
int main(){
scanf("%d %d", &m, &n);
for (int i = 1; i <= n; i ++){
int v, p, q;
scanf("%d %d %d", &v, &p, &q);
if (!q) a[i] = {v, v * p};
else b[q].push_back({v, v * p});
}
for (int i = 1; i <= n; i ++)
if (a[i].v){ //数组a[i]有可能没有物品,要特判
for (int j = m; j >= 0; j --)
for (int k = 0; k < 1 << b[i].size(); k ++){ //二进制数枚举集合选择
int v = a[i].v, w = a[i].w;
for (int u = 0; u < b[i].size(); u ++) //判断二进制数中各位对应物品是否被选
if (k >> u & 1){
v += b[i][u].v;
w += b[i][u].w;
}
if (j >= v) f[j] = max(f[j], f[j - v] + w);
}
}
printf("%d", f[m]);
return 0;
}
六、开心的金明
金明的预算方案一题的弱化版本,只有主件,无附件,简单的01背包问题。
代码实现:
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 50010;
int n, m;
int f[N];
int main(){
scanf("%d %d", &m, &n);
for (int i = 1; i <= n; i ++){
int v, p;
scanf("%d %d", &v, &p);
for (int j = m; j >= v; j --)
f[j] = max(f[j], f[j - v] + v * p);
}
printf("%d", f[m]);
return 0;
}