黄金矿工问题【动态规化】

黄金矿工

很久很久以前,有一位国王拥有5座金矿,每座金矿的黄金储量不同,需要参与挖掘的工人人数也不同。例如有的金矿储量是500kg黄金,需要5个工人来挖掘;有的金矿储量是200kg黄金,需要3个工人来挖掘…如果参与挖矿的工人的总数是10。每座金矿要么全挖,要么不挖,不能派出一半人挖取一半的金矿。要求用程序求出,要想得到尽可能多的黄金,应该选择挖取哪几座金矿? 《漫画算法》

解题思路

动态规划:

对于最后的金矿,都有挖或者不挖的选项:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fTyBApBR-1614584588261)(https://secure-static.wolai.com/static/r4RfjqfoRpPSF1ZTBy2JE/image.png)]

对比两种情况的收益,最终可得到最大收益。

①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=0w=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(n1,w)(n1,w<p[n1])

  • 在常规情况下,具有两种最优子结构(挖当前金矿或不挖当前金矿

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(n1,w),F(n1,wp[n1]+g[n1)])(n>=1,p>=p[n1])

递归实现

/**
 * @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]);
}

二维数组实现

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2a9Qx9SK-1614584588264)(https://secure-static.wolai.com/static/j5BkyQ2MApYgRR82mwxKrs/image.png)]
从上图可发现在递归的时候出现了很多次的重复调用,为解决此问题我们可选择一个二维数组来记录中间数据,如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q9Xz4EOP-1614584588266)(https://secure-static.wolai.com/static/r4uCiWnJUKX9RidLRQZhLd/image.png)]
使用上面的方程填满这张表就可以得出最终数据:
在这里插入图片描述

/**
 * 获得金矿最优收益
 *
 * @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值时,要获取上一行的旧数据(比当前下标小),如果从左往右统计,当前位置左侧的数据已经全是本行的新数据,获取不到上一行的旧数据导致结果错误

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值