單調隊列、優先隊列

「若是一我的比你年輕還比你強,那你就要被踢出去了……」——單調隊列算法

「來來來,神犇巨佬、金牌\(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\)

相關文章
相關標籤/搜索