單調隊列、優先隊列


“如果一個人比你年輕還比你強,那你就要被踢出去了……”——單調隊列

“來來來,神犇巨佬、金牌\(Au\)爺、\(AKer\)站在最上面,蒟蒻都靠下站!!!”——優先隊列

Part 1:單調隊列

單調隊列的功能

顧名思義,所謂單調隊列,那么其中的元素從隊頭到隊尾一定要具有單調性(單調升、單調降等)

它被廣泛地用於“滑動窗口”這一類\(RMQ\)問題,其功能是\(O(n)\)維護整個序列中長度為\(k\)的區間最大值或最小值

單調隊列實現原理

滑動窗口問題

給定一個長度為\(n\)的序列\(a\)和一個窗口長度\(k\),窗口初始覆蓋了\(1\rightarrow k\)這些元素

之后窗口每次向右移一個單位,即從覆蓋\(1\rightarrow k\)變成覆蓋\(2\rightarrow k+1\)

要求求出每次移動(包括初始時)窗口所覆蓋的元素中的最大值(如圖,花括號內即為被窗口覆蓋的元素)

數據范圍:\(1\leq k\leq n\leq 10^6,a_i\in[-2^{31},2^{31})\)

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

“越接近暴力的數據結構,能維護的東西就越多”——真理

線段樹和樹狀數組維護不了眾數,但分塊可以。你再看暴力,它什么都能維護……

很簡單,每次從窗口最左端掃到最右端,然后取最大值就\(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\)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM