3200.三角形的最大高度

目标

给你两个整数 red 和 blue,分别表示红色球和蓝色球的数量。你需要使用这些球来组成一个三角形,满足第 1 行有 1 个球,第 2 行有 2 个球,第 3 行有 3 个球,依此类推。

每一行的球必须是 相同 颜色,且相邻行的颜色必须 不同。

返回可以实现的三角形的 最大 高度。

示例 1:

输入: red = 2, blue = 4
输出: 3
解释:
上图显示了唯一可能的排列方式。

示例 2:

输入: red = 2, blue = 1
输出: 2
解释:
上图显示了唯一可能的排列方式。

示例 3:

输入: red = 1, blue = 1
输出: 1

示例 4:

输入: red = 10, blue = 1
输出: 2
解释:
上图显示了唯一可能的排列方式。

说明:

  • 1 <= red, blue <= 100

思路

有红球 red 个,蓝球 blue 个,使用这两种球组成三角形,要求每一行只能由同一种颜色的球组成,且相邻行球的颜色不同,问三角形的最大高度。

三角形第 i 行球的个数为 i,奇数行的总个数为 1 + 3 + 5 + …… 偶数行的总个数为 2 + 4 + 6 + ……,根据等差数列求和公式,奇数行所需球的总个数为 oddRowCnt^2,偶数行所需球的总个数为 evenRowCnt^2 + evenRowCnt

假设 red 放在奇数行,代入可得 redOddRowCnt = sqrt(red),如果放在偶数行,则 redEvenRowCnt = (sqrt(1 + 4 * red) - 1) / 2。同理可求出 blueOddRowCnt blueEvenRowCnt

接下来分两种情况讨论:

  1. 红色球放第一行:如果 |redOddRowCnt - blueEvenRowCnt| > 1,取 Math.min(redOddRowCnt, blueEvenRowCnt) * 2 + 1,否则取 Math.min(redOddRowCnt, blueEvenRowCnt) * 2
  2. 蓝色球放第一行:如果 |blueOddRowCnt - redEvenRowCnt| > 1,取 Math.min(blueOddRowCnt, redEvenRowCnt) * 2 + 1,否则取 Math.min(blueOddRowCnt, redEvenRowCnt) * 2

取上面两种情况的最大值即可。

代码


/**
 * @date 2024-10-15 9:25
 */
public class MaxHeightOfTriangle3200 {

    public int maxHeightOfTriangle(int red, int blue) {
        int redOddRowCnt = (int) Math.sqrt(red);
        int redEvenRowCnt = (int) ((Math.sqrt(1 + 4 * red) - 1) / 2);
        int blueOddRowCnt = (int) Math.sqrt(blue);
        int blueEvenRowCnt = (int) ((Math.sqrt(1 + 4 * blue) - 1) / 2);
        int r, b;
        if (redOddRowCnt - blueEvenRowCnt >= 1) {
            r = 2 * blueEvenRowCnt + 1;
        } else {
            r = 2 * redOddRowCnt;
        }
        if (blueOddRowCnt - redEvenRowCnt >= 1) {
            b = 2 * redEvenRowCnt + 1;
        } else {
            b = 2 * blueOddRowCnt;
        }
        return Math.max(r, b);
    }

}

性能

887.鸡蛋掉落

目标

给你 k 枚相同的鸡蛋,并可以使用一栋从第 1 层到第 n 层共有 n 层楼的建筑。

已知存在楼层 f ,满足 0 <= f <= n ,任何从 高于 f 的楼层落下的鸡蛋都会碎,从 f 楼层或比它低的楼层落下的鸡蛋都不会破。

每次操作,你可以取一枚没有碎的鸡蛋并把它从任一楼层 x 扔下(满足 1 <= x <= n)。如果鸡蛋碎了,你就不能再次使用它。如果某枚鸡蛋扔下后没有摔碎,则可以在之后的操作中 重复使用 这枚鸡蛋。

请你计算并返回要确定 f 确切的值 的 最小操作次数 是多少?

示例 1:

输入:k = 1, n = 2
输出:2
解释:
鸡蛋从 1 楼掉落。如果它碎了,肯定能得出 f = 0 。 
否则,鸡蛋从 2 楼掉落。如果它碎了,肯定能得出 f = 1 。 
如果它没碎,那么肯定能得出 f = 2 。 
因此,在最坏的情况下我们需要移动 2 次以确定 f 是多少。 

示例 2:

输入:k = 2, n = 6
输出:3

示例 3:

输入:k = 3, n = 14
输出:4

提示:

  • 1 <= k <= 100
  • 1 <= n <= 10^4

思路

参考1884.鸡蛋掉落-两枚鸡蛋,这次是 k 枚鸡蛋。

//todo

代码

性能

1884.鸡蛋掉落-两枚鸡蛋

目标

给你 2 枚相同 的鸡蛋,和一栋从第 1 层到第 n 层共有 n 层楼的建筑。

已知存在楼层 f ,满足 0 <= f <= n ,任何从 高于 f 的楼层落下的鸡蛋都 会碎 ,从 f 楼层或比它低 的楼层落下的鸡蛋都 不会碎 。

每次操作,你可以取一枚 没有碎 的鸡蛋并把它从任一楼层 x 扔下(满足 1 <= x <= n)。如果鸡蛋碎了,你就不能再次使用它。如果某枚鸡蛋扔下后没有摔碎,则可以在之后的操作中 重复使用 这枚鸡蛋。

请你计算并返回要确定 f 确切的值 的 最小操作次数 是多少?

示例 1:

输入:n = 2
输出:2
解释:我们可以将第一枚鸡蛋从 1 楼扔下,然后将第二枚从 2 楼扔下。
如果第一枚鸡蛋碎了,可知 f = 0;
如果第二枚鸡蛋碎了,但第一枚没碎,可知 f = 1;
否则,当两个鸡蛋都没碎时,可知 f = 2。

示例 2:

输入:n = 100
输出:14
解释:
一种最优的策略是:
- 将第一枚鸡蛋从 9 楼扔下。如果碎了,那么 f 在 0 和 8 之间。将第二枚从 1 楼扔下,然后每扔一次上一层楼,在 8 次内找到 f 。总操作次数 = 1 + 8 = 9 。
- 如果第一枚鸡蛋没有碎,那么再把第一枚鸡蛋从 22 层扔下。如果碎了,那么 f 在 9 和 21 之间。将第二枚鸡蛋从 10 楼扔下,然后每扔一次上一层楼,在 12 次内找到 f 。总操作次数 = 2 + 12 = 14 。
- 如果第一枚鸡蛋没有再次碎掉,则按照类似的方法从 34, 45, 55, 64, 72, 79, 85, 90, 94, 97, 99 和 100 楼分别扔下第一枚鸡蛋。
不管结果如何,最多需要扔 14 次来确定 f 。

说明:

  • 1 <= n <= 1000

思路

有一个 1 ~ n 层楼的建筑,存在一个楼层 f,任何大于 f 层落下的鸡蛋都会摔碎。现在有两个鸡蛋,每次操作可以从任意楼层向下扔鸡蛋,如果鸡蛋碎了则无法再使用,求确定 f 值的最小操作次数。

为了确保能够找到 f,如果第一个尝试的鸡蛋碎了,那么另一个鸡蛋只能从已知的安全楼层一层一层向上尝试。

观察示例2,可以从 n 开始 减 1 2 3 …… i 直到小于等于零,返回 i - 1即可。

看了题解,这样做可行的逻辑是这样的:

假设已知最小操作次数 k,我们扔第一枚鸡蛋选第几层?显然,应该选第 k 层,因为如果第一枚鸡蛋碎了,只需要从 1 ~ k - 1 枚举即可。

如果第一枚鸡蛋没碎,那么下一次选第几层?现在还剩下 k - 1 次尝试,所以应该选 k + 1 + (k - 2) = k + (k - 1) 层,因为如果在该层扔鸡蛋碎了,只需从 k + 1 ~ k + k - 2 枚举即可,共 k - 2 次,再加上前面尝试的 2 次,总次数为 k

以此类推,我们可以确定总层数 n = k + (k - 1) + (k - 2) + …… + 2 + 1 = k * (k + 1)/2,解方程得 k = (sqrt(1+8*n) - 1)/2,结果需要向上取整。

代码


/**
 * @date 2024-10-13 19:30
 */
public class TwoEggDrop1884 {

    public int twoEggDrop_v1(int n) {
        return (int) Math.ceil((Math.sqrt(1 + 8 * n) - 1) / 2);
    }

    public int twoEggDrop(int n) {
        int i = 1;
        while (n > 0){
            n -= i++;
        }
        return i - 1;
    }
}

性能

3158.求出出现两次数字的XOR值

目标

给你一个数组 nums ,数组中的数字 要么 出现一次,要么 出现两次。

请你返回数组中所有出现两次数字的按位 XOR 值,如果没有数字出现过两次,返回 0 。

示例 1:

输入:nums = [1,2,1,3]
输出:1
解释:
nums 中唯一出现过两次的数字是 1 。

示例 2:

输入:nums = [1,2,3]
输出:0
解释:
nums 中没有数字出现两次。

示例 3:

输入:nums = [1,2,2,1]
输出:3
解释:
数字 1 和 2 出现过两次。1 XOR 2 == 3 。

说明:

  • 1 <= nums.length <= 50
  • 1 <= nums[i] <= 50
  • nums 中每个数字要么出现过一次,要么出现过两次。

思路

有一个数组 nums,其中的元素要么出现一次,要么出现两次。求出现两次的元素的异或值,如果没有则返回 0。

由于 nums[i] <= 50,可以使用数组记录元素是否出现过,如果它第二次出现则计算异或值。

网友题解则使用了 long 型变量作为 bitmap 标识是否已经出现过。空间复杂度降为 O(1)

代码


/**
 * @date 2024-10-12 8:46
 */
public class DuplicateNumbersXOR3158 {

    public int duplicateNumbersXOR(int[] nums) {
        // nums[i] <= 50
        boolean[] seen = new boolean[51];
        int res = 0;
        for (int num : nums) {
            if (seen[num]) {
                res ^= num;
            } else {
                seen[num] = true;
            }
        }
        return res;
    }
}

性能

3164.优质数对的总数II

目标

给你两个整数数组 nums1 和 nums2,长度分别为 n 和 m。同时给你一个正整数 k。

如果 nums1[i] 可以被 nums2[j] * k 整除,则称数对 (i, j) 为 优质数对(0 <= i <= n - 1, 0 <= j <= m - 1)。

返回 优质数对 的总数。

示例 1:

输入:nums1 = [1,3,4], nums2 = [1,3,4], k = 1
输出:5
解释:
5个优质数对分别是 (0, 0), (1, 0), (1, 1), (2, 0), 和 (2, 2)。

示例 2:

输入:nums1 = [1,2,4,12], nums2 = [2,4], k = 3
输出:2
解释:
2个优质数对分别是 (3, 0) 和 (3, 1)。

说明:

  • 1 <= n, m <= 10^5
  • 1 <= nums1[i], nums2[j] <= 10^6
  • 1 <= k <= 10^3

思路

这与昨天 3162.优质数对的总数I 的区别是数据范围变大了,暴力解法不可行。

枚举因子的时间复杂度为 O(n * sqrt(max1/k) + m), 代入计算 10^5 * 10^3 + 10^5 大约 10^8,耗时560ms。

枚举倍数的时间复杂度为 O(n + m + max1/k * log(m))10^5 + 10^5 + 10^6 * ln(10^5) ≈ 10^5 + 10^5 + 10^6 * 11.5110^7,耗时 88ms。

代码


/**
 * @date 2024-10-11 8:40
 */
public class NumberOfPairs3164 {

    /**
     * 枚举因子 560ms
     */
    public long numberOfPairs_v1(int[] nums1, int[] nums2, int k) {
        Map<Integer, Integer> factorCnt = new HashMap<>();
        for (int num : nums1) {
            if (num % k != 0) {
                continue;
            }
            num /= k;
            for (int i = 1; i * i <= num; i++) {
                if (num % i != 0) {
                    continue;
                }
                factorCnt.merge(i, 1, Integer::sum);
                if (i * i < num) {
                    factorCnt.merge(num / i, 1, Integer::sum);
                }
            }
        }
        long res = 0L;
        for (int num : nums2) {
            res += factorCnt.getOrDefault(num, 0);
        }
        return res;
    }

    /**
     * 枚举倍数 88ms
     */
    public long numberOfPairs(int[] nums1, int[] nums2, int k) {
        Map<Integer, Integer> map1 = new HashMap<>(nums1.length);
        Map<Integer, Integer> map2 = new HashMap<>(nums2.length);
        int max1 = 0;
        for (int num : nums1) {
            map1.merge(num, 1, Integer::sum);
            max1 = Math.max(max1, num);
        }
        for (int num : nums2) {
            map2.merge(num, 1, Integer::sum);
        }
        long res = 0L;
        for (int num : map2.keySet()) {
            int multiple = num * k;
            for (int i = multiple; i <= max1; i += multiple) {
                if (map1.containsKey(i)) {
                    res += (long) map1.get(i) * map2.get(num);
                }
            }
        }
        return res;
    }

}

性能

3162.优质数对的总数I

目标

给你两个整数数组 nums1 和 nums2,长度分别为 n 和 m。同时给你一个正整数 k。

如果 nums1[i] 可以被 nums2[j] * k 整除,则称数对 (i, j) 为 优质数对(0 <= i <= n - 1, 0 <= j <= m - 1)。

返回 优质数对 的总数。

示例 1:

输入:nums1 = [1,3,4], nums2 = [1,3,4], k = 1
输出:5
解释:
5个优质数对分别是 (0, 0), (1, 0), (1, 1), (2, 0), 和 (2, 2)。

示例 2:

输入:nums1 = [1,2,4,12], nums2 = [2,4], k = 3
输出:2
解释:
2个优质数对分别是 (3, 0) 和 (3, 1)。

说明:

  • 1 <= n, m <= 50
  • 1 <= nums1[i], nums2[j] <= 50
  • 1 <= k <= 50

思路

有两个数组 nums1 nums2,计算满足 nums1[i] % (nums2[j] * k) == 0 的数对 (i, j) 的个数。

数据量不大直接枚举即可。可以优化的点是提前判断 nums[i] 能否整除 k,如果不能,则必然也无法整除 nums2[j] * k,省去了内部循环乘以 k 的计算。

官网题解的非暴力做法是使用两个哈希表 map1 map2 分别统计 nums1 nums2 中元素出现的次数,同时找出 nums1 中的最大值 max1,然后枚举 map2 的 keyset,内层循环初值与步长为 nums2[j] * k,判断倍数是否在 map1 中,直到 倍数 <= max1。这样做的好处是寻找数对的时间复杂度由原来的 O(mn) 变成了 O(n + m + max1/k * log(m))

这里的 log(max2) 是一个近似的估计,针对每一个外层循环的值 a,内层循环的次数为 max1 / (a * k),假设 nums2 的元素值为 1 2 …… max2,那么总的循环次数为 (max1/k)Σ(1/a) 这是调和级数的形式,即 1 + 1/2 + 1/3 + …… + 1/max2 + ……,调和级数的部分和 Hn 近似为 ln(max2),这里 max2 其实是 nums2 的个数 m。当 nums2 中的元素都是 1 时,map2.keySet 中的元素个数也为 1,进化为 O(n + m + max1/k),当 nums2 中的元素都是 max2 时,进化为 O(n + m + max1/(max2 * k))

代码


/**
 * @date 2024-10-10 8:56
 */
public class NumberOfPairs3162 {

    public int numberOfPairs_v1(int[] nums1, int[] nums2, int k) {
        int res = 0;
        for (int i = 0; i < nums1.length; i++) {
            if (nums1[i] % k != 0) {
                continue;
            }
            int tmp = nums1[i] / k;
            for (int j = 0; j < nums2.length; j++) {
                if (tmp % nums2[j] == 0) {
                    res++;
                }
            }
        }
        return res;
    }

    public int numberOfPairs(int[] nums1, int[] nums2, int k) {
        int res = 0;
        for (int i = 0; i < nums1.length; i++) {
            for (int j = 0; j < nums2.length; j++) {
                if (nums1[i] % (nums2[j] * k) == 0){
                    res++;
                }
            }
        }
        return res;
    }

}

性能

3171.找到按位或最接近K的子数组

目标

给你一个数组 nums 和一个整数 k 。你需要找到 nums 的一个 子数组,满足子数组中所有元素按位或运算 OR 的值与 k 的 绝对差 尽可能 小 。换言之,你需要选择一个子数组 nums[l..r] 满足 |k - (nums[l] OR nums[l + 1] ... OR nums[r])| 最小。

请你返回 最小 的绝对差值。

子数组 是数组中连续的 非空 元素序列。

示例 1:

输入:nums = [1,2,4,5], k = 3
输出:0
解释:
子数组 nums[0..1] 的按位 OR 运算值为 3 ,得到最小差值 |3 - 3| = 0 。

示例 2:

输入:nums = [1,3,1,3], k = 2
输出:1
解释:
子数组 nums[1..1] 的按位 OR 运算值为 3 ,得到最小差值 |3 - 2| = 1 。

示例 3:

输入:nums = [1], k = 10
输出:9
解释:
只有一个子数组,按位 OR 运算值为 1 ,得到最小差值 |10 - 1| = 9 。

说明:

  • 1 <= nums.length <= 10^5
  • 1 <= nums[i] <= 10^9
  • 1 <= k <= 10^9

思路

寻找子数组使子数组元素按位或运算的值 or 尽可能地接近 k,即求 |k - or| 的最小值。

暴力求解的基本逻辑是,外层枚举右端点,内层从后向前枚举左端点,使用 nums[j] 保存 子数组 [j, i] 的或值,通过判断 (nums[j] | right) != nums[j] 决定 right 是否对或值有贡献,如果没有贡献,那么可以不再继续向前枚举左端点,因为前面的或值已经累加了后面的值,如果对子数组 [j, i] 的或值没有贡献,那么对前面的 [j - 1, i] 包含了 [j, i] 的或值更没有贡献。

代码


/**
 * @date 2024-10-09 8:59
 */
public class MinimumDifference3171 {

    public int minimumDifference_v1(int[] nums, int k) {
        int n = nums.length;
        int res = Integer.MAX_VALUE;
        for (int i = 0; i < n; i++) {
            int right = nums[i];
            for (int j = i - 1; j >= 0 && ((nums[j] | right) != nums[j]); j--) {
                nums[j] |= right;
                res = Math.min(res, Math.abs(nums[j] - k));
            }
            res = Math.min(res, Math.abs(right - k));
        }
        return res;
    }

}

性能

1436.旅行终点站

目标

给你一份旅游线路图,该线路图中的旅行线路用数组 paths 表示,其中 paths[i] = [cityAi, cityBi] 表示该线路将会从 cityAi 直接前往 cityBi 。请你找出这次旅行的终点站,即没有任何可以通往其他城市的线路的城市。

题目数据保证线路图会形成一条不存在循环的线路,因此恰有一个旅行终点站。

示例 1:

输入:paths = [["London","New York"],["New York","Lima"],["Lima","Sao Paulo"]]
输出:"Sao Paulo" 
解释:从 "London" 出发,最后抵达终点站 "Sao Paulo" 。本次旅行的路线是 "London" -> "New York" -> "Lima" -> "Sao Paulo" 。

示例 2:

输入:paths = [["B","C"],["D","B"],["C","A"]]
输出:"A"
解释:所有可能的线路是:
"D" -> "B" -> "C" -> "A". 
"B" -> "C" -> "A". 
"C" -> "A". 
"A". 
显然,旅行终点站是 "A" 。

示例 3:

输入:paths = [["A","Z"]]
输出:"Z"

说明:

  • 1 <= paths.length <= 100
  • paths[i].length == 2
  • 1 <= cityAi.length, cityBi.length <= 10
  • cityAi != cityBi
  • 所有字符串均由大小写英文字母和空格字符组成。

思路

找到旅行的终点,实际上是求在 end 集合而不在 start 集合中的元素,即 end - (start ∩ end),测试用例保证只有一个终点。

代码


/**
 * @date 2024-10-08 8:43
 */
public class DestCity1436 {

    public String destCity(List<List<String>> paths) {
        Set<String> start = new HashSet<>();
        for (List<String> path : paths) {
            start.add(path.get(0));
        }
        for (List<String> path : paths) {
            String dest = path.get(1);
            if (!start.contains(dest)) {
                return dest;
            }
        }
        return null;
    }
}

性能

871.最低加油次数

目标

汽车从起点出发驶向目的地,该目的地位于出发位置东面 target 英里处。

沿途有加油站,用数组 stations 表示。其中 stations[i] = [positioni, fueli] 表示第 i 个加油站位于出发位置东面 positioni 英里处,并且有 fueli 升汽油。

假设汽车油箱的容量是无限的,其中最初有 startFuel 升燃料。它每行驶 1 英里就会用掉 1 升汽油。当汽车到达加油站时,它可能停下来加油,将所有汽油从加油站转移到汽车中。

为了到达目的地,汽车所必要的最低加油次数是多少?如果无法到达目的地,则返回 -1 。

注意:如果汽车到达加油站时剩余燃料为 0,它仍然可以在那里加油。如果汽车到达目的地时剩余燃料为 0,仍然认为它已经到达目的地。

示例 1:

输入:target = 1, startFuel = 1, stations = []
输出:0
解释:可以在不加油的情况下到达目的地。

示例 2:

输入:target = 100, startFuel = 1, stations = [[10,100]]
输出:-1
解释:无法抵达目的地,甚至无法到达第一个加油站。

示例 3:

输入:target = 100, startFuel = 10, stations = [[10,60],[20,30],[30,30],[60,40]]
输出:2
解释:
出发时有 10 升燃料。
开车来到距起点 10 英里处的加油站,消耗 10 升燃料。将汽油从 0 升加到 60 升。
然后,从 10 英里处的加油站开到 60 英里处的加油站(消耗 50 升燃料),
并将汽油从 10 升加到 50 升。然后开车抵达目的地。
沿途在两个加油站停靠,所以返回 2 。

说明:

  • 1 <= target, startFuel <= 10^9
  • 0 <= stations.length <= 500
  • 1 <= positioni < positioni+1 < target
  • 1 <= fueli < 10^9

思路

已知汽车油耗为 1 英里/升,现在想要开车前往 target 英里外的目的地,出发时车中有 startFuel 升油,沿途有 stations.length 个加油站,stations[i][0] 表示加油站 i 距出发地的距离,stations[i][1] 表示加油站 i 的汽油总量。假设汽车油箱无限大,求到达目的地的最少加油次数,如果无法到达返回 -1

可以将出发点、加油站、目的地画到数轴上,由于油耗为 1 英里/升,有多少升油就可以到达多远的距离。那么问题转化为从 n 个加油站中选取最少的个数,使油箱油量大于等于 target。要使选择的加油站最少,那么应该首选油量多的加油站,前提是能够抵达该加油站。我们可以使用大顶堆维护加油站油量,将能够抵达的加油站放入其中,然后将堆顶的油加入油箱,将新的能够抵达的加油站放入堆中,直到油箱中的油量大于等于 target

代码


/**
 * @date 2024-10-07 21:09
 */
public class MinRefuelStops871 {

    public int minRefuelStops(int target, int startFuel, int[][] stations) {
        PriorityQueue<Integer> q = new PriorityQueue<>((a, b) -> b - a);
        int n = stations.length;
        int i = 0;
        int res = 0;
        int fuel = startFuel;
        while (fuel < target) {
            while (i < n && fuel >= stations[i][0]) {
                q.offer(stations[i++][1]);
            }
            if (!q.isEmpty()) {
                fuel += q.poll();
                res++;
                if (fuel >= target) {
                    return res;
                }
            } else if (i == n || fuel < stations[i][0]) {
                // 没有剩余的加油站或者无法抵达
                return -1;
            }
        }
        return res;
    }

}

性能

134.加油站

目标

在一条环路上有 n 个加油站,其中第 i 个加油站有汽油 gas[i] 升。

你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。

给定两个整数数组 gas 和 cost ,如果你可以按顺序绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1 。如果存在解,则 保证 它是 唯一 的。

示例 1:

输入: gas = [1,2,3,4,5], cost = [3,4,5,1,2]
输出: 3
解释:
从 3 号加油站(索引为 3 处)出发,可获得 4 升汽油。此时油箱有 = 0 + 4 = 4 升汽油
开往 4 号加油站,此时油箱有 4 - 1 + 5 = 8 升汽油
开往 0 号加油站,此时油箱有 8 - 2 + 1 = 7 升汽油
开往 1 号加油站,此时油箱有 7 - 3 + 2 = 6 升汽油
开往 2 号加油站,此时油箱有 6 - 4 + 3 = 5 升汽油
开往 3 号加油站,你需要消耗 5 升汽油,正好足够你返回到 3 号加油站。
因此,3 可为起始索引。

示例 2:

输入: gas = [2,3,4], cost = [3,4,3]
输出: -1
解释:
你不能从 0 号或 1 号加油站出发,因为没有足够的汽油可以让你行驶到下一个加油站。
我们从 2 号加油站出发,可以获得 4 升汽油。 此时油箱有 = 0 + 4 = 4 升汽油
开往 0 号加油站,此时油箱有 4 - 3 + 2 = 3 升汽油
开往 1 号加油站,此时油箱有 3 - 3 + 3 = 3 升汽油
你无法返回 2 号加油站,因为返程需要消耗 4 升汽油,但是你的油箱只有 3 升汽油。
因此,无论怎样,你都不可能绕环路行驶一周。

说明:

  • gas.length == n
  • cost.length == n
  • 1 <= n <= 10^5
  • 0 <= gas[i], cost[i] <= 10^4

思路

一条环线上有 n 个加油站,加油站 igas[i] 升汽油,从加油站 i 到达加油站 i + 1 需要消耗汽油 cost[i] 升,假设有一辆油箱无限大的汽车,初始油箱为空,问从哪个加油站出发可以绕环线行驶一周,测试用例保证如果解存在则唯一,如果解不存在返回 -1

枚举起点,模拟加油与耗油过程,记录剩余汽油,直到完成环线或者无法抵达下一加油站。值得注意的是,如果从起点 a 出发无法到达 b,那么从 (a, b) 之间的任意加油站出发都无法到达 b,因为到达下一站剩余的汽油是大于等于 0 的,如果带着剩余汽油都无法抵达,那么初始汽油为空更无法到达。我们可以将起点设为 b 继续枚举。

最开始写的是两次循环,后来又改成在一个while循环中,时间复杂度为 O(2n)。之所以是 2n 是因为 start 如果为 n - 1 那么需要再判断能否到达 0 ~ n - 1

优化:如果 gasSum >= costSum, 那么解一定存在。注意到 remainder += gas[i] - cost[i] 其实就是在计算 gasSum - costSum,只是中间小于零的时候重置了,我们只需要一个变量 diff 将其保存起来,直接判断 diff 是否大于 0, 没必要再进行后续判断了。

代码


/**
 * @date 2024-04-13 21:02
 */
public class CanCompleteCircuit134 {

    public int canCompleteCircuit_v2(int[] gas, int[] cost) {
        int n = gas.length;
        int remainder = 0;
        int start = 0;
        int diff = 0;
        for (int i = start; i < n; i++) {
            remainder += gas[i] - cost[i];
            if (remainder < 0) {
                start = i + 1;
                diff += remainder;
                remainder = 0;
            }
        }
        diff += remainder;
        return diff < 0 ? -1 : start;
    }

    public int canCompleteCircuit_v1(int[] gas, int[] cost) {
        int start = 0;
        int cur = 0;
        int n = gas.length;
        int remainder = 0;
        while (start < n) {
            int i = cur % n;
            remainder += gas[i] - cost[i];
            if (remainder < 0) {
                start = ++cur;
                remainder = 0;
            } else if ((i + 1) % n == start) {
                return start;
            } else {
                cur++;
            }
        }
        return -1;
    }

    public int canCompleteCircuit(int[] gas, int[] cost) {
        int n = gas.length;
        int remainder = 0;
        int start = 0;
        // 第一次循环,遇到断点就重新开始
        for (int i = start; i < n; i++) {
            remainder += gas[i] - cost[i];
            if (remainder < 0) {
                start = i + 1;
                remainder = 0;
            }
        }
        // 第二次循环,遇到断点说明不可达
        for (int i = 0; i < start; i++) {
            remainder += gas[i] - cost[i];
            if (remainder < 0) {
                return -1;
            }
        }
        return start;
    }

}

性能

两次循环

写成一次循环,实际上是循环了两个变量,并且循环中的操作也翻倍了

一次循环