小M的能力选择挑战 | 豆包MarsCode AI刷题

48 阅读7分钟

具体描述

image.png

代码

import java.util.*;

public class Main {
    public static int solution(int n, int g, int s, int[][] array) {
        int maxSpeed = 100 * s + 1; 
        int[][] dp = new int[g + 1][maxSpeed];
        
        for (int i = 0; i <= g; i++) {
            Arrays.fill(dp[i], -1);
        }
        
        dp[g][s] = 0;
        
        for (int[] ability : array) {
            int cost = ability[0];
            int speed = ability[1];
            int atk = ability[2];
            
            for (int i = g; i >= cost; i--) {
                for (int j = maxSpeed - 1; j >= 0; j--) {
                    if (dp[i][j] != -1) {
                        int newSpeed = j + speed;
                        if (newSpeed >= 0 && newSpeed < maxSpeed) {
                            dp[i - cost][newSpeed] = Math.max(dp[i - cost][newSpeed], dp[i][j] + atk);
                        }
                    }
                }
            }
        }
        
        int maxAtk = 0;
        for (int i = 0; i <= g; i++) {
            for (int j = 0; j < maxSpeed; j++) {
                if (dp[i][j] != -1) {
                    maxAtk = Math.max(maxAtk, dp[i][j]);
                }
            }
        }
        
        return maxAtk;
    }

    public static void main(String[] args) {
        int[][] test1 = {
            {71, 51, 150},
            {40, 50, 100},
            {40, 50, 100},
            {30, 30, 70},
            {30, 30, 70},
            {30, 30, 70}
        };
        System.out.println(solution(6, 100, 100, test1) == 240);

        int[][] test2 = {
            {71, -51, 150},
            {40, -50, 100},
            {40, -50, 100},
            {30, -30, 70},
            {30, -30, 70},
            {30, -30, 70}
        };
        System.out.println(solution(6, 100, 100, test2) == 210);
    }
}

思路

这是一个典型的 0/1背包问题,其中:

  • 背包容量是金币数量 g,即最多可以花费 g 个金币购买能力。

  • 物品是商店中的每种能力,每种能力有以下三个特性:

    • c:能力所需的金币。
    • s:能力对攻击速度的影响,可能为正、负或零。
    • d:能力增加的攻击力。

问题要求

  • 小M必须保证攻击速度 s 不小于 0。
  • 在此约束下,最大化增加的攻击力。

思路分析

  1. 状态表示:我们使用动态规划(Dynamic Programming, DP)来表示每种状态。具体地,定义 dp[i][j] 表示拥有 i 个金币且攻击速度为 j 时的最大攻击力。

    • i 的范围是 [0, g](金币的数量)。
    • j 的范围是 [0, maxSpeed](攻击速度的值)。为了避免负数速度,我们将攻击速度偏移,使其从 0 开始表示。
  2. 转移方程

    • 对于每一个能力 (c, s, d),如果当前状态 dp[i][j] 可达(即 dp[i][j] != -1),则购买该能力后,新状态 dp[i - c][j + s] 的攻击力将更新为 max(dp[i - c][j + s], dp[i][j] + d)。其中 j + s 为新的攻击速度,需要确保它不为负值。
  3. 初始化

    • 初始状态 dp[g][s] = 0,表示初始时拥有 g 金币、攻击速度为 s,攻击力为 0。
  4. 限制条件

    • 在更新状态时,攻击速度不能小于 0,因此在 dp[i][j] 更新时,要保证 j + s >= 0
  5. 目标

    • 最终要找出在所有状态中,攻击力的最大值。

解法分析

  • 初始化 DP 数组

    • dp[i][j] 数组初始为 -1,表示状态不可达。仅 dp[g][s] = 0 表示初始状态可达。
  • 遍历能力项

    • 对于每个能力 (c, s, d),遍历 dp 数组,从后向前遍历金币和攻击速度,确保每个能力只能被购买一次。
  • 边界检查

    • 更新 dp[i - c][j + s] 时,检查 j + s >= 0j + s < maxSpeed
  • 输出最大攻击力

    • 遍历 dp 数组,寻找所有状态下的最大攻击力。

分步解析

1. 状态定义

定义状态 dp[i][j] 来表示在拥有 i 个金币,攻击速度为 j 时的最大攻击力。

  • i 表示剩余的金币数量,范围是 [0, g],表示从 0 到最大可用金币。
  • j 表示当前的攻击速度,为了避免处理负数,我们将速度做了偏移,使其从 0 开始表示,具体的偏移量是 maxSpeed

为什么要使用 maxSpeed 作为偏移量:

  • 原问题中的攻击速度 s 可以为负,因此我们需要在 dp 数组中确保速度的所有值都能作为有效索引。为避免负值,我们可以把 s 变换为一个新的表示方法。为了表示速度从负到正的变化,我们将攻击速度的最小值设置为 0,并且假设最大的速度不会超过 100 * s + 1,这是一个合理的假设范围。

2. 初始状态设置

初始化 dp 数组为 -1,表示每个状态是不可达的。

  • dp[g][s] = 0:这是初始状态,表示拥有 g 个金币,攻击速度为 s 时,攻击力为 0
for (int i = 0; i <= g; i++) {
    Arrays.fill(dp[i], -1); // 将所有状态初始化为 -1,表示不可达
}

dp[g][s] = 0;  // 初始状态:拥有 g 金币,速度为 s,攻击力为 0

3. 能力项的状态转移

对于每一个商店中的能力 (c, s, d),能力包含:

  • c:该能力的金币消耗。
  • s:该能力对攻击速度的影响,可以为负、零或正。
  • d:该能力带来的攻击力增加。

我们需要根据每个能力来更新 dp 数组。

3.1. 状态更新方式

我们从后向前遍历 dp 数组,确保每个能力只购买一次。具体地:

  • 金币:从 gcost(能力所需金币)倒序遍历,以避免在一次更新中多次购买同一能力。
  • 攻击速度:从最大速度 maxSpeed0 倒序遍历,确保每次更新时新的攻击速度是基于旧的状态。

每次更新时,假设当前有 dp[i][j] 这个状态,即拥有 i 个金币,攻击速度为 j,最大攻击力为 dp[i][j]。若此时购买当前能力 c,则状态转移为 dp[i - c][j + s],其中:

  • i - c 是新的金币数量,
  • j + s 是更新后的攻击速度。

我们还需要确保:

  • 更新后的速度 j + s 必须大于等于 0(攻击速度不能为负)。
  • 更新后的速度 j + s 不能超过最大速度 maxSpeed,即 0 <= j + s < maxSpeed
for (int i = g; i >= cost; i--) {  // 从后向前遍历金币
    for (int j = maxSpeed - 1; j >= 0; j--) {  // 从后向前遍历速度
        if (dp[i][j] != -1) { // 当前状态可达
            int newSpeed = j + speed;
            if (newSpeed >= 0 && newSpeed < maxSpeed) {
                dp[i - cost][newSpeed] = Math.max(dp[i - cost][newSpeed], dp[i][j] + atk);
            }
        }
    }
}

4. 找到最大攻击力

遍历整个 dp 数组,找到所有状态下的最大攻击力。我们只需要关注 dp[i][j] != -1 的状态,并从这些状态中找出最大的攻击力。

int maxAtk = 0;
for (int i = 0; i <= g; i++) {
    for (int j = 0; j < maxSpeed; j++) {
        if (dp[i][j] != -1) {
            maxAtk = Math.max(maxAtk, dp[i][j]);
        }
    }
}
return maxAtk;

5. 边界情况

5.1. 无法购买能力

如果商店中的能力都无法购买,或者所有的能力都导致攻击速度低于 0,那么 dp 数组中只有初始状态 dp[g][s] = 0 是有效的,最终结果就是 0

5.2. 攻击速度始终不受影响

如果所有能力对攻击速度的影响为零,那么这就变成了一个普通的背包问题,仅考虑金币和攻击力的最大化。


6. 时间复杂度分析

6.1. 外层循环:遍历能力项

  • 共有 n 个能力项,遍历一次能力项需要更新 dp 数组。

6.2. 内层循环:更新 dp 数组

  • 对于每个能力项,我们需要遍历 g 个金币和 maxSpeed 个速度状态。由于我们从后向前遍历,每次状态转移只更新一次,因此需要的时间为 O(g * maxSpeed)

时空复杂度

时间复杂度:

  • 对于每个能力项,我们需要遍历 g 个金币状态和 maxSpeed 个速度状态。因此,每个能力的处理时间复杂度为 O(g * maxSpeed)
  • 总共有 n 个能力项,因此总时间复杂度为 O(n * g * maxSpeed)

空间复杂度:

  • 我们使用了一个 g + 1maxSpeed 的二维 DP 数组,空间复杂度为 O(g * maxSpeed)

总结

这道题是一个典型的 0/1 背包问题,但与传统的背包问题不同的是,这里不仅要考虑金币消耗,还要考虑攻击速度的变化,并且攻击速度不能低于 0。因此,在解这道题的过程中,充分体现了动态规划在处理具有多个约束条件问题时的能力。