區間DP小結

區間DP主要是把一個大區間拆分成幾個小區間,先求小區間的最優值,然後合併起來求大區間的最優值。

區間DP最關鍵的就是滿足最優子結構以及無後效性!!!

//一般區間DP實現代碼
memset(dp, 0x3f, sizeof(dp));
for (int i = 1; i <= n; i++) //區間長度爲1的初始化
    dp[i][i] = 0;
for (int len = 2; len <= n; len++) //枚舉區間長度
{
    for (int i = 1, j = len; j <= n; i++, j++) //區間[i,j]
    {
        //DP方程實現
    }
}

我們規定dp爲狀態轉移方程數組,a爲數據數組(下標從1開始),sum爲數據前綴和數組 
假設a[] = {1,2,3,4}, sum[] = {1,3,6,10}

第一種模型

石子合併

一條直線上有N堆石子,現在要將所有石子合併成一堆,每次只能合併相鄰的兩堆,合併花費爲新合成的一堆石子的數量,求最小的花費。

1堆,花費爲0 
2堆,花費爲sum[2] 
3堆,花費爲min(a[1] + a[2], a[2] + a[3]) + sum[3] 
如果我們有n堆,合併的最後一次操作一定是從兩堆合併成一堆,

第一種模型就是將大區間從任意位置分成兩個小區間

規定dp[i][j]爲合併第i堆到第j堆的最小花費 
DP方程爲: 
dp[i][j] = min(dp[i][k] + dp[k+1][j]) + sum[j] - sum[i-1] (i <= k < j)

//複雜度O(n^3)  可以用平行四邊形優化到O(n^2)
//http://blog.csdn.net/find_my_dream/article/details/4931222
memset(dp, 0x3f, sizeof(dp));
for (int i = 1; i <= n; i++) 
    dp[i][i] = 0;
for (int len = 2; len <= n; len++)
{
    for (int i = 1, j = len; j <= n; i++, j++)
    {
        for (int k = i; k < j; k++)
        {
            if(dp[i][j] > dp[i][k] + dp[k+1][j] + sum[j] - sum[i-1])
                dp[i][j] = dp[i][k] + dp[k+1][j] + sum[j] - sum[i-1];
        }
    }
}
printf("%d\n", dp[1][n]);
  •  

第二種模型

括號匹配 poj2955

給一個括號組成的字符串,問最多能匹配多少個括號 
像([)]這樣的字符串匹配度爲2,但是像()[]、[()]的字符串匹配度爲4,也就是說括號具有分隔作用。

長度爲1的串匹配度 0 
長度爲2的串匹配度 0 或 2

規定dp[i][j]爲合併第i到第j個字符的最大匹配度 
長度爲n時,我們可以先檢測a[i]和a[j]是否匹配, 
匹配dp[i][j] = dp[i+1][j-1] + 2 
不匹配,那我們可以按第一種模型處理,從任意位置分成兩個區間

while (gets(a+1))
{
    if (a[1] == 'e') break;
    memset(dp, 0, sizeof(dp));
    int n = strlen(a+1);
    for (int len = 2; len <= n; len++)
    {
        for(int i = 1, j = len; j <= n; i++, j++)
        {
            if((a[i]=='('&&a[j]==')') || (a[i]=='['&&a[j]==']'))
                dp[i][j] = dp[i+1][j-1] + 2;
            for (int k = i; k < j; k++)
                if(dp[i][j] < dp[i][k] + dp[k+1][j])
                    dp[i][j] = dp[i][k] + dp[k+1][j];
        }
    }
    printf("%d\n",dp[1][n]);
}
  •  

當然,這樣並不能稱之爲第二種模型

我們可以把[i,j]區間的字符當成由[i+1,j]在前面加個字符或[i,j-1]在後面加一個字符得來的

這裏我們只考慮[i,j]由[i+1,j]在前面加一個字符的情況 
如果a[i+1]到a[j]沒有和a[i]匹配的,那麼dp[i][j] = dp[i+1][j] 
如果a[k]和a[i]匹配(i < k <= j),那麼dp[i][j] = max(dp[i][j], dp[i+1][k-1] + dp[k+1][j] + 2); 
比如:[xxxxx]yyyyy通過括號分成兩個子串

第二種模型就是根據匹配信息把區間劃分成[i+1,k-1]和[k+1,j]

//代碼還可以這樣寫
while (gets(a+1))
{
    if(a[1] == 'e') break;
    memset(dp, 0, sizeof(dp));
    int n = strlen(a+1);
    for (int len = 2; len <= n; len++)
    {
        for(int i = 1, j = len; j <= n; i++, j++)
        {
            dp[i][j] = dp[i+1][j];
            for (int k = i; k <= j; k++)
                if((a[i]=='('&&a[k]==')') || (a[i]=='['&&a[k]==']'))
                    dp[i][j] = max(dp[i][j], dp[i+1][k-1] + dp[k+1][j] + 2);
        }
    }
    printf("%d\n",dp[1][n]);
}
  •  

看兩道題體會下這種模型吧 ^-^

You Are the One HDU 4283

Halloween Costumes LightOJ - 1422和這道差不多 
題意:一羣小屌絲,排成一排,第k個上場屌絲值就爲a[i]*(k-1),可以通過一個小黑屋(棧)調整順序,但是先進屋的最後才能出來,求屌絲值和最小。 
思路:首先,我們知道棧有順序,比如1、2、3都進棧,出棧就一定是3、2,1了。 
規定dp[i][j]爲從第i個人到第j個人出場的最小屌絲值。 
如果第i個人第k個上場(1 <= k <= j-i+1),那麼[i+1,i+k-1]區間的人一定在i上場前都上場了,這時候剛好棧空,前k個人全上場了,然後就能以i的上場次序劃分區間了?? 
DP方程: 
dp[i][j] = min(dp[i][j], dp[i+1][i+k-1] + a[i]*(k-1) + dp[i+k][j] + (sum[j] - sum[i+k-1])*k)

memset(dp, 0, sizeof(dp)); //會訪問到dp[j+1][j]這樣的區間,賦值成0就好了
for (int i = 1; i <= n; i++)
    for (int j = i+1; j <= n; j++)
        dp[i][j] = INF;
for (int len = 2; len <= n; len++)
{
    for (int i = 1, j = len; j <= n; i++, j++)
    {
        for (int k = 1; k <= j-i+1; k++)//k的枚舉也可以是[i,j],不過等待時間要做些改變
            dp[i][j] = min(dp[i][j], dp[i+1][i+k-1] + a[i]*(k-1) + dp[i+k][j] + (sum[j]-sum[i+k-1])*k);
    }
}
  •  

String painter HDU - 2476

這道題要先是用區間dp預處理,就是把空白串變成串t的最小花費,然後再計算由串s到串t的最小花費,看代碼吧。

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int MAXN = 105;
char s[MAXN],t[MAXN];
int dp[MAXN][MAXN];
int a[MAXN];
int main()
{
    while (~scanf(" %s %s", s+1,t+1))
    {
        int n = strlen(s+1);
        memset(dp, 0, sizeof(dp));
        for (int i = 1; i <= n; i++) 
            dp[i][i] = 1;
        for (int len = 2; len <= n; len++)
        {
            for (int i = 1, j = len; j <= n; i++, j++)
            {
                dp[i][j] = 1 + dp[i+1][j]; //前面增加的字符需要修改一次
                for (int k = i+1; k <= j; k++)
                    if (t[i] == t[k])
                        dp[i][j] = min(dp[i][j], dp[i+1][k-1] + dp[k][j]);
            }
        }

        for (int i = 1; i <= n; i++)
        {
            a[i] = dp[1][i];
            if (s[i] == t[i]) 
                a[i] = a[i-1];
            else
            {
                for (int j = 1; j <= i; j++)
                    a[i] = min(a[i], a[j] + dp[j+1][i]);
            }
        }
        printf("%d\n", a[n]);
    }
    return 0;
}
  •  

第三種模型

這種比較簡單,不需要枚舉區間k∈[i,j],這種類型只和左右邊界相關。

Cheapest Palindrome POJ 3280

題意:n個字符組成長度爲m的字符串,給出增刪字符的花費,可在字符串任意位置增刪字符,求把字符串修改成迴文串的最小花費。 
規定dp[i][j]爲將[i,j]區間改成迴文串的最小花費,可以看成有迴文串 
[i+1,j]在前面加一個字符 =>前面刪或後面增 
[i,j-1]在後面加一個字符 =>前面增或後面刪 
共四種情況,當a[i] == a[j]時,加個dp[i+1][j-1]的情況就好了

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int MAXN = 2005;
char a[MAXN], ch;
int dp[MAXN][MAXN];
int add[30],sub[30];
int main()
{
    int n, m;
    scanf("%d %d", &n, &m);
    scanf("%s", a+1);
    for (int i = 1; i <= n; i++)
    {
        scanf(" %c", &ch);
        scanf("%d %d", &add[ch-'a'], &sub[ch-'a']);
    }
    memset(dp, 0x3f, sizeof(dp));
    for (int i = 1; i <= m; i++) 
        dp[i][i] = 0;
    for (int len = 2; len <= m; len++)
        for(int i = 1, j = len; j <= m; i++, j++)
        {
            dp[i][j] = min(dp[i][j], min(add[a[i]-'a'],sub[a[i]-'a']) + dp[i+1][j]);
            dp[i][j] = min(dp[i][j], dp[i][j-1] + min(add[a[j]-'a'],sub[a[j]-'a']));
            if (a[i] == a[j])
            {
                if (len==2) 
                    dp[i][j] = 0;
                else
                    dp[i][j] = min(dp[i][j], dp[i+1][j-1]);
            }
        }
    printf("%d\n", dp[1][m]);
    return 0;
}
  •  

Tian Ji – The Horse Racing HDU 1052

題意:田忌賽馬,贏一場得200,負一場輸200,問最後最多能贏多少錢(or輸的最少) 
思路:假設齊王從最強的馬開始出,如果田忌最強的馬能贏齊王最強的,就用最強的馬,不能贏,就能最弱的馬。 
是不是很像貪心?但是處理最強的打平比較麻煩,可以選擇平或者選擇輸。

比如田忌的馬速度 2,3 齊王 1,3,這樣速度3的打平,速度2的勝一場。 
田忌的馬速度 1,2,3,4 齊王的馬速度 1,2,3,4,速度1的輸給4,其他三場勝。

但是我們能分析出每次要麼派最強的上場,要麼派最弱的上場。 
這裏只討論DP的方法。 
規定dp[i][j]爲田忌第i到第j匹馬和齊王慢的j-i+1匹馬比的勝場減負場值

我們繼續看這個例子:田忌的馬速度 1,2,3,4 齊王的馬速度 1,2,3,4 
dp[1][3]爲田忌速度爲1,2,3的馬和齊王速度爲1,2,3的馬比 
dp[2][4]爲田忌速度爲2,3,4的馬和齊王速度爲1,2,3的馬比

dp[1][4]爲田忌速度爲1,2,3,4的馬和齊王速度爲1,2,3,4的馬比 
我們可以把他看成[1,3]加了一匹速度4的馬,[2,4]加了一匹速度1的馬 
齊王都是加了速度4的馬。 
這時候就有兩種選擇: 
1.讓最強的馬(速度4)和齊王的強馬比 
2.讓最慢的馬(速度1)和齊王的強馬比

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
using namespace std;
const int MAXN = 1005;
int dp[MAXN][MAXN];
int tian[MAXN], qi[MAXN];
int val(int a, int b)
{
    if (a > b) return 1;
    if (a < b) return -1;
    return 0;
}
int main()
{
    int n;
    while (~scanf("%d", &n) && n)
    {
        for (int i = 1; i <= n; i++) scanf("%d", &tian[i]);
        for (int i = 1; i <= n; i++) scanf("%d", &qi[i]);
        sort(tian+1, tian+n+1);
        sort(qi+1, qi+n+1);
        memset(dp, 0x8f, sizeof(dp));
        for (int i = 1; i <= n; i++) 
            dp[i][i] = val(tian[i], qi[1]);
        for (int len = 2; len <= n; len++)
            for (int i = 1, j = len; j <= n; i++, j++)
                dp[i][j] = max(dp[i][j-1] + val(tian[j], qi[len]), dp[i+1][j] + val(tian[i], qi[len]));
        printf("%d\n", dp[1][n] * 200);
    }
    return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章