发现一篇好文,可惜发现有一些地方有排版问题。于是改了一下,并加了一些自己的内容。
对区间DP和其变式环形DP的总结。
首先先来例题。
石子归并
题目描述 Description
有n堆石子排成一列,每堆石子有一个重量w[i], 每次合并可以合并相邻的两堆石子,一次合并的代价为两堆石子的重量和w[i]+w[i+1]。问安排怎样的合并顺序,能够使得总合并代价达到最小。
输入描述 Input Description
第一行一个整数n(n<=100)
第二行n个整数w1,w2...wn (wi <= 100)
输出描述 Output Description
一个整数表示最小合并代价
样例输入 Sample Input
4
4 1 1 4
样例输出 Sample Output
18
区间DP的模型很显然,特征就是所给的数据是一条链或者一条环(环形DP)。
划分问题:
我们所求区间\([1,n]\)的最小合并代价,而\(dp[1][n]\)必然是有两段区间合并而成\(dp[1][n] = {dp[1][k]+dp[k+1][n]+sum[l][r]}\)
重写DP方程:\(dp[1][n] = {dp[1][k]+dp[k+1][n] + sum[n]-sum[0]}(1\le k<n)\)
因为我们不知道\([1,n]\)最终是由哪段合并而来的,所以我们必然要枚举断点\(k\).
我们发现,在计算\([1,n]\)时我们需要已经计算完成了的\([1,k],[k+1,n]\)
所以我们在枚举到\(n\)之前必须先把其前面的区间计算完成。然后再从n开始反向枚举左端点,比如计算\([1,5]\)区间且断点枚举到3的时候,我们必须已经计算出区间\([1,3]\)和区间\([4,5]\)。区间[1,3]好办,因为我们的r是顺着枚举的,区间\([4,5]\)要计算在前面也好办,内层循环反着枚举。(看代码)
我们归纳出一般性的方程\(dp[l][r] = {dp[l][k]+dp[k+1][r]+sum[r]-sum[l-1]}(l\le k\le r)\)
状态定义\(dp[l][r]\):区间\([l,r]\)合并的最小代价,(状态必须是二维,因为我们要表示一个区间,区间的特征是由左界/右界来表现)
代码演示如下:
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
using namespace std;
int n,w[101];
int f[101][101];//f[i][j]表示区间[i,j]合并的最小代价
int sum[101] ;//预处理i→j的合并代价 (降低算法复杂度)
inline void dp()
{
for(int j=2; j<=n; j++)
for(int i=j-1; i>=1; i--)
{
f[i][j]=1e7;//定义一个极大值
for(int k=i; k<j; k++)
f[i][j] = min(f[i][j],f[i][k] + f[k + 1][j] + sum[j] - sum[i - 1]);
//状态转移方程 f[i][j] = min{f[i][k] + f[k + 1][j] + sum[j] - sum[i - 1]} | i <= k < j
}
}
int main()
{
scanf("%d",&n);
for(int i=1; i<=n; i++)
{
scanf("%d",&w[i]);
sum[i]=sum[i-1]+w[i];
}
dp();
printf("%d",f[1][n]);
return 0;
}
当然,我们还有另一种写法。描述一个区间的状态,我们也可以用左端点+长度来表示。当然状态定义还是不变(因为要记录的时候\(dp[l][l+len]\)等价于\(dp[l][r]\)),只是枚举的方式要改变。这里就不写全,只给出核心部分。
下面代码的执行逻辑和上面的有点区别,先算出任意相邻两段的合并代价,然后算任意相邻3段的合并代价。
因为我们在算3段的时候,例如\([1,3]\)我们已经算出了\([1,2]\),\([2,3]\)的合并代价,所以可以直接递推。
for(int len = 2; len <= n; len++) //长度
for(int l = 1; l+len-1<=n; l++) //左端点
{
int r = l+len-1;
for(int k=l; k<r; k++) //断点
dp[l][r] = max(dp[l][r],dp[l][k]+dp[k+1][r]+sum[r]-sum[l-1]);
}
答案是\(dp[1][n]\)。
区间DP的写法一般就这两种形式,选一种你喜欢的方式记下即可。状态定义一般也是二维[l,r]
好,然后我们再来看下一道例题
[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
数据由链变成了环,难道我们就没法处理了?
这里有一种技巧叫做 “断环为链”
将链复制一倍 a[i+n] = a[i]
然后我们控制枚举的长度n即可。看代码实现。
最后算答案的时候再枚举一遍起点和固定长度\(n\),\(dp[l][l+n-1](1\le l\le n)\)。
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
using namespace std;
int n,w[101];
int f[201][201];//f[i][j]表示区间[i,j]合并的最小代价
int g[201][201];//f[i][j]表示区间[i,j]合并的最大代价
int sum[201] ;//预处理i→j的合并代价 (降低算法复杂度)
int ans_min=1e7;
int ans_max=0;
inline void dp()
{
for(int j=2; j<=n+n; j++)
for(int i=j-1; i>=1&&j-i<n; i--)
{
f[i][j]=1e7;//定义一个极大值
for(int k=i; k<j; k++)
{
f[i][j] = min(f[i][j],f[i][k] + f[k + 1][j] + sum[j] - sum[i - 1]);
//状态转移方程 f[i][j] = min{f[i][k] + f[k + 1][j] + sum[j] - sum[i - 1]} | i <= k < j
g[i][j] = max(g[i][j],g[i][k] + g[k + 1][j] + sum[j] - sum[i - 1]);
}
if(ans_max<g[i][j])ans_max=g[i][j];
}
}
int main()
{
scanf("%d",&n);
for(int i=1; i<=n; i++)
{
scanf("%d",&w[i]);
w[i+n]=w[i];
}
for(int i=1; i<=2*n; i++)
{
sum[i]=sum[i-1]+w[i];
}
dp();
for(int i=1; i<=n; i++)
{
if(ans_min>f[i][i+n-1])ans_min=f[i][i+n-1];
}
printf("%d\n",ans_min);
printf("%d\n",ans_max);
return 0;
}
上面代码是用第一种方式写的,你也很容易用第二种(左端点+长度)的方式写出来。这里就不给出代码了。
啊,顺手就写了一份,好吧,还是把代码贴出来吧。不过希望大家认真思考,DP的题看到代码和方程可能很容易懂,但难度在于想到代码和方程,或者是定义状态。初学者可以先学习,过一段时间后再来做。复习的人就不要看代码啦。
#include<cstdio>
#include<iostream>
#include<cmath>
#include<algorithm>
#include<cstring>
using namespace std;
int n;
int w[220],sum[220];
int f[220][220];
int g[220][220];
int main()
{
scanf("%d",&n);
for(int i=1; i<=n; i++)
{
scanf("%d",&w[i]);
w[i+n] = w[i];
}
for(int i=1; i<=2*n; i++)
sum[i]=w[i]+sum[i-1];
for(int len = 2; len <= n; len++)
{
for(int l=1; l<=2*n-len+1; l++) //这里要注意
{
int r = l+len-1;
f[l][r]=1e9;
for(int k=l; k<r; k++)
{
f[l][r] = min(f[l][r],f[l][k]+f[k+1][r]+sum[r]-sum[l-1]);
g[l][r] = max(g[l][r],g[l][k]+g[k+1][r]+sum[r]-sum[l-1]);
}
}
}
int ans_max = 0,ans_min = 1e9;
for(int i=1; i<=n; i++)
{
ans_min = min(ans_min,f[i][i+n-1]);
ans_max = max(ans_max,g[i][i+n-1]);
}
cout<<ans_min<<endl;
cout<<ans_max;
return 0;
}