题目描述

✅ 403. 青蛙过河

image-20260928224011403

image-20260928224011406

题意分析

石头位置严格递增,青蛙从位置 0 出发,第一跳必须为 1。若上一跳长度为 k,下一跳只能为 k - 1、k 或 k + 1,并且必须向前落在石头上,判断能否到达最后一块石头。

解法:哈希表 + 逐步扩展

核心思路

[!blue]

只知道某块石头可达还不够:用不同步长到达这里,会得到不同的下一跳选择。状态必须同时记录「当前石头位置」和「上一跳长度」,而相同的这两个量对应完全相同的后续选择,可以合并去重。

用 reach[position] 保存能够到达该石头的所有步长。只为实际存在的石头建立集合,既能判断落点是否在水面上,也不用按可能很大的坐标范围开数组。初始只向 reach[0] 加入虚拟步长 0;枚举下一步并过滤非正数后,只剩长度 1,正好满足首跳要求。

对每个可达状态 (position, k),尝试 k - 1、k、k + 1。只有步长为正且 position + nextStep 确实有石头时,才把 nextStep 加入目标石头的集合。这些条件保证新增状态都对应合法的一跳;反过来,任何合法跳跃也一定是这三种选择之一,因此不会遗漏路径。

所有转移都指向更大的位置,按石头升序处理时,当前集合已经收到全部可能的前驱状态,之后也不会再被补充。它的转移只会修改未来石头的集合,不会修改正在遍历的集合。处理完后,只要最后一块石头的集合非空,就存在一条到达它的路径。

解题步骤

  1. 为每块石头建立空集合,并向起点集合加入 0。
  2. 按石头位置升序,枚举它的每个可达步长;空集合表示目前不可达,无需扩展。
  3. 枚举三种下一跳长度,跳过非正长度和不存在的落点,将合法步长加入目标集合。
  4. 返回最后一块石头的集合是否非空,不要求必须经过中间的每块石头。

代码实现

// 从起点 0 出发,初始步长为 0。
class Solution {
    public boolean canCross(int[] stones) {
        Map<Integer, Set<Integer>> reach = new HashMap<>();

        for (int stone : stones) {
            reach.put(stone, new HashSet<>());
        }

        // 虚拟零步长结合正步长过滤,使第一跳只能为一
        reach.get(0).add(0);

        for (int stone : stones) {
            Set<Integer> steps = reach.get(stone);

            for (int k : steps) {
                for (int delta = -1; delta <= 1; delta++) {
                    int nextStep = k + delta;

                    // 只向未来石头转移,不修改当前遍历中的步长集合
                    if (nextStep <= 0) {
                        continue;
                    }

                    int nextPos = stone + nextStep;

                    if (reach.containsKey(nextPos)) {
                        reach.get(nextPos).add(nextStep);
                    }
                }
            }
        }

        return !reach.get(stones[stones.length - 1]).isEmpty();
    }
}
// 从起点 0 出发,初始步长为 0。
func canCross(stones []int) bool {
    reach := make(map[int]map[int]bool, len(stones))
    for _, stone := range stones {
        reach[stone] = make(map[int]bool)
    }

    // 虚拟零步长结合正步长过滤,使第一跳只能为一
    reach[0][0] = true

    for _, stone := range stones {
        for step := range reach[stone] {
            for delta := -1; delta <= 1; delta++ {
                nextStep := step + delta
                // 只向未来石头转移,不修改当前遍历中的步长集合
                if nextStep <= 0 {
                    continue
                }

                nextPos := stone + nextStep
                if _, ok := reach[nextPos]; ok {
                    reach[nextPos][nextStep] = true
                }
            }
        }
    }

    return len(reach[stones[len(stones)-1]]) > 0
}

复杂度分析

  • 时间复杂度:期望 $O(n^2)$。每跳最多增加 1,且向前最多经过 n - 1 次跳跃,因此步长只有 $O(n)$ 种;每块石头的每个步长只做三次哈希操作。
  • 空间复杂度:$O(n^2)$,保存位置与步长状态。

关键点总结

[!green]

  • 位置与上一跳长度共同决定后续选择,不能只按位置去重。
  • 正步长让转移只写未来石头,保证升序处理的正确性,也避免修改当前集合。
  • 存在性只看末石能否以任意步长到达。

易错点总结

[!yellow]

  • 只记录位置,丢失后续跳跃限制。
  • 起点步长设为 1 会让统一转移允许首次跳 2;应使用虚拟步长 0。
  • 为水面创建新位置,改变了只能落石头的规则。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2022/88741267
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!