多重背包:有\(n\)种物品与承重为\(V\)的背包。每种物品有有限件\(s_i\),每个物品都有对应的重量\(v_i\)与价值\(c_i\),求最大能装的价值。
暴力做法 \(O(nVs)\)
for(int i=1; i<=n; i++) {
for(int j=V; j>=0; j--) {
for(int k=0; k<=s[i]; k++) {
if(j-v[i]*k < 0) break;
f[j] = max(f[j], f[j-v[i]*k] + c[i]*k);
}
}
}
二进制分组优化 \(O(nV log s)\)
这个优化是把第三个循环优化。原来是枚举\(0\)~\(s_i\)。可以对\(s_i\)拆分
如\(s_i\) \(=\) \(12\), 分解成\(1+2+4+5\),这样所有\(1\)~\(12\)的数都可以用它们中某些数的和表示.因此只要循环4次就能得到正确答案
具体拆分方法是从\(2^0\)开始,\(2^1\),\(2^2\),...最后剩下的无法拆的单独算一个.
单调队列优化 \(O(nV)\)
令\(d = v_i\), \(a = j / v_i\), \(b =\) \(j \mod v_i\)
实际上f[j]可以由以下这些状态转移来:
f[y] (\(y=b\), \(b+d\), \(b+2d\), \(b+3d\), \(b+4d\),.., \(j\))
那么f[j] = max(f[b+k*d] - k*c[i]) + a*c[i];
当前问题就是求前面\(j\)在\(y\)中的前面的\(s_i+1\)个数的最大值
第一个状态序列存储可用状态
第二个单调队列可以维护状态队列的最值,查询\(O(1)\)
两种优化的代码
Luogu P1833 樱花
题目描述
爱与愁大神后院里种了n棵樱花树,每棵都有美学值Ci。爱与愁大神在每天上学前都会来赏花。爱与愁大神可是生物学霸,他懂得如何欣赏樱花:一种樱花树看一遍过,一种樱花树最多看Ai遍,一种樱花树可以看无数遍。但是看每棵樱花树都有一定的时间Ti。爱与愁大神离去上学的时间只剩下一小会儿了。求解看哪几棵樱花树能使美学值最高且爱与愁大神能准时(或提早)去上学。
输入输出格式
输入格式:
共n+1行:
第1行:三个数:现在时间Ts(几点:几分),去上学的时间Te(几点:几分),爱与愁大神院子里有几棵樱花树n。
第2行~第n+1行:每行三个数:看完第i棵树的耗费时间Ti,第i棵树的美学值Ci,看第i棵树的次数Pi(Pi=0表示无数次,Pi是其他数字表示最多可看的次数Pi)。
输出格式:
只有一个整数,表示最大美学值。
输入输出样例
输入样例#1:
6:50 7:00 3
2 1 0
3 3 1
4 5 4
输出样例#1:
11
说明
100%数据:Te-Ts ≤ 1000,n ≤ 10000
样例解释:赏第一棵樱花树一次,赏第三棵樱花树2次
题解
(当时做这都题的时候不优化的暴力DP是过不了的,现在貌似又放宽了时限)
二进制优化版:
#include <iostream>
#include <cstdio>
using namespace std;
#define MAXN 10100
int V, n;
int v[MAXN], c[MAXN], s[MAXN], f[MAXN];
int main() {
int a1, b1, c1, d1;
scanf("%d:%d %d:%d %d", &a1, &b1, &c1, &d1, &n);
V = (c1 - a1) * 60 + d1 - b1;
for(int i=1; i<=n; i++) {
scanf("%d%d%d", &v[i], &c[i], &s[i]);
if(!s[i]) {
for(int j=v[i]; j<=V; j++)
f[j] = max(f[j], f[j-v[i]] + c[i]);
} else {
int k, m;
for(k=1; (k<<1)<s[i]; k<<=1) {
for(int j=V; j>=v[i]*k; j--)
f[j] = max(f[j], f[j-v[i]*k] + c[i]*k);
}
m = s[i] - k + 1;
for(int j=V; j>=v[i]*m; j--)
f[j] = max(f[j], f[j-v[i]*m] + c[i]*m);
}
}
printf("%d\n", f[V]);
return 0;
}
单调队列优化版:
#include <iostream>
#include <cstdio>
using namespace std;
#define MAXN 10010
int V, n;
int v[MAXN], c[MAXN], s[MAXN], f[MAXN / 10];
int Q1[MAXN], Q2[MAXN]; //合法状态queue 与单调queue
int main() {
int a1, b1, c1, d1;
scanf("%d:%d %d:%d %d", &a1, &b1, &c1, &d1, &n);
V = (c1 - a1) * 60 + d1 - b1;
int Head1, Head2, Tail1, Tail2, Cnt, t; //queue相关
for(int i=1; i<=n; i++) {
scanf("%d%d%d", &v[i], &c[i], &s[i]);
if(!s[i]) { //先跑完全背包
for(int j=v[i]; j<=V; j++)
f[j] = max(f[j], f[j-v[i]] + c[i]);
}
}
for(int i=1; i<=n; i++) { //单调队列优化的多重背包
if(!s[i]) continue;
s[i] = min(s[i], V / v[i]);
for(int j = 0; j < v[i]; j++) {
Head1 = Tail1 = Head2 = Tail2 = Cnt = 0;
for(int k = j; k <= V; k += v[i]) {
if(Tail1 - Head1 == s[i] + 1) {
if(Q2[Head2 + 1] == Q1[Head1 + 1]) ++ Head2;
++ Head1;
}
t = f[k] - Cnt * c[i];
Q1[++ Tail1] = t;
while(Head2 < Tail2 && Q2[Tail2] < t) -- Tail2;
Q2[++ Tail2] = t;
f[k] = Q2[Head2 + 1] + Cnt * c[i];
++ Cnt;
}
}
}
printf("%d\n", f[V]);
return 0;
}