LeetCode851~863题目解析

4 阅读9分钟

LeetCode 851-863. 题解合集


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 穷”的直接邻居的答案,邻居
入度归零后入队,逐步把最优信息传播到传播链末端。

代码实现

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] > arr[i + 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),实现直观但常数更差。

代码实现

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) 按位置升序排好,从终点
方向往回扫描,遇到「到达时间比右侧已记录的最大到达时间还大」的车就计为新车队。思路与
解法一相同,仅遍历顺序相反。

代码实现

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 求最少步数, 逻辑等价、代码更短,两种实现结果一致。

代码实现

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) 遍历全部已坐座位,仅适合小规模数据。

代码实现

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,线性时间、常数空间。

代码实现

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) 量级,仅适合小数据。

代码实现

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 次。

代码实现

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 时需互为“换位”。实现略繁琐但思路与 解法一一致,作为验证交叉。

代码实现

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 元;该写法把“优先花大额”显式表达为分支条件,逻辑等价。

代码实现

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)。 解法二(按位直接求解,对照实现):由于每列独立且可翻转,先把数组复制一份并真正执行 行翻转,再用计数求列最优;实现直接、便于对照,但多一次拷贝。

代码实现

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) 仅作思路 正确性参考,不适合大数据(本地仅用小用例验证)。

代码实现

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 的结点加入答案(注意不要重复经过路径上的下一个结点)。

代码实现

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)