这两天研究了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循环,如果方程组比较抽象,
可以画表格帮助分析
先写到这里,感谢阅读!