黄金矿工
很久很久以前,有一位国王拥有5座金矿,每座金矿的黄金储量不同,需要参与挖掘的工人人数也不同。例如有的金矿储量是500kg黄金,需要5个工人来挖掘;有的金矿储量是200kg黄金,需要3个工人来挖掘…如果参与挖矿的工人的总数是10。每座金矿要么全挖,要么不挖,不能派出一半人挖取一半的金矿。要求用程序求出,要想得到尽可能多的黄金,应该选择挖取哪几座金矿? 《漫画算法》
解题思路
动态规划:
对于最后的金矿,都有挖或者不挖的选项:
对比两种情况的收益,最终可得到最大收益。
①400kg/5人②500kg/5人③200kg/3人④300kg/4人⑤350kg/3人
如何确定挖不挖⑤:比较a.10个人挖前4个的收益和b.7个人挖前4个的收益+⑤的收益
-
10个人挖4座的收益
-
10个工人在前3个金矿中做出最优选择。
-
6(10-4=6)个工人在前3个金矿中做出最优选择。
-
-
7个人挖4座
-
7个工人在前3个金矿中做出最优选择。
-
3(7-4=3)个工人在前3个金矿中做出最优选择
-
依次类推,把问题依次拆分。一直把问题简化成在0个金矿或0个工人时的最优选择,这个收益结果显然是0,也就是问题的边界 。
设金矿数量为n,工人数为w,金矿开采需要的工人数量为数组p,金矿储量为数组g,获得的收益为F(n,w)
- 问题边界,金矿数为0或工人数为0的情况。
F ( n , w ) = 0 ( n = 0 或 w = 0 ) F(n,w) = 0 (n=0或w=0) F(n,w)=0(n=0或w=0)
- 当所剩工人不够挖掘当前金矿时,只有一种最优子结构。
F ( n , w ) = F ( n − 1 , w ) ( n ≥ 1 , w < p [ n − 1 ] ) F(n,w) = F(n-1,w) (n≥1, w<p[n-1]) F(n,w)=F(n−1,w)(n≥1,w<p[n−1])
- 在常规情况下,具有两种最优子结构(挖当前金矿或不挖当前金矿
F ( n , w ) = m a x ( F ( n − 1 , w ) , F ( n − 1 , w − p [ n − 1 ] + g [ n − 1 ) ] ) ( n > = 1 , p > = p [ n − 1 ] ) F(n,w)=max(F(n-1, w), F(n-1, w-p[n-1]+g[n-1)]) (n>= 1, p>=p[n-1]) F(n,w)=max(F(n−1,w),F(n−1,w−p[n−1]+g[n−1)])(n>=1,p>=p[n−1])
递归实现
/**
* @param w 工人数量
* @param n 金矿数量
* @param p 金矿开采需要的工人数量
* @param g 金矿储量
* @return 最优
*/
// 时间复杂度O(2^n)
public static int getBestGoldMining(int w, int n, int[] p, int[] g) {
if (w == 0 || n == 0) {
return 0;
}
if (w < p[n - 1]) {
return getBestGoldMining(w, n - 1, p, g);
}
return Math.max(getBestGoldMining(w, n - 1, p, g),
getBestGoldMining(w - p[n - 1], n - 1, p, g) + g[n - 1]);
}
二维数组实现
从上图可发现在递归的时候出现了很多次的重复调用,为解决此问题我们可选择一个二维数组来记录中间数据,如下:
使用上面的方程填满这张表就可以得出最终数据:
/**
* 获得金矿最优收益
*
* @param w 工人数量
* @param p 金矿开采需要的工人数量
* @param g 金矿储量
* @return 最大收益
*/
public static int getBestGoldMining(int w, int[] p, int[] g) {
// 创建表格
int[][] resultTable = new int[g.length + 1][w + 1];
// 填充表格 i个金矿
for (int i = 1; i <= g.length; i++) {
// j个人
for (int j = 1; j <= w; j++) {
if (j < p[i - 1]) {
resultTable[i][j] = resultTable[i - 1][j];
} else {
// 1、不挖当前金矿
int v1 = resultTable[i - 1][j];
// 2、挖当前金矿
int v2 = resultTable[i - 1][j - p[i - 1]] + g[i - 1];
// 3、比较两种情况的收益
resultTable[i][j] = Math.max(v1, v2);
}
}
}
//返回最后1个格子的值
return resultTable[g.length][w];
}
一维数组实现
在上面方法的循环中我们可以发现每次计算其实只用到了上一行的数据,因此,无论金矿有多少座,我们只保存1行的数据即可。在计算下一行时,要从右向左统计,把旧的数据一个一个替换掉。
public static int getBestGoldMiningV2(int w, int[] p, int[] g) {
//创建当前结果
int[] results = new int[w + 1];
//填充一维数组
for (int i = 1; i <= g.length; i++) {
// 在计算下一行时,要从右向左统计
for (int j = w; j >= 1; j--) {
if (j >= p[i - 1]) {
// 1、不挖当前金矿
int v1 = results[j];
// 2、挖当前金矿
int v2 = results[j - p[i - 1]] + g[i - 1];
// 3、比较两种情况的收益
results[j] = Math.max(results[j], results[j - p[i - 1]] + g[i - 1]);
}
}
}
//返回最后1个格子的值
return results[w];
}
从右向左原因:在计算v2值时,要获取上一行的旧数据(比当前下标小),如果从左往右统计,当前位置左侧的数据已经全是本行的新数据,获取不到上一行的旧数据导致结果错误