题目描述:
小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时 ,玩家攻击力的最大值。
本题的难点有两个:
- 负数如何处理?
- 带负数体积的二维01背包。其实除了数组下标取值可以小于0之外和正常的就没区别了。所以只要处理一下标越界的问题。只需要让该取值整体平移
D个单位,即初始状态为dp[0][D+S] = 0,表示不花费金币时,攻击力为0,攻击速度为初始值D+S
- 带负数体积的二维01背包。其实除了数组下标取值可以小于0之外和正常的就没区别了。所以只要处理一下标越界的问题。只需要让该取值整体平移
- 状态转移方程是什么?
- 最原始的转移方程:
- 因为dp方程只与前一个状态有关,可以去掉第一维度,优化空间。得到 。
- 此时金币
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;
}
本题总结
- 理解问题:我们需要在有限的预算(金币)内,选择一些能力,使得攻击力最大化,同时保证攻击速度不低于0。
- 数据结构选择:使用动态规划(DP)来解决这个问题。我们可以定义一个二维DP数组
dp[i][j],其中i表示当前使用的金币数,j表示当前的攻击速度。 - 状态转移:对于每个能力,我们可以选择是否购买它。如果购买,我们需要更新当前的金币数和攻击速度,并计算新的攻击力。如果不购买,则保持当前状态。
- 边界条件:初始状态为
dp[0][S] = 0,表示不花费金币时,攻击力为0,攻击速度为初始值S。 - 最终结果:遍历所有可能的金币数和攻击速度,找到攻击力的最大值。
通过上述步骤,我们可以有效地解决这个问题,找到在给定条件下攻击力的最大值。
对于动态规划的思考
动态规划(Dynamic Programming, DP)是一种通过将复杂问题分解为更简单的子问题来解决的算法技术。在解决动态规划问题时,通常可以分为以下三个关键步骤:
1. 确定如何表示状态
状态表示是动态规划的基础,它决定了如何存储和访问子问题的解。状态通常是一个或多个变量的组合,这些变量描述了问题的当前状态。例如,在背包问题中,状态可能表示为当前背包的容量和当前考虑的物品。在确定状态时,需要确保状态能够唯一地表示问题的子集,并且能够通过这些状态推导出最终问题的解。
2. 推导状态转移方程
状态转移方程描述了如何从一个状态转移到另一个状态。它通常基于问题的递推关系,即当前状态的解可以通过之前状态的解计算得到。推导状态转移方程时,需要考虑问题的所有可能情况,并确保每种情况都被正确处理。例如,在最长递增子序列问题中,状态转移方程可能表示为当前元素是否被选中,以及如何更新最长子序列的长度。
3. 思考动规初始状态
初始状态是动态规划的起点,它决定了如何开始计算状态转移。初始状态通常是问题中最简单的情况,可以直接给出解。例如,在斐波那契数列问题中,初始状态是前两个数 F(0) 和 F(1)。在确定初始状态时,需要确保所有可能的状态都能从初始状态开始,通过状态转移方程逐步推导出来。
通过这三个步骤,可以系统地解决动态规划问题。首先,确定状态表示,确保能够唯一地描述问题的子集;然后,推导状态转移方程,确保能够通过之前的状态计算当前状态;最后,确定初始状态,确保能够从初始状态开始逐步推导出最终解。这三个步骤相互依赖,缺一不可,是解决动态规划问题的核心。