邱老师的博客

致力于OI

区间DP
区间型动态规划,又称为合并类动态规划,是线性动态规划的扩展,它在分阶段地划分问题时,与阶段中元素出现的顺序和由前一阶段的区间中哪些元素合并而来有很大的关系。如对于状态f[i,j],它表示划分的阶段为j,状态出现的位置为i,它的值取决于第i个元素出现的位置和i到j这段区间的值。这一类型的天天过后,阶段特征非常明显,求最优值时需预先设置阶段内的区间统计值,还要分动态规划的起始位置来判断。
    区间型动态规划在信息学竞赛中应用甚广,它是动态规划中的经典问题,最小代价字母树是这类动态规划最经典的体现,对于初学者而言这类动态规划并不太好理解。于是,区间型动态规划又成了动态规划中的难点问题。
    区间型动态规划是各大信息竞赛出题的热点,具体体现在以下题目:
        1). 合并石子---- NOI1995 
        2). 能量项链--- NOIP2006 
        3). 乘积最大---- NOIP2000 
        4). 加分二叉树---- NOIP2003 
        5). 最优排序二叉树---- CTSC96
区间动归状态转移方程及一般动规过程:
for k:=1 to n-1 do    //区间长度
  for i:=1 to n-k do     //区间起点
    for j:=i to i+k-1 do     //区间中任意点
      dp[i,i+k]:=max{dp[i,j] + dp[j+1,i+k] + a[i,j] + a[j+1,i+k]};

    1. 状态转移方程字面意义:寻找区间dp[i,i+k]的一种合并方式dp[i,j] + dp[j+1,i+k],使得其值最大或最小。
    2. 区间长度k必须要放到第一层循环,来保证方程中状态dp[i,j]、dp[j+1,i+k]值在dp[i,i+k]之前就已计算出来。
    3. 其中a[i,j]+a[j+1,i+k]可以不要,也可以灵活多变,指的是合并区间时产生的附加值。

【例题1】石子合并

在一个园形操场的四周摆放n堆石子,现要将石子有次序地合并成一堆.规定每次只能选相邻的2堆合并成新的一堆,并将新的一堆的石子数,记为该次合并的得分。 试设计出1个算法,计算出将n堆石子经过n-1次合并成1堆的最小得分和最大得分。
输入格式:
    数据的第1行试正整数n,1≤n≤500,表示有n堆石子。第2行有n个数,分别表示每堆石子的个数。
输出格式:
    输出共2行,第1行为最小得分,第2行为最大得分。
样例输入:
    4
    4 5 9 4 
样例输出
    43
    54
【分析】
    看到题目,很像贪心,采用尽可能逼近目标的贪心法来逐次合并。如样例数据,从最上面一堆开始,沿顺时针方向排成一个环,要计算得分最小时,第一次去相邻得分最小的4,4两堆合并,得分为8分,合并后剩下3堆,分别为8 5 9;再选择相邻得分最小的5,8合并,得13分,最后选9和13合并,得22分,总共分数为43,与样例输出一样,好像这种贪心是正确的。
    其实不然,这种策略存在反例。如6堆石子,从最上面一堆顺时针数起,石子数分别为3,4,6,5,4,2,采用贪心策略合并时,合并过程如下:

    但是经过反复推敲,可以得到一种总得分更小的合并方案,合并过程如图2。

    显然,图2的合并方案更优。样例数据其实是一种用贪心策略可以解决问题的假象,导致很多选手使用了贪心策略,从而丢了很多分。下面来分析第二种合并方案。从后往前推,有图2可以看出,6堆石子合并的最小得分方案min{merge(1,2,3,4,5,6)}是由merge(1,2,3)和merge(4,5,6)得来的,上述中,merge表示合并, 1,2,3,…表示第1,2,3,…堆的石子。merge(1,2,3)时,它有两种合并方案,即先merge(1,2)两堆,再将1,2合并得结果与第3堆合并,merge(merge(1,2),3);或者先merge(2,3),然后merge(1,merge(2,3))。
    第一种合并方案(3+4)+6的合并得分为7+13=20;
    第二种合并方案3+(4+6)的合并得分为10+13=23。
    明显第一种方案得分少。merge(4,5,6)时,也同样有两种方案。合并1到6堆得过程如下:
        合并1堆:1,2,…,6;
        合并2堆:12,23,…,61;
        合并3堆:123,234,…,612;
        合并4堆:1234,2345,…,6123;
        合并5堆:12345,23456,…,61234;
        合并6堆:123456,234561,…,612345。
    因此,从第i堆开始合并到第j堆时,它的值可以为第i堆+min{merge(i+1,i+2,…,i+j-1)}。如从第1堆开始合并4堆时,它可以为第1堆+min{merge(2,3,4)},也可以为min{merge(1,2)+merge(3,4)},也可以为min{merge(1,2,3)}+第4堆,共3种来源,与区间的合并有关。依次类推,合并到6堆时,去从第i堆开始合并6堆得最小值,即得到合并得总的最小值。所以,此问题具备最优子结构的性质,而且无后效性
    阶段:合并得堆数。
    状态:dp[i,j]。表示从第i堆起,顺时针合并j堆得总得分最小值,它包括合并前j-1堆的最小总得分加上这次合并得得分,用sum[i,j]表示这次合并得得分。合并时的堆数可以表示为序列{第i堆,第i+1堆,….,第(i+j-2) mod n+1堆}。序列总得来的方案有很多种,我们用子序列1和子序列2表示,如子序列1为{第i堆},子序列2为{第i+1堆,…,第(i+j-2)mod n+1堆}。子序列1和子序列2相邻,所以,假如子序列1为k堆,则子序列2为j-k堆。由此,可以得到动规方程:
      dp[i,j]=min{dp[i,k]+dp[i+k,j-k]+sum[i,j] , 1<=k<=j-1}
    stone[i]表示初始时每堆的石子数,则动规的初始条件为:dp[i,1]=0
    动规的边界为1<=i<n,1<=j<=n。求最大得分与最小得分方法一样,只是在计算时反一下就可以了。
从石子合并得算法来看,状态dp[i,j]与合并时的起始位置和前j-1个合并区间有关,是典型的区间型动态规划。
【例题2】乘积最大
今年是国际数学联盟确定的“2000——世界数学年”,又恰逢我国著名数学家华罗庚先生诞辰90周年。在华罗庚先生的家乡江苏金坛,组织了一场别开生面的数学智力竞赛的活动,你的一个好朋友XZ也有幸得以参加。活动中,主持人给所有参加活动的选手出了这样一道题目: 
    设有一个长度N的数字串,要求选手使用K个乘号将它分成K+1个部分,找出一种分法,使得这K+1个部分的乘积能够为最大。 
    同时,为了帮助选手能够正确理解题意,主持人还举了如下的一个例子: 
    有一个数字串: 312,当N=3,K=1时会有以下两种分法: 
        1)3*12=36 
        2)31*2=62 
    这时,符合题目要求的结果是: 31*2=62 
    现在,请你帮助你的好朋友XZ设计一个程序,求得正确的答案。
输入格式:
    程序的输入共有两行: 
    第一行共有2个自然数N,K (6<=N<=40,1<=K<=30) 
    第二行是一个长度为N的数字串。
输出格式:
    相对于输入,应输出所求得的最大乘积(一个自然数)。 
样例输入:
    4 2
    1231 
样例输出:
    62

【分析】
    从题意来看,“*”号的插入方式非常重要。比如样例,如果插入位置为1*23*1时,结果为23;插入位置为12*3*1时,结果为36;插入位置为1*2*31时,结果为62,这种方式的值最大。从这点来看,本题与石子合并非常相像。设输入的数字串味s,在s1,...,si(2<=i<=n)中插入j个“*”时,假设在s1,..,sk中插入了j-1个“*”号,则乘式中第j个“*”号后边的式子sk+1,..,si为常量;设f[i,j]表示在长度为i的数字串中插入j个“*”的最大乘积,要得到f[i,j]的最大值时,就要得到max{f[k,j-1]*sk+1...sn (j<=k<=i-1)}的值;一一枚举k的位置,即可得到max{f[k,j-1]*sk+1..sn}的值。最后输出f[n,m]的值即可。显然,这一问题具备最优子结构的性质,且无后效性,也是区间型动规类问题。
    阶段:数字串的长度;
    状态:长度为i的数字串中插入的“*”的个数;
    决策:第j个“*”的最佳插入位置。
【例题3】能量项链
在Mars星球上,每个Mars人都随身佩带着一串能量项链。在项链上有N颗能量珠。能量珠是一颗有头标记与尾标记的珠子,这些标记对应着某个正整数。并且,对于相邻的两颗珠子,前一颗珠子的尾标记一定等于后一颗珠子的头标记。因为只有这样,通过吸盘(吸盘是Mars人吸收能量的一种器官)的作用,这两颗珠子才能聚合成一颗珠子,同时释放出可以被吸盘吸收的能量。如果前一颗能量珠的头标记为m,尾标记为r,后一颗能量珠的头标记为r,尾标记为n,则聚合后释放的能量为m*r*n(Mars单位),新产生的珠子的头标记为m,尾标记为n。 需要时,Mars人就用吸盘夹住相邻的两颗珠子,通过聚合得到能量,直到项链上只剩下一颗珠子为止。显然,不同的聚合顺序得到的总能量是不同的,请你设计一个聚合顺序,使一串项链释放出的总能量最大。
    例如:设N=4,4颗珠子的头标记与尾标记依次为(2,3) (3,5) (5,10) (10,2)。我们用记号⊕表示两颗珠子的聚合操作,(j⊕k)表示第j,k两颗珠子聚合后所释放的能量。则第4、1两颗珠子聚合后释放的能量为:
        (4⊕1)=10*2*3=60。
    这一串项链可以得到最优值的一个聚合顺序所释放的总能量为
        ((4⊕1)⊕2)⊕3)=10*2*3+10*3*5+10*5*10=710。
输入格式:
    输入文件的第一行是一个正整数N(4≤N≤100),表示项链上珠子的个数。第二行是N个用空格隔开的正整数,所有的数均不超过1000。第i个数为第i颗珠子的头标记(1≤i≤N),当1≤i<N时,第i颗珠子的尾标记应该等于第i+1颗珠子的头标记。第N颗珠子的尾标记应该等于第1颗珠子的头标记。
至于珠子的顺序,你可以这样确定:将项链放到桌面上,不要出现交叉,随意指定第一颗珠子,然后按顺时针方向确定其他珠子的顺序。
输出格式:
    输出文件只有一行,是一个正整数E(E≤2.1*10^9),为一个最优聚合顺序所释放的总能量。
样例输入:
    4
    2 3 5 10 
样例输出:
    710

【分析】
    很多选手在比赛时认为确定第一颗珠子后,必须按顺序合并所有的珠子,使用贪心策略,逐一枚举所有珠子为第一颗,选择最大的方案输出。恰好样例也符合这种逻辑,导致很多同学只得了30分。其实这道题并未要求按顺序合并,知识告诉按顺时针方向摆放珠子,合并时可以任意两个珠子合并,只要总能量最大即可。
    题意为:给你一串项链,项链上有n颗珠子,相邻的两颗珠子可以合并(两个合并成一个),合并得同时会放出一定的能量。不同珠子的合并所释放的能量是不同的。<br.    问:按照怎样的次序合并才能使释放的能量最多?
     用head表示第i颗珠子的头标记,用tail表示尾标记,合并两颗相邻珠子所释放的能量为:energy=head[i]*tail[i]*tail[i+1]
    合并时不一定按输入顺序合并,与石子合并问题类似,第n次合并,可以归结到第n-1次合并,具有明显地动规性质。用f[I,j]表示从第i颗珠子合并到第j颗珠子时产生的最大能量,用k表示最后一次的合并位置,则有:  dp[i,j]=max{dp[i,k]+dp[k+1,j]+head[i]*tail[k]*tail[j] , i<=k<=j}    上式中,dp[i,k]表示第i颗到第k颗珠子产生的最大能量,dp[k+1,j]表示合并第k+1颗到第j颗时产生的最大能量,head[i]*tail[k]*tail[j]表示最后一次合并时产生的能量。dp[i,j]的值,分成两个区间,取最大值,是典型的区间型动规。最后一次合并时,产生的能量为什么是head[i]*tail[k]*tail[j]呢?假设有5颗珠子,每颗珠子的能量为10,2,3,5,6,当i=1,j=4,k=2时,如图3:


    由图3可以看出,合并dp[1,2],dp[3,4]后,还剩下1,3,5三颗珠子(从最上面开始顺时针数),此时1号珠子head[1]=10,tail[1]=3,相当于原图的tail[2];3号珠子tail[3]=6,相当于原图的tail[4]。最后合并dp[1,4]时,相当于合并1,3两颗珠子,产生的能量为最右边图的10*3*6,相当于原图中的head[1]*tail[2]*tail[4],即为上式中的head[i]*tail[k]*tail[j]。
    由于项链是一个环,我们把项链以2*n-1长度,一水平线的形式平铺在桌面上,从左到右逐一扫描,得出最大值。


【例题4】牢房
SB王国中有一个奇怪的监狱,这个监狱里一共有P间牢房,这些牢房一字排开,从左到右按1到P进行编号,第i间后面紧挨着第(i+1)间(最后一间除外)。现在有P名罪犯被关押在这P间牢房里。某一天,上级下发了一个释放名单,要求每天释放名单上的一个人。这可把监狱中的看守们吓得不轻,因为看守们知道,现在牢房中的P个人,可以相互之间传话。如果某个人离开了,那么原来和这个人能够传上话的人都会很气愤,导致他们那天会一直大吼大叫,搞得看守们很是头疼,但是如果给这些要发火的人吃上肉,他们就会安静。现在看守们想知道,如何安排释放的顺序,才能使得他们花费的肉钱最少。1<=p<=1000,1<=Q<=100
输入
    第一行两个数P和Q,Q表示要释放名单上的人数;
    第二行Q个数,表示释放哪些人
输出
    仅一行,表示最少给多少人次送肉.
样例输入
    20 3
    3 6 14
样例输出
    35

【提示】:(样例说明)
    先释放14号监狱中的罪犯,要给1到13号监狱和15到20号监狱中的19人送肉吃;再释放6号监狱中的罪犯,要给1到5号监狱和7到13号监狱中的12人送肉吃;最后释放3号监狱中的罪犯,要给1到2号监狱和4到5号监狱中的4人送肉吃。
【分析】:用DP[i][j]表示在[i,j]区间所有该被释放的人都被释放的最小支付代价。
若是释放k,那么代价为DP[i][k-1]+DP[k+1][j]+(j-i),所以动态转移方程为:
 DP[i][j]=min(DP[i][k-1]+DP[k+1][j]+(j-i)).
#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm> 
using namespace std;
const int MAX=1010;
const int Inf=100000000;
int n,m,a[MAX],sum[MAX],DP[MAX][MAX];
int main()
{   for(int i=0;i<MAX;i++) 
    {   fill(DP[i],DP[i]+MAX,Inf);
        DP[i][i]=0; 
    } 
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++) scanf("%d",&a[i]);
    sort(a+1,a+m+1);  
    for(int i=1;i<=m;i++) sum[i]=a[i]-a[i-1]-1;
    sum[m+1]=n-a[m];
    for(int i=1;i<=m+1;i++) sum[i]=sum[i]+sum[i-1];
    for(int i=m+1;i>=1;i--)
        for(int j=i+1;j<=m+1;j++)
           for(int k=i;k<j;k++) DP[i][j]=min(DP[i][j], DP[i][k]+DP[k+1][j]+sum[j]-sum[i-1]+j-i-1);
    printf("%d\n",DP[1][m+1]);
    return 0;
}
//bzoj1260涂色#include <iostream>#include <algorithm>#include <cstdio>#include <cstring>#include <cstdlib>using namespace std;char in[1000];int dp[1000][1000];int main(){ scanf("%s",in+1);//方便下面dp int n=strlen(in+1); for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)dp[i][j]=i==j?1:0x3fffffff;//对于一个点的涂色次数,只能是1次 for(int len=1;len<=n;len++)//循环长度 { for(int l=1,r;(r=l+len)<=n;l++)//生成左右区间 { if(in[l]==in[r])//如果相等那么就可以如下这么转移咯 { if(len==1)dp[l][r]=1;//如果区间长度为1,也就是说l,r是相邻的两个格子,所以只能一笔涂上 else dp[l][r]=min(dp[l+1][r-1]+1,min(dp[l][r-1],dp[l+1][r])); /* 否则的话说明可以从区间l,r中进行转移。 判断dp[l][r]所包含的三个子区间 然后就是dp[l+1][r]跟dp[l][r-1]了。 先把最右端/最左端为起点一笔涂到另一头,应该是这意思吧? */ } else//否则的话只能将区间l,r分割成两个小区间然后min咯 for(int i=l;i<r;i++) dp[l][r]=min(dp[l][r],dp[l][i]+dp[i+1][r]); } } printf("%d\n",dp[1][n]);//答案}


作者:KingSann
链接:https://www.jianshu.com/p/24feb3ccaf2e
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
//bzoj1260涂色#include <iostream>#include <algorithm>#include <cstdio>#include <cstring>#include <cstdlib>using namespace std;char in[1000];int dp[1000][1000];int main(){ scanf("%s",in+1);//方便下面dp int n=strlen(in+1); for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)dp[i][j]=i==j?1:0x3fffffff;//对于一个点的涂色次数,只能是1次 for(int len=1;len<=n;len++)//循环长度 { for(int l=1,r;(r=l+len)<=n;l++)//生成左右区间 { if(in[l]==in[r])//如果相等那么就可以如下这么转移咯 { if(len==1)dp[l][r]=1;//如果区间长度为1,也就是说l,r是相邻的两个格子,所以只能一笔涂上 else dp[l][r]=min(dp[l+1][r-1]+1,min(dp[l][r-1],dp[l+1][r])); /* 否则的话说明可以从区间l,r中进行转移。 判断dp[l][r]所包含的三个子区间 然后就是dp[l+1][r]跟dp[l][r-1]了。 先把最右端/最左端为起点一笔涂到另一头,应该是这意思吧? */ } else//否则的话只能将区间l,r分割成两个小区间然后min咯 for(int i=l;i<r;i++) dp[l][r]=min(dp[l][r],dp[l][i]+dp[i+1][r]); } } printf("%d\n",dp[1][n]);//答案}


作者:KingSann
链接:https://www.jianshu.com/p/24feb3ccaf2e
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
//bzoj1260涂色#include <iostream>#include <algorithm>#include <cstdio>#include <cstring>#include <cstdlib>using namespace std;char in[1000];int dp[1000][1000];int main(){ scanf("%s",in+1);//方便下面dp int n=strlen(in+1); for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)dp[i][j]=i==j?1:0x3fffffff;//对于一个点的涂色次数,只能是1次 for(int len=1;len<=n;len++)//循环长度 { for(int l=1,r;(r=l+len)<=n;l++)//生成左右区间 { if(in[l]==in[r])//如果相等那么就可以如下这么转移咯 { if(len==1)dp[l][r]=1;//如果区间长度为1,也就是说l,r是相邻的两个格子,所以只能一笔涂上 else dp[l][r]=min(dp[l+1][r-1]+1,min(dp[l][r-1],dp[l+1][r])); /* 否则的话说明可以从区间l,r中进行转移。 判断dp[l][r]所包含的三个子区间 然后就是dp[l+1][r]跟dp[l][r-1]了。 先把最右端/最左端为起点一笔涂到另一头,应该是这意思吧? */ } else//否则的话只能将区间l,r分割成两个小区间然后min咯 for(int i=l;i<r;i++) dp[l][r]=min(dp[l][r],dp[l][i]+dp[i+1][r]); } } printf("%d\n",dp[1][n]);//答案}


作者:KingSann
链接:https://www.jianshu.com/p/24feb3ccaf2e
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
//bzoj1260涂色 
#include <iostream> 
#include <algorithm> 
#include <cstdio> 
#include <cstring> 
#include <cstdlib> 
using namespace std; 
char in[1000]; 
int dp[1000][1000]; 
int main() 
{ 
	scanf("%s",in+1);//方便下面dp 
	int n=strlen(in+1); 
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
			dp[i][j]=i==j?1:0x3fffffff;//对于一个点的涂色次数,只能是1次 
			for(int len=1;len<=n;len++)//循环长度 
			{ 
				for(int l=1,r;(r=l+len)<=n;l++)//生成左右区间 
				{ 
					if(in[l]==in[r])//如果相等那么就可以如下这么转移咯 
					{ 
						if(len==1)dp[l][r]=1;//如果区间长度为1,也就是说l,r是相邻的两个格子,所以只能一笔涂上 
						else dp[l][r]=min(dp[l+1][r-1]+1,min(dp[l][r-1],dp[l+1][r])); 
						/* 否则的话说明可以从区间l,r中进行转移。 判断dp[l][r]所包含的三个子区间 然后就是dp[l+1][r]跟dp[l][r-1]了。 先把最右端/最左端为起点一笔涂到另一头,应该是这意思吧? */ } 
						else//否则的话只能将区间l,r分割成两个小区间然后min咯 
							for(int i=l;i<r;i++) dp[l][r]=min(dp[l][r],dp[l][i]+dp[i+1][r]); 
							} 
						} 
					printf("%d\n",dp[1][n]);//答案 
}

//tyvj P1193 括号序列
/* 至于为什么这个dp是对的,小谈一下我的个人理解。 由于区间长度len是
从小到的枚举的,所以每一轮都是从很小的单位区间开始更新,似乎可以看作bfs(雾)? 因为len
是由小到大进行枚举,所以每一次min的时候都是从将已经判断好的子区间进行min。 或许用滚雪球
形容会好点?从一个小区间慢慢滚成了一个大区间? 以上就是我对于初级区间dp的理解。。。 */ 
#include <cstring> 
#include <iostream> 
#include <algorithm> 
#include <cstdio> 
using namespace std; 
char in[1000]; 
int dp[1000][1000],n; 
bool test(char a,char b)//判断a,b是否匹配 
{ 
	if(a=='(')return b==')'; 
	if(a=='[')return b==']'; 
	if(a=='<')return b=='>'; 
	if(a=='{')return b=='}'; 
	return 0; 
} 
int main() 
{ 
/* dp[i][i]=1 若是单个括号的话只能是添加所对应的括号完成匹配,所以为1 dp[l][r]:区间l,r的
最小添加括号数量 dp[l][r]=min{n,dp[l+1][r-1](match(s[l],s[r]),dp[l][k]+dp[k+1][r]|l<=k<r} */
	gets(in+1);//为了能够顺手的写代码,所以下标从1开始 
	n=strlen(in+1); 
	while(in[n]=='\n'||in[n]=='\r') n--; //不知道为啥gets会莫名其妙的读入一个换行/回车,所以第一次就WA了。。。所以加上这个while判一下是否有换行,不过似乎最多就循环一次? 
	for(int i=1;i<=n;i++)dp[i][i]=1; 
	for(int len=1;len<=n;len++) 
	{ 
		for(int l=1,r;(r=l+len)<=n;l++) 
		{ 
			dp[l][r]=r-l+1;//边界,对于区间l,r一共有r-l+1个括号,所以最坏情况下需要添加r-l+1个括号,不过似乎写成n也能A掉?好吧写成dp[l][r]=n会快点。。。毕竟避免了加减法运算 
			if(test(in[l],in[r]))//如果左右两端匹配,那么就可以进行一次转移 
				dp[l][r]=min(dp[l][r],dp[l+1][r-1]);//如果当前区间左右都是匹配的括号的话,状态转移为dp[l+1][r-1],也就是说转移到去掉括号后的序列 
			for(int i=l;i<r;i++)//枚举区间l,r中的所有区间 
				dp[l][r]=min(dp[l][r],dp[l][i]+dp[i+1][r]); 
		} 
	} 
	printf("%d\n",dp[1][n]);//最后的结果就是dp[1][n] 
}



阅读更多
文章标签: 提高组
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

不良信息举报

区间DP

最多只允许输入30个字

加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!
关闭
关闭