2024字节青训营笔记(六)小M的能力选择挑战 | 豆包MarsCode AI刷题

290 阅读7分钟

题目描述:

小M最近在玩一款叫做“狼人拯救者”的游戏。在游戏中,玩家可以通过消耗金币来购买能力,这些能力会增加攻击力,但也会影响攻击速度。小M是一个以攻击力为优先的玩家,但他必须保证自己的攻击速度不能低于0,因为攻击速度为负时将无法攻击。

现在小M面对商店中的N种能力,他拥有G枚金币和S点初始攻击速度。他想知道,在保持攻击速度大于等于0的前提下,他最多可以获得多少攻击力。

商店中每种能力用三元组表示为 array[i] = [c, s, d],其中:

  • c 表示购买该能力需要的金币数;
  • s 表示该能力对攻击速度的影响,可以为正数、零或负数;
  • d 表示该能力对攻击力的增加值。

小M需要通过合理的选择能力,使得在花费不超过G枚金币的情况下,攻击速度始终保持在0或以上,并且攻击力最大化。

测试样例

样例1:

输入:n = 6 ,g = 100 ,s = 100 ,array = [[71, -51, 150], [40, 50, 100], [40, 50, 100], [30, 30, 70], [30, 30, 70], [30, 30, 70]] 输出:240

样例2:

输入:n = 6 ,g = 100 ,s = 100 ,array = [[71, -51, 150], [40, -50, 100], [40, -50, 100], [30, -30, 70], [30, -30, 70], [30, -30, 70]] 输出:210

样例3:

输入:n = 5 ,g = 50 ,s = 50 ,array = [[25, -25, 100], [25, -25, 50], [20, 0, 60], [15, -15, 40], [10, -5, 30]] 输出:170

解题思路

我们发现,每种能力只有选和不选两种状态,而且是需要决策的,所以背包是最好的选择。进一步分析发现,本题有两个条件限制,不难想到二维费用01背包。

首先进行状态定义

  • 我们可以定义一个二维DP数组 dp[i][j]dp[i][j] 表示当前使用的金币数为 i ,当前的攻击速度为j时 ,玩家攻击力的最大值。

本题的难点有两个

  1. 负数如何处理?
    • 带负数体积的二维01背包。其实除了数组下标取值可以小于0之外和正常的就没区别了。所以只要处理一下标越界的问题。只需要让该取值整体平移D个单位,即初始状态为 dp[0][D+S] = 0,表示不花费金币时,攻击力为 0 ,攻击速度为初始值 D+S
  2. 状态转移方程是什么?
    • 最原始的转移方程: dp[i][j][k+D]=max(dp[i1][j][k+D],dp[i1][jc[i]][ks[i]+D]+d[i])dp[i][j][k+D]=max(dp[i−1][j][k+D],dp[i−1][j−c[i]][k−s[i]+D]+d[i])
    • 因为dp方程只与前一个状态有关,可以去掉第一维度,优化空间。得到 dp[j][k+D]=max(dp[j][k+D],dp[jc[i]][ks[i]+D]+d[i])dp[j][k+D]=max(dp[j][k+D],dp[j−c[i]][k−s[i]+D]+d[i])
      • 此时金币 j 需要倒序枚举。
      • 当速度 s[i]>=0 时,需要倒序枚举k
      • 当速度 s[i]<0 时,需要正序枚举k (因为此时k-s[i]>=0,从后面转移过来)。
// 定义solution函数,接收能力数量n、金币数g、初始攻击速度s和能力数组array
int solution(int n, int g, int s, vector<vector<int>> array) {

    // 定义一个常量D,用于处理攻击速度的偏移量
    int D = 1000;
    
    // 定义一个二维动态规划数组dp,大小为(g+1) x (2*D + s + 1000)
    // dp[i][j]表示使用i枚金币和攻击速度为j时的最大攻击力
    vector<vector<int>> dp(g + 1, vector<int>(2 * D + s + 1000, -1e9));

    // 初始状态:不花费金币时,攻击力为0,攻击速度为初始值s
    dp[0][D + s] = 0;
    
    // 遍历每一种能力
    for (int i = 0; i < n; i++) {
        // 遍历当前金币数,从g到array[i][0](即当前能力所需金币数)
        for (int j = g; j >= array[i][0]; j--) {
            // 如果当前能力的攻击速度影响为正或零
            if (array[i][1] >= 0) {
                // 遍历当前攻击速度,从2*D+s到array[i][1]+s
                for (int k = 2 * D + s; k >= array[i][1] + s; k--) {
                    // 更新dp数组,选择购买当前能力或不购买
                    dp[j][k] = max(dp[j][k], dp[j - array[i][0]][k - array[i][1]] + array[i][2]);
                }
            } else { // 如果当前能力的攻击速度影响为负
                // 遍历当前攻击速度,从s到2*D+array[i][1]+s
                for (int k = s; k <= 2 * D + array[i][1] + s; k++) {
                    // 更新dp数组,选择购买当前能力或不购买
                    dp[j][k] = max(dp[j][k], dp[j - array[i][0]][k - array[i][1]] + array[i][2]);
                }
            }
        }
    }

    // 初始化最大攻击力为负无穷
    int ans = -1e9;
    
    // 遍历所有可能的金币数和攻击速度,找到最大攻击力
    for (int i = 0; i <= g; i++) {
        for (int j = D; j <= 2 * D + s; j++) {
            ans = max(ans, dp[i][j]);
        }
    }

    // 返回最大攻击力
    return ans;
}

本题总结

  1. 理解问题:我们需要在有限的预算(金币)内,选择一些能力,使得攻击力最大化,同时保证攻击速度不低于0。
  2. 数据结构选择:使用动态规划(DP)来解决这个问题。我们可以定义一个二维DP数组 dp[i][j],其中 i 表示当前使用的金币数,j 表示当前的攻击速度。
  3. 状态转移:对于每个能力,我们可以选择是否购买它。如果购买,我们需要更新当前的金币数和攻击速度,并计算新的攻击力。如果不购买,则保持当前状态。
  4. 边界条件:初始状态为 dp[0][S] = 0,表示不花费金币时,攻击力为0,攻击速度为初始值S。
  5. 最终结果:遍历所有可能的金币数和攻击速度,找到攻击力的最大值。

通过上述步骤,我们可以有效地解决这个问题,找到在给定条件下攻击力的最大值。

对于动态规划的思考

动态规划(Dynamic Programming, DP)是一种通过将复杂问题分解为更简单的子问题来解决的算法技术。在解决动态规划问题时,通常可以分为以下三个关键步骤:

1. 确定如何表示状态

状态表示是动态规划的基础,它决定了如何存储和访问子问题的解。状态通常是一个或多个变量的组合,这些变量描述了问题的当前状态。例如,在背包问题中,状态可能表示为当前背包的容量和当前考虑的物品。在确定状态时,需要确保状态能够唯一地表示问题的子集,并且能够通过这些状态推导出最终问题的解。

2. 推导状态转移方程

状态转移方程描述了如何从一个状态转移到另一个状态。它通常基于问题的递推关系,即当前状态的解可以通过之前状态的解计算得到。推导状态转移方程时,需要考虑问题的所有可能情况,并确保每种情况都被正确处理。例如,在最长递增子序列问题中,状态转移方程可能表示为当前元素是否被选中,以及如何更新最长子序列的长度。

3. 思考动规初始状态

初始状态是动态规划的起点,它决定了如何开始计算状态转移。初始状态通常是问题中最简单的情况,可以直接给出解。例如,在斐波那契数列问题中,初始状态是前两个数 F(0)F(1)。在确定初始状态时,需要确保所有可能的状态都能从初始状态开始,通过状态转移方程逐步推导出来。

通过这三个步骤,可以系统地解决动态规划问题。首先,确定状态表示,确保能够唯一地描述问题的子集;然后,推导状态转移方程,确保能够通过之前的状态计算当前状态;最后,确定初始状态,确保能够从初始状态开始逐步推导出最终解。这三个步骤相互依赖,缺一不可,是解决动态规划问题的核心。