区间DP

区间DP

两个难点:

1、枚举所有可能的区间

2、状态转移方程

经典问题1:石子合并

1274:【例9.18】合并石子

时间限制: 1000 ms         内存限制: 65536 KB
提交数: 5121     通过数: 3243 
【题目描述】
在一个操场上一排地摆放着N堆石子。现要将石子有次序地合并成一堆。规定每次只能选相邻的2堆石子合并成新的一堆,并将新的一堆石子数记为该次合并的得分。

计算出将N堆石子合并成一堆的最小得分。

【输入】
第一行为一个正整数N (2≤N≤100);

以下N行,每行一个正整数,小于10000,分别表示第i堆石子的个数(1≤i≤N)。

【输出】
一个正整数,即最小得分。

【输入样例】
7
13
7
8
16
21
4
18
【输出样例】
239
合并石子

状态:f[i][j]表示区间i到j之间的最小花费,那么f[1][n]就是最后的答案

状态转移方程:f[i][j]=f[i][k]+f[k][j]+sum[i][j-i+1];

所以在这里需要求一个前缀和sum[i][j-i+1] 就相当于sum[j]-sum[i-1]

#include <bits/stdc++.h>
using namespace std;
const int N=130,inf=0x3f3f3f;; 
int sum[N],c[N];
int f[N][N],n,x,t; 
inline int read()
{
    int x=0;char y='*',z=getchar();
    while(z<'0'||z>'9') y=z,z=getchar();
    while(z>='0'&&z<='9') x=(x<<3)+(x<<1)+(z-'0'),z=getchar();
    return y=='-'?-x:x;
}
int main()
{
    n=read();
    for(int i=1;i<=n;i++){
        x=read(); 
        sum[i]=sum[i-1]+x;//前缀和
    } 
    memset(f,127/3,sizeof(f));
    for(int i=1;i<=n;i++) f[i][i]=0;
    for(int i=n-1;i>=1;i--)
        for(int j=i+1;j<=n;j++)
            for(int k=i;k<=j-1;k++)
                f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]+sum[j]-sum[i-1]);
    cout<<f[1][n]<<endl;    
    return 0;
}

上述代码有三重循环,所以复杂度为O(n3),所以这种问题只能处理n<250的问题;

该问题在动态规划中也有写过,咳咳还有一个环形的情况

优化:

平行四边形优化原理:是区间DP常用的优化方法;主要思想就是用s[i][j]来存储最优分割点

只需要更改几个地方:(红色)!!

#include <bits/stdc++.h>
using namespace std;
const int N=130,inf=0x3f3f3f;; 
int sum[N],c[N],s[N][N];
int f[N][N],n,x,t; 
inline int read()
{
    int x=0;char y='*',z=getchar();
    while(z<'0'||z>'9') y=z,z=getchar();
    while(z>='0'&&z<='9') x=(x<<3)+(x<<1)+(z-'0'),z=getchar();
    return y=='-'?-x:x;
}
int main()
{
    n=read();
    for(int i=1;i<=n;i++){
        x=read(); 
        sum[i]=sum[i-1]+x;//前缀和
    } 
    memset(f,127/3,sizeof(f));
    for(int i=1;i<=n;i++) {
        f[i][i]=0;s[i][i]=i;
    }
    for(int i=n-1;i>=1;i--)
        for(int j=i+1;j<=n;j++)
            for(int k=s[i][j-1];k<=s[i+1][j];k++)
                if(f[i][j]>f[i][k]+f[k+1][j]+sum[j]-sum[i-1])
                {
                    f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]+sum[j]-sum[i-1]);
                    s[i][j]=k;
                }
                   
    cout<<f[1][n]<<endl;    
    return 0;
}

这样优化以后,复杂度更加接近于O(n2),科技解决接近n<3000的问题了~

经典问题2:回文串

回文串是正读反读都是一样的字符串,最经典的问题就是:给定一个字符串,通过增加或者删除得到一个回文串。

poj 3280 给出一个由m中字母组成的长度为n的串,给出m种字母添加和删除花费的代价,求让给出的串变成回文串的代价。

我们定义dp [ i ] [ j ] 为区间 i 到 j 变成回文的最小代价。

那么有三种情况:

首先:对于一个串如果s【i】==s【j】,那么dp【i】【j】=dp【i+1】【j-1】

其次:如果dp【i+1】【j】是回文串,那么dp【i】【j】=dp【i+1】【j】+min(add【i】,del【i】);

最后,如果dp【i】【j-1】是回文串,那么dp【i】【j】=dp【i】【j-1】 + min(add【j】,del【j】);

参考代码:

int main()
{
    int n,m,t,i,j;
    while(~scanf("%d%d",&n,&m))
    {
        scanf("%s",s);
        for(i=0; i<n; i++)
        {
            scanf("%s",c);
            scanf("%d%d",&cost[c[0]-'a'],&t);
            if(t<cost[c[0]-'a'])
                cost[c[0]-'a']=t;
        }
        memset(dp,0,sizeof(dp));
        for(i=m-1; i>=0; i--)
        {
            for(j=i+1; j<m; j++)
            {
                dp[i][j]=min(dp[i+1][j]+cost[s[i]-'a'],dp[i][j-1]+cost[s[j]-'a']);
                if(s[i]==s[j])
                    dp[i][j]=min(dp[i][j],dp[i+1][j-1]);
            }
        }
        printf("%d
",dp[0][m-1]);
    }
    return 0;
}

例题:回文词

Description
  回文词是一种对称的字符串——也就是说,一个回文词,从左到右读和从右到左读得到的结果是一样的。任意给定一个字符串,通过插入若干字符,都可以变成一个回文词。你的任务是写一个程序,求出将给定字符串变成回文词所需插入的最少字符数。比如字符串“Ab3bd”,在插入两个字符后可以变成一个回文词(“dAb3bAd”“Adb3bdA”)。然而,插入两个以下的字符无法使它变成一个回文词。
Input
  第一行包含一个整数N,表示给定字符串的长度(3≤N≤50 00)。
  第二行是一个长度为N的字符串。字符串仅由大写字母“A”到“Z”,小写字母“a”到“z”和数字“0”到“9”构成。大写字母和小写字母将被认为是不同的。
Output
  只有一行,包含一个整数,表示需要插入的最少字符数。
Sample Input

5
Ab3bd
Sample Output
2
回文词

用字符串长度来减去(原字符串与反串)的最长公共子序列长度只能得到70分

还是得用这种记忆化的dp才能AC:

#include <bits/stdc++.h>
using namespace std;
const int N=5005,inf=0x3f3f3f;; 
int f[N][N],n,x;
char s[N],rs[N]; 
int dfs(int i,int j)
{
    if (f[i][j]!=0xfffffff)
        return f[i][j];
    if (s[i-1]==s[j-1])
        return f[i][j]=dfs(i+1,j-1);
    else
        {  f[i][j]=min(dfs(i+1,j),dfs(i,j-1))+1;
        }
        return f[i][j];
}
int main()
{  
    scanf("%d",&n);
    cin>>s;
    for (int i=1;i<=n;i++)
        for (int j=i+1;j<=n;j++)
        {
            f[i][j]=0xfffffff;f[i][i]=0;
        }
    f[1][n]=dfs(1,n);
    printf("%d",f[1][n]);
    return 0;
}

区间动态规划例题:(做到就来补一补)

 添加括号:

Description
【题目背景】
  给定一个正整数序列a1,a2,...,an,(1<=n<=20)
  不改变序列中每个元素在序列中的位置,把它们相加,并用括号记每次加法所得的和,称为中间和。
  例如:
  给出序列是4,123。
  第一种添括号方法:
    ((4+1)+(2+3))=((5)+(5))=(10)
  有三个中间和是5,510,它们之和为:5+5+10=20
  第二种添括号方法 
    (4+((1+2)+3))=(4+((3)+3))=(4+(6))=(10)
  中间和是3,610,它们之和为19。 
【问题描述】 
  现在要添上n-1对括号,加法运算依括号顺序进行,得到n-1个中间和,求出使中间和之和最小的添括号方法。 
Input
  共两行。
  第一行,为整数n。(1<=n<=20)
  第二行,为a1,a2,...,an这n个正整数,每个数字不超过100。
Output
  输出3行。
  第一行,为添加括号的方法。
  第二行,为最终的中间和之和。
  第三行,为n-1个中间和,按照从里到外,从左到右的顺序输出。
Sample Input
4
4 1 2 3
Sample Output
(4+((1+2)+3))
19
3 6 10
添加括号

主要是输出!!!其实计算最小值就跟合并石子差不多

#include <bits/stdc++.h>
using namespace std;
const int N=110,inf=0x3f3f3f;
int n,f[N][N],a[N],sum[N],loc[N][N];
void print_equa(int l,int r)
{
    if(l==r){ cout<<a[l];return; }
    cout<<"(";
    int k=loc[l][r];
    print_equa(l,k);
    cout<<"+";
    print_equa(k+1,r);
    cout<<")";
}
void print_sum(int l,int r)
{
    if(!loc[l][r]) return;
    int k=loc[l][r];
    print_sum(l,k);
    print_sum(k+1,r);
    cout<<sum[r]-sum[l-1]<<" ";
}
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>a[i]; 
        sum[i]=sum[i-1]+a[i];
    }
    memset(f,0x3f,sizeof(f));
    for(int i=1;i<=n;i++) f[i][i]=0;
    for(int t=2;t<=n;t++)
        for(int i=1;i<=n-t+1;i++)
        {
            int j=i+t-1;
            for(int k=i;k<j;k++)
            {
                if(f[i][j]>f[i][k]+f[k+1][j]+sum[j]-sum[i-1])
                {
                    f[i][j]=f[i][k]+f[k+1][j]+sum[j]-sum[i-1];
                    loc[i][j]=k;//记录两点之间的最佳位置 
                }
            }
        }
    print_equa(1,n); 
    cout<<endl<<f[1][n]<<endl;
    print_sum(1,n);
} 
View Code

 能量项链:

【题目描述】
原题来自:NOIP 2006

在 Mars 星球上,每个 Mars 人都随身佩带着一串能量项链。在项链上有 NN 颗能量珠。能量珠是一颗有头标记和尾标记的珠子,这些标记对应着某个正整数。并且,对于相邻的两颗珠子,前一颗珠子的尾标记必定等于后一颗珠子的头标记。因为只有这样,通过吸盘——Mars 人吸收能量的器官的作用,这两颗珠子才能聚合成一颗珠子,同时释放出可被吸盘吸收的能量。如果一颗能量珠头标记为 mm,尾标记为 rr,后一颗能量珠头标记为 rr,尾标记为 nn,则聚合后释放出 m×r×nm×r×n Mars单位的能量,新珠子头标记为 mm,尾标记为 nn。

当需要时,Mars 人就用吸盘夹住相邻的两颗珠子,通过聚合得到能量,直到项链上只剩下一颗珠子为止。显然,不同的聚合顺序得到的总能量是不一样的。请设计一个聚合顺序使得一串珠子聚合后释放出的总能量最大。

例如,设 N=4N=4,四颗珠子头标记与尾标记分别为 (2,3),(3,5),(5,10),(10,2)(2,3),(3,5),(5,10),(10,2)。我们用记号 ⨂⨂ 表示两颗珠子的聚合操作,(j⨂kj⨂k) 表示 j,kj,k 两颗珠子聚合后释放出的能量,则4,14,1两颗珠子聚合后所释放的能量为(41)=10×2×3=60(41)=10×2×3=60,这一串项链可以得到最优值的一个聚合顺序所释放出的总能量为(((41)⨂2)⨂3)=10×2×3+10×3×5+10×5×10=710(((41)⨂2)⨂3)=10×2×3+10×3×5+10×5×10=710
现在给你一串项链,项链上有 nn 颗珠子,相邻两颗珠子可以合并成一个,合并同时会放出一定的能量,不同珠子合并放出能量不相同,请问按怎样的次序合并才能使得释放的能量最多?

【输入】
第一行一个正整数 nn
第二行 nn 个不超过 10001000 的正整数,第 i(1≤i≤n)i(1≤i≤n) 个数为第 ii 颗珠子的头标记,当 i≠ni≠n 时第 ii 颗珠子的尾标记等于第 i+1i+1 颗珠子的头标记,当 i=ni=n 时第 ii 颗珠子的尾标记等于第 11 颗珠子的头标记。

至于珠子的顺序,你可以这样确定:将项链放在桌面上,不要出现交叉,随机指定一颗珠子为第一颗珠子,按顺时针确定其它珠子的顺序。

【输出】
输出只有一行,一个不超过 2.1×1092.1×109 的正整数,表示最优聚合顺序所释放的能量。

【输入样例】
4
2 3 5 10
【输出样例】
710
【提示】
数据范围与提示:

对于 100% 的数据,4≤n≤1004≤n≤100
能量项链

和环形的石子合并题目十分相似

#include <bits/stdc++.h>
using namespace std;
const int N=410,inf=0x3f3f3f;; 
int tail[N],head[N];
int f[N][N],n,x,maxx; 
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>head[i];
        head[i+n]=head[i];//
    }
    for(int i=1;i<=2*n-1;i++) tail[i]=head[i+1];
    tail[2*n]=head[1];
    for(int i=1;i<=2*n;i++) f[i][i]=0;//初始化 
    for(int t=1;t<=n-1;t++)
        for(int i=1;i<=2*n-t;i++)
        {
            int j=i+t;
            for(int k=i;k<=j-1;k++)
            {
                f[i][j]=max(f[i][j],f[i][k]+f[k+1][j]+head[i]*tail[k]*tail[j]);
            }    
        }
    for(int i=1;i<=n;i++)
        maxx=max(maxx,f[i][i+n-1]);
    cout<<maxx<<endl;
    return 0;
}
View Code

 凸多边形的划分:

难!的地方就是在所有的都是高精度计算

【题目描述】
给定一个具有 NN 个顶点的凸多边形,将顶点从 11 至 NN 标号,每个顶点的权值都是一个正整数。将这个凸多边形划分成 N−2N−2 个互不相交的三角形,试求这些三角形顶点的权值乘积和至少为多少。

【输入】
输入第一行为顶点数 NN
第二行依次为顶点 11 至顶点 NN 的权值。

【输出】
输出仅一行,为这些三角形顶点的权值乘积和的最小值。

【输入样例】
5
121 122 123 245 231
【输出样例】
12214884
【提示】
数据范围与提示:

对于 100% 的数据,有 N≤50N≤50,每个点权值小于 109109
凸多边形的划分
#include <bits/stdc++.h>
using namespace std;
const int N=110,inf=0x3f3f3f;;
/*思路跟前面一样,但是这个题需要高精度计算*/
typedef long long ll;
ll f[N][N][N],a[N],s1[N],s2[N],s3[N];
int n;
void Mark(ll c[])
{
    for(int i=1;i<=c[0];i++)
    {
        c[i+1]+=c[i]/10000;
        c[i]%=10000;
    }
    while(c[c[0]+1])
    {
        c[0]++;
        c[c[0]+1]+=c[c[0]]/10000;
        c[c[0]]%=10000;
    }
}
void Add(ll a[],ll b[],ll c[])
{
    if(a[0]>b[0]) c[0]=a[0];
    else c[0]=b[0];
    for(int i=1;i<=c[0];i++) c[i]=a[i]+b[i];
    Mark(c);
}
int Compare(ll a[],ll b[])
{
    if(a[0]<b[0]) return 0;
    if(a[0]>b[0]) return 1;
    for(int i=a[0];i>=1;i--)
        if(a[i]<b[i]) return 0;
        else if(a[i]>b[i]) return 1;
    return 0;
}
void print()
{
    cout<<f[1][n][f[1][n][0]];
    for(int i=f[1][n][0]-1;i>=1;i--)
    {
        cout<<f[1][n][i]/1000;
        cout<<f[1][n][i]/100%10;
        cout<<f[1][n][i]/10%10;
        cout<<f[1][n][i]%10;
    }
    cout<<endl;
}
void Mul(ll a1,ll a2,ll a3,ll c[])
{
    c[0]=c[1]=1;
    for(int i=1;i<=c[0];i++) c[i]*=a1;
    Mark(c);
    for(int i=1;i<=c[0];i++) c[i]*=a2;
    Mark(c);
    for(int i=1;i<=c[0];i++) c[i]*=a3;
    Mark(c);
}
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++) cin>>a[i];
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)
            f[i][j][0]=0;//初始化
    for(int t=2;t<=n-1;t++)
        for(int i=1;i<=n-t;i++)
        {
            int j=i+t;
            f[i][j][0]=60;//这一步不懂? 
            for(int k=i+1;k<=j-1;k++){//枚举中间的位置 
                memset(s1,0,sizeof(s1));
                memset(s2,0,sizeof(s2));
                memset(s3,0,sizeof(s3));
                Mul(a[i],a[k],a[j],s1);//把三个数相乘放进s1中
                Add(f[i][k],f[k][j],s2);//把这两个相加放进s2中
                Add(s1,s2,s3);//把S1与s2加起来放s3中
                if(Compare(f[i][j],s3)){
                    memcpy(f[i][j],s3,sizeof(s3));
                } //就是f[i][j]=min(f[i][j],f[i][k]+f[k][j]+a[i]*a[j]*a[k]); 
            } 
        } 
    print();
    return 0;
}
//其实最难的还是高精度的计算,大体的方向还是知道,但是一些小的细节的地方还是不是很清楚 
View Code

 括号匹配:

比较重要的思想就是一个算式:如果s[i]和s[j]相等的话,就有f[i][j]=f[i+1][j-1]+2;

【题目描述】
Hecy 又接了个新任务:BEBE 处理。BEBE 中有一类被称为 GBEGBE。

以下是 GBEGBE 的定义:

空表达式是 GBEGBE
如果表达式 AA 是 GBEGBE,则 [A][A] 与 (A)(A) 都是 GBEGBE
如果 AA 与 BB 都是 GBEGBE,那么 ABAB 是 GBEGBE。

【输入】
输入仅一行,为字符串 BEBE。

【输出】
输出仅一个整数,表示增加的最少字符数

【输入样例】
[])
【输出样例】
1
【提示】
数据范围与提示:

对于 100% 的数据,输入的字符串长度小于 100100
括号配对
#include <bits/stdc++.h>
using namespace std;
const int N=110,inf=0x3f3f3f;
int f[N][N];
string s;
int main()
{
    cin>>s;
    for(int t=1;t<=s.size();t++)
        for(int i=0;i<=s.size()-t;i++)
        {
            int j=i+t;
            if((s[i]=='('&&s[j]==')')||(s[i]=='['&&s[j]==']'))
                f[i][j]=f[i+1][j-1]+2;
            for(int k=i;k<j;k++)
                f[i][j]=max(f[i][j],f[i][k]+f[k+1][j]);
        }
    cout<<s.size()-f[0][s.size()-1]<<endl;
}
View Code

 分离与合体:

这个题的其实也是典型的区间DP,唯一可能有区别的就是输出,是有次数的,哎,反正开始读题不是很理解这个输出。这是我自己最好理解的输出办法了

【题目描述】
经过在机房里数日的切磋,LYD 从杜神牛那里学会了分离与合体,出关前,杜神牛给了他一个测试……

杜神牛造了 nn 个区域,他们紧邻着排成一行,编号 1..n1..n。在每个区域里都放着一把 OI 界的金钥匙,每一把都有一定的价值,LYD 当然想得到他们了。然而杜神牛规定 LYD 不能一下子把他们全部拿走,而是每次只可以拿一把。为了尽快得到所有金钥匙,LYD 自然就用上了刚学的分离与合体特技。

一开始 LYD 可以选择 1..n−11..n−1 中的任何一个区域进入,我们不妨把这个区域记为 kk。进入后 LYD 会在 kk 区域发生分离,从而分离成两个小 LYD。分离完成的同时会有一面墙在 kk 区域和 k+1k+1 区域间升起,从而把 1..k1..k 和 k+1..nk+1..n 阻断成两个独立的区间,并在各自区间内任选除区间末尾之外(即从 1..k−11..k−1 和 k+1..n−1k+1..n−1中选取)的任意一个区域再次发生分离,这样就有了四个小小 LYD……重复以上所叙述的分离,直到每个小 LYD 发现自己所在的区间只剩下了一个区域,那么他们就可以抱起自己梦寐以求的 OI 金钥匙。

但是 LYD 不能就分成这么多个个体存在于世界上,这些小 LYD 还会再合体,合体的小 LYD 所在区间中间的墙会消失。合体会获得 ((合并后所在区间左右端区域里金钥匙价值之和)×(之前分离的时候所在区域的金钥匙价值))。

例如,LYD 曾在 1..31..3 区间中的 22 号区域分离成为 1..21..23..33..3 两个区间,合并时获得的价值就是 (( 11 号金钥匙价值 +3+3 号金钥匙价值)×( 22 号金钥匙价值))。

LYD 请你编程求出最终可以获得的最大总价值,并按照分离阶段从前到后,区域从左到右的顺序,输出发生分离区域编号。若有多种方案,选择分离区域尽量靠左的方案(也可以理解为输出字典序最小的)。

例如先打印一分为二的区域,然后从左到右打印二分为四的分离区域,然后是四分为八的……

【输入】
第一行一个正整数 nn

第二行 nn 个用空格分开的正整数 aiai ,表示 1..n1..n 区域里每把金钥匙的价值。

【输出】
第一行一个数,表示获得的最大价值

第二行按照分离阶段从前到后,区域从左到右的顺序,输出发生分离区域编号。若有多种方案,选择分离区域尽量靠左的方案(也可以理解为输出字典序最小的)。

【输入样例】
7
1 2 3 4 5 6 7
【输出样例】
238
1 2 3 4 5 6
【提示】
数据范围与提示:

对于 20% 的数据,n≤10n≤10;

对于 40% 的数据,n≤50n≤50;

对于 100% 的数据,n,ai≤300n,ai≤300,保证运算过程和结果不超过 3232 位正整数范围。
1573:分离与合体
//yrnddup c++ code
#include <bits/stdc++.h>
using namespace std;
const int N=305,inf=0x3f3f3f;
int n;
long long f[N][N],a[N],loc[N][N],h[N][N],o[N];
void print(int l,int r,int count)
{
    if(l==r) return;
    h[count][++o[count]]=loc[l][r];
    print(l,loc[l][r],count+1);
    print(loc[l][r]+1,r,count+1);
}
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++)
        cin>>a[i];
    for(int t=1;t<n;t++)
        for(int i=1;i<=n-t;i++)
        {
            int j=i+t;
            for(int k=i;k<j;k++)
                if(f[i][j]<f[i][k]+f[k+1][j]+(a[i]+a[j])*a[k])
                {
                    f[i][j]=f[i][k]+f[k+1][j]+(a[i]+a[j])*a[k];
                    loc[i][j]=k;
                }
        }
    cout<<f[1][n]<<endl;
    print(1,n,1);
    int i=1;
    while(o[i])
    {
        for(int j=1;j<=o[i];j++)
            cout<<h[i][j]<<" ";
        i++;
    }
    return 0;
}
View Code
原文地址:https://www.cnblogs.com/sunny99/p/12644022.html