1. 程式人生 > >單調佇列優化DP動態規劃(1)

單調佇列優化DP動態規劃(1)

一.前言

學好了單調佇列不僅可以單獨使用,他還可以有更多的廣泛用途。這裡我主要講其對動態規劃的優化,較簡單。


二.例題1:單調佇列本身的靈活應用——測量溫度

在講用單調佇列優化DP前要先講一講單調佇列本身的靈活應用,所以引入一道題目——測量溫度。


1.題目

題目描述

某國進行了連續N(1<=N<=1000000天的溫度測量,測量存在誤差,測量結果是第i天溫度在[l_i,r_i]範圍內。其中-10^9<l_i<=r_i<=10^9
求最長的連續的一段,滿足該段內可能溫度不降

輸入

第一行一個整數n。

接下來n行,每一行兩個整數,表示l_i和r_i。

輸出

接下來輸出一個整數,表示答案。

輸入樣例

6
6 10
1 5
4 8
2 5
6 8
3 5

輸出樣例

4


2.思路

我先來說一說這道題的難點吧:但遇到一天的溫度在你前面連續幾天溫度不降的基礎上降低了,這時就只能在前面連續的一段中找一天的最低溫度不高於這一天的最高溫度,但是這樣的話你還要把這個序列處理完,確保是一個不降的序列,才能繼續往後走,自然就會超時。

既然難點已經丟擲來了,平常的思路又解決不了,就想一想其他思路吧。我們要滿足連續幾天的溫度不降,就要讓這幾天的最高溫都大於這幾天最高的最低溫度,於是乎,我們就可以用一個單調下降佇列來存每天的最低溫度,隊頭就是最高溫度。那麼每天的溫度一進去就要和對頭比較,拿這一天的最高溫度與對頭比較,如果大於等於的話說明加上這一天也可以組成一個溫度不降的序列,否則就要去掉對頭元素,與更低的溫度進行比較,直到可以組成一個不降序列。到了這裡還沒完,我們還沒有處理這一天的最低溫度呢!這次就是從隊尾最低的最低溫度開始,找到合適這一天的最低溫度的位置,然後入隊即可。在邊維護佇列時,可以邊找出最長的溫度連續不降的天數。


如此,單調佇列的隊頭和隊尾又完美的配合了一次,大將可能有點懵,請看一下思維圖:


這下思路清晰了吧,附程式碼:

#include <cstdio>
#define max(a, b) a > b ? a : b
#define M 1000005
int n, l[M], r[M], tail, head, ans, dl[M], len;//dl為單調下降佇列
int main (){
    scanf ("%d", &n);
    for (int i = 1; i <= n; i ++)
        scanf ("%d %d", &l[i], &r[i]);
    dl[++ tail] = ans = len = 1;
    head = tail;
    for (int i = 2; i <= n; i ++){//不用到n+1,因為len的計算方式是i - dl[head - 1]
        while (head <= tail && l[dl[head]] > r[i])//處理隊頭元素的最小溫度要小於i的最大溫度
            head ++;
        if(head > tail)
            len = 1;
        else
            len = i - dl[head - 1];
        while (head <= tail && l[i] > l[dl[tail]])//處理要達到的最小溫度
            tail --;
        ans = max (ans, len);
        dl[++ tail] = i;
    }
    printf ("%d", ans);
    return 0;
}

練習:

好,理解了這道題,就來看重頭戲,單調佇列優化DP:

三.例題2:單調佇列優化DP:猴子


1.題目

題目描述

有Q只猴子要從第一棵樹到第n棵樹去,第i只猴子一次跳躍的最遠距離為Ki。如果它在第x棵樹,那它最遠可以跳到第x+Ki棵樹。如果第j棵樹的高度比第i棵樹高或相等,那麼它從第i棵樹直接跳到第j棵樹,它的勞累值會增加1。所有猴子一開始在第一棵樹,請問每隻猴子要跳到第n棵樹花費的勞累值最小。

輸入

第一行一個整數n,表示有n棵樹。(2<=n<=1000000)

接下來第二行給出n個正整數D1,D2,……,Dn(1<=Di<=10^9),其中Di表示第i棵樹的高度。

第三行給出了一個整數Q(1<=Q<=25),接下來Q行,給出了每隻猴子一次跳躍的最遠距離Ki(1<=Ki<=N-1)。

輸出

輸出Q行,每行一個整數,表示一隻猴子的最小的勞累值。

輸入樣例 

9

4 6 3 6 3 7 2 6 5

5

 輸出樣例

2

1


2.思路 

這道題剛拿到,是不是不知道如何用單調佇列?那就對了。我既然都說了單調佇列優化DP,那麼這道題的主體思路肯定是DP動態規劃呀。現在就讓我們來研究一下狀態轉移方程。

1.狀態轉移方程

只需定義一個一維的DP陣列:dp[i]。它表示猴子跳到第i棵樹需要花費的勞累值。那麼,樸素的DP演算法就是兩重迴圈,第一重迴圈從1到n列舉猴子跳到每一棵樹的最小勞累值,內層迴圈列舉在猴子的跳躍範圍內,從哪棵樹跳過來最划算。於是狀態轉移方程是不難想到:dp[i] = min (dp[i], dp[j] + (d[i] >= d[j]))(O_{n^{2}}).(d[i] >= d[j])是指當符合要求時返回1,否則返回0。

2.單調隊列出場

但以上的DP樸素演算法太耗時間了,我們不妨把dp陣列放入單調上升佇列中,每次取隊頭元素就是最優的那一個dp[j]。但是還有很多細節我們沒有處理,共有以下幾點:

①確保隊頭元素與i這棵樹的距離不超過猴子能跳的最大距離。這個問題只需每次迴圈一開始就判斷隊頭元素與i這棵樹的距離,如果大於了猴子能跳的最遠距離,就讓對頭元素出隊。

②難道隊頭元素就是最優解嗎?答案:是。可能有一種十分玄妙的情況:如果有一個元素的值與對頭元素相同,且如果從那一個元素的那棵樹跳到i不需要花費,而從隊頭元素跳到i需要花費,這就可以說明隊頭元素不一定是最優解呀?對於這種情況,我敢擔保,一定不會出現,為何?請看下一步:

③處理隊尾元素。第一種情況大家都想得到,就是當dp[i]小於隊尾元素時,要讓隊尾元素出隊;還有一種情況,就是迴應剛才第二步的問題:當dp[i]等於隊尾元素時,取高度最高的那一棵樹,這樣就能避免第二步中的質疑,保證隊頭元素不僅最小,還是最小的之中樹高度最高的。

好了,我們就完成了一次簡單的單調佇列優化DP動態規劃。


思維導圖如下:


附程式碼:

#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
#define M 1000005
#define reg register
int n, d[M], Q, k, head, tail, a[M], dp[M];
inline void read (int &x){
    int f = 1; x = 0; char c = getchar();
    while (c < '0' || c > '9') {if (c == '-') f = -1; c = getchar();}
    while (c >= '0' && c <= '9') {x = x * 10 + c - 48; c = getchar();}
    x *= f;
}
int main (){
    read (n);
    for (reg int i = 1; i <= n; i ++)
        read (d[i]);
    read (Q);
    while (Q --){
        read (k);
        head = tail = 1;//預處理
        dp[1] = 0;
        a[tail] = 1;
        for (reg int i = 2; i <= n; i ++){
            while (i - k > a[head] && tail >= head)//處理隊頭
                head ++;
            dp[i] = dp[a[head]] + (d[i] >= d[a[head]]);//計算dp[i]
            while (tail >= head){//處理隊尾元素
                if (dp[a[tail]] > dp[i] || (dp[a[tail]] == dp[i] && d[a[tail]] < d[i]))//兩種情況
                    tail --;
                else
                    break;
            }
            a[++ tail] = i;
        }
        printf ("%d\n", dp[n]);
    }
    return 0;
}

四.總結

至此,我們已經掌握了簡單的單調佇列優化DP,知道了單調佇列優化DP的基本思路,一定是先想出DP遞推公式,再加以單調佇列的優化。其實本章的第一題也是需要掌握的,因為要會靈活地運用單調佇列,才能去優化DP動態規劃。下期會難度加深喲!敬請期待。