一、货币系统I
简单的完全背包求方案数问题。
f [ i , j ] = f [ i − 1 , j ] + f [ i , j − v ] f[i,j]=f[i-1,j]+f[i,j-v] f[i,j]=f[i−1,j]+f[i,j−v]
代码实现:
#include <cstdio>
using namespace std;
const int N = 3010;
typedef long long ll;
int n, m;
ll f[N];
int main(){
scanf("%d %d", &n, &m);
f[0] = 1;
for (int i = 1; i <= n; i ++){
int v;
scanf("%d", &v);
for (int j = v; j <= m; j ++)
f[j] += f[j - v];
}
printf("%lld", f[m]);
return 0;
}
二、货币系统II
NOIP提高组原题,一般NOIP提高组的题目中都会隐藏一些性质,发现这些性质后题目便迎刃而解。
最优解 ( m , b ) (m,b) (m,b) 有这样三个性质:
- ( n , a ) (n,a) (n,a) 中所有货币的面额均可用 ( m , b ) (m,b) (m,b) 中货币的面额表示
-
(
m
,
b
)
(m,b)
(m,b) 中任意一种货币的面额不能用
(
m
,
b
)
(m,b)
(m,b) 中其他货币的面额表示
反证法,若能,则去掉这种货币, ( n , a ) (n,a) (n,a) 与 ( m − 1 , b ) (m-1,b) (m−1,b) 仍等价, m m m 一定不是最小值,矛盾。 -
(
m
,
b
)
(m,b)
(m,b) 中任意一种货币的面额都在
(
n
,
a
)
(n,a)
(n,a) 中出现过
反证法,若 b [ x ] b[x] b[x] 未在 ( n , a ) (n,a) (n,a) 中出现过,有以下两种情况:
(1) b [ x ] b[x] b[x] 不能用 ( n , a ) (n,a) (n,a) 表示,从而 ( n , a ) (n,a) (n,a) 与 ( m , b ) (m,b) (m,b) 不等价,矛盾;
(2) b [ x ] b[x] b[x] 可以用 ( n , a ) (n,a) (n,a) 表示,由1, b [ x ] b[x] b[x] 可以用 ( m , b ) (m,b) (m,b) 中其他货币的面额表示,与2矛盾。
有了以上三个性质,题目即求出 ( n , a ) (n,a) (n,a) 中不能用其他货币的面额表示出的面额数。
将 ( n , a ) (n,a) (n,a) 从小到大排序,对于每种面额 a [ i ] a[i] a[i],先判断它是否能由比它小的其他面额来表示,如果不能,再用类似完全背包的方法,更新所有 (不超过 ( n , a ) (n,a) (n,a) 种最大面额) 能用前 i i i 种面额表示出的面额。
代码实现:
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 25010;
int n;
int a[N];
int f[N];
int main(){
int T;
scanf("%d", &T);
while (T --){
scanf("%d", &n);
int maxa = 0;
for (int i = 0; i < n; i ++)
scanf("%d", &a[i]), maxa = max(maxa, a[i]);
sort(a, a + n);
memset(f, 0, sizeof f);
f[0] = 1;
int res = 0;
for (int i = 0; i < n; i ++)
if (!f[a[i]]){
res ++;
for (int j = a[i]; j <= maxa; j ++) //类似完全背包,更新所有能用前i中面额表示出的且小于maxa的面额
f[j] |= f[j - a[i]];
}
printf("%d\n", res);
}
return 0;
}
三、混合背包问题
01背包:
f
[
i
,
j
]
=
m
a
x
(
f
[
i
−
1
,
j
]
,
f
[
i
−
1
,
j
−
v
]
+
w
)
f[i,j]=max(f[i-1,j],f[i-1,j-v]+w)
f[i,j]=max(f[i−1,j],f[i−1,j−v]+w)
完全背包:
f
[
i
,
j
]
=
m
a
x
(
f
[
i
−
1
,
j
]
,
f
[
i
,
j
−
v
]
+
w
)
f[i,j]=max(f[i-1,j],f[i,j-v]+w)
f[i,j]=max(f[i−1,j],f[i,j−v]+w)
多重背包:
f
[
i
,
j
]
=
m
a
x
(
f
[
i
−
1
,
j
]
,
f
[
i
−
1
,
j
−
v
]
+
w
,
f
[
i
−
1
,
j
−
2
v
]
+
2
w
,
.
.
.
,
f
[
i
−
1
,
j
−
s
v
]
+
s
w
)
f[i,j]=max(f[i-1,j],f[i-1,j-v]+w,f[i-1,j-2v]+2w,...,f[i-1,j-sv]+sw)
f[i,j]=max(f[i−1,j],f[i−1,j−v]+w,f[i−1,j−2v]+2w,...,f[i−1,j−sv]+sw)
三种背包问题状态表示相同,仅在单个物品的状态计算时不同,且不同物品之间互不影响,所以每一个物品分开来做,只能选一个就按01背包,能选无限个就按完全背包,能选有限个就按多重背包。
代码实现:
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, m;
int f[N];
int main(){
scanf("%d %d", &n, &m);
for (int i = 1; i <= n; i ++){
int v, w, s;
scanf("%d %d %d", &v, &w, &s);
if (!s) //完全背包
for (int j = v; j <= m; j ++)
f[j] = max(f[j], f[j - v] + w);
else{ //多重背包二进制优化
if (s < 0) s = 1; //01背包(看成s=1的多重背包)
for (int k = 1; k <= s; k <<= 1){
for (int j = m; j >= k * v; j --)
f[j] = max(f[j], f[j - k * v] + k * w);
s -= k;
}
if (s)
for (int j = m; j >= s * v; j --)
f[j] = max(f[j], f[j - s * v] + s * w);
}
}
printf("%d", f[m]);
return 0;
}
四、有依赖的背包问题
有依赖的背包问题由金明的预算方案一题拓展而来。金明的预算方案一题中每个主件最多有 2 2 2 个附件,因此对于每个主件,最多有 2 2 2^2 22 种选择情况。然而,在更一般的有依赖的背包问题中,依赖关系组成一棵树 (即附件可能还有属于它的附件,且附件个数不限),这样我们就不能用分组背包思想来做,引入树形 DP:在 DFS 同时更新状态,状态转移时仅考虑上下两层 (父节点与子节点) 之间的状态关系。
闫氏 DP 分析法:
- 状态表示:
f
[
u
,
j
]
f[u,j]
f[u,j]
(1) 集合:所有从以 u u u 为根的子树中选 ( u u u 一定要选),且总体积不超过 j j j 的选法的集合
(2) 属性:Max - 状态计算:金明的预算方案中,按照选择方案,“最后一步”至多有
4
4
4 种情况,但本题中,按照选择方案,“最后一步”情况太多,因此不能用最后一步的选择方案来划分集合。
这里,我们先按照子节点 (子树的根) 划分集合,将每个子树看作一个物品组 (类似分组背包),物品组中按体积划分为 k k k 类物品,一个物品组中至多选一种物品:
(1) 体积为 1 1 1, f [ u , j − 1 ] + f [ s o n , 1 ] f[u,j-1]+f[son,1] f[u,j−1]+f[son,1]
(2) 体积为 2 2 2, f [ u , j − 2 ] + f [ s o n , 2 ] f[u,j-2]+f[son,2] f[u,j−2]+f[son,2]
…
(k) 体积为 k k k, f [ k , j − k ] + f [ s o n , k ] f[k,j-k]+f[son,k] f[k,j−k]+f[son,k]
f [ u , j ] f[u,j] f[u,j] 为所有子树中上述的最大值。
代码实现:
#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;
const int N = 110;
int n, m, root;
int f[N][N];
int v[N], w[N];
vector <int> e[N];
void dfs(int u){
for (int i = 0; i < e[u].size(); i ++){ //循环物品组(子树)
int son = e[u][i];
dfs(son); //树形DP:先递归求解子树
//分组背包:
for (int j = m - v[u]; j >= 0; j --) //先循环体积
for (int k = 0; k <= j; k ++) //再循环决策
f[u][j] = max(f[u][j], f[u][j - k] + f[son][k]);
}
//将物品u加进去
for (int i = m; i >= v[u]; i --) f[u][i] = f[u][i - v[u]] + w[u];
for (int i = 0; i < v[u]; i ++) f[u][i] = 0;
}
int main(){
scanf("%d %d", &n, &m);
for (int i = 1; i <= n; i ++){
int p;
scanf("%d %d %d", &v[i], &w[i], &p);
if (p < 0) root = i;
else e[p].push_back(i);
}
dfs(root);
printf("%d", f[root][m]);
return 0;
}
五、背包问题求(最优解)方案数
以01背包求最优解方案数为例。
闫氏 DP 分析法:
- 状态表示:
f
[
i
,
j
]
,
g
[
i
,
j
]
f[i,j],g[i,j]
f[i,j],g[i,j]
(1) 集合:所有只从前 i i i 个物品中选,且总体积恰好是 j j j 的选法的集合
(2) 属性: f f f–Max, g g g–Count (最优解) - 状态计算:
(1) f [ i , j ] = m a x ( f [ i − 1 , j ] , f [ i − 1 , j − v ] + w ) f[i,j]=max(f[i-1,j],f[i-1,j-v]+w) f[i,j]=max(f[i−1,j],f[i−1,j−v]+w)
(2) 上式中,如果取最大值的是前者, g [ i , j ] = g [ i − 1 , j ] g[i,j]=g[i-1,j] g[i,j]=g[i−1,j];如果是后者, g [ i , j ] = g [ i − 1 , j − v ] g[i,j]=g[i-1,j-v] g[i,j]=g[i−1,j−v];如果两者相等, g [ i , j ] = g [ i − 1 , j ] + g [ i − 1 , j − v ] g[i,j]=g[i-1,j]+g[i-1,j-v] g[i,j]=g[i−1,j]+g[i−1,j−v]
状态初始化:
f
[
0
,
0
]
=
0
,
f
[
0
,
i
]
=
−
∞
(
i
≠
0
)
,
g
[
0
,
0
]
=
1
f[0,0]=0,f[0,i]=-\infty (i\ne 0),g[0,0]=1
f[0,0]=0,f[0,i]=−∞(i=0),g[0,0]=1
最终答案:令
m
a
x
{
f
[
n
,
i
]
∣
0
≤
i
≤
m
}
=
r
e
s
max\{f[n,i]|0\le i\le m\}=res
max{f[n,i]∣0≤i≤m}=res,最终答案为
c
n
t
cnt
cnt,则
c
n
t
=
∑
1
≤
i
≤
m
,
f
[
n
,
i
]
=
r
e
s
g
[
n
,
i
]
cnt=\sum\limits_{1\le i\le m,f[n,i]=res}g[n,i]
cnt=1≤i≤m,f[n,i]=res∑g[n,i]
代码实现:
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 1010, MOD = 1e9 + 7;
int n, m;
int f[N], g[N];
int main(){
scanf("%d %d", &n, &m);
memset(f, -0x3f, sizeof f);
f[0] = 0, g[0] = 1;
for (int i = 1; i <= n; i ++){
int v, w;
scanf("%d %d", &v, &w);
for (int j = m; j >= v; j --){
int t = max(f[j], f[j - v] + w), c = 0;
if (t == f[j]) c = g[j];
if (t == f[j - v] + w) c = (c + g[j - v]) % MOD;
f[j] = t, g[j] = c;
}
}
int res = 0, cnt = 0;
for (int i = 0; i <= m; i ++) res = max(res, f[i]);
for (int i = 0; i <= m; i ++)
if (f[i] == res) cnt = (cnt + g[i]) % MOD;
printf("%d", cnt);
return 0;
}
当然,也可以不按照y总的“体积恰好是”状态表示,直接照搬01背包问题的状态表示,将
f
[
i
,
j
]
,
g
[
i
,
j
]
f[i,j],g[i,j]
f[i,j],g[i,j] 的集合定义为所有只从前
i
i
i 个物品中选,且总体积不超过
j
j
j 的选法的集合,这样的状态的递推式与上述完全相同,仅在状态初始化和最终答案上有少许不同:
状态初始化:
g
[
0
,
i
]
=
1
g[0,i]=1
g[0,i]=1
最终答案:
g
[
n
,
m
]
g[n,m]
g[n,m]
代码实现:
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 1010, MOD = 1e9 + 7;
int n, m;
int f[N], g[N];
int main(){
scanf("%d %d", &n, &m);
for (int i = 0; i <= m; i ++) g[i] = 1;
for (int i = 1; i <= n; i ++){
int v, w;
scanf("%d %d", &v, &w);
for (int j = m; j >= v; j --){
int t = f[j - v] + w;
if (f[j] < t)
f[j] = t, g[j] = g[j - v];
else if (f[j] == t)
g[j] = (g[j] + g[j - v]) % MOD;
}
}
printf("%d", g[m]);
return 0;
}
六、能量石
贪心+01背包
为什么不能直接01背包求解?因为对于同一堆石头,若吃的顺序不同,则石头损失的能量不同,最后吃到的总能量也不同。相较于传统的01背包问题,本题需要考虑两个问题:按照什么样的顺序吃,吃哪些石头。
- 选择吃哪样的能量石:肯定不会吃能量已经降为 0 0 0 的能量石
- 按照什么样的顺序吃:
对任意两个相邻的能量石 i , i + 1 i,i+1 i,i+1,先假定吃这两块能量石时它们的能量未降为 0 0 0,那么先吃 i i i 再吃 i + 1 i+1 i+1 所获得能量为 E i , + E i + 1 , − S i ∗ L i + 1 E_i^,+E_{i+1}^,-S_i*L_{i+1} Ei,+Ei+1,−Si∗Li+1,先吃 i + 1 i+1 i+1 再吃 i i i 所获得能量为 E i + 1 , + E i , − S i + 1 ∗ L i E_{i+1}^,+E_i^,-S_{i+1}*L_i Ei+1,+Ei,−Si+1∗Li ( E i , , E i + 1 , E_i^,,E_{i+1}^, Ei,,Ei+1, 代表刚开始吃这两块能量石时各自剩下的能量)。因此,先吃 i i i 再吃 i + 1 i+1 i+1 所获得能量更多当且仅当 S i ∗ L i + 1 < S i + 1 ∗ L i S_i*L_{i+1}<S_{i+1}*L_i Si∗Li+1<Si+1∗Li,即 S i L i < S i + 1 L i + 1 ( L ≠ 0 ) \frac{S_i}{L_i}<\frac{S_{i+1}}{L_{i+1}}\ (L\ne 0) LiSi<Li+1Si+1 (L=0)。
综合1、2可得,存在一组最优解,选择了一部分石头,按照 S i L i \frac{S_i}{L_i} LiSi 从小到大的顺序吃,且选择的所有石头能量均不会降为 0 0 0。接下来要求的就是最优解选择了哪些石头,用01背包问题求解。
- 状态表示:
f
[
i
,
j
]
f[i,j]
f[i,j]
(1) 集合:所有只从前 i i i 块石头中选,且总体积 (时间) 恰好是 j j j 的选法的集合
(2) 属性:Max - 状态计算: f [ i , j ] = m a x ( f [ i − 1 , j ] , f [ i − 1 , j − S ] + E − ( j − S ) ∗ L ) f[i,j]=max(f[i-1,j],f[i-1,j-S]+E-(j-S)*L) f[i,j]=max(f[i−1,j],f[i−1,j−S]+E−(j−S)∗L)
- 状态初始化: f [ 0 , 0 ] = 0 , f [ 0 , i ] = − ∞ ( i ≠ 0 ) f[0,0]=0,f[0,i]=-\infty\ (i\ne 0) f[0,0]=0,f[0,i]=−∞ (i=0)
代码实现:
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 110, M = N * N;
int n;
int f[M];
struct Stone{
int s, e, l;
bool operator < (const Stone &W) const{ //结构体内部重载小于号
return s * W.l < W.s * l;
}
}stone[N];
/*
bool cmp(Stone a, Stone b){
return a.s * b.l < a.l * b.s; //自定义比较函数,返回值是1等价于a排在b前面
}
*/
int main(){
int T;
scanf("%d", &T);
for (int C = 1; C <= T; C ++){
int m = 0;
scanf("%d", &n);
for (int i = 0; i < n; i ++){
int s, e, l;
scanf("%d %d %d", &s, &e, &l);
stone[i] = {s, e, l};
m += s;
}
sort(stone, stone + n);
//若是定义比较函数应为 sort(stone, stone + n, cmp);
memset(f, -0x3f, sizeof f);
f[0] = 0;
for (int i = 0; i < n; i ++){
int s = stone[i].s, e = stone[i].e, l = stone[i].l;
for (int j = m; j >= s; j --)
f[j] = max(f[j], f[j - s] + e - (j - s) * l);
}
int res = 0;
for (int i = 0; i <= m; i ++)
res = max(res, f[i]);
printf("Case #%d: %d\n", C, res);
}
return 0;
}