LeetCode 1345. 跳跃游戏 IV
题目描述


题意分析
从下标 0 出发,每步可以移到合法的左右相邻下标,或跳到任意另一个值相同的下标,求到达末尾的最少跳数。每次跳跃都算一步,跳得远并不增加代价。
解法:BFS + 同值下标分组
核心思路
[!blue]
把每个数组下标看作图节点,所有允许的跳跃都是代价为 1 的边。BFS 按距离逐层扩展,处理距离为
steps的节点时,新发现的邻居距离就是steps + 1,因此第一次处理到末尾时即可返回当前层数。左右邻居可以直接计算,同值邻居则预先用
indices[value]保存全部下标。一个值出现 $t$ 次时,每个位置都能跳到同组其他位置,如果反复扫描整组就会产生 $O(t^2)$ 的工作量。设某个值组第一次被展开时,当前节点的最短距离为 $d$。这次扫描后,组内每个节点都已以不超过 $d+1$ 的距离被发现。BFS 之后处理的同组节点距离不会小于 $d$,再从它跳到组内成员,也不可能得到比已有距离更短的结果。因此整组扫描一次后就能删除;这些节点自己的左右邻居仍会在各自出队时正常扩展。
visited在入队时就置为真,让左右移动和同值跳跃统一判重,保证每个下标只入队一次。每层开始固定当前队列长度size,只处理这批节点,期间追加的节点属于下一层;全部处理完后才增加steps。
解题步骤
- 预先建立完整值到下标列表。
- 按层扩展左右邻居与同值组,发现即标记。
- 同值组扫描后删除。
- 处理到末下标返回层数。
数组只有一个元素时,起点就是终点,直接返回 0。题目保证数组非空,而且一直向右移动总能到达末尾,因此正常输入一定会找到答案。
代码实现
class Solution {
public int minJumps(int[] arr) {
int n = arr.length;
if (n == 1) {
return 0;
}
Map<Integer, List<Integer>> indices = new HashMap<>();
for (int i = 0; i < n; i++) {
indices.computeIfAbsent(arr[i], key -> new ArrayList<>()).add(i);
}
Deque<Integer> queue = new ArrayDeque<>();
boolean[] visited = new boolean[n];
queue.offer(0);
visited[0] = true;
int steps = 0;
while (!queue.isEmpty()) {
int size = queue.size();
for (int i = 0; i < size; i++) {
int cur = queue.poll();
if (cur == n - 1) {
return steps;
}
offer(queue, visited, cur - 1);
offer(queue, visited, cur + 1);
List<Integer> same = indices.get(arr[cur]);
if (same != null) {
for (int next : same) {
offer(queue, visited, next);
}
// 该值组已全部发现,删除后避免再次扫描整组。
indices.remove(arr[cur]);
}
}
steps++;
}
return -1;
}
private void offer(Deque<Integer> queue, boolean[] visited, int idx) {
if (idx < 0 || idx >= visited.length || visited[idx]) {
return;
}
// 发现即标记,左右移动与同值跳跃统一判重。
visited[idx] = true;
queue.offer(idx);
}
}
func minJumps(arr []int) int {
n := len(arr)
if n == 1 {
return 0
}
indices := make(map[int][]int)
for i, num := range arr {
indices[num] = append(indices[num], i)
}
queue := []int{
0,
}
visited := make([]bool, n)
visited[0] = true
offer := func(idx int) {
if idx < 0 || idx >= n || visited[idx] {
return
}
// 发现即标记,左右移动与同值跳跃统一判重。
visited[idx] = true
queue = append(queue, idx)
}
steps := 0
for len(queue) > 0 {
size := len(queue)
for i := 0; i < size; i++ {
cur := queue[i]
if cur == n-1 {
return steps
}
offer(cur - 1)
offer(cur + 1)
for _, next := range indices[arr[cur]] {
offer(next)
}
// 该值组已全部发现,删除后避免再次扫描整组。
delete(indices, arr[cur])
}
queue = queue[size:]
steps++
}
return -1
}
复杂度分析
- 时间复杂度:期望 $O(n)$。每个下标最多入队一次,左右邻居各检查一次;所有同值列表总长度为 $n$,且每组只扫描一次。哈希表操作按平均常数时间计算。
- 空间复杂度:$O(n)$,分组、访问与队列。
关键点总结
[!green]
- 节点访问标记与值组是否展开是两类状态。
- 同值跳跃可跨到任一同值位置,不局限最近者。
- Go 代码在一层结束后执行
queue = queue[size:],留下的正是本轮追加的下一层节点。
易错点总结
[!yellow]
- 重复扫描同值列表,会在大量重复值时退化为平方工作量。
- 只保留向右移动,会漏掉先远跳再左移的最优路径。
- 每出队一个节点就增加步数,会把节点数量当距离。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 815. 公交路线 | 困难 | 同样通过共享属性形成大量隐式连接,某个同值组或公交线路展开一次后应避免重复扫描。 |
| 1306. 跳跃游戏 III | 中等 | 同样在数组下标图上BFS,本题额外允许跳到任意同值位置,边的生成方式不同。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!