首页 > 代码库 > 算法整理之动态规划

算法整理之动态规划

我现在介绍的这个版本,是从算法爱好者中看到的一个别人的漫画版本。
题目:
有一座高度是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 }
View Code

 

上面的爬楼梯只是动态规划中比较简单的一个例子,规划的维度只有一个,下面给出一个较为复杂的一个

题目二:过往和金矿的问题( 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 }
View Code

 

算法整理之动态规划