贪心算法实战:数组标记与分数计算(LeetCode 同类题解析)
一、问题描述
给定一个正整数数组 nums
,按以下规则计算最终分数:
- 初始分数
score = 0
- 每次选择最小且未被标记的元素(值相同选下标最小)
- 选中元素值加入
score
,并标记该元素及其相邻元素 - 重复直到所有元素被标记
示例:
输入:[2, 1, 3, 4, 5]
输出:1 + 3 + 5 = 9
(解析:先选下标 1 的 1,标记 0/1/2;剩下下标 3/4,选 3 的 4 会被跳过(下标 3 未标记但选最小的 4?不,剩下元素是 4 和 5,最小是 4 但下标 3 未被标记?哦原数组处理过程:
- 第一次选 1(下标 1),标记 0、1、2 → 剩下元素下标 3、4(值 4、5)
- 第二次选 4(下标 3),标记 2、3、4 → 但下标 2 已被标记,所以实际标记 3 和 4
- 分数累加 1+4? 等等原示例输出是 9,正确流程应该是:
第一次选 1(下标 1),score=1,标记 0、1、2
剩下下标 3(4)、4(5),下一次选 4(下标 3),但此时下标 3 未被标记,加入 score=1+4=5,标记 2、3、4(但 2 已标记)
最后剩下下标 4 已被标记,结束? 这显然和示例输出不符。哦原示例的正确流程应该是:
数组[2,1,3,4,5]
第一次选最小 1(下标 1),score=1,标记 0、1、2 → 剩余下标 3、4(值 4、5)
第二次选最小 4(下标 3),score=1+4=5,标记 2、3、4 → 但下标 2 已标记,实际标记 3 和 4
此时所有元素已标记,最终 score=5? 这说明我之前的理解有误。正确的示例应该是:
正确示例解析:
输入 [2,1,3,4,5]
- 第一次选下标 1 的 1(最小),标记 0、1、2 → 剩余元素下标 3(4)、4(5)
- 第二次选下标 3 的 4(当前最小),标记 2、3、4 → 但 2 已标记,实际标记 3 和 4
- 分数累加 1+4=5? 但题目示例的预期输出是 9,这说明我的理解错误。哦,原题的正确规则是:标记被选中元素,如果有相邻元素,则同时标记与它相邻的两个元素。即选中元素本身必须被标记,相邻的左右元素(如果存在)也被标记。例如:
选中元素下标 i,标记 i,以及 i-1 和 i+1(如果存在)。
所以正确流程:
[2,1,3,4,5]
- 选下标 1(值 1),score=1,标记 0、1、2
- 剩余下标 3、4(值 4、5),选下标 3(值 4),score=1+4=5,标记 2、3、4(但 2 已标记)
- 所有元素已标记,结束。但预期输出是 9,这说明示例可能不同。哦,原题可能有不同的示例,比如官方示例:
比如 LeetCode 2583. 统计公平数对的数目(假设本题是类似题目),正确示例应该是:
输入:[2,1,3,5,4]
处理流程:
选 1(下标 1),标记 0、1、2 → score=1
剩余下标 3(5)、4(4),选 4(下标 4),标记 3、4、5(不存在 5)→ 标记 3、4 → score=1+4=5
但实际正确输出可能需要重新确认。这里可能用户提供的示例需要明确,假设正确的示例是输入[1,2,3,4,5]
,输出 1+3+5=9:
- 选 1(下标 0),标记 - 1(无)、0、1 → 标记 0 和 1
- 剩余下标 2、3、4,选 3(下标 2),标记 1、2、3 → 1 已标记,标记 2 和 3
- 剩余下标 4,选 5(下标 4),标记 3、4、5(无)→ 标记 4
- 总 score=1+3+5=9
这说明正确的逻辑是:每次选中未被标记的最小元素,标记自己和相邻元素,无论相邻是否已被标记。
二、核心思路:贪心 + 优先队列
为什么用优先队列?
每次需要快速找到未被标记的最小元素(值优先,下标次优先),优先队列(最小堆)天然适合这种场景。
关键步骤:
- 元素存储:将元素值和下标打包存入堆,堆排序规则:先比较值,值相同比较下标(确保下标小的优先)。
- 标记处理:使用布尔数组记录是否被标记。取出堆顶元素时,若已被标记则跳过(说明其相邻元素已被处理)。
- 相邻标记:选中元素后,标记自己及左右邻居(无论是否越界,布尔数组自动处理边界)。
算法流程图:
初始化堆 → 存入所有(值, 下标)
初始化标记数组
score = 0
while 堆不为空:取出堆顶(当前最小未标记元素)if 未被标记:score += 值标记当前下标及左右邻居
三、代码实现(Java)
java">import java.util.PriorityQueue;class Solution {public static long findScore(int[] nums) {int n = nums.length;boolean[] marked = new boolean[n];// 优先队列:先按值升序,值相同按下标升序PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> {if (a[0] != b[0]) return a[0] - b[0]; // 值小优先return a[1] - b[1]; // 下标小优先});for (int i = 0; i < n; i++) {pq.offer(new int[]{nums[i], i}); // 存入(值,下标)}long score = 0;while (!pq.isEmpty()) {int[] curr = pq.poll();int val = curr[0], idx = curr[1];if (!marked[idx]) { // 未被标记才处理score += val;marked[idx] = true; // 标记自己if (idx > 0) marked[idx - 1] = true; // 左邻居if (idx < n - 1) marked[idx + 1] = true; // 右邻居}}return score;}
}
四、代码细节解析
1. 优先队列的比较器
(a, b) -> a[0] - b[0]
:保证值小的先出队- 值相同则比较下标:
a[1] - b[1]
,确保下标小的优先(符合题目要求)
2. 标记逻辑
- 选中元素后,必须标记自己(即使没有相邻元素)
- 左右邻居的标记无需判断是否越界:
idx > 0
和idx < n-1
确保数组不越界
3. 跳过已标记元素
堆中可能存在已被标记的元素(因为相邻元素被处理时连带标记),取出时需检查 marked[idx]
,跳过已处理的元素。
五、复杂度分析
- 时间复杂度:O(n log n)
- 堆插入:O (n log n)
- 堆弹出:每个元素最多入堆 / 弹出一次,O (n log n)
- 空间复杂度:O(n)
- 标记数组:O (n)
- 堆存储:O (n)
六、测试用例
测试用例 1:基础情况
输入:[2,1,3,4,5]
处理流程:
- 选 1(下标 1),标记 0、1、2 → score=1
- 剩余下标 3(4)、4(5),选 4(下标 3)→ 已被标记?不,下标 3 未标记,因为第一次标记的是 0、1、2。所以第二次选 4(下标 3),标记 2、3、4 → 标记 3 和 4(2 已标记)
- score=1+4=5? 但根据正确逻辑,这里可能示例错误。正确的示例应该是:
输入:[1,2,3,4,5]
输出:1(下标 0)+3(下标 2)+5(下标 4)=9
处理流程:
- 选 1(下标 0),标记 - 1(无)、0、1 → 标记 0 和 1
- 剩余下标 2、3、4,选 3(下标 2),标记 1、2、3 → 标记 2 和 3
- 剩余下标 4,选 5(下标 4),标记 3、4、5 → 标记 4
- 总 score=1+3+5=9
测试用例 2:全相同元素
输入:[5,5,5]
输出:5(下标 0,标记 0、1)→ 剩余下标 2 已被标记?不,选下标 0,标记 0 和 1,下标 2 未被标记? 不,选中下标 0 后,标记 0 和 1(左右邻居),下标 2 未被标记。但堆中还有下标 1 和 2 的 5。取下标 1 时已被标记,跳过。取下标 2 的 5,未被标记,加入 score=5+5=10,标记 1、2、3(不存在)→ 标记 2。最终 score=10?
正确逻辑:
- 选下标最小的 0,标记 0 和 1 → score=5
- 下标 2 未被标记,选下标 2,标记 1 和 2 → score=5+5=10
输出 10。
七、常见错误点
- 优先队列排序错误:如果先比较下标再比较值,会导致值大的元素被优先处理,违反题意。
- 标记范围错误:忘记标记当前元素本身,只标记了相邻元素。
- 重复处理:未检查元素是否已被标记,导致重复累加分数。
八、优化思考
是否可以不用优先队列?
比如先排序数组,记录下标,然后按顺序处理未被标记的元素。但需要维护下标顺序,时间复杂度相同,但优先队列更简洁。
九、总结
本题的核心是贪心选择最小元素,结合优先队列高效获取最小未标记元素,通过标记数组避免重复处理。这种贪心策略确保每次选择最优解,最终得到全局最优分数。理解标记逻辑(自己 + 相邻)和优先队列的排序规则是解题关键。
适用场景:需要多次选择极值(最小 / 最大)并处理关联元素的问题,优先队列是常用工具。标记数组的使用在类似 “跳跃游戏”、“区间覆盖” 问题中也常见,值得举一反三。