首页 > 代码库 > 算法整理之动态规划
算法整理之动态规划
我现在介绍的这个版本,是从算法爱好者中看到的一个别人的漫画版本。
题目:
有一座高度是10级台阶的楼梯,从下往上走,每跨一步只能向上1级或者2级台阶。要求用程序来求出一共有多少种
走法。
比如,每次走1级台阶,一共走10步,这是其中一种走法。我们可以简写成 1,1,1,1,1,1,1,1,1,1。
再比如,每次走2级台阶,一共走5步,这是另一种走法。我们可以简写成 2,2,2,2,2。
分析:
这种问题是典型的使用动态规划解决的问题,在使用动态规划的方法之前,能想到的方法可能就是使用排列组合,
这是一个非常复杂的嵌套循环,俗称暴力枚举,不提倡使用。
[动态规划]dynamic programming是一种分阶段求解决策问题的数学思想。它不知用于变成领域,也应用在管理学
经济学和生物学中。简言之,“大事化小,小事化了”。刚才的题目简单分析如下:
1)当还差1步走完10级台阶的时候,这时有两种情况i)剩下1级台阶:9->10,ii)剩下两级台阶:8->10.暂且不说,
如果从1->8,1->9分别有X,Y中方法,那么原问题(一共有多少种走法)结果就是X+Y
2)把上面的分析形式化一下就是,我们要求的,F(9)=Y,F(8)=X,F(10)=F(9)+F(8),分析求解F(9),F(8)的过程又和
上面是分析求f(10)的过程是相似的,F(9)=f(8)+F(7)...上面的过程就是在把一个复杂的问题分蘖阶段进行简化,
逐步简化成简单的问题,这就是动态规划的思想:大事化小,化繁为简.
3)进一步往前推,显然
F(1)=1
F(2)=2
F(n)=F(n-1)+F(n-2) (n>=3)
动态规划中有三个非常中澳的概念:【最优子结构】、【边界】、【状态转移公式】
在F(10)=F(9)+F(8)中,F(9)和F(8)就是F(10)的【最优子结构】
当只有1级台阶或者级台阶时我们可以直接得出结果,无需继续花间,我们成F(1)F(2)是问题的【边界】
F(n)=F(n-1)+F(n-2) (n>=3)是阶段与阶段之间的【状态转移方程】这是动态规划的核心
解法分析与升级:
解法一:朴素的递归解法
getClimbingWyas1 是一个基础的递归解法,这个方法就是一个很朴素的递归求解,复杂度较高,这里顺带介绍
了递归复杂度的粗略估计方法:做出递归求解的递归树,发现就是一个二叉树,其中每一个节点就是程序需要计算
的,那么大致复杂度就是 O(2^n)
解法二:升级版的递归解法,降低复杂度-----备忘录递归
之所以会有那么糟糕的复杂度,就是因为认真观察上面列出的递归树,会发现,有很多节点是被重复计算了,为了
避免这种重复计算,通常使用的做法是使用一个哈希缓存:先创建一个缓存,每次吧不同参数的计算结果存入哈希表
当遇到相同的参数是,再从哈希表中取出,节省了计算时间。这种方法有个专业名字【备忘录算法】,改进后的算法
实现为getClimbingWays2(int).
使用哈细胞改造后的,复杂度分析:从F(1)到F(N)一共是N个不同的输入,在哈希表里面存储了N-2个结果,所以时间
复杂度和空间复杂度都是好O(N)
貌似【备忘录】结合不同的题目会有不同的,形式,我记得当年看到的一些题目里面用的好像是二维数组,当年傻傻
的不明白。
解法三:【正真的动态规划求解】
对上面的递归球阀进一步的,优化的结果
前面的递归方法是一种”自定向下“的解法,逆向思维,使用自底向上的思想来优化:
所谓的自底向上的思路:当 n>=3时,F(N)之和 前面的F(n-1)和F(n-2)有关,所以,只需要用前面两个状态迭代出
后面的第三个状态就行了。于是以前的递归就变成了一个为递归方法getclimbingWays3(int n)
瞬间时间复杂度变成了o(N) 空间复杂度变成了O(1)
1 package dynamicprogram; 2 3 import java.util.HashMap; 4 import java.util.Map; 5 6 /* 7 * 这里主要是用来讲解动态规划算法的10级台阶问题的编码,与求解的优化 8 */ 9 public class TenStairs { 10 11 /* 12 * 使用递归求解的基础版 13 */ 14 public static int getClimbingWays1(int n) 15 { 16 17 if ( n<0 ) 18 return 0; 19 else if (n ==1 ) 20 return 1; 21 else if ( n == 2) 22 return 2; 23 else 24 return getClimbingWays1(n-1)+getClimbingWays1(n-2); 25 } 26 27 28 /* 29 * 使用 备忘录算法给今后的 递归方法, 30 * 这里递归程序的编写有一个小技巧,为了不定义一个static的map,把map作为递归 31 * 函数的一个参数,一层层的向下传递 32 */ 33 public static int getClimbingWays2( int n, Map<Integer, Integer> map) 34 { 35 if ( n<0 ) return 0; 36 if ( n == 1 ) return 1; 37 if ( n == 2 ) return 2; 38 39 if ( map.containsKey(n)) 40 return map.get(n); 41 else{ 42 int value = http://www.mamicode.com/getClimbingWays2(n-1, map) + getClimbingWays2(n-2, map); 43 map.put(n, value); 44 return value; 45 } 46 } 47 48 /* 49 * 使用自底向上的迭代方法 50 */ 51 public static int getClimbingWays3(int n) 52 { 53 if ( n<1 ) return 0; 54 if ( n == 1) return 1; 55 if ( n == 2) return 2; 56 57 int a = 1; 58 int b = 2; 59 int tmp = 0; 60 61 for ( int i=3; i <= n; i ++){ 62 tmp = a + b; 63 a = b; 64 b = tmp; 65 } 66 return tmp; 67 } 68 69 public static void main(String[] args) { 70 int ways = 0; 71 Map<Integer, Integer> map = new HashMap<Integer, Integer>(); 72 73 long start = System.currentTimeMillis(); 74 ways = getClimbingWays3(10); 75 76 long end = System.currentTimeMillis(); 77 78 System.out.println(ways); 79 System.out.println((end-start)/1000); 80 81 } 82 }
上面的爬楼梯只是动态规划中比较简单的一个例子,规划的维度只有一个,下面给出一个较为复杂的一个
题目二:过往和金矿的问题( get most gold)
有一个国家发现了5座金矿,每座金矿的黄金储量不同,需要参与挖掘的工人数也不同。参与挖矿工人的总数是
10人。每座金矿要么全挖,要么不挖,不能派出一半人挖取一半金矿。
各个金矿的投入和产出: 400金/5人, 500金/5人, 200金/3人, 300金/4人, 350金/3人
要求用程序求解出,要想得到尽可能多的黄金,应该选择挖取哪几座金矿?
分析:
拿到一个动态规划的问题,都要考虑其核心的三要素:【最优子结构】、【边界】、【状态转移方程】
寻找最优子结构,我们往往倒着来。
为了便于描述,这里设金矿数量为n,工人数为w,出尽量为g[],每一座金矿的用工数为p[].
那么5座金矿和4座金矿之间的最优解存在这样的关系,F(5, 10) = MAX( F(4,10), F(4,10-p[4])+g[4] )
边界问题,就是考虑只有一座金矿的时候,又要考虑下面两种情况呢:
n=1, w>=p[0], f(n,w)=g[0]
n=1, w<p[0], f(n,w)=0
于是整理之后就可以得到状态方程如下:
F(n,w) = 0 (n<=1, w<p[0]);
F(n,w) = g[0] (n==1, w>=p[0]);
F(n,w) = F(n-1,w) (n>1, w<p[n-1])
F(n,w) = max(F(n-1,w), F(n-1,w-p[n-1])+g[n-1]) (n>1, w>=p[n-1])
其实有了具体的状态转移方程,就能够很快的写出朴素的递归解法。几种常见的解法见下:
解法一:排列组合,暴力枚举
每一座金矿都有挖与不挖两种选择,如果有N座金矿,很明显就有2^N中选择,对所有可能的情况做边淋,排除那些
用人总数超出10的选择,在剩下的选择里寻找获取金币最多的。这种方法没有太多好说的。
方法的时间复杂度,是O(2^n)
解法二:使用朴素的递归方法。
很简单,就是把状态转移方程翻译成代码即可,时间复杂度就是递归树的节点个数数量级O(2^n)
解法三:使用备忘录算法:
这种方法在简单的递归基础上创建一个hashmap备忘录,hashmao的key是一个包含了金矿数n和工人数w的对象,value
是最优选择获得的黄金数。
解法四:真正的动态规划方法的实现。
在上一个例子是一维的,转化为自底向上的迭代的时候就是简单的,在一维显性平面上的迭代。
当前的这个问题因为有两个量在变,这时候,寻找迭代关系就是在一个二维表中,其实这都很固定的分析问题的方法
类似的问题都可以如此解决。
使用转台庄毅方程,填写下面的状态转移表,其实就是上面map总的内容
1工人 2工人 3工人 4工人 5工人 6工人 7工人 8工人 9工人 10工人
1金矿 0 0 0 0 400 400 400 400 400 400
2金矿 0 0 0 0 500 500 500 500 500 900
3金矿 0 0 200 200 500 500 500 700 700 900
4金矿 0 0 200 300 500 500 500 700 800 900
5金矿 0 0 350 350 500 550 650 850 850 900
观察上面的状态转移表,可以发现,除了第1行以为,每个格子都是前一行的一个或者两个格子推到出来的,所以程序
实现时,只需要存储一行结果,就可以推导出新的一行。
这种方法的复杂度是O(n*w)当n,w较大时效率不及朴素的递归运算。
1 package dynamicprogram; 2 3 import java.sql.Array; 4 import java.util.Arrays; 5 import java.util.HashMap; 6 import java.util.Map; 7 8 /* 9 * 题目二:掘金问题 10 */ 11 12 public class MostGold { 13 14 /* 15 * 使用暴力枚举的方法 16 */ 17 public static int getMostGold1(int n, int w, int p[], int g[]) 18 { 19 int most = 0; 20 String str = "00000"; 21 int N = (int) Math.pow(2, n)-1; // 一共这么多种情况 22 for (int i=0; i<=N; i++) 23 { 24 int result = 0; 25 String r1 = Integer.toBinaryString((i&N)); 26 r1 = new StringBuffer(r1).reverse().toString(); 27 String strTmp = str.substring(r1.length(), str.length()); 28 r1 = r1+strTmp; 29 int pc = 0; 30 for (int index=0; index < r1.length(); index ++) 31 { 32 int flg = r1.charAt(index)-‘0‘; 33 pc = pc + p[index]*flg; 34 result = result + g[index]*flg; 35 } 36 37 if ( pc==10 ) 38 System.out.println("i = " + i + ", pc = " + pc + ", reslut = "+result); 39 } 40 return most; 41 } 42 43 /* 44 * 使用 朴素的递归方法 45 */ 46 public static int getMostGold2(int n, int w, int p[], int g[]) 47 { 48 if ( n<=1 && w<p[0] ) return 0; 49 if ( n==1 && w>=p[0] ) return g[0]; 50 if ( n>1 && w<p[n-1] ) return getMostGold2(n-1, w, p, g); 51 else 52 return Math.max(getMostGold2(n-1, w, p, g), getMostGold2(n-1, w-p[n-1], p, g)+g[n-1]); 53 } 54 55 /* 56 * 使用 带有hash备忘录的 改进递归方法 57 * 为了 实现使用hash方法这里,把包含的金矿个数n和工人数w构造成一个对象,为此写一个内部类,在这个方法的下面 58 */ 59 public static int getMostGold3(int n, int w, int p[], int g[], Map< Key, Integer> map) 60 { 61 if ( n<=1 && w<p[0] ) return 0; 62 if ( n==1 && w>=p[0] ) return g[0]; 63 64 Key key = new Key(n-1, w); 65 int value; 66 if ( n>1 && w<p[n-1] ) 67 { 68 if (map.containsKey(key)) 69 value =http://www.mamicode.com/ map.get(key); 70 else{ 71 value = http://www.mamicode.com/getMostGold2(n-1, w, p, g); 72 map.put(key, value); 73 } 74 } 75 else 76 { 77 if (map.containsKey(key)) 78 value =http://www.mamicode.com/ map.get(key); 79 else 80 { 81 value = http://www.mamicode.com/Math.max(getMostGold2(n-1, w, p, g), getMostGold2(n-1, w-p[n-1], p, g)+g[n-1]); 82 map.put(key, value); 83 } 84 85 } 86 return value; 87 } 88 89 /** 90 * 真正的 动态规划的实现 (就是迭代版的解法) 91 * 如果不是自己亲自写一下,你不知道,这么短小的一段程序会花费你自己多少时间, 92 * 伪代码描述的知识思路,重要的还是自己去写,去实现 93 * @param args 94 */ 95 public static int getMostGold3(int n, int w, int p[], int g[]) 96 { 97 int[] preResults = new int[w+1]; //为了处理的方便,加了0行0列 98 99 // 填充边界情况,也就是只有1个金矿的情况 100 for ( int j=1; j <= w; j++){ 101 if ( j < p[0] ) 102 { 103 preResults[j] = 0; 104 }else{ 105 preResults[j] = g[0]; 106 } 107 } 108 System.out.println(Arrays.toString(preResults)); 109 110 // 填充 其余格子, n>1 111 for ( int i = 2; i <= n; i ++) // 从第二行开始 112 { 113 int[] results = new int[w+1]; // 计算下一行 114 for (int j = 1; j <= w; j ++){ 115 if ( j < p[i-1] ) 116 results[j] = preResults[j]; 117 else 118 results[j] = Math.max(preResults[j], preResults[j-p[i-1]]+g[i-1]); 119 } 120 preResults = results; 121 System.out.println(Arrays.toString(preResults)); 122 } 123 return preResults[w]; 124 } 125 126 public static void main(String[] args) { 127 int g[] = {400, 500, 200, 300, 350}; // 每矿出金量 128 int p[] = {5, 5, 3, 4, 3}; //每矿用工数 129 Map< Key, Integer> map = new HashMap<Key, Integer>(); 130 131 System.out.println(getMostGold3(5, 10, p, g)); 132 } 133 } 134 class Key{ 135 int n; 136 int w; 137 138 public Key(int n, int w) 139 { 140 this.n = n; 141 this.w = w; 142 } 143 144 @Override 145 public int hashCode() { 146 // TODO Auto-generated method stub 147 return n*100; 148 } 149 @Override 150 public boolean equals(Object obj) { 151 Key key = (Key) obj; 152 if ( this.n == key.n && this.w == key.w) return true; 153 else return false; 154 } 155 }
算法整理之动态规划