矿工挖矿问题
- 该题有递归和非递归的实现,这里两种实现思路都给出来了
问题描述
- 有5个金矿,每个金矿黄金储量不同,需要参与挖掘的工人数目也不同,假定有10个工人,每个金矿要么全挖,要么不挖,不可以拆分,试问,想得到最多的黄金,应该选择挖取哪几个金矿。
- 金矿信息如下表:
算法思路
-
首先动态规划的三要素(最优子结构,边界,状态转移方程)中我们最先需要找到的就是状态转移方程,而状态转移方程又会涉及到子问题的最优结构,所以我们先看看子问题是什么?
-
在这里首先10个旷工是不会变的,无论挖几个矿都是这10个旷工,所以我们要去减少问题规模的话就是减少矿的个数。所以子问题为:10个旷工挖前 X 个矿最多可以赚多少钱! 实际上后面发现子问题还要被扩展为 w个旷工挖前n个矿最大钱数
-
在动态规划的算法设计中,我们往往不能够从第一步去开始分析,而是从最后一步开始分析!在这里就是分析已经计算完了10个旷工挖前4个矿最多赚多少钱,如何去计算挖前5个矿的最大钱数。而此时我们会面临两个抉择:
-
这里先设F(n,w)为 w 个工人挖 n 个矿的最大金钱数,p(i)为挖第 i 个矿需要的矿工人数,G(i)为第 i 个矿中的黄金数量
-
根据动态规划的算法,假设我们现在已经计算出了F(4,10),如何计算F(5,10),其实思路很简单,要不然就挖第五个矿,要不然就不挖第五个矿
-
如果不挖第五个矿:那么意思就是这十个人就挖前四个,计算出最大钱数
M1 = F(4,10) 、
-
如果挖第五个矿: 那就要将十个人分出3个人挖第五个矿,剩下的七个人挖前四个矿,那么此时计算出的最大钱数为:
M2 = F(4, 7)+ G(5) -
最终我们应该取M1和M2的最大值,即Max(M1,M2) ,推广到一般式就是
F(n,w) = max(F(n-1,w),F(n-1,w - p(i))+ G(i))
-
-
根据以上思路我们就可以得推广到一般情况,得出动态规划的三要素:
-
最优子结构:就是上面的F(n - 1,w),F(n -1,w-p(i))
-
边界:当只挖一个矿的时候F(n,w)
- n == 1时,w >= P【1】,F(n,w) = G【1】
- n == 1时,w < P【1】,F(n,w) = 0;
-
状态转移函数:
- n == 1时,w >= P【1】,F(n,w) = G【1】
- n == 1时,w < P【1】,F(n,w) = 0;
- n > 1时,w < P【n】,F(n,w) = F(n-1,w)
- n > 1时,w >= P【n】,F(n,w) = max(F(n-1,w),F(n-1,w - p(i))+ G(i))
-
-
这里稍微解释一下上面状态转移函数的第三种情况,例如我有一个这种情况F(2,4),四个人挖前两个矿,但是第二个矿至少要五个人,那么就不可能挖第二个矿,所以只能回归到子问题,F(1,4)。
-
以上来看动态规划的三要素都已经出来了,那么接下来就是实现了!
递归实现
public static int[] money = new int[]{400,500,200,300,350};
public static int[] workernums = new int[]{5,5,3,4,5};
/**
*
* @param w 几个工人
* @param n 挖几个矿
* @return
*/
public static int caculateMaxMoney(int w, int n){
if(n == 1 && w < workernums[0]) {
return 0;
}
if(n == 1 && w >= workernums[0]) {
return money[0];
}
//人数不够挖新的矿了
if(n > 1 && w < workernums[n - 1]){
return caculateMaxMoney(w, n-1);
}
if(n > 1 && w >= workernums[n - 1]){
int M1 = caculateMaxMoney(w - workernums[n - 1], n - 1) + money[n - 1];
int M2 = caculateMaxMoney(w,n-1);
return Math.max(M1,M2);
}
throw new IllegalArgumentException("no result!");
}
非递归实现
- 对于非递归实现,就需要设计一个备忘录,也就是一个dp矩阵,去存储子问题的计算结果
- 这里定义dp【6】【11】,dp【i】【j】表示 j 个矿工挖前 i 个金矿的最大价值
- 但是需要注意一点,我们需要对其进行初始化,特别是对i = 1行进行初始化,初始化后dp矩阵如下图,这里的初始化也就是上述状态方程中n == 1的前两个状态方程的计算。
- 之后开始两层遍历每一个dp【i】【j】,根据两个状态方程进行判断计算
public static int[] money = new int[]{400,500,200,300,350};
public static int[] workernums = new int[]{5,5,3,4,5};
public static int caculateMaxMoney1(){
int[][] dp = new int[6][11];
//dp矩阵初始化
for(int j = 1; j < 11; j++ ){
if(j >= workernums[0]) dp[1][j] = money[0];
else dp[1][j] = 0;
}
for(int i = 2; i < 6; i++){
for(int j = 1; j < 11; j++){
if(j < workernums[i - 1]) dp[i][j] = dp[i-1][j];
else {
dp[i][j] = Math.max(dp[i-1][j],dp[i - 1][j - workernums[i - 1]] + money[i - 1]);
}
}
}
return dp[5][10];
}
最后的dp矩阵如下:
总结:
- 对于动态规划的思路解法,往往不能从第一步开始看,而是要从最后一步开始看,是否可以找到子问题和状态转移方程