1. 程式人生 > >單調佇列優化dp詳解

單調佇列優化dp詳解

要想了解單調佇列,我們得要先看一道題來明白.我們可以先看看這道叫做“我要長高”的題目.

Description
韓父有N個兒子,分別是韓一,韓二…韓N。由於韓家演技功底深厚,加上他們間的密切配合,演出獲得了巨大成功,票房甚至高達2000萬。舟子是名很有威望的公知,可是他表面上兩袖清風實則內心陰暗,看到韓家紅紅火火,嫉妒心遂起,便發微薄調侃韓二們站成一列時身高參差不齊。由於舟子的影響力,隨口一句便會造成韓家的巨大損失,具體虧損是這樣計算的,韓一,韓二…韓N站成一排,損失即為C*(韓i與韓i+1的高度差(1<=i< N))之和,搞不好連女兒都賠了.韓父苦苦思索,決定給韓子們內增高(注意韓子們變矮是不科學的只能增高或什麼也不做),增高1cm是很容易的,可是增高10cm花費就很大了,對任意韓i,增高Hcm的花費是H^2.請你幫助韓父讓韓家損失最小。

Input
有若干組資料,一直處理到檔案結束。 每組資料第一行為兩個整數:韓子數量N(1<=N<=50000)和舟子係數C(1<=C<=100) 接下來N行分別是韓i的高度(1<=hi<=100)。

我們可以看到這道題:dp!怎麼d?想怎麼d怎麼d。。。好吧其實我們可以分析一下————很容易想到在列舉每個兒子的時候,當前兒子的花費都會受到且只受到前一個兒子的影響,可以建一個dp[i][j]表示當前第i個孩子身高為j的情況。狀態轉移方程為dp[i][j]=min(dp[i-1][k] + abs(j-k)C + (a[i]-j)(a[i]-j)) a[i]是當前列舉人本身的身高,我們知道他們都可以增高,所以你在狀態轉移的時候,需要列舉前一個人的身高和這個人的身高k和j,a[k]<=k<=100,a[j]<=j<=100(注意只能增高——可能穿了什麼恨天高牌增高鞋),這樣dp確實可以d,但是不要高興的太早了——我們來分析一下時間複雜度——我們要列舉每個人,列舉到每個人的時候還要列舉當前人的身高,還有之前人的身高,從給出的Input資料來看,在兩秒的時間限制內————超時!怎麼辦,玫瑰色的人生還沒有開始就結束了,我能怎麼辦,我也很絕望…

不過不用著急,我們可以用單調佇列來幫你解決.分析一下這個方程,我們可以看到有一個abs的東西,即是絕對值,所以我們來分析兩種情況,一種是前面那個人比當前列舉的人高,一種前面那個人比當前列舉的人矮,這樣我們就可以吧絕對值的帽子去掉,建設新農村…好吧優化dp的新辦法。我們看一下去掉絕對值的方程.
當第 i 個兒子的身高比第 i-1 個兒子的身高要高時,
dp[i][j]=min(dp[i-1][k] + j*C-k*C + X); ( k<=j ) 其中 X=(x[i]-j)*(x[i]-j)。
當第 i 個兒子的身高比第 i-1 個兒子的身高要矮時,
dp[i][j]=min(dp[i-1][k] - j*C+k*C + X); ( k>=j ).

我們便可以在去掉絕對值的情況下進行分類討論.首先,我們先令一個二維陣列f,f[i-1][k]=dp[i-1][k]-k*C, g[i][j]=j*C+X; 於是dp[i][j] =min (f[i-1][k])+g[i][j]。

在兩個取絕對值的式子裡面,我們可以看到都需要前一個人的花費求到一個最小值來更新當前狀態,那我們可以建造一個單調遞增的佇列,在列舉第i個人的時候,我們把前一個人(i-1)在不同身高的狀態所用的花費做成一個這樣的單調佇列,這樣就不用列舉前一個人的身高(因為我們想要的最小值已經求出來了),所以就大大減少了時間複雜度。程式碼如下.

#include<stdio.h>
#include<algorithm>
using namespace std;
const int inf=0x3f3f3f3f;
int dp[2][101],q[101],h,t,cur,x,n,c,ans,k;
inline const int read()
{
  register int x=0,f=1;
  register char ch=getchar();
  while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
  while(ch>='0'&&ch<='9'){x=(x<<3)+(x<<1)+ch-'0';ch=getchar();}
  return x*f;
}//讀入優化
int main()
{ 
  while(scanf("%d%d",&n,&c)!=EOF)
  { ans=inf;
    x=read();
    cur=0;//cur 是為了滾動陣列的 
    for(int i=0;i<x;i++) dp[cur][i]=inf;//不能變矮,賦為超大值
    for(int i=x;i<=100;i++) dp[cur][i]=(i-x)*(i-x);//增高的花費
    for(register int i=1;i<n;i++)
    { 
      x=read(),cur=cur^1,h=1,t=0;//單調佇列初始化,cur有涉及到位運算,cur^1可以看做1-cur
      for(int j=0;j<=100;j++)//前一個比當前高 
      {
        k=dp[cur^1][j]-j*c;
        while(h<=t&&q[t]>k) t--;//如果隊尾比當前大就彈出佇列,為什麼?我們維護的是一個單調遞增的序列,隊首儲存最優,為了滿足這個性質,不優的全部踢掉
        q[++t]=k;//儲存當前元素
        if(j<x) dp[cur][j]=inf;
        else dp[cur][j]=q[h]+j*c+(x-j)*(x-j);
        //可以代入上面的方程看看,好理解一點
      }  
      h=1,t=0;
      for(int j=100;j>=0;j--)//前一個比當前矮 
      {
        k=dp[cur^1][j]+j*c;
        while(h<=t&&q[t]>k) t--;
        q[++t]=k;
        if(j>=x) dp[cur][j]=min(dp[cur][j],q[h]-j*c+(x-j)*(x-j));//直接比較求最優了
        //這就是單調佇列的好處,我們可以直接呼叫q[h],因為我們維護的是一個單調遞增的序列,隊首的自然是最小的,即min值,不需再列舉前一個人的身高,直接呼叫!
      }  
    } 
    for(int i=0;i<=100;i++) ans=min(ans,dp[cur][i]);
    printf("%d\n",ans);
  }
   return 0;
}

不過這道題其實是可以不用單調佇列的,因為我們只需要儲存最小值就可以了…用一個Min不斷比較儲存前一個人的最小花費即可,程式碼如下(不過單調佇列的思想很重要!)

#include <cstdlib>
#include <cstdio>
#include <cstring>
#include <algorithm>
#define inf 0x3fffffff
using namespace std;
int n,c,h[50005],dp[2][105],cur,limit,Min;
inline const int read()
{
  register int x=0,f=1;
  register char ch=getchar();
  while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
  while(ch>='0'&&ch<='9'){x=(x<<3)+(x<<1)+ch-'0';ch=getchar();}
  return x*f;
}
int main()
{
    while (scanf("%d%d",&n,&c)==2) {
        cur=1;
        limit=-inf;
        Min=inf;
        for(register int i=1;i<=n;++i){h[i]=read();limit=max(limit,h[i]);} 
        for(register int i=0;i<=1;++i) memset(dp[i],0x3f,sizeof(dp[i]));     
        for(int i=h[1];i<=limit;++i) dp[cur][i]=(i-h[1])*(i-h[1]); 
        for(register int i=2;i<=n;++i) { 
            cur=cur^1;
            Min=inf;
            for(int j=h[i-1];j<=limit;++j) {
                Min=min(Min,dp[cur^1][j]-c*j); 
                if (j>=h[i]) dp[cur][j]=Min+c*j+(j-h[i])*(j-h[i]);
            }
            Min=inf;
            for(int j=limit;j>=h[i];--j) {
                Min=min(Min,dp[cur^1][j]+c*j);
                if(j>=h[i]) dp[cur][j]=min(dp[cur][j],Min-c*j+(j-h[i])*(j-h[i]));
            }
            memset(dp[cur^1],0x3f,sizeof(dp[cur^1]));
        }
        for (int i=h[n];i<=limit;++i) {
            Min=min(Min,dp[cur][i]);
        }
        printf("%d\n",Min);
    }
    return 0;    
}

相信大家對單調佇列有了一個初步的瞭解,說白了,就是少一種列舉,減去一維,直接可以獲取單調佇列裡儲存的最優解,但注意,並非所有的dp都可以用單調佇列來優化,只有狀態轉移方程中出現了求min或max的時候才可能可以用,因為這樣才符合單調佇列裡的性質。

現在我們進行深入,做一道很經典的題,這道題描述的是一個股神,可以預測股市行情,但是這個天才都這麼牛了還需要我們幫他計算最大利潤…這道題是hdu3401,可以自己看看題目大意(不過是英文的哦).

分析一下這道題,很容易知道在第i天做當前決策的時候,我們有三個選擇,一是從前一天不買也不賣,二是從前i-W-1天買進一些股(w是冷凍時間,你進行一次股市操作就要至少w天后才能再次進行一次操作——注意,每次操作只能是買或者是賣,當然,你也可以不買不賣.),三是從i-W-1天賣掉一些股.問什麼一定是i-w-1天呢?因為有第一種情況我們知道,在i-w-2天時我們可以通過不買也不賣來講最優狀態轉移到i-w-1天,所以沒必要考慮i-w-1天之前的了.
分析完情況,我們便可以想到方程。這道題的買和賣可以分成兩個方程,跟上面那道我要長高分成前一個人比我高或者比我矮一樣的道理.我們來看一看買的狀態轉移方程.

dp[i][j]=max(dp[i-W-1][k]+k*AP[i])-j*AP[i]

我們發現了max符號——求最大值?是不是意味著我們只要提前用單調佇列求出最大值即可?所以我們可以令f[i-W-1][k]=dp[i-W-1][k]+k*AP[i],則dp[i][j]=max(f[i-W-1][k]) - j*AP[i]。把f陣列用一個單調遞減的序列維護(隊首保留最大值)即可,程式碼如下.

#include<stdio.h>
#include<algorithm>
using namespace std;
int t,ap,bp,as,bs,dp[2004][2004],q[2004],v[2004],l,r;
int n,maxp,w;
inline const int read()
{
   register int x=0,f=1;
   register char ch=getchar();
   while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
   while(ch>='0'&&ch<='9'){x=(x<<3)+(x<<1)+ch-'0';ch=getchar();}
   return f*x;
}
int main()
{
   t=read();
   while(t--)
   {
    n=read(),maxp=read(),w=read();   
    register int i,j;
    for(i=1;i<=w+1;i++)
    {
     ap=read(),bp=read(),as=read(),bs=read();
     for(j=0;j<=maxp;j++)
     {
      if(j<=as) dp[i][j]=-j*ap;
      else dp[i][j]=-210000000;
      if(i>1) dp[i][j]=max(dp[i][j],dp[i-1][j]);
     }
    }
    for(register int i=w+2;i<=n;i++)
    {
     ap=read(),bp=read(),as=read(),bs=read();
     int k=i-w-1;
     l=0,r=-1;
     for(j=0;j<=maxp;j++)
     { //int tmp=dp[k][j]-ap*(maxp-j);
       while(l<=r&&tmp>v[r]) r--;
       q[++r]=j;
       v[r]=tmp; //v陣列儲存的是值
       while(j-q[l]>as) l++;
       dp[i][j]=max(dp[i-1][j],v[l]+ap*(maxp-j));
     }
     l=0,r=-1;
     for(j=maxp;j>=0;j--)
     { //int tmp=dp[k][j]+bp*j;
       while(l<=r&&tmp>v[r]) r--;
       q[++r]=j;
       v[r]=tmp;
       while(q[l]-j>bs) l++;
       dp[i][j]=max(dp[i][j],v[l]-bp*j);
     }
    }
    printf("%d\n",dp[n][0]);
   }
}

最後一道,NOI2009的經典題,瑰麗華爾茲.這道題的描述有點長,可以直接看主幹部分.
【任務描述】

你跳過華爾茲嗎?當音樂響起,當你隨著旋律滑動舞步,是不是有一種漫步仙境的愜意?眾所周知,跳華爾茲時,最重要的是有好的音樂。但是很少有幾個人知道,世界上最偉大的鋼琴家一生都漂泊在大海上,他的名字叫丹尼•佈德曼•T.D.•檸檬•1900,朋友們都叫他1900。
1900出生於20世紀的第一年出生在往返於歐美的郵輪弗吉尼亞號上,然後就被拋棄了。1900剛出生就成了孤兒,孤獨的成長在弗吉尼亞號上,從未離開過這個搖晃的世界;也許是對他命運的補償,上帝派可愛的小天使艾米麗照顧他。
可能是天使的點化,1900擁有不可思議的鋼琴天賦,從未有人教,從沒看過樂譜,但他卻能憑著自己的感覺彈出最沁人心脾的旋律。當1900的音樂獲得郵輪上所有人的歡迎時,他才8歲,而此時他已經乘著海輪往返歐美50多次了。
雖說是鋼琴奇才,但1900還是個8歲的孩子,他有著和一般男孩一樣的好奇的調皮,不過可能更有一層浪漫的色彩罷了:
這是一個風雨交加的夜晚,海風捲起層層巨浪拍打著弗吉尼亞號,郵輪隨著巨浪劇烈的搖擺。船上的新薩克斯手邁克斯?託尼暈船了,1900將他 邀請到舞廳,然後——,然後鬆開了固定鋼琴的閘,於是,鋼琴隨著海輪的傾斜滑動起來。準確的說,我們的主角1900、鋼琴、郵輪隨著1900的旋律一起跳 起了華爾茲,所有的事物好像都化為一體,隨著“強弱弱”的節奏,託尼的暈船症也奇蹟般地一點一點恢復。正如託尼在回憶錄上這樣寫道:
大海搖晃著我們
使我們轉來轉去
快速的掠過燈和傢俱
我意識到我們正在和大海一起跳舞
真是完美而瘋狂的舞者
晚上在金色的地板上快樂的跳著華爾茲是不是很愜意呢?也許,我們忘記了一個人,那就是艾米麗,她可沒閒著:她必須在適當的時候施魔法幫助1900,不讓鋼琴碰上舞廳裡的傢俱。而艾米麗還小,她無法施展魔法改變鋼琴的運動方向或速度,而只能讓鋼琴停一下。
不妨認為舞廳是一個N行M列的矩陣,矩陣中的某些方格上堆放了一些傢俱,其他的則是空地。鋼琴可以在空地上滑動,但不能撞上傢俱或滑出舞廳,否則會損壞鋼琴和傢俱,引來難纏的船長。
每個時刻,鋼琴都會隨著船體傾斜的方向向相鄰的方格滑動一格,其中相鄰的方格可以是向東、向西、向南或向北的。而艾米麗可以選擇施魔法或不施魔法,如果不施魔法,則鋼琴會滑動,而如果施魔法,則鋼琴會原地不動。 艾米麗是個天使,她知道每段時間的船體的傾斜情況。她想使鋼琴儘量長時間在舞廳裡滑行,這樣1900會非常高興,同時也有利於治療託尼的暈船。但艾米麗還太小,不會算,所以希望你能幫助她。

【輸入格式】

輸入檔案的第一行包含5個數N, M, x, y和K。N和M描述舞廳的大小,x和y為在第1時刻初鋼琴的位置(x行y列);我們對船體傾斜情況是按時間的區間來描述的,比如“在[1, 3]時間裡向東傾斜,[4, 5]時間裡向北傾斜”,因此這裡的K表示區間的數目。
以下N行,每行M個字元,描述舞廳裡的傢俱。第i行第j列的字元若為‘ . ‘,則表示該位置是空地;若為‘ x ‘,則表示有傢俱。
以下K行,順序描述K個時間區間,格式為:si ti di(1 ≤ i ≤ K)。表示在時間區間[si, ti]內,船體都是向di方向傾斜的。di為1, 2, 3, 4中的一個,依次表示北、南、西、東(分別對應矩陣中的上、下、左、右)。輸入保證區間是連續的,即

s1 = 1
ti = si-1 + 1 (1 < i ≤ K)
tK = T
【輸出格式】

輸出檔案僅有1行,包含一個整數,表示鋼琴滑行的最長距離(即格子數)。

【輸入樣例】

4 5 4 1 3
..xx.
…..
…x.
…..
1 3 4
4 5 1
6 7 3
【輸出樣例】

6

顯然我們可以列舉時間,行,列無腦do,當然——要超時.我們可以看見,在dp的是後當d==1即往上走,每一列就可用單調佇列維護資訊,時間由原來的列舉t,變到列舉k(時間段).

#include<stdio.h>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxn=201;

int dx[4]={-1,1,0,0},dy[4]={0,0,-1,1};
int f[2][maxn][maxn],q[maxn],v[maxn];
char map[maxn][maxn];
int n,m,x,y,k,ans;
int cur1=1,cur2;
int a,b,c;

inline const int read(){
   register int x=0,f=1;
   register char ch=getchar();
   while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
   while(ch>='0'&&ch<='9'){x=(x<<3)+(x<<1)+ch-'0';ch=getchar();}
   return x*f;
}

inline void solve(int x,int y,int w,int len,int c){
   register int i,h=1,t=0,k;
   for(i=1;i<=w;i++){
     if(map[x][y-1]=='.'){
       k=f[cur1][x][y]+w-i;
       while(h<=t&&k>v[t]) t--;
       v[++t]=k;
       q[t]=i;
       while(i-q[h]>len) h++;
       f[cur2][x][y]=v[h]-w+i;
     }
     else h=t+1,f[cur2][x][y]=-2e9;
     x=x+dx[c];
     y=y+dy[c];
   }
}

int main(){  
   register int i,j;
   n=read(),m=read(),x=read(),y=read(),k=read();
   for(i=1;i<=n;i++) scanf("%s",map[i]);
   for(i=1;i<=n;i++)
       for(j=1;j<=m;j++) f[0][i][j]=-2e9;
   f[0][x][y]=0;
   for(i=1;i<=k;i++){
       cur1=cur1^1;
       cur2=cur2^1;
       a=read(),b=read(),c=read(); 
       if(c==1) for(j=1;j<=m;j++) solve(n,j,n,b-a+1,c-1);  
       if(c==2) for(j=1;j<=m;j++) solve(1,j,n,b-a+1,c-1);  
       if(c==3) for(j=1;j<=n;j++) solve(j,m,m,b-a+1,c-1);  
       if(c==4) for(j=1;j<=n;j++) solve(j,1,m,b-a+1,c-1);   
   } 
   for(i=1;i<=n;i++)  
       for(j=1;j<=m;j++) ans=max(ans,f[cur2][i][j]);  
   printf("%d\n",ans); 
}

希望這篇部落格能對大家有所幫助.