LeetCode每日一题,20251011
施咒的最大伤害
1. 问题描述
给定一个魔法师拥有多个伤害值不同的咒语,目标是最大化魔法师的伤害值总和,但有如下约束:
- 如果选择了伤害为
power[i]
的咒语,则无法选择伤害为power[i] - 2
、power[i] - 1
、power[i] + 1
或power[i] + 2
的咒语。 - 每个伤害值最多只能使用一次。
在这个问题中,您需要返回魔法师能使用的最大伤害值总和。
2. 解决思路
为了求解最大伤害值总和,我们可以使用动态规划(DP)结合二分查找来优化。
动态规划(DP):
我们用一个数组 f[i]
表示考虑前 i
个不同的伤害值时能得到的最大总伤害值。
对于每个伤害值 a[i]
,我们有两个选择:
- 跳过当前伤害值
a[i]
:此时的最大伤害为f[i-1]
。 - 选择当前伤害值
a[i]
:此时我们需要考虑前面已经选择的伤害值,前提是前面的伤害值不与当前的伤害值冲突。
二分查找:
为了高效地找到前一个不冲突的伤害值,我们使用二分查找:
- 对于当前伤害值
x = a[i]
,我们需要找出 最后一个小于x-2
的伤害值下标。 - 使用二分查找可以在对已排序的伤害值数组
a
进行查找时,节省时间复杂度。
注意点:
- 边界问题:在二分查找时,查找的位置是从
0
到i-1
,但动态规划的数组f[i]
是从1
开始的。为了处理这个偏移,我们在f
数组中的下标加1
。 - 在二分查找时,如果找不到符合条件的伤害值,应该将
l
设置为-1
,表示没有找到前一个兼容的伤害值。
3. 实现代码
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;class Solution {public long maximumTotalDamage(int[] power) {// 1️⃣ 统计每种伤害值的出现次数Map<Integer, Integer> cnt = new HashMap<>();for (int x : power) {cnt.merge(x, 1, Integer::sum); // 计数}// 2️⃣ 将伤害值提取出来并排序int n = cnt.size();int[] a = new int[n];int k = 0;for (int x : cnt.keySet()) {a[k++] = x;}Arrays.sort(a); // 将不同伤害值升序排列// 3️⃣ 动态规划数组,f[i] 表示考虑前 i 个不同伤害值的最大伤害值long[] f = new long[n + 1]; // 使用 f 数组来存储最大伤害值// 4️⃣ 动态规划:遍历每个伤害值并进行二分查找for (int i = 1; i <= n; i++) {int x = a[i-1];// 用二分查找找到最后一个小于 x-2 的位置int l = 0, r = i - 1;while (l < r) {int mid = (l + r + 1) >> 1; // 二分查找if (a[mid] < x - 2) l = mid; // 如果 a[mid] < x-2,继续向右查找else r = mid - 1; // 否则,缩小查找范围}// 边界处理:如果没有找到合适的位置,l 会被更新为 -1if (a[l] >= x - 2) l = -1;// 选择当前伤害值或跳过当前伤害值long take = f[l+1] + (long) x * cnt.get(x); // 选择当前伤害值long skip = f[i-1]; // 不选择当前伤害值f[i] = Math.max(skip, take); // 取最大值}// 5️⃣ 返回最大伤害值return f[n];}// 测试public static void main(String[] args) {int[] power = {1, 1, 3, 4};Solution sol = new Solution();System.out.println(sol.maximumTotalDamage(power)); // 输出 10}
}
4. 核心逻辑讲解
-
统计伤害值的出现次数:
我们通过Map<Integer, Integer>
来记录每个伤害值出现的次数,确保在之后的计算中,我们能够正确地处理相同伤害值的咒语。 -
排序:
将所有不同的伤害值排序,以便于后续的二分查找。 -
二分查找:
对于当前伤害值x = a[i]
,我们需要找到前一个不与它冲突的伤害值。在二分查找中,我们搜索 最后一个小于x-2
的下标。- 如果找到,说明前面的值可以与当前值共存;
- 如果找不到,说明当前值没有任何兼容的前驱伤害值。
-
动态规划状态转移:
take
:选择当前伤害值时,最大伤害为f[l+1] + (long) x * cnt.get(x)
,其中l+1
表示前l
组的最优解。skip
:跳过当前伤害值时,最大伤害为f[i-1]
。
-
边界处理:
- 在二分查找后,若没有找到适合的前驱位置,
l
会被更新为-1
,因此我们通过f[l+1]
来保证边界不出错。
- 在二分查找后,若没有找到适合的前驱位置,
5. 时间复杂度分析
-
统计伤害值出现次数:
这一步的时间复杂度是 O(n),其中n
是power
数组的长度。 -
排序:
将伤害值排序的时间复杂度是 O(n log n),其中n
是不同伤害值的数量。 -
动态规划与二分查找:
对每个伤害值执行一次二分查找,二分查找的时间复杂度是 O(log n),因此整体的时间复杂度为 O(n log n)。
因此,整个算法的时间复杂度为 O(n log n),这是一个高效的解决方案。