區間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;
}