多重背包单调队列优化思路_单调队列优化多重背包问题

题目链接:

6. 多重背包问题 III - AcWing题库​www.acwing.com 背包九讲bilibili​www.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]时只是将滑动窗口右移了一步,类似下图的效果:

35726dde6e6dab5124fcf364e4f6dd4b.png

只不过移动的时候,前面的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]都能计算出来。

上面的讲解可能有点啰嗦,归纳起来就两点:

  1. 需要将所有状态按照%c的余数进行分类,每个分类可以计算出下一层对应的分类
  2. 计算下一层对应分类的过程类似于滑动窗口取最大值,利用单调队列可以实现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;
}

说实话,这种第一次不照着答案敲是很难写对的,不过思路明白了下次写就容易很多了。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值