Bootstrap

(详解)区间DP —— 平行四边形优化

本文从普通区间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)–四边形不等式

;