「若是一我的比你年輕還比你強,那你就要被踢出去了……」——單調隊列算法
「來來來,神犇巨佬、金牌\(Au\)爺、\(AKer\)站在最上面,蒟蒻都靠下站!!!」——優先隊列數組
顧名思義,所謂單調隊列,那麼其中的元素從隊頭到隊尾必定要具備單調性(單調升、單調降等)數據結構
它被普遍地用於「滑動窗口」這一類\(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})\)
隊列
「越接近暴力的數據結構,能維護的東西就越多」——真理it
線段樹和樹狀數組維護不了衆數,但分塊能夠。你再看暴力,它什麼都能維護……io
很簡單,每次從窗口最左端掃到最右端,而後取最大值就\(OK\)了
顯然在這種數據強度下暴力是過不了的,代碼就不給了
思考暴力爲何慢了:由於窗口每次才移動\(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;//統計答案 } }
一個悲傷的故事背景:
從前,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\),因此不會找到錯誤的最值