【暖*墟】 #队列# 单调队列:子段最大值 + 最大子序和

STL 中队列的使用(queue)

一.基本操作:

push(x) 将x压入队列的末端  //不能push_back

pop() 弹出队列的第一个元素(队顶元素),注意此函数并不返回任何值

front() 返回第一个元素(队顶元素)

back() 返回最后被压入的元素(队尾元素)

empty() 当队列为空时,返回true

size() 返回队列的长度

头文件:#include <queue>

声明方法: 1、普通声明 queue<int>q;

2、结构体 struct node{ int x, y; };  queue<node>q;

二.STL 中优先队列的使用方法(priority_queue)

优先队列容器与队列一样,只能从队尾插入元素,从队首删除元素

但是它有一个特性,就是队列中最大的元素总是位于队首,

所以出队时,并非按照先进先出的原则进行,而是将当前队列中最大的元素出队。

这点类似于给队列里的元素进行了由大互小的顺序排序。

元素的比较规则默认按元素值由大到小排序,可以重载“<”操作符来重新定义比较规则。  

基本操作:empty() 如果队列为空返回真

pop() 删除对顶元素  push() 加入一个元素

top() 返回优先队列对顶元素

在默认的优先队列中,优先级高的先出队。

在默认的int型中先出队的为较大的数。

头文件:#include <queue>

1.普通方法:priority_queue<int>q;  //通过操作,按照元素从大到小的顺序出队

priority_queue<int,vector<int>,greater<int> >q; 

//通过操作,按照元素从小到大的顺序出队

2、自定义优先级: priority_queue<int, vector<int>, cmp>q; //定义方法

//其中,第二个参数为容器类型。第三个参数为比较函数。

 

单调队列

即所有在队列里的数都必须按递增(或递减)的顺序列队的队列。

用法:在决策队列中,及时排除一定不是最优解的选择。

【例题1】(来源:caioj 1172)[ 子段最大值---单调递减队列 ]

给定一个n个数的数列,从左至右输出每个长度为m的数列段内的最大数。

解法1*** 如果按照常规方法,我们在求f[i]即i~i+m-1区间内的最值时,

要把区间内的所有数都访问一遍,时间复杂度约为O(nm)。

解法2*** 上个算法找当前的f(i)的时候,i的前面k-1个数其它在算f(i-1)的时候我们就比较过了。

保存上一次的结果?当然主要是保存i的前k-1个数中的最大值了。要用到单调递减队列。

使用单调队列就涉及到去头和删尾***

1、队列的头一定是在一段时间前就加入了队列,现在的队列头会不会离开了我们处理的区间呢?

如果它离我们正在处理的i太远了,我们就要把它去掉,去除冗杂的信息。

2、为了保证队列的递减性,在从列队尾新插入元素v时,要考虑队列尾的值是否大于v,

如果是,队列呈现 队列尾-1的值 > 队列尾的值 > v ,此时队列递减性没有消失;

如果不是,队列呈现 队列尾-1的值 > 队列尾的值 < v ,队列递减性被打破。

为了维护递减性,我们做如下考虑:v是最新值,它的位置是目前最靠后的,

它可成为以后的最大值,必须留下;队列尾-1的值与v大小不定,不能冒然删去它;

队列尾的值夹在v和队列尾-1之间,它不但不是最大值,对于以后的情况又不如v优

因为v相比队列尾更靠后(v可以影响到后m个值,队列尾只能影响到从它的位置往后数m-1个值),

而且值更大,所以删队列尾是必定的。

の 在维护好一个 区间正确、严格递减 的单调递减队列后,队列头就是当前区间的最大值了。

#include <cmath>
#include <iostream>
#include <cstdio>
#include <string>
#include <cstring>
#include <vector>
#include <algorithm>
#include <stack>
#include <queue>
using namespace std;
typedef long long ll;

/*【caioj 1172】单调队列找max
给定一个n个数的数列,从左至右输出每个长度为m的数列段内的最大数。 */

//队列中存之后有可能成为最大值的数,每次入队时,清除不可能的队头和队尾。

struct node{
    int x,p; //价值 和 位置
}list[201000];

int a[201000];

int main(){
    int n,m; scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++) scanf("%d",&a[i]);

    int head=1,tail=1; //手写优先队列
    list[1].x=a[1]; list[1].p=1; //初始点为1
    for(int i=2;i<=n;i++){
        while(head<=tail && list[tail].x<=a[i]) tail--; 
        //↑↑新数比前几个大,前几个不可能再成为最大值(可能不止一个)
        tail++; list[tail].x=a[i]; list[tail].p=i; //a[i]加入队尾
        while(list[head].p<i-m+1) head++; //p的作用:判断区间长度
        if(i>=m) printf("%d\n",list[head].x); //每一次的队头都是当前段最大值
        //单调递减队列:如果后方有数更大,前面就删除;所以队列一定单调递减
    }
    return 0;
}

【例题2】caioj 1173

#include <cmath>
#include <iostream>
#include <cstdio>
#include <string>
#include <cstring>
#include <vector>
#include <algorithm>
#include <stack>
#include <queue>
using namespace std;
typedef long long ll;

/*【caioj 1173】单调队列 [减少继承的状态]
有N个数(N<=100000),在连续M(M<=N)个数里至少要有一个数被选择。 
求选出来数的最小总和。  */

//队列中存之后有可能成为最大值的数,每次入队时,清除不可能的队头和队尾。

struct node{
    int x,p; //价值 和 位置
}list[201000];

int a[201000],f[201000]; //f[i]选择i,且保证1~i符合题意的最小价值

int main(){
    int n,m; scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++) scanf("%d",&a[i]);

    int head=1,tail=1;
    list[1].x=0; list[1].p=0; //继承0位置
    for(int i=1;i<=n;i++){
        while(head<=tail && i-list[head].p>m) head++; //淘汰超出范围的队头
        f[i]=list[head].x+a[i]; //选择遇到的第一个不能删的队头,可以管理的后面的数最多
        while(head<=tail && list[tail].x>f[i]) tail--; //list中是升序序列
        tail++; list[tail].x=f[i]; list[tail].p=i;
    }
    int ans=(1<<30);
    for(int i=n-m+1;i<=n;i++) //最后一段,任选f[i]都能保证满足
        if(f[i]<ans) ans=f[i];
    printf("%d",ans);
    return 0;
}

【例题3】最大子序和  tyvj - 1305

#include <cmath>
#include <iostream>
#include <cstdio>
#include <string>
#include <cstring>
#include <vector>
#include <algorithm>
#include <stack>
#include <queue>
using namespace std;
typedef long long ll;

/*【最大子序和】单调队列  
输入一个长度为n的整数序列,从中找出一段不超过M的连续子序列,使得整个序列的和最大。 */

//先求出前缀和s[i],枚举右端点i,寻找最小的s[j]对应的左端点
//最优策略集合一定是“下标位置递增,对应前缀和s也递增”的序列
//1.判断队头决策和i的距离是否超过m的范围;
//2.此时队头是右端点为i,左端点为j的最优选择;
//3.不断删除队尾决策,直到队尾对应的s值小于s[i],使i入队。
//由此,队列变成了 关于前缀和的 单调递增序列↑↑↑↑↑

/*【分析】维护一个{ 下标递增且前缀和递增 }的队列。
每一次入队一个i,就删除在i前面所有不满足“单调性”的元素,
即当i入队后,这些元素一定不如i,在i的存在下这些元素一定不会被选入最优答案集合中,
也就是类似sj<=si且j<i的元素。删除的时候直接让队尾出队即可。 */

const int MAXN = 300000 + 10;
int q[MAXN],a[MAXN],s[MAXN];

int main() {
    int n,m,ans; scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++) {
        scanf("%d",a[i]);
        s[i]=a[i]+s[i-1]; //前缀和
    }
    int l=1,r=1;
    for(int i=1;i<=n;i++) { //先枚举来固定右端点,问题转化为枚举左端点
        while(l<=r&&q[l]<i-m) l++; //判断队头决策和i的距离是否超过m的范围
        ans=max(ans,s[i]-s[q[l]]); //此时队头q[l]是右端点为i的最优左端点选择
        while(l<=r&&s[q[r]]>=s[i]) r--; //核心,及时弹出[对答案贡献一定不如i]的元素
        q[++r]=i;
    }
    printf("%d", ans);
    return 0;
}

整理归纳单调队列的定义:

1、维护区间最值;
2、去除冗杂状态;
3、保持队列单调(最大值是单调递减序列,最小值是单调递增序列);
4、最优选择在队首。

整理归纳单调队列的使用方法:

1、维护队首(对于上题就是如果你已经是当前的m个之前那你就可以被删了) ;
2、在队尾插入(每插入一个就要从队尾开始往前去除冗杂状态) ;
3、取出需要的最优解(队列头的值即是);
4、借助最优解,得到目前所求的最优解(通常此处插入DP方程)。

单调队列的原理:

在处理f[i]时,去除冗杂、多余的状态,使得每个状态在队列中只会出现一次;

同时维护一个能瞬间得出最优解的队列,减少重新访问的时间;

在取得自己所需的值后,为后续的求解做好准备。

猜你喜欢

转载自blog.csdn.net/flora715/article/details/81093632