单调队列优化多重背包

题目描述

有 N 种物品和一个容量是 V 的背包。

第 i 种物品最多有 \(s_i\) 件,每件体积是 \(v_i\),价值是 \(w_i\)

求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。

输入格式

第一行两个整数,\(N,V (0<N≤1000, 0<V≤20000)\),用空格隔开,分别表示物品种数和背包容积。

接下来有 N 行,每行三个整数 \(v_i,w_i,s_i\),用空格隔开,分别表示第 i 种物品的体积、价值和数量。

输出格式

输出一个整数,表示最大价值。

数据范围

\(0<N≤1000\)
\(0<V≤20000\)
\(0<v_i,w_i,s_i≤20000\)

提示

本题考查多重背包的单调队列优化方法。

输入样例

4 5
1 2 3
2 4 1
3 4 3
4 5 2

输出样例:

10

算法思路

从最简单的多重背包讲起,代码如下

cin >> n >> m;

for(int i = 1; i <= n; i++)
  	cin >> v >> w >> s;
  	for(int j = 1; j <= m; j++)
      	for(int k = 0; k <= s && k * v <= j; k++)
          dp[i][j] = max(dp[i][j], dp[i-1][j-k*v]+k*w);
cout << dp[n][m];

我们将体积j换一种表达方式写出进行观察

0*v, 0*v+1, 0*v+2, ..., 0*v+v-1, 1*v+0, 1*v+2, ..., k*v+t		 (0 <= t < v)
这里k*v+t == j

可以发现,当更新dp[i][j]时,只需要用到j mod v相同的dp元素

\(比如当j = t*v + 3 (0\le t \le k)时\)

\(dp[j] = max(dp[t*v+3], dp[(t-1)*v+3],dp[(t-2)*v+3],...,dp[3])\)

因此在对dp[i][j]进行更新时,可以按照mod v的值将其分类,

根据j % v = 0、1、 2、......、v - 1分成v类,按照类进行更新

按照类更新,我们可以引入单调队列,减少寻找max的花费

假设当前j = t *v + 3,我们的更新顺序就是dp[3], dp[3*2],dp[3*3], .....我们将每个结果存储到单调队列中

因为\(dp[j] = dp[t*v + 3] = max(dp[t*v + 3],dp[(t-1)*v+3],dp[(t-2)*v+3],...)\)

因此在更新dp[j]时,只需要将dp[j]入队,此时单调队列的队头即为max结果

Q1、处理w的变化问题

假设j = 2 * v + 3

\(dp[j] = max(dp[3]+2w,dp[v+3]+w,dp[2v+3])\)

而当j = 3 * v + 3时

\(dp[j] = max(dp[3]+3w,dp[v+3]+2w,dp[2v+3]+w,dp[3v+3])\)

可以看出当j不同时,dp[3]后边的w不断的在变化,因此之前更新dp[3]时放入的值,在后续更新时是无效的,因此我们换一种表达方法÷

dp[j]    =     dp[j]
dp[j+v]  = max(dp[j], dp[j+v] - w) + w
dp[j+2v] = max(dp[j], dp[j+v] - w, dp[j+2v] - 2w) + 2w
dp[j+3v] = max(dp[j], dp[j+v] - w, dp[j+2v] - 2w, dp[j+3v] - 3w) + 3w
...
这样,每次入队的值是 dp[j+k*v] - k*w, 出队时加上对应的w即可

Q2、单调队列长度是多少?什么时候需要出队?

注意多重背包是有个数限制的,观察转移方程,假设体积j足够大,那么

\(dp[i][j] = max(dp[i-1][j], dp[j-v]+w,dp[j-2v]+2w,...dp[j-sv]+sw)\)

观察可知,点j处的值,只和[j-s*v, j]内的元素有关,因此

  • 单调队列长度是s*v

  • 当队头元素的位置小于( j-s*v )时,就应该出队

实现

#include<iostream>
#include<cstring>

using namespace std;

const int N = 20010;

int n, m;
int v, w, s;
int p[N], dp[N], pre[N];

int main(){
    cin >> n >> m;
    for(int i = 0; i < n; i++){
        cin >> v >> w >> s;
        memcpy(pre, dp, sizeof dp);
        for(int j = 0; j < v; j++){
            int hh = 0, tt = -1;
            for(int k = j; k <= m; k+=v){
                //1.看要不要出队
                if(hh <= tt && p[hh] < k - s * v) ++hh;
                //2.找到位置
                while(hh <= tt && pre[p[tt]] - (p[tt] - j) / v * w <= pre[k] - (k - j) / v * w) --tt;
                //3.入队
                p[++tt] = k;
                //4.更新dp
                dp[k] = pre[p[hh]] + (k - p[hh]) / v * w;
            }
        }
    }
    cout << dp[m];
}
posted @ 2022-04-11 00:39  INnoVation-V2  阅读(111)  评论(0编辑  收藏  举报