单调队列优化多重背包详解

多重背包基本模型如下:
给定N种物品,其中第i种物品的体积为Vi,价值为Wi,并且有Ci个。有一容积为M的背包,要求选择若干个物品放入背包,使得物品总体积不超过M的前提下,物品价值总和最大。

输入格式
第一行两个整数,N,M,用空格隔开,分别表示物品种数和背包容积。

接下来有 N 行,每行三个整数 Vi,Wi,Ci,用空格隔开,分别表示第 i 种物品的体积、价值和数量。

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

输入样例
4 5
1 2 3
2 4 1
3 4 3
4 5 2
输出样例:
10
数据范围
0<N,M≤100
0<Vi,Wi,Ci≤100

使用单调队列优化的动态规划求解多重背包问题,时间复杂度可以进一步优化到O(NM),与0/1背包中的DP算法效率相同。

在朴素解法中,DP数组省略了“阶段”这一维。当外层循环进行到i时,F[j]表示从前i种物品中选出若干个放入背包,体积之和为j时,价值之和最大是多少。倒序循环j,在状态转移时,考虑选取第i个物品的个数为cnt:
在这里插入图片描述
画出能够转移到j的决策候选集合{j-cnt*Vi|1<=cnt<=Ci}:
在这里插入图片描述
当循环变量 j 减小时:
在这里插入图片描述
可以发现,相邻两个状态 j 和 j-1 对应的候选集合没有重叠,很难快速地从 j-1 对应的集合得到 j 对应的集合。

但是,我们试着考虑一下 j 和 j-Vi:

在这里插入图片描述
这两者对应的决策候选集合之间的关系,与我们前面讲解的单调队列题目非常相似,只有一个新决策加入候选集合、一个已有决策被排除。所以,我们应该把状态 j 按照以Vi的余数分组,对每一组分别进行计算,不同组之间的状态在阶段 i 不会互相转移:
余数为0—0,Vi,2Vi,…
余数为1—1,1+Vi,1+2Vi,…
余数位2—2,2+Vi,2+2Vi,…

余数为Vi-1—(Vi-1),(Vi-1)+Vi,(Vi-1)+2Vi,…
把候选集合组合起来就是0,1,2,3,…,Vi-1,Vi,…
就是连续的候选集合。

把倒序循环 j 的过程改为
枚举每个余数u{0,Vi-1},倒序循环p=(M-u)/ Vi ~ 0 。
反过来,用余数和向下取整的p可以计算出 j。
对应的状态就是 j=u+p * Vi。
虽然背包容量为M,放第 i 中物种最多有p个,但第 i 种物品只有Ci个,
故能转移到 j=u+p * vi的候选集合就是{u+k * Vi | p-Ci<= k <= p-1}。
写出新的转移方程:
F[u+p * Vi]=max{F[u+k * Vi+(p-k) * Wi} p-Ci<=k<=p-1
我来解释一下这个转移方程,正常按0/1背包处理,那么转移方程应该为
F[u+p * Vi]=max{F[u+p * Vi-k * Vi]+k * Wi} 1<=k<=Ci
枚举余数u和p可以组成 j 从0到M,相当于以前0/1背包枚举背包容量 j 是一样的。把上面的转移方程中的p * Vi - k * Vi 改为K * Vi ,把k 的范围改为 p-Ci<= k <= p-1
转换的过程其实就是数学中的换元法:
p * Vi - k * Vi =(p-k) * Vi
设p-k=t那么k=p-t
1<=k<=Ci
那么 1<=p-t<=Ci
1 -p<=-t<=Ci-p
Ci-p<=t<=p-1
再把 t 换成 k 就ok了。
如果不转化,F[u+p * Vi-k * Vi]+k * Wi]中k的取值范围,无论外层循环 i 和 u怎么变化,决策k的取值范围始终是1<=k<=Ci,不符合单调队列决策k范围上下界均单调递减或单调递增。
我们以前讲过单调队列解决的问题是求区间长度固定,连续区间内数列的最值
如下图:
在这里插入图片描述

我们把外层循环 i 和 u看作定值,当内层循环变量p减小1是,决策k的取值范围[p-Ci,p-1]的上、下界均单调减小。状态转移方程等号右侧的式子仍然分为两部分,仅包含变量p和p * Wi和仅包含变量k的F[u+k * Wi]-k * Wi。综上所述,我们可以建立一个决策点k单调递减、数值F[u+k * Vi]-k * Wi单调递减的队列,用于维护候选集合。对于每个p,执行单调队列的三个惯例操作:
1、检查对头合法性,把大于p-1的决策点出队。
2、取对头为最优决策,更新F[u+p * Vi]。
3、把新决策k=p-Ci-1插入队尾,入队前检查单调性,排除无用决策。
整个算法时间复杂度O(NM)。
代码:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#include<string>
using namespace std;
int n, m, V[210], W[210], C[210];
int f[20010], q[20010];

int calc(int i, int u, int k) {
	return f[u + k*V[i]] - k*W[i];
}

int main() {
	cin >> n >> m;
	memset(f, 0xcf, sizeof(f)); // -INF
	f[0] = 0;
	for (int i = 1; i <= n; i++) {
		scanf("%d%d%d", &V[i], &W[i], &C[i]);
			for (int u = 0; u < V[i]; u++) {
		
			int l = 1, r = 0;
		
			int maxp = (m - u) / V[i];
			for (int k = maxp - 1; k >= max(maxp - C[i], 0); k--) {
				while (l <= r && calc(i, u, q[r]) <= calc(i, u, k)) r--;
				q[++r] = k;
			}
		
			for (int p = maxp; p >= 0; p--) {
			
				while (l <= r && q[l] > p - 1) l++;
			
				if (l <= r)
					f[u + p*V[i]] = max(f[u + p*V[i]], calc(i, u, q[l]) + p*W[i]);
				
				if (p - C[i] - 1 >= 0) {
					while (l <= r && calc(i, u, q[r]) <= calc(i, u, p - C[i] - 1)) r--;
					q[++r] = p - C[i] - 1;
				}
			}
		}
	}
	int ans = 0;
	for (int i = 1; i <= m; i++) ans = max(ans, f[i]);
	cout << ans << endl;
}
  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值