区间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
#include
#include
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
#include
#include
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
#include
#include
#include
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;
}

你可能感兴趣的:(区间DP)