动态规划之区间DP专题
什么是区间DP
所谓区间dp,就是在一个区间上进行的dp, 一般通过将大区间分割成小区间进行dp。
区间型动态规划,又称为合并类动态规划,是线性动态规划的扩展,它在分阶段地划分问题时,与阶段中元素出现的顺序和由前一阶段的区间中哪些元素合并而来有很大的关系。
区间动归状态转移方程及一般动规过程:
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]可以不要,也可以灵活多变,指的是合并区间时产生的附加值。
【NOI1995】石子合并
【在线测试提交传送门】
【问题描述】
在一个圆形操场的四周摆放N堆石子,现要将石子有次序地合并成一堆。规定每次只能选相邻的2堆合并成新的一堆,并将新的一堆的石子数,记为该次合并的得分。 设计出1个算法,计算出将N堆石子合并成1堆的最小得分和最大得分。
【输入格式】
数据的第1行试正整数N,1≤N≤100,表示有N堆石子.第2行有N个数,分别表示每堆石子的个数。
【输出格式】
输出共2行,第1行为最小得分,第2行为最大得分。
【输入样例1】
4 4 5 9 4
【输出样例1】
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个合并区间有关,是典型的区间型动态规划。
#include<cstdio>
using namespace std;
const int maxn=99999999;
int num[510];
int sum[510][510];//这一次合并得到的分数
int fmax[510][510];//从第i堆石子开始,合并j堆石子得到的最大值
int fmin[510][510];//从第i堆石子开始,合并j堆石子得到的最小值
int main()
{
int n,i,j;
scanf("%d",&n);
for(i=1;i<=n;++i)
{
scanf("%d",&num[i]);
sum[i][1]=num[i];
fmax[i][1]=0;
fmin[i][1]=0;
}
for(j=2;j<=n;++j)
for(i=1;i<=n;++i)
sum[i][j]=num[i]+sum[(i%n)+1][j-1];//转一圈,相当于从谁开始合并都考虑了。
for(j=2;j<=n;++j)
{
for(i=1;i<=n;++i)
{
fmax[i][j]=0;
fmin[i][j]=maxn;
for(int k=1;k<=j-1;++k)
{
int next=((i+k-1)%n)+1;
if(fmax[i][j]<sum[i][j]+fmax[next][j-k]+fmax[i][k])//序列一序列二合并也会得分而且得的分都是sum[i][j]因为加法满足结合律。
fmax[i][j]=sum[i][j]+fmax[next][j-k]+fmax[i][k];
if(fmin[i][j]>sum[i][j]+fmin[next][j-k]+fmin[i][k])
fmin[i][j]=sum[i][j]+fmin[next][j-k]+fmin[i][k];
}
}
}
//哪个位置当第一次合并
int min=maxn ,max=0;
for(i=1;i<=n;++i)
{
if(min>fmin[i][n])
min=fmin[i][n];