加油站问题:环形数组中的良好出发点

加油站问题:环形数组中的良好出发点

在算法面试中,加油站问题是一个经典的环形数组题目。今天我们要解决的是找出所有良好出发点的问题,这比LeetCode上只要求返回一个出发点的版本更加复杂和有趣。

问题描述

有 N 个加油站组成一个环形路线,给定两个长度都是 N 的非负数组:

  • oil[i]:第 i 个加油站存储的油量可以跑多少千米
  • dis[i]:第 i 个加油站到下一个加油站的距离(千米)

假设你有一辆油箱足够大的车,初始时车里没有油。如果从第 i 个加油站出发,最终能够回到这个加油站,那么第 i 个加油站就是良好出发点

要求:返回长度为 N 的 boolean 数组 res,其中 res[i] 表示第 i 个加油站是否是良好出发点。

直观理解

想象你开着一辆车在一个环形路线上行驶:

  • 每到一个加油站,你可以加满该站的所有油
  • 从一个站到下一个站需要消耗对应的距离油量
  • 如果在任何时刻油量变成负数,说明无法继续前进
  • 只有能完整绕一圈回到起点的出发点才算"良好"

暴力解法分析

最直接的想法是对每个加油站都尝试一次:

// 暴力解法(时间复杂度 O(N²))
public boolean[] bruteForce(int[] oil, int[] dis) {
    int n = oil.length;
    boolean[] result = new boolean[n];
    
    for (int start = 0; start < n; start++) {
        int currentOil = 0;
        boolean canComplete = true;
        
        // 尝试从start出发绕一圈
        for (int i = 0; i < n; i++) {
            int station = (start + i) % n;
            currentOil += oil[station] - dis[station];
            if (currentOil < 0) {
                canComplete = false;
                break;
            }
        }
        result[start] = canComplete;
    }
    return result;
}

问题:时间复杂度 O(N²),对于大数组效率太低。

优化思路:前缀和 + 单调队列

核心观察

  1. 净油量数组:定义 arr[i] = oil[i] - dis[i],表示在第 i 个站的净收益
  2. 环形处理:将数组复制一份,变成长度为 2N 的数组,这样就不用处理模运算
  3. 前缀和:计算前缀和数组,prefix[i] 表示从位置 0 到位置 i 的累计净油量
  4. 关键条件:从位置 i 出发能完成一圈,当且仅当在区间 [i, i+N-1] 内,任意位置 j 的前缀和都满足:prefix[j] - prefix[i-1] >= 0

数学转换

从位置 i 出发,到达位置 j 时的油量为:

oil[i] - dis[i] + oil[i+1] - dis[i+1] + ... + oil[j] - dis[j]
= prefix[j] - prefix[i-1]

要保证整个过程中油量不为负,需要:

min{prefix[j] - prefix[i-1]} >= 0, 其中 j ∈ [i, i+N-1]

等价于:

min{prefix[j]} >= prefix[i-1], 其中 j ∈ [i, i+N-1]

算法步骤

  1. 构建扩展数组:将净油量数组复制一份,长度变为 2N
  2. 计算前缀和:得到长度为 2N 的前缀和数组
  3. 滑动窗口最小值:使用单调队列维护每个长度为 N 的窗口内的最小前缀和
  4. 判断条件:对于每个起始位置 i,检查窗口最小值是否 >= prefix[i-1]

详细代码解析

public static boolean[] goodArray(int[] g, int[] c) {
    int N = g.length;
    int M = N << 1; // M = 2 * N
    
    // 步骤1:构建扩展的净油量数组并计算前缀和
    int[] arr = new int[M];
    for (int i = 0; i < N; i++) {
        arr[i] = g[i] - c[i];      // 原数组
        arr[i + N] = g[i] - c[i];  // 复制一份
    }
    
    // 计算前缀和
    for (int i = 1; i < M; i++) {
        arr[i] += arr[i - 1];
    }
    // 现在arr就是前缀和数组
    
    // 步骤2:初始化单调递增队列(维护窗口最小值)
    LinkedList<Integer> window = new LinkedList<>();
    
    // 初始化单调递增队列,用来维护第一个滑动窗口 [0, N-1] 中的最小值
    for (int i = 0; i < N; i++) {
        while (!window.isEmpty() && arr[window.peekLast()] >= arr[i]) {
            window.pollLast();
        }
        window.addLast(i);
    }
    
    // 步骤3:滑动窗口,检查每个起始位置
    boolean[] ans = new boolean[N];
    
    // offset = arr[i-1],即prefix[i-1]
    // i 是当前考虑的起始位置
    // j 是当前窗口的右边界
    for (int offset = 0, i = 0, j = N; j < M; offset = arr[i++], j++) {
        // 检查当前窗口 [i, j-1] 的最小值是否 >= offset
        if (arr[window.peekFirst()] - offset >= 0) {
            ans[i] = true;
        }
        
        // 移除窗口左边界的元素(如果它在队列头部)
        if (window.peekFirst() == i) {
            window.pollFirst();
        }
        
        // 添加新元素到窗口右边界
        while (!window.isEmpty() && arr[window.peekLast()] >= arr[j]) {
            window.pollLast();
        }
        window.addLast(j);
    }
    
    return ans;
}

执行过程演示

假设 oil = [1, 2, 3, 4, 5], dis = [3, 4, 5, 1, 2]

  1. 净油量数组[-2, -2, -2, 3, 3]
  2. 扩展数组[-2, -2, -2, 3, 3, -2, -2, -2, 3, 3]
  3. 前缀和数组[-2, -4, -6, -3, 0, -2, -4, -6, -3, 0]

现在检查每个起始位置:

  • 起始位置 0:窗口 [0,4],最小值 -6offset = 0-6 - 0 < 0 → false
  • 起始位置 1:窗口 [1,5],最小值 -6offset = -2-6 - (-2) = -4 < 0 → false
  • 起始位置 2:窗口 [2,6],最小值 -6offset = -4-6 - (-4) = -2 < 0 → false
  • 起始位置 3:窗口 [3,7],最小值 -6offset = -6-6 - (-6) = 0 >= 0 → true
  • 起始位置 4:窗口 [4,8],最小值 -6offset = -3-6 - (-3) = -3 < 0 → false

所以只有位置 3 是良好出发点。

算法复杂度分析

  • 时间复杂度:O(N)

    • 构建数组:O(N)
    • 计算前缀和:O(N)
    • 单调队列操作:每个元素最多入队出队一次,O(N)
  • 空间复杂度:O(N)

    • 扩展数组:O(N)
    • 单调队列:O(N)

相比暴力解法的 O(N²),这是一个显著的优化!

关键要点总结

  1. 环形数组处理:通过复制数组避免模运算
  2. 前缀和转换:将路径问题转化为区间最小值问题
  3. 单调队列应用:高效维护滑动窗口最小值
  4. 数学条件转换min - offset >= 0 是核心判断条件

扩展思考

这种思路还可以应用到其他环形数组问题:

  • 环形数组的最大子数组和
  • 环形数组的最小平均值子数组
  • 其他需要在环形结构中找满足条件区间的题目

掌握这种"扩展数组 + 前缀和 + 单调队列"的组合技巧,会让你在解决环形数组问题时更加游刃有余!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值