LeetCode814~825题目解析

3 阅读8分钟

LeetCode 814-825. 题解合集


814. Binary Tree Pruning(二叉树剪枝)

题目描述

给你二叉树的根结点 root,此外树的每个结点的值要么是 0,要么是 1。
返回移除了所有不包含 1 的子树的原二叉树。
节点 node 的子树为 node 本身加上所有 node 的后代。

示例 1

  • 输入:root = [1,null,0,0,1]
  • 输出:[1,null,0,null,1]

示例 2

  • 输入:root = [1,0,1,0,0,0,1]
  • 输出:[1,null,1,null,1]

示例 3

  • 输入:root = [1,1,0,1,1,0,1,0]
  • 输出:[1,1,0,1,1,null,1]
    提示:
  • 树中节点的数目在范围 [1, 200] 内
  • Node.val 为 0 或 1

解题思路

需要删去所有不含 1 的子树。自顶向下无法在见到子树之前判断,应先递归处理左右孩子,再根据
子树是否已被清空决定当前结点是否保留。
解法一(后序递归,主提交):后序返回后,若结点值为 0 且左右皆空,则整棵子树可删。
解法二(非递归后序 + 标记的对照实现):显式栈后序遍历,处理后结点与孩子,便于理解递归
展开过程。

代码实现

class Solution {

    // 解法一:后序递归(推荐提交)
    public TreeNode pruneTree(TreeNode root) {
        if (root == null) {
            return null;
        }
        root.left = pruneTree(root.left);
        root.right = pruneTree(root.right);
        if (root.val == 0 && root.left == null && root.right == null) {
            return null;
        }
        return root;
    }

    // 解法二:非递归后序(对照实现)
    public TreeNode pruneTree2(TreeNode root) {
        java.util.Deque<TreeNode> st = new java.util.ArrayDeque<>();
        java.util.Set<TreeNode> done = new java.util.HashSet<>();
        TreeNode cur = root;
        TreeNode last = null;
        while (cur != null || !st.isEmpty()) {
            while (cur != null) {
                st.push(cur);
                cur = cur.left;
            }
            TreeNode top = st.peek();
            if (top.right != null && !done.contains(top.right)) {
                cur = top.right;
            } else {
                st.pop();
                // 剪枝:若该结点值为 0 且孩子都被删除(或原本为空)
                boolean keep = top.val == 1 || top.left != null || top.right != null;
                if (!keep) {
                    // 需要通知父结点:本子树已删除
                    if (!st.isEmpty()) {
                        TreeNode parent = st.peek();
                        if (parent.left == top) {
                            parent.left = null;
                        } else {
                            parent.right = null;
                        }
                    } else {
                        root = null;
                    }
                }
                done.add(top);
                last = top;
                cur = null;
            }
        }
        return root;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;

        // 示例 1: [1,null,0,0,1] -> [1,null,0,null,1]
        TreeNode t1 = TreeNode.fromArray(new Integer[]{1, null, 0, 0, 1});
        TreeNode r1 = sol.pruneTree(t1);
        ++total; if ("1,null,0,null,1".equals(TreeNode.toLeetCodeString(r1))) ++pass; else System.out.println("FAIL 814-1: " + TreeNode.toLeetCodeString(r1));

        // 示例 2: [1,0,1,0,0,0,1] -> [1,null,1,null,1]
        TreeNode t2 = TreeNode.fromArray(new Integer[]{1, 0, 1, 0, 0, 0, 1});
        TreeNode r2 = sol.pruneTree(t2);
        ++total; if ("1,null,1,null,1".equals(TreeNode.toLeetCodeString(r2))) ++pass; else System.out.println("FAIL 814-2: " + TreeNode.toLeetCodeString(r2));

        // 示例 3: [1,1,0,1,1,0,1,0] -> [1,1,0,1,1,null,1]
        TreeNode t3 = TreeNode.fromArray(new Integer[]{1, 1, 0, 1, 1, 0, 1, 0});
        TreeNode r3 = sol.pruneTree(t3);
        ++total; if ("1,1,0,1,1,null,1".equals(TreeNode.toLeetCodeString(r3))) ++pass; else System.out.println("FAIL 814-3: " + TreeNode.toLeetCodeString(r3));

        // 全 0 -> null
        TreeNode t4 = TreeNode.fromArray(new Integer[]{0, 0, 0});
        TreeNode r4 = sol.pruneTree(t4);
        ++total; if (r4 == null) ++pass; else System.out.println("FAIL 814-4");

        // 解法二
        TreeNode t5 = TreeNode.fromArray(new Integer[]{1, null, 0, 0, 1});
        TreeNode r5 = sol.pruneTree2(t5);
        ++total; if ("1,null,0,null,1".equals(TreeNode.toLeetCodeString(r5))) ++pass; else System.out.println("FAIL 814-5: " + TreeNode.toLeetCodeString(r5));

        TreeNode t6 = TreeNode.fromArray(new Integer[]{1, 0, 1, 0, 0, 0, 1});
        TreeNode r6 = sol.pruneTree2(t6);
        ++total; if ("1,null,1,null,1".equals(TreeNode.toLeetCodeString(r6))) ++pass; else System.out.println("FAIL 814-6: " + TreeNode.toLeetCodeString(r6));

        if (pass == total) {
            System.out.println("== 0814 PASS ==");
        } else {
            System.out.println("== 0814 FAIL ==");
            System.exit(1);
        }
    }

    private static class TreeNode {
        int val;
        TreeNode left;
        TreeNode right;

        TreeNode() {
        }

        TreeNode(int val) {
            this.val = val;
        }

        TreeNode(int val, TreeNode left, TreeNode right) {
            this.val = val;
            this.left = left;
            this.right = right;
        }

        static TreeNode fromArray(Integer[] arr) {
            if (arr == null || arr.length == 0 || arr[0] == null) {
                return null;
            }
            TreeNode root = new TreeNode(arr[0]);
            java.util.Queue<TreeNode> q = new java.util.LinkedList<>();
            q.offer(root);
            int idx = 1;
            while (!q.isEmpty() && idx < arr.length) {
                TreeNode cur = q.poll();
                if (idx < arr.length && arr[idx] != null) {
                    cur.left = new TreeNode(arr[idx]);
                    q.offer(cur.left);
                }
                ++idx;
                if (idx < arr.length && arr[idx] != null) {
                    cur.right = new TreeNode(arr[idx]);
                    q.offer(cur.right);
                }
                ++idx;
            }
            return root;
        }

        static String toLeetCodeString(TreeNode root) {
            if (root == null) {
                return "";
            }
            java.util.List<String> tokens = new java.util.ArrayList<>();
            tokens.add(String.valueOf(root.val));
            java.util.Queue<TreeNode> q = new java.util.LinkedList<>();
            q.offer(root);
            while (!q.isEmpty()) {
                TreeNode cur = q.poll();
                if (cur.left != null) {
                    tokens.add(String.valueOf(cur.left.val));
                    q.offer(cur.left);
                } else {
                    tokens.add("null");
                }
                if (cur.right != null) {
                    tokens.add(String.valueOf(cur.right.val));
                    q.offer(cur.right);
                } else {
                    tokens.add("null");
                }
            }
            while (!tokens.isEmpty() && "null".equals(tokens.get(tokens.size() - 1))) {
                tokens.remove(tokens.size() - 1);
            }
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < tokens.size(); ++i) {
                if (i > 0) {
                    sb.append(",");
                }
                sb.append(tokens.get(i));
            }
            return sb.toString();
        }
    }
}

复杂度分析

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

815. Bus Routes(公交路线)

题目描述

给你一个数组 routes,表示一系列公交线路,其中每个 routes[i] 表示一条公交线路,第 i
辆公交车将会在上面循环行驶。例如,路线 routes[0] = [1, 5, 7] 表示第 0 辆公交车会一直按
序列 1 -> 5 -> 7 -> 1 -> ... 这样的车站路线行驶。
现在从 source 车站出发(初始时不在公交车上),要前往 target 车站。期间仅可乘坐公交车。
求出最少乘坐的公交车数量。如果不可能到达终点车站,返回 -1。

示例 1

  • 输入:routes = [[1,2,7],[3,6,7]], source = 1, target = 6
  • 输出:2

示例 2

  • 输入:routes = [[7,12],[4,5,15],[6],[15,19],[9,12,13]], source = 15, target = 12
  • 输出:-1
    提示:
  • 1 <= routes.length <= 500
  • 1 <= routes[i].length <= 10^5
  • sum(routes[i].length) <= 10^5
  • 0 <= routes[i][j] < 10^6
  • 0 <= source, target < 10^6

解题思路

每次乘坐一辆公交可到达该线路上的任意站点,求最少乘坐次数。按站点逐步走边会把同一辆车
重复展开;站点与线路规模较大,应把「换乘」作为 BFS 的一层。
解法一(BFS 换乘分层,主提交):先建站点到线路的倒排。队列中存站点与已乘车次数,每条线路
只展开一次,把它的全部未访问站点入队。这样每辆车至多处理一遍。
解法二(双层 BFS,对照实现):先按「线路-线路是否共站」建图再跑 BFS,思路不同但同样线性。

代码实现

class Solution {

    // 解法一:BFS 换乘分层(推荐提交)
    public int numBusesToDestination(int[][] routes, int source, int target) {
        if (source == target) {
            return 0;
        }
        // 站点 -> 线路
        java.util.Map<Integer, java.util.List<Integer>> stopToBus = new java.util.HashMap<>();
        for (int i = 0; i < routes.length; ++i) {
            for (int stop : routes[i]) {
                stopToBus.computeIfAbsent(stop, k -> new java.util.ArrayList<>()).add(i);
            }
        }
        if (!stopToBus.containsKey(source) || !stopToBus.containsKey(target)) {
            return -1;
        }
        // 队列:站点 + 已乘次数
        java.util.ArrayDeque<int[]> q = new java.util.ArrayDeque<>();
        boolean[] visBus = new boolean[routes.length];
        java.util.Set<Integer> visStop = new java.util.HashSet<>();
        q.offer(new int[]{source, 0});
        visStop.add(source);
        while (!q.isEmpty()) {
            int[] cur = q.poll();
            int stop = cur[0], cnt = cur[1];
            if (stop == target) {
                return cnt;
            }
            for (int bus : stopToBus.get(stop)) {
                if (visBus[bus]) {
                    continue;
                }
                visBus[bus] = true;
                for (int next : routes[bus]) {
                    if (!visStop.contains(next)) {
                        visStop.add(next);
                        q.offer(new int[]{next, cnt + 1});
                    }
                }
            }
        }
        return -1;
    }

    // 解法二:线路图 BFS(对照实现)
    public int numBusesToDestination2(int[][] routes, int source, int target) {
        if (source == target) {
            return 0;
        }
        int m = routes.length;
        java.util.Map<Integer, java.util.List<Integer>> stopToBus = new java.util.HashMap<>();
        for (int i = 0; i < m; ++i) {
            for (int stop : routes[i]) {
                stopToBus.computeIfAbsent(stop, k -> new java.util.ArrayList<>()).add(i);
            }
        }
        if (!stopToBus.containsKey(source) || !stopToBus.containsKey(target)) {
            return -1;
        }
        boolean[][] share = new boolean[m][m];
        for (int i = 0; i < m; ++i) {
            java.util.Set<Integer> s = new java.util.HashSet<>();
            for (int stop : routes[i]) {
                s.add(stop);
            }
            for (int j = i + 1; j < m; ++j) {
                for (int stop : routes[j]) {
                    if (s.contains(stop)) {
                        share[i][j] = share[j][i] = true;
                        break;
                    }
                }
            }
        }
        int[] dist = new int[m];
        java.util.Arrays.fill(dist, -1);
        java.util.ArrayDeque<Integer> q = new java.util.ArrayDeque<>();
        boolean[] startBus = new boolean[m];
        for (int b : stopToBus.get(source)) {
            startBus[b] = true;
            dist[b] = 1;
            q.offer(b);
        }
        while (!q.isEmpty()) {
            int b = q.poll();
            for (int nb = 0; nb < m; ++nb) {
                if (share[b][nb] && dist[nb] == -1) {
                    dist[nb] = dist[b] + 1;
                    q.offer(nb);
                }
            }
        }
        int ans = Integer.MAX_VALUE;
        for (int b : stopToBus.get(target)) {
            if (dist[b] != -1) {
                ans = Math.min(ans, dist[b]);
            }
        }
        return ans == Integer.MAX_VALUE ? -1 : ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;

        ++total; if (sol.numBusesToDestination(new int[][]{{1, 2, 7}, {3, 6, 7}}, 1, 6) == 2) ++pass; else System.out.println("FAIL 815-1");
        ++total; if (sol.numBusesToDestination(new int[][]{{7, 12}, {4, 5, 15}, {6}, {15, 19}, {9, 12, 13}}, 15, 12) == -1) ++pass; else System.out.println("FAIL 815-2");
        ++total; if (sol.numBusesToDestination(new int[][]{{1, 2}, {2, 3}}, 1, 3) == 2) ++pass; else System.out.println("FAIL 815-3");
        ++total; if (sol.numBusesToDestination(new int[][]{{1, 2}, {2, 3}}, 1, 1) == 0) ++pass; else System.out.println("FAIL 815-4");
        ++total; if (sol.numBusesToDestination(new int[][]{{1, 2, 3}}, 1, 3) == 1) ++pass; else System.out.println("FAIL 815-5");
        ++total; if (sol.numBusesToDestination(new int[][]{{1}, {2}, {3}}, 1, 3) == -1) ++pass; else System.out.println("FAIL 815-6");

        ++total; if (sol.numBusesToDestination2(new int[][]{{1, 2, 7}, {3, 6, 7}}, 1, 6) == 2) ++pass; else System.out.println("FAIL 815-7");
        ++total; if (sol.numBusesToDestination2(new int[][]{{7, 12}, {4, 5, 15}, {6}, {15, 19}, {9, 12, 13}}, 15, 12) == -1) ++pass; else System.out.println("FAIL 815-8");
        ++total; if (sol.numBusesToDestination2(new int[][]{{1, 2}, {2, 3}}, 1, 3) == 2) ++pass; else System.out.println("FAIL 815-9");
        ++total; if (sol.numBusesToDestination2(new int[][]{{1, 2, 3}}, 1, 3) == 1) ++pass; else System.out.println("FAIL 815-10");

        if (pass == total) {
            System.out.println("== 0815 PASS ==");
        } else {
            System.out.println("== 0815 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(L)O(L)
解法二O(L + m^2)(m 为线路数)O(L + m^2)

816. Ambiguous Coordinates(模糊坐标)

题目描述

我们有一些二维坐标,如 "(1, 3)" 或 "(2, 0.5)",然后我们移除所有逗号,小数点和空格,得到
一个字符串 S。返回所有可能的原始字符串到一个列表中。
原始的坐标表示法不会存在多余的零,所以不会出现类似于 "00", "0.0", "0.00", "1.0", "001",
"00.01" 或一些其他更小的数来表示坐标。此外,一个小数点前至少存在一个数,所以也不会出现
".1" 形式的数字。
最后返回的列表可以是任意顺序的。而且注意返回的两个数字中间(逗号之后)都有一个空格。

示例 1

  • 输入:"(123)"
  • 输出: ["(1, 23)", "(12, 3)", "(1.2, 3)", "(1, 2.3)"]

示例 2

  • 输入:"(00011)"
  • 输出: ["(0.001, 1)", "(0, 0.011)"]

示例 3

  • 输入:"(0123)"
  • 输出: ["(0, 123)", "(0, 12.3)", "(0, 1.23)", "(0.1, 23)", "(0.1, 2.3)", "(0.12, 3)"]

示例 4

  • 输入:"(100)"
  • 输出: ["(10, 0)"]
    提示:
  • 4 <= S.length <= 12
  • S[0] = "(", S[S.length - 1] = ")",且字符串 S 中的其他元素都是数字

解题思路

在括号内插入逗号与可选小数点,枚举所有合法坐标。串长不超过 12,枚举逗号位置后再枚举
小数点位置即可。
解法一(分段合法写法 + 组合,主提交):对左半 [1,i) 与右半 [i,n-1) 分别生成所有合法写法,
再两两组合成 "(x, y)"。
解法二(整体枚举 + 去重,对照实现):三重循环直接枚举逗号与小数点位置,用集合去重后输出。

代码实现

class Solution {

    // 解法一:分段合法写法组合(推荐提交)
    public java.util.List<String> ambiguousCoordinates(String s) {
        int n = s.length();
        java.util.List<String> ans = new java.util.ArrayList<>();
        for (int i = 2; i < n - 1; ++i) {
            java.util.List<String> xs = valid(s, 1, i);
            java.util.List<String> ys = valid(s, i, n - 1);
            for (String x : xs) {
                for (String y : ys) {
                    ans.add("(" + x + ", " + y + ")");
                }
            }
        }
        return ans;
    }

    // 返回 s[from,to) 的所有合法数字写法
    private java.util.List<String> valid(String s, int from, int to) {
        java.util.List<String> res = new java.util.ArrayList<>();
        for (int k = 1; k <= to - from; ++k) {
            String l = s.substring(from, from + k);
            String r = s.substring(from + k, to);
            // 左半不能以 0 开头(除非只有 0);右半不能以 0 结尾
            boolean okL = l.length() == 1 || l.charAt(0) != '0';
            boolean okR = r.isEmpty() || r.charAt(r.length() - 1) != '0';
            if (okL && okR) {
                StringBuilder sb = new StringBuilder(l);
                if (!r.isEmpty()) {
                    sb.append('.').append(r);
                }
                res.add(sb.toString());
            }
        }
        return res;
    }

    // 解法二:整体枚举 + 去重
    public java.util.List<String> ambiguousCoordinates2(String s) {
        int n = s.length();
        java.util.TreeSet<String> set = new java.util.TreeSet<>();
        for (int i = 2; i < n - 1; ++i) { // 逗号在 i 之前
            for (String x : splits(s.substring(1, i))) {
                for (String y : splits(s.substring(i, n - 1))) {
                    set.add("(" + x + ", " + y + ")");
                }
            }
        }
        return new java.util.ArrayList<>(set);
    }

    private java.util.List<String> splits(String t) {
        java.util.List<String> res = new java.util.ArrayList<>();
        for (int k = 1; k <= t.length(); ++k) {
            String l = t.substring(0, k);
            String r = t.substring(k);
            if (isValid(l, r)) {
                res.add(r.isEmpty() ? l : l + "." + r);
            }
        }
        return res;
    }

    private boolean isValid(String l, String r) {
        if (l.length() > 1 && l.charAt(0) == '0') {
            return false;
        }
        if (!r.isEmpty() && r.charAt(r.length() - 1) == '0') {
            return false;
        }
        return true;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;

        ++total; if (sol.ambiguousCoordinates("(123)").size() == 4) ++pass; else System.out.println("FAIL 816-1: " + sol.ambiguousCoordinates("(123)"));
        ++total; if (sol.ambiguousCoordinates("(00011)").size() == 2) ++pass; else System.out.println("FAIL 816-2: " + sol.ambiguousCoordinates("(00011)"));
        ++total; if (sol.ambiguousCoordinates("(0123)").size() == 6) ++pass; else System.out.println("FAIL 816-3: " + sol.ambiguousCoordinates("(0123)"));
        ++total; if (sol.ambiguousCoordinates("(100)").size() == 1) ++pass; else System.out.println("FAIL 816-4: " + sol.ambiguousCoordinates("(100)"));
        ++total; if (sol.ambiguousCoordinates("(123)").contains("(1, 23)")) ++pass; else System.out.println("FAIL 816-5");

        ++total; if (sol.ambiguousCoordinates2("(123)").size() == 4) ++pass; else System.out.println("FAIL 816-6");
        ++total; if (sol.ambiguousCoordinates2("(00011)").size() == 2) ++pass; else System.out.println("FAIL 816-7");
        ++total; if (sol.ambiguousCoordinates2("(0123)").size() == 6) ++pass; else System.out.println("FAIL 816-8");
        ++total; if (sol.ambiguousCoordinates2("(100)").size() == 1) ++pass; else System.out.println("FAIL 816-9");

        if (pass == total) {
            System.out.println("== 0816 PASS ==");
        } else {
            System.out.println("== 0816 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(n^3)O(n^3)(结果集)
解法二O(n^3)O(n^3)

817. Linked List Components(链表组件)

题目描述

给你一个链表的头节点 head,该链表包含唯一的整数值,以及一个整数数组 nums,它是链表值的
一个子集。
返回列表 nums 中组件的个数,这里对组件的定义为:链表中一段最长连续结点的值(该值必须在
列表 nums 中)构成的集合。

示例 1

  • 输入:head = [0,1,2,3], nums = [0,1,3]
  • 输出: 2
  • 解释: 链表中,0 和 1 是相连接的,且 nums 中不包含 2,所以 [0, 1] 是 nums 的一个组件,同理
    [3] 也是一个组件,故返回 2。

示例 2

  • 输入:head = [0,1,2,3,4], nums = [0,3,1,4]
  • 输出: 2
    提示:
  • 链表中节点数为 n
  • 1 <= n <= 10^4
  • 0 <= Node.val < n
  • Node.val 中所有值不同
  • 1 <= nums.length <= n
  • nums 中所有值不同

解题思路

连通分量是 nums 中值在链表上连续出现的极大段。n <= 10^4,把 nums 放入集合后沿链表扫一遍即可。
解法一(哈希集合 + 一次遍历,主提交):跳过不在集合中的结点;每遇到一段在集合中的连续结点
计为一个分量,并滑过整段。
解法二(前驱标记法,对照实现):记录每个 in-set 结点且其前驱不在集合中的个数,等价于
分量的起点数。

代码实现

class Solution {

    // 解法一:一次遍历(推荐提交)
    public int numComponents(ListNode head, int[] nums) {
        java.util.Set<Integer> set = new java.util.HashSet<>();
        for (int v : nums) {
            set.add(v);
        }
        int ans = 0;
        ListNode cur = head;
        while (cur != null) {
            if (set.contains(cur.val)) {
                ++ans;
                while (cur != null && set.contains(cur.val)) {
                    cur = cur.next;
                }
            } else {
                cur = cur.next;
            }
        }
        return ans;
    }

    // 解法二:分量起点计数
    public int numComponents2(ListNode head, int[] nums) {
        java.util.Set<Integer> set = new java.util.HashSet<>();
        for (int v : nums) {
            set.add(v);
        }
        int ans = 0;
        boolean inSet = false;
        ListNode cur = head;
        while (cur != null) {
            if (set.contains(cur.val)) {
                if (!inSet) {
                    ++ans; // 新分量起点
                    inSet = true;
                }
            } else {
                inSet = false;
            }
            cur = cur.next;
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;

        ListNode h1 = ListNode.fromArray(new int[]{0, 1, 2, 3});
        ++total; if (sol.numComponents(h1, new int[]{0, 1, 3}) == 2) ++pass; else System.out.println("FAIL 817-1");

        ListNode h2 = ListNode.fromArray(new int[]{0, 1, 2, 3, 4});
        ++total; if (sol.numComponents(h2, new int[]{0, 3, 1, 4}) == 2) ++pass; else System.out.println("FAIL 817-2");

        ListNode h3 = ListNode.fromArray(new int[]{0, 1, 2});
        ++total; if (sol.numComponents(h3, new int[]{0, 1, 2}) == 1) ++pass; else System.out.println("FAIL 817-3");

        ListNode h4 = ListNode.fromArray(new int[]{0});
        ++total; if (sol.numComponents(h4, new int[]{0}) == 1) ++pass; else System.out.println("FAIL 817-4");

        ++total; if (sol.numComponents2(h1, new int[]{0, 1, 3}) == 2) ++pass; else System.out.println("FAIL 817-5");
        ++total; if (sol.numComponents2(h2, new int[]{0, 3, 1, 4}) == 2) ++pass; else System.out.println("FAIL 817-6");
        ++total; if (sol.numComponents2(h3, new int[]{0, 1, 2}) == 1) ++pass; else System.out.println("FAIL 817-7");

        if (pass == total) {
            System.out.println("== 0817 PASS ==");
        } else {
            System.out.println("== 0817 FAIL ==");
            System.exit(1);
        }
    }
}

class ListNode {
    int val;
    ListNode next;

    ListNode() {
    }

    ListNode(int val) {
        this.val = val;
    }

    ListNode(int val, ListNode next) {
        this.val = val;
        this.next = next;
    }

    static ListNode fromArray(int[] arr) {
        if (arr == null || arr.length == 0) {
            return null;
        }
        ListNode dummy = new ListNode(0);
        ListNode tail = dummy;
        for (int v : arr) {
            tail.next = new ListNode(v);
            tail = tail.next;
        }
        return dummy.next;
    }
}

复杂度分析

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

818. Race Car(赛车)

题目描述

你的赛车可以从位置 0 开始,并且速度为 +1,在一条无限长的数轴上行驶。赛车也可以向负方向
行驶。赛车可以按照由加速指令 'A' 和倒车指令 'R' 组成的指令序列自动行驶。

  • 当收到指令 'A' 时,赛车这样行驶:position += speed; speed *= 2
  • 当收到指令 'R' 时,赛车这样行驶:如果速度为正数,那么 speed = -1;否则 speed = 1。
    当前所处位置不变。
    例如,在执行指令 "AAR" 后,赛车位置变化为 0 -> 1 -> 3 -> 3,速度变化为 1 -> 2 -> 4 -> -1。
    给你一个目标位置 target,返回能到达目标位置的最短指令序列的长度。

示例 1

  • 输入:target = 3
  • 输出:2
  • 解释:最短指令序列是 "AA"。

示例 2

  • 输入:target = 6
  • 输出:5
  • 解释:最短指令序列是 "AAARA"。
    提示:
  • 1 <= target <= 10^4

解题思路

指令序列对应在数轴上加速或掉头,目标位置可达 10^4。连续 k 次 A 到达 2^k-1,任何目标都可
由「越过再回头」或「中途回头再加速」拼成。
解法一(动态规划,主提交):dp[i] 为到达位置 i 的最少指令。若 i = 2^k-1 则恰好 k 次 A;
否则比较「冲到 2^k-1 再回头」以及在第 k-1 次加速后提前回头走 j 步再折返的代价,取最小。
解法二(BFS 状态搜索,对照实现):状态 (pos, speed) 展开,A/R 两种转移,逐层搜索最短长度;
数据规模小,可交叉验证。

代码实现

class Solution {

    // 解法一:动态规划(推荐提交)
    public int racecar(int target) {
        int[] dp = new int[target + 1];
        for (int i = 1; i <= target; ++i) {
            int k = 32 - Integer.numberOfLeadingZeros(i); // i 的二进制位数
            if (i == (1 << k) - 1) {
                dp[i] = k;
                continue;
            }
            // 先冲到 2^k - 1 再回头
            dp[i] = dp[(1 << k) - 1 - i] + k + 1;
            // 冲到 2^(k-1) - 1 后掉头,走 j 次再掉头
            for (int j = 0; j < k; ++j) {
                dp[i] = Math.min(dp[i],
                        dp[i - (1 << (k - 1)) + (1 << j)] + k - 1 + j + 2);
            }
        }
        return dp[target];
    }

    // 解法二:BFS(对照实现)
    public int racecar2(int target) {
        if (target == 0) {
            return 0;
        }
        java.util.ArrayDeque<int[]> q = new java.util.ArrayDeque<>();
        java.util.Set<Long> vis = new java.util.HashSet<>();
        q.offer(new int[]{0, 1}); // pos, speed
        long key0 = pack(0, 1);
        vis.add(key0);
        int steps = 0;
        while (!q.isEmpty()) {
            int sz = q.size();
            for (int t = 0; t < sz; ++t) {
                int[] cur = q.poll();
                int pos = cur[0], speed = cur[1];
                // A
                int np = pos + speed;
                if (np == target) {
                    return steps + 1;
                }
                // 允许范围限制,避免无限扩张
                if (Math.abs(np - target) < (1 << 14)) {
                    long nk = pack(np, speed * 2);
                    if (!vis.contains(nk)) {
                        vis.add(nk);
                        q.offer(new int[]{np, speed * 2});
                    }
                }
                // R
                int ns = speed > 0 ? -1 : 1;
                long nk2 = pack(pos, ns);
                if (!vis.contains(nk2)) {
                    vis.add(nk2);
                    q.offer(new int[]{pos, ns});
                }
            }
            ++steps;
        }
        return -1;
    }

    private long pack(int pos, int speed) {
        return ((long) (pos + 30000) << 20) | (speed & 0xFFFFF);
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;

        ++total; if (sol.racecar(3) == 2) ++pass; else System.out.println("FAIL 818-1");
        ++total; if (sol.racecar(6) == 5) ++pass; else System.out.println("FAIL 818-2");
        ++total; if (sol.racecar(1) == 1) ++pass; else System.out.println("FAIL 818-3");
        ++total; if (sol.racecar(2) == 4) ++pass; else System.out.println("FAIL 818-4");
        ++total; if (sol.racecar(4) == 5) ++pass; else System.out.println("FAIL 818-5");
        ++total; if (sol.racecar(7) == 3) ++pass; else System.out.println("FAIL 818-6");
        ++total; if (sol.racecar(5) == 7) ++pass; else System.out.println("FAIL 818-7");

        ++total; if (sol.racecar2(3) == 2) ++pass; else System.out.println("FAIL 818-8");
        ++total; if (sol.racecar2(6) == 5) ++pass; else System.out.println("FAIL 818-9");
        ++total; if (sol.racecar2(1) == 1) ++pass; else System.out.println("FAIL 818-10");
        ++total; if (sol.racecar2(4) == 5) ++pass; else System.out.println("FAIL 818-11");

        if (pass == total) {
            System.out.println("== 0818 PASS ==");
        } else {
            System.out.println("== 0818 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

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

819. Most Common Word(最常见的单词)

题目描述

给你一个字符串 paragraph 和一个表示禁用词的字符串数组 banned,返回出现频率最高的非
禁用词。题目数据保证至少存在一个非禁用词,且答案唯一。
paragraph 中的单词不区分大小写,答案应以小写形式返回。
注意:单词不包含标点符号。

示例 1

  • 输入:paragraph = "Bob hit a ball, the hit BALL flew far after it was hit.", banned = ["hit"]
  • 输出: "ball"

示例 2

  • 输入:paragraph = "a.", banned = []
  • 输出: "a"
    提示:
  • 1 <= paragraph.length <= 1000
  • paragraph 由英文字母、空格 ' '、和以下符号组成:"!?',;."
  • 0 <= banned.length <= 100
  • 1 <= banned[i].length <= 10
  • banned[i] 仅由小写英文字母组成

解题思路

要在段落中统计单词频率并排除禁用表,同时标点视为分隔符。段落长度 <= 1000,先规范化再计数。
解法一(正则取词 + 哈希计数,主提交):段落转小写后按连续字母取出单词,统计出现次数,
找出不在禁用集中的最高频词。
解法二(双指针切词 + 哈希计数,对照实现):不用正则,手动扫描字母段,逻辑等价。

代码实现

class Solution {

    // 解法一:正则取词 + 哈希计数(推荐提交)
    public String mostCommonWord(String paragraph, String[] banned) {
        java.util.Set<String> bannedWords = new java.util.HashSet<>();
        for (String w : banned) {
            bannedWords.add(w);
        }
        java.util.Map<String, Integer> cnt = new java.util.HashMap<>();
        java.util.regex.Matcher matcher =
                java.util.regex.Pattern.compile("[a-z]+").matcher(paragraph.toLowerCase());
        while (matcher.find()) {
            String word = matcher.group();
            if (!bannedWords.contains(word)) {
                cnt.put(word, cnt.getOrDefault(word, 0) + 1);
            }
        }
        String ans = null;
        int max = -1;
        for (java.util.Map.Entry<String, Integer> e : cnt.entrySet()) {
            if (e.getValue() > max) {
                max = e.getValue();
                ans = e.getKey();
            }
        }
        return ans;
    }

    // 解法二:双指针切词(对照实现)
    public String mostCommonWord2(String paragraph, String[] banned) {
        java.util.Set<String> bannedWords = new java.util.HashSet<>();
        for (String w : banned) {
            bannedWords.add(w);
        }
        java.util.Map<String, Integer> cnt = new java.util.HashMap<>();
        String s = paragraph.toLowerCase();
        int n = s.length();
        int i = 0;
        while (i < n) {
            while (i < n && !isLetter(s.charAt(i))) {
                ++i;
            }
            int j = i;
            while (j < n && isLetter(s.charAt(j))) {
                ++j;
            }
            if (i < j) {
                String word = s.substring(i, j);
                if (!bannedWords.contains(word)) {
                    cnt.put(word, cnt.getOrDefault(word, 0) + 1);
                }
            }
            i = j;
        }
        String ans = null;
        int max = -1;
        for (java.util.Map.Entry<String, Integer> e : cnt.entrySet()) {
            if (e.getValue() > max) {
                max = e.getValue();
                ans = e.getKey();
            }
        }
        return ans;
    }

    private boolean isLetter(char c) {
        return c >= 'a' && c <= 'z';
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;

        ++total; if ("ball".equals(sol.mostCommonWord("Bob hit a ball, the hit BALL flew far after it was hit.", new String[]{"hit"}))) ++pass; else System.out.println("FAIL 819-1");
        ++total; if ("a".equals(sol.mostCommonWord("a.", new String[]{}))) ++pass; else System.out.println("FAIL 819-2");
        ++total; if ("ball".equals(sol.mostCommonWord("Bob. hIt, baLl", new String[]{"bob", "hit"}))) ++pass; else System.out.println("FAIL 819-3");
        ++total; if ("a".equals(sol.mostCommonWord("a a a b b", new String[]{"b"}))) ++pass; else System.out.println("FAIL 819-4");
        ++total; if ("b".equals(sol.mostCommonWord("a a b b b", new String[]{"a"}))) ++pass; else System.out.println("FAIL 819-5");

        ++total; if ("ball".equals(sol.mostCommonWord2("Bob hit a ball, the hit BALL flew far after it was hit.", new String[]{"hit"}))) ++pass; else System.out.println("FAIL 819-6");
        ++total; if ("a".equals(sol.mostCommonWord2("a.", new String[]{}))) ++pass; else System.out.println("FAIL 819-7");
        ++total; if ("a".equals(sol.mostCommonWord2("a a a b b", new String[]{"b"}))) ++pass; else System.out.println("FAIL 819-8");

        if (pass == total) {
            System.out.println("== 0819 PASS ==");
        } else {
            System.out.println("== 0819 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

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

820. Short Encoding of Words(单词的压缩编码)

题目描述

单词数组 words 的有效编码由任意助记字符串 s 和下标数组 indices 组成,且满足:

  • words.length == indices.length
  • 助记字符串 s 以 '#' 字符结尾
  • 对于每个下标 indices[i],s 的一个从 indices[i] 开始、到下一个 '#' 字符结束(但不包括 '#')
    的子字符串恰好与 words[i] 相等
    给你一个单词数组 words,返回成功对 words 进行编码的最小助记字符串 s 的长度。

示例 1

  • 输入:words = ["time", "me", "bell"]
  • 输出:10
  • 解释:一组有效编码为 s = "time#bell#" 和 indices = [0, 2, 5]。

示例 2

  • 输入:words = ["t"]
  • 输出:2
    提示:
  • 1 <= words.length <= 2000
  • 1 <= words[i].length <= 7
  • words[i] 仅由小写字母组成

解题思路

编码是若干单词加 # 的拼接,较短词若是较长词的后缀则可被其编码覆盖。
解法一(倒序 Trie,主提交):把单词倒序插入 Trie,公共后缀会落在同一条路径上。只有叶子
对应的单词需要单独编码,长度贡献为单词长加一;非叶子已被更长单词覆盖。
解法二(排序去后缀,对照实现):把每个单词反转,按字典序排序。排序后相邻的单词若互为
前后缀,则较短的可被覆盖,只统计不被覆盖的部分。

代码实现

class Solution {

    // 解法一:倒序维护 (去重后可复用)(推荐提交)
    public int minimumLengthEncoding(String[] words) {
        java.util.Set<String> set = new java.util.HashSet<>(java.util.Arrays.asList(words));
        // 如果一个词是另一个词的后缀,删掉短的
        java.util.List<String> list = new java.util.ArrayList<>(set);
        for (String w : list) {
            for (int k = 1; k < w.length(); ++k) {
                set.remove(w.substring(k));
            }
        }
        int ans = 0;
        for (String w : set) {
            ans += w.length() + 1;
        }
        return ans;
    }

    // 解法二:倒序 Trie,DFS 统计叶子深度(题解主流思路)
    private static class TrieNode {
        TrieNode[] children = new TrieNode[26];
    }

    public int minimumLengthEncoding2(String[] words) {
        TrieNode root = new TrieNode();
        for (String w : words) {
            TrieNode cur = root;
            for (int i = w.length() - 1; i >= 0; --i) {
                int idx = w.charAt(i) - 'a';
                if (cur.children[idx] == null) {
                    cur.children[idx] = new TrieNode();
                }
                cur = cur.children[idx];
            }
        }
        return dfsSum(root, 1); // 深度从 1 开始(计入 '#')
    }

    private int dfsSum(TrieNode cur, int depth) {
        boolean isLeaf = true;
        int ans = 0;
        for (int i = 0; i < 26; ++i) {
            if (cur.children[i] != null) {
                isLeaf = false;
                ans += dfsSum(cur.children[i], depth + 1);
            }
        }
        if (isLeaf) {
            ans += depth;
        }
        return ans;
    }

    // 解法三:反转 + 排序去重(对照实现)
    public int minimumLengthEncoding3(String[] words) {
        java.util.List<String> rev = new java.util.ArrayList<>();
        for (String w : words) {
            rev.add(new StringBuilder(w).reverse().toString());
        }
        java.util.Collections.sort(rev);
        int ans = 0;
        for (int i = 0; i < rev.size(); ++i) {
            if (i + 1 < rev.size() && rev.get(i + 1).startsWith(rev.get(i))) {
                continue; // 是下一个的后缀(反转后是前缀),可覆盖
            }
            ans += rev.get(i).length() + 1;
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;

        ++total; if (sol.minimumLengthEncoding(new String[]{"time", "me", "bell"}) == 10) ++pass; else System.out.println("FAIL 820-1");
        ++total; if (sol.minimumLengthEncoding(new String[]{"t"}) == 2) ++pass; else System.out.println("FAIL 820-2");
        ++total; if (sol.minimumLengthEncoding(new String[]{"time", "time", "time"}) == 5) ++pass; else System.out.println("FAIL 820-3");
        ++total; if (sol.minimumLengthEncoding(new String[]{"me", "time"}) == 5) ++pass; else System.out.println("FAIL 820-4");
        ++total; if (sol.minimumLengthEncoding(new String[]{"fe", "x", "be"}) == 8) ++pass; else System.out.println("FAIL 820-5");

        ++total; if (sol.minimumLengthEncoding2(new String[]{"time", "me", "bell"}) == 10) ++pass; else System.out.println("FAIL 820-6");
        ++total; if (sol.minimumLengthEncoding2(new String[]{"t"}) == 2) ++pass; else System.out.println("FAIL 820-7");
        ++total; if (sol.minimumLengthEncoding2(new String[]{"me", "time"}) == 5) ++pass; else System.out.println("FAIL 820-8");

        ++total; if (sol.minimumLengthEncoding3(new String[]{"time", "me", "bell"}) == 10) ++pass; else System.out.println("FAIL 820-9");
        ++total; if (sol.minimumLengthEncoding3(new String[]{"t"}) == 2) ++pass; else System.out.println("FAIL 820-10");
        ++total; if (sol.minimumLengthEncoding3(new String[]{"me", "time"}) == 5) ++pass; else System.out.println("FAIL 820-11");

        if (pass == total) {
            System.out.println("== 0820 PASS ==");
        } else {
            System.out.println("== 0820 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(sum(len(w)))O(sum(len(w)))
解法二O(sum(len(w)) + L log L)O(L)

821. Shortest Distance to a Character(字符的最短距离)

题目描述

给你一个字符串 s 和一个字符 c,且 c 是 s 中出现过的字符。
返回一个整数数组 answer,其中 answer.length == s.length 且 answer[i] 是 s 中从下标
i 到离它最近的字符 c 的距离。
两个下标 i 和 j 之间的距离为 abs(i - j),其中 abs 是绝对值函数。

示例 1

  • 输入:s = "loveleetcode", c = "e"
  • 输出:[3,2,1,0,1,0,0,1,2,2,1,0]

示例 2

  • 输入:s = "aaab", c = "b"
  • 输出:[3,2,1,0]
    提示:
  • 1 <= s.length <= 10^4
  • s[i] 和 c 均为小写英文字母
  • 题目数据保证 c 在 s 中至少出现一次

解题思路

对每个位置求到最近字符 c 的距离。最近的 c 只可能来自左侧或右侧。
解法一(两次遍历,主提交):从左向右记录最近的 c 下标,再从右向左扫一遍取更近者,
两遍线性扫描。
解法二(记录出现位置 + 二分,对照实现):先收集 c 的所有位置,对每个下标二分查找最近的
c 位置,逻辑直观。

代码实现

class Solution {

    // 解法一:两次遍历(推荐提交)
    public int[] shortestToChar(String s, char c) {
        int n = s.length();
        int[] ans = new int[n];
        java.util.Arrays.fill(ans, Integer.MAX_VALUE);
        int pre = Integer.MIN_VALUE / 2;
        for (int i = 0; i < n; ++i) {
            if (s.charAt(i) == c) {
                pre = i;
            }
            ans[i] = Math.min(ans[i], i - pre);
        }
        int suf = Integer.MAX_VALUE / 2;
        for (int i = n - 1; i >= 0; --i) {
            if (s.charAt(i) == c) {
                suf = i;
            }
            ans[i] = Math.min(ans[i], suf - i);
        }
        return ans;
    }

    // 解法二:位置收集 + 二分(对照实现)
    public int[] shortestToChar2(String s, char c) {
        java.util.List<Integer> pos = new java.util.ArrayList<>();
        for (int i = 0; i < s.length(); ++i) {
            if (s.charAt(i) == c) {
                pos.add(i);
            }
        }
        int[] ans = new int[s.length()];
        for (int i = 0; i < s.length(); ++i) {
            int idx = lowerBound(pos, i);
            int best = Integer.MAX_VALUE;
            if (idx < pos.size()) {
                best = Math.min(best, pos.get(idx) - i);
            }
            if (idx > 0) {
                best = Math.min(best, i - pos.get(idx - 1));
            }
            ans[i] = best;
        }
        return ans;
    }

    private int lowerBound(java.util.List<Integer> a, int key) {
        int lo = 0, hi = a.size();
        while (lo < hi) {
            int mid = (lo + hi) >>> 1;
            if (a.get(mid) < key) {
                lo = mid + 1;
            } else {
                hi = mid;
            }
        }
        return lo;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;

        ++total; if (java.util.Arrays.equals(sol.shortestToChar("loveleetcode", 'e'), new int[]{3, 2, 1, 0, 1, 0, 0, 1, 2, 2, 1, 0})) ++pass; else System.out.println("FAIL 821-1");
        ++total; if (java.util.Arrays.equals(sol.shortestToChar("aaab", 'b'), new int[]{3, 2, 1, 0})) ++pass; else System.out.println("FAIL 821-2");
        ++total; if (java.util.Arrays.equals(sol.shortestToChar("a", 'a'), new int[]{0})) ++pass; else System.out.println("FAIL 821-3");
        ++total; if (java.util.Arrays.equals(sol.shortestToChar("abaa", 'b'), new int[]{1, 0, 1, 2})) ++pass; else System.out.println("FAIL 821-4");

        ++total; if (java.util.Arrays.equals(sol.shortestToChar2("loveleetcode", 'e'), new int[]{3, 2, 1, 0, 1, 0, 0, 1, 2, 2, 1, 0})) ++pass; else System.out.println("FAIL 821-5");
        ++total; if (java.util.Arrays.equals(sol.shortestToChar2("aaab", 'b'), new int[]{3, 2, 1, 0})) ++pass; else System.out.println("FAIL 821-6");
        ++total; if (java.util.Arrays.equals(sol.shortestToChar2("abaa", 'b'), new int[]{1, 0, 1, 2})) ++pass; else System.out.println("FAIL 821-7");

        if (pass == total) {
            System.out.println("== 0821 PASS ==");
        } else {
            System.out.println("== 0821 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(n)O(1)(除答案数组)
解法二O(n log m)O(m)

822. Card Flipping Game(翻转卡片游戏)

题目描述

在桌子上有 n 张卡片,每张卡片的正面和背面都写着一个正数(正面与背面上的数有可能不一样)。
我们可以先翻转任意张卡片,然后选择其中一张卡片。
如果选中的那张卡片背面的数字 x 与任意一张卡片的正面的数字都不同,那么这个数字是我们想要
的数字。
哪个数是这些想要的数字中最小的数(找到这些数中的最小值)呢?如果没有一个数字符合要求的,
输出 0。
其中,fronts[i] 和 backs[i] 分别代表第 i 张卡片的正面和背面的数字。
如果我们通过翻转卡片来交换正面与背面上的数,那么当初在正面的数就变成背面的数,背面的数就
变成正面的数。

示例 1

  • 输入:fronts = [1,2,4,4,7], backs = [1,3,4,1,3]
  • 输出:2

示例 2

  • 输入:fronts = [1], backs = [1]
  • 输出:0
    提示:
  • n == fronts.length == backs.length
  • 1 <= n <= 1000
  • 1 <= fronts[i], backs[i] <= 2000

解题思路

翻牌后正面朝上的数若在某张牌的正反面同时出现,则该数不可能成为答案。n <= 1000,先找出
所有正反相同的数,它们永远不能作为最终正面。
解法一(哈希集合,主提交):所有正反面相同的数加入集合;在其余出现过的数字中取最小者,
若没有合法数字则返回 0。
解法二(布尔数组标记 + 两次扫描,对照实现):数值上界 2000,用布尔数组模拟集合,
逻辑与解法一同源。

代码实现

class Solution {

    // 解法一:哈希集合(推荐提交)
    public int flipgame(int[] fronts, int[] backs) {
        java.util.Set<Integer> forbidden = new java.util.HashSet<>();
        int n = fronts.length;
        for (int i = 0; i < n; ++i) {
            if (fronts[i] == backs[i]) {
                forbidden.add(fronts[i]);
            }
        }
        int ans = Integer.MAX_VALUE;
        for (int i = 0; i < n; ++i) {
            if (!forbidden.contains(fronts[i])) {
                ans = Math.min(ans, fronts[i]);
            }
            if (!forbidden.contains(backs[i])) {
                ans = Math.min(ans, backs[i]);
            }
        }
        return ans == Integer.MAX_VALUE ? 0 : ans;
    }

    // 解法二:布尔数组标记(对照实现)
    public int flipgame2(int[] fronts, int[] backs) {
        boolean[] forbidden = new boolean[2001];
        int n = fronts.length;
        for (int i = 0; i < n; ++i) {
            if (fronts[i] == backs[i]) {
                forbidden[fronts[i]] = true;
            }
        }
        int ans = 0;
        for (int i = 0; i < n; ++i) {
            if (!forbidden[fronts[i]]) {
                ans = (ans == 0) ? fronts[i] : Math.min(ans, fronts[i]);
            }
            if (!forbidden[backs[i]]) {
                ans = (ans == 0) ? backs[i] : Math.min(ans, backs[i]);
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;

        ++total; if (sol.flipgame(new int[]{1, 2, 4, 4, 7}, new int[]{1, 3, 4, 1, 3}) == 2) ++pass; else System.out.println("FAIL 822-1");
        ++total; if (sol.flipgame(new int[]{1}, new int[]{1}) == 0) ++pass; else System.out.println("FAIL 822-2");
        ++total; if (sol.flipgame(new int[]{1, 2}, new int[]{1, 3}) == 2) ++pass; else System.out.println("FAIL 822-3");
        ++total; if (sol.flipgame(new int[]{1, 1}, new int[]{2, 2}) == 1) ++pass; else System.out.println("FAIL 822-4");
        ++total; if (sol.flipgame(new int[]{2, 3}, new int[]{2, 2}) == 3) ++pass; else System.out.println("FAIL 822-5");

        ++total; if (sol.flipgame2(new int[]{1, 2, 4, 4, 7}, new int[]{1, 3, 4, 1, 3}) == 2) ++pass; else System.out.println("FAIL 822-6");
        ++total; if (sol.flipgame2(new int[]{1}, new int[]{1}) == 0) ++pass; else System.out.println("FAIL 822-7");
        ++total; if (sol.flipgame2(new int[]{2, 3}, new int[]{2, 2}) == 3) ++pass; else System.out.println("FAIL 822-8");

        if (pass == total) {
            System.out.println("== 0822 PASS ==");
        } else {
            System.out.println("== 0822 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

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

823. Binary Trees With Factors(带因子的二叉树)

题目描述

给出一个含有不重复整数元素的数组 arr,每个整数 arr[i] 均大于 1。
用这些整数来构建二叉树,每个整数可以使用任意次数。其中:每个非叶结点的值应等于它的两个
子结点的值的乘积。
满足条件的二叉树一共有多少个?答案可能很大,返回对 10^9 + 7 取余的结果。

示例 1

  • 输入:arr = [2, 4]
  • 输出: 3
  • 解释: 可以得到这些二叉树: [2], [4], [4, 2, 2]

示例 2

  • 输入:arr = [2, 4, 5, 10]
  • 输出: 7
  • 解释: 可以得到这些二叉树: [2], [4], [5], [10], [4, 2, 2], [10, 2, 5], [10, 5, 2]
    提示:
  • 1 <= arr.length <= 1000
  • 2 <= arr[i] <= 10^9
  • arr 中的所有值互不相同

解题思路

以数组中的数为结点、且孩子之积等于父结点,统计可构成的带标号二叉树。数组长度 <= 1000,
值互不相同,应按值从小到大构造,以便孩子先于父亲算完。
解法一(排序 + 动态规划,主提交):f[i] 为以 arr[i] 为根的方案数,枚举左孩子 b,若 a/b
也在数组中则累加 f[b] * f[c]。每个结点至少有一种单结点树,最后对 f 求和取模。
解法二(记忆化搜索,对照实现):对每个数递归计算其方案数,逻辑与 DP 等价。

代码实现

class Solution {

    private static final int MOD = 1_000_000_007;

    // 解法一:排序 + DP(推荐提交)
    public int numFactoredBinaryTrees(int[] arr) {
        java.util.Arrays.sort(arr);
        int n = arr.length;
        long[] f = new long[n];
        java.util.Arrays.fill(f, 1);
        java.util.Map<Integer, Integer> idx = new java.util.HashMap<>();
        for (int i = 0; i < n; ++i) {
            idx.put(arr[i], i);
        }
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < i; ++j) {
                int b = arr[j];
                if (arr[i] % b == 0) {
                    int c = arr[i] / b;
                    Integer k = idx.get(c);
                    if (k != null) {
                        f[i] = (f[i] + f[j] * f[k]) % MOD;
                    }
                }
            }
        }
        long ans = 0;
        for (long v : f) {
            ans = (ans + v) % MOD;
        }
        return (int) ans;
    }

    // 解法二:记忆化搜索(对照实现)
    private java.util.Map<Integer, Long> memo;
    private java.util.Set<Integer> all;

    public int numFactoredBinaryTrees2(int[] arr) {
        all = new java.util.HashSet<>();
        for (int v : arr) {
            all.add(v);
        }
        memo = new java.util.HashMap<>();
        long ans = 0;
        for (int v : arr) {
            ans = (ans + count(v)) % MOD;
        }
        return (int) ans;
    }

    private long count(int v) {
        Long cached = memo.get(v);
        if (cached != null) {
            return cached;
        }
        long ways = 1; // 单结点
        for (int b : all) {
            if (v % b == 0 && b * 1L * b <= v) { // 只枚举 b <= sqrt,另一侧乘 2 处理
                int c = v / b;
                if (c != b) {
                    if (all.contains(c)) {
                        ways = (ways + count(b) * count(c) * 2) % MOD;
                    }
                } else {
                    if (all.contains(c)) {
                        ways = (ways + count(b) * count(c)) % MOD;
                    }
                }
            }
        }
        memo.put(v, ways);
        return ways;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;

        ++total; if (sol.numFactoredBinaryTrees(new int[]{2, 4}) == 3) ++pass; else System.out.println("FAIL 823-1");
        ++total; if (sol.numFactoredBinaryTrees(new int[]{2, 4, 5, 10}) == 7) ++pass; else System.out.println("FAIL 823-2");
        ++total; if (sol.numFactoredBinaryTrees(new int[]{2}) == 1) ++pass; else System.out.println("FAIL 823-3");
        ++total; if (sol.numFactoredBinaryTrees(new int[]{2, 3, 6}) == 5) ++pass; else System.out.println("FAIL 823-4");
        ++total; if (sol.numFactoredBinaryTrees(new int[]{18, 3, 6, 2}) == 12) ++pass; else System.out.println("FAIL 823-5");

        ++total; if (sol.numFactoredBinaryTrees2(new int[]{2, 4}) == 3) ++pass; else System.out.println("FAIL 823-6");
        ++total; if (sol.numFactoredBinaryTrees2(new int[]{2, 4, 5, 10}) == 7) ++pass; else System.out.println("FAIL 823-7");
        ++total; if (sol.numFactoredBinaryTrees2(new int[]{2, 3, 6}) == 5) ++pass; else System.out.println("FAIL 823-8");

        if (pass == total) {
            System.out.println("== 0823 PASS ==");
        } else {
            System.out.println("== 0823 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

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

824. Goat Latin(山羊拉丁文)

题目描述

给你一个由若干单词组成的句子 sentence,单词间由空格分隔。每个单词仅由大写和小写英文字母
组成。
请你将句子转换为“山羊拉丁文(Goat Latin)”(一种类似于猪拉丁文 - Pig Latin 的虚构语言)。
山羊拉丁文的规则如下:

  1. 如果单词以元音开头('a', 'e', 'i', 'o', 'u'),在单词后添加 "ma"。
    例如,单词 "apple" 变为 "applema"。
  2. 如果单词以辅音字母开头(即,非元音字母),移除第一个字符并将它放到末尾,之后再添加 "ma"。
    例如,单词 "goat" 变为 "oatgma"。
  3. 根据单词在句子中的索引,在单词最后添加与索引相同数量的字母 'a',索引从 1 开始。
    例如,在第一个单词后添加 "a",在第二个单词后添加 "aa",以此类推。
    返回将 sentence 转换为山羊拉丁文后的句子。

示例 1

  • 输入:sentence = "I speak Goat Latin"
  • 输出:"Imaa peaksmaaa oatGmaaaa atinLmaaaaa"

示例 2

  • 输入:sentence = "The quick brown fox jumped over the lazy dog"
  • 输出:"heTmaa uickqmaaa rownbmaaaa oxfmaaaaa umpedjmaaaaaa overmaaaaaaa hetmaaaaaaaa azylmaaaaaaaaa ogdmaaaaaaaaaa"
    提示:
  • 1 <= sentence.length <= 150
  • sentence 由英文字母和空格组成
  • sentence 不含前导或尾随空格
  • sentence 中的所有单词由单个空格分隔

解题思路

按规则改写每个单词:辅音开头则把首字母移到末尾,再统一加 ma 以及与下标相应个数的 a。
解法一(逐词处理 + 字符串拼接,主提交):用小写判断元音,第 i 个单词(从 1 计)追加 i 个 a,
最后用空格拼回。
解法二(StringBuilder 单次扫描,对照实现):不 split,直接扫描单词并构造,减少中间串。

代码实现

class Solution {

    private static final java.util.Set<Character> VOWELS =
            new java.util.HashSet<>(java.util.Arrays.asList('a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U'));

    // 解法一:split 逐词处理(推荐提交)
    public String toGoatLatin(String sentence) {
        String[] words = sentence.split(" ");
        StringBuilder ans = new StringBuilder();
        for (int i = 0; i < words.length; ++i) {
            String w = words[i];
            StringBuilder t = new StringBuilder();
            if (VOWELS.contains(w.charAt(0))) {
                t.append(w);
            } else {
                t.append(w.substring(1)).append(w.charAt(0));
            }
            t.append("ma");
            for (int j = 0; j <= i; ++j) {
                t.append('a');
            }
            if (i > 0) {
                ans.append(' ');
            }
            ans.append(t);
        }
        return ans.toString();
    }

    // 解法二:单次扫描(对照实现)
    public String toGoatLatin2(String sentence) {
        StringBuilder ans = new StringBuilder();
        int idx = 1;
        int i = 0, n = sentence.length();
        while (i < n) {
            int j = i;
            while (j < n && sentence.charAt(j) != ' ') {
                ++j;
            }
            String w = sentence.substring(i, j);
            StringBuilder t = new StringBuilder();
            if (VOWELS.contains(w.charAt(0))) {
                t.append(w);
            } else {
                t.append(w.substring(1)).append(w.charAt(0));
            }
            t.append("ma");
            for (int k = 0; k < idx; ++k) {
                t.append('a');
            }
            if (ans.length() > 0) {
                ans.append(' ');
            }
            ans.append(t);
            ++idx;
            i = j + 1;
        }
        return ans.toString();
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;

        ++total; if ("Imaa peaksmaaa oatGmaaaa atinLmaaaaa".equals(sol.toGoatLatin("I speak Goat Latin"))) ++pass; else System.out.println("FAIL 824-1");
        ++total; if ("heTmaa uickqmaaa rownbmaaaa oxfmaaaaa umpedjmaaaaaa overmaaaaaaa hetmaaaaaaaa azylmaaaaaaaaa ogdmaaaaaaaaaa".equals(sol.toGoatLatin("The quick brown fox jumped over the lazy dog"))) ++pass; else System.out.println("FAIL 824-2");
        ++total; if ("amaa".equals(sol.toGoatLatin("a"))) ++pass; else System.out.println("FAIL 824-3");
        ++total; if ("applemaa applemaaa".equals(sol.toGoatLatin("apple apple"))) ++pass; else System.out.println("FAIL 824-4");
        ++total; if ("eachwordmaa".equals(sol.toGoatLatin("eachword"))) ++pass; else System.out.println("FAIL 824-5");
        ++total; if ("bmaa".equals(sol.toGoatLatin("b"))) ++pass; else System.out.println("FAIL 824-6");

        ++total; if ("Imaa peaksmaaa oatGmaaaa atinLmaaaaa".equals(sol.toGoatLatin2("I speak Goat Latin"))) ++pass; else System.out.println("FAIL 824-7");
        ++total; if ("heTmaa uickqmaaa rownbmaaaa oxfmaaaaa umpedjmaaaaaa overmaaaaaaa hetmaaaaaaaa azylmaaaaaaaaa ogdmaaaaaaaaaa".equals(sol.toGoatLatin2("The quick brown fox jumped over the lazy dog"))) ++pass; else System.out.println("FAIL 824-8");
        ++total; if ("amaa".equals(sol.toGoatLatin2("a"))) ++pass; else System.out.println("FAIL 824-9");

        if (pass == total) {
            System.out.println("== 0824 PASS ==");
        } else {
            System.out.println("== 0824 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

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

825. Friends Of Appropriate Ages(适龄的朋友)

题目描述

在社交媒体网站上有 n 个用户。给你一个整数数组 ages,其中 ages[i] 是第 i 个用户的年龄。 如果下述任意一个条件为真,那么用户 x 将不会向用户 y(x != y)发送好友请求: 1. ages[y] <= 0.5 * ages[x] + 7 2. ages[y] > ages[x] 3. ages[y] > 100 && ages[x] < 100 否则,x 将会向 y 发送一条好友请求。 注意,如果 x 向 y 发送一条好友请求,y 不必也向 x 发送一条好友请求。另外,用户不会向自己发送 好友请求。 返回在该社交媒体网站上产生的好友请求总数。

示例 1

  • 输入:ages = [16,16]
  • 输出:2

示例 2

  • 输入:ages = [16,17,18]
  • 输出:2

示例 3

  • 输入:ages = [20,30,100,110,120]
  • 输出:3 提示:
  • n == ages.length
  • 1 <= n <= 2 * 10^4
  • 1 <= ages[i] <= 120

解题思路

好友请求的条件只依赖两个年龄,且年龄范围仅 1..120,而 n 可达 2*10^4。两两枚举人会重复 计算相同年龄对。 解法一(计数 + 枚举年龄对,主提交):先计数每个年龄的人数,再枚举年龄对 (ax, ay) 判断是否 满足三个不等式。同一年龄内部要去掉自己给自己发请求,贡献为 x * (y - [ax == ay])。 解法二(排序 + 双指针,对照实现):对年龄排序,对每个 x 用二分/边界确定满足条件 y 的区间, 统计区间内人数后减去自身。

代码实现

class Solution {

    // 解法一:计数 + 枚举年龄对(推荐提交)
    public int numFriendRequests(int[] ages) {
        int[] cnt = new int[121];
        for (int a : ages) {
            ++cnt[a];
        }
        int ans = 0;
        for (int ax = 1; ax <= 120; ++ax) {
            for (int ay = 1; ay <= 120; ++ay) {
                if (canSend(ax, ay)) {
                    if (ax == ay) {
                        ans += cnt[ax] * (cnt[ax] - 1);
                    } else {
                        ans += cnt[ax] * cnt[ay];
                    }
                }
            }
        }
        return ans;
    }

    // x 是否可以向 y 发送请求
    private boolean canSend(int ax, int ay) {
        if (ay <= 0.5 * ax + 7) {
            return false;
        }
        if (ay > ax) {
            return false;
        }
        if (ay > 100 && ax < 100) {
            return false;
        }
        return true;
    }

    // 解法二:排序 + 二分(对照实现)
    public int numFriendRequests2(int[] ages) {
        java.util.Arrays.sort(ages);
        int n = ages.length;
        int ans = 0;
        for (int i = 0; i < n; ++i) {
            int ax = ages[i];
            // y 必须满足 ay > 0.5*ax + 7 且 ay <= ax
            int lo = upperBound(ages, (int) (0.5 * ax + 7)); // 第一个 > 0.5*ax+7 的下标
            int hi = upperBound(ages, ax) - 1; // 最后一个 <= ax 的下标
            if (lo <= hi) {
                ans += hi - lo + 1;
                // 减去自己给自己发的一条(自己一定在区间内)
                --ans;
            }
        }
        return ans;
    }

    private int upperBound(int[] a, int key) {
        int lo = 0, hi = a.length;
        while (lo < hi) {
            int mid = (lo + hi) >>> 1;
            if (a[mid] <= key) {
                lo = mid + 1;
            } else {
                hi = mid;
            }
        }
        return lo;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;

        ++total; if (sol.numFriendRequests(new int[]{16, 16}) == 2) ++pass; else System.out.println("FAIL 825-1");
        ++total; if (sol.numFriendRequests(new int[]{16, 17, 18}) == 2) ++pass; else System.out.println("FAIL 825-2");
        ++total; if (sol.numFriendRequests(new int[]{20, 30, 100, 110, 120}) == 3) ++pass; else System.out.println("FAIL 825-3");
        ++total; if (sol.numFriendRequests(new int[]{101, 56, 69, 48, 30}) == 4) ++pass; else System.out.println("FAIL 825-4");
        ++total; if (sol.numFriendRequests(new int[]{73, 106, 39, 6, 26, 15, 30, 100, 71, 35, 46, 112, 6, 60, 110}) == 29) ++pass; else System.out.println("FAIL 825-5");

        ++total; if (sol.numFriendRequests2(new int[]{16, 16}) == 2) ++pass; else System.out.println("FAIL 825-6");
        ++total; if (sol.numFriendRequests2(new int[]{16, 17, 18}) == 2) ++pass; else System.out.println("FAIL 825-7");
        ++total; if (sol.numFriendRequests2(new int[]{20, 30, 100, 110, 120}) == 3) ++pass; else System.out.println("FAIL 825-8");
        ++total; if (sol.numFriendRequests2(new int[]{101, 56, 69, 48, 30}) == 4) ++pass; else System.out.println("FAIL 825-9");

        if (pass == total) {
            System.out.println("== 0825 PASS ==");
        } else {
            System.out.println("== 0825 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

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