【題解】滑動窗口


為了解決滑動窗口,我們引入單調隊列的概念。

分析題目的要求,我們需要建立一種數據結構,可以滿足以下要求:

  • 可以快速讀取一個區間的最大值和最小值

  • 能根據編號的大小將元素快速彈出

先分析最大值。對於上述要求,我們可以用一個單調隊列來解決這個問題。

我們不妨先看一組測試數據。

8 3
1 3 -1 -3 5 3 6 7

滑動窗口的運動軌跡如下:

1] 3 -1 -3 5 3 6 7

1 3 ] -1 -3 5 3 6 7

[1 3 -1] -3 5 3 6 7 此時滑動窗口已經完全進入了數列

1 [3 -1 -3] 5 3 6 7

1 3 [-1 -3 5] 3 6 7

1 3 -1 [-3 5 3] 6 7

1 3 -1 -3 [5 3 6] 7

1 3 -1 -3 5 [3 6 7] 滑動窗口已經滑到了最右邊

我們可以用一個單調遞減隊列來解決這個問題——我們可以在隊首取到最大值。

我們用一個變量 \(i\) 來模擬窗口的右側。任意一個時刻內,寬度為\(m\)的窗口,可以表示成一個運動的區間\([i-m+1,i]\)。我們讓\(i\)從1到n循環枚舉,每一次,我們都對掃描到的元素進行判斷,看其能否進入隊列。注意,我們使用的是一個單調隊列,隊列里面的元素是單調遞減的,這樣我們就可以在對頭取到最大值。如果當前元素比隊尾的元素還要大,根據單調隊列的定義,若把當前元素加入到隊列中,那么原來隊尾的元素就會處於一個低谷狀態:它是不可能成為最大值的。原因很簡單:隊列里面的所有元素都會往隊首跑,這個“低谷狀態”的隊列元素最終會到達隊首,而它的前一號元素會比它大。這不符合我們“在隊首取得最大值”的要求。這個“低谷元素”就沒有存在的必要了。

    while(head<=tail && q[tail]<=a[i])
                    --tail;
                q[++tail]=a[i];
                p[tail]=i;//p表示隊列對應元素的編號

我們發現,這里單調隊列的使用有一點點像“棧”。如果僅僅只是像這樣子掃描,然后入隊,我們還不如建立一個單調棧呢?

其實不然。單調隊列有一個特點,就是既可以從隊首出隊,又可以從隊尾出隊。我們除了考慮快速取得最值,還要考慮一點:由於是滑動窗口,有些窗口內的元素最終會運動到窗口外。所以,我們還要考慮隊列里面的元素是否“過時”。

由於我們是按照時間順序將元素存入隊列內,因此過時的元素更有可能出現在隊首,因為隊尾都是新鮮的元素。由於窗口的右邊界是\(i\),我們只要判斷隊列元素的編號和窗口左邊界的關系\(i-m+1\)就可以了。如果當前元素的編號\(rank<i-m+1\),即\(rank<=i-m\),我們就把它從隊首彈出。

while(p[head]<=i-m)
                    ++head;

綜上所述,我們可以用單調隊列解決這個問題。分析最小值同理。

#include<bits/stdc++.h>
#define For(i,a,b) for(register int i=a;i<=b;i++)
using namespace std;

struct Mq{
    static const int nmax=1000001;
    int n,k,a[nmax];
    int q[nmax],head,tail,p[nmax];

    void read()
        {
            scanf("%d %d",&n,&k);
            for(register int i=1;i<=n;++i)
                scanf("%d",&a[i]);
        }
    void Mmax()
        {
            head=1;
            tail=0;
            for(register int i=1;i<=n;++i)
            {
                while(head<=tail && q[tail]<=a[i])
                    --tail;
                q[++tail]=a[i];
                p[tail]=i;
                while(p[head]<=i-k)
                    ++head;
                if(i>=k)printf("%d ",q[head]);
            }
            printf("\n");
        }
    void Mmin()
        {
            head=1,tail=0;
            for(register int i=1;i<=n;++i)
            {
                while(head<=tail && q[tail]>=a[i])
                    --tail;
                q[++tail]=a[i];
                p[tail]=i;
                while(p[head]<=i-k)
                    ++head;
                if(i>=k)
                    printf("%d ",q[head]);
            }
            printf("\n");
        }
}monotone_queue;

int main()
{
    monotone_queue.read();
    monotone_queue.Mmin();
    monotone_queue.Mmax();
    return 0;
}

總結一下單調隊列的三部曲:

  • 判單調
  • 判過期
  • 更答案

注意一下,這三個步驟的具體順序還是要看題目的要求。注意在掃描的過程中,只有當當前新決策的收益或代價可以確定時,才能判定單調,且必須將其插入隊列。
建議根據上面的要求,在以下兩種順序中選一個:

判單調\(\rightarrow\)判過期\(\rightarrow\)更答案

判過期\(\rightarrow\)更答案\(\rightarrow\)判單調


免責聲明!

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



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