高效算法之动态规划(第15章)

有人说:越炫耀什么,越缺少什么。但我却以为:越缺少什么,越觉得别人炫耀什么。 ——李宫俊《李宫俊的诗》

0. 前言

参考图书《算法导论》
  动态规划通常用来解决最优化问题,在这类问题中,我们通常做出一组选择来表达最优解。在做出这个选择的同时,通常会生成与原问题形式相同的子问题。当多于一个选择子集都生成相同的子问题时,动态规划技术通常很有效,其关键技术就是对每一个这样的子问题都保存其解,当其重复出现的时候即可避免重复求解。这种思想可以将指数时间的算法转换为多项式时间的算法。
  动态规划(dynamic programming)是运筹学的一个分支,是求解决策过程(decision process)最优化的数学方法。动态规划(dynamic programming)中的programming指的是一种表格法,不是编写计算机程序。
  
###1. 动态规划解析
采用动态规划求解的问题的一般要具有3个性质:

(1) 最优化原理:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。
(2) 无后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。

(3)有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)。

动态规划求解基本步骤:

  1. 刻画一个最优解的结构特征。
  2. 递归的定义最优解的值。
  3. 计算最优解的值,通常采用自底向上的方法。
  4. 利用计算出的信息构造一个最优解。

###2. 动态规划的应用
####2.1 钢条切割
**问题陈述:**给定一个长度为 n n n英寸的钢条和一个价格表 p i ( i = 1 , 2 , 3 , 4.... , n ) p_i(i=1,2,3,4....,n) pii=1,2,3,4....,n,求切割钢条的方案,使得销售收入最大。

长度 i i i12345678910
价格 p i p_i pi1589101717202430

**问题分析:**从题目中我们可以得出给定的长度为 n n n的钢条有 2 n − 1 2^{n-1} 2n1种不同的切割方案,因为在距离钢条左端 i ( i = 1 , 2 , 3.. , n − 1 ) i(i=1,2,3..,n-1) i(i=1,2,3..,n1)英寸处,我们总是可以选择切割或者不切割。对于上述的价格表样例,我们可以观察所有最优收益值 r i ( i = 1 , 2 , 3 , 4...10 ) r_i(i=1,2,3,4...10) ri(i=1,2,3,4...10) 以及对应的最优切割方案:

$ r_1=1 $, 切割方案1=1(无切割)
$ r_2=5 $, 切割方案2=2(无切割)
$ r_3=8 $, 切割方案3=3(无切割)
$ r_4=10 $, 切割方案4=2+2
$ r_5=13 $, 切割方案5=2+3
$ r_6=17 $, 切割方案6=6(无切割)
$ r_7=18 $, 切割方案7=6+1或者7=2+2+3
$ r_8=22 $, 切割方案8=2+6
$ r_9=25 $, 切割方案9=3+6
$ r_{10}=30 , 切 割 方 案 10 = 10 ( 无 切 割 )     更 一 般 的 对 于 , 切割方案10=10(无切割)   更一般的对于 ,10=10  r_n (n>=1)$, 我们可以用更短的钢条的最优切割收益来描述它:
r n = m a x ( p n , r 1 + r n − 1 , r 2 + r n − 2 , r 3 + r n − 3 . . . . r n − 2 + r 2 , r n − 1 + r 1 ) r_n= max(pn,r_1+r_{n-1},r_2+r_{n-2},r_3+r_{n-3}....r_{n-2}+r_2,r_{n-1}+r_1) rn=max(pn,r1+rn1,r2+rn2,r3+rn3....rn2+r2,rn1+r1)
注意,为了求解规模为n的原问题,我们求解形式完全一样(最优解结构特征刻画),完成首次切割之后我们将两段钢条看作两个独立的钢条切割问题,通过组合两个相关子问题的最优解(最优子结构),并且在所有可能的两段切割方案中选取组合收益最大者,构成原问题的最优解。
除了上述求解方式还有一种递归求解的方式公式如下:
r n = m a x ( p i + r n − 1 ) ( 1 ≤ i ≤ n ) r_n= max(p_i+r_{n-1}) (1\leq i \leq n) rn=max(pi+rn1)(1in)

算法设计:
自顶向下的普通递归算法设计:

SteelCut(p[],n){
	//初次判断
	if(n==0){
		return 0;
	}
	int q=-1;
	//不做切割
	if(n<=p.length){
		q=p[n];
	}
	//递归求解
	for(i=1 to n){
		q=max(q,p[i]+SteelCut(p[],n-i));
	}
	return q;
}

使用动态规划的算法设计:

//算法1:带备忘录的自顶向下法
MemorizedSteelCut(p[],n){
	int r[] = new int[n];
	for(i=0 to n){
		r[i]=-1;
	}
	return MemorizedSteelCutAux(p[],n,r);
}
MemorizedSteelCutAux(p[],n,r){
	if(r[n]>=0){
		return r[n];
	}
	if(n==0){
		q=0;
	}
	else{
		q=-1;
		for(i=1 to n){
			q=max(q,p[i]+MemorizedSteelCutAux(p[],n-i,r));		
		}
	}
	r[n]=q;
	return q;
}

//算法2:自底向上法
BottomUpSteelCut(p[],n){
	int r[] = new int[n];
	r[0]=0;
	for(j=1 to n){
		q=-1;
		for(i=1 to j){
			q=max(q,p[i]+r[j-i]);
		}
		r[j]=q;
	}
}


Java实现:

package lbz.ch15.dp.ins1;
/** 
 * @author LbZhang
 * @version 创建时间:2016年3月4日 下午2:20:33 
 * @description 钢条切割问题
 */
public class SteelCutting {

	public static void main(String[] args) {
		System.out.println("DP 在钢条切割问题中的应用 ");
		int price[] = {1,5,8,9,10,17,17,20,24,30};
		int n = 10;
		//自顶向下的递归实现
		int result = 0;
		result = TopToBottomRecursion(price,n);
		System.out.println("常规的思路:"+result);
		
		//使用动态规划来实现  备忘录
		result=MemorizedSteelCut(price,n);
		System.out.println("备忘录法:"+result);
		
		//使用动态规划的自底向上非递归的实现
		result=BottomToTopSteelCut(price,n);
		System.out.println("自底向上非递归方法:"+result);
		

	}
	private static int BottomToTopSteelCut(int[] price, int n) {
		int r[] = new int[n+1];
		r[0]=0;//动态表的开头
		for(int j=1;j<=n;j++){
			int q=-1;
			for(int i=1;i<=j;i++){
				q=maxOfTwo(q,price[i-1]+r[j-i]);
			}
			r[j]=q;
		}
		
		return r[n];
	}
	/**
	 * 备忘录方法
	 * @param price
	 * @param n
	 * @return
	 */
	private static int MemorizedSteelCut(int[] price, int n) {
		int r[] = new int[n+1];
		for(int i=0;i<=n;i++){
			r[i]=-1;
		}
		return MemorizedSteelCutAux(price,n,r);
	}
	/**
	 * 辅助过程的备忘录核心算法
	 * @param price
	 * @param n
	 * @param r
	 * @return
	 */
	private static int MemorizedSteelCutAux(int[] price, int n, int[] r) {
		if(r[n]>=0){
			return r[n];
		}
		int q=0;
		
		if(n==0){
			q=0;
		}else{
			q=-1;
			for(int i=1;i<=n;i++){
				//price[i-1] 应为price的下标是从0开始,
				q=maxOfTwo(q,price[i-1]+MemorizedSteelCutAux(price,n-i,r));		
			}
		}
		
		r[n]=q;
		return q;
	}
	/**
	 * //自顶向下的递归实现 常规思路
	 * @param price                                 ``````````````````````````````````````````````````````````````````
	 * @param n
	 * @return
	 */
	
	private static int TopToBottomRecursion(int[] price, int n) {
		if(n==0) return 0;
		int q = -1;
		if(n<=price.length){
			q=price[n-1];
		}
		for(int i=1;i<n;i++){
			q=maxOfTwo(q,price[i-1]+TopToBottomRecursion(price,n-i));
		}
		
		return q;
		
		
	}
	private static int maxOfTwo(int x, int y) {
		return x>y?x:y;//三目运算符的使用
	}
	
	

}

重构解-对源程序进行修改

private static int BottomToTopSteelCut(int[] price, int n) {
		int r[] = new int[n+1];
		int s[] = new int[n+1];
		
		r[0]=0;//动态表的开头
		for(int j=1;j<=n;j++){
			int q=-1;
			for(int i=1;i<=j;i++){
				//q=maxOfTwo(q,price[i-1]+r[j-i]);
				if(q<price[i-1]+r[j-i]){
					q=price[i-1]+r[j-i];
					s[j]=i;
				}
			}
			r[j]=q;
		}
		System.out.println();
		for(int temp=0;temp<=n;temp++){
			System.out.print(s[temp]+"|-"+temp+"-|");
		}
		System.out.println();
		
		//正确的组合输出
		printToFormal(s);
		
		
		return r[n];
	}
	private static void printToFormal(int[] s) {
		int len=s.length-1;
		int temp=s[len];
		
		System.out.print("钢条切割的组合方式: "+temp+" ");
		while(temp!=len){
			len=len-temp;
			temp=s[len];
			System.out.print("+ "+temp+" ");
		}
		System.out.println();
			
	}

####2.2 斐波那契数列
下面直接给出斐波那契数列的Java实现的 O ( n ) O(n) O(n)的动态规划算法实现。

package lbz.ch15.dp.ins1;

/**
 * @author LbZhang
 * @version 创建时间:2016年3月7日 下午9:42:15
 * @description 类说明
 */
public class MemoryAndTable {
	static int MAX = 20;
	static int[] lookUp = new int[MAX];

	public static int fibMemory(int n) {
		if (lookUp[n] == 0) {
			if (n <= 1) {
				lookUp[n] = n;
			} else {
				lookUp[n] = fibMemory(n - 1) + fibMemory(n - 2);
			}
		}
		return lookUp[n];
	}

	// //打表(自下而上)
	public static int fibTable(int n) {
		int[] f = new int[n + 1];
		int i;
		f[0] = 0;
		f[1] = 1;
		for (i = 2; i <= n; i++) {
			f[i] = f[i - 1] + f[i - 2];
		}
		return f[n];
	}

	public static void main(String[] args) {
		int n = 5;
		System.out.println(fibMemory(9));
		System.out.println();
		// int res=0;
		// res=fibTable(9);
		System.out.println(fibTable(9));
	}
}

注意:在动态规划中,子问题解决方案被存储在一个表中,以便这些不必重新计算。 因此,如果这个问题是没有共同的(重叠)子问题, 动态规划是没有用的。例如,二分查找不具有共同的子问题。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值