動態規划入門題之國王和金礦(0-1背包問題)


這兩天研究了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循環,如果方程組比較抽象,

      可以畫表格幫助分析

 

先寫到這里,感謝閱讀!

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM