這兩天研究了1篇寫的比較通俗易懂的動態規划入門文章( https://wx.abbao.cn/a/4736-4b66e5f9ec584ee0.html ),
但是發現作者思路雖然是對的,但是寫的代碼有錯誤,尤其是第二個例子國王與金礦(其實就是0-1背包問題)的動態規划解法的代碼中出現了如下比較嚴重的錯誤.這個錯誤不注意還發現不了,我也是debug了好一會才發現問題所在.
我會在下面補上這題DP解法正確的代碼,另外作者沒有寫這道題遞歸解法和備忘錄算法解法的代碼,我也一並補上.
題目:
有一個國家發現了5座金礦,每座金礦的黃金儲量不同,需要參與挖掘的工人數也不同。參與挖礦工人的總數是10人。每座金礦要么全挖,要么不挖,不能派出一半人挖取一半金礦。要求用程序求解出,要想得到盡可能多的黃金,應該選擇挖取哪幾座金礦?
這題建模后得到的方程組如下:
F(n,w) = 0 (n<1)
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])
其中g[ ] 中存儲每座金礦的金礦數,p[ ] 中存儲挖g[ ] 中對應金礦所需的工人數,F(n,w)表示工人數為w時挖g[ ] 中前n座金礦所能得到的最大金礦數,
這題有3種解法,第一種是簡單遞歸解法,第二種是在遞歸的基礎上的備忘錄算法解法,第三種是動態規划解法,需要注意的是,當輸入的礦山數多時用動態規划解法效率高,但是當輸入的工人數多時,DP的效率反而不如簡單遞歸,所以說不同算法沒有絕對的好壞,要視具體場景而定.
三種解法都是根據上面的方程組來寫代碼,其中動態規划解法可以畫張表格來輔助分析(因為這題有2個輸入維度),如下所示.
三種解法的完整代碼如下:
1 package ustb.dp; 2 3 import java.util.HashMap; 4 5 6 public class GoldMiner { 7 8 /** 9 * 10 * @param n 11 * @param w 12 * @param g 數組g中存儲每座金礦的金礦數 13 * @param p 數組p中存儲挖每座金礦需要的工人數 14 * @return 該方法返回工人數為w時挖數組g中前n座金礦所能得到的最大金礦數 15 */ 16 //遞歸解法 17 public static int getMostGold(int n, int w, int[] g, int[] p) { 18 if (n > g.length) 19 throw new RuntimeException("輸入的n值大於給定的金礦數"); 20 21 if (n <= 1 && w < p[0]) 22 return 0; 23 24 if (n == 1 && w >= p[0]) 25 return g[0]; 26 27 if (n > 1 && w < p[n-1]) 28 return getMostGold(n-1, w, g, p); 29 30 int a = getMostGold(n-1, w, g, p); 31 int b = getMostGold(n-1, w - p[n-1], g, p) + g[n-1]; 32 return Math.max(a, b); 33 34 } 35 36 37 38 /** 39 * @author 26062 40 * @describe 該內部類對象用於備忘錄算法中作為HashMap存儲的鍵 41 */ 42 private static class Input{ 43 private int n; 44 private int w; 45 46 public Input(int n, int w) { 47 super(); 48 this.n = n; 49 this.w = w; 50 } 51 52 @Override 53 public int hashCode() { 54 final int prime = 31; 55 int result = 1; 56 result = prime * result + n; 57 result = prime * result + w; 58 return result; 59 } 60 61 @Override 62 public boolean equals(Object obj) { 63 if (this == obj) 64 return true; 65 if (obj == null) 66 return false; 67 if (getClass() != obj.getClass()) 68 return false; 69 Input other = (Input) obj; 70 if (n != other.n) 71 return false; 72 if (w != other.w) 73 return false; 74 return true; 75 } 76 77 } 78 79 //備忘錄算法解法 80 public static int getMostGold2(int n, int w, HashMap<Input, Integer> map, int[] g, int[] p) { 81 if (n > g.length) 82 throw new RuntimeException("輸入的n值大於給定的金礦數"); 83 84 if (n <= 1 && w < p[0]) 85 return 0; 86 87 if (n == 1 && w >= p[0]) 88 return g[0]; 89 90 if (n > 1 && w < p[n-1]) { 91 Input input = new Input(n-1, w); 92 if (map.containsKey(input)) 93 return map.get(input); 94 95 int value = getMostGold2(n-1, w, map, g, p); 96 map.put(input, value); 97 return value; 98 } 99 100 Input input1 = new Input(n-1, w); 101 Input input2 = new Input(n-1, w-p[n-1]); 102 int a = 0; //用於記錄F(n-1,w)的值 103 int b = 0; //用於記錄F(n-1,w-p[n-1])+g[n-1])的值 104 105 if (map.containsKey(input1)) 106 a = map.get(input1); 107 a = getMostGold2(n-1, w, map, g, p); 108 map.put(input1, a); 109 110 if (map.containsKey(input2)) 111 b = map.get(input2) + g[n-1]; 112 b = getMostGold2(n-1, w-p[n-1], map, g, p); 113 map.put(input2, b); 114 b += g[n-1]; 115 116 return a > b ? a : b; 117 } 118 119 120 121 //DP解法 122 public static int getMostGold3(int n, int w, int[] g, int[] p) { 123 if (n > g.length) 124 throw new RuntimeException("輸入的n值大於給定的金礦數"); 125 126 if (w < 0) { 127 throw new RuntimeException("輸入的工人數w不能為負數"); 128 } 129 130 if (n < 1 || w == 0) { 131 return 0; 132 } 133 134 int col = w+1; ////因為F(x,0)也要用到,所以表格應該有w+1列 135 int[] preResult = new int[col]; 136 int[] result = new int[col]; 137 138 //初始化第一行(邊界) 139 for (int i = 0; i < col; i++) { 140 if (i < p[0]) 141 preResult[i] = 0; 142 else 143 preResult[i] = g[0]; 144 } 145 146 if (n == 1) { 147 return preResult[w]; 148 } 149 150 //用上一行推出下一行,外循環控制遞推的輪數,內循環進行遞推 151 for (int i = 1; i < n; i++) { 152 for (int j = 0; j < col; j++) { 153 if (j < p[i]) 154 result[j] = preResult[j]; 155 else 156 result[j] = Math.max(preResult[j], preResult[j-p[i]] + g[i]); 157 } 158 159 for (int j = 0; j < col; j++) { //更新上一行的值,為下一輪遞推做准備 160 preResult[j] = result[j]; 161 } 162 } 163 164 return result[w]; 165 } 166 167 168 //測試 169 public static void main(String[] args) { 170 int [] g = {400, 500, 200, 300, 350}; 171 int [] p = {5, 5, 3, 4, 3}; 172 System.out.println(getMostGold(5, 10, g, p)); 173 System.out.println(getMostGold2(5, 10, new HashMap<GoldMiner.Input, Integer>(), g, p)); 174 System.out.println(getMostGold3(5, 10, g, p)); 175 176 } 177 178 }
最后總結一下DP算法的思路:
核心: 最優子結構、邊界條件、狀態轉移方程
解題步驟: 1,建立數學模型 2,寫代碼求解問題
如何建模?
先寫出所求問題的最優子結構,進而分析出邊界和狀態轉移方程,數學模型即這2者的組合
對於2輸入維度動態規划 畫表格幫助分析 行列分別代表1個輸入維度
如何求解?
建好模后,根據方程組寫出自底向上的動態規划代碼,一維輸入就是1個for循環,二維輸入就是2個for循環,如果方程組比較抽象,
可以畫表格幫助分析
先寫到這里,感謝閱讀!