【DP】【单调队列--多重背包】
【01背包模板】
1 #include <iostream> 2 #include <algorithm> 3 4 using namespace std; 5 6 const int Maxv = 1001; 7 int N, V, F[Maxv]; 8 9 int main() 10 { 11 cin >> N >> V; 12 for (int i = 1, w, c; i <= N; i ++) 13 { 14 cin >> w >> c; 15 for (int j = V; j >= w; j --) 16 F[j] = max(F[j], F[j - w] + c); 17 } 18 cout << F[V] << endl; 19 return 0; 20 }
【完全背包模板】
1 #include <iostream> 2 #include <algorithm> 3 4 using namespace std; 5 6 const int Maxv = 1001; 7 int N, V, F[Maxv]; 8 9 int main() 10 { 11 cin >> N >> V; 12 for (int i = 1, w, c; i <= N; i ++) 13 { 14 cin >> w >> c; 15 for (int j = w; j <= V; j ++) 16 F[j] = max(F[j], F[j - w] + c); 17 } 18 cout << F[V] << endl; 19 return 0; 20 }
【多重背包(二进制拆分)模板】
1 #include <iostream> 2 #include <algorithm> 3 4 using namespace std; 5 6 const int Maxv = 1001; 7 int N, V, F[Maxv]; 8 9 int main() 10 { 11 cin >> N >> V; 12 for (int i = 1, p, w, c; i <= N; i ++) 13 { 14 cin >> p >> w >> c; p = min(p, V / w); 15 int m = 1; 16 while (p) 17 { 18 if (m > p) m = p; p -= m; 19 for (int j = V; j >= m * w; j --) 20 F[j] = max(F[j], F[j - m * w] + m * c); 21 m *= 2; 22 } 23 } 24 cout << F[V] << endl; 25 return 0; 26 }
【二进制拆分解析】本质是将多重背包转化为01背包,然后运用01背包的状态进行转移。对于物品I,假设它的数量有PI,将PI进行拆分。注意:此处并非是将PI拆成PI个物品,而是运用二进制的思想,将PI拆分为1,2,2^2,2^3,...,2^k,PI-2^k(这个物品要注意到)这些数量的物品,(物品的价值等于数量*个数),因为通过这些物品的01组合,可以自然地得出0..PI所有数量的物品个数。所以,问题就转化成了01背包的求解,只要将这些物品看成是普通的01背包中的物品即可。
有个小优化是p=min(p, V/w);p表示的是这个物品的数量。
【多重背包(单调队列)模板】
1 #include <iostream> 2 #include <deque> 3 #include <algorithm> 4 5 using namespace std; 6 7 struct Pack 8 { 9 int sum, cost; 10 Pack(int _s, int _c) : sum (_s), cost(_c) {} 11 }; 12 13 const int Maxv = 1001; 14 deque <Pack> Q; 15 int N, V, F[Maxv]; 16 17 int main() 18 { 19 cin >> N >> V; 20 for (int i = 1, p, w, c; i <= N; i ++) 21 { 22 cin >> p >> w >> c; p = min(p, V / w); 23 for (int j = 0; j < w; j ++) 24 { 25 Q.clear(); 26 for (int k = 0; k <= (V - j) / w; k ++) 27 { 28 int y = F[k * w + j] - k * c; 29 while (Q.size() && Q.back().cost <= y) Q.pop_back(); 30 Q.push_back(Pack(k, y)); 31 if (Q.front().sum < k - c) Q.pop_front(); 32 F[k * w + j] = Q.front().cost + k * c; 33 } 34 } 35 } 36 cout << F[V] << endl; 37 return 0; 38 }
【单调队列解析】
V表示背包总容量,Wi表示当前物品的重量,Ci表示当前物品的价值,Pi表示当前物品的数量。
最朴素的多重背包转移方程为:F[I][V] = Max{F[I - 1][V - K * Wi] + K * Ci} 0<=k<=min(Pi, V / W)
现在对其进行分析,F[I][V]可以从F[I-1][V-Wi],F[I-1][V-2Wi],F[I-1][V-3Wi]...转移而来
F[I][V-Wi]可以从F[I-1][V-2Wi],F[I-1][V-3Wi] ...转移而来。
可以看到,这2者之间存在着重复的过程,因此可以进行优化。方法如下:
假设Wi=3,将V对Wi的余数进行编号:
上一排表示从0..V的体积,下一排表示对Wi的余数。
接着进行分组:将余数相同的体积分为一组!对于余数为d的这一组,我们进行分析:
F[I][d], F[I][d + Wi], F[I][d + 2Wi],F[I][d+3Wi]...
以上这些体积进行状态转移时候只需要借助的是:
F[I-1][d], F[I-1][d + Wi], F[I-1][d + 2Wi],F[I-1][d+3Wi]...
由于F[I][]要反复地借用F[I-1][]这些状态,因此想到将F[I-1][]的这些状态放入一个队列,倘若这个队列每次可以通过O(1)时间取出F[I-1][]中最优的决策,那么问题就得到本质的优化。
此处要注意一个问题,就是F[I-1][d], F[I-1][d + Wi], F[I-1][d + 2Wi],F[I-1][d+3Wi]...这些状态互相之间是没有可比性的!因为体积大的自然价值也就会大。
将它们转变为F[I-1][d], F[I-1][d + Wi]-Ci, F[I-1][d + 2Wi]-2Ci,F[I-1][d+3Wi]-3Ci
这样做的目的在于使得每个状态相当于体积为d,然后互相之间就具有了可比性。因此,这之中最优的那个状态就可以用来进行F[I][]的转移。
下面解释一下单调队列为什么单调以及它的维护过程:
队列中的元素需要记录2个域,1个是sum,为F[I-1][d + sum*Wi]中的那个sum,表示它是在余数为d的基础上,多放了sum个Wi。另1个是cost,即F[I-1][d + sum*Wi]-sum*Ci。
每次在求F[I][d+sum*Wi]的时候,需要将F[I-1][d],F[I-1][d+Wi]-Ci,F[I-1][d+2Wi]-2Ci,F[I-1][d+3Wi]-3Ci...F[I][d+sum*Wi]-sum*Ci放进队列中。然后取出其中的最大值,然后再加上sum*Ci即可。
每次循环到当前的F[I][d+K*Wi]的时候,就新添加一个元素(K,F[I-1][d+K*Wi]-K*Ci)进入队列。新元素进入队列的时候,从队列的最右端开始检测,倘若遇到cost值小于等于新元素cost值的元素,那么原来的这些元素就不具有价值了。因为新元素的sum值和cost值都大于等于旧元素的sum和cost,那么在状态转移时候,新元素完全可以取代旧元素。因此删除这些旧元素。这样一来,队列就变成了cost值严格单调递减的队列。每次只需要取出队头元素更新即可。需要注意一点是,队头元素的sum+Pi之后如果仍然<当前的K,那么就要将队头元素出队了。详情看代码可知。