动态规划入门题之国王和金矿(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