動態規划算法:
基本思想:
動態規划算法通常用於求解具有某種最優性質的問題。在這類問題中,可能會有許多可行解。每一個解都對應於一個值,我們希望找到具有最優值的解。動態規划算法與分治法類似,其基本思想也是將待求解問題分解成若干個子問題,先求解子問題,然后從這些子問題的解得到原問題的解。與分治法不同的是,適合於用動態規划求解的問題,經分解得到子問題往往不是互相獨立的。若用分治法來解這類問題,則分解得到的子問題數目太多,有些子問題被重復計算了很多次。如果我們能夠保存已解決的子問題的答案,而在需要時再找出已求得的答案,這樣就可以避免大量的重復計算,節省時間。我們可以用一個表來記錄所有已解的子問題的答案。不管該子問題以后是否被用到,只要它被計算過,就將其結果填入表中。這就是動態規划法的基本思路。具體的動態規划算法多種多樣,但它們具有相同的填表格式。
動態規划算法與分治法最大的差別是:適合於用動態規划法求解的問題,經分解后得到的子問題往往不是互相獨立的(即下一個子階段的求解是建立在上一個子階段的解的基礎上,進行進一步的求解)
應用場景:
適用動態規划的問題必須滿足最優化原理、無后效性和重疊性。
1.最優化原理(最優子結構性質) 最優化原理可這樣闡述:一個最優化策略具有這樣的性質,不論過去狀態和決策如何,對前面的決策所形成的狀態而言,余下的諸決策必須構成最優策略。簡而言之,一個最優化策略的子策略總是最優的。一個問題滿足最優化原理又稱其具有最優子結構性質。
2.無后效性 將各階段按照一定的次序排列好之后,對於某個給定的階段狀態,它以前各階段的狀態無法直接影響它未來的決策,而只能通過當前的這個狀態。換句話說,每個狀態都是過去歷史的一個完整總結。這就是無后向性,又稱為無后效性。
3.子問題的重疊性 動態規划將原來具有指數級時間復雜度的搜索算法改進成了具有多項式時間復雜度的算法。其中的關鍵在於解決冗余,這是動態規划算法的根本目的。動態規划實質上是一種以空間換時間的技術,它在實現的過程中,不得不存儲產生過程中的各種狀態,所以它的空間復雜度要大於其它的算法。
動態規划算法經典案例:
案例一:
有n級台階,一個人每次上一級或者兩級,問有多少種走完n級台階的方法。
分析:動態規划的實現的關鍵在於能不能准確合理的用動態規划表來抽象出 實際問題。在這個問題上,我們讓f(n)表示走上n級台階的方法數。
那么當n為1時,f(n) = 1,n為2時,f(n) =2,就是說當台階只有一級的時候,方法數是一種,台階有兩級的時候,方法數為2。那么當我們要走上n級台階,必然是從n-1級台階邁一步或者是從n-2級台階邁兩步,所以到達n級台階的方法數必然是到達n-1級台階的方法數加上到達n-2級台階的方法數之和。即f(n) = f(n-1)+f(n-2),我們用dp[n]來表示動態規划表,dp[i],i>0,i<=n,表示到達i級台階的方法數。
1 public class CalculationSteps { 2 //動態規划表,用來記錄到達i級台階的方法數 3 public static int[] steps = new int[11]; 4 5 public static void main(String[] args) { 6 steps[10] = calStep(10); 7 8 for (int i = 0; i < steps.length; i++) { 9 System.out.print(steps[i]+" "); 10 } 11 System.out.println(); 12 System.out.println(steps[10]); 13 } 14 15 //計算到達i級台階的方法數 16 public static int calStep(int n){ 17 //如果為第一級台階或者第二級台階 則直接返回n 18 if(n==1||n==2){ 19 return n; 20 } 21 //計算到達n-1級台階的方法數 22 if(steps[n-1]==0){ 23 steps[n-1] = calStep(n-1); 24 } 25 //計算到達n-2級台階的方法數 26 if(steps[n-2] == 0){ 27 steps[n-2] = calStep(n-2); 28 } 29 //到達第n級台階=到達n-1級台階+到達n-2級台階 30 return steps[n-1]+steps[n-2]; 31 } 32 }
運行結果如下:
0 1 2 3 5 8 13 21 34 55 89 89
案例2:
給定一個矩陣m,從左上角開始每次只能向右走或者向下走,最后達到右下角的位置,路徑中所有數字累加起來就是路徑和,返回所有路徑的最小路徑和,如果給定的m如下,那么路徑1,3,1,0,6,1,0就是最小路徑和,返回12.
1 3 5 9
8 1 3 4
5 0 6 1
8 8 4 0
分析:對於這個題目,假設m是m行n列的矩陣,那么我們用dp[m][n]來抽象這個問題,dp[i][j]表示的是從原點到i,j位置的最短路徑和。我們首先計算第一行和第一列,直接累加即可,那么對於其他位置,要么是從它左邊的位置達到,要么是從上邊的位置達到,我們取左邊和上邊的較小值,然后加上當前的路徑值,就是達到當前點的最短路徑。然后從左到右,從上到下依次計算即可。
Java代碼實現:
1 /** 2 * 給定一個矩陣m,從左上角開始每次只能向右走或者向下走 3 * 最后達到右下角的位置,路徑中所有數字累加起來就是路徑和, 4 * 返回所有路徑的最小路徑和 5 */ 6 public class MinSteps { 7 8 public static int[][] steps=new int[4][4]; 9 10 public static void main(String[] args) { 11 int[][] arr = {{4,1,5,3},{3,2,7,7},{6,5,2,8},{8,9,4,5}}; 12 steps[3][3] = minSteps(arr, 3, 3); 13 print(steps); 14 } 15 16 17 public static int minSteps(int[][] arr,int row,int col){ 18 //如果為起始位置,則直接返回 19 if(row==0&&col==0){ 20 steps[row][col] = arr[row][col]; 21 return steps[row][col]; 22 } 23 24 //計算到arr[row][col]的左面位置的值 25 if(col>=1&&steps[row][col-1]==0){ 26 steps[row][col-1]=minSteps(arr, row, col-1); 27 } 28 //計算到arr[row][col]的上面位置的值 29 if(row>=1&&steps[row-1][col]==0){ 30 steps[row-1][col]=minSteps(arr, row-1, col); 31 } 32 //如果為第一行,則直接加左面位置上的值 33 if(row==0&&col!=0){ 34 steps[row][col] = arr[row][col]+steps[row][col-1]; 35 }else if(col == 0&&row!=0){ 36 //如果為第一列,則直接加上上面位置上的值 37 steps[row][col] = arr[row][col]+steps[row-1][col]; 38 }else{ 39 //比較到達左面位置和到達上面位置的值的大小,加上兩者的最大值 40 steps[row][col] =arr[row][col]+min(steps[row][col-1],steps[row-1][col]); 41 } 42 return steps[row][col]; 43 } 44 45 private static int min(int minSteps, int minSteps2) { 46 return minSteps>minSteps2?minSteps:minSteps2; 47 } 48 49 50 static void print(int[][] arr){ 51 for (int i = 0; i < arr.length; i++) { 52 for (int j = 0; j < arr[i].length; j++) { 53 System.out.println("到達arr["+i+"]["+j+"]的最大路徑:"+arr[i][j]); 54 } 55 } 56 } 57 }
運行結果:
到達arr[0][0]的最大路徑:4 到達arr[0][1]的最大路徑:5 到達arr[0][2]的最大路徑:10 到達arr[0][3]的最大路徑:13 到達arr[1][0]的最大路徑:7 到達arr[1][1]的最大路徑:9 到達arr[1][2]的最大路徑:17 到達arr[1][3]的最大路徑:24 到達arr[2][0]的最大路徑:13 到達arr[2][1]的最大路徑:18 到達arr[2][2]的最大路徑:20 到達arr[2][3]的最大路徑:32 到達arr[3][0]的最大路徑:21 到達arr[3][1]的最大路徑:30 到達arr[3][2]的最大路徑:34 到達arr[3][3]的最大路徑:39
案例3:最長公共子序列問題
最長公共子序列問題是要找到兩個字符串間的最長公共子序列。假設有兩個字符串sudjxidjs和xidjxidpolkj,其中djxidj就是他們的最長公共子序列。許多問題都可以看成是公共子序列的變形。例如語音識別問題就可以看成最長公共子序列問題。
假設兩個字符串分別為A=a1a2..am,B=b1b2..bn,則m為A的長度,n為B的長度。那么他們的最長公共子序列分為兩種情況。
1、am=bn,這時他們的公共子序列一定為的長度F(m,n)=F(m-1,n-1)+am;
2、am≠bn,這時他們的公共子序列一定為的長度F(m,n)=Max(F(m-1,n),F(m,n-1));
1 /** 2 * 求兩個字符串之間的最長子序列 3 */ 4 public class MaxCommonStr { 5 // 數組用來存儲兩個字符串的最長公共子序列 6 public static String[][] result = new String[10][15]; 7 8 public static void main(String[] args) { 9 String strA = "sudjxidjs"; 10 String strB = "xidjxidpolkj"; 11 System.out.println(maxCommonStr(strA, strB)); 12 // System.out.println(strA.charAt(strA.length()-1)); 13 } 14 15 /** 16 * 獲取兩個字符串的最大公共子序列 17 * 18 * @param strA 19 * @param strB 20 * @return 21 */ 22 public static String maxCommonStr(String strA, String strB) { 23 // 分別獲取兩個字符串的長度 24 int lenA = strA.length(); 25 int lenB = strB.length(); 26 27 // 如果字符串strA的長度為1,那么如果strB包含字符串strA,則公共子序列為strA,否則為null 28 if (lenA == 1) { 29 if (strB.contains(strA)) { 30 result[lenA - 1][lenA - 1] = strA; 31 } else { 32 result[lenA - 1][lenA - 1] = ""; 33 } 34 return result[lenA - 1][lenA - 1]; 35 } 36 37 // 如果字符串strB的長度為1,那么如果strA包含字符串strB,則公共子序列為strB,否則為null 38 if (lenB == 1) { 39 if (strA.contains(strB)) { 40 result[lenA - 1][lenA - 1] = strB; 41 } else { 42 result[lenA - 1][lenA - 1] = ""; 43 } 44 return result[lenA - 1][lenA - 1]; 45 } 46 47 // 如果字符串strA的最后一位和strB的最后一位相同的話, 48 if (strA.charAt(lenA - 1) == strB.charAt(lenB - 1)) { 49 //先判斷數組result[lenA - 2][lenB - 2] == null,這樣可以減少一些重復運算 50 if (result[lenA - 2][lenB - 2] == null) { 51 //求strA和strB都去除最后一位剩余字符串的最大公共子序列f 52 result[lenA - 2][lenB - 2] = maxCommonStr(strLenSub(strA), strLenSub(strB)) ; 53 } 54 //strA和strB的最大公共子序列就是他們各去除最后一位剩余字符串的最大公共子序列+strA或者strB的最后一位 55 result[lenA-1][lenB-1] = result[lenA - 2][lenB - 2]+ strA.charAt(lenA - 1); 56 } else { 57 //否則 58 if (result[lenA - 2][lenB-1] == null) { 59 //計算strA去除最后一位后和strB的最大子序列 60 result[lenA - 2][lenB-1] = maxCommonStr(strLenSub(strA), strB); 61 } 62 if (result[lenA-1][lenB - 2] == null) { 63 //計算strB去除最后一位后和strA的最大子序列 64 result[lenA-1][lenB - 2] = maxCommonStr(strA, strLenSub(strB)); 65 } 66 //等於result[lenA - 2][lenB-1]和result[lenA-1][lenB - 2]中的最大數 67 result[lenA-1][lenB-1] = max(result[lenA - 2][lenB-1], result[lenA-1][lenB - 2]); 68 } 69 return result[lenA-1][lenB-1]; 70 } 71 72 /** 73 * 使字符串去除最后一位,返回該新的字符串 74 * @param str 75 * @return 76 */ 77 public static String strLenSub(String str) { 78 return str.substring(0, str.length() - 1); 79 } 80 81 /** 82 * 比較兩個字符串長度,返回最長字符串 當兩個字符串長度相等時,返回任意字符串 83 * 84 * @param strA 85 * @param strB 86 * @return 87 */ 88 public static String max(String strA, String strB) { 89 if (strA == null && strB == null) { 90 return ""; 91 } else if (strA == null) { 92 return strB; 93 } else if (strB == null) { 94 return strA; 95 } 96 if (strA.length() > strB.length()) { 97 return strA; 98 } else { 99 return strB; 100 } 101 } 102 }
運行結果:
djxidj
寫在最后:
此篇隨筆僅用來記錄我的學習內容,如有錯誤,歡迎指正。謝謝!!!
