多重背包-O(VN)详解
多重背包问题:
有N种物品和容量为V的背包,若第i种物品,容量为v[i],价值为w[i],共有n[i]件。怎样装才能使背包内的物品总价值最大?
网上关于“多重背包”的资料倒是不少,但是关于怎么实现O(N*V)算法的资料,真得好少呀,关于“单调队列”那部分算法,又没说明得很清楚,看了几遍没看懂原理,只好自己动脑去想怎么实现O(N*V)算法。
若用F[i][j]表示对容量为j的背包,处理完前i种物品后,背包内物品可达到的最大总价值,并记m[i] = min(n[i], j / v[i])。放入背包的第i种物品的数目可以是:0、1、2……,可得:
F[i][j] = max { F[i - 1] [j – k * v[i] ] + k * w[i] } (0 <= k <= m[i]) ㈠
如何在O(1)时间内求出F[i][j]呢?
先看一个例子:取m[i] = 2, v[i] = v, w[i] = w, V > 9 * v,
并假设 f(j) = F[i - 1][j],观察公式右边要求最大值的几项:
j = 6*v: f(6*v)、f(5*v)+w、f(4*v)+2*w 这三个中的最大值
j = 5*v: f(5*v)、f(4*v)+w、f(3*v)+2*w 这三个中的最大值
j = 4*v: f(4*v)、f(3*v)+w、f(2*v)+2*w 这三个中的最大值
显然,公式㈠右边求最大值的几项随j值改变而改变,但如果将j = 6*v时,每项减去6*w,j=5*v时,每项减去5*w,j=4*v时,每项减去4*w,就得到:
j = 6*v: f(6*v)-6*w、f(5*v)-5*w、f(4*v)-4*w 这三个中的最大值
j = 5*v: f(5*v)-5*w、f(4*v)-4*w、f(3*v)-3*w 这三个中的最大值
j = 4*v: f(4*v)-4*w、f(3*v)-3*w、f(2*v)-2*w 这三个中的最大值
很明显,要求最大值的那些项,有很多重复。
根据这个思路,可以对原来的公式进行如下调整:
假设d = v[i],a = j / d,b = j % d,即 j = a * d + b,代入公式㈠,并用k替换a - k得:
F[i][j] = max { F[i - 1] [b + k * d] - k * w[i] } + a * w[i] (a – m[i] <= k <= a) ㈡
对F[i - 1][y] (y= b b+d b+2d b+3d b+4d b+5d b+6d … j)
F[i][j]就是求j的前面m[i] + 1个数对应的F[i - 1] [b + k * d] - k * w[i]的最大值,加上a * w[i],如果将F[i][j]前面所有的F[i - 1][b + k * d] – k * w放入到一个队列,那么,F[i][j]就是求这个队列最大长度为m[i] + 1时,队列中元素的最大值,加上a * w[i]。因而原问题可以转化为:O(1)时间内求一个队列的最大值。
该问题可以这样解决:
① 用另一个队列B记录指定队列的最大值(或者记录最大值的地址),并通过下面两个操作保证队列B的第一个元素(或其所指向的元素)一定是指定队列的当前最大值。
② 当指定队列有元素M进入时,删除队列B中的比M小的(或队列B中所指向的元素小等于M的)所有元素,并将元素M(或其地址)存入队列B。
③ 当指定队列有元素M离开时,队列B中的第一个元素若与M相等(或队列B第一个元素的地址与M相等),则队列B的第一个元素也离队。
经过上述处理,可以保证队列B中的第一个元素(或其指向的元素)一定是所指定队列所有元素的最大值。显然队列B的元素(或其所指向的元素)是单调递减的,这应该就是《背包九讲》中的提到的“单调队列”吧,初看的时候被这个概念弄得稀里糊涂,网上的资料提到“维护队列的最大值”,刚开始还以为是维护这个单调队列的最大值,对其采用的算法,越看越糊涂。其实,只要明白用一个“辅助队列”,求另一个队列的最值,那么具体的算法,和该“辅助队列”的性质(单调变化),都很容易推导出来。
在多重背包问题中,所有要进入队列的元素个数的上限值是已知的,可以直接用一个大数组模拟队列。
“多重背包”通用模板:
- //“多重背包”通用模板
- const int MAX_V = 100004;
- //v、n、w:当前所处理的这类物品的体积、个数、价值
- //V:背包体积, MAX_V:背包的体积上限值
- //f[i]:体积为i的背包装前几种物品,能达到的价值上限。
- inline void pack(int f[], int V, int v, int n, int w)
- {
- if (n == 0 || v == 0) return;
- if (n == 1) { //01背包
- for (int i = V; i >= v; --i)
- if (f[i] < f[i - v] + w) f[i] = f[i - v] + w;
- return;
- }
- if (n * v >= V - v + 1) { //完全背包(n >= V / v)
- for (int i = v; i <= V; ++i)
- if (f[i] < f[i - v] + w) f[i] = f[i - v] + w;
- return;
- }
- int va[MAX_V], vb[MAX_V]; //va/vb: 主/辅助队列
- for (int j = 0; j < v; ++j) { //多重背包
- int *pb = va, *pe = va - 1; //pb/pe分别指向队列首/末元素
- int *qb = vb, *qe = vb - 1; //qb/qe分别指向辅助队列首/末元素
- for (int k = j, i = 0; k <= V; k += v, ++i) {
- if (pe == pb + n) { //若队列大小达到指定值,第一个元素X出队。
- if (*pb == *qb) ++qb; //若辅助队列第一个元素等于X,该元素也出队。
- ++pb;
- }
- int tt = f[k] - i * w;
- *++pe = tt; //元素X进队
- //删除辅助队列所有小于X的元素,qb到qe单调递减,也可以用二分法
- while (qe >= qb && *qe < tt) --qe;
- *++qe = tt; //元素X也存放入辅助队列
- f[k] = *qb + i * w; //辅助队列首元素恒为指定队列所有元素的最大值
- }
- }
- }
多重背包特例:物品价值和体积相等(w = v)
由于w = v,上面的代码可进行如下修改:
入队的元素: tt = f[k] - (k / v) * w = f[k] - (k - j) = f[k] - k + j
返回的最大值:*qb + (k / v) * w = *qb + k - j
由于j是定值,可调整入队的元素为: f[k] - k,最大值为 *qb + k
但这种做法相当低效。实际上,这相当于一个“覆盖”问题:在放入前i个物品后,体积为j的背包,只存在两种状态:是否能刚好装满,也就是,是否能被覆盖。因而只要记录下该状态就可以了,前面的分析进行相应的调整:
对F[i - 1][y] (y= b b+d b+2d b+3d b+4d b+5d b+6d … j)
F[i][j]就是求j的前面m[i] + 1个数对应的F[i - 1] [b + k * d](其值为0或1)的最大值,即j前面的m[i] + 1个0、1数据中是否存在1,这又可以简化为判断它们的和是否不等于0。
- //pack-01
- const int MAX_V = 100004;
- //w = v 特例
- inline void pack(bool f[], int V, int v, int n)
- {
- if (n == 0 || v == 0) return;
- if (n == 1) { //01背包
- for (int i = V; i - v >= 0; --i)
- if (! f[i] && f[i - v]) f[i] = true;
- //if (f[i - v]) f[i] = true;
- return;
- }
- if (n * v >= V - v + 1) { //完全背包 n >= V / v
- for (int i = v; i <= V; ++i)
- if (! f[i] && f[i - v]) f[i] = true;
- //if (f[i - v]) f[i] = true;
- return;
- }
- bool va[MAX_V];
- for (int j = 0; j < v; ++j) { //多重背包
- bool *pb = va, *pe = va - 1;
- size_t sum = 0;
- for (int k = j; k <= V; k += v) {
- if (pe == pb + n) sum -= *pb++; //队列已满,队首元素出队
- *++pe = f[k]; //进队
- sum += f[k];
- if (! f[k] && sum != 0) f[k] = true;
- //f[k] = (bool)sum;
- }
- }
- }
另外,可以倒着读数据,这样就不需要额外使用一个数组存放临时数据:
- //pack-02
- //w = v 特例
- inline void pack(bool f[], int V, int v, int n)
- {
- if (n == 0 || v == 0) return;
- if (n == 1) { //01背包
- for (int i = V; i - v >= 0; --i)
- if (! f[i] && f[i - v]) f[i] = true;
- //if (f[i - v]) f[i] = true;
- return;
- }
- if (n * v >= V - v + 1) { //完全背包 n >= V / v
- for (int i = v; i <= V; ++i)
- if (! f[i] && f[i - v]) f[i] = true;
- //if (f[i - v]) f[i] = true;
- return;
- }
- for (int j = 0; j < v; ++j) { //多重背包
- int k = V - j, sum = 0;
- //前n + 1个元素入队,前面的判断可以保证: V - j - n * v > 0
- for (; k >= std::max(0, V - j - n * v); k -= v) sum += f[k];
- for (int i = V - j; i > 0; k -= v, i -= v) {
- if (f[i]) --sum; //出队: sum -= f[i]
- else if (sum != 0) f[i] = true;
- //int tt = f[i]; f[i] = (bool)sum; sum -= tt;
- if (k >= 0) sum += f[k];
- }
- }
- }
前面的代码,都在循环中对队列的元素个数进行判断,这可以通过下面的方法避免,将循环拆分成两部分:一部分都有入队和出队操作、另一部分只有入队(或出队)操作。
对该特例,还有一种O(N * V)解法:用一个数组记录当前物品已经使用数,关键代码:
if (! f[i] && f[i - v] && count[i - v] < n)
f[i] = true, count[i] = count[i - v] + 1;
每计算一类物品,count数组都要初始化一次,比较费时,可以再用一个数组记录上一次处理的物品编号,通过判断上一次放入那一类的物品编号与当前这类物品编号是否一致(不一致时,相当于count[i]值为0的情况),从而避免对count数组的初始化操作。还可以将初始化count数组和后面的循环整合在一起。
- //pack-1
- for (int i = 0; i <= V; ++i) count[i] = 0;
- for (int i = v; i <= V; ++i) { //多重背包
- if (! f[i] && f[i - v] && count[i - v] < n) {
- count[i] = count[i - v] + 1;
- f[i] = true;
- }
- }
- //pack-2 cur为当前这类物品的编号
- for (int i = v; i <= V; ++i) { //多重背包
- if (! f[i] && f[i - v]) {
- if (last[i - v] != cur) count[i] = 1, last[i] = cur, f[i] = true;
- else if (count[i - v] < n) {
- count[i] = count[i - v] + 1;
- last[i] = cur;
- f[i] = true;
- }
- }
- }
- //pack-4
- for (int i = v; i <= V; ++i) { //多重背包
- if (f[i]) count[i] = 0;
- else if (f[i - v]) {
- if (i < 2 * v) count[i] = 1, f[i] = true;
- else if (count[i - v] < n) {
- count[i] = count[i - v] + 1;
- f[i] = true;
- }
- }
- }
POJ 1742:有若干不同面值的纸币,问能组合出1到m中的几种面值?
- //poj1742
- #include<algorithm>
- #include<cstdio>
- #define AB 0
- //MAX_N 物品种类数最大值 MAX_n每种物品数目的最大值,MAX_V背包体积最大值
- const int MAX_N = 101, MAX_V = 100004;
- //w = v特例
- inline void pack(bool f[], int V, int v, int n, int& total)
- {
- //if (n == 0 || v == 0) return;
- if (n == 1) { //01背包
- for (int i = V; i - v >= 0; --i)
- if (! f[i] && f[i - v]) f[i] = true, ++total;
- return;
- }
- if (n * v >= V - v + 1) { //完全背包 n >= V / v
- for (int i = v; i <= V; ++i)
- if (! f[i] && f[i - v]) f[i] = true, ++total;
- return;
- }
- for (int j = 0; j < v; ++j) { //多重背包
- int k = V - j, sum = 0;
- //前n + 1个元素入队,前面的判断可以保证: V - j - n * v > 0
- for (; k >= V - j - n * v; k -= v) sum += f[k];
- for (int i = V - j; i > 0; k -= v, i -= v) {
- if (f[i]) --sum; //出队: sum -= f[i]
- else if (sum != 0) f[i] = true, ++total;
- //int tt = f[i]; f[i] = (bool)sum; sum -= tt;
- if (k >= 0) sum += f[k];
- }
- }
- }
- struct Node {
- int n, v, V;
- bool operator<(const Node& other) const { return V < other.V;}
- };
- int main()
- {
- #if AB == 1
- freopen("src.txt","r",stdin);
- freopen("z-b.txt","w",stdout);
- #endif
- Node node[MAX_N];
- int V, N;
- bool f[MAX_V];
- while (scanf("%d %d",&N,&V) != EOF) {
- if (N + V == 0) break;
- Node *np = node + N;
- for (Node *p = node; p < np; ++p) scanf("%d", &p->v);
- for (Node *p = node; p < np; ++p) {
- scanf("%d", &p->n);
- p->V = std::min(p->n * p->v, V);
- }
- std::sort(node, np);
- int total = 0;
- f[0] = true;
- for (int i = 1; i <= V; ++i) f[i] = false;
- int mv = 0;
- for (Node *p = node; p < np && total < V; ++p) {
- mv = std::min(mv + p->V, V);
- pack(f,mv,p->v,p->n, total);
- }
- printf("%d/n",total);
- }
- }
用自己随机生成的数据测试了下,上面提到的几种方法,所用时间都是7秒多点,有排序的比没排序的稍微快点。但在POJ上提交的结果,不同代码的耗时相差挺大,快的在1秒左右,慢的接近1.5秒。
还有一种特例:
给定面值为1、2、5的纸币若干个,问其所不能支付的最低价格(假设为自然数)。
这可以用多重背包(或母函数)来解决,但实际上是有O(1)解法的。
多重背包-O(VN)详解相关推荐
- 0x52. 动态规划 - 背包(习题详解 × 19)
目录 0x52. 动态规划 - 背包 0x52.1 0/10/10/1 背包 Problem A. 数字组合 Problem B. 背包问题求具体方案 Problem C. jury Compromi ...
- 01背包经典例题详解
转载自点击打开链接 首先01背包题目的雏形是 有N件物品和一个容量为V的背包.第i件物品的费用是c[i],价值是w[i].求解将哪些物品装入背包可使价值总和最大. 从这个题目中可以看出,01背包的特点 ...
- 多重背包O(VN)算法——单调队列优化
多重背包问题: 有N种物品和容量为V的背包,若第i种物品容量为v[i],价值为w[i],总共有n[i]件,怎样装才能使背包内的物品总价值最大? 设dp[i][j]表示对容量为j的背包,处理完前i种物品 ...
- 【算法】01背包及其优化详解
[01背包] 一.问题描述: 在NNN个物品,背包容量为CCC的情况下,每个物品的价值为viv_ivi,重量为wiw_iwi,每个物品选择装入背包(1)或者选择不装入背包(0).然后选择物品装入背 ...
- 动态规划——背包问题(详解)
动态规划是我最早接触的算法,一开始非常简单,固定模板题,后来愈发愈发难起来了,条件,状态压缩等等,难点主要是,状态怎么表示,状态转移方程怎么写,这篇文章将会从背包五大问题详解,希望能帮助到大家去类比, ...
- 多重背包O(N*V)算法详解(——使用单调队列)
多重背包O(N*V)算法详解(--使用单调队列) 多重背包问题: 有N种物品和容量为V的背包,若第i种物品,容量为v[i],价值为w[i],共有n[i]件.怎样装才能使背包内的物品总价值最大? 网上关 ...
- 动态规划背包问题之多重背包详解
背包问题前几篇文章: 01背包详解 完全背包详解 文章目录 一.什么是多重背包问题? 二.例题分析 1. 题目: 2.第一种:朴素做法 3.第二种:二进制优化 4.第二种:单调队列优化<待写&g ...
- 背包详解:完全背包与多重背包
目录 完全背包 优化一:输入优化 优化二:二进制 优化三:重复放入的 01 背包 多重背包 总结 完全背包 有一个大小为 m 的背包,有 N 种物体,每种物品的价值为 Vi, 大小为 Ai, 并且每种 ...
- 背包问题详解:01背包、完全背包、多重背包
参考链接: http://www.cnblogs.com/fengty90/p/3768845.html http://blog.csdn.net/mu399/article/details/7722 ...
最新文章
- 【Vegas原创】安装rhel6.2,不能进图形化界面的终极解决方法
- 计算机网络系统中hn是,中南大学计算机网络作业1.pdf
- HDU- 1754 I Hate It
- 解决myeclipse报错500,MySQLyog报错误码问题
- [6] ADB 文件管理
- matplotlib之contours等高线、3D图(笔记五)
- MongoDB集群架构 调整,增加延迟备份节点服务器,删除仲裁节点(9)
- 芯烨 XP-C76II+ 打印机驱动
- 计算机桌面变小了是怎么回事啊,电脑桌面整体变小了要怎么调回来的
- oracle索引b 树,oracle btree索引概述
- adobe怎么统计字数_pdf文件怎么快速统计字数?
- 什么是前端框架,当前主流的前端框架
- 2016年总结:教师路的开启,爱情味的初尝 (下)
- 解决关键词这个问题,ASO优化效果事半功倍
- Field not found; typically this occurs with arrays which are not mapped as single value
- 【2018ECCV】Zero-Shot Deep Domain Adaptation 零样本深度域适应
- 汇集了很多swift 学习指南
- 除了SCI-HUB,还有那么多免费外文文献网站,资源速取。
- 360极速浏览器审查模式保存图片
- 华视身份证读卡器网页信息采集失灵处理办法
热门文章
- 【phpcms-v9】phpcms-v9系统搭建wap网站
- python matplotlib常见图形画法
- swiper 自定义分页器显示_swiper自定义分页器使用方法详解
- VM重启后 Ubuntu 网卡丢失 没有网络连接 的原因
- 计算机黑屏图片,关于电脑黑屏的问题!
- Log4j的扩展-支持设置最大日志数量MaxFileSize的DailyRollingFileAppender
- Java中的数组方法and二维数组
- 母亲节|TcaplusDB祝天下母亲节日快乐!
- 假想面试题:现在有一串字符串2, 2, 3……,其中字符串中的数字类似于Word文档中的标题级别,最终效果是让它们按照Word文档导航窗格中的标题级别格式进行展示
- Linux下查看Python安装路径