天天看點

單調隊列優化DP詳解 例題 Tyvj1305 分析

單調隊列,即單調的隊列。有時用于優化1D/1D方程。

例題 Tyvj1305

時間: 1000ms / 空間: 131072KiB / Java類名: Main

描述

輸入一個長度為n的整數序列,從中找出一段不超過M的連續子序列,使得整個序列的和最大。 

例如

1,-3,5,1,-2,3
當m=4時,S=5+1-2+3=7
當m=2或m=3時,S=5+1=6             
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

輸入格式

  • 第一行兩個數n,m
  • 第二行有n個數,要求在n個數找到最大子序和

輸出格式

  • 一個數,數出他們的最大子序和

測試樣例

輸入
6 4 
1 -3 5 1 -2 3           
  • 1
  • 2
  • 1
  • 2
輸出
7           
  • 1
  • 1
備注

資料範圍: 

100%滿足n,m<=300000

分析

這是一個典型的動态規劃題目,不難得出一個1D/1D方程:

f(i) = sum[i]-min{sum[k]|i-M≤k≤i}            
  • 1
  • 1

如果不明白這個方程的來曆,戳這裡

由于方程是1D/1D的,是以我們不想隻得出簡單的Θ(n^2)算法。不難發現,此優化的難點是計算

min{sum[i-M]..sum[i-1]}

。在上面的連結中,我們成功的用Θ(nlgn)的算法解決了這個問題。但如果資料範圍進一步擴大,運用st表解決就力不從心了。是以我們需要一種更高效的方法,即可以在Θ(n)的攤還時間内解決問題的單調隊列。 

單調隊列(Monotone queue)是一種特殊的優先隊列,提供了兩個操作:插入,查詢最小值(最大值)。它的特殊之處在于它插入的不是值,而是一個指針(key)(wiki原文:imposes the restriction that a key (item) may only be inserted if its priority is greater than that of the last key extracted from the queue)。所謂單調,指當一組資料的指針1..n(優先級為A1..An)插入單調隊列Q時,隊列中的指針是單調遞增的,隊列中指針的優先級也是單調的。因為這裡要維護優先級的最小值,那麼隊列是單調減的,也說隊列是單調減的。

查詢最小值

由于優先級是單調減的,是以最小值一定是隊尾元素。直接取隊尾即可。

插入操作

當一個資料指針i(優先級為Ai)插入單調隊列Q時,方法如下:

  1. 如果隊列已空或隊頭的優先級比Ai大,删除隊頭元素。
  2. 否則将i插入隊頭

比如說,一個優先隊列已經有優先級分别為 

{5,3,-2}

 的三個元素,插入一個新元素,優先級為2,操作如下:

  1. 因為2 < 5,删除隊頭,

    {3,-2}

  2. 因為2 < 3,删除隊頭,

    {-2}

  3. 因為2 > -2,插入隊頭,

    {2,-2}

證明性質可以得到維護

證明指針的單調減 :由于插入指針i一定比已經在隊列中所有元素大,是以指針是單調減的。 

證明優先級的單調減:由于每次将優先級比Ai大的删除,隻要原隊列優先級是單調的,新隊列一定是單調的。用循環不變式易證正确性。 

為什麼删除隊頭:直覺的,指針比i小(靠左)而優先級比Ai大的資料沒有希望成為任何一個需要的子序列中的最小值。這一點是我們使用優先隊列的根本原因。

維護區間大小

當一串資料A1..Ak插入時,得到的最小值是A1..Ak的最小值。反觀dp方程:

f(i) = sum[i]-min{sum[k]|i-M≤k≤i}            
  • 1
  • 1

在這裡,A = sum。對于f(i),我們需要的其實是Ai-M .. Ai的最小值,而不是所有已插入資料的最小值(A1..Ai-1)。是以必須維護區間大小,使隊列中的元素嚴格處于Ai-M..Ai-1這一區間,或者說删去哪些A中過于靠前而違反題目條件的值。由于隊列中指針是單調的,也就是靠左的指針大于靠右的,或者說在優先隊列中靠左的值,在A中一定靠後;優先隊列中靠右的值,在A中一定靠前。我們想要删除過于靠前的,隻需要在優先隊列中從右一直删除,直到最右邊(隊尾)的值符合條件。具體地:當隊頭指針p滿足i-m≤p時。 

形象地說,就是忍痛割愛删去哪些較好但是不符合題目限制的資料。

解決問題

這裡用std::list表示隊列,直接按照上面的方法查詢最小值,然後根據方程,

f(k) = s[i] - s[queue.back()]

。直接給出代碼:

#include <iostream>
#include <list>
#include <cstdio>
using namespace std;

int n, m;
long long s[];
// 字首和

list<int> queue;
// 連結清單做單調隊列

int main() {
    cin >> n >> m;
    s[] = ;
    for (int i=; i<=n; i++) {
        cin >> s[i];
        s[i] += s[i-];
    }
    long long maxx = ;
    for (int i=; i<=n; i++) {
        while (!queue.empty() and s[queue.front()] > s[i])
            queue.pop_front();
        // 保持單調性
        queue.push_front(i);
        // 插入目前資料
        while (!queue.empty() and i-m > queue.back())
            queue.pop_back();
        // 維護區間大小,使i-m >= queue.back()
        if (i > )
            maxx = max(maxx, s[i] - s[queue.back()]);
        else
            maxx = max(maxx, s[i]);
        // 更新最值
    }
    cout << maxx << endl;
    return ;
}           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

分析時間複雜度

運用聚合分析,由于一個元素最多進隊一次,出隊一次,while循環總共最多運作Θ(2n)=Θ(n)次,是以算法的攤還效率是Θ(n)。通過單調隊列,實作了線上性複雜度内解決形如:

f[x] = max or min{g(k) | b[x] <= k < x} + w[x]
其中b[x]随x單調不降,即b[]<=b[]<=b[]<=...<=b[n]
g[k]表示一個和k或f[k]有關的函數,w[x]表示一個和x有關的函數           
  • 1
  • 2
  • 3
單調隊列優化DP詳解 例題 Tyvj1305 分析
  • 1
  • 2
  • 3

的動态規劃問題。

參考資料:百度百科,wiki 

感謝這兩位優美的一問一答: 

http://zhidao.baidu.com/link?url=uQuBcPkzFeA_xoxxzKwNCXbdlmihh4ema-RUQwlcdhZ7oDzR9awb2Ec4tjudlYzyyOOpYlaQTGYntLVDDwe5-q