有人说:越炫耀什么,越缺少什么。但我却以为:越缺少什么,越觉得别人炫耀什么。 ——李宫俊《李宫俊的诗》
0. 前言
参考图书《算法导论》
动态规划通常用来解决最优化问题,在这类问题中,我们通常做出一组选择来表达最优解。在做出这个选择的同时,通常会生成与原问题形式相同的子问题。当多于一个选择子集都生成相同的子问题时,动态规划技术通常很有效,其关键技术就是对每一个这样的子问题都保存其解,当其重复出现的时候即可避免重复求解。这种思想可以将指数时间的算法转换为多项式时间的算法。
动态规划(dynamic programming)是运筹学的一个分支,是求解决策过程(decision process)最优化的数学方法。动态规划(dynamic programming)中的programming指的是一种表格法,不是编写计算机程序。
1. 动态规划解析
采用动态规划求解的问题的一般要具有3个性质:
(1) 最优化原理:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。
(2) 无后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。
(3)有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)。
动态规划求解基本步骤:
1. 刻画一个最优解的结构特征。
2. 递归的定义最优解的值。
3. 计算最优解的值,通常采用自底向上的方法。
4. 利用计算出的信息构造一个最优解。
2. 动态规划的应用
2.1 钢条切割
问题陈述:给定一个长度为
长度 |
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|
价格 |
1 | 5 | 8 | 9 | 10 | 17 | 17 | 20 | 24 | 30 |
问题分析:从题目中我们可以得出给定的长度为
更一般的对于
注意,为了求解规模为n的原问题,我们求解形式完全一样(最优解结构特征刻画),完成首次切割之后我们将两段钢条看作两个独立的钢条切割问题,通过组合两个相关子问题的最优解(最优子结构),并且在所有可能的两段切割方案中选取组合收益最大者,构成原问题的最优解。
除了上述求解方式还有一种递归求解的方式公式如下:
算法设计:
自顶向下的普通递归算法设计:
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实现的
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));
}
}
注意:在动态规划中,子问题解决方案被存储在一个表中,以便这些不必重新计算。 因此,如果这个问题是没有共同的(重叠)子问题, 动态规划是没有用的。例如,二分查找不具有共同的子问题。