题目链接:https://www.acwing.com/problem/content/6/
解题过程:
首先原始的多重背包问题的最原始的做法是直接开三重循环,然后进一步优化是通过二进制将其转换成0/1背包去做,这里我用的方法是通过单调队列去优化原始的三重循环,时间复杂度为0(n * m);这道题目是:2e7,不会超时。
首先我们需要知道这样的算式:
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 - v] = max( , f[i - 1, j - v], f[i - 1, j - 2v]+ w, …, f[i - 1, j- sv]+(s - 1)w, f[i - , j - (s+1)v]+sw)
当我们使用原始的做法的时候,对于每一个f[i - kv], 我们是直接通过遍历的方法去求其最大数值,通过单调队列到达的目的就是将这一重循环给去掉,通过上面的算式,我们发现,对于f[i, j]来说,其实有很大一部分与f[i, j - v]是重复的,他们的差距仅仅只是相差一个w, 这个是可以通过相减来处理掉的,并且由于是求最大数值,所以刚好想到可以用单调队列来写。
代码:
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
const int maxn = 20005;
int f[maxn], g[maxn], q[maxn];
/*
f[k]:f[i, k]
g[k]:f[i - 1, k]
q[]:是一个数组模拟的单调队列
*/
int main(void) {
int n, m; scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i ++) {
int v, w, s;
scanf("%d%d%d", &v, &w, &s);
memcpy(g, f, sizeof f); //每次都将f复制道g中,这样在计算f[i, k]地时候,就可以直接调用g数组
//这里采用的方法是,遍历所有可能的余数来进行dp;与原始的遍历方法有一点理解上的细微差别
for(int j = 0; j < v; j ++) {
int hh = 0, tt = -1; //hh:队头, tt:队尾
for(int k = j; k <= m; k += v) {
//判断当前队头元素是否已经出列,注:队列中存储的是体积,相当于下标
if(hh <= tt && k - s * v > q[hh]) hh ++;
//在队列不为空的前提下,判断新进入的元素与队尾元素进行比较,若是前者更大,则将队尾元素出队
//为什么这里是 - 号,原因:我们可以去看写出的算式,发现越是体积越大的时候,其实越后面才会加一个w,
//刚好构成了一个等差数列,那么我们如果通过这个相对w之差,则刚好等价于原来相加的情况
while(hh <= tt && g[q[tt]] - q[tt] / v * w <= g[k] - k / v * w) tt --;
//入队
q[++ tt] = k;
//将队头元素放进f中,队头元素:此时的最大数值
f[k] = g[q[hh]] + (k - q[hh]) / v * w;
}
}
}
printf("%d\n", f[m]);
return 0;
}