本文从普通区间dp、平行四边形优化区间dp、例题三个步骤,详细分析了区间动态规划!!!
编写不宜,希望各位兄台耐心阅读完整!!!
1.区间dp
区间dp
其实就是一种建立在线性结构上
的整体上对区间
的动态规划- 区间上dp,大多数题目的状态都是由区间(类似于
dp[l][r]
这种形式)构成的,就是我们可以把大区间转化成小区间
来处理,然后对小区间递推处理求出大区间的值,主要的方法有两种,记忆化搜索和递推。 - 区间DP模版(三要素):
- 区间长度
- 区间起点、终点(由
长度
与起点
推出终点
、最后一个起点
的位置) - 分割
for(int len=2;len<=n;len++) { //枚举长度
for(int i=1;i<=n-len+1;i++) { //枚举起点
int j=i+len-1; //由起点推导终点
dp[i][j]=Integer.MAX_VALUE; //初始化
for(int k=i;k<j;k++) { //枚举由k到j的每个分割点
//状态转移方程
dp[i][j]=Math.min(dp[i][j], (dp[i][k]+dp[k+1][j]+sum[j]-sum[i-1]));
}
}
}
- 如果我们要得知一个大区间的情况,由于它必定是由从多个长度不一的小区间转移而来(转移情况未知),我们可以通过求得多个小区间的情况,从而合并信息,得到大区间。这样的算法复杂度为
O(N^3)
。
2.平行四边形优化
-
在动态规划中,经常遇到形如下式的状态转移方程:
dp(i,j)=min{dp(i,k-1),dp(k,j)}+cost(i,j)(i≤k≤j)
(min也可以改为max)上述的
dp(i,j)
表示区间[i,j]上的某个最优值(符合条件最优解的取值)。cost(i,j)
表示在转移时需要额外付出的代价。该方程的时间复杂度为O(N^3) -
下面我们通过四边形不等式来优化上述方程,首先介绍什么是
“区间包含的单调性”
和“四边形不等式”
1.区间包含的单调性:如果对于i≤i'<j≤j'
,有cost(i',j)≤cost(i,j')
,那么说明 cost 具有区间包含的单调性
。(可以形象理解为如果小区间包含于大区间中,那么小区间的cost值不超过大区间的cost值 ),证明可通过题目定义的cost(i,j)
的含义进行论证2、四边形不等式:如果对于
i≤i'<j≤j'
,有cost(i,j)+cost(i',j')≤cost(i',j)+cost(i,j')
,我们称函数 cost 满足四边形不等式。(可以形象理解为两个交错区间的cost的和不超过小区间与大区间的cost的和——(交叉小于包含
))证明
: 可以采用函数单调性进行证明,eg:
显然,当且仅当对于所有的i,j,均满足如下式子
(令i< i+1<= j< j+1)
cost[i][j]+cost[i+1][j+1]<=cost[i][j+1]+cost[i+1][j]
(利用交叉小于包含
写出来的式子)
转移一下式子
即要证明对于所有的i,j,要使
cost[i][j]-cost[i+1][j]<=cost[i][j+1]-cost[i+1][j+1]
试想一个函数f(j)=cost[i][j]-cost[i+1][j],
那么要证明这个四边形不等式成立,也就是要证明这个函数f(j)单调递增
-
两个定理:
1、如果上述的cost 函数
同时满足区间包含单调性
和四边形不等式性质
,那么函数 dp
也满足四边形不等式性质
我们再定义s(i,j)
表示dp(i,j) 取得最优值时对应的下标k
(即 i≤k≤j 时,k 处的 dp 值最最优,则 s(i,j)=k)。此时有如下定理:
2、假如 dp(i,j) 满足四边形不等式,那么s(i,j)
(自变量为i、j两个变量) 单调,即s(i,j)≤s(i,j+1)≤s(i+1,j+1)
。 -
推导:
有了上述的两个定理后,我们发现如果cost函数
满足区间包含单调性和四边形不等式性质,那么有s(i,j-1)≤s(i,j)≤s(i+1,j)
。
即原来的状态转移方程
可以改写为下式:
dp(i,j)=min{dp(i,k-1),dp(k,j)}+cost(i,j)(s(i,j-1)≤k≤s(i+1,j))
(min也可以改为max)将
k
的枚举区间由[i,j-1)
优化到了[s[i][j-1],s[i+1][j]]
,其中s[][]
的值由之前的第三重循环就已经找到了,区间的长度最多有n个,对于固定的长度 L,不同的状态也有 n 个,故时间复杂度为 O(N^2),而原来的时间复杂度为 O(N^3),实现了优化!
今后只需要根据方程的形式
以及cost 函数
是否满足两条性质
即可考虑使用四边形不等式
来优化了。 -
平行四边形优化区间DP模版:
for(int len=2;len<=n;len++) { //枚举长度
for(int i=1;i<=n-len+1;i++) {
int j=i+len-1; //根据起点推终点
dp[i][j]=Integer.MAX_VALUE; //初始化值
for(int k=s[i][j-1];k<=s[i+1][j];k++) { //通过平行四边形优化,缩小枚举范围
if(dp[i][k]+dp[k+1][j]+cost[j]-cost[i-1]<dp[i][j]) { //换成if语句,可以减少不必要的循环赋值
dp[i][j]=dp[i][k]+dp[k+1][j]+cost[j]-cost[i-1];
s[i][j]=k; //记录最优分割点
}
}
}
}
3.例题(合并石子)
问题描述
在一条直线上有n堆石子,每堆有一定的数量,每次可以将两堆相邻的石子合并,合并后放在两堆的中间位置,合并的费用为两堆石子的总数。求把所有石子合并成一堆的最小花费。
输入格式
输入多组测试数据
输入第一行包含一个整数n,表示石子的堆数。
接下来一行,包含n个整数,按顺序给出每堆石子的大小 。
输出格式
输出一个整数,表示合并的最小花费。
样例输入
5
1 2 3 4 5
样例输出
33
数据规模和约定
1<=n<=1000, 每堆石子至少1颗,最多10000颗。
- 解法一: 普通区间dp,最高可处理n<=250的数据规模
import java.util.*;
public class Main {
public static int n; //记录石子的堆数
public static int[] sum; //记录由第一堆到第i堆所需要的力气总和
public static void fun_dp() {
int[][] dp=new int[n+1][n+1]; //状态数组,记录从i到j合并石子所花费的最小力气
for(int len=2;len<=n;len++) {
for(int i=1;i<=n-len+1;i++) {
int j=i+len-1;
dp[i][j]=Integer.MAX_VALUE; //初始化为最大,便于求解最小值
for(int k=i;k<j;k++) { //枚举由k到j的每个分割点
dp[i][j]=Math.min(dp[i][j], (dp[i][k]+dp[k+1][j]+sum[j]-sum[i-1]));
}
}
}
System.out.println(dp[1][n]);
}
public static void main(String[] args) {
Scanner sc=new Scanner(System.in);
while(sc.hasNextInt()) {
n=sc.nextInt();
int x=0;
sum=new int[n+1]; //记录前i个区间的数目和
for(int i=1;i<=n;i++) {
x=sc.nextInt();
sum[i]=sum[i-1]+x;
}
fun_dp();
}
}
}
- 解法二: 平行四边形优化区间dp,最高可处理n<=1000的数据规模
package 石子合并;
import java.util.*;
//平行四边形算法解决:石子合并
public class Main2 {
public static int n; //记录石子堆数
public static int[] cost; //记录由1到i的花费
public static int[][] s; //记录将石子i到j全部合并的最佳分割点
public static void fun_dp() {
int[][] dp=new int[n+5][n+5];
for(int len=2;len<=n;len++) {
for(int i=1;i<=n-len+1;i++) {
int j=i+len-1;
dp[i][j]=Integer.MAX_VALUE; //初始化值
//循环方法一
// for(int k=s[i][j-1];k<s[i+1][j];k++) { //平行四边形优化
// dp[i][j]=Math.min(dp[i][j],dp[i][k]+dp[k+1][j]+cost[j]-cost[i-1]);
// s[i][j]=k; //记录最佳分割点
// }
//循环方法二
for(int k=s[i][j-1];k<=s[i+1][j];k++) {
if(dp[i][k]+dp[k+1][j]+cost[j]-cost[i-1]<dp[i][j]) { //换成if语句,可以减少不必要的循环赋值
dp[i][j]=dp[i][k]+dp[k+1][j]+cost[j]-cost[i-1];
s[i][j]=k;
}
}
}
}
System.out.println(dp[1][n]);
}
public static void main(String[] args) {
Scanner sc=new Scanner(System.in);
while(sc.hasNextInt()) {
n=sc.nextInt();
cost=new int[n+5]; //记录由1到i的花费
s=new int[n+5][n+5];
int x=0;
for(int i=1;i<=n;i++) {
x=sc.nextInt();
cost[i]=cost[i-1]+x;
s[i][i]=i; //初始化最佳分割点
}
fun_dp();
}
}
}
如果以上讲解不足以使你明白,可以参考罗勇军教授的专题讲解,包含了四边形不等式的论证,及平行四边形优化的证明:链接: DP优化(1)–四边形不等式