题目描述

牛客原题: ✅ 补充题 180. 过桥的最少踩石数

桥长 L <= 10^9,从 0 开始,每次向右跳 S…T 个单位,1 <= S <= T <= 10。到达或越过 L 即完成。

桥上最多 100 颗石子,起终点没有石子。返回最少踩到多少颗。

示例 1:

输入: L = 10, S = 2, T = 3, stones = [2,3,5,6,7]
输出: 2
解释: 可沿 0→2→4→7→10 跳跃,仅踩到位置 2、7 的石子。

提示:

  • 1≤L≤10^9,1≤S≤T≤10。
  • 最多 100 颗石子,起点和终点没有石子。
  • 到达或越过 L 即完成,不要求恰好落在 L。

题意分析

桥长可达十亿,不能直接对每个原始坐标做动态规划,但石子只有至多 100 颗。代价只在落到石子时增加,足够长的无石子间隔只负责连接两侧,可以先压缩这些间隔。

固定步长 S == T 时落点余数无法改变,不能随意缩短间隔;直接统计石子中位置为 S 倍数的点即可。

解法:压缩长空段后做落点 DP

核心思路

[!blue]

S < T 时至少能跳 S 和 S+1。任意 D >= S(S-1),写成 D = qS+r 后有 q >= r,可改写为 (q-r)S+r(S+1),所以足够长的空段中不再存在不可达的距离余数。

对相邻石子以及末石子到桥尾的间隔,保留至多 T²。这个长度在扣除空段两侧一次跳跃所影响的边界范围后,仍覆盖上述连续可达阈值,因而保留两端落点间的连接能力,同时不增加踩石代价。

压缩后 dp[x] 表示恰好落在位置 x 的最少踩石数,初始只有 dp[0]=0 可达。从 x-step 转移,当前位置有石子才加 1。完成条件是到达或越过桥尾,最后一跳最多长 T,所以答案取 [end,end+T-1] 的最小值,而不是只取 dp[end]。

解题步骤

  1. S=T 时只统计位于步长整数倍处的石子。
  2. 其余情况排序石子,把每段空隙截到 T²,并处理最后一颗石子到桥尾的间距。
  3. 用 dp[x] 表示落在 x 的最小踩石子数,从 x-T 至 x-S 转移。
  4. 首次越过桥尾只可能落在终点至终点+T-1,取其中最小值。

代码实现

class Solution {
    public int minStones(int length, int s, int t, int[] stones) {
        if (s == t) {
            int count = 0;

            for (int x : stones) {
                if (x % s == 0) {
                    count++;
                }
            }

            return count;
        }

        Arrays.sort(stones);
        int cap = t * t;
        int position = 0;
        int previous = 0;
        Set<Integer> marked = new HashSet<>();

        for (int x : stones) {
            position += Math.min(cap, x - previous);
            marked.add(position);
            previous = x;
        }

        int end = position + Math.min(cap, length - previous);
        int[] dp = new int[end + t];

        Arrays.fill(dp, stones.length + 1);
        dp[0] = 0;

        for (int i = 1; i < dp.length; i++) {
            for (int step = s; step <= t && step <= i; step++) {
                dp[i] = Math.min(dp[i], dp[i - step] + (marked.contains(i) ? 1 : 0));
            }
        }

        int answer = stones.length + 1;

        for (int i = end; i < dp.length; i++) {
            answer = Math.min(answer, dp[i]);
        }

        return answer;
    }
}
import "sort"

func minStones(length, s, t int, stones []int) int {
    if s == t {
        count := 0
        for _, x := range stones {
            if x%s == 0 {
                count++
            }
        }
        return count
    }
    sort.Ints(stones)
    cap, position, previous := t*t, 0, 0
    marked := map[int]bool{}
    for _, x := range stones {
        position += min(cap, x-previous)
        marked[position] = true
        previous = x
    }
    end := position + min(cap, length-previous)
    dp := make([]int, end+t)
    for i := range dp {
        dp[i] = len(stones) + 1
    }
    dp[0] = 0
    for i := 1; i < len(dp); i++ {
        cost := 0
        if marked[i] {
            cost = 1
        }
        for step := s; step <= t && step <= i; step++ {
            dp[i] = min(dp[i], dp[i-step]+cost)
        }
    }
    answer := len(stones) + 1
    for i := end; i < len(dp); i++ {
        answer = min(answer, dp[i])
    }
    return answer
}

复杂度分析

  • 时间复杂度:$O(M \log (M+1)+(M+1)T^3)$;S=T 的分支只需 $O(M)$ 时间。
  • 空间复杂度:额外空间 $O((M+1)T^2)$;S=T 的分支只需 $O(1)$ 额外空间。

设 M 为石子数。压缩后的坐标长度为 $O((M+1)T^2)$,包含桥尾的最后一段。

关键点总结

[!green]

S<T 时可以使用连续跳长 S、S+1;任意 D≥S(S-1) 可写成 qS+r,其中 q≥r,再改写为 (q-r)S+r(S+1)。T² 足够保留空段两侧边界窗口并连接中间可达距离,因此截短足够长的无石子空段不会改变最优踩石子数。

易错点总结

[!yellow]

不能按原L分配数组;固定步长时不可随意压缩余数;到达或越过终点都算成功。T²覆盖S与S+1形成连续可达距离的阈值及两侧至多T个位置的边界偏移。

相似题目

题目 难度 关联与区别
746. 使用最小花费爬楼梯 简单 都把踩到位置的代价放入状态转移,本题允许跳长范围且需先压缩巨大坐标。
403. 青蛙过河 困难 同样研究跳跃可达性,原题只能落在石子上,本题空位置也能落且踩石子才计成本。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/54402612
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!