单调队列、优先队列

“若是一我的比你年轻还比你强,那你就要被踢出去了……”——单调队列算法

“来来来,神犇巨佬、金牌\(Au\)爷、\(AKer\)站在最上面,蒟蒻都靠下站!!!”——优先队列数组

Part 1:单调队列

单调队列的功能

顾名思义,所谓单调队列,那么其中的元素从队头到队尾必定要具备单调性(单调升、单调降等)数据结构

它被普遍地用于“滑动窗口”这一类\(RMQ\)问题,其功能是\(O(n)\)维护整个序列中长度为\(k\)的区间最大值或最小值spa

单调队列实现原理

滑动窗口问题

给定一个长度为\(n\)的序列\(a\)和一个窗口长度\(k\),窗口初始覆盖了\(1\rightarrow k\)这些元素code

以后窗口每次向右移一个单位,即从覆盖\(1\rightarrow k\)变成覆盖\(2\rightarrow k+1\)blog

要求求出每次移动(包括初始时)窗口所覆盖的元素中的最大值(如图,花括号内即为被窗口覆盖的元素)继承

数据范围:\(1\leq k\leq n\leq 10^6,a_i\in[-2^{31},2^{31})\)
队列

\(Solution\) \(1:\)暴力碾标算\(O(nk)\)

“越接近暴力的数据结构,能维护的东西就越多”——真理it

线段树和树状数组维护不了众数,但分块能够。你再看暴力,它什么都能维护……io

很简单,每次从窗口最左端扫到最右端,而后取最大值就\(OK\)

显然在这种数据强度下暴力是过不了的,代码就不给了

\(Solution\) \(2:\)单调队列\(O(n)\)

思考暴力为何慢了:由于窗口每次才移动\(1\)个单位,可是暴力算法每次都重复统计了\(k-2\)个元素

那咱们把中间那一大堆数的最大值记录下来,每次进来一个元素,出去一个元素,统计一下最值,这不就快了吗?

可是,不幸的是,若是出去的那个元素正好是最值,那就得从新统计了

考虑维护一个单调不升队列,每次新元素进来以前,从这个队列的最小值向最大值依次比较

若是这个队列中的一个数\(a\)没有新来的那个元素\(b\)大,那么把\(a\)踢出序列

由于\(a\)必定在新来的数以前出现,它的值没有\(b\)大,因此在以后的统计中\(a\)永远也不可能成为最大值,就不必记录\(a\)

处理完新元素,如今看看旧元素怎么处理:

一个数\(a\)若是不在窗口里,那么须要把它踢出这个队列,可是若是咱们每次移动都要找到这个\(a\)再踢出,那么复杂度又变成了\(O(nk)\),显然不行

发现新元素不受旧元素的影响,每次必定会进入到队列里,不会由于旧元素而把新元素卡掉,并且咱们只是查询最大值,因此没有必要严格维护序列里每一个值都在窗口里,只要保证最大值出自窗口里便可

由于这个队列单调不升,因此队头必定是咱们要查询的最大值,那么咱们能够对队头扫描,若是这个队头在窗口以外,把这个队头踢出去,新的队头是原来的第二个元素

重复上述操做,直到队头在窗口里便可,由于序列单调不升,因此队头必定是窗口内的最大值

以上就是单调队列算法的所有内容

复杂度分析

有些刚学的同窗,看到循环\(n\)重嵌套,立刻来一句:这个算法的复杂度是\(O(n^n)\) 的,这是不对的!!!

好比刚才咱们的这个算法,看似每次窗口移动时都要对整个单调队列进行扫描,可是,从整体来看,每一个元素只会入队一次,出队一次,因此复杂度是\(O(n)\)

核心\(Code\)

struct Node{
      int num,und;//num是值,und是下标
      Node(){}
}q[1e6+10];
int main(){
      int i,head=1,tail=0;//创建单调队列维护k个数中最大值,head是队头,tail是队尾
	for(i=1;i<k;i++){//先把k个元素都进来
		while(head<=tial&&q[tail].num<a[i]) tail--;//若是队尾没有新元素大,那么在以后的统计中,它永远不可能成为最大值,踢出
		q[++tail].und=i,q[tail].num=a[i];//新元素插入队尾
	}
	for(;i<=n;i++){
		while(head<=tail&&q[tial].num<a[i]) tail--;
		q[++tail].und=i,q[tail].num=a[i];
		while(q[head].und<i-k+1) head++;//队头过期了,踢出
		ans[i]=q[head].num;//统计答案
	}
}

Part 2:优先队列

一个悲伤的故事背景:

从前,NOI系列比赛禁止使用\(C++STL\)时,优先队列是每个\(OI\)选手必定会熟练手写的数据结构。

可是自从\(STL\)盛行,会手写优先队列的选手愈来愈少了……传统手艺没有人继承,真是世风日下(STL真香)啊……

优先队列的功能

优先队列有另外一个名字:二叉堆

功能是维护一堆数的最大值(大根堆)/最小值(小根堆),存放在堆顶(也就是根)

注意:凡是\(STL\)都自带常数

优先队列实现原理

没错,实现原理就是\(C++STL\)

\(C++STL\)\(#include<queue>\)头文件为咱们提供了一个免费的优先队列——\(priority\)_\(queue\),可是不支持随机删除,只支持删除堆顶

优先队列的声明和操做方法

声明方法

std::priority_queue<int>Q;

上面就声明了一个\(int\)类型的大根堆,想要一个小根堆?不要紧,你能够这么写:

std::priority_queue< int,std::vector<int>,std::greater<int> >Q;

或者把每一个数入堆时都取相反数,而后在用的时候再取相反数

对于结构体,咱们还有更骚的操做:重载小于号运算符

struct Node{
      int x,y;
      Node(){}
}
bool operator < (const Node a,const Node b){ return a.x<b.x; }
std::priority_queue<Node>Q;

这样就是按照\(x\)大小比较的大根堆,若是你想要小根堆,那么把重载运算符改为这句:

bool operator < (const Node a,const Node b){ return a.x>b.x; }

这样,系统就会认为小的更大,因此小的就会跑到堆顶去

可是,如你想要\(int\)类型的小根堆,千万不要重载运算符,这样普通的两个\(int\)数就不能正常比较了(系统会认为小的更大)

经常使用操做命令

//std priority_queue 操做命令
Q.push();//插入元素,复杂度O(logn)
Q.pop();//弹出堆顶,复杂度O(logn)
Q.size();//返回堆中元素个数,复杂度O(1)
Q.top();//返回堆顶元素,复杂度O(1)

奇技淫巧

什么?你想让\(priority\)_\(queue\)支持随机删除,可是又不想手写?(那你可真是懒

可是这能难倒人类智慧吗?显然不能,这里有一个玄学的延迟删除法,能够知足需求

咱们能够维护另外一个优先队列(删除堆),每次要删除一个数(假设为\(x\)

当须要删除\(x\)的时候,咱们并不去真正的堆里面删除\(x\),而是把\(x\)加入删除堆

访问维护最值的堆时,看看堆顶是否是和删除堆堆顶同样,若是同样,说明这个数已经被删掉了,在原堆和删除堆中同时\(pop\)

这个方法为何对呢?万一原堆的堆顶\(x\)已经被删了,而删除堆的堆顶不是\(x\),致使找到了错的最值,怎么办呢?

其实这种状况不可能出现。假设咱们维护了一个大根堆,若是删除堆的堆顶不是\(x\),那必然是一个比\(x\)大的数\(y\)

若是\(y\)尚未被删除,那么比\(y\)小的\(x\)必定还不是堆顶,几回弹出后,堆顶是\(y\),发现删除堆堆顶一样是\(y\)\(y\)从原堆和删除堆中删除

换句话说,当原堆的堆顶是\(x\)时,删除堆堆顶和原堆中还须要删除的数必定\(\leq x\),因此不会找到错误的最值

感谢您的阅读,给个三连球球辣!\(OvO\)