具体描述
代码
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。 - 在此约束下,最大化增加的攻击力。
思路分析
-
状态表示:我们使用动态规划(Dynamic Programming, DP)来表示每种状态。具体地,定义
dp[i][j]表示拥有i个金币且攻击速度为j时的最大攻击力。i的范围是 [0, g](金币的数量)。j的范围是 [0, maxSpeed](攻击速度的值)。为了避免负数速度,我们将攻击速度偏移,使其从 0 开始表示。
-
转移方程:
- 对于每一个能力
(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为新的攻击速度,需要确保它不为负值。
- 对于每一个能力
-
初始化:
- 初始状态
dp[g][s] = 0,表示初始时拥有g金币、攻击速度为s,攻击力为 0。
- 初始状态
-
限制条件:
- 在更新状态时,攻击速度不能小于 0,因此在
dp[i][j]更新时,要保证j + s >= 0。
- 在更新状态时,攻击速度不能小于 0,因此在
-
目标:
- 最终要找出在所有状态中,攻击力的最大值。
解法分析
-
初始化 DP 数组:
dp[i][j]数组初始为 -1,表示状态不可达。仅dp[g][s] = 0表示初始状态可达。
-
遍历能力项:
- 对于每个能力
(c, s, d),遍历dp数组,从后向前遍历金币和攻击速度,确保每个能力只能被购买一次。
- 对于每个能力
-
边界检查:
- 更新
dp[i - c][j + s]时,检查j + s >= 0和j + 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 数组,确保每个能力只购买一次。具体地:
- 金币:从
g到cost(能力所需金币)倒序遍历,以避免在一次更新中多次购买同一能力。 - 攻击速度:从最大速度
maxSpeed到0倒序遍历,确保每次更新时新的攻击速度是基于旧的状态。
每次更新时,假设当前有 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 + 1乘maxSpeed的二维 DP 数组,空间复杂度为O(g * maxSpeed)。
总结
这道题是一个典型的 0/1 背包问题,但与传统的背包问题不同的是,这里不仅要考虑金币消耗,还要考虑攻击速度的变化,并且攻击速度不能低于 0。因此,在解这道题的过程中,充分体现了动态规划在处理具有多个约束条件问题时的能力。