算法整理之動態規劃
我現在介紹的這個版本,是從算法愛好者中看到的一個別人的漫畫版本。
題目:
有一座高度是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
算法整理之動態規劃