1. 程式人生 > >算法整理之動態規劃

算法整理之動態規劃

getc 程序 span bool hid 沒有 知識 else 多好

我現在介紹的這個版本,是從算法愛好者中看到的一個別人的漫畫版本。
題目:
有一座高度是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 = 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 = map.get(key);
 70             else{
 71                 value = getMostGold2(n-1, w, p, g);
 72                 map.put(key, value);
 73             }
 74         }
 75         else
 76         {
 77             if (map.containsKey(key))
 78                 value = map.get(key);
 79             else
 80             {
 81                 value = 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

算法整理之動態規劃