题目链接:
6. 多重背包问题 III - AcWing题库www.acwing.com 背包九讲bilibiliwww.bilibili.com闫学灿大神的背包九讲到两种完全背包问题的优化算法,第一种是通过二进制拆包将时间复杂度从N*S*M降低到N*logS*M。第二种是通过单调队列将算法的时间复杂度进一步降到N*M。不过视频讲解太快,思路没跟上来。核心代码就四五行,但是没有理解思路的话是很难看懂的,我花了一天时间才算看明白。网络上找到的资料要么排版太差,要么就是符号不一致,看起来很费力,所以干脆自己整理一份资料。
设N表示有多少种物品,C[i]记录每种物品的容量,W[i]记录每种物品的价值,S[i]记录每种最多能放多少个,M表示背包的容量。如果是一路看大雪菜视频过来的话,很容易得到状态转移方程:
从公式中可以看出f[j]和f[j+c]都是从s+1个数里面取最大值,计算f[j+c]时只是将滑动窗口右移了一步,类似下图的效果:
只不过移动的时候,前面的s个元素都增加了w,每个元素加上相同的数不影响计算最大值。使用单调队列可以在O(N)时间复杂度下找到所有滑动窗口的最大值,关于单调队列处理滑动窗口的问题可以看看这篇文章:
labuladong:特殊数据结构:单调队列zhuanlan.zhihu.com容易知道f[j]的计算只依赖于g[k],其中j%c = k%c。因此可以将g[0~m]按%c的余数进行分类:
g[0],g[c],g[2c],g[3c],...
g[1],g[1+c],g[1+2c],g[1+3c],...
...
g[c-1],g[2c-1],g[3c-1],g[4c-1]...
每个分类可以计算出:
f[0],f[c],f[2c],f[3c],...
f[1],f[1+c],f[1+2c],f[1+3c],...
...
f[c-1],f[2c-1],f[3c-1],f[4c-1]...
从而整个f[0~m]都能计算出来。
上面的讲解可能有点啰嗦,归纳起来就两点:
- 需要将所有状态按照%c的余数进行分类,每个分类可以计算出下一层对应的分类
- 计算下一层对应分类的过程类似于滑动窗口取最大值,利用单调队列可以实现O(N)的时间复杂度
综上所述,C++代码如下:
#include <iostream>
#include <deque>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
int n, m;
cin >> n >> m;
deque<int> q;
vector<int> f(m + 1), g(m + 1);
for (int i = 0; i < n; ++i) {
int c, w, s;
cin >> c >> w >> s;
swap(f, g);
for (int j = 0; j < c; ++j) {
q.clear();
for (int k = j; k <= m; k += c) {
f[k] = g[k];
if (!q.empty() && k - s * c > q.front()) q.pop_front(); // 最多s+1个元素,超出个数限制则移除队首元素
if (!q.empty()) f[k] = max(f[k], g[q.front()] + (k - q.front()) / c * w); // 队首肯定是最大的
while (!q.empty() && g[q.back()] + (k - q.back()) / c * w <= g[k]) q.pop_back(); //将k压入队列前,先把所有比它小的出队
q.push_back(k);
}
}
}
cout << f[m] << endl;
return 0;
}
遗憾的是居然超时了。。。。。主要是因为deque.clear()太频繁了,每次clear都会清空内存,也不存在lazy clear的API。没办法只能用数组模拟队列了。最终提交通过的代码如下:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
int n, m;
cin >> n >> m;
int q[20010];
vector<int> f(m + 1), g(m + 1);
for (int i = 0; i < n; ++i) {
int c, w, s;
cin >> c >> w >> s;
swap(f, g);
for (int j = 0; j < c; ++j) {
int hh = 0, tt = -1;
for (int k = j; k <= m; k += c) {
f[k] = g[k];
if (hh <= tt && k - s * c > q[hh]) ++hh; // 最多s+1个元素,超出个数限制则移除队首元素
if (hh <= tt) f[k] = max(f[k], g[q[hh]] + (k - q[hh]) / c * w); // 队首肯定是最大的
while (hh <= tt && g[q[tt]] + (k - q[tt]) / c * w <= g[k]) --tt; //将k压入队列前,先把所有比它小的出队
q[++tt] = k;
}
}
}
cout << f[m] << endl;
return 0;
}
说实话,这种第一次不照着答案敲是很难写对的,不过思路明白了下次写就容易很多了。