區間dp

區間dp寫起來就比概率dp寫起來有套路多了。
總的來說,自認爲區間dp就只有三四種套路。

大致分成兩種,小區間推大區間,大區間推小區間

一些平常的區間dp只要定義dp[][]兩維就行,兩種不同的定義方式。一種dp[i][j] 表示左端爲i右端爲j。另一種是dp[i][j] 表示左端爲i,長度爲j。兩種寫法對應的循環也不同。

以小區間爲例
對於第一種,dp[i][j] 左端點+右端點

for(int i=n;i>=1;i--)
    for(int j=i;j<=n;j++)

一般第一重循環表示右端點,第二重循環表示左端點。這樣寫是當要求的答案是dp[1][n] 的時候。
對於第二種,dp[i][j] 左端點+長度

for(int i=1;i<=n;i++)
    for(int j=1;i+j<=n;j++)
        ……

一般第一重循環表示長度,第二重循環表示左端點。

還有些題目就不能直接這樣寫了,可能區間dp沒那麼明顯,區間性不高。可以根據一些性質轉化成區間dp。

以下是例題分析:(有題不是區間dp)

一、HDU 3280 Equal Sum Partitions
第一題就不是區間dp……區間dp第一題不是區間dp,虧我想了這麼久……暴力枚舉和,然後判斷一下,玄學就過了。

二、HDU 4283 You Are the One
比較明顯的區間dp。開始時想把一個點往相鄰的區間的頭尾放,然後轉移就行了。然後帶了一組數據發現完全錯。然後發現作爲一個棧,爲什麼只能往頭尾放?棧是先進後出,那麼一個點進去後,可以晚點出來。那麼對於一個點,就可以插入到後面區間任意一個位置。那麼轉移就簡單了。套用第一個模板

for(int l=1;l<=n;l++)
    for(int i=1;i+l<=n;i++){
        int j=i+l;
        dp[i][j]=1e9;
        for(int k=1;i+k-1<=j;k++)
            dp[i][j]=min(dp[i][j],dp[i+1][i+k-1]+dp[i+k][j]+k*(cnt[j]-cnt[i+k-1])+A[i]*(k-1));//cnt是前綴和
    }

三、HDU 4293 Groups
這道題如果直接去想區間dp,肯定會被他的分組弄糊。反方向想,如果一個人說他前面有a個人,後面b個人,那麼如果想要這個人說真話,那麼他肯定要站在[a+1,n-b]這個區間內。這個顯然,那麼這樣就劃分出區間來了。可以知道對於一個區間[L,R],有很多人要站在裏面,那麼這個區間可能要滿出來了,那就要判一下。讀入的時候把這些區間求出來。問題就轉化爲多個有權值的區間,從中選出一些不重疊的區間使權值最大。這種問題dp只要一維就行了。

//預處理部分
for(int i=1;i<=n;i++){
    int x,y;
    scanf("%d%d",&x,&y);
    if(x+y<=n&&n-y-x>A[x+1][n-y])A[x+1][n-y]++;//存在區間&&區間人數足夠 
}

//dp部分
for(int i=1;i<=n;i++)
    for(int j=1;j<=i;j++)
        dp[i]=max(dp[i],dp[j-1]+A[j][i]);

四、HDU 4412 Sky Soldiers
求期望的區間dp。可以說dp的轉移不是很難,難在預處理那一段。dp[i][j] 表示前i個點建立j個大本營要的代價。此時轉移還要一個數組cost[i][j] 來輔助,表示在[i,j]區間建一個大本營的代價。那麼循環一箇中間點,然後往上一層轉移就行。

for(int i=2;i<=n;i++)
    for(int j=1;j<=m;j++){
        dp[i][j]=1e9;
        for(int k=0;k<i;k++)dp[i][j]=min(dp[i][j],dp[k][j-1]+cost[k+1][i]);
        }

主要是預處理cost這個數組。如果給了降落點的範圍,那麼就沒有那麼麻煩。但是題目沒給範圍,那就要離散了。離散用了map,還要用迭代器,麻煩。現在講講不用離散的。現在要往一個區間裏放一個大本營,那麼就要選一個最優的位置放大本營的位置,而[i,j]又要循環過去都求一遍,那麼可以一邊枚舉左右端點,一邊選出一個最優的大本營地址。我是從後往前算的。當區間右端點固定時,最短點不斷延伸,那麼大本營顯然會向左移動,那麼每次都只要往左移動,那麼大概就是n^2了。但是因爲還要加map,一下子不知道怎麼去重,看了一下題解,發現可以直接裝起來,然後再拎出來就行了。

五、HDU 4597 Play Game
這個和取數遊戲是一樣的。因爲兩個人都是最有策略,那麼只要用當前的總和減去另一個人的最有策略就行。dp要定義四維,分別是兩行的左右端點。這道題其實用記憶化搜索寫起來更加方便易懂。

六、HDU 4745 Two Rabbits
先肯定要把環變成一條鏈,只要多存一組就行了。轉化爲線性後,題目就變成了在一條鏈上求一個最長的迴文子序列,因爲順時針、逆時針一樣。那麼轉移起來就很簡單了。要麼從旁邊傳遞值,要麼相等加2。求答案時要注意一點,兩個人的起點可能從同一個點開始,那麼此時的值就是在n-1的長度裏的最值再加1。

for(int i=2*n;i>=1;i--)
    for(int j=i+1;j<=2*n;j++){
        dp[i][j]=max(dp[i+1][j],dp[i][j-1]);//轉移
        if(A[i]==A[j])dp[i][j]=max(dp[i][j],dp[i+1][j-1]+2);//相等
    }
int ans=0;
for(int i=1;i<=n;i++)ans=max(ans,max(dp[i][i+n-1],dp[i][i+n-2]+1));//長度爲n但起點不同,長度爲n-1單起點相同

七、HDU 5115 Dire Wolf
這道套一下模板,把一個區間按照一個點切開成兩個區間,然後左邊的加右邊的再加一下附加攻擊值就行。用第二個模板

for(int i=n;i>=1;i--)
    for(int j=i;j<=n;j++){
        dp[i][j]=1e9;
        for(int k=i;k<=j;k++)//中間點切開
            dp[i][j]=min(dp[i][j],dp[i][k-1]+dp[k+1][j]+A[k]+B[i-1]+B[j+1]);
            }

八、HDU 5273 Dylans loves sequence
其實我一直不覺的只是一道區間dp。明明就是前綴和維護一下就行了……

九、HDU 5396 Expression
這個區間dp的大致想法和第七題狼那道是一樣的。也是在一個區間內枚舉一個點,通過這個點把整各區間分成三部分(左,此點,右),然後再累加計算就行了。我寫的是記憶化搜索,因爲轉移的時候計算的東西很多。題目要求合併的順序不同,儘管答案相同,也算不同方案。這樣就要在存一個數組表示這一個區間的方案又是多少。然後怎麼求和呢。一個點把區間分成左右兩部分,左邊區間的和爲L,方案數爲Lc,右邊分別爲R,Rc。那麼對於左區間來說,右區間的方案數就是左區間算的次數,那麼左邊的和就是L* Rc。同理右邊是R* Lc。累加起來就行(只限於加減,乘法只要L*R就行)。方案數要多求一個東西。Lc和Rc僅限於他們自己的區間,合併後可以左邊一個右邊一個,這也是不同的方案,雖然答案一樣。那麼還要乘一個C(l+r,l),l,r分別是左右區間的符號數。總方案數就按上面的方法變成了C(l+r,l) * Lc * Rc。
代碼可以上上度娘查,我的太醜了。

十、HDU 5900 QSC and Master
也是一個比較模板的題。用一個點去切一個區間。但是切的時候要注意,因爲要使兩個數可以合併,除了gcd>1,還要這兩個數之間的數合併完了,這還要一個數組來記錄這個。

for(int i=n;i>=1;i--)
    for(int j=i+1;j<=n;j++){
        if(dp[i+1][j]>dp[i][j-1])dp[i][j]=dp[i+1][j];else dp[i][j]=dp[i][j-1];
        Q[i][j]=-1;
        for(int k=i+1;k<=j;k++)
            if(Q[i+1][k-1]!=-1&&gcd(A[i],A[k])>1){
                dp[i][j]=max(dp[i][j],dp[i+1][k-1]+dp[k+1][j]+1LL*V[i]+1LL*V[k]);
                Q[i][k]=1;
                if(Q[k+1][j]!=-1){Q[i][j]=1;break;}
            }
    }

我這個程序要卡常,因爲gcd太慢了,如果把if語句裏的gcd放在前面判斷的話會T,如果先判斷區間是否全部已經合併會快一些。

十一、HDU 6103 Kirinriki
這也不是區間dp。因爲求的兩個字符串可以看成是對稱的,對稱的就存在一箇中間點,那麼就枚舉中間點。枚舉中間點後,還要判斷他們的值是否小於等於m。可以用尺取的方法。先把每一個對應的點求出來,然後尺取就行。中心點也分兩種情況:一種是中心點是點,一種中心點不存在,但是是堆成的。貼其中一段的。

for(int i=1;i<=n;i++){//中心點
    int len=min(i-1,n-i);//此中心點下最長的長度
    for(int j=1;j<=len;j++){
        dp[j]=abs(str[i-j]-str[j+i]);//對應的值
    }
    int l=1,r=1,t=dp[1];
    while(r<=len){//尺取
        while(l<=r&&t>m){
            t-=dp[l];
            l++;
        }
        if(ans<r-l+1)ans=r-l+1;
        r++;
        t+=dp[r];
    }

十二、HDU 6212 Zuma
寫前要處理一下。給的是01串,我們不可能直接用01串來吧?顏色相同的幾個球肯定是放在一起考慮的,那麼就合併起來就行了。
定義dp[i][j] 表示[i,j]區間打完要多少下。轉移有三點,我只想到了兩點,第三點真的想不到。
第一點:把區間[L,R]分成[L,k]和[k+1,R]兩塊,然後把兩塊的直接加起來轉移進來,這要枚舉一箇中間點
第二點:當區間[L,R]的兩端球的顏色相同時,把中間的球打掉,兩邊的球會往中間靠攏,當靠攏後的球大於等於三個時,直接轉移,否則在往裏面打一個球就行了(因爲求的個數肯定大於等於1)
第三點:當區間[L,R]的兩端的球的顏色相同時,可以找一個顏色相同的中間點k且此點球只有一個,把[L+1,k-1]和[k+1,R-1]的區間打掉,然後合併。此情況兩邊的球要保證!(A[L]==2&&A[R]==2)。原因是如果左邊的球有兩個,右邊的球有兩個的話,一往中間合併就炸掉了。例如2+1+2,左邊的2+1變3,右邊的1+2變3,不管怎麼樣都不行。但是1+1+2行,因爲可以先1+1變2再2+2變4.

for(int i=n;i>=1;i--)
    for(int j=i;j<=n;j++){
        if(i==j){dp[i][j]=3-A[i];continue;}
        dp[i][j]=1e9;
        for(int k=i;k<j;k++)dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]);//第一種情況,[L,R]=[L,k]+[k+1,R]
        if((j-i)%2==1)continue;
        dp[i][j]=min(dp[i][j],dp[i+1][j-1]+(A[i]+A[j]==2));//第二種
        if(!(A[i]==2&&A[j]==2))//第三種
            for(int k=i+1;k<j;k++)
                if(A[k]==1&&(k-i)%2==0&&(j-k)%2==0)dp[i][j]=min(dp[i][j],dp[i+1][k-1]+dp[k+1][j-1]);
    }

總結一下:
1、普通的區間dp都是很有套路的,模板就兩個,按照自身習慣,再結合一下題意看寫哪一個方便寫哪一個。
2、區間dp常見的轉移方式有:

1)兩個區間合併成一個區間
2)把一個點將一個區間切成兩部分,然後再把兩邊的加起來
3)在一個區間裏枚舉一個點,然後切成兩部分

3、把題目中的一些條件轉換成範圍性的東西,然後用區間dp
4、其他:環變鏈:多一倍;字符串的迴文或匹配:枚舉中心點。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章