LeetCode851~875题目解析

LeetCode 851-875. 题解合集

文章目录


851. Loud and Rich(喧闹和富有)

题目描述

有一组 n 个人作为实验对象,从 0 到 n - 1 编号,其中每个人都有不同数目的钱,

以及不同程度的安静值。为了方便起见,我们将编号为 x 的人简称为 "person x"。

给你一个数组 richer,其中 richer[i] = [a_i, b_i] 表示 person a_i 比 person

b_i 更有钱。另给你一个整数数组 quiet,其中 quiet[i] 是 person i 的安静值。

richer 中所给出的数据逻辑自洽(也就是说,在 person x 比 person y 更有钱的同时,

不会出现 person y 比 person x 更有钱的情况)。

现在,返回一个整数数组 answer 作为答案,其中 answer[x] = y 的前提是:在所有拥有的钱

肯定不少于 person x 的人中,person y 是最安静的人(也就是安静值 quiet[y] 最小的人)。

示例 1

  • 输入:richer = \[1,0,2,1,3,1,3,7,4,3,5,3,6,3], quiet = 3,2,5,4,6,1,7,0
  • 输出:5,5,2,5,4,5,6,7
  • 解释 :
    answer[0] = 5:
    person 5 比 person 3 有钱,person 3 比 person 1 有钱,person 1 比 person 0 有钱。
    所以是 person 5 比 person 0 有钱,输出 5(quiet 值最小的人)。

示例 2

  • 输入:richer = \[\], quiet = 0
  • 输出 :0
    提示:
  • n == quiet.length
  • 1 <= n <= 500
  • 0 <= quiet[i] < n
  • 0 <= richer.length <= n * (n - 1) / 2

解题思路

每个人 x 的答案,需要在「所有比 x 有钱或与 x 相同的人」中取安静值最小者。

解法一(记忆化 DFS,主提交):把"比 b 有钱"看成依赖关系,建图 g[b].add(a),递归

求 x 的答案 = x 与所有 g[x] 中节点的答案里安静值最小的人。每个人只算一次,记忆化。

解法二(拓扑排序):建图 g[a].add(b)(a 比 b 有钱),入度为 0 的点(最有钱的人,

答案就是自己)入队;弹出 u 时用 ans[u] 更新所有"比 u 穷"的直接邻居的答案,邻居

入度归零后入队,逐步把最优信息传播到传播链末端。

代码实现

java 复制代码
import java.util.ArrayList;
import java.util.ArrayDeque;
import java.util.List;
import java.util.Queue;

class Solution {

    private List<Integer>[] g;
    private int[] quiet;
    private int[] memo;

    // 解法一:记忆化 DFS(推荐提交)
    public int[] loudAndRich(int[][] richer, int[] quiet) {
        int n = quiet.length;
        this.quiet = quiet;
        g = new List[n];
        for (int i = 0; i < n; ++i) {
            g[i] = new ArrayList<>();
        }
        for (int[] e : richer) {
            // e[0] 比 e[1] 有钱:求 e[1] 的答案需要先知道 e[0]
            g[e[1]].add(e[0]);
        }
        memo = new int[n];
        java.util.Arrays.fill(memo, -1);
        int[] ans = new int[n];
        for (int i = 0; i < n; ++i) {
            ans[i] = dfs(i);
        }
        return ans;
    }

    private int dfs(int x) {
        if (memo[x] != -1) {
            return memo[x];
        }
        int best = x;
        for (int y : g[x]) {
            int t = dfs(y);
            if (quiet[t] < quiet[best]) {
                best = t;
            }
        }
        return memo[x] = best;
    }

    // 解法二:拓扑排序传播(对照实现)
    public int[] loudAndRich2(int[][] richer, int[] quiet) {
        int n = quiet.length;
        this.quiet = quiet;
        List<Integer>[] graph = new List[n];
        for (int i = 0; i < n; ++i) {
            graph[i] = new ArrayList<>();
        }
        int[] indeg = new int[n];
        for (int[] e : richer) {
            graph[e[0]].add(e[1]);
            ++indeg[e[1]];
        }
        int[] ans = new int[n];
        Queue<Integer> q = new ArrayDeque<>();
        for (int i = 0; i < n; ++i) {
            ans[i] = i;
            if (indeg[i] == 0) {
                q.offer(i);
            }
        }
        while (!q.isEmpty()) {
            int u = q.poll();
            for (int v : graph[u]) {
                // u 比 v 有钱:v 的候选答案可以尝试 u 的答案
                if (quiet[ans[u]] < quiet[ans[v]]) {
                    ans[v] = ans[u];
                }
                if (--indeg[v] == 0) {
                    q.offer(v);
                }
            }
        }
        return ans;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[][] r1 = {{1, 0}, {2, 1}, {3, 1}, {3, 7}, {4, 3}, {5, 3}, {6, 3}};
        int[] q1 = {3, 2, 5, 4, 6, 1, 7, 0};
        ++total;
        if (java.util.Arrays.equals(s.loudAndRich(r1, q1), new int[]{5, 5, 2, 5, 4, 5, 6, 7})) {
            ++pass;
        } else {
            System.out.println("FAIL 851-1: " + java.util.Arrays.toString(s.loudAndRich(r1, q1)));
        }
        ++total;
        if (java.util.Arrays.equals(s.loudAndRich(new int[][]{}, new int[]{0}), new int[]{0})) {
            ++pass;
        } else {
            System.out.println("FAIL 851-2");
        }
        ++total;
        if (java.util.Arrays.equals(s.loudAndRich2(r1, q1), new int[]{5, 5, 2, 5, 4, 5, 6, 7})) {
            ++pass;
        } else {
            System.out.println("FAIL 851-3: " + java.util.Arrays.toString(s.loudAndRich2(r1, q1)));
        }
        ++total;
        if (java.util.Arrays.equals(s.loudAndRich2(new int[][]{}, new int[]{0}), new int[]{0})) {
            ++pass;
        } else {
            System.out.println("FAIL 851-4");
        }
        if (pass == total) {
            System.out.println("== 0851 PASS ==");
        } else {
            System.out.println("== 0851 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(n + m) O(n + m)
解法二 O(n + m) O(n + m)

852. Peak Index in a Mountain Array(山脉数组的峰顶索引)

题目描述

符合下列属性的数组 arr 称为山脉数组:

arr.length >= 3

存在 i(0 < i < arr.length - 1)使得:

arr[0] < arr[1] < ... < arr[i - 1] < arr[i]

arr[i] > arr[i + 1] > ... > arr[arr.length - 1]

给你由整数组成的山脉数组 arr,返回任何满足 arr[0] < arr[1] < ... < arr[i - 1] < arr[i] > arri + 1> ... >arr[arr.length - 1] 的下标 i。

示例 1

  • 输入 :arr = 0,1,0
  • 输出:1

示例 2

  • 输入 :arr = 0,2,1,0
  • 输出:1

示例 3

  • 输入 :arr = 0,10,5,2
  • 输出 :1
    提示:
  • 3 <= arr.length <= 10^5
  • 0 <= arr[i] <= 10^6
  • 题目数据保证 arr 是一个山脉数组

解题思路

山脉数组严格"先升后降",峰值左边 arr[i] < arr[i+1]、右边 arr[i] > arr[i+1],具有单调分界。

解法一(二分查找,主提交):在 [0, n-1] 上二分:mid 处若 arr[mid] < arr[mid+1],

说明峰值在右侧(mid 处于上升段),l = mid + 1;否则峰值在 mid 或左侧,r = mid。

循环结束后 l 即峰顶索引,时间复杂度 O(log n)。

解法二(线性扫描,对照实现):从 0 开始一路比较相邻元素,找到第一个

arr[i] > arr[i+1] 的位置就是峰顶,时间复杂度 O(n),实现直观但常数更差。

代码实现

java 复制代码
class Solution {

    // 解法一:二分查找(推荐提交)
    public int peakIndexInMountainArray(int[] arr) {
        int n = arr.length;
        int l = 0, r = n - 1;
        while (l < r) {
            int mid = (l + r) >>> 1;
            if (arr[mid] < arr[mid + 1]) {
                l = mid + 1;   // 处于上升段,峰顶在右侧
            } else {
                r = mid;       // 处于下降段(或正好峰顶),峰顶在 mid 或左侧
            }
        }
        return l;
    }

    // 解法二:线性扫描(对照实现)
    public int peakIndexInMountainArray2(int[] arr) {
        int i = 0;
        while (arr[i] < arr[i + 1]) {
            ++i;
        }
        return i;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[][] arrs = {
            {0, 1, 0},
            {0, 2, 1, 0},
            {0, 10, 5, 2},
            {24, 69, 100, 99, 79, 78, 67, 36, 26, 19},
            {3, 5, 3, 2, 0}
        };
        int[] exp = {1, 1, 1, 2, 1};
        for (int t = 0; t < arrs.length; ++t) {
            ++total;
            if (s.peakIndexInMountainArray(arrs[t]) == exp[t]
                    && s.peakIndexInMountainArray2(arrs[t]) == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 852-" + (t + 1) + ": got "
                        + s.peakIndexInMountainArray(arrs[t]));
            }
        }
        if (pass == total) {
            System.out.println("== 0852 PASS ==");
        } else {
            System.out.println("== 0852 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(log n) O(1)
解法二 O(n) O(1)

853. Car Fleet(车队)

题目描述

在一条单行道上,有 n 辆车开往同一目的地(目的地是几英里以外的 target 英里处)。

给定两个整数数组 position 和 speed,长度都是 n,其中 position[i] 是第 i 辆车的

位置,speed[i] 是第 i 辆车的速度(单位:英里/小时)。

一辆车永远不会超过前面的另一辆车,但它可以追上去,并以较慢车的速度在另一辆车旁边行驶。

车队 是指并排行驶的一辆或几辆汽车。车队的速度是车队中 最慢 的车的速度。

即便一辆车在 target 才赶上了一个车队,它们仍然会被视作是同一个车队。

返回到达目的地的车队数量。

示例 1

  • 输入 :target = 12, position = 10,8,0,5,3, speed = 2,4,1,1,3
  • 输出:3
  • 解释 :
    从 10(速度为 2)和 8(速度为 4)开始的车会组成一个车队,它们在 12 相遇。
    从 0(速度为 1)开始的车不会追上其它任何车,所以它自己是一个车队。
    从 5(速度为 1)和 3(速度为 3)开始的车组成一个车队,在 6 相遇。

示例 2

  • 输入 :target = 10, position = 3, speed = 3
  • 输出:1

示例 3

  • 输入 :target = 100, position = 0,2,4, speed = 4,2,1
  • 输出 :1
    提示:
  • n == position.length == speed.length
  • 1 <= n <= 10^5
  • 0 < target <= 10^6
  • 0 <= position[i] < target
  • position 中每个值都不同
  • 0 < speed[i] <= 10^6

解题思路

每辆车单独行驶按自己的速度到达 target 所需时间 time = (target - position) / speed。

位置靠后的车若到达时间 ≤ 前车(距离 target 更近的车)的到达时间,就会追上并并入车

队;只有到达时间严格更大的车,才会形成新的车队。

解法一(按位置从近到远扫描,主提交):按 position 降序排序,记录前方车队所需的最长

时间 lead。遍历每辆车,若其到达时间 t > lead,说明它追不上前面的任何车,车队数加一

并更新 lead;否则并入当前车队。时间复杂度主要来自排序 O(n log n)。

解法二(升序排列后反向扫描,对照实现):把 (position, time) 按位置升序排好,从终点

方向往回扫描,遇到「到达时间比右侧已记录的最大到达时间还大」的车就计为新车队。思路与

解法一相同,仅遍历顺序相反。

代码实现

java 复制代码
import java.util.Arrays;

class Solution {

    // 解法一:按 position 降序扫描(推荐提交)
    public int carFleet(int target, int[] position, int[] speed) {
        int n = position.length;
        Integer[] idx = new Integer[n];
        for (int i = 0; i < n; ++i) {
            idx[i] = i;
        }
        // 位置越接近 target 的越靠前
        Arrays.sort(idx, (a, b) -> Integer.compare(position[b], position[a]));
        int ans = 0;
        double lead = 0;   // 前方车队所需的最大时间
        for (int i : idx) {
            double t = (double) (target - position[i]) / speed[i];
            if (t > lead) {
                ++ans;
                lead = t;
            }
        }
        return ans;
    }

    // 解法二:升序排列后从终点方向反向扫描(对照实现)
    public int carFleet2(int target, int[] position, int[] speed) {
        int n = position.length;
        double[][] cars = new double[n][2];   // {position, time}
        for (int i = 0; i < n; ++i) {
            cars[i][0] = position[i];
            cars[i][1] = (double) (target - position[i]) / speed[i];
        }
        Arrays.sort(cars, (a, b) -> Double.compare(a[0], b[0]));
        int ans = 0;
        double maxTime = 0;   // 从终点往回已经记录的最大到达时间
        for (int i = n - 1; i >= 0; --i) {
            if (cars[i][1] > maxTime) {
                ++ans;
                maxTime = cars[i][1];
            }
        }
        return ans;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        ++total;
        if (s.carFleet(12, new int[]{10, 8, 0, 5, 3}, new int[]{2, 4, 1, 1, 3}) == 3
                && s.carFleet2(12, new int[]{10, 8, 0, 5, 3}, new int[]{2, 4, 1, 1, 3}) == 3) {
            ++pass;
        } else {
            System.out.println("FAIL 853-1: " + s.carFleet(12, new int[]{10, 8, 0, 5, 3}, new int[]{2, 4, 1, 1, 3}));
        }
        ++total;
        if (s.carFleet(10, new int[]{3}, new int[]{3}) == 1
                && s.carFleet2(10, new int[]{3}, new int[]{3}) == 1) {
            ++pass;
        } else {
            System.out.println("FAIL 853-2");
        }
        ++total;
        if (s.carFleet(100, new int[]{0, 2, 4}, new int[]{4, 2, 1}) == 1
                && s.carFleet2(100, new int[]{0, 2, 4}, new int[]{4, 2, 1}) == 1) {
            ++pass;
        } else {
            System.out.println("FAIL 853-3");
        }
        ++total;
        if (s.carFleet(10, new int[]{6, 8}, new int[]{3, 2}) == 2
                && s.carFleet2(10, new int[]{6, 8}, new int[]{3, 2}) == 2) {
            ++pass;
        } else {
            System.out.println("FAIL 853-4");
        }
        if (pass == total) {
            System.out.println("== 0853 PASS ==");
        } else {
            System.out.println("== 0853 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(n log n) O(n)
解法二 O(n log n) O(n)

854. K-Similar Strings(相似度为 K 的字符串)

题目描述

字符串 s1 和 s2 是 k 相似的(对于某些非负整数 k),如果我们可以通过交换字符串

s1 中两个字母的位置正好 k 次,使结果字符串等于 s2。

给定两个字谜游戏所需的字谜 s1 和 s2,返回 s1 和 s2 的相似度 k 的最小值。

示例 1

  • 输入 :s1 = "ab", s2 = "ba"
  • 输出:1

示例 2

  • 输入 :s1 = "abc", s2 = "bca"
  • 输出 :2
    提示:
  • 1 <= s1.length <= 20
  • s2.length == s1.length
  • s1 和 s2 只包含集合 {'a', 'b', 'c', 'd', 'e', 'f'} 中的小写字母
  • 题目数据保证两个 s1 和 s2 是字谜(组成字母及个数相同,仅顺序不同)

解题思路

问题等价于求从 s1 出发,每次「交换任意两个不同下标的字符」,到达 s2 所需的最少交换

次数。每个状态是一个字符串,目标是最短路径,用 BFS。

解法一(BFS + 首个错位剪枝,主提交):每层取当前串,找到第一个与 s2 不同的位置 i,

只尝试把某个 j > i 处、字符恰为 s2[i] 且不在正确位置上的字符交换到 i。该剪枝保证

每次交换至少修复一个错位,分支显著缩小,每个串最多入队一次,层数即最少交换次数。

解法二(记忆化 DFS,对照实现):与 BFS 相同剪枝前提下,用递归 + memo 求最少步数,

逻辑等价、代码更短,两种实现结果一致。

代码实现

java 复制代码
import java.util.ArrayDeque;
import java.util.HashSet;
import java.util.Queue;
import java.util.Set;

class Solution {

    private String target;
    private java.util.HashMap<String, Integer> memoMap;

    // 解法一:BFS + 首个错位剪枝(推荐提交)
    public int kSimilarity(String s1, String s2) {
        if (s1.equals(s2)) {
            return 0;
        }
        Queue<String> q = new ArrayDeque<>();
        Set<String> seen = new HashSet<>();
        q.offer(s1);
        seen.add(s1);
        int step = 0;
        int n = s1.length();
        char[] t = s2.toCharArray();
        while (!q.isEmpty()) {
            ++step;
            int size = q.size();
            for (int round = 0; round < size; ++round) {
                String cur = q.poll();
                char[] cs = cur.toCharArray();
                int i = 0;
                while (cs[i] == t[i]) {
                    ++i;
                }
                for (int j = i + 1; j < n; ++j) {
                    if (cs[j] == t[i] && cs[j] != t[j]) {
                        char tmp = cs[i];
                        cs[i] = cs[j];
                        cs[j] = tmp;
                        String next = new String(cs);
                        if (next.equals(s2)) {
                            return step;
                        }
                        if (!seen.contains(next)) {
                            seen.add(next);
                            q.offer(next);
                        }
                        tmp = cs[i];
                        cs[i] = cs[j];
                        cs[j] = tmp;
                    }
                }
            }
        }
        return -1;
    }

    // 解法二:记忆化 DFS(对照实现)
    public int kSimilarity2(String s1, String s2) {
        target = s2;
        memoMap = new java.util.HashMap<>();
        return dfs2(s1);
    }

    private int dfs2(String cur) {
        if (cur.equals(target)) {
            return 0;
        }
        Integer cached = memoMap.get(cur);
        if (cached != null) {
            return cached;
        }
        int n = target.length();
        char[] cs = cur.toCharArray();
        char[] t = target.toCharArray();
        int i = 0;
        while (cs[i] == t[i]) {
            ++i;
        }
        int best = Integer.MAX_VALUE;
        for (int j = i + 1; j < n; ++j) {
            if (cs[j] == t[i] && cs[j] != t[j]) {
                char tmp = cs[i];
                cs[i] = cs[j];
                cs[j] = tmp;
                int sub = dfs2(new String(cs));
                if (sub != Integer.MAX_VALUE) {
                    best = Math.min(best, 1 + sub);
                }
                tmp = cs[i];
                cs[i] = cs[j];
                cs[j] = tmp;
            }
        }
        memoMap.put(cur, best);
        return best;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        String[][] cases = {
            {"ab", "ba"},
            {"abc", "bca"},
            {"abccaacceecdeea", "bcaacceeccdeaae"},
            {"cdebcdeadedaaaebfbcf", "fdeceabceabddaefadcb"},
            {"aaa", "aaa"}
        };
        int[] exp = {1, 2, 9, 9, 0};
        for (int t = 0; t < cases.length; ++t) {
            ++total;
            int got1 = s.kSimilarity(cases[t][0], cases[t][1]);
            int got2 = s.kSimilarity2(cases[t][0], cases[t][1]);
            if (got1 == exp[t] && got2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 854-" + (t + 1) + ": got1=" + got1 + " got2=" + got2 + " exp=" + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0854 PASS ==");
        } else {
            System.out.println("== 0854 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(n^2 * 状态数) O(状态数)
解法二 O(n^2 * 状态数) O(状态数)

855. Exam Room(考场就座)

题目描述

在考场里,有 n 个座位排成一行,编号为 0 到 n - 1。

当学生进入考场后,他必须坐在离最近的人最远的座位上。如果有多个这样的座位,他会坐在编号

最小的座位上。(另外,如果考场里没有人,那么学生就坐在 0 号座位上。)

设计一个模拟所述考场的类。

实现 ExamRoom 类:

  • ExamRoom(int n) 用座位的数量 n 初始化考场对象。
  • int seat() 返回下一个学生将会入座的座位编号。
  • void leave(int p) 指定坐在座位 p 的学生将离开教室。保证座位 p 上会有一位学生。

示例 1

  • 输入 :

    "ExamRoom", "seat", "seat", "seat", "seat", "leave", "seat"

    \[10\], \[\], \[\], \[\], \[\], \[4\], \[\]

  • 输出 :`null`, 0, 9, 4, 2, `null`, 5
  • 解释 :
    ExamRoom examRoom = new ExamRoom(10);
    examRoom.seat(); // 返回 0,房间里没有人,学生坐在 0 号座位。
    examRoom.seat(); // 返回 9,学生最后坐在 9 号座位。
    examRoom.seat(); // 返回 4,学生最后坐在 4 号座位。
    examRoom.seat(); // 返回 2,学生最后坐在 2 号座位。
    examRoom.leave(4);
    examRoom.seat(); // 返回 5,学生最后坐在 5 号座位。
    提示:
  • 1 <= n <= 10^9
  • 保证有学生正坐在座位 p 上
  • seat 和 leave 最多被调用 10^4 次

解题思路

学生总是坐到「距离最近已坐者最远」的位置,等价于把空座位按相连区间维护,每次选择最佳

区间。由于 n 可达 10^9,不能按座位逐位扫描。

解法一(有序集合 + 哈希表,主提交):用 TreeSet 存所有「空区间」,区间 [l, r)

(-1 与 n 表示墙,两边端点在外侧空区间),比较器先比「该区间能提供的到最近者的距离

dist」降序、再比左端点升序。哈希表 left/right 分别记下:以 l 为左端点的区间的

右端点、以 r 为右端点的区间的左端点。seat() 取堆顶区间,坐到 dist 最大的位置

(端点区间坐到 0 或 n-1),拆成两个子区间;leave(p) 用两个哈希表找到 p 左右两个

区间并合并。所有操作 O(log n)。

解法二(有序集合存已坐者,对照实现):用 TreeSet<Integer> 存放当前已坐的座位,

seat() 时枚举相邻已坐者(含两侧墙 -1 与 n)求最大间隙;实现直观,但一次 seat 需要

O(n) 遍历全部已坐座位,仅适合小规模数据。

代码实现

java 复制代码
import java.util.HashMap;
import java.util.Map;
import java.util.TreeSet;

class ExamRoom {

    private TreeSet<int[]> ts;          // 空区间集合,key 为左端点
    private Map<Integer, Integer> left; // 区间左端点 -> 右端点
    private Map<Integer, Integer> right;// 区间右端点 -> 左端点
    private int n;

    private int dist(int[] s) {
        int l = s[0], r = s[1];
        // 端点区间(贴着墙)能提供的最大距离 = 区间长度;中间区间 = 中点距离
        return l == -1 || r == n ? r - l - 1 : (r - l) >> 1;
    }

    public ExamRoom(int n) {
        this.n = n;
        ts = new TreeSet<>((a, b) -> {
            int d1 = dist(a), d2 = dist(b);
            return d1 != d2 ? Integer.compare(d2, d1) : Integer.compare(a[0], b[0]);
        });
        left = new HashMap<>();
        right = new HashMap<>();
        add(new int[]{-1, n});
    }

    public int seat() {
        int[] s = ts.first();
        int l = s[0], r = s[1];
        int p = (l + r) >> 1;
        if (l == -1) {
            p = 0;
        } else if (r == n) {
            p = n - 1;
        }
        del(s);
        add(new int[]{l, p});
        add(new int[]{p, r});
        return p;
    }

    public void leave(int p) {
        int l = right.get(p);   // p 左侧空区间的左端点
        int r = left.get(p);    // p 右侧空区间的右端点
        del(new int[]{l, p});
        del(new int[]{p, r});
        add(new int[]{l, r});
    }

    private void add(int[] s) {
        ts.add(s);
        left.put(s[0], s[1]);
        right.put(s[1], s[0]);
    }

    private void del(int[] s) {
        ts.remove(s);
        left.remove(s[0]);
        right.remove(s[1]);
    }

    // 解法二:有序集合存已坐者,枚举相邻座位找最大间隙(对照实现)
    // 设计题两种实现分开写,避免字段冲突
    static class ExamRoom2 {

        private final TreeSet<Integer> seated;
        private final int n;

        public ExamRoom2(int n) {
            this.n = n;
            seated = new TreeSet<>();
            seated.add(-1);
            seated.add(n);
        }

        public int seat() {
            int best = -1;
            int bestDist = -1;
            Integer prev = null;
            for (int cur : seated) {
                if (prev == null) {
                    prev = cur;
                    continue;
                }
                // prev 与 cur 之间的空区间 [prev+1, cur-1]
                int d;
                int p;
                if (prev == -1) {
                    p = 0;
                    d = cur;         // 到 0 号位的距离
                } else if (cur == n) {
                    p = n - 1;
                    d = n - 1 - prev;
                } else {
                    p = (prev + cur) >> 1;
                    d = Math.min(p - prev, cur - p);
                }
                if (d > bestDist || (d == bestDist && p < best)) {
                    bestDist = d;
                    best = p;
                }
                prev = cur;
            }
            seated.add(best);
            return best;
        }

        public void leave(int p) {
            seated.remove(p);
        }
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        int pass = 0, total = 0;
        { // 官方示例
            ExamRoom room = new ExamRoom(10);
            ExamRoom2 room2 = new ExamRoom2(10);
            int[] expects = {0, 9, 4, 2};
            int[] acts = new int[4], acts2 = new int[4];
            for (int i = 0; i < 4; ++i) {
                acts[i] = room.seat();
                acts2[i] = room2.seat();
            }
            ++total;
            if (java.util.Arrays.equals(acts, expects) && java.util.Arrays.equals(acts2, expects)) {
                ++pass;
            } else {
                System.out.println("FAIL 855-1: " + java.util.Arrays.toString(acts));
            }
            room.leave(4);
            room2.leave(4);
            ++total;
            if (room.seat() == 5 && room2.seat() == 5) {
                ++pass;
            } else {
                System.out.println("FAIL 855-2: got " + room.seat());
            }
        }
        { // n=1
            ExamRoom room = new ExamRoom(1);
            ++total;
            if (room.seat() == 0) {
                ++pass;
            } else {
                System.out.println("FAIL 855-3a");
            }
            room.leave(0);
            ++total;
            if (room.seat() == 0) {
                ++pass;
            } else {
                System.out.println("FAIL 855-3b");
            }
        }
        { // 连续 seat 到满 (n=8)
            ExamRoom room = new ExamRoom(8);
            int[] expects = {0, 7, 3, 5, 1, 2, 4, 6};
            int[] acts = new int[8];
            for (int i = 0; i < 8; ++i) {
                acts[i] = room.seat();
            }
            ++total;
            if (java.util.Arrays.equals(acts, expects)) {
                ++pass;
            } else {
                System.out.println("FAIL 855-4: " + java.util.Arrays.toString(acts));
            }
            room.leave(0);
            ++total;
            if (room.seat() == 0) {
                ++pass;
            } else {
                System.out.println("FAIL 855-5: got " + room.seat());
            }
        }
        { // 交替坐/离(混合操作)
            ExamRoom room = new ExamRoom(6);
            int[] seq1 = {room.seat(), room.seat()};
            ++total;
            if (seq1[0] == 0 && seq1[1] == 5) {
                ++pass;
            } else {
                System.out.println("FAIL 855-6: " + java.util.Arrays.toString(seq1));
            }
            room.leave(0);
            ++total;
            if (room.seat() == 0) {
                ++pass;
            } else {
                System.out.println("FAIL 855-7");
            }
        }
        if (pass == total) {
            System.out.println("== 0855 PASS ==");
        } else {
            System.out.println("== 0855 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

| 解法 | 时间复杂度 | 空间复杂度 |

| --- | | --- |


856. Score of Parentheses(括号的分数)

题目描述

给定一个平衡括号字符串 s,按下述规则计算该字符串的分数:

  • () 得 1 分。
  • AB 得 A + B 分,其中 A 和 B 是平衡括号字符串。
  • (A) 得 2 * A 分,其中 A 是平衡括号字符串。

示例 1

  • 输入:"()"
  • 输出:1

示例 2

  • 输入:"(())"
  • 输出:2

示例 3

  • 输入:"()()"
  • 输出:2

示例 4

  • 输入:"(()(()))"
  • 输出 :6
    提示:
  • s 是平衡括号字符串,且只含有 ( 和 )
  • 2 <= s.length <= 50

解题思路

括号串天然形成嵌套结构,可用栈模拟"分数相加"。每个分数在括号闭合时翻倍。

解法一(栈,主提交):栈中保存「当前层累加的分数」。遇到 ( 压入 0 作为新层;

遇到 ) 弹出顶层分数 v:若 v == 0 表示 (),本层贡献 1;否则翻倍 2 * v。

把这一贡献加到新的栈顶(父层),若栈空则把该值累加到答案。所有括号遍历完即得总分。

解法二(深度计数):() 是最基本元素,其分数贡献为 1 << depth,其中 depth 是该

括号所在的嵌套深度。遍历时维护当前深度,遇到 () 直接累加 1 << depth,线性时间、常数空间。

代码实现

java 复制代码
import java.util.ArrayDeque;
import java.util.Deque;

class Solution {

    // 解法一:栈(推荐提交)
    public int scoreOfParentheses(String s) {
        Deque<Integer> stack = new ArrayDeque<>();
        int ans = 0;
        for (char c : s.toCharArray()) {
            if (c == '(') {
                stack.push(0);
            } else {
                int v = stack.pop();
                if (v == 0) {
                    v = 1;                 // () 得 1 分
                } else {
                    v <<= 1;               // (A) 得 2 * A 分
                }
                if (stack.isEmpty()) {
                    ans += v;
                } else {
                    stack.push(stack.pop() + v);   // AB 得 A + B
                }
            }
        }
        return ans;
    }

    // 解法二:深度计数(对照实现)
    public int scoreOfParentheses2(String s) {
        int ans = 0;
        int depth = 0;
        for (int i = 0; i < s.length(); ++i) {
            char c = s.charAt(i);
            if (c == '(') {
                ++depth;
            } else {
                --depth;
                if (i > 0 && s.charAt(i - 1) == '(') {
                    ans += 1 << depth;     // 基本元素 () 的贡献
                }
            }
        }
        return ans;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        String[] cs = {"()", "(())", "()()", "(()(()))", "((()))", "()(())"};
        int[] exp = {1, 2, 2, 6, 4, 3};
        for (int t = 0; t < cs.length; ++t) {
            ++total;
            if (s.scoreOfParentheses(cs[t]) == exp[t] && s.scoreOfParentheses2(cs[t]) == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 856-" + (t + 1) + ": got "
                        + s.scoreOfParentheses(cs[t]) + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0856 PASS ==");
        } else {
            System.out.println("== 0856 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(n) O(n)
解法二 O(n) O(1)

857. Minimum Cost to Hire K Workers(雇佣 K 名工人的最低成本)

题目描述

有 n 名工人。给定两个数组 quality 和 wage,其中 quality[i] 表示第 i 名工人的工作

质量,其最低期望工资为 wage[i]。

现在我们想雇佣 k 名工人组成一个工资组。在雇佣一组 k 名工人时,我们必须按照下述规则

向他们支付工资:

  • 对工资组中的每名工人,应当按其工作质量与同组其他工人的工作质量的比例来支付工资。
  • 工资组中的每名工人至少应当得到他们的最低期望工资。
    给定整数 k,返回组成满足上述条件的付费群体所需的最小金额。与实际答案误差相差在
    10^-5 以内的答案将被接受。

示例 1

  • 输入 :quality = 10,20,5, wage = 70,50,30, k = 2
  • 输出:105.00000
  • 解释:我们向 0 号工人支付 70,向 2 号工人支付 35。

示例 2

  • 输入 :quality = 3,1,10,10,1, wage = 4,8,2,2,7, k = 3
  • 输出:30.66667
  • 解释 :我们向 0 号工人支付 4,向 2 号和 3 号分别支付 13.33333。
    提示:
  • n == quality.length == wage.length
  • 1 <= k <= n <= 10^4
  • 1 <= quality[i], wage[i] <= 10^4

解题思路

若以某名工人 i 为「工资基准」(按比例支付时恰好等于其最低期望工资),则组内任意工人 j

的工资为 wage[i] / quality[i] * quality[j],且该值必须不小于 wage[j],即

wage[j] / quality[j] <= wage[i] / quality[i]。因此把工人按 ratio = wage / quality

升序排序后,最优组的「基准」一定是组内 ratio 最大的人,组内其余人只需在前缀里取。

解法一(排序 + 大根堆,主提交):按 ratio 升序排序后从左到右扫描,把 quality 累加进

大根堆,堆内始终保留扫描到的前 k 小质量;堆满 k 个时,用「当前基准的 ratio × 质量总和」

计算以当前工人为基准的组成本,取最小值。扫描完即得全局最优。

解法二(暴力枚举基准 + 前缀选 k 小质量,对照实现):对每个工人作为基准,在前 i 个工人中

选质量最小的 k - 1 个(当 i >= k - 1),计算总成本取最小。思路最直观,但每次选择需要

排序或堆,复杂度 O(n^2 log n) 量级,仅适合小数据。

代码实现

java 复制代码
import java.util.Arrays;
import java.util.PriorityQueue;

class Solution {

    // 解法一:排序 + 大根堆贪心(推荐提交)
    public double mincostToHireWorkers(int[] quality, int[] wage, int k) {
        int n = quality.length;
        Integer[] idx = new Integer[n];
        for (int i = 0; i < n; ++i) {
            idx[i] = i;
        }
        // 按"单位质量期望工资"升序排序
        Arrays.sort(idx, (a, b) -> Double.compare(
                (double) wage[a] / quality[a], (double) wage[b] / quality[b]));
        double ans = Double.MAX_VALUE;
        int qSum = 0;
        // 大根堆:保留已扫描工人的最大质量,便于弹出最大的以保留 k 个最小质量
        PriorityQueue<Integer> pq = new PriorityQueue<>((a, b) -> Integer.compare(b, a));
        for (int i = 0; i < n; ++i) {
            int qi = quality[idx[i]];
            pq.offer(qi);
            qSum += qi;
            if (pq.size() > k) {
                qSum -= pq.poll();
            }
            if (pq.size() == k) {
                double ratio = (double) wage[idx[i]] / quality[idx[i]];
                ans = Math.min(ans, ratio * qSum);
            }
        }
        return ans;
    }

    // 解法二:枚举基准 + 堆选 k-1 小质量(对照实现)
    public double mincostToHireWorkers2(int[] quality, int[] wage, int k) {
        int n = quality.length;
        Integer[] idx = new Integer[n];
        for (int i = 0; i < n; ++i) {
            idx[i] = i;
        }
        Arrays.sort(idx, (a, b) -> Double.compare(
                (double) wage[a] / quality[a], (double) wage[b] / quality[b]));
        double ans = Double.MAX_VALUE;
        for (int t = 0; t < n; ++t) {
            int base = idx[t];
            double ratio = (double) wage[base] / quality[base];
            if (t + 1 < k) {
                continue;   // 前缀人数不足 k 人
            }
            // 在前 t 个工人(不含基准)中选质量最小的 k-1 个
            PriorityQueue<Integer> pq = new PriorityQueue<>();   // 小根堆
            for (int j = 0; j < t; ++j) {
                pq.offer(quality[idx[j]]);
            }
            long qSum = quality[base];
            for (int c = 0; c < k - 1; ++c) {
                qSum += pq.poll();
            }
            ans = Math.min(ans, ratio * qSum);
        }
        return ans;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        double eps = 1e-5;
        ++total;
        if (Math.abs(s.mincostToHireWorkers(new int[]{10, 20, 5}, new int[]{70, 50, 30}, 2) - 105.0) < eps) {
            ++pass;
        } else {
            System.out.println("FAIL 857-1: " + s.mincostToHireWorkers(new int[]{10, 20, 5}, new int[]{70, 50, 30}, 2));
        }
        ++total;
        if (Math.abs(s.mincostToHireWorkers(new int[]{3, 1, 10, 10, 1}, new int[]{4, 8, 2, 2, 7}, 3) - 30.66667) < 1e-3) {
            ++pass;
        } else {
            System.out.println("FAIL 857-2: " + s.mincostToHireWorkers(new int[]{3, 1, 10, 10, 1}, new int[]{4, 8, 2, 2, 7}, 3));
        }
        ++total;
        if (Math.abs(s.mincostToHireWorkers(new int[]{2, 1, 3}, new int[]{3, 2, 4}, 2) - 6.0) < eps) {
            ++pass;
        } else {
            System.out.println("FAIL 857-3: " + s.mincostToHireWorkers(new int[]{2, 1, 3}, new int[]{3, 2, 4}, 2));
        }
        ++total;
        if (Math.abs(s.mincostToHireWorkers2(new int[]{10, 20, 5}, new int[]{70, 50, 30}, 2) - 105.0) < eps
                && Math.abs(s.mincostToHireWorkers2(new int[]{3, 1, 10, 10, 1}, new int[]{4, 8, 2, 2, 7}, 3) - 30.66667) < 1e-3) {
            ++pass;
        } else {
            System.out.println("FAIL 857-4");
        }
        if (pass == total) {
            System.out.println("== 0857 PASS ==");
        } else {
            System.out.println("== 0857 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(n log n) O(n)
解法二 O(n^2 log n) O(n)

858. Mirror Reflection(镜面反射)

题目描述

有一个特殊的正方形房间,每面墙上都有一面镜子。除西南角以外,每个角落都放有一个接受器,

编号为 0、1 和 2。

正方形房间的墙壁长度为 p,一束激光从西南角射出,首先会与东墙相遇,入射点到射入点的

距离为 q(即激光在竖直方向上走了 q 个单位)。

返回光线最先到达的接受器的编号(保证光线最终会到达一个接收器)。

示例 1

  • 输入 :p = 2, q = 1
  • 输出:2
  • 解释 :此射线在反射后第一次到达接受器 2(东北角)。
    提示:
  • 1 <= q <= p <= 1000

解题思路

把镜子反射"展开":光线进入无数个上下、左右堆叠的镜像房间,横向坐标在 0 与 p 之间

来回折返,纵向整体持续上升 q。光线第 k 次撞墙时纵向坐标为 k * q,能碰到某个接收器

当且仅当纵向坐标是 p 的整数倍。

解法一(数学法,主提交):令 g = gcd(p, q),设 pp = (p/g) % 2、qq = (q/g) % 2

(p、q 同除最大公约数后看奇偶):

  • pp == 1 && qq == 1:到达左侧角落,接收器 1;
  • pp == 1:到达左侧墙且纵向在墙中(上下对称位置),接收器 0;
  • 否则(pp == 0):到达右侧角落,接收器 2。
    解法二(展开模拟,对照实现):把房间横向宽度翻倍为 2p,直接由 k 计算
    x = (p * k) % (2p)、y = (q * k) % (2p),若 y == p(到达南北墙)且 x == p 则接收器
    1,x == 0 则接收器 2;若 y == 0 且 x == p 则接收器 0。最多迭代 2p 次。

代码实现

java 复制代码
class Solution {

    // 解法一:数学(约分看奇偶,推荐提交)
    public int mirrorReflection(int p, int q) {
        int g = gcd(p, q);
        int pp = (p / g) % 2;
        int qq = (q / g) % 2;
        if (pp == 1 && qq == 1) {
            return 1;
        }
        return pp == 1 ? 0 : 2;
    }

    private int gcd(int a, int b) {
        return b == 0 ? a : gcd(b, a % b);
    }

    // 解法二:展开房间逐次模拟(对照实现)
    public int mirrorReflection2(int p, int q) {
        int mod = 2 * p;
        for (int k = 1; k <= mod * 2; ++k) {
            int x = (p * k) % mod;
            int y = (q * k) % mod;
            if (y == p) {
                if (x == p) {
                    return 1;
                }
                if (x == 0) {
                    return 2;
                }
            } else if (y == 0) {
                if (x == p) {
                    return 0;
                }
            }
            if (x == 0 && y == 0) {
                continue;   // 回到起点(西南角),继续
            }
        }
        return -1;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[][] cases = {{2, 1}, {3, 1}, {3, 2}, {4, 2}, {5, 3}, {1000, 999}};
        int[] exp = {2, 1, 0, 2, 1, 2};
        for (int t = 0; t < cases.length; ++t) {
            ++total;
            int g1 = s.mirrorReflection(cases[t][0], cases[t][1]);
            int g2 = s.mirrorReflection2(cases[t][0], cases[t][1]);
            if (g1 == exp[t] && g2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 858-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0858 PASS ==");
        } else {
            System.out.println("== 0858 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(log min(p, q)) O(1)
解法二 O(p / gcd(p, q)) O(1)

859. Buddy Strings(亲密字符串)

题目描述

给你两个字符串 s 和 goal,只要我们可以通过交换 s 中的两个字母得到与 goal 相等

的结果,就返回 true;否则返回 false。

交换字母的定义是:取两个下标 i 和 j(下标从 0 开始)且满足 i != j,接着交换

s[i] 和 s[j]。

例如,在 "abcd" 中交换下标 0 和下标 2 的元素可以产生 "cbad"。

示例 1

  • 输入 :s = "ab", goal = "ba"
  • 输出 :true

示例 2

  • 输入 :s = "ab", goal = "ab"
  • 输出 :false

示例 3

  • 输入 :s = "aa", goal = "aa"
  • 输出 :true
    提示:
  • 1 <= s.length, goal.length <= 2 * 10^4
  • s 和 goal 由小写英文字母组成

解题思路

按 s 与 goal 是否已经相等分两种情况。

解法一(分类讨论,主提交):若两串长度不等直接返回 false。

  • 若 s == goal:需要交换两个不同下标且交换后不变,即存在某个字符出现至少两次,
    s 中有一对相同字符可交换,返回 true,否则 false。
  • 若 s != goal:统计所有 s[i] != goal[i] 的下标。恰好两个位置不同,且
    s[i] == goal[j] && s[j] == goal[i] 才可由一次交换成功。
    时间复杂度 O(n),空间 O(1)。
    解法二(计数比对,对照实现):先检查长度相等与任一字符是否重复;再用两个长度 26 的
    计数器分别统计 s 与 goal 的字符频次,若不相似则某些频次不同直接返回 false;最后按
    不同位置的个数判断:为 0 时需有重复字符,为 2 时需互为"换位"。实现略繁琐但思路与
    解法一一致,作为验证交叉。

代码实现

java 复制代码
class Solution {

    // 解法一:分类讨论(推荐提交)
    public boolean buddyStrings(String s, String goal) {
        int n = s.length();
        if (n != goal.length()) {
            return false;
        }
        if (s.equals(goal)) {
            int[] cnt = new int[26];
            for (int i = 0; i < n; ++i) {
                int c = s.charAt(i) - 'a';
                if (++cnt[c] >= 2) {
                    return true;
                }
            }
            return false;
        }
        int first = -1, second = -1;
        for (int i = 0; i < n; ++i) {
            if (s.charAt(i) != goal.charAt(i)) {
                if (first == -1) {
                    first = i;
                } else if (second == -1) {
                    second = i;
                } else {
                    return false;   // 超过两处不同
                }
            }
        }
        return second != -1
                && s.charAt(first) == goal.charAt(second)
                && s.charAt(second) == goal.charAt(first);
    }

    // 解法二:计数比对(对照实现)
    public boolean buddyStrings2(String s, String goal) {
        int n = s.length();
        if (n != goal.length()) {
            return false;
        }
        int[] cs = new int[26];
        int[] cg = new int[26];
        boolean dup = false;
        for (int i = 0; i < n; ++i) {
            if (++cs[s.charAt(i) - 'a'] >= 2) {
                dup = true;
            }
            ++cg[goal.charAt(i) - 'a'];
        }
        for (int i = 0; i < 26; ++i) {
            if (cs[i] != cg[i]) {
                return false;
            }
        }
        int diff = 0;
        for (int i = 0; i < n; ++i) {
            if (s.charAt(i) != goal.charAt(i)) {
                ++diff;
            }
        }
        return diff == 2 || (diff == 0 && dup);
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        String[][] cases = {
            {"ab", "ba"}, {"ab", "ab"}, {"aa", "aa"}, {"aaaaaaabc", "aaaaaaacb"},
            {"", ""}, {"abcaa", "abcbb"}, {"a", "a"}, {"ab", "ab"}
        };
        boolean[] exp = {true, false, true, true, false, false, false, false};
        for (int t = 0; t < cases.length; ++t) {
            ++total;
            if (s.buddyStrings(cases[t][0], cases[t][1]) == exp[t]
                    && s.buddyStrings2(cases[t][0], cases[t][1]) == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 859-" + (t + 1) + ": got "
                        + s.buddyStrings(cases[t][0], cases[t][1]) + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0859 PASS ==");
        } else {
            System.out.println("== 0859 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(n) O(1)
解法二 O(n) O(1)

860. Lemonade Change(柠檬水找零)

题目描述

在柠檬水摊上,每一杯柠檬水的售价为 5 美元。顾客排队购买你的产品,(按账单 bills

支付的顺序)一次购买一杯。

每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客

正确找零,也就是说净交易是每位顾客向你支付 5 美元。

注意,一开始你手头没有任何零钱。

给你一个整数数组 bills,其中 bills[i] 是第 i 位顾客付的账。如果你能给每位顾客正确

找零,返回 true,否则返回 false。

示例 1

  • 输入:bills = 5,5,5,10,20
  • 输出 :true
  • 解释 :
    前 3 位顾客那里,我们按顺序收取 3 张 5 美元的钞票。
    第 4 位顾客那里,我们收取一张 10 美元的钞票,并找回 5 美元。
    第 5 位顾客那里,我们找还一张 10 美元的钞票和一张 5 美元的钞票。
    由于所有客户都得到了正确的找零,所以我们输出 true。

示例 2

  • 输入:bills = 5,5,10,10,20
  • 输出 :false
  • 解释 :
    前 2 位顾客那里,我们按顺序收取 2 张 5 美元的钞票。
    对于接下来的 2 位顾客,我们收取一张 10 美元的钞票,然后找回 5 美元。
    对于最后一位顾客,我们无法退回 15 美元,因为我们现在只有两张 10 美元的钞票。
    由于不是每位顾客都得到了正确的找零,所以答案是 false。
    提示:
  • 1 <= bills.length <= 10^5
  • bills[i] 不是 5 就是 10 或是 20

解题思路

找零时永远优先花掉大面额(10 美元),把 5 美元留给更需要的场合,这是最优贪心策略。

解法一(贪心模拟,主提交):维护 five、ten 两张面额的张数。

  • 收 5 元:five++;
  • 收 10 元:必须有 5 元,five--,ten++;
  • 收 20 元:优先 ten--, five--(找 15),否则 five -= 3,任何一档不足即返回 false。
    20 元面值无法找回,只进不出。
    解法二(计数数组递推,对照实现):用长度为 3 的数组计数。无论哪种找零顺序,核心仍是
    20 元场景先花 10 元;该写法把"优先花大额"显式表达为分支条件,逻辑等价。

代码实现

java 复制代码
class Solution {

    // 解法一:贪心模拟(推荐提交)
    public boolean lemonadeChange(int[] bills) {
        int five = 0, ten = 0;
        for (int b : bills) {
            if (b == 5) {
                ++five;
            } else if (b == 10) {
                if (five == 0) {
                    return false;
                }
                --five;
                ++ten;
            } else {   // 20
                if (ten > 0 && five > 0) {
                    --ten;
                    --five;
                } else if (five >= 3) {
                    five -= 3;
                } else {
                    return false;
                }
            }
        }
        return true;
    }

    // 解法二:计数数组(对照实现)
    public boolean lemonadeChange2(int[] bills) {
        int[] cnt = new int[3];   // 下标 0 -> 5 元,1 -> 10 元,2 -> 20 元
        for (int b : bills) {
            if (b == 5) {
                ++cnt[0];
            } else if (b == 10) {
                if (cnt[0] == 0) {
                    return false;
                }
                --cnt[0];
                ++cnt[1];
            } else {
                if (cnt[1] > 0 && cnt[0] > 0) {
                    --cnt[1];
                    --cnt[0];
                } else if (cnt[0] >= 3) {
                    cnt[0] -= 3;
                } else {
                    return false;
                }
            }
        }
        return true;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[][] billsArr = {
            {5, 5, 5, 10, 20},
            {5, 5, 10, 10, 20},
            {5, 5, 5, 20},
            {5, 10, 5, 20},
            {20},
            {5}
        };
        boolean[] exp = {true, false, true, true, false, true};
        for (int t = 0; t < billsArr.length; ++t) {
            ++total;
            if (s.lemonadeChange(billsArr[t]) == exp[t]
                    && s.lemonadeChange2(billsArr[t]) == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 860-" + (t + 1) + ": got "
                        + s.lemonadeChange(billsArr[t]) + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0860 PASS ==");
        } else {
            System.out.println("== 0860 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(n) O(1)
解法二 O(n) O(1)

861. Score After Flipping Matrix(翻转矩阵后的得分)

题目描述

给你一个大小为 m x n 的二元矩阵 grid,其中每个单元格的值为 0 或 1(下标从 0 开始

编号)。我们可以按下面的规则进行任意次操作:

  • 选择一行翻转:这一行的 1 变成 0,0 变成 1。
  • 选择一列翻转:这一列的 1 变成 0,0 变成 1。
    每行解释为一个二进制数(从最高有效位开始),矩阵的得分是所有这些数字的总和。
    在执行任意次操作后,返回矩阵可能获得的最高分数。

示例 1

  • 输入 :grid = \[0,0,1,1,1,0,1,0,1,1,0,0]
  • 输出:39
  • 解释 :
    0b1111 + 0b1001 + 0b1011 = 15 + 9 + 11 = 39

示例 2

  • 输入 :grid = \[0]
  • 输出 :1
    提示:
  • m == grid.length
  • n == grid[i].length
  • 1 <= m, n <= 20
  • grid[i][j] 不是 0 就是 1

解题思路

最高位(每行首位)的权重比该行其余所有位加起来还大,因此必然先把每行首位翻成 1;

之后列之间互不影响,对每一列,尽量让 1 的数量更多(若 1 少于 0 就翻该列)。

解法一(贪心,主提交):第一遍:对每行,若 grid[i][0] == 0 则整行翻转(等价于仅作

行翻转操作,不必真实翻转,先计算时直接把该行整体取反处理)。第二遍:对每列统计

grid[i][j] == 1(行首为 0 的行按取反计)的个数,取 max(ones, m - ones) 贡献到答案。

不实际修改矩阵,直接用公式累加:ans = sum over j of max(ones_j, m - ones_j) << (n-1-j)。

解法二(按位直接求解,对照实现):由于每列独立且可翻转,先把数组复制一份并真正执行

行翻转,再用计数求列最优;实现直接、便于对照,但多一次拷贝。

代码实现

java 复制代码
class Solution {

    // 解法一:贪心(不实际翻转,直接计算,推荐提交)
    public int matrixScore(int[][] grid) {
        int m = grid.length, n = grid[0].length;
        int ans = 0;
        for (int j = 0; j < n; ++j) {
            int ones = 0;
            for (int i = 0; i < m; ++i) {
                // 若第 i 行首位是 0,该行会被翻转,因此该格实际值取反
                ones += (grid[i][0] ^ grid[i][j]);   // grid[i][0]==1 时按原值,==0 时按取反
            }
            ans += Math.max(ones, m - ones) << (n - 1 - j);
        }
        return ans;
    }

    // 解法二:真实翻转后统计(对照实现)
    public int matrixScore2(int[][] grid) {
        int m = grid.length, n = grid[0].length;
        int[][] g = new int[m][n];
        for (int i = 0; i < m; ++i) {
            for (int j = 0; j < n; ++j) {
                g[i][j] = grid[i][0] == 1 ? grid[i][j] : (1 - grid[i][j]);
            }
        }
        int ans = 0;
        for (int j = 0; j < n; ++j) {
            int ones = 0;
            for (int i = 0; i < m; ++i) {
                ones += g[i][j];
            }
            if (ones * 2 < m) {   // 该列 1 少,翻转整列
                for (int i = 0; i < m; ++i) {
                    g[i][j] = 1 - g[i][j];
                }
                ones = m - ones;
            }
            ans += ones << (n - 1 - j);
        }
        return ans;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        ++total;
        if (s.matrixScore(new int[][]{{0, 0, 1, 1}, {1, 0, 1, 0}, {1, 1, 0, 0}}) == 39) {
            ++pass;
        } else {
            System.out.println("FAIL 861-1: " + s.matrixScore(new int[][]{{0, 0, 1, 1}, {1, 0, 1, 0}, {1, 1, 0, 0}}));
        }
        ++total;
        if (s.matrixScore(new int[][]{{0}}) == 1) {
            ++pass;
        } else {
            System.out.println("FAIL 861-2");
        }
        ++total;
        if (s.matrixScore(new int[][]{{0, 1}, {1, 1}}) == 5) {
            ++pass;
        } else {
            System.out.println("FAIL 861-3: " + s.matrixScore(new int[][]{{0, 1}, {1, 1}}));
        }
        ++total;
        if (s.matrixScore2(new int[][]{{0, 0, 1, 1}, {1, 0, 1, 0}, {1, 1, 0, 0}}) == 39
                && s.matrixScore2(new int[][]{{0}}) == 1) {
            ++pass;
        } else {
            System.out.println("FAIL 861-4");
        }
        if (pass == total) {
            System.out.println("== 0861 PASS ==");
        } else {
            System.out.println("== 0861 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(m n) O(1)
解法二 O(m n) O(m n)

862. Shortest Subarray with Sum at Least K(和至少为 K 的最短子数组)

题目描述

给你一个整数数组 nums 和一个整数 k,找出 nums 中和至少为 k 的最短非空子数组,

并返回该子数组的长度。如果不存在这样的子数组,返回 -1。

子数组是数组中连续的一部分。

示例 1

  • 输入 :nums = 1, k = 1
  • 输出:1

示例 2

  • 输入 :nums = 1,2, k = 4
  • 输出:-1

示例 3

  • 输入 :nums = 2,-1,2, k = 3
  • 输出 :3
    提示:
  • 1 <= nums.length <= 10^5
  • -10^5 <= nums[i] <= 10^5
  • 1 <= k <= 10^9

解题思路

和至少为 k 的子数组 [i, j] 等价于前缀和满足 pre[j] - pre[i] >= k,其中 i < j。

由于 nums 含负数,前缀和不单调,不能直接双指针,需要用单调队列维护候选起点。

解法一(前缀和 + 单调递增队列,主提交):构造前缀和数组 pre(pre[0] = 0)。遍历

j 从 0 到 n:

  • 队首 i 满足 pre[j] - pre[i] >= k 时,更新答案 j - i,并把 i 弹出(i 已经不会
    为更远的 j 提供更短答案);
  • 维护队列前缀和严格递增:新下标 j 入队前,弹出所有 pre[j] <= pre[队尾] 的元素
    (队尾若不如 j"小",永远不会成为更优起点)。
    队列操作均摊 O(1),总复杂度 O(n)。
    解法二(暴力前缀枚举,对照实现):枚举所有 (i, j) 对检查前缀和差,O(n^2) 仅作思路
    正确性参考,不适合大数据(本地仅用小用例验证)。

代码实现

java 复制代码
import java.util.ArrayDeque;
import java.util.Deque;

class Solution {

    // 解法一:前缀和 + 单调递增队列(推荐提交)
    public int shortestSubarray(int[] nums, int k) {
        int n = nums.length;
        long[] pre = new long[n + 1];
        for (int i = 0; i < n; ++i) {
            pre[i + 1] = pre[i] + nums[i];
        }
        Deque<Integer> dq = new ArrayDeque<>();   // 存前缀和下标,pre 严格递增
        int ans = Integer.MAX_VALUE;
        for (int j = 0; j <= n; ++j) {
            while (!dq.isEmpty() && pre[j] - pre[dq.peekFirst()] >= k) {
                ans = Math.min(ans, j - dq.pollFirst());
            }
            while (!dq.isEmpty() && pre[j] <= pre[dq.peekLast()]) {
                dq.pollLast();
            }
            dq.offerLast(j);
        }
        return ans == Integer.MAX_VALUE ? -1 : ans;
    }

    // 解法二:暴力前缀枚举(对照实现,仅小数据)
    public int shortestSubarray2(int[] nums, int k) {
        int n = nums.length;
        long[] pre = new long[n + 1];
        for (int i = 0; i < n; ++i) {
            pre[i + 1] = pre[i] + nums[i];
        }
        int ans = Integer.MAX_VALUE;
        for (int i = 0; i <= n; ++i) {
            for (int j = i + 1; j <= n; ++j) {
                if (pre[j] - pre[i] >= k) {
                    ans = Math.min(ans, j - i);
                    break;   // 更长的同起点不会更优
                }
            }
        }
        return ans == Integer.MAX_VALUE ? -1 : ans;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[][] numsArr = {{1}, {1, 2}, {2, -1, 2}, {84, -37, 32, 40, 95}, {-1, -1, 1}, {17, 85, 93, -45, -21}};
        int[] kArr = {1, 4, 3, 167, 1, 150};
        int[] exp = {1, -1, 3, 3, 1, 2};
        for (int t = 0; t < numsArr.length; ++t) {
            ++total;
            int g1 = s.shortestSubarray(numsArr[t], kArr[t]);
            int g2 = s.shortestSubarray2(numsArr[t], kArr[t]);
            if (g1 == exp[t] && g2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 862-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0862 PASS ==");
        } else {
            System.out.println("== 0862 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(n) O(n)
解法二 O(n^2) O(n)

863. All Nodes Distance K in Binary Tree(二叉树中所有距离为 K 的结点)

题目描述

给定一个二叉树(具有根结点 root),一个目标结点 target,和一个整数值 k。

返回到目标结点 target 距离为 k 的所有结点的值的列表。答案可以以任何顺序返回。

示例 1

  • 输入 :root = 3,5,1,6,2,0,8,`null`,`null`,7,4, target = 5, k = 2
  • 输出:7,4,1
  • 解释 :所求结点为与目标结点(值为 5)距离为 2 的结点,值分别为 7、4 以及 1。
    提示:
  • 树中节点数在 [1, 500] 范围内
  • 0 <= Node.val <= 500
  • 树中每个节点的值互不相同
  • 目标节点 target 是树上的节点
  • 0 <= k <= 1000

解题思路

距离为 k 的结点可能分布在 target 的子树内,也可能通过父节点走到其他分支,因此需先建立

从子到父的映射,再以 target 为起点做广度优先搜索。

解法一(父指针 + BFS,主提交):先用一次 DFS 记录每个结点的父节点;从 target 出发做 BFS,

记录步数,第 k 层的所有结点即为答案。用 (node, distance) 入队并记录已访问。

解法二(DFS 求路径 + 向下计数,对照实现):找到从 root 到 target 的路径,路径上的每个

祖先结点到 target 的距离为 d;对相隔 d 的祖先,其"另一侧"子树中距离该祖先恰为

k - d 的结点加入答案(注意不要重复经过路径上的下一个结点)。

代码实现

java 复制代码
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Queue;

class Solution {

    // 解法一:父指针 + BFS(推荐提交)
    public List<Integer> distanceK(TreeNode root, TreeNode target, int k) {
        Map<TreeNode, TreeNode> parent = new HashMap<>();
        dfsParent(root, null, parent);

        Queue<TreeNode> q = new ArrayDeque<>();
        q.offer(target);
        Map<TreeNode, Integer> seen = new HashMap<>();
        seen.put(target, 0);
        List<Integer> ans = new ArrayList<>();
        while (!q.isEmpty()) {
            TreeNode cur = q.poll();
            int d = seen.get(cur);
            if (d == k) {
                ans.add(cur.val);
                continue;
            }
            TreeNode[] nxt = {cur.left, cur.right, parent.get(cur)};
            for (TreeNode nb : nxt) {
                if (nb != null && !seen.containsKey(nb)) {
                    seen.put(nb, d + 1);
                    q.offer(nb);
                }
            }
        }
        return ans;
    }

    private void dfsParent(TreeNode node, TreeNode par, Map<TreeNode, TreeNode> parent) {
        if (node == null) {
            return;
        }
        if (par != null) {
            parent.put(node, par);
        }
        dfsParent(node.left, node, parent);
        dfsParent(node.right, node, parent);
    }

    // 解法二:路径记录 + 祖先侧子树收集(对照实现)
    public List<Integer> distanceK2(TreeNode root, TreeNode target, int k) {
        List<TreeNode> path = new ArrayList<>();
        findPath(root, target, path);
        List<Integer> ans = new ArrayList<>();
        int m = path.size();
        for (int i = 0; i < m; ++i) {
            TreeNode anc = path.get(i);
            int distAnc = m - 1 - i;   // anc 到 target 的距离
            TreeNode blocked = i + 1 < m ? path.get(i + 1) : null;   // 朝向 target 的子结点
            collect(anc, blocked, k - distAnc, ans);
        }
        return ans;
    }

    private boolean findPath(TreeNode node, TreeNode target, List<TreeNode> path) {
        if (node == null) {
            return false;
        }
        path.add(node);
        if (node == target) {
            return true;
        }
        if (findPath(node.left, target, path) || findPath(node.right, target, path)) {
            return true;
        }
        path.remove(path.size() - 1);
        return false;
    }

    private void collect(TreeNode node, TreeNode blocked, int rem, List<Integer> ans) {
        if (node == null || node == blocked) {
            return;
        }
        if (rem == 0) {
            ans.add(node.val);
            return;
        }
        collect(node.left, blocked, rem - 1, ans);
        collect(node.right, blocked, rem - 1, ans);
    }

    // 二叉树节点定义
    static class TreeNode {
        int val;
        TreeNode left;
        TreeNode right;
        TreeNode(int x) {
            val = x;
        }
    }

    // 本地测试辅助:按层序数组建树
    private static TreeNode build(Integer[] vals) {
        if (vals == null || vals.length == 0) {
            return null;
        }
        TreeNode root = new TreeNode(vals[0]);
        Queue<TreeNode> q = new ArrayDeque<>();
        q.offer(root);
        int i = 1;
        while (!q.isEmpty() && i < vals.length) {
            TreeNode cur = q.poll();
            if (vals[i] != null) {
                cur.left = new TreeNode(vals[i]);
                q.offer(cur.left);
            }
            ++i;
            if (i < vals.length && vals[i] != null) {
                cur.right = new TreeNode(vals[i]);
                q.offer(cur.right);
            }
            ++i;
        }
        return root;
    }

    private static java.util.List<Integer> sortList(java.util.List<Integer> list) {
        java.util.Collections.sort(list);
        return list;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        ++total;
        {
            TreeNode root = build(new Integer[]{3, 5, 1, 6, 2, 0, 8, null, null, 7, 4});
            TreeNode target = find(root, 5);
            List<Integer> got = sortList(new ArrayList<>(s.distanceK(root, target, 2)));
            List<Integer> exp = new ArrayList<>(java.util.Arrays.asList(7, 4, 1));
            java.util.Collections.sort(exp);
            if (got.equals(exp)) {
                ++pass;
            } else {
                System.out.println("FAIL 863-1: " + got);
            }
        }
        ++total;
        {
            TreeNode root = build(new Integer[]{1});
            TreeNode target = find(root, 1);
            List<Integer> got = sortList(new ArrayList<>(s.distanceK(root, target, 3)));
            if (got.equals(new ArrayList<>())) {
                ++pass;
            } else {
                System.out.println("FAIL 863-2: " + got);
            }
        }
        ++total;
        {
            TreeNode root = build(new Integer[]{3, 5, 1, 6, 2, 0, 8, null, null, 7, 4});
            TreeNode target = find(root, 5);
            List<Integer> got = sortList(new ArrayList<>(s.distanceK2(root, target, 2)));
            List<Integer> exp = new ArrayList<>(java.util.Arrays.asList(7, 4, 1));
            java.util.Collections.sort(exp);
            if (got.equals(exp)) {
                ++pass;
            } else {
                System.out.println("FAIL 863-3: " + got);
            }
        }
        ++total;
        {
            TreeNode root = build(new Integer[]{1, 2, 3, 4, 5, 6});
            TreeNode target = find(root, 2);
            List<Integer> got = sortList(new ArrayList<>(s.distanceK(root, target, 1)));
            List<Integer> exp = new ArrayList<>(java.util.Arrays.asList(1, 4, 5));
            java.util.Collections.sort(exp);
            if (got.equals(exp)) {
                ++pass;
            } else {
                System.out.println("FAIL 863-4: " + got);
            }
        }
        if (pass == total) {
            System.out.println("== 0863 PASS ==");
        } else {
            System.out.println("== 0863 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(n) O(n)
解法二 O(n) O(n)

864. Shortest Path to Get All Keys(获取所有钥匙的最短路径)

题目描述

给你一个二维网格 grid,其中:

  • '.' 代表一个空房间;
  • '#' 代表一堵墙;
  • '@' 是起点;
  • 小写字母代表钥匙;
  • 大写字母代表锁。
    我们从起点开始出发,一次移动是指向四个基本方向之一行走一个单位空间。一些网格区域有锁。
    如果我们有对应的钥匙,我们才能通过锁的区域。开始时我们没有任何钥匙。
    每次经过一个钥匙格,我们都可以把它拾起来,并锁上它对应的门(即该钥匙格标记为已收集)。
    请你返回获取所有钥匙所需要的移动的最少次数。如果无法到达,请返回 -1。

示例 1

  • 输入 :grid = "@.`a`...","###.#","`b.A.B`"
  • 输出:8
  • 解释:目标是获得所有钥匙,而不是打开所有锁。

示例 2

  • 输入 :grid = "@...`aA`","...B#.","...`b`"
  • 输出 :6
    提示:
  • m == grid.length
  • n == grid[i].length
  • 1 <= m, n <= 30
  • grid[i][j] 只含有 '.'、'#'、'@'、'a' - 'f' 以及 'A' - 'F'
  • 钥匙的数目范围是 [1, 6]
  • 每个钥匙都对应至少一个锁,每个锁也对应一把钥匙
  • 钥匙和锁的字母互不相同

解题思路

最短路径同时依赖"位置"与"当前已收集的钥匙集合",把状态扩展为 (x, y, mask) 做 BFS,

其中 mask 为 6 位二进制位表示 a~f 六把钥匙是否已拿到。

解法一(状态压缩 BFS,主提交):先扫描网格找到起点、统计钥匙总数 K(每把钥匙对应

一个位)。BFS 状态 (x, y, mask);从起点出发,四方向移动:

  • 越界或撞墙跳过;
  • 遇到锁(大写字母)时,必须已持有对应钥匙(mask >> (c-'A') & 1 == 1)才能进入;
  • 遇到钥匙(小写字母)时更新 mask,若 mask == (1<<K)-1 立即返回步数。
    用 dist[x][y][mask] 去重,保证每个状态至多扩展一次。
    解法二(优先队列 Dijkstra 视角,对照实现):把移动代价视为 1,用带 (x,y,mask) 状态的
    堆化队列做 BFS,本质上相同;此处通过自定义对象入队记录步数展示另一种状态组织方式。

代码实现

java 复制代码
import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.Map;
import java.util.Queue;

class Solution {

    private static final int[] DX = {-1, 1, 0, 0};
    private static final int[] DY = {0, 0, -1, 1};

    // 解法一:状态压缩 BFS(推荐提交)
    public int shortestPathAllKeys(String[] grid) {
        int m = grid.length, n = grid[0].length();
        int sx = 0, sy = 0, keyMask = 0;
        for (int i = 0; i < m; ++i) {
            for (int j = 0; j < n; ++j) {
                char c = grid[i].charAt(j);
                if (c == '@') {
                    sx = i;
                    sy = j;
                } else if (c >= 'a' && c <= 'f') {
                    keyMask |= 1 << (c - 'a');
                }
            }
        }
        boolean[][][] vis = new boolean[m][n][64];
        Queue<int[]> q = new ArrayDeque<>();   // {x, y, mask}
        q.offer(new int[]{sx, sy, 0});
        vis[sx][sy][0] = true;
        int step = 0;
        while (!q.isEmpty()) {
            int size = q.size();
            for (int round = 0; round < size; ++round) {
                int[] cur = q.poll();
                int x = cur[0], y = cur[1], mask = cur[2];
                if (mask == keyMask) {
                    return step;
                }
                for (int d = 0; d < 4; ++d) {
                    int nx = x + DX[d], ny = y + DY[d];
                    if (nx < 0 || nx >= m || ny < 0 || ny >= n) {
                        continue;
                    }
                    char c = grid[nx].charAt(ny);
                    if (c == '#') {
                        continue;
                    }
                    int nmask = mask;
                    if (c >= 'A' && c <= 'F') {
                        if ((mask & (1 << (c - 'A'))) == 0) {
                            continue;   // 没有对应钥匙不能通过
                        }
                    } else if (c >= 'a' && c <= 'f') {
                        nmask |= 1 << (c - 'a');
                    }
                    if (!vis[nx][ny][nmask]) {
                        vis[nx][ny][nmask] = true;
                        q.offer(new int[]{nx, ny, nmask});
                    }
                }
            }
            ++step;
        }
        return -1;
    }

    // 解法二:带步数的状态队列(对照实现)
    public int shortestPathAllKeys2(String[] grid) {
        int m = grid.length, n = grid[0].length();
        int sx = 0, sy = 0, keyMask = 0;
        for (int i = 0; i < m; ++i) {
            for (int j = 0; j < n; ++j) {
                char c = grid[i].charAt(j);
                if (c == '@') {
                    sx = i;
                    sy = j;
                } else if (c >= 'a' && c <= 'f') {
                    keyMask |= 1 << (c - 'a');
                }
            }
        }
        Map<String, Integer> dist = new HashMap<>();
        Queue<int[]> q = new ArrayDeque<>();   // {x, y, mask, dist}
        q.offer(new int[]{sx, sy, 0, 0});
        dist.put(sx + "," + sy + "," + 0, 0);
        int ans = Integer.MAX_VALUE;
        while (!q.isEmpty()) {
            int[] cur = q.poll();
            int x = cur[0], y = cur[1], mask = cur[2], d = cur[3];
            if (mask == keyMask) {
                ans = Math.min(ans, d);
                continue;
            }
            for (int dir = 0; dir < 4; ++dir) {
                int nx = x + DX[dir], ny = y + DY[dir];
                if (nx < 0 || nx >= m || ny < 0 || ny >= n) {
                    continue;
                }
                char c = grid[nx].charAt(ny);
                if (c == '#') {
                    continue;
                }
                int nmask = mask;
                if (c >= 'A' && c <= 'F') {
                    if ((mask & (1 << (c - 'A'))) == 0) {
                        continue;
                    }
                } else if (c >= 'a' && c <= 'f') {
                    nmask |= 1 << (c - 'a');
                }
                String key = nx + "," + ny + "," + nmask;
                Integer old = dist.get(key);
                if (old == null || old > d + 1) {
                    dist.put(key, d + 1);
                    q.offer(new int[]{nx, ny, nmask, d + 1});
                }
            }
        }
        return ans == Integer.MAX_VALUE ? -1 : ans;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        String[][] grids = {
            {"@.a..", "###.#", "b.A.B"},   // 官方示例 1
            {"@..aA", "..B#.", "....b"},   // 官方示例 2
            {"@Aa"},                       // 官方示例 3:钥匙被锁挡在墙后,不可达
            {"@a.", "b##", ".c#"},         // 无锁,收集 3 把钥匙需 5 步
            {"@.a..A.b"}                   // 需先拿 a 再过 A 拿 b,共 7 步
        };
        int[] exp = {8, 6, -1, 5, 7};
        for (int t = 0; t < grids.length; ++t) {
            ++total;
            int g1 = s.shortestPathAllKeys(grids[t]);
            int g2 = s.shortestPathAllKeys2(grids[t]);
            if (g1 == exp[t] && g2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 864-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0864 PASS ==");
        } else {
            System.out.println("== 0864 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(m * n * 2^K) O(m * n * 2^K)
解法二 O(m * n * 2^K) O(m * n * 2^K)

865. Smallest Subtree with all the Deepest Nodes(具有所有最深节点的最小子树)

题目描述

给定一个根为 root 的二叉树,每个节点的深度是它到根的最短距离。

返回包含原树中所有最深节点的最小子树。如果一个节点是整棵树的叶子节点且它的深度最大,

那么它自己就是包含所有最深节点的最小子树。

示例 1

  • 输入 :root = 3,5,1,6,2,0,8,`null`,`null`,7,4
  • 输出:2,7,4
  • 解释:最深节点为 7 和 4(深度 3),包含它们的最小子树是以 2 为根的子树。

示例 2

  • 输入 :root = 1
  • 输出:1

示例 3

  • 输入 :root = 0,1,3,`null`,2
  • 输出:2
  • 解释 :节点 2 是唯一的叶子节点,深度 2 最大,所以它自己就是答案。
    提示:
  • 树中节点数量在 [1, 500] 范围内
  • 0 <= Node.val <= 500
  • 每个节点的值都是独一无二的

解题思路

所有最深节点的最近公共祖先就是答案。可以自底向上判定:哪个子树"同时包含所有最深节点"。

解法一(两遍 DFS:最大深度 + 定位 LCA,主提交):第一遍求出整棵树最大深度 maxDepth;

第二遍从根开始找"同时包含 maxDepth 层节点的最深子树":若当前节点深度为 maxDepth,

它本身就是答案;否则递归左右子树,若左右子树都存在深度 maxDepth 的节点,当前节点就是

答案,否则返回包含最深节点的那个子树。

解法二(递归返回 (深度, 子树),对照实现):每个节点返回 (maxDepth, 该深度对应的子树);

若左右子树最大深度相同且都达到全局最大,则合并返回当前节点;否则返回更深一侧的结果。

只需一遍 DFS。

代码实现

java 复制代码
import java.util.ArrayDeque;
import java.util.Queue;

class Solution {

    // 解法一:最大深度 + LCA 定位(推荐提交)
    public TreeNode subtreeWithAllDeepest(TreeNode root) {
        int maxDepth = depth(root);
        return find(root, 1, maxDepth);
    }

    private int depth(TreeNode node) {
        if (node == null) {
            return 0;
        }
        return 1 + Math.max(depth(node.left), depth(node.right));
    }

    // 返回包含所有"深度 == maxDepth"节点的最小子树
    private TreeNode find(TreeNode node, int d, int maxDepth) {
        if (node == null) {
            return null;
        }
        if (d == maxDepth) {
            return node;   // 当前节点就是最深节点
        }
        TreeNode l = find(node.left, d + 1, maxDepth);
        TreeNode r = find(node.right, d + 1, maxDepth);
        if (l != null && r != null) {
            return node;   // 左右都有最深节点,交汇于此
        }
        return l != null ? l : r;
    }

    // 解法二:一遍 DFS 返回 (深度, 子树)(对照实现)
    public TreeNode subtreeWithAllDeepest2(TreeNode root) {
        // 返回二元组:deepest(root) = {最大深度, 覆盖全部最深节点的子树}
        return post(root).node;
    }

    private Result post(TreeNode node) {
        if (node == null) {
            return new Result(0, null);
        }
        Result l = post(node.left);
        Result r = post(node.right);
        if (l.depth == r.depth) {
            return new Result(l.depth + 1, node);   // 两侧同深,当前节点成为 LCA
        }
        if (l.depth > r.depth) {
            return new Result(l.depth + 1, l.node);
        }
        return new Result(r.depth + 1, r.node);
    }

    private static class Result {
        int depth;
        TreeNode node;
        Result(int depth, TreeNode node) {
            this.depth = depth;
            this.node = node;
        }
    }

    // 二叉树节点定义
    static class TreeNode {
        int val;
        TreeNode left;
        TreeNode right;
        TreeNode(int x) {
            val = x;
        }
    }

    // 本地测试辅助:层序建树
    private static TreeNode build(Integer[] vals) {
        if (vals == null || vals.length == 0) {
            return null;
        }
        TreeNode root = new TreeNode(vals[0]);
        Queue<TreeNode> q = new ArrayDeque<>();
        q.offer(root);
        int i = 1;
        while (!q.isEmpty() && i < vals.length) {
            TreeNode cur = q.poll();
            if (vals[i] != null) {
                cur.left = new TreeNode(vals[i]);
                q.offer(cur.left);
            }
            ++i;
            if (i < vals.length && vals[i] != null) {
                cur.right = new TreeNode(vals[i]);
                q.offer(cur.right);
            }
            ++i;
        }
        return root;
    }

    // 树结构转层序字符串(便于对比;LinkedList 允许 null)
    private static String dump(TreeNode root) {
        if (root == null) {
            return "[]";
        }
        StringBuilder sb = new StringBuilder("[");
        Queue<TreeNode> q = new java.util.LinkedList<>();
        q.offer(root);
        while (!q.isEmpty()) {
            TreeNode cur = q.poll();
            if (cur == null) {
                sb.append("null,");
                continue;
            }
            sb.append(cur.val).append(",");
            q.offer(cur.left);
            q.offer(cur.right);
        }
        // 去掉尾部 null
        String s = sb.toString();
        while (s.endsWith("null,")) {
            s = s.substring(0, s.length() - 5);
        }
        return s.substring(0, s.length() - 1) + "]";
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        ++total;
        {
            TreeNode root = build(new Integer[]{3, 5, 1, 6, 2, 0, 8, null, null, 7, 4});
            if (dump(s.subtreeWithAllDeepest(root)).equals("[2,7,4]")
                    && dump(s.subtreeWithAllDeepest2(root)).equals("[2,7,4]")) {
                ++pass;
            } else {
                System.out.println("FAIL 865-1: " + dump(s.subtreeWithAllDeepest(root)));
            }
        }
        ++total;
        {
            TreeNode root = build(new Integer[]{1});
            if (dump(s.subtreeWithAllDeepest(root)).equals("[1]")) {
                ++pass;
            } else {
                System.out.println("FAIL 865-2");
            }
        }
        ++total;
        {
            TreeNode root = build(new Integer[]{0, 1, 3, null, 2});
            TreeNode got = s.subtreeWithAllDeepest(root);
            if (got != null && got.val == 2 && s.subtreeWithAllDeepest2(root).val == 2) {
                ++pass;
            } else {
                System.out.println("FAIL 865-3: " + (got == null ? "null" : got.val));
            }
        }
        ++total;
        {
            TreeNode root = build(new Integer[]{1, 2, 3, 4});
            if (dump(s.subtreeWithAllDeepest(root)).equals("[4]")) {
                ++pass;
            } else {
                System.out.println("FAIL 865-4: " + dump(s.subtreeWithAllDeepest(root)));
            }
        }
        if (pass == total) {
            System.out.println("== 0865 PASS ==");
        } else {
            System.out.println("== 0865 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(n) O(h)(树高,递归栈)
解法二 O(n) O(h)

866. Prime Palindrome(回文质数)

题目描述

返回大于或等于 n 的最小回文质数。

一个整数如果它本身是回文数(正读反读相同),并且是质数,那么它就是回文质数。

示例 1

  • 输入 :n = 6
  • 输出:7

示例 2

  • 输入 :n = 8
  • 输出:11

示例 3

  • 输入 :n = 13
  • 输出 :101
    提示:
  • 1 <= n <= 10^8
  • 答案确保是一个小于 2 * 10^8 的 32 位有符号整数

解题思路

直接枚举每个整数判断回文再判断质数可行但较慢;利用"偶数长度的回文数一定被 11 整除

(除 11 本身)",因此长度大于 1 的回文质数必为奇数位。可从低到高枚举回文数的"上半部分",

构造出完整回文后检查是否 >= n 且为质数,保证答案按数值升序遍历。

解法一(枚举回文数,主提交):从 1 位开始向上枚举奇数位长度,对每个长度枚举"前一半"

的数字 i(最小为 10^(len-1) 的数量级),镜像构造回文,跳过偶数长度(除 11),逐个判断

质数,第一个 >= n 且为质数的即是答案。

解法二(枚举整数 + 改进质数判断,对照实现):从 n 开始逐个整数试探,若该数是回文且是

质数则返回;通过数字转字符串判回文、质数判断只试到 sqrt。在题目范围内通常足够快,

但最坏情况可能多尝试一些数字,思路更直接。

代码实现

java 复制代码
class Solution {

    // 解法一:枚举回文数(推荐提交)
    public int primePalindrome(int n) {
        if (n <= 2) {
            return 2;
        }
        if (n == 3) {
            return 3;
        }
        // 2、3、5、7、11 特判
        if (n <= 5) {
            return 5;
        }
        if (n <= 7) {
            return 7;
        }
        if (n <= 11) {
            return 11;
        }
        // 构造长度大于等于从 n 的位数开始(答案位数 >= n 的位数)
        for (int len = String.valueOf(n).length(); ; ++len) {
            if (len % 2 == 0) {
                // 偶数位回文数(除 11)均为 11 的倍数,跳过
                if (len == 2) {
                    continue;   // 11 已特判
                }
                continue;
            }
            int half = (len + 1) / 2;          // 前半部分长度
            long lo = (long) Math.pow(10, half - 1);
            long hi = (long) Math.pow(10, half);   // 前半部分范围 [lo, hi)
            for (long prefix = lo; prefix < hi; ++prefix) {
                long pal = mirror(prefix);
                if (pal < n) {
                    continue;
                }
                if (isPrime(pal)) {
                    return (int) pal;
                }
            }
        }
    }

    // 由前半部分构造奇数位回文数
    private long mirror(long prefix) {
        String s = Long.toString(prefix);
        String rev = new StringBuilder(s).reverse().toString();
        return Long.parseLong(s + rev.substring(1));
    }

    private boolean isPrime(long x) {
        if (x < 2) {
            return false;
        }
        if (x == 2 || x == 3) {
            return true;
        }
        if (x % 2 == 0 || x % 3 == 0) {
            return false;
        }
        for (long i = 5; i * i <= x; i += 6) {
            if (x % i == 0 || x % (i + 2) == 0) {
                return false;
            }
        }
        return true;
    }

    // 解法二:从 n 逐个枚举(对照实现)
    public int primePalindrome2(int n) {
        if (n <= 2) {
            return 2;
        }
        for (int x = n; ; ++x) {
            // 偶数位回文数跳过(除 11)
            if (x > 11 && String.valueOf(x).length() % 2 == 0) {
                // 跳到下一个奇数位长度起点:10^(digits)
                int digits = String.valueOf(x).length();
                x = (int) Math.pow(10, digits) - 1;   // 循环 ++ 后为 10^digits
                continue;
            }
            if (isPalindrome(x) && isPrime(x)) {
                return x;
            }
        }
    }

    private boolean isPalindrome(int x) {
        String s = Integer.toString(x);
        int l = 0, r = s.length() - 1;
        while (l < r) {
            if (s.charAt(l) != s.charAt(r)) {
                return false;
            }
            ++l;
            --r;
        }
        return true;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[] ns = {6, 8, 13, 1, 2, 3, 11, 100, 9989900};
        int[] exp = {7, 11, 101, 2, 2, 3, 11, 101, 100030001};
        for (int t = 0; t < ns.length; ++t) {
            ++total;
            int g1 = s.primePalindrome(ns[t]);
            int g2 = s.primePalindrome2(ns[t]);
            if (g1 == exp[t] && g2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 866-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0866 PASS ==");
        } else {
            System.out.println("== 0866 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(10^(L/2) * sqrt(10^L)),L 为答案位数 O(1)
解法二 O(答案与 n 的差 * sqrt(答案)) O(1)

867. Transpose Matrix(转置矩阵)

题目描述

给你一个二维整数数组 matrix,返回 matrix 的转置矩阵。

矩阵的转置是指将矩阵的主对角线翻转,交换矩阵的行索引与列索引。

示例 1

  • 输入 :matrix = \[1,2,3,4,5,6,7,8,9]
  • 输出:\[1,4,7,2,5,8,3,6,9]

示例 2

  • 输入 :matrix = \[1,2,3,4,5,6]
  • 输出 :\[1,4,2,5,3,6]
    提示:
  • m == matrix.length
  • n == matrix[i].length
  • 1 <= m, n <= 1000
  • 1 <= m * n <= 10^5
  • -10^9 <= matrix[i][j] <= 10^9

解题思路

转置就是把 matrix[i][j] 放到 res[j][i]。原矩阵是 m×n,转置后是 n×m。

解法一(直接构造,主提交):申请 n x m 的新数组,双重循环赋值。时间 O(mn),空间 O(mn)。

解法二(逐列填充,对照实现):逐列处理原矩阵,把第 j 列的所有元素拷贝为新矩阵第 j

行。逻辑等价,便于理解列优先的视角。

代码实现

java 复制代码
class Solution {

    // 解法一:直接构造(推荐提交)
    public int[][] transpose(int[][] matrix) {
        int m = matrix.length, n = matrix[0].length;
        int[][] res = new int[n][m];
        for (int i = 0; i < m; ++i) {
            for (int j = 0; j < n; ++j) {
                res[j][i] = matrix[i][j];
            }
        }
        return res;
    }

    // 解法二:逐列填充(对照实现)
    public int[][] transpose2(int[][] matrix) {
        int m = matrix.length, n = matrix[0].length;
        int[][] res = new int[n][m];
        for (int j = 0; j < n; ++j) {
            for (int i = 0; i < m; ++i) {
                res[j][i] = matrix[i][j];
            }
        }
        return res;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[][][] mats = {
            {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}},
            {{1, 2, 3}, {4, 5, 6}},
            {{1}},
            {{1, 2}, {3, 4}, {5, 6}}
        };
        int[][][] exps = {
            {{1, 4, 7}, {2, 5, 8}, {3, 6, 9}},
            {{1, 4}, {2, 5}, {3, 6}},
            {{1}},
            {{1, 3, 5}, {2, 4, 6}}
        };
        for (int t = 0; t < mats.length; ++t) {
            ++total;
            if (java.util.Arrays.deepEquals(s.transpose(mats[t]), exps[t])
                    && java.util.Arrays.deepEquals(s.transpose2(mats[t]), exps[t])) {
                ++pass;
            } else {
                System.out.println("FAIL 867-" + (t + 1));
            }
        }
        if (pass == total) {
            System.out.println("== 0867 PASS ==");
        } else {
            System.out.println("== 0867 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(m * n) O(m * n)
解法二 O(m * n) O(m * n)

868. Binary Gap(二进制间距)

题目描述

给定一个正整数 n,找到并返回 n 的二进制表示中两个相邻 1 之间的最长距离。如果不存在两个

相邻的 1,则返回 0。

如果只有 0 将两个 1 分隔开(可能不存在 0),则认为这两个 1 彼此相邻。两个 1 之间的距离是

它们二进制表示中位置差值的绝对值。例如,"1001" 中的两个 1 之间的距离为 3。

示例 1

  • 输入 :n = 22
  • 输出:2
  • 解释 :22 的二进制是 "10110"。
    在 22 的二进制表示中,有三个 1,组成两对相邻的 1。
    第一对相邻的 1 中,两个 1 之间的距离为 2。
    第二对相邻的 1 中,两个 1 之间的距离为 1。
    答案取两个距离之中最大的,也就是 2。

示例 2

  • 输入 :n = 8
  • 输出:0
  • 解释:8 的二进制是 "1000"。在 8 的二进制表示中没有相邻的两个 1,所以返回 0。

示例 3

  • 输入 :n = 5
  • 输出:2
  • 解释 :5 的二进制是 "101",两个相邻的 1 之间的距离为 2。
    提示:
  • 1 <= n <= 10^9

解题思路

相邻两个 1 的间距是它们在二进制串中的位置差。逐位扫描二进制位,记录上一个 1 的位置。

解法一(位扫描,主提交):用 Integer 从最低位到最高位依次检查:若当前位为 1,且之前

出现过 1,则用当前位置与上次位置的差更新答案;再用 n >>= 1 或直接用 position 递增

模拟位权。

解法二(toString 法,对照实现):Integer.toBinaryString(n) 得到二进制串,扫描找相邻 1

的位置差最大值;思路直观,借助字符串 API。

代码实现

java 复制代码
class Solution {

    // 解法一:位扫描(推荐提交)
    public int binaryGap(int n) {
        int prev = -1;
        int ans = 0;
        for (int i = 0; i < 31; ++i) {
            if (((n >> i) & 1) == 1) {
                if (prev != -1) {
                    ans = Math.max(ans, i - prev);
                }
                prev = i;
            }
        }
        return ans;
    }

    // 解法二:二进制字符串法(对照实现)
    public int binaryGap2(int n) {
        String s = Integer.toBinaryString(n);
        int prev = -1;
        int ans = 0;
        for (int i = 0; i < s.length(); ++i) {
            if (s.charAt(i) == '1') {
                if (prev != -1) {
                    ans = Math.max(ans, i - prev);
                }
                prev = i;
            }
        }
        return ans;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[] ns = {22, 8, 5, 1, 6, 0};
        int[] exp = {2, 0, 2, 0, 1, 0};
        for (int t = 0; t < ns.length; ++t) {
            ++total;
            if (s.binaryGap(ns[t]) == exp[t] && s.binaryGap2(ns[t]) == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 868-" + (t + 1) + ": got " + s.binaryGap(ns[t]) + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0868 PASS ==");
        } else {
            System.out.println("== 0868 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(log n) O(1)
解法二 O(log n) O(log n)(字符串)

869. Reordered Power of 2(重新排序得到 2 的幂)

题目描述

给定正整数 n,我们按任何顺序(包括原始顺序)将数字重新排序,注意其前导数字不能为零。

如果我们可以通过上述方式得到 2 的幂,返回 true;否则返回 false。

示例 1

  • 输入 :n = 1
  • 输出 :true

示例 2

  • 输入 :n = 10
  • 输出 :false

示例 3

  • 输入 :n = 46
  • 输出 :true
    提示:
  • 1 <= n <= 10^9

解题思路

数字重排只改变数位顺序,不改变各数字的出现次数。因此只需比较 n 的数位频次是否与

某个 2 的幂完全一致。由于 n <= 10^9,只需枚举不超过 30 个 2 的幂。

解法一(频次数组比较,主提交):把 n 转成 10 位长频次数组(每位数字计数),枚举

1 << k(k = 0...30)检查其数位频次是否与 n 相同,相同返回 true,否则 false。

解法二(排序字符串比较,对照实现):把 n 的十进制字符串排序与所有 2 的幂的十进制

字符串排序比对,借助排序避免手写频次数组,思路等价。

代码实现

java 复制代码
import java.util.HashSet;
import java.util.Set;

class Solution {

    // 解法一:频次数组比较(推荐提交)
    public boolean reorderedPowerOf2(int n) {
        int[] cnt = count(n);
        for (int k = 0; k < 31; ++k) {
            if (same(cnt, count(1 << k))) {
                return true;
            }
        }
        return false;
    }

    private int[] count(int x) {
        int[] c = new int[10];
        String s = Integer.toString(x);
        for (int i = 0; i < s.length(); ++i) {
            ++c[s.charAt(i) - '0'];
        }
        return c;
    }

    private boolean same(int[] a, int[] b) {
        for (int i = 0; i < 10; ++i) {
            if (a[i] != b[i]) {
                return false;
            }
        }
        return true;
    }

    // 解法二:排序字符串比较(对照实现)
    public boolean reorderedPowerOf2_2(int n) {
        String target = sortedStr(n);
        for (int k = 0; k < 31; ++k) {
            if (target.equals(sortedStr(1 << k))) {
                return true;
            }
        }
        return false;
    }

    private String sortedStr(int x) {
        char[] cs = Integer.toString(x).toCharArray();
        java.util.Arrays.sort(cs);
        return new String(cs);
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[] ns = {1, 10, 46, 24, 679, 111, 1000, 1024};
        boolean[] exp = {true, false, true, false, false, false, false, true};
        for (int t = 0; t < ns.length; ++t) {
            ++total;
            if (s.reorderedPowerOf2(ns[t]) == exp[t]
                    && s.reorderedPowerOf2_2(ns[t]) == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 869-" + (t + 1) + ": got " + s.reorderedPowerOf2(ns[t]) + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0869 PASS ==");
        } else {
            System.out.println("== 0869 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(31 * 10)(数位最多 10 位) O(1)
解法二 O(31 * d log d),d 为数位 O(d)

870. Advantage Shuffle(优势洗牌)

题目描述

给定两个长度相等的数组 nums1 和 nums2,nums1 相对于 nums2 的优势可以用满足

nums1[i] > nums2[i] 的索引 i 的数目来描述。

返回 nums1 的任意排列,使其相对于 nums2 的优势最大化。

示例 1

  • 输入:nums1 = 2,7,11,15, nums2 = 1,10,4,11
  • 输出:2,11,7,15

示例 2

  • 输入:nums1 = 12,24,8,32, nums2 = 13,25,32,11
  • 输出 :24,32,8,12
    提示:
  • 1 <= nums1.length <= 10^5
  • 0 <= nums1[i] <= 10^9
  • 0 <= nums2[i] <= 10^9
  • nums1.length == nums2.length

解题思路

与"田忌赛马"同理:用最小代价去消耗对方最强的牌,用强牌去对抗对方比自己弱的牌。

解法一(排序 + 双指针,主提交):把 nums1 升序排序,nums2 的下标按值升序排序。

用双指针处理已排序的 nums1:对于 nums2 中最小的尚未匹配的元素,若 nums1[l] 能赢它

(nums1[l] > nums2[i])就配对并 l++;否则把 nums1[l] 这张"废牌"配给 nums2 中

当前最大的元素(消耗对方强牌),l++、r--。这样最大化胜场数。

解法二(TreeMap 贪心,对照实现):把 nums1 放进 TreeMap 计数;对每个 nums2[i],

用 higherKey(nums2[i]) 找比它大的最小牌,有则用,没有则用当前最小牌(放弃本局)。

思路直观,O(n log n)。

代码实现

java 复制代码
import java.util.Arrays;

class Solution {

    // 解法一:排序 + 双指针(推荐提交)
    public int[] advantageCount(int[] nums1, int[] nums2) {
        int n = nums1.length;
        Arrays.sort(nums1);
        Integer[] idx = new Integer[n];
        for (int i = 0; i < n; ++i) {
            idx[i] = i;
        }
        // nums2 的索引按值升序
        Arrays.sort(idx, (a, b) -> Integer.compare(nums2[a], nums2[b]));
        int[] ans = new int[n];
        int left = 0, right = n - 1;
        for (int i = n - 1; i >= 0; --i) {
            int j = idx[i];    // 当前 nums2 中最大元素的下标
            if (nums1[right] > nums2[j]) {
                ans[j] = nums1[right--];   // 用最大的牌赢过对方最大值
            } else {
                ans[j] = nums1[left++];    // 赢不了则用最小的牌消耗对方最大值
            }
        }
        return ans;
    }

    // 解法二:TreeMap 贪心(对照实现)
    public int[] advantageCount2(int[] nums1, int[] nums2) {
        int n = nums1.length;
        java.util.TreeMap<Integer, Integer> map = new java.util.TreeMap<>();
        for (int x : nums1) {
            map.put(x, map.getOrDefault(x, 0) + 1);
        }
        int[] ans = new int[n];
        for (int i = 0; i < n; ++i) {
            Integer better = map.higherKey(nums2[i]);
            if (better == null) {
                better = map.firstKey();      // 放弃本局,用最小的牌
            }
            ans[i] = better;
            if (map.get(better) == 1) {
                map.remove(better);
            } else {
                map.put(better, map.get(better) - 1);
            }
        }
        return ans;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        // 期待值不定,改为验证得分(优势数)是否达到最优
        int[][] n1s = {
            {2, 7, 11, 15}, {12, 24, 8, 32}, {1, 1, 1, 2}, {2, 4, 6}, {1, 2, 3}
        };
        int[][] n2s = {
            {1, 10, 4, 11}, {13, 25, 32, 11}, {1, 1, 1, 1}, {3, 1, 2}, {1, 2, 3}
        };
        int[] best = {4, 3, 1, 3, 2};
        for (int t = 0; t < n1s.length; ++t) {
            ++total;
            int score1 = score(s.advantageCount(n1s[t], n2s[t]), n2s[t]);
            int score2 = score(s.advantageCount2(n1s[t], n2s[t]), n2s[t]);
            if (score1 == best[t] && score2 == best[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 870-" + (t + 1) + ": score " + score1 + "/" + score2 + " best " + best[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0870 PASS ==");
        } else {
            System.out.println("== 0870 FAIL ==");
            System.exit(1);
        }
    }

    private static int score(int[] res, int[] nums2) {
        int cnt = 0;
        for (int i = 0; i < res.length; ++i) {
            if (res[i] > nums2[i]) {
                ++cnt;
            }
        }
        return cnt;
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(n log n) O(n)
解法二 O(n log n) O(n)

871. Minimum Number of Refueling Stops(最低加油次数)

题目描述

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

沿途有加油站,用数组 stations 表示。其中 stations[i] = [position_i, fuel_i] 表示第 i

个加油站位于出发位置东面 position_i 英里处,并且有 fuel_i 升汽油。

假设汽车油箱的容量是无限的,其中最初有 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 和 60 两个加油站各加一次油。
    提示:
  • 1 <= target, startFuel <= 10^9
  • 0 <= stations.length <= 500
  • 1 <= position_i < position_{i+1} < target
  • 1 <= fuel_i < 10^9

解题思路

贪心 + "后悔"思想:把已经经过的加油站的油量放入大根堆,当油不够开到下一站时,从堆中取

出最大油量"补加",尽可能用最少的次数获得最多的油。

解法一(贪心 + 大根堆,主提交):把终点也当作一个 position = target, fuel = 0 的站点

参与扫描。维护当前位置 pre、当前油量 fuel、大根堆 pq(存已经过的站点的油量)。

依次推进:每到一个站点 pos,fuel -= pos - pre;若 fuel < 0,从 pq 中反复弹出

最大油量加入 fuel 并计数,直到 fuel >= 0;若 pq 空仍不够则返回 -1;把该站油量入堆,

更新 pre。遍历结束返回加油次数。

解法二(动态规划,对照实现):dp[i] 表示加油 i 次能到达的最远距离,初始 dp[0] = startFuel。

遍历每个加油站,从大到小更新 dp[j + 1] = max(dp[j+1], dp[j] + fuel_i)(前提 dp[j] >= position_i),

第一个 dp[i] >= target 的 i 就是答案。O(n^2)。

代码实现

java 复制代码
import java.util.PriorityQueue;

class Solution {

    // 解法一:贪心 + 大根堆(推荐提交)
    public int minRefuelStops(int target, int startFuel, int[][] stations) {
        PriorityQueue<Integer> pq = new PriorityQueue<>((a, b) -> Integer.compare(b, a));
        int ans = 0;
        int pre = 0;
        int fuel = startFuel;
        int n = stations.length;
        for (int i = 0; i <= n; ++i) {
            int pos = i < n ? stations[i][0] : target;
            int dist = pos - pre;
            fuel -= dist;
            while (fuel < 0 && !pq.isEmpty()) {
                fuel += pq.poll();
                ++ans;
            }
            if (fuel < 0) {
                return -1;
            }
            if (i < n) {
                pq.offer(stations[i][1]);
                pre = pos;
            }
        }
        return ans;
    }

    // 解法二:动态规划(对照实现)
    public int minRefuelStops2(int target, int startFuel, int[][] stations) {
        int n = stations.length;
        long[] dp = new long[n + 1];   // dp[i] = 加油 i 次能到的最远距离
        dp[0] = startFuel;
        for (int i = 0; i < n; ++i) {
            for (int j = i; j >= 0; --j) {
                if (dp[j] >= stations[i][0]) {
                    dp[j + 1] = Math.max(dp[j + 1], dp[j] + stations[i][1]);
                }
            }
        }
        for (int i = 0; i <= n; ++i) {
            if (dp[i] >= target) {
                return i;
            }
        }
        return -1;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        ++total;
        if (s.minRefuelStops(1, 1, new int[][]{}) == 0
                && s.minRefuelStops2(1, 1, new int[][]{}) == 0) {
            ++pass;
        } else {
            System.out.println("FAIL 871-1");
        }
        ++total;
        if (s.minRefuelStops(100, 1, new int[][]{{10, 100}}) == -1
                && s.minRefuelStops2(100, 1, new int[][]{{10, 100}}) == -1) {
            ++pass;
        } else {
            System.out.println("FAIL 871-2");
        }
        ++total;
        if (s.minRefuelStops(100, 10, new int[][]{{10, 60}, {20, 30}, {30, 30}, {60, 40}}) == 2
                && s.minRefuelStops2(100, 10, new int[][]{{10, 60}, {20, 30}, {30, 30}, {60, 40}}) == 2) {
            ++pass;
        } else {
            System.out.println("FAIL 871-3: " + s.minRefuelStops(100, 10, new int[][]{{10, 60}, {20, 30}, {30, 30}, {60, 40}}));
        }
        ++total;
        if (s.minRefuelStops(1000, 1, new int[][]{}) == -1
                && s.minRefuelStops2(1000, 1, new int[][]{}) == -1) {
            ++pass;
        } else {
            System.out.println("FAIL 871-4");
        }
        ++total;
        if (s.minRefuelStops(100, 50, new int[][]{{10, 60}, {20, 100}, {30, 30}}) == 1
                && s.minRefuelStops2(100, 50, new int[][]{{10, 60}, {20, 100}, {30, 30}}) == 1) {
            ++pass;
        } else {
            System.out.println("FAIL 871-5: " + s.minRefuelStops(100, 50, new int[][]{{10, 60}, {20, 100}, {30, 30}}));
        }
        if (pass == total) {
            System.out.println("== 0871 PASS ==");
        } else {
            System.out.println("== 0871 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(n log n) O(n)
解法二 O(n^2) O(n)

872. Leaf-Similar Trees(叶子相似的树)

题目描述

请考虑一棵二叉树上所有的叶子,这些叶子的值按从左到右的顺序排列形成一个叶值序列。

如果有两棵二叉树的叶值序列是相同,那么我们就认为它们是叶相似的。

如果给定的两个根结点分别为 root1 和 root2 的树是叶相似的,则返回 true;否则返回

false。

示例 1

  • 输入 :root1 = 3,5,1,6,2,9,8,`null`,`null`,7,4, root2 = 3,5,1,6,7,4,2,`null`,`null`,`null`,`null`,`null`,`null`,9,8
  • 输出 :true

示例 2

  • 输入:root1 = 1,2,3, root2 = 1,3,2
  • 输出 :false
    提示:
  • 给定的两棵树结点数在 [1, 200] 范围内
  • 给定的两棵树上的值在 [0, 200] 范围内

解题思路

叶值序列是按"从左到右"的先序(中序亦可,叶子顺序天然一致)收集叶子节点得到的结果。

解法一(DFS 收集两棵树的叶子,主提交):递归遍历,左右孩子都为空即为叶子,按先序顺序

(先左后右)收集;得到两个序列直接相等判断。

解法二(迭代栈 DFS,对照实现):用显式栈模拟先序遍历收集叶子,避免递归栈深度占用;

两棵树的叶子序列分别收集后比较。实现不依赖系统栈,思路与解法一相同。

代码实现

java 复制代码
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;

class Solution {

    // 解法一:DFS 递归收集(推荐提交)
    public boolean leafSimilar(TreeNode root1, TreeNode root2) {
        List<Integer> l1 = new ArrayList<>();
        List<Integer> l2 = new ArrayList<>();
        dfs(root1, l1);
        dfs(root2, l2);
        return l1.equals(l2);
    }

    private void dfs(TreeNode node, List<Integer> leaves) {
        if (node == null) {
            return;
        }
        if (node.left == null && node.right == null) {
            leaves.add(node.val);
            return;
        }
        dfs(node.left, leaves);
        dfs(node.right, leaves);
    }

    // 解法二:迭代栈(对照实现)
    public boolean leafSimilar2(TreeNode root1, TreeNode root2) {
        return collect(root1).equals(collect(root2));
    }

    private List<Integer> collect(TreeNode root) {
        List<Integer> leaves = new ArrayList<>();
        if (root == null) {
            return leaves;
        }
        ArrayDeque<TreeNode> stack = new ArrayDeque<>();
        stack.push(root);
        while (!stack.isEmpty()) {
            TreeNode cur = stack.pop();
            if (cur.left == null && cur.right == null) {
                leaves.add(cur.val);
                continue;
            }
            // 先右后左,保证弹出时先处理左子树
            if (cur.right != null) {
                stack.push(cur.right);
            }
            if (cur.left != null) {
                stack.push(cur.left);
            }
        }
        return leaves;
    }

    // 二叉树节点定义
    static class TreeNode {
        int val;
        TreeNode left;
        TreeNode right;
        TreeNode(int x) {
            val = x;
        }
    }

    // 本地测试辅助:层序建树
    private static TreeNode build(Integer[] vals) {
        if (vals == null || vals.length == 0) {
            return null;
        }
        TreeNode root = new TreeNode(vals[0]);
        Queue<TreeNode> q = new ArrayDeque<>();
        q.offer(root);
        int i = 1;
        while (!q.isEmpty() && i < vals.length) {
            TreeNode cur = q.poll();
            if (vals[i] != null) {
                cur.left = new TreeNode(vals[i]);
                q.offer(cur.left);
            }
            ++i;
            if (i < vals.length && vals[i] != null) {
                cur.right = new TreeNode(vals[i]);
                q.offer(cur.right);
            }
            ++i;
        }
        return root;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        ++total;
        {
            TreeNode r1 = build(new Integer[]{3, 5, 1, 6, 2, 9, 8, null, null, 7, 4});
            TreeNode r2 = build(new Integer[]{3, 5, 1, 6, 7, 4, 2, null, null, null, null, null, null, 9, 8});
            if (s.leafSimilar(r1, r2) && s.leafSimilar2(r1, r2)) {
                ++pass;
            } else {
                System.out.println("FAIL 872-1");
            }
        }
        ++total;
        {
            TreeNode r1 = build(new Integer[]{1, 2, 3});
            TreeNode r2 = build(new Integer[]{1, 3, 2});
            if (!s.leafSimilar(r1, r2) && !s.leafSimilar2(r1, r2)) {
                ++pass;
            } else {
                System.out.println("FAIL 872-2");
            }
        }
        ++total;
        {
            TreeNode r1 = build(new Integer[]{1});
            TreeNode r2 = build(new Integer[]{1, 2, 3});
            if (s.leafSimilar(r1, r2) == false && s.leafSimilar2(r1, r2) == false) {
                ++pass;
            } else {
                System.out.println("FAIL 872-3");
            }
        }
        ++total;
        {
            TreeNode r1 = build(new Integer[]{1, null, 2});
            TreeNode r2 = build(new Integer[]{1, 2});
            if (s.leafSimilar(r1, r2) && s.leafSimilar2(r1, r2)) {
                ++pass;
            } else {
                System.out.println("FAIL 872-4");
            }
        }
        if (pass == total) {
            System.out.println("== 0872 PASS ==");
        } else {
            System.out.println("== 0872 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(n1 + n2) O(h1 + h2)(递归深度)
解法二 O(n1 + n2) O(h1 + h2)(显式栈)

873. Length of Longest Fibonacci Subsequence(最长的斐波那契子序列的长度)

题目描述

如果序列 x_1, x_2, ..., x_n 满足下列条件,就说它是斐波那契式的:

  • n >= 3
  • 对于所有 i + 2 <= n,都有 x_i + x_{i+1} == x_{i+2}
    给定一个严格递增的正整数数组形成序列 arr,找到 arr 中最长的斐波那契式的子序列的长度。
    如果不存在,返回 0。
    子序列是通过从另一个序列 arr 中删除任意数量的元素(包括删除 0 个元素)得到的,同时不改变
    剩余元素顺序。例如,[3, 5, 8] 是 [3, 4, 5, 6, 7, 8] 的子序列。

示例 1

  • 输入 :arr = 1,2,3,4,5,6,7,8
  • 输出: 5
  • 解释: 最长的斐波那契式子序列为 1,2,3,5,8。

示例 2

  • 输入 :arr = 1,3,7,11,12,14,18
  • 输出: 3
  • 解释 : 最长的斐波那契式子序列有 1,11,12、3,11,14 以及 7,11,18。
    提示:
  • 3 <= arr.length <= 1000
  • 1 <= arr[i] < arr[i + 1] <= 10^9

解题思路

斐波那契式子序列每新增一项由前两项决定,可以把状态定义为「以 (arri, arrj) 结尾的

子序列长度」,其中 i < j 且 arr[j] 是最后一项。

解法一(动态规划 + 值到下标映射,主提交):用 dp[i][j] 表示以 arr[i] 和 arr[j]

为最后两项的最长斐波那契式子序列长度(dp[i][j] >= 2 表示至少两项)。枚举最右侧的

j 与中间的 i,若 k = arr[j] - arr[i] 存在于 arr 且 k < arr[i](值为 k 的下标

p 满足 p < i,即 k 在原序列中更靠左),则 dp[i][j] = dp[p][i] + 1,否则 = 2;

全局取最大,若最大 < 3 则返回 0。实现时用一维滚动以省内存(相邻两项 -> 长度)。

解法二(哈希表逐步扩展,对照实现):枚举每个可能的起始对 (i, j),用哈希表查找

arr[i] + arr[j] 是否存在,存在则继续向后扩展,统计长度。以每个起始对为起点,

O(n^2) 起始对,每对的扩展步数 O(n),最坏 O(n^3) 但实际很快;作为对照思路。

代码实现

java 复制代码
import java.util.HashMap;
import java.util.Map;

class Solution {

    // 解法一:动态规划(推荐提交)
    public int lenLongestFibSubseq(int[] arr) {
        int n = arr.length;
        Map<Integer, Integer> pos = new HashMap<>();
        for (int i = 0; i < n; ++i) {
            pos.put(arr[i], i);
        }
        int[][] dp = new int[n][n];   // dp[i][j]: 以 arr[i], arr[j] 结尾(i<j)的最长长度
        int ans = 0;
        for (int j = 0; j < n; ++j) {
            for (int i = 0; i < j; ++i) {
                int need = arr[j] - arr[i];
                Integer p = pos.get(need);
                if (p != null && p < i) {
                    dp[i][j] = dp[p][i] + 1;
                } else {
                    dp[i][j] = 2;
                }
                ans = Math.max(ans, dp[i][j]);
            }
        }
        return ans < 3 ? 0 : ans;
    }

    // 解法二:哈希表逐步扩展(对照实现)
    public int lenLongestFibSubseq2(int[] arr) {
        int n = arr.length;
        Map<Integer, Integer> pos = new HashMap<>();
        for (int i = 0; i < n; ++i) {
            pos.put(arr[i], i);
        }
        int ans = 0;
        for (int i = 0; i < n; ++i) {
            for (int j = i + 1; j < n; ++j) {
                int a = arr[i], b = arr[j];
                int len = 2;
                while (true) {
                    Integer k = pos.get(a + b);
                    if (k == null) {
                        break;
                    }
                    int tmp = b;
                    b = a + b;
                    a = tmp;
                    ++len;
                }
                if (len >= 3) {
                    ans = Math.max(ans, len);
                }
            }
        }
        return ans;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[][] arrs = {
            {1, 2, 3, 4, 5, 6, 7, 8},
            {1, 3, 7, 11, 12, 14, 18},
            {1, 2, 3},
            {1, 2},
            {2, 4, 7, 8, 11, 12, 14, 15, 18, 19}
        };
        int[] exp = {5, 3, 3, 0, 4};
        for (int t = 0; t < arrs.length; ++t) {
            ++total;
            int g1 = s.lenLongestFibSubseq(arrs[t]);
            int g2 = s.lenLongestFibSubseq2(arrs[t]);
            if (g1 == exp[t] && g2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 873-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0873 PASS ==");
        } else {
            System.out.println("== 0873 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(n^2) O(n^2)(或 O(n) 滚动)
解法二 O(n^3) 最坏 O(n)

874. Walking Robot Simulation(模拟行走机器人)

题目描述

机器人在一个无限大小的 XY 网格平面上行走,从点 (0, 0) 处开始出发,面向北方。该机器人

可以接收以下三种类型的命令 commands:

  • -2:向左转 90 度;
  • -1:向右转 90 度;
  • 1 <= x <= 9:向前移动 x 个单位长度。
    在网格上有一些格子被视为障碍物 obstacles。第 i 个障碍物位于网格点
    obstacles[i] = (x_i, y_i)。
    机器人无法走到障碍物上,它将会停在障碍物的前一个网格方块上,但仍然可以继续尝试进行该路线
    的其余部分。
    返回从原点到机器人所有经过的路径点(坐标为整数)的最大欧氏距离的平方。

示例 1

  • 输入:commands = 4,-1,3, obstacles = \[\]
  • 输出:25
  • 解释:机器人开始位于 (0, 0):
  1. 向北移动 4 个单位,到达 (0, 4)。
  2. 右转。
  3. 向东移动 3 个单位,到达 (3, 4)。
    距离原点最远的是 (3, 4) ,距离为 3^2 + 4^2 = 25。

示例 2

  • 输入:commands = 4,-1,4,-2,4, obstacles = \[2,4]
  • 输出:65
  • 解释:机器人开始位于 (0, 0):
  1. 向北移动 4 个单位,到达 (0, 4)。
  2. 右转。
  3. 向东移动 1 个单位,然后被位于 (2, 4) 的障碍物阻挡,停在 (1, 4)。
  4. 左转。
  5. 向北走 4 个单位,到达 (1, 8)。
    距离原点最远的是 (1, 8) ,距离为 1^2 + 8^2 = 65。
    提示:
  • 1 <= commands.length <= 10^4
  • commands[i] 是 -2、-1 或者是范围 [1, 9] 内的一个整数
  • 0 <= obstacles.length <= 10^4
  • -3 * 10^4 <= x_i, y_i <= 3 * 10^4
  • 答案保证小于 2^31

解题思路

机器人朝四个方向移动,方向用 0=北、1=东、2=南、3=西 表示;-1 右转相当于

dir = (dir + 1) % 4,-2 左转相当于 dir = (dir + 3) % 4。移动时逐格检查前方是否

有障碍物(障碍物集合用哈希存储坐标编码),有则停在该格前。

解法一(方向数组 + HashSet 障碍判定,主提交):四个方向向量

{``{0,1},{1,0},{0,-1},{-1,0}},移动时每步试探下一格,若在障碍集合中则停止本次前进,

否则真正移动并更新最远距离平方。

解法二(坐标编码 String,对照实现):把障碍物编码为 x + "," + y 的字符串放入 Set,

方向切换拆成独立 if 分支;逻辑等价,但字符串比较比 int 编码略慢,仅作对照。

代码实现

java 复制代码
import java.util.HashSet;
import java.util.Set;

class Solution {

    private static final int[][] DIRS = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};

    // 解法一:方向数组 + int 编码障碍(推荐提交)
    public int robotSim(int[] commands, int[][] obstacles) {
        Set<Integer> block = new HashSet<>();
        for (int[] o : obstacles) {
            block.add(encode(o[0], o[1]));
        }
        int x = 0, y = 0, dir = 0;   // 0=北 1=东 2=南 3=西
        int ans = 0;
        for (int c : commands) {
            if (c == -1) {
                dir = (dir + 1) % 4;
            } else if (c == -2) {
                dir = (dir + 3) % 4;
            } else {
                int dx = DIRS[dir][0], dy = DIRS[dir][1];
                for (int step = 0; step < c; ++step) {
                    int nx = x + dx, ny = y + dy;
                    if (block.contains(encode(nx, ny))) {
                        break;
                    }
                    x = nx;
                    y = ny;
                }
                ans = Math.max(ans, x * x + y * y);
            }
        }
        return ans;
    }

    private int encode(int x, int y) {
        // 坐标范围 +-30000,偏移后编码到 64 位内唯一
        return (x + 30000) * 100000 + (y + 30000);
    }

    // 解法二:字符串编码障碍(对照实现)
    public int robotSim2(int[] commands, int[][] obstacles) {
        Set<String> block = new HashSet<>();
        for (int[] o : obstacles) {
            block.add(o[0] + "," + o[1]);
        }
        int x = 0, y = 0, dir = 0;
        int ans = 0;
        for (int c : commands) {
            if (c == -2) {
                dir = dir == 0 ? 3 : dir - 1;
            } else if (c == -1) {
                dir = (dir + 1) % 4;
            } else {
                int dx = DIRS[dir][0], dy = DIRS[dir][1];
                for (int step = 0; step < c; ++step) {
                    int nx = x + dx, ny = y + dy;
                    if (block.contains(nx + "," + ny)) {
                        break;
                    }
                    x = nx;
                    y = ny;
                }
                ans = Math.max(ans, x * x + y * y);
            }
        }
        return ans;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        ++total;
        if (s.robotSim(new int[]{4, -1, 3}, new int[][]{}) == 25) {
            ++pass;
        } else {
            System.out.println("FAIL 874-1: " + s.robotSim(new int[]{4, -1, 3}, new int[][]{}));
        }
        ++total;
        if (s.robotSim(new int[]{4, -1, 4, -2, 4}, new int[][]{{2, 4}}) == 65) {
            ++pass;
        } else {
            System.out.println("FAIL 874-2: " + s.robotSim(new int[]{4, -1, 4, -2, 4}, new int[][]{{2, 4}}));
        }
        ++total;
        if (s.robotSim(new int[]{6, -2, 8}, new int[][]{}) == 100) {
            ++pass;
        } else {
            System.out.println("FAIL 874-3: " + s.robotSim(new int[]{6, -2, 8}, new int[][]{}));
        }
        ++total;
        if (s.robotSim2(new int[]{4, -1, 3}, new int[][]{}) == 25
                && s.robotSim2(new int[]{4, -1, 4, -2, 4}, new int[][]{{2, 4}}) == 65) {
            ++pass;
        } else {
            System.out.println("FAIL 874-4");
        }
        if (pass == total) {
            System.out.println("== 0874 PASS ==");
        } else {
            System.out.println("== 0874 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(n * m),m 为单次最大步数(9) + O(k) 建集 O(k)
解法二 O(n * m) + O(k) O(k)

875. Koko Eating Bananas(爱吃香蕉的珂珂)

题目描述

珂珂喜欢吃香蕉。这里有 n 堆香蕉,第 i 堆中有 piles[i] 根香蕉。警卫已经离开了,

将在 h 小时后回来。

珂珂可以决定她吃香蕉的速度 k(单位:根/小时)。每个小时,她将会选择一堆香蕉,从中吃掉

k 根。如果这堆香蕉少于 k 根,她将吃掉这堆的所有香蕉,然后这一小时内不会再吃更多的

香蕉。

珂珂喜欢慢慢吃,但仍然想在警卫回来前吃掉所有的香蕉。

返回她可以在 h 小时内吃掉所有香蕉的最小速度 k(k 为整数)。

示例 1

  • 输入 :piles = 3,6,7,11, h = 8
  • 输出:4

示例 2

  • 输入 :piles = 30,11,23,4,20, h = 5
  • 输出:30

示例 3

  • 输入 :piles = 30,11,23,4,20, h = 6
  • 输出 :23
    提示:
  • 1 <= piles.length <= 10^4
  • piles.length <= h <= 10^9
  • 1 <= piles[i] <= 10^9

解题思路

吃完全部香蕉所需小时数随速度 k 单调递减,因此可对速度二分查找第一个使总耗时

<= h 的 k。每堆耗时 ceil(piles[i] / k)。

解法一(二分答案,主提交):二分区间 [1, max(piles)]。ok(k) 判断以速度 k 是否能在

h 小时内吃完:对每堆累加 (p + k - 1) / k,注意用 long 防溢出;总耗时 <= h 即合法。

二分取合法区间左边界,即为最小速度。

解法二(暴力从 1 递增,对照实现):从 k = 1 开始逐个测试(每堆沿用上面的耗时公式),

找到第一个合法速度。最坏情况接近 max(piles) 时很慢,仅作正确性对照。

代码实现

java 复制代码
import java.util.Arrays;

class Solution {

    // 解法一:二分答案(推荐提交)
    public int minEatingSpeed(int[] piles, int h) {
        int lo = 1, hi = 0;
        for (int p : piles) {
            hi = Math.max(hi, p);
        }
        while (lo < hi) {
            int mid = lo + (hi - lo) / 2;
            if (canFinish(piles, h, mid)) {
                hi = mid;
            } else {
                lo = mid + 1;
            }
        }
        return lo;
    }

    private boolean canFinish(int[] piles, int h, int k) {
        long hours = 0;
        for (int p : piles) {
            hours += (p + k - 1L) / k;
            if (hours > h) {
                return false;
            }
        }
        return true;
    }

    // 解法二:递增枚举(对照实现)
    public int minEatingSpeed2(int[] piles, int h) {
        int k = 1;
        while (true) {
            long hours = 0;
            for (int p : piles) {
                hours += (p + k - 1L) / k;
                if (hours > h) {
                    break;
                }
            }
            if (hours <= h) {
                return k;
            }
            ++k;
        }
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        ++total;
        if (s.minEatingSpeed(new int[]{3, 6, 7, 11}, 8) == 4) {
            ++pass;
        } else {
            System.out.println("FAIL 875-1: " + s.minEatingSpeed(new int[]{3, 6, 7, 11}, 8));
        }
        ++total;
        if (s.minEatingSpeed(new int[]{30, 11, 23, 4, 20}, 5) == 30) {
            ++pass;
        } else {
            System.out.println("FAIL 875-2: " + s.minEatingSpeed(new int[]{30, 11, 23, 4, 20}, 5));
        }
        ++total;
        if (s.minEatingSpeed(new int[]{30, 11, 23, 4, 20}, 6) == 23) {
            ++pass;
        } else {
            System.out.println("FAIL 875-3: " + s.minEatingSpeed(new int[]{30, 11, 23, 4, 20}, 6));
        }
        ++total;
        if (s.minEatingSpeed(new int[]{1, 1, 1}, 3) == 1) {
            ++pass;
        } else {
            System.out.println("FAIL 875-4");
        }
        ++total;
        if (s.minEatingSpeed2(new int[]{3, 6, 7, 11}, 8) == 4
                && s.minEatingSpeed2(new int[]{30, 11, 23, 4, 20}, 6) == 23) {
            ++pass;
        } else {
            System.out.println("FAIL 875-5");
        }
        if (pass == total) {
            System.out.println("== 0875 PASS ==");
        } else {
            System.out.println("== 0875 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(n log max(piles)) O(1)
解法二 O(max(piles) * n) O(1)
相关推荐
技术老孙1 小时前
给 Agent 装上对话框 —— AgentScope-Java 接入 Spring Boot 与 SSE 流式实战
java·开发语言·spring boot
Navigator_Z1 小时前
LeetCode //C - 1304. Find N Unique Integers Sum up to Zero
c语言·算法·leetcode
指剑2 小时前
亚麻云之EC2——安全组与密钥对:SSH 端口怎么开才安全,Permission denied (publickey) 和连接超时排查
java·安全·ssh
_Evan_Yao9 小时前
Redis 缓存穿透、击穿、雪崩,一次讲清!
java·spring·mybatis
梓豪csd10 小时前
消息订阅中心:谁该收到哪条通知
java·开发语言·人工智能·低代码·自动化·1024程序员节
秋饼10 小时前
Spring AI 2.1 Message Parts 深度实战:推理签名回放与跨轮记忆持久化的源码级落地
java·ai·技术分享·后端开发
thefool11226611 小时前
TCP 1
java·1024程序员节
金銀銅鐵13 小时前
[Java] 用 GUI 展示 EnumSet 增减元素时的内部变化(下)
java·后端·ai编程
?abc!13 小时前
(数组)581. 最短无序连续子数组
数据结构·算法·leetcode