BAT 面试之动态规划(二)腾讯笔试中的背包问题应用

版权声明:尊重原创,码字不易,转载请注明出处。 https://blog.csdn.net/Xu_JL1997/article/details/89222577

题目关键词:角色扮演、金钱、贿赂、多对一(咦!?)
在这里插入图片描述

题目

小Q在玩闯关游戏,会在关卡依次遇到 nn 只怪兽,每只怪兽都会有自己的武力值。为了顺利闯关,小Q需要使用金币贿赂怪兽,并携带被贿赂的怪兽继续闯关。如果携带的所有怪兽的总武力值低于遇到的怪兽的武力值,必须继续贿赂。请问,小Q至少使用多少枚金币才能成功闯关?

输入输出

怪兽的数量 nn,每只怪兽的武力值,贿赂所需要的金币数量。

// 一个例子
int n = 5;
int[] power = {4,1,1,1,5}; // 武力值数组
int[] cost = {1,2,2,2,3}; // 需要的金币数组
// 最少使用3枚金币,贿赂前两只怪物即可

分析

比较典型的对背包问题的应用,怎么划分子问题是解决这道题的关键。

看这道题之前推荐大家看一下背包问题,如果你对它比较陌生的话,我相信这篇文章能帮到你:BAT 面试之动态规划(一)详解背包问题

对于武力值超过自身的怪兽,我们必须选择收买。对于武力值低于自己的怪兽,我们需要权衡之后的情况,有可能收买这只怪兽对我们毫无意义,也有可能它能助我们以最低的开销闯过后面的关卡。

就像背包问题中的背包容量是有限的,我们希望在有限的背包空间中尽可能使装入的物品价值达到最大。在这个问题中,我们的金币的数量也是有限的,最大值就是所有怪兽都同时收买花费的金币 max_coinsmax\_coins,我们希望使用尽可能少的金币解决问题。

在考察前 i1i-1 只怪兽能不能解决问题的时候,我们的标准是携带怪兽的总武力值能否大于第 ii 只怪兽。如果能,我们不需要收买第 ii 只怪兽,反之,我们就要收买它,携带的总武力值也会发生变化。所以,类比背包问题,我们发现实际上需要存储的中间结果就是携带怪兽的总武力值。

状态转换方程为:
P[i][j]=max(P[i1][j],P[i1][jcost[i]]+power[i])P[i][j] = max(P[i-1][j],P[i-1][j-cost[i]]+power[i])

P[i][j]P[i][j] 使用 jj 枚金币闯过前 i1i-1 只怪兽的关卡后携带的武力值。

为什么是最大武力值?

这是一个隐藏的优化目标,可以理解为武力值越大,局势就越倾向于对我们有利的方向,就越有可能解决下一个子问题。

状态方程的怎么来的?

假如使用 jj 枚金币闯过前 i1i-1 只怪兽的关卡后携带的武力值大于第 ii 只怪兽,我们就可以令 P[i][j]=P[i1][j]P[i][j] = P[i-1][j]

但是也考虑到现在既然有 jj 枚金币,那么若我们拿出其中的 cost[i]cost[i] 枚收买第 ii 只怪兽,最终的武力值会不会更优?我们需要考察 jcost[i]j-cost[i] 枚金币闯过前 i1i-1 关后的武力值加上第 ii 只怪兽的武力值后得到的值,即 P[i1][jcost[i]]+power[i]P[i-1][j-cost[i]]+power[i] 会不会大于 P[i1][j]P[i-1][j]

优化与实现

在实现的时候,我们会遇到子问题无法解决的情况,因为 P[i][j]P[i][j] 是表示使用 jj 枚金币闯过前 ii 只怪兽的关卡后携带的武力值,jj 的取值在 0~max_coinsmax\_coins 之间,所以 jj 并不总是足够闯过 ii 只怪兽,比如 j=0j = 0 的时候。

我们可以使用 -1 表示子问题无法解决,那么最终的结果就是在最后一行从前往后搜索第一个不是 -1 的值,返回对应的 jj,这就是闯过前 ii 关最小的 jj 取值。

优化之后使用一维数组解决问题,状态转换方程为:
P[j]=max(P[j],P[jcost[i]]+power[i]) P[j] = max(P[j],P[j-cost[i]]+power[i])

怎么优化,我上面推荐的文章也有叙述,这里简单地说,就是状态的更新只依赖于上一行,且每次下标都刚好错开,我们使用一维数组从后往前更新就能实现二维数组一样的效果。

毫无疑问,使用一维数组在空间上的开销是二维数组的 1n\frac{1}{n},这种优化是我们必须掌握的,尤其是在面试笔试中。优化也并不难,只是将行标 ii 的部分去除,其他部分没有区别。

下面是实现的 Java 代码:

	/*
	 * 使用int数组表示中间结果
	 */
    private static int MinCoins(int n, int[] power, int[] cost) {
        if (power.length == 0) return 0;
        int max = 0;
        for (int c : cost) max += c;  // 最多需要max枚金币

        int[] P = new int[max + 1];
        for (int p : P) p = -1;  // 初始化-1
        for (int i = 0; i < n; i++) {
            for (int j = max; j >= 0; j--) { // 从后往前更新
                int temp1 = P[j] >= power[i] ? P[j] : -1; // 武力值要大于遇到的怪兽
                int temp2 = (j >= cost[i]) && (P[j - cost[i]]!= -1) ? // 子问题必须已经解决
                        P[j - cost[i]] + power[i] : -1;

                P[j] = temp1 > temp2 ? temp1 : temp2;
            }
        }
        // 结果
        int index = 0;
        for (; index < max+1;index++) {
            if(P[index] != -1) break;
        }
        return index;
    }

参考

BAT 面试之动态规划(一)详解背包问题
博客园:腾讯2019实习笔试


正文结束,欢迎留言。

展开阅读全文

没有更多推荐了,返回首页