多重背包问题的单调队列优化

多重背包问题的单调队列优化

温馨提示:先吃甜点,再进入正餐食用更佳噢~

0-1背包问题(餐前甜点)

https://www.acwing.com/problem/content/2/

多重背包问题的单调队列优化

朴素解法

#include

using namespace std;
const int N = 1010;
int n, m; //n物品个数 m背包最大容量
int dp[N][N]; //dp[i][j]表:考虑前i个物品并且背包容量为j个体积单位的最大价值
int v[N], w[N];

int main() {
    cin >> n >> m;
    for (int i = 1; i > v[i] >> w[i];

    for (int i = 1; i = v[i]) dp[i][j] = max(dp[i][j], dp[i - 1][j - v[i]] + w[i]);
        }
    }
    cout << dp[n][m] << endl; //考虑前n个(所有)物品,背包体积容量为m的最大价值即为答案
}

空间降维

dp第一维实际上多余,因为i只需要用到i-1的状态,但实际上刚开始第i轮枚举的时候dp【i][j]的第二维表示的都是i-1时的状态,可以降维(下图所示)。

多重背包问题的单调队列优化

但是我们不能按照体积从小到大枚举,不然后续的状态更新会用到i的状态(下图所示)。

多重背包问题的单调队列优化

降序枚举,则可以避免(下图所示)。

多重背包问题的单调队列优化

降维压缩之后的代码:

#include

using namespace std;
const int N = 1010;
int n, m; //n物品个数 m背包最大容量
int dp[N]; //dp[j]表:背包容量为j个体积单位的最大价值

int main() {
    cin >> n >> m;
    for (int i = 0; i < n; i ++) {
        int v, w; //第i个物品的体积和价值
        cin >> v >> w;
        //不选第i个,dp[j] = dp[j];
        //选第i个,dp[j] = dp[j - v] + w;
        for (int j = m; j >= v; j --) dp[j] = max(dp[j], dp[j - v] + w); //从大到小枚举
    }
    cout << dp[m] << endl;
}

多重背包问题(正餐)

https://www.acwing.com/problem/content/4/

多重背包问题的单调队列优化

与0-1背包的唯一区别在于,多重背包的物品可能有多件s。

选法不像0-1背包那样:对于第i件物品要么选0件要么选1件,只有两种选法:

多重背包问题的单调队列优化

而是,一共有s+1种选法[0,s]:

多重背包问题的单调队列优化

朴素(暴力)解法

在0-1背包的代码基础上加一层循环:

#include
#include

using namespace std;
const int N = 110;
int n, m;
int f[N];

int main() {
    cin >> n >> m;

    for (int i = 0; i < n; i ++) {
        int v, w, s;
        cin >> v >> w >> s;
        for (int j = m; j >= v; j --) {
            for (int k = 1; k = k * v; k ++) { //枚举选[1,s]件的s种选法和不选的情况一起比较
                f[j] = max(f[j], f[j - k * v] + k * w);
            }
        }
    }
    cout << f[m] << endl;
}

时间复杂度O(NVS) = O(N^3) 复杂度很高,考虑优化一下。

二进制优化

https://www.acwing.com/problem/content/5/

多重背包问题的单调队列优化

实际上我们考虑将每种物品堆(s个)分组一下,把每一组看成1个物品,当成0-1背包来求解。

为了使得时间复杂度尽可能的小,我们分得的组别数必须尽可能地少,而且这些组别随机组合能够连续表示[0,s],即做一个等价类。

例如s=7,按照上文的朴素方法,等价于分成了7组:1、1、1、1、1、1、1

这里我们考虑二进制拆分,拆分成:1、2、4

0 = 不选
1 = 选1
2 = 选2
3 = 选1、2
4 = 选4
5 = 选1、4
6 = 选2、4
7 = 选1、2、4

实际上是分成:

多重背包问题的单调队列优化

s+1如果不是2的某次幂,例如10的拆法:

那就拆分成:1 2 4 3
其中:1 2 4 可表示[0, 7]
所以1 2 4 3可表示[0, 10]

思路讲解完,上代码:

#include
#include
#include
using namespace std;
int dp[2010];

struct Good{
    int v, w; //物品体积和价值
};

int main(){
    int n, m; //物品个数和背包最大容量
    cin >> n >> m;
    vector goods; //存储分组后的物品
    for(int i = 0; i < n; i++){
        int v, w, s;
        cin >> v >> w >> s;
        for(int j = 1; j = good.v; j --) //注意从大到小枚举
            dp[j] = max(dp[j], dp[j - good.v] + good.w);
    }
    cout << dp[m] << endl;
    return 0;
}

时间复杂度O(NV×log(S))=O(N^2×log(N)),实际上,复杂度还是不可观。

究极优化之单调队列优化

https://www.acwing.com/problem/content/6/

多重背包问题的单调队列优化

v[i](下面都简写成v)表示第i个物品体积,其中j=v-1,m表示背包最大容量。这里我们假设m=kv+j,其实也有可能是kv+j-1,…,kv+1,kv 只是为了方便下面的这个矩形演示,不妨假设成m=kv+j。

dp[0]

dp[1]

dp[2]

dp[3]

… … … … … … … dp[j-1]

dp[j]

多重背包问题的单调队列优化

回顾一下上文所提及的解法,在代码中的实现的第二层循环的dp都是这个状态转移流程:对于每一个物品i,都会从大到小枚举值在[v,m]的所有情况都进行一遍更新(标蓝的元素),枚举的顺序如下图示:

多重背包问题的单调队列优化

下面做具体分析:

其中标蓝元素代表待更新的状态(需要取max),粗体代表能转移到待更新状态的状态(当然,由于物品个数的限制,可能没有k个,不会是这么长,这里只是为了方便演示,暂不考虑物品个数)

多重背包问题的单调队列优化
dp[kv+j]=max( dp[(k-1)v+j] + w , dp[(k-2)v+j] + 2w , ... , dp[3v+j] + (k-3)w , dp[2v+j] + (k-2)w , dp[v+j] + (k-1)w , dp[j] + kw )

多重背包问题的单调队列优化

……

……

多重背包问题的单调队列优化

多重背包问题的单调队列优化
dp[(k-1)v+j]=max( dp[(k-2)v+j] + w , ... , dp[3v+j] + (k-4)w , dp[2v+j] + (k-3)w , dp[v+j] + (k-2)w , dp[j] + (k-1)w )

到这里的时候对比上图和下图,细心的你突然发现这里好像进行了很多没必要(貌似重复冗余但又不得不做的工作)的比较,下面进行分析:

多重背包问题的单调队列优化

而我们在进行dp[(k-1)v+j]的状态更新(取max)的时候又重新将它们再遍历了一遍。

​ 问题出在:我们每次取max都需要从”0″开始对集合(同一行)内的所有元素比较,而不能在之前的比较结果的基础上进行。

​ 导致问题的原因:我们是从大到小枚举的。举个例子:这就相当于我们遍历一个正整数集合,得到这个集合的最大值,然后我们从集合中剔除一个元素,新集合的最大值对于我们来说不是确定的(细品),我们无法利用上一次的遍历所做的工作(劳动成果不能为这次所用)。

​ 思考:如果做逆向思维,我们遍历一个正整数集合,得到这个集合的最大值,然后我们往集合中增加一个元素,新集合的最大值对于我们来说是确定的,我们可以利用上一次的遍历所做的工作(劳动成果能够为这次所用)。

​ 解决方法:所以我们应该摒弃前文描述的”从大到小枚举压缩空间”的思想,选择从小到大枚举,并且利用一种数据结构来模拟这个”变大的集合”,并且在此基础上做一些限制条件实现物品个数的限制。由于只有差值为v的时候状态才能转移,我们可以把整个集合以模v的余数为划分规则做一个等价划分,可以划分成为v个子集(模v余[0, v-1] 则每行代表一个子集,这也是本文设计这个矩形的目的),这个时候我们分别对每个集合从小到大(状态更新,在下表中从左往右)进行枚举更新,还要考虑物品的个数。

多重背包问题的单调队列优化

具体实施:以一行(同余的一个子集)为例,设置一个滑动窗口,窗口大小设置为该物品的个数+1,并在窗口内部维护一个单调队列。

至于为什么窗口大小是该物品的个数+1,举个例子:如果该物品只有2个,dp[3v+j]从dp[j]状态转移过来需要装进来3个该物品,所以不可能从dp[j]转移过来,因此也就没有必要去将dp[j]考虑进来,只需要维护窗口大小为3范围内的单调队列。

多重背包问题的单调队列优化

首先解释一下单调队列:

&#x987E;&#x540D;&#x601D;&#x4E49;&#xFF0C;&#x5355;&#x8C03;&#x961F;&#x5217;&#x7684;&#x91CD;&#x70B9;&#x5206;&#x4E3A; "&#x5355;&#x8C03;" &#x548C; "&#x961F;&#x5217;"

"&#x5355;&#x8C03;" &#x6307;&#x7684;&#x662F;&#x5143;&#x7D20;&#x7684;&#x7684; "&#x89C4;&#x5F8B;"&#x2014;&#x2014;&#x9012;&#x589E;&#xFF08;&#x6216;&#x9012;&#x51CF;&#xFF09;

"&#x961F;&#x5217;" &#x6307;&#x7684;&#x662F;&#x5143;&#x7D20;&#x53EA;&#x80FD;&#x4ECE;&#x961F;&#x5934;&#x548C;&#x961F;&#x5C3E;&#x8FDB;&#x884C;&#x64CD;&#x4F5C;&#xFF0C;&#x4F46;&#x662F;&#x6B64;"&#x961F;&#x5217;" &#x975E;&#x5F7C;&#x961F;&#x5217;&#x3002;

​ 如果要求每连续的k个数中的最大值,很明显,当一个数进入所要 “寻找” 最大值的范围中时,若这个数比其前面(先进队)的数要大,显然,前面的数会比这个数先出队且不再可能是最大值。

​ 也就是说——当满足以上条件时,可将前面的数 “踢出”,再将该数push进队尾。

​ 这就相当于维护了一个递减的队列,符合单调队列的定义,减少了重复的比较次数(前面的”劳动成果”能够为后面所用),不仅如此,由于维护出的队伍是查询范围内的且是递减的,队头必定是该查询区域内的最大值,因此输出时只需输出队头即可。显而易见的是,在这样的算法中,每个数只要进队与出队各一次,因此时间复杂度被降到了O(N)。

如果对于文字解释看不懂也没关系,结合模拟来介绍:假设物品个数为2,则窗口大小为3,进行模拟。在这个过程中,因为我们是从小到大进行更新,所以需要对dp的i-1状态备份一份到g中(空间换时间)。

首先给g[j]入队列尾,此时,单调队列中只有g[j],用队头g[j]更新dp[j]:

多重背包问题的单调队列优化

dp[j]更新之后变成i时候的状态,这里我们假定(g[j]+w > g[v+j])。

g[v+j]入队之前,先从队尾起,把统统不比它大的都踢出队列,然后再入队尾(g[j]+w比它大,踢不掉)。

取队头g[j]+w更新dp[v+j]:

多重背包问题的单调队列优化

dp[v+j]更新之后变成i时候的状态。

(情况一)如果(g[j]+2w > g[v+j]+w > g[2v+j] )。

g[2v+j]入队之前,先从队尾起比较,发现队尾比它大,踢不了,然后乖乖入队尾。

此时,取队头g[j]+2w更新dp[2v+j]:

多重背包问题的单调队列优化

(情况二)如果(g[j]+2w > g[2v+j] >= g[v+j]+w)。

g[2v+j]入队之前,发现队尾的g[v+j]+w不比它大,踢掉了,然后再比较此时的队尾g[j]+2w,比它大,乖乖入队尾。

此时,还是取队头g[j]+2w更新dp[2v+j]:

多重背包问题的单调队列优化

(情况三)如果(g[2v+j] >= g[j]+2w > g[v+j]+w)。

g[2v+j]入队之前,发现队尾的g[v+j]+w不比它大,踢掉了,然后再比较此时的队尾g[j]+2w,也不比它大,踢掉。此时队列为空,它进入队列。

此时,则取队头g[2v+j]更新dp[2v+j]:

多重背包问题的单调队列优化

假定我们是以上面三种中的第一种情况( g[j]+2w > g[v+j]+w > g[2v+j] )结束的:

多重背包问题的单调队列优化

dp[2v+j]更新之后变成i时候的状态。

g[2v+j]入队之前,检查单调队列内的元素是否都在窗口(长度为3)之内,发现g[j]+3w不在,则踢掉,然后……

多重背包问题的单调队列优化

至此,在本次问题中单调队列维护的规则和思路都已经演示清楚,下面直接上代码:

#include
#include
#include
using namespace std;
//多重背包问题: 限制每种物品可取次数
//究极优化:单调队列
const int M = 20010, N = 1010;
int n, m;
int dp[M], g[M];
int que[M]; //队列只存储在同余的集合中是第几个,不存储对应值
int main() {
    cin >> n >> m;
    for(int i = 0; i < n; i ++){
        int v, w, s;
        cin >> v >> w >> s;

        //复制一份副本g,因为这里会是从小到大,不能像0-1背包那样从大到小,所以必须申请副本存i-1状态的,不然会被影响
        memcpy(g, dp, sizeof dp);
        for(int r = 0; r < v; r ++) {   //因为只有与v同余的状态 相互之间才会影响,余0,1,...,v-1 分为v组
            int head = 0, tail = -1;
            for(int k = 0; r + k * v  s) head++;

                //这第k个准备进来,把不大于它的队尾统统踢掉,也是为了保持队列的单调降(判断式实际上是两边同时减去了k * w)
                //实际意义应该是 g[r + k * v] >= g[r + que[tail] * v] + (k - que[tail]) * w 为判断条件
                while(head = g[r + que[tail] * v] - que[tail] * w) tail --;

                que[++ tail] = k; //将第k个入列,队列只存储在同余中是第几个,不存储对应值

                //余r的这组的第k个取队头更新,队头永远是使之max的决策
                dp[r + k * v] = g[r + que[head] * v] + (k - que[head]) * w;
            }
        }
    }
    cout << dp[m] << endl;
    return 0;
}

时间复杂度:

多重背包问题的单调队列优化

以上内容如有错误的地方,恳请指正。

参考

https://oi-wiki.org/ds/monotonous-queue/

Original: https://www.cnblogs.com/BingweiHuang/p/15976681.html
Author: bwh
Title: 多重背包问题的单调队列优化

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/583662/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • linux多线程—使用mmap映射实现文件拷贝

    一、代码实现思路 1、示意图 2、示意图注解 循环创建i个线程,将src文件分为i段拷贝到dest文件中 (1)src文件的大小为src_size,前i-1个线程拷贝的文件大小为s…

    数据结构和算法 2023年6月8日
    0103
  • 低配版五子棋

    五子棋是一个比较简单的经典小游戏,使用QT制作五子棋的需要用到绘图事件处理函数 paintEvent(QPaintEvent *event)和鼠标事件处理函数 mousePress…

    数据结构和算法 2023年6月12日
    079
  • Redis的特性以及优势(附官网)

    NoSQL:一类新出现的数据库(not only sql) 泛指非关系型的数据库 不支持SQL语法 存储结构跟传统关系型数据库中的那种关系表完全不同,nosql中存储的数据都是KV…

    数据结构和算法 2023年6月7日
    075
  • 常用数据结构之数组

    1.背景 数组是最最基本的数据存储方式 数据结构从根本来看其实就2中数组和链表其他都是在这两种的基础上扩展出来的比如:队列-数组链表都能实现栈-数组链表都能实现哈希表-数组和队列实…

    数据结构和算法 2023年6月12日
    069
  • B树-插入

    B树系列文章 1. B树-介绍 2. B树-查找 3. B树-插入 4. B树-删除 插入 根据B树的以下两个特性 每一个结点最多有m个子结点 有k个子结点的非叶子结点拥有 k −…

    数据结构和算法 2023年6月8日
    082
  • 初识C++01:初探C++

    c++介绍 c++支持面向过程编程(如c),面向对象编程(OOP)和泛型编程; c/c++编译器比较多,window下是微软编译器cl.exe,Linux机下是GCC编译器,mac…

    数据结构和算法 2023年6月12日
    093
  • KMP算法详解

    -1.前置约定 如非特殊说明,以下文字中(T)代表主串,(P)代表模式串,(m)代表主串长度,(n)代表模式串长度 真前缀 一个字符串除了它本身之外的前缀。例如, moo 是 mo…

    数据结构和算法 2023年6月12日
    0102
  • java多线程之-不可变final

    package com.ldp.demo08final; import lombok.extern.slf4j.Slf4j; import java.text.ParseExcep…

    数据结构和算法 2023年6月12日
    081
  • 前端浅学之Vue

    Hello Vue {{message}} var app=new Vue({ //element,绑定id el:"#app", data:{ message…

    数据结构和算法 2023年6月7日
    086
  • 完全背包问题

    完全背包问题❓ 完全背包问题简介🐳 有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。 每件物品都有无限个(也就是可以放…

    数据结构和算法 2023年6月7日
    0111
  • 排序算法-快速排序

    快速排序 快速排序法介绍: 快速排序(Quicksort)是对冒泡排序的一种改进。基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有…

    数据结构和算法 2023年6月12日
    098
  • CF1637E Best Pair 题解

    心理阴影题。考试时写了奇怪的线段树分治,在每个线段树节点上维护单调栈在栈内二分…… 这题注意到两维分别是 (x,cnt_x),应该对 (cnt) 比较敏感的…

    数据结构和算法 2023年6月12日
    087
  • AcWing 第16场周赛

    按题意1.统计大小写字母个数2.按照要求转换为小写或大写,输出 #include<cstdio> #include<cstring> #include&lt…

    数据结构和算法 2023年6月7日
    076
  • PTA刷题笔记

    两周之内刷完GPLT L2和L3的题,持续更新,包括AK代码,坑点,和少量评论 PTA刷题记录 仓库地址: https://github.com/Haorical/Code/tre…

    数据结构和算法 2023年6月16日
    0108
  • 循环链表(约瑟夫环)思路及实现

    单链表的尾节点指向首节点,即可构成循环链表 约瑟夫问题:有 N 个人围成一圈,每个人都有一个编号,编号由入圈的顺序决定,第一个入圈的人编号为 1,最后一个为 N,从第 K (1 O…

    数据结构和算法 2023年6月12日
    081
  • 二叉树——判断二叉树是否是平衡二叉树

    判断二叉树是否为平衡二叉树 平衡二叉树 1.1. 问题 平衡二叉树的性质:要么是一棵空树,要么任何一个节点的左右子树的高度差的绝对值不超过1,。 给定一个二叉树的头结点head,判…

    数据结构和算法 2023年6月10日
    0102
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球