LeetCode776~789题目解析

0 阅读1分钟

LeetCode 776-789. 题解合集


776. Split BST(拆分二叉搜索树)

题目描述

给你一棵二叉搜索树(BST)的根结点 root 和一个整数 target。请将该树按要求拆分为两个子树:
其中第一个子树结点的值都必须小于等于给定的目标值;另一个子树结点的值都必须大于目标值;
树中并非一定要存在值为 target 的结点。
除此之外,树中大部分结构都需要保留,也就是说原始树中父节点 p 的任意子节点 c,假如拆分后
它们仍在同一个子树中,那么结点 p 应仍为 c 的父结点。
按顺序返回两个子树的根结点的数组。

示例 1

  • 输入:root = [4,2,6,1,3,5,7], target = 2
  • 输出:[[2,1],[4,3,6,null,null,5,7]]

示例 2

  • 输入:root = [1], target = 1
  • 输出:[[1],[]]
    提示:
  • 二叉搜索树节点个数在 [1, 50] 范围内
  • 0 <= Node.val, target <= 1000

解题思路

解法一(递归分治,主提交):递归函数返回以当前节点为根的子树拆成的两个部分。
若 root.val <= target,则 root 连同其左子树整体属于「小树」,只需继续拆分右子树:
拆出的"较小部分"接在 root.right 上,root 本身作为小树的根;
若 root.val > target,对称地,整体属于「大树」,继续拆左子树,"较大部分"接在 root.left 上。
时间复杂度 O(h),空间复杂度 O(h)(h 为树高)。
解法二(迭代 + 哨兵拼接):用两个哨兵节点分别做小树大树的链尾,沿树游走:遇到 val <= target
的节点就挂到小树链尾并向右深入(其左子树天然属于小树),遇到 val > target 的节点就挂到
大树链尾并向左深入。每挂一个节点即断开对应方向的指针,最终哨兵的下一个节点即两棵树的根。
时间复杂度 O(h),空间复杂度 O(1)。

代码实现

import java.util.ArrayDeque;
import java.util.Deque;

class Solution {

    private static class TreeNode {
        int val;
        TreeNode left;
        TreeNode right;
        TreeNode(int val) { this.val = val; }
    }

    // 解法一:递归分治
    public TreeNode[] splitBST(TreeNode root, int target) {
        if (root == null) {
            return new TreeNode[] {null, null};
        }
        if (root.val <= target) {
            TreeNode[] pair = splitBST(root.right, target);
            root.right = pair[0];
            return new TreeNode[] {root, pair[1]};
        } else {
            TreeNode[] pair = splitBST(root.left, target);
            root.left = pair[1];
            return new TreeNode[] {pair[0], root};
        }
    }

    // 解法二:迭代 + 哨兵拼接
    public TreeNode[] splitBST2(TreeNode root, int target) {
        TreeNode small = new TreeNode(-1);
        TreeNode large = new TreeNode(-1);
        TreeNode ps = small, pl = large;
        TreeNode cur = root;
        while (cur != null) {
            if (cur.val <= target) {
                ps.right = cur;   // 挂到小树链尾
                ps = cur;
                cur = cur.right;  // 保存右孩子引用
                ps.right = null;  // 断开,下一轮继续拼接
            } else {
                pl.left = cur;    // 挂到大树链尾
                pl = cur;
                cur = cur.left;
                pl.left = null;
            }
        }
        return new TreeNode[] {small.right, large.left};
    }

    // ===== 本地测试 =====
    private static TreeNode fromArray(Integer[] a) {
        if (a == null || a.length == 0) {
            return null;
        }
        TreeNode root = new TreeNode(a[0]);
        Deque<TreeNode> q = new ArrayDeque<>();
        q.offer(root);
        int i = 1;
        while (!q.isEmpty() && i < a.length) {
            TreeNode cur = q.poll();
            if (a[i] != null) {
                cur.left = new TreeNode(a[i]);
                q.offer(cur.left);
            }
            ++i;
            if (i < a.length && a[i] != null) {
                cur.right = new TreeNode(a[i]);
                q.offer(cur.right);
            }
            ++i;
        }
        return root;
    }

    private static String trace(TreeNode root) {
        if (root == null) {
            return "[]";
        }
        StringBuilder sb = new StringBuilder("[");
        java.util.List<TreeNode> q = new java.util.ArrayList<>();
        q.add(root);
        for (int head = 0; head < q.size(); ++head) {
            TreeNode cur = q.get(head);
            if (cur == null) {
                sb.append("null,");
                continue;
            }
            sb.append(cur.val).append(',');
            q.add(cur.left);
            q.add(cur.right);
        }
        String s = sb.toString();
        // 去掉尾部连续 null
        while (s.endsWith("null,")) {
            s = s.substring(0, s.length() - 5);
        }
        s = s.substring(0, s.length() - 1) + "]";
        return s;
    }

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

        TreeNode t1 = fromArray(new Integer[] {4, 2, 6, 1, 3, 5, 7});
        TreeNode[] r1 = sol.splitBST(t1, 2);
        ++total;
        if ("[2,1]".equals(trace(r1[0])) && "[4,3,6,null,null,5,7]".equals(trace(r1[1]))) ++pass;
        else System.out.println("FAIL 776-1 got " + trace(r1[0]) + " | " + trace(r1[1]));

        TreeNode t2 = fromArray(new Integer[] {1});
        TreeNode[] r2 = sol.splitBST(t2, 1);
        ++total;
        if ("[1]".equals(trace(r2[0])) && "[]".equals(trace(r2[1]))) ++pass;
        else System.out.println("FAIL 776-2 got " + trace(r2[0]) + " | " + trace(r2[1]));

        // 迭代法同样验证
        TreeNode u1 = fromArray(new Integer[] {4, 2, 6, 1, 3, 5, 7});
        TreeNode[] r3 = sol.splitBST2(u1, 2);
        ++total;
        if ("[2,1]".equals(trace(r3[0])) && "[4,3,6,null,null,5,7]".equals(trace(r3[1]))) ++pass;
        else System.out.println("FAIL 776-3 got " + trace(r3[0]) + " | " + trace(r3[1]));

        TreeNode u2 = fromArray(new Integer[] {5, 3, 8, 1, 4, 7, 10, null, null, null, null, null, null, 9, 11});
        TreeNode[] r4 = sol.splitBST2(u2, 6);
        ++total;
        if ("[5,3,null,1,4]".equals(trace(r4[0])) && "[8,7,10,null,null,9,11]".equals(trace(r4[1]))) ++pass;
        else System.out.println("FAIL 776-4 got " + trace(r4[0]) + " | " + trace(r4[1]));

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

复杂度分析

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

777. Swap Adjacent in LR String(在LR字符串中交换相邻字符)

题目描述

在一个由 'L'、'R' 和 'X' 三个字符组成的字符串(例如 "RXXLRXRXL")中进行移动操作。
一次移动操作指用一个 "LX" 替换一个 "XL",或者用一个 "XR" 替换一个 "RX"。
现给定起始字符串 start 和结束字符串 result,请编写代码,当且仅当存在一系列移动操作
使得 start 可以转换成 result 时,返回 true。

示例 1

  • 输入:start = "RXXLRXRXL", result = "XRLXXRRLX"
  • 输出:true
  • 解释:通过以下步骤我们可以将 start 转化为 result:
    RXXLRXRXL -> XRXLRXRXL -> XRLXRXRXL -> XRLXXRRXL -> XRLXXRRLX

示例 2

  • 输入:start = "X", result = "L"
  • 输出:false
    提示:
  • 1 <= start.length <= 10^4
  • start.length == result.length
  • start 和 result 都只包含 'L'、'R' 或 'X'

解题思路

观察操作本质:L 只能向左移动(XL -> LX),R 只能向右移动(RX -> XR),X 是空位。
因此两个字符串在去掉 'X' 后,L/R 的相对顺序必须完全一致;同时每个 L 在 result 中的位置
不能比其在 start 中的位置更靠右(L 只能左移),每个 R 的位置不能更靠左(R 只能右移)。
解法一(双指针一趟扫描,主提交):用两个指针各自跳过 'X',逐一对齐非 X 字符比较。
解法二(位置数组法):先过滤 X 比较序列(必要),再收集 start 与 result 中 L/R 的下标,
逐对校验方向合法(对照实现,思路更直白)。

代码实现

import java.util.ArrayList;
import java.util.List;

class Solution {

    // 解法一:双指针一趟扫描
    public boolean canTransform(String start, String result) {
        int n = start.length();
        if (n != result.length()) {
            return false;
        }
        int i = 0, j = 0;
        while (i < n || j < n) {
            while (i < n && start.charAt(i) == 'X') {
                ++i;
            }
            while (j < n && result.charAt(j) == 'X') {
                ++j;
            }
            if (i == n || j == n) {
                return i == n && j == n;
            }
            char a = start.charAt(i);
            char b = result.charAt(j);
            if (a != b) {
                return false;
            }
            if (a == 'L' && i < j) {
                return false; // L 只能向左,result 中位置更靠右则不合法
            }
            if (a == 'R' && i > j) {
                return false; // R 只能向右,result 中位置更靠左则不合法
            }
            ++i;
            ++j;
        }
        return true;
    }

    // 解法二:过滤 X + 位置校验
    public boolean canTransform2(String start, String result) {
        if (start.length() != result.length()) {
            return false;
        }
        // 去掉 X 后序列必须一致
        if (!strip(start).equals(strip(result))) {
            return false;
        }
        // 收集每个 L/R 在两边字符串中的下标
        List<int[]> aPos = collect(start);
        List<int[]> bPos = collect(result);
        for (int idx = 0; idx < aPos.size(); ++idx) {
            int ch = aPos.get(idx)[0];
            int a = aPos.get(idx)[1];
            int b = bPos.get(idx)[1];
            if (ch == 'L' && a < b) {
                return false; // L 只能左移
            }
            if (ch == 'R' && a > b) {
                return false; // R 只能右移
            }
        }
        return true;
    }

    private String strip(String s) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < s.length(); ++i) {
            if (s.charAt(i) != 'X') {
                sb.append(s.charAt(i));
            }
        }
        return sb.toString();
    }

    private List<int[]> collect(String s) {
        List<int[]> list = new ArrayList<>();
        for (int i = 0; i < s.length(); ++i) {
            char c = s.charAt(i);
            if (c == 'L' || c == 'R') {
                list.add(new int[] {c, i});
            }
        }
        return list;
    }

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

        ++total; if (sol.canTransform("RXXLRXRXL", "XRLXXRRLX")) ++pass; else System.out.println("FAIL 777-1");
        ++total; if (!sol.canTransform("X", "L")) ++pass; else System.out.println("FAIL 777-2");
        ++total; if (!sol.canTransform("RXXLRXRXL", "XRLXXRRLXL")) ++pass; else System.out.println("FAIL 777-3");
        ++total; if (sol.canTransform("XXXL", "LXXX")) ++pass; else System.out.println("FAIL 777-4");
        ++total; if (!sol.canTransform("LXXX", "XXXL")) ++pass; else System.out.println("FAIL 777-5");
        ++total; if (!sol.canTransform("XXRXLX", "XRXXLX")) ++pass; else System.out.println("FAIL 777-6");
        ++total; if (sol.canTransform2("RXXLRXRXL", "XRLXXRRLX")) ++pass; else System.out.println("FAIL 777-7");
        ++total; if (sol.canTransform2("XXXL", "LXXX")) ++pass; else System.out.println("FAIL 777-8");
        ++total; if (!sol.canTransform2("LXXX", "XXXL")) ++pass; else System.out.println("FAIL 777-9");

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

复杂度分析

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

778. Swim in Rising Water(水位上升的泳池中游泳)

题目描述

在一个 n x n 的整数矩阵 grid 中,每一个方格的值 grid[i][j] 表示位置 (i, j) 的平台高度。
当开始下雨时,在时间为 t 时,水池中的水位为 t。你可以从一个平台游向四周相邻的任意一个
平台,但是前提是此时水位必须同时淹没这两个平台。假定你可以瞬间移动无限距离,也就是默认在
方格内部游动是不耗时的。当然,在你游泳的时候你必须待在坐标方格里面。
你从坐标方格的左上平台 (0, 0) 出发。返回你到达坐标方格的右下平台 (n-1, n-1) 所需的最少时间。

示例 1

  • 输入:grid = [[0,2],[1,3]]
  • 输出:3

示例 2

  • 输入:grid = [[0,1,2,3,4],[24,23,22,21,5],[12,13,14,15,16],[11,17,18,19,20],[10,9,8,7,6]]
  • 输出:16
    提示:
  • n == grid.length
  • n == grid[i].length
  • 1 <= n <= 50
  • 0 <= grid[i][j] < n^2
  • grid[i][j] 中每个值均无重复

解题思路

本题的实质是:找一条从左上到右下的路径,使路径上最大的平台高度最小,答案即该最小最大值
(t 时刻水位为 t,能走通当且仅当路径上所有平台都 <= t)。
解法一(二分答案 + BFS,主提交):对时间 t 二分,check(t) 用 BFS 验证水位为 t 时
能否从 (0,0) 出发到达 (n-1,n-1)(只走平台高度 <= t 的格子)。答案区间为
[grid[0][0], n^2-1],单调可行,二分即可。
解法二(Dijkstra / 优先队列):把每个格子视为节点,边权为相邻两格平台高度的较大值,
用优先队列按"当前路径最大高度"从小到大扩展,最早到达右下角时的最大高度即答案。

代码实现

import java.util.ArrayDeque;
import java.util.PriorityQueue;
import java.util.Deque;

class Solution {

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

    // 解法一:二分答案 + BFS
    public int swimInWater(int[][] grid) {
        int n = grid.length;
        int lo = grid[0][0], hi = n * n - 1;
        while (lo < hi) {
            int mid = (lo + hi) >>> 1;
            if (canReach(grid, mid)) {
                hi = mid;
            } else {
                lo = mid + 1;
            }
        }
        return lo;
    }

    private boolean canReach(int[][] grid, int t) {
        int n = grid.length;
        if (grid[0][0] > t) {
            return false;
        }
        boolean[][] seen = new boolean[n][n];
        Deque<int[]> q = new ArrayDeque<>();
        q.offer(new int[] {0, 0});
        seen[0][0] = true;
        while (!q.isEmpty()) {
            int[] cur = q.poll();
            if (cur[0] == n - 1 && cur[1] == n - 1) {
                return true;
            }
            for (int[] d : DIRS) {
                int x = cur[0] + d[0];
                int y = cur[1] + d[1];
                if (x >= 0 && x < n && y >= 0 && y < n && !seen[x][y] && grid[x][y] <= t) {
                    seen[x][y] = true;
                    q.offer(new int[] {x, y});
                }
            }
        }
        return false;
    }

    // 解法二:Dijkstra(优先队列维护路径最大高度最小)
    public int swimInWater2(int[][] grid) {
        int n = grid.length;
        boolean[][] done = new boolean[n][n];
        PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[2] - b[2]);
        pq.offer(new int[] {0, 0, grid[0][0]});
        while (!pq.isEmpty()) {
            int[] cur = pq.poll();
            int x = cur[0], y = cur[1], h = cur[2];
            if (done[x][y]) {
                continue;
            }
            done[x][y] = true;
            if (x == n - 1 && y == n - 1) {
                return h;
            }
            for (int[] d : DIRS) {
                int nx = x + d[0];
                int ny = y + d[1];
                if (nx >= 0 && nx < n && ny >= 0 && ny < n && !done[nx][ny]) {
                    pq.offer(new int[] {nx, ny, Math.max(h, grid[nx][ny])});
                }
            }
        }
        return -1; // 不会到达
    }

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

        int[][] g1 = {{0, 2}, {1, 3}};
        ++total; if (sol.swimInWater(g1) == 3) ++pass; else System.out.println("FAIL 778-1 got " + sol.swimInWater(g1));
        int[][] g2 = {{0, 1, 2, 3, 4}, {24, 23, 22, 21, 5}, {12, 13, 14, 15, 16}, {11, 17, 18, 19, 20}, {10, 9, 8, 7, 6}};
        ++total; if (sol.swimInWater(g2) == 16) ++pass; else System.out.println("FAIL 778-2 got " + sol.swimInWater(g2));
        int[][] g3 = {{3, 2}, {0, 1}};
        ++total; if (sol.swimInWater(g3) == 3) ++pass; else System.out.println("FAIL 778-3 got " + sol.swimInWater(g3));

        int[][] g4 = {{0, 2}, {1, 3}};
        ++total; if (sol.swimInWater2(g4) == 3) ++pass; else System.out.println("FAIL 778-4 got " + sol.swimInWater2(g4));
        int[][] g5 = {{0, 1, 2, 3, 4}, {24, 23, 22, 21, 5}, {12, 13, 14, 15, 16}, {11, 17, 18, 19, 20}, {10, 9, 8, 7, 6}};
        ++total; if (sol.swimInWater2(g5) == 16) ++pass; else System.out.println("FAIL 778-5 got " + sol.swimInWater2(g5));
        int[][] g6 = {{3, 2}, {0, 1}};
        ++total; if (sol.swimInWater2(g6) == 3) ++pass; else System.out.println("FAIL 778-6 got " + sol.swimInWater2(g6));

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

复杂度分析

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

779. K-th Symbol in Grammar(第K个语法符号)

题目描述

我们构建了一个包含 n 行(索引从 1 开始)的表。首先在第一行我们写上一个 0。
接下来的每一行,将前一行中的 0 替换为 01,1 替换为 10。
例如,对于 n = 3,第 1 行是 0,第 2 行是 01,第 3 行是 0110。
给定行数 n 和序数 k,返回第 n 行中第 k 个字符(k 从索引 1 开始)。

示例 1

  • 输入:n = 1, k = 1
  • 输出:0

示例 2

  • 输入:n = 2, k = 1
  • 输出:0

示例 3

  • 输入:n = 2, k = 2
  • 输出:1
    提示:
  • 1 <= n <= 30
  • 1 <= k <= 2^(n-1)

解题思路

第 n 行的前一半与第 n-1 行完全相同,后一半是第 n-1 行每位取反。
解法一(递归倒推,主提交):若 k 在前半段,ans(n,k) = ans(n-1,k);
若 k 在后半段,ans(n,k) = 1 - ans(n-1, k - 2^(n-2)),逐层向上直到 n=1。
解法二(位运算性质):把 k-1 写成二进制,第 n 行第 k 个字符等于 popcount(k-1) % 2。
原因:第 n 行的第 k 位由起点 0 经过 n-1 次"翻倍取反"得到,k 每进入后半段翻转一次,
翻转次数恰为 k-1 中 1 的个数。

代码实现

class Solution {

    // 解法一:递归倒推
    public int kthGrammar(int n, int k) {
        if (n == 1) {
            return 0;
        }
        int half = 1 << (n - 2); // 前一半长度 2^(n-2)
        if (k <= half) {
            return kthGrammar(n - 1, k);
        }
        return 1 - kthGrammar(n - 1, k - half);
    }

    // 解法二:位运算(popcount 奇偶性)
    public int kthGrammar2(int n, int k) {
        return Integer.bitCount(k - 1) & 1;
    }

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

        ++total; if (sol.kthGrammar(1, 1) == 0) ++pass; else System.out.println("FAIL 779-1");
        ++total; if (sol.kthGrammar(2, 1) == 0) ++pass; else System.out.println("FAIL 779-2");
        ++total; if (sol.kthGrammar(2, 2) == 1) ++pass; else System.out.println("FAIL 779-3");
        ++total; if (sol.kthGrammar(3, 3) == 1) ++pass; else System.out.println("FAIL 779-4");
        ++total; if (sol.kthGrammar(3, 4) == 0) ++pass; else System.out.println("FAIL 779-5");
        ++total; if (sol.kthGrammar(4, 5) == 1) ++pass; else System.out.println("FAIL 779-6");
        ++total; if (sol.kthGrammar(30, 1 << 29) == 1) ++pass; else System.out.println("FAIL 779-7");

        ++total; if (sol.kthGrammar2(1, 1) == 0) ++pass; else System.out.println("FAIL 779-8");
        ++total; if (sol.kthGrammar2(2, 2) == 1) ++pass; else System.out.println("FAIL 779-9");
        ++total; if (sol.kthGrammar2(3, 4) == 0) ++pass; else System.out.println("FAIL 779-10");
        ++total; if (sol.kthGrammar2(4, 5) == 1) ++pass; else System.out.println("FAIL 779-11");

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

复杂度分析

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

780. Reaching Points(到达终点)

题目描述

给定四个整数 sx、sy、tx 和 ty,如果通过一系列的转换可以从起点 (sx, sy) 到达
终点 (tx, ty),则返回 true,否则返回 false。
从点 (x, y) 可以转换到 (x, x+y) 或者 (x+y, y)。

示例 1

  • 输入:sx = 1, sy = 1, tx = 3, ty = 5
  • 输出:true
  • 解释:可以通过以下一系列转换从起点转换到终点:
    (1, 1) -> (1, 2) -> (3, 2) -> (3, 5)

示例 2

  • 输入:sx = 1, sy = 1, tx = 2, ty = 2
  • 输出:false

示例 3

  • 输入:sx = 1, sy = 1, tx = 1, ty = 1
  • 输出:true
    提示:
  • 1 <= sx, sy, tx, ty <= 10^9

解题思路

正推(加法)是爆炸式的,需反向(减法/取模)思考:(tx, ty) 的上一状态只可能是
(tx - ty, ty)(若 tx > ty)或 (tx, ty - tx)(若 ty > tx)——即把较大坐标减去较小坐标。
解法一(递归 + 取模加速,主提交):当 tx > ty 时反复令 tx -= ty,若 ty == sy 则可直接用
取模一次算到底:(tx - sx) % ty == 0 即说明终可通过若干次减法到达;ty > tx 对称处理。
解法二(朴素反向减法):每步把较大的坐标减去较小的坐标,直到两边都 <= 起点,再判断是否相等。
正确但大数据下较慢,适合作为对照理解。

代码实现

class Solution {

    // 解法一:反向 + 取模加速
    public boolean reachingPoints(int sx, int sy, int tx, int ty) {
        while (tx >= sx && ty >= sy) {
            if (tx == sx && ty == sy) {
                return true;
            }
            if (tx > ty) {
                if (ty == sy) {
                    // 只剩横向减法,一次算到底
                    return (tx - sx) % ty == 0;
                }
                tx %= ty;
            } else {
                if (tx == sx) {
                    return (ty - sy) % tx == 0;
                }
                ty %= tx;
            }
        }
        return false;
    }

    // 解法二:朴素反向减法(对照)
    public boolean reachingPoints2(int sx, int sy, int tx, int ty) {
        while (tx > sx || ty > sy) {
            if (tx > ty) {
                tx -= ty;
            } else {
                ty -= tx;
            }
            if (tx < sx || ty < sy) {
                return false;
            }
            if (tx <= 0 || ty <= 0) {
                return false;
            }
        }
        return tx == sx && ty == sy;
    }

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

        ++total; if (sol.reachingPoints(1, 1, 3, 5)) ++pass; else System.out.println("FAIL 780-1");
        ++total; if (!sol.reachingPoints(1, 1, 2, 2)) ++pass; else System.out.println("FAIL 780-2");
        ++total; if (sol.reachingPoints(1, 1, 1, 1)) ++pass; else System.out.println("FAIL 780-3");
        ++total; if (!sol.reachingPoints(1, 2, 2, 2)) ++pass; else System.out.println("FAIL 780-4");
        ++total; if (!sol.reachingPoints(2, 3, 4, 3)) ++pass; else System.out.println("FAIL 780-5");
        ++total; if (sol.reachingPoints(3, 3, 3, 3)) ++pass; else System.out.println("FAIL 780-6");
        ++total; if (sol.reachingPoints(1, 1, 1000000000, 1)) ++pass; else System.out.println("FAIL 780-7");

        ++total; if (!sol.reachingPoints2(1, 1, 2, 2)) ++pass; else System.out.println("FAIL 780-8");
        ++total; if (!sol.reachingPoints2(2, 3, 4, 3)) ++pass; else System.out.println("FAIL 780-9");
        ++total; if (sol.reachingPoints2(1, 1, 10, 9)) ++pass; else System.out.println("FAIL 780-10");
        ++total; if (!sol.reachingPoints2(1, 2, 2, 1)) ++pass; else System.out.println("FAIL 780-11");

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

复杂度分析

解法时间复杂度空间复杂度
解法一O(log max(tx, ty))O(1)
解法二O(max(tx, ty) / min)O(1)

781. Rabbits in Forest(森林中的兔子)

题目描述

森林中有未知数量的兔子。提问其中若干只兔子 "还有多少只其它兔子与你(指被提问的兔子)颜色相同?"
将答案收集到一个整数数组 answers 中,其中 answers[i] 是第 i 只兔子的回答。
给你数组 answers,返回森林中兔子的最少数量。

示例 1

  • 输入:answers = [1,1,2]
  • 输出:5
  • 解释:两只回答了 "1" 的兔子可能有相同的颜色,设为红色。
    之后回答了 "2" 的兔子不会是红色,否则他们的回答会相互矛盾。
    设回答了 "2" 的兔子为蓝色。此外,森林中还应有另外 2 只蓝色兔子的回答没有包含在数组中。
    因此森林中兔子的最少数量是 5 只:3 只回答的和 2 只没有回答的。

示例 2

  • 输入:answers = [10,10,10]
  • 输出:11
    提示:
  • 1 <= answers.length <= 1000
  • 0 <= answers[i] < 1000

解题思路

若一只兔子回答 x,说明该颜色兔子总数至多为 x + 1 只(另有 x 只同类)。
回答同为 x 的兔子最多只有 x + 1 只能归为同一颜色(超过则必须另起一组)。
解法一(哈希表贪心计数,主提交):用 map 记录每个 x 已归入组中的兔子数(累计),
遇新兔子时若 x + 1 已满则开新组并把总数加上。更简单的方式:统计每个答案的出现次数
cnt[x],该答案需要 ceil(cnt[x] / (x+1)) 组,每组 x+1 只。
解法二(频率数组累加):因为 0 <= answers[i] < 1000,用数组计数后按组数公式求和。

代码实现

import java.util.HashMap;
import java.util.Map;

class Solution {

    // 解法一:哈希表计数 + 组数公式
    public int numRabbits(int[] answers) {
        Map<Integer, Integer> cnt = new HashMap<>();
        for (int a : answers) {
            cnt.put(a, cnt.getOrDefault(a, 0) + 1);
        }
        int ans = 0;
        for (Map.Entry<Integer, Integer> e : cnt.entrySet()) {
            int x = e.getKey();
            int c = e.getValue();
            int groupSize = x + 1;      // 每组最多 x+1 只
            int groups = (c + groupSize - 1) / groupSize; // 向上取整
            ans += groups * groupSize;
        }
        return ans;
    }

    // 解法二:频率数组
    public int numRabbits2(int[] answers) {
        int[] cnt = new int[1000];
        for (int a : answers) {
            cnt[a]++;
        }
        int ans = 0;
        for (int x = 0; x < 1000; ++x) {
            if (cnt[x] == 0) {
                continue;
            }
            int groupSize = x + 1;
            int groups = (cnt[x] + groupSize - 1) / groupSize;
            ans += groups * groupSize;
        }
        return ans;
    }

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

        ++total; if (sol.numRabbits(new int[] {1, 1, 2}) == 5) ++pass; else System.out.println("FAIL 781-1");
        ++total; if (sol.numRabbits(new int[] {10, 10, 10}) == 11) ++pass; else System.out.println("FAIL 781-2");
        ++total; if (sol.numRabbits(new int[] {0, 0, 1, 1, 1}) == 6) ++pass; else System.out.println("FAIL 781-3");
        ++total; if (sol.numRabbits(new int[] {0}) == 1) ++pass; else System.out.println("FAIL 781-4");
        ++total; if (sol.numRabbits(new int[] {2, 2, 2, 2, 2}) == 6) ++pass; else System.out.println("FAIL 781-5");

        ++total; if (sol.numRabbits2(new int[] {1, 1, 2}) == 5) ++pass; else System.out.println("FAIL 781-6");
        ++total; if (sol.numRabbits2(new int[] {10, 10, 10}) == 11) ++pass; else System.out.println("FAIL 781-7");
        ++total; if (sol.numRabbits2(new int[] {2, 2, 2, 2, 2}) == 6) ++pass; else System.out.println("FAIL 781-8");
        ++total; if (sol.numRabbits2(new int[] {0, 0, 1, 1, 1}) == 6) ++pass; else System.out.println("FAIL 781-9");

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

复杂度分析

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

782. Transform to Chessboard(变为棋盘)

题目描述

一个 n x n 的二维网络 board 仅由 0 和 1 组成。每次移动,你能交换任意两列或是两行的位置。
返回将这个矩阵变为 "棋盘" 所需的最小移动次数。如果不存在可行的变换,输出 -1。
"棋盘" 是指任意一格的上下左右四个方向的值均与本身不同的矩阵。

示例 1

  • 输入:board = [[0,1,1,0],[0,1,1,0],[1,0,0,1],[1,0,0,1]]
  • 输出:2
  • 解释:一种可行的变换方式如下,从左到右:
    第一次移动交换了第一列和第二列。第二次移动交换了第二行和第三行。

示例 2

  • 输入:board = [[0,1],[1,0]]
  • 输出:0

示例 3

  • 输入:board = [[1,0],[1,0]]
  • 输出:-1
    提示:
  • n == board.length
  • n == board[i].length
  • 2 <= n <= 30
  • board[i][j] 将只包含 0 或 1

解题思路

要能变成棋盘,矩阵必须满足两个充分必要条件:

  1. 所有行要么与第 0 行完全相同,要么与第 0 行完全相反(这样才能只需交换列得到棋盘);
  2. 第 0 行中 0 与 1 的个数差不超过 1(n 为偶数时相等,n 为奇数时相差 1),
    且第 0 列也满足同样的条件(行、列角色对称)。
    之后把行/列分别各自重排:目标列交错模式有两种(以 0 开头或以 1 开头),统计当前序列与
    目标模式的失配次数,交换次数 = 失配数 / 2,取两种模式中的较小值。行与列的结果相加。
    解法一(如上,主提交):先校验再分别统计行、列的最小交换次数。
    解法二(对照暴力):小规模 n(<=8)直接 BFS 状态搜索验证,作为正确性对照。

代码实现

import java.util.HashMap;
import java.util.Map;

class Solution {

    // 解法一:棋盘性质 + 分别统计行/列交换
    public int movesToChessboard(int[][] board) {
        int n = board.length;
        // 1. 校验:每一行必须等于第 0 行或其反相
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < n; ++j) {
                if (board[i][j] != (board[0][j] ^ board[i][0] ^ board[0][0])) {
                    return -1;
                }
            }
        }
        // 2. 无论怎么交换行/列,每行每列中 0、1 的个数必须满足棋盘约束
        int rowOne = 0, colOne = 0;
        for (int i = 0; i < n; ++i) {
            rowOne += board[0][i]; // 第 0 行的 1 个数(所有行都一样)
            colOne += board[i][0]; // 第 0 列的 1 个数
        }
        if (Math.abs(rowOne * 2 - n) > 1 || Math.abs(colOne * 2 - n) > 1) {
            return -1;
        }
        // 3. 计算使第 0 行变为标准棋盘序列所需的最小交换次数
        int rowSwaps = minSwaps(board[0], n);
        // 4. 计算使第 0 列变为标准棋盘序列所需的最小交换次数
        int[] col = new int[n];
        for (int i = 0; i < n; ++i) {
            col[i] = board[i][0];
        }
        int colSwaps = minSwaps(col, n);
        return rowSwaps + colSwaps;
    }

    // 将长度为 n 的 0/1 序列变成 0101... 或 1010... 的最小交换次数(一次交换两列/两行)
    private int minSwaps(int[] seq, int n) {
        int costStart0 = 0; // 目标 0,1,0,1...
        int costStart1 = 0; // 目标 1,0,1,0...
        for (int i = 0; i < n; ++i) {
            if (seq[i] != (i & 1)) {
                costStart0++;
            }
            if (seq[i] != (1 - (i & 1))) {
                costStart1++;
            }
        }
        if ((n & 1) == 0) {
            return Math.min(costStart0, costStart1) / 2;
        }
        // n 为奇数时,只能选 1 的个数更少的模式(奇数长度下 1 多的那个必然无法一次抵对)
        return (costStart0 & 1) == 0 ? costStart0 / 2 : costStart1 / 2;
    }

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

        int[][] b1 = {{0, 1, 1, 0}, {0, 1, 1, 0}, {1, 0, 0, 1}, {1, 0, 0, 1}};
        ++total; if (sol.movesToChessboard(b1) == 2) ++pass; else System.out.println("FAIL 782-1 got " + sol.movesToChessboard(b1));
        int[][] b2 = {{0, 1}, {1, 0}};
        ++total; if (sol.movesToChessboard(b2) == 0) ++pass; else System.out.println("FAIL 782-2 got " + sol.movesToChessboard(b2));
        int[][] b3 = {{1, 0}, {1, 0}};
        ++total; if (sol.movesToChessboard(b3) == -1) ++pass; else System.out.println("FAIL 782-3 got " + sol.movesToChessboard(b3));
        int[][] b4 = {{0, 1, 0}, {1, 0, 1}, {0, 1, 0}};
        ++total; if (sol.movesToChessboard(b4) == 0) ++pass; else System.out.println("FAIL 782-4 got " + sol.movesToChessboard(b4));
        int[][] b5 = {{0, 0, 1}, {1, 1, 0}, {1, 1, 0}};
        ++total; if (sol.movesToChessboard(b5) == 2) ++pass; else System.out.println("FAIL 782-5 got " + sol.movesToChessboard(b5));
        int[][] b6 = {{0, 1, 0, 0}, {1, 0, 1, 1}, {0, 1, 0, 0}, {1, 0, 1, 1}};
        ++total; if (sol.movesToChessboard(b6) == -1) ++pass; else System.out.println("FAIL 782-6 got " + sol.movesToChessboard(b6));

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

复杂度分析

解法时间复杂度空间复杂度
解法一O(n^2)O(n)
解法二O(n! * n^2)(仅小规模对照)O(n!)

783. Minimum Distance Between BST Nodes(二叉搜索树节点最小距离)

题目描述

给你一个二叉搜索树的根节点 root,返回树中任意两不同节点值之间的最小差值。
差值是一个正数,其数值等于两值之差的绝对值。

示例 1

  • 输入:root = [4,2,6,1,3]
  • 输出:1

示例 2

  • 输入:root = [1,0,48,null,null,12,49]
  • 输出:1
    提示:
  • 树中节点的数目范围是 [2, 100]
  • 0 <= Node.val <= 10^5
  • 注意:本题与 530 相同

解题思路

二叉搜索树中序遍历得到严格递增序列,最小差值一定出现在相邻两个元素之间。
解法一(中序遍历 + 维护前驱,主提交):递归中序遍历,记录上一个访问值,用当前值减
前驱更新最小差。
解法二(迭代中序遍历):用显式栈模拟中序遍历,逻辑等价、空间 O(h)。

代码实现

import java.util.ArrayDeque;
import java.util.Deque;

class Solution {

    private static class TreeNode {
        int val;
        TreeNode left;
        TreeNode right;
        TreeNode(int val) { this.val = val; }
    }

    private int prev;
    private int best;

    // 解法一:递归中序遍历
    public int minDiffInBST(TreeNode root) {
        prev = -1;
        best = Integer.MAX_VALUE;
        dfs(root);
        return best;
    }

    private void dfs(TreeNode node) {
        if (node == null) {
            return;
        }
        dfs(node.left);
        if (prev >= 0) {
            best = Math.min(best, node.val - prev);
        }
        prev = node.val;
        dfs(node.right);
    }

    // 解法二:迭代中序遍历
    public int minDiffInBST2(TreeNode root) {
        int prevVal = -1;
        int bestVal = Integer.MAX_VALUE;
        Deque<TreeNode> stack = new ArrayDeque<>();
        TreeNode cur = root;
        while (cur != null || !stack.isEmpty()) {
            while (cur != null) {
                stack.push(cur);
                cur = cur.left;
            }
            cur = stack.pop();
            if (prevVal >= 0) {
                bestVal = Math.min(bestVal, cur.val - prevVal);
            }
            prevVal = cur.val;
            cur = cur.right;
        }
        return bestVal;
    }

    // ===== 本地测试 =====
    private static TreeNode fromArray(Integer[] a) {
        if (a == null || a.length == 0) {
            return null;
        }
        TreeNode root = new TreeNode(a[0]);
        Deque<TreeNode> q = new ArrayDeque<>();
        q.offer(root);
        int i = 1;
        while (!q.isEmpty() && i < a.length) {
            TreeNode cur = q.poll();
            if (a[i] != null) {
                cur.left = new TreeNode(a[i]);
                q.offer(cur.left);
            }
            ++i;
            if (i < a.length && a[i] != null) {
                cur.right = new TreeNode(a[i]);
                q.offer(cur.right);
            }
            ++i;
        }
        return root;
    }

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

        TreeNode t1 = fromArray(new Integer[] {4, 2, 6, 1, 3});
        ++total; if (sol.minDiffInBST(t1) == 1) ++pass; else System.out.println("FAIL 783-1");
        TreeNode t2 = fromArray(new Integer[] {1, 0, 48, null, null, 12, 49});
        ++total; if (sol.minDiffInBST(t2) == 1) ++pass; else System.out.println("FAIL 783-2");
        TreeNode t3 = fromArray(new Integer[] {90, 69, null, 49, 89, null, 52});
        ++total; if (sol.minDiffInBST(t3) == 1) ++pass; else System.out.println("FAIL 783-3");

        TreeNode u1 = fromArray(new Integer[] {4, 2, 6, 1, 3});
        ++total; if (sol.minDiffInBST2(u1) == 1) ++pass; else System.out.println("FAIL 783-4");
        TreeNode u2 = fromArray(new Integer[] {1, 0, 48, null, null, 12, 49});
        ++total; if (sol.minDiffInBST2(u2) == 1) ++pass; else System.out.println("FAIL 783-5");
        TreeNode u3 = fromArray(new Integer[] {56, 42, 71, 28, 49, 63, 84, null, null, null, null, 60, 66});
        ++total; if (sol.minDiffInBST2(u3) == 3) ++pass; else System.out.println("FAIL 783-6");

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

复杂度分析

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

784. Letter Case Permutation(字母大小写全排列)

题目描述

给定一个字符串 s,通过将字符串 s 中的每个字母转变大小写,我们可以获得一个新的字符串。
返回所有可能得到的字符串集合。以任意顺序返回输出。

示例 1

  • 输入:s = "a1b2"
  • 输出:["a1b2", "a1B2", "A1b2", "A1B2"]

示例 2

  • 输入:s = "3z4"
  • 输出:["3z4","3Z4"]
    提示:
  • 1 <= s.length <= 12
  • s 由小写英文字母、大写英文字母和数字组成

解题思路

每个字母有两种形态(原样或切换大小写),数字只有一种,总组合数 <= 2^12。
解法一(回溯 DFS,主提交):从左到右处理,遇字母分两支(小写/大写),数字直接追加深搜。
解法二(位掩码枚举):先收集所有字母的位置,共有 m 个字母,枚举 2^m 种掩码,
掩码第 i 位为 1 表示第 i 个字母用大写,为 0 用小写。

代码实现

import java.util.ArrayList;
import java.util.List;

class Solution {

    private List<String> ans;
    private char[] chars;

    // 解法一:回溯 DFS
    public List<String> letterCasePermutation(String s) {
        ans = new ArrayList<>();
        chars = s.toCharArray();
        dfs(0);
        return ans;
    }

    private void dfs(int idx) {
        if (idx == chars.length) {
            ans.add(new String(chars));
            return;
        }
        char c = chars[idx];
        if (Character.isLetter(c)) {
            chars[idx] = Character.toLowerCase(c);
            dfs(idx + 1);
            chars[idx] = Character.toUpperCase(c);
            dfs(idx + 1);
            chars[idx] = c; // 回溯还原
        } else {
            dfs(idx + 1);
        }
    }

    // 解法二:位掩码枚举
    public List<String> letterCasePermutation2(String s) {
        List<String> res = new ArrayList<>();
        List<Integer> pos = new ArrayList<>();
        StringBuilder base = new StringBuilder();
        for (int i = 0; i < s.length(); ++i) {
            char c = s.charAt(i);
            if (Character.isLetter(c)) {
                pos.add(i);
                base.append(Character.toLowerCase(c));
            } else {
                base.append(c);
            }
        }
        int m = pos.size();
        for (int mask = 0; mask < (1 << m); ++mask) {
            char[] arr = base.toString().toCharArray();
            for (int b = 0; b < m; ++b) {
                if ((mask & (1 << b)) != 0) {
                    arr[pos.get(b)] = Character.toUpperCase(arr[pos.get(b)]);
                }
            }
            res.add(new String(arr));
        }
        return res;
    }

    private static boolean sameSet(List<String> a, List<String> b) {
        if (a.size() != b.size()) {
            return false;
        }
        java.util.Set<String> set = new java.util.HashSet<>(a);
        for (String s : b) {
            if (!set.contains(s)) {
                return false;
            }
        }
        return true;
    }

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

        List<String> r1 = sol.letterCasePermutation("a1b2");
        ++total; if (r1.size() == 4 && r1.contains("a1b2") && r1.contains("a1B2") && r1.contains("A1b2") && r1.contains("A1B2"))
            ++pass; else System.out.println("FAIL 784-1 " + r1);

        List<String> r2 = sol.letterCasePermutation("3z4");
        ++total; if (r2.size() == 2 && r2.contains("3z4") && r2.contains("3Z4")) ++pass; else System.out.println("FAIL 784-2 " + r2);

        List<String> r3 = sol.letterCasePermutation("12345");
        ++total; if (r3.size() == 1 && r3.get(0).equals("12345")) ++pass; else System.out.println("FAIL 784-3");

        List<String> r4 = sol.letterCasePermutation2("a1b2");
        ++total; if (sameSet(sol.letterCasePermutation2("a1b2"), sol.letterCasePermutation("a1b2"))) ++pass; else System.out.println("FAIL 784-4");
        ++total; if (sameSet(sol.letterCasePermutation2("3z4"), sol.letterCasePermutation("3z4"))) ++pass; else System.out.println("FAIL 784-5");
        ++total; if (r4.size() == 4) ++pass; else System.out.println("FAIL 784-6");
        List<String> r5 = sol.letterCasePermutation2("C");
        ++total; if (r5.size() == 2 && r5.contains("C") && r5.contains("c")) ++pass; else System.out.println("FAIL 784-7 " + r5);

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

复杂度分析

解法时间复杂度空间复杂度
解法一O(2^m * n)O(n)(递归深度)+ O(2^m * n)(答案)
解法二O(2^m * n)O(m) + O(2^m * n)(答案)

785. Is Graph Bipartite(判断二分图)

题目描述

存在一个无向图,图中有 n 个节点。其中每个节点都有一个介于 0 到 n-1 之间的唯一编号。
给你一个二维数组 graph,其中 graph[u] 是一个节点数组,由节点 u 的邻接节点组成。
该无向图同时具有以下属性:

  • 不存在自环,不存在平行边
  • 如果 v 在 graph[u] 内,那么 u 也应该在 graph[v] 内(该图是无向图)
  • 这个图可能不是连通图
    二分图定义:如果能将一个图的节点集合分割成两个独立的子集 A 和 B,并使图中的每一条边
    的两个节点一个来自 A 集合,一个来自 B 集合,就将这个图称为二分图。
    如果图是二分图,返回 true;否则,返回 false。

示例 1

  • 输入:graph = [[1,2,3],[0,2],[0,1,3],[0,2]]
  • 输出:false

示例 2

  • 输入:graph = [[1,3],[0,2],[1,3],[0,2]]
  • 输出:true
    提示:
  • graph.length == n
  • 1 <= n <= 100

解题思路

染色法:给每个节点染色(0/1 两色),相邻节点必须异色;若出现同色相邻则不是二分图。
图可能不连通,需要对每个未染色节点启动一次遍历。
解法一(BFS 染色,主提交):队列逐层染色扩散。
解法二(DFS 染色 / 并查集):DFS 递归染色等价;并查集则把每个节点的所有邻居两两合并,
若某邻居与节点本身同属一个集合则矛盾。

代码实现

import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Arrays;

class Solution {

    // 解法一:BFS 染色
    public boolean isBipartite(int[][] graph) {
        int n = graph.length;
        int[] color = new int[n]; // 0 未染色,1/-1 两种颜色
        for (int i = 0; i < n; ++i) {
            if (color[i] != 0) {
                continue;
            }
            color[i] = 1;
            Deque<Integer> q = new ArrayDeque<>();
            q.offer(i);
            while (!q.isEmpty()) {
                int cur = q.poll();
                for (int nb : graph[cur]) {
                    if (color[nb] == 0) {
                        color[nb] = -color[cur];
                        q.offer(nb);
                    } else if (color[nb] == color[cur]) {
                        return false;
                    }
                }
            }
        }
        return true;
    }

    // 解法二:DFS 染色
    public boolean isBipartite2(int[][] graph) {
        int n = graph.length;
        int[] color = new int[n];
        for (int i = 0; i < n; ++i) {
            if (color[i] == 0 && !dfs(i, 1, color, graph)) {
                return false;
            }
        }
        return true;
    }

    private boolean dfs(int u, int c, int[] color, int[][] graph) {
        if (color[u] != 0) {
            return color[u] == c;
        }
        color[u] = c;
        for (int v : graph[u]) {
            if (!dfs(v, -c, color, graph)) {
                return false;
            }
        }
        return true;
    }

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

        int[][] g1 = {{1, 2, 3}, {0, 2}, {0, 1, 3}, {0, 2}};
        ++total; if (!sol.isBipartite(g1)) ++pass; else System.out.println("FAIL 785-1");
        int[][] g2 = {{1, 3}, {0, 2}, {1, 3}, {0, 2}};
        ++total; if (sol.isBipartite(g2)) ++pass; else System.out.println("FAIL 785-2");
        int[][] g3 = {{}, {2, 4, 6}, {1, 4, 8, 9}, {7, 8}, {1, 2, 8, 9}, {6, 9}, {1, 5, 7, 8, 9}, {3, 6, 9}, {2, 3, 4, 6, 9}, {2, 4, 5, 6, 7, 8}};
        ++total; if (!sol.isBipartite(g3)) ++pass; else System.out.println("FAIL 785-3");
        int[][] g4 = {{}, {3}, {}, {1}, {}};
        ++total; if (sol.isBipartite(g4)) ++pass; else System.out.println("FAIL 785-4");

        ++total; if (!sol.isBipartite2(g1)) ++pass; else System.out.println("FAIL 785-5");
        ++total; if (sol.isBipartite2(g2)) ++pass; else System.out.println("FAIL 785-6");
        ++total; if (!sol.isBipartite2(g3)) ++pass; else System.out.println("FAIL 785-7");
        ++total; if (sol.isBipartite2(g4)) ++pass; else System.out.println("FAIL 785-8");

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

复杂度分析

解法时间复杂度空间复杂度
解法一O(n + e)O(n)
解法二O(n + e) / O(n alpha)O(n)

786. K-th Smallest Prime Fraction(第K个最小的质数分数)

题目描述

给你一个按递增顺序排序的数组 arr 和一个整数 k。数组 arr 由 1 和若干质数组成,
且其中所有整数互不相同。
对于每对满足 0 <= i < j < arr.length 的 i 和 j,可以得到分数 arr[i] / arr[j]。
那么第 k 个最小的分数是多少呢?以长度为 2 的整数数组返回你的答案,这里
answer[0] == arr[i] 且 answer[1] == arr[j]。

示例 1

  • 输入:arr = [1,2,3,5], k = 3
  • 输出:[2,5]
  • 解释:已构造好的分数,排序后如下所示:
    1/5, 1/3, 2/5, 1/2, 3/5, 2/3
    很明显第三个最小的分数是 2/5

示例 2

  • 输入:arr = [1,7], k = 1
  • 输出:[1,7]
    提示:
  • 2 <= arr.length <= 1000
  • 1 <= arr[i] <= 3 * 10^4
  • arr[0] == 1
  • arr[i] 是一个质数,i > 0
  • 1 <= k <= arr.length * (arr.length - 1) / 2
  • 进阶:设计并实现时间复杂度小于 O(n^2) 的算法解决此问题

解题思路

所有分数可组织成「每行 j 固定、i 增大则分数增大」的阶梯结构:对每个分母 arr[j],
分子 arr[i](i < j)越小分数越小,即每一列是单调递增序列。
解法一(优先队列归并,主提交):把每个分母 j 对应的最小分数 arr[0]/arr[j] 入堆,
每次弹出最小的,若该分母还能取下一个分子(i+1 < j),把 arr[i+1]/arr[j] 入堆。
弹出 k 次即得答案。时间 O(k log n),n <= 1000,满足进阶要求。
解法二(全量枚举排序):n <= 1000,最多约 5e5 个分数,全部枚举后按分数值排序取第 k 个
(对照实现,思路最直白,O(n^2 log n) 在本规模下可行)。

代码实现

import java.util.ArrayList;
import java.util.List;
import java.util.PriorityQueue;

class Solution {

    // 解法一:优先队列归并
    public int[] kthSmallestPrimeFraction(int[] arr, int k) {
        int n = arr.length;
        // 堆元素:[arr[i], arr[j], i, j]
        PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) ->
                Long.compare((long) a[0] * b[1], (long) b[0] * a[1]));
        for (int j = 1; j < n; ++j) {
            pq.offer(new int[] {arr[0], arr[j], 0, j});
        }
        for (int t = 1; t < k; ++t) {
            int[] cur = pq.poll();
            int i = cur[2];
            if (i + 1 < cur[3]) {
                pq.offer(new int[] {arr[i + 1], arr[cur[3]], i + 1, cur[3]});
            }
        }
        int[] top = pq.peek();
        return new int[] {top[0], top[1]};
    }

    // 解法二:全量枚举排序
    public int[] kthSmallestPrimeFraction2(int[] arr, int k) {
        int n = arr.length;
        List<int[]> list = new java.util.ArrayList<>();
        for (int i = 0; i < n; ++i) {
            for (int j = i + 1; j < n; ++j) {
                list.add(new int[] {arr[i], arr[j]});
            }
        }
        java.util.Collections.sort(list, (a, b) ->
                Long.compare((long) a[0] * b[1], (long) b[0] * a[1]));
        return list.get(k - 1);
    }

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

        int[] r1 = sol.kthSmallestPrimeFraction(new int[] {1, 2, 3, 5}, 3);
        ++total; if (r1[0] == 2 && r1[1] == 5) ++pass; else System.out.println("FAIL 786-1 " + r1[0] + "/" + r1[1]);
        int[] r2 = sol.kthSmallestPrimeFraction(new int[] {1, 7}, 1);
        ++total; if (r2[0] == 1 && r2[1] == 7) ++pass; else System.out.println("FAIL 786-2 " + r2[0] + "/" + r2[1]);
        int[] r3 = sol.kthSmallestPrimeFraction(new int[] {1, 2, 3, 5}, 6);
        ++total; if (r3[0] == 2 && r3[1] == 3) ++pass; else System.out.println("FAIL 786-3 " + r3[0] + "/" + r3[1]);
        int[] r4 = sol.kthSmallestPrimeFraction(new int[] {1, 2, 3, 5}, 1);
        ++total; if (r4[0] == 1 && r4[1] == 5) ++pass; else System.out.println("FAIL 786-4 " + r4[0] + "/" + r4[1]);

        int[] s1 = sol.kthSmallestPrimeFraction2(new int[] {1, 2, 3, 5}, 3);
        ++total; if (s1[0] == 2 && s1[1] == 5) ++pass; else System.out.println("FAIL 786-5 " + s1[0] + "/" + s1[1]);
        int[] s2 = sol.kthSmallestPrimeFraction2(new int[] {1, 2, 3, 5}, 6);
        ++total; if (s2[0] == 2 && s2[1] == 3) ++pass; else System.out.println("FAIL 786-6 " + s2[0] + "/" + s2[1]);
        int[] s3 = sol.kthSmallestPrimeFraction2(new int[] {1, 7}, 1);
        ++total; if (s3[0] == 1 && s3[1] == 7) ++pass; else System.out.println("FAIL 786-7 " + s3[0] + "/" + s3[1]);

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

复杂度分析

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

787. Cheapest Flights Within K Stops(K站中转内最便宜的航班)

题目描述

有 n 个城市通过一些航班连接。给你一个数组 flights,其中 flights[i] = [from_i, to_i, price_i], 表示该航班都从城市 from_i 开始,以价格 price_i 抵达 to_i。 现在给定所有的城市和航班,以及出发城市 src 和目的地 dst,你的任务是找到出一条 最多经过 k 站中转的路线,使得从 src 到 dst 的价格最便宜,并返回该价格。 如果不存在这样的路线,则输出 -1。

示例 1

  • 输入:n = 4, flights = [[0,1,100],[1,2,100],[2,0,100],[1,3,600],[2,3,200]], src = 0, dst = 3, k = 1
  • 输出:700

示例 2

  • 输入:n = 3, flights = [[0,1,100],[1,2,100],[0,2,500]], src = 0, dst = 2, k = 1
  • 输出:200

示例 3

  • 输入:n = 3, flights = [[0,1,100],[1,2,100],[0,2,500]], src = 0, dst = 2, k = 0
  • 输出:500 提示:
  • 2 <= n <= 100
  • 0 <= src, dst, k < n
  • src != dst
  • 1 <= price_i <= 10^4

解题思路

带「最多 k 站中转」限制的最短路,不能用普通 Dijkstra 的永久标记,因为绕路也可能有 更少的边数(中转数)以达到更优的后继状态。 解法一(Bellman-Ford 分层松弛,主提交):dp[t][v] 表示经过至多 t 次飞行到达 v 的 最小花费。外层循环 n 次,每次用上一层的 dp 数组对每条边松弛,最终取 dp[k+1][dst]。 解法二(BFS 分层 DP):按站数逐层扩展,记录每层到达各站的最小花费,层数 <= k+1。 也可视为 SPFA 的思路;这里实现为「滚动数组 + 按层松弛」。

代码实现

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.PriorityQueue;

class Solution {

    // 解法一:Bellman-Ford 分层松弛(滚动数组)
    public int findCheapestPrice(int n, int[][] flights, int src, int dst, int k) {
        int INF = Integer.MAX_VALUE / 2;
        int[] dp = new int[n];
        Arrays.fill(dp, INF);
        dp[src] = 0;
        // 最多 k+1 次飞行(k 站中转 = k+1 条边)
        for (int step = 0; step <= k; ++step) {
            int[] next = Arrays.copyOf(dp, n);
            for (int[] f : flights) {
                int u = f[0], v = f[1], w = f[2];
                if (dp[u] + w < next[v]) {
                    next[v] = dp[u] + w;
                }
            }
            dp = next;
        }
        return dp[dst] >= INF ? -1 : dp[dst];
    }

    // 解法二:按层扩展(显式队列,逐层松弛)
    public int findCheapestPrice2(int n, int[][] flights, int src, int dst, int k) {
        List<int[]>[] g = buildGraph(n, flights);
        int INF = Integer.MAX_VALUE / 2;
        int[] best = new int[n];
        Arrays.fill(best, INF);
        best[src] = 0;
        List<Integer> layer = new ArrayList<>();
        layer.add(src);
        int stops = 0;
        while (!layer.isEmpty() && stops <= k) {
            List<Integer> nextLayer = new ArrayList<>();
            int[] layerBest = new int[n];
            Arrays.fill(layerBest, INF);
            for (int u : layer) {
                for (int[] e : g[u]) {
                    int v = e[0], w = e[1];
                    if (best[u] + w < layerBest[v]) {
                        if (layerBest[v] == INF) {
                            nextLayer.add(v);
                        }
                        layerBest[v] = best[u] + w;
                    }
                }
            }
            for (int v : nextLayer) {
                best[v] = Math.min(best[v], layerBest[v]);
            }
            layer = nextLayer;
            ++stops;
        }
        return best[dst] >= INF ? -1 : best[dst];
    }

    private List<int[]>[] buildGraph(int n, int[][] flights) {
        List<int[]>[] g = new List[n];
        for (int i = 0; i < n; ++i) {
            g[i] = new ArrayList<>();
        }
        for (int[] f : flights) {
            g[f[0]].add(new int[] {f[1], f[2]});
        }
        return g;
    }

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

        int[][] f1 = {{0, 1, 100}, {1, 2, 100}, {2, 0, 100}, {1, 3, 600}, {2, 3, 200}};
        ++total; if (sol.findCheapestPrice(4, f1, 0, 3, 1) == 700) ++pass; else System.out.println("FAIL 787-1");
        int[][] f2 = {{0, 1, 100}, {1, 2, 100}, {0, 2, 500}};
        ++total; if (sol.findCheapestPrice(3, f2, 0, 2, 1) == 200) ++pass; else System.out.println("FAIL 787-2");
        ++total; if (sol.findCheapestPrice(3, f2, 0, 2, 0) == 500) ++pass; else System.out.println("FAIL 787-3");
        int[][] f3 = {{0, 1, 10}, {1, 2, 10}, {2, 3, 10}, {0, 3, 100}};
        ++total; if (sol.findCheapestPrice(4, f3, 0, 3, 1) == 100) ++pass; else System.out.println("FAIL 787-4");
        ++total; if (sol.findCheapestPrice(4, f3, 0, 3, 2) == 30) ++pass; else System.out.println("FAIL 787-5");
        ++total; if (sol.findCheapestPrice(4, f3, 0, 3, 0) == 100) ++pass; else System.out.println("FAIL 787-6");

        ++total; if (sol.findCheapestPrice2(4, f1, 0, 3, 1) == 700) ++pass; else System.out.println("FAIL 787-7");
        ++total; if (sol.findCheapestPrice2(3, f2, 0, 2, 1) == 200) ++pass; else System.out.println("FAIL 787-8");
        ++total; if (sol.findCheapestPrice2(3, f2, 0, 2, 0) == 500) ++pass; else System.out.println("FAIL 787-9");
        ++total; if (sol.findCheapestPrice2(4, f3, 0, 3, 1) == 100) ++pass; else System.out.println("FAIL 787-10");
        ++total; if (sol.findCheapestPrice2(4, f3, 0, 3, 2) == 30) ++pass; else System.out.println("FAIL 787-11");
        ++total; if (sol.findCheapestPrice2(4, f3, 0, 3, 0) == 100) ++pass; else System.out.println("FAIL 787-12");

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

复杂度分析

解法时间复杂度空间复杂度
解法一O(k * e)O(n)
解法二O(k * e)O(n)(每层)

788. Rotated Digits(旋转数字)

题目描述

我们称一个数 X 为好数,如果它的每位数字逐个地被旋转 180 度后,我们仍可以得到一个 有效的,且和 X 不同的数。要求每位数字都要被旋转。 如果一个数的每位数字被旋转以后仍然还是一个数字,则这个数是有效的。0、1 和 8 被旋转后 仍然是它们自己;2 和 5 可以互相旋转成对方;6 和 9 同理,除了这些以外其他的数字旋转以后 都不再是有效的数字。 现在我们有一个正整数 N,计算从 1 到 N 中有多少个数 X 是好数?

示例

  • 输入:10
  • 输出:4
  • 解释:在 [1, 10] 中有四个好数:2, 5, 6, 9。 注意 1 和 10 不是好数,因为他们在旋转之后不变。 提示:
  • N 的取值范围是 [1, 10000]

解题思路

「好数」= 每一位都只含 {0,1,2,5,6,8,9}(保证旋转有效)且至少含一个 {2,5,6,9} (保证旋转后与自身不同)。含 3、4、7 的数直接不合法。 解法一(逐数检查,主提交):对 1..N 每个数逐位判断,合法且必变的计数。 解法二(三态数字 DP):把好数判定做成状态机(0-不合法 / 1-纯镜像 / 2-好数), 用数位 DP 或直接打表递推:dp[i] 记录 0..9 与 i 的映射后的状态。因为 N <= 10000, 也可直接遍历数字化判;此处实现一个基于字符替换的判定对照。

代码实现

class Solution {

    // 解法一:逐数检查
    public int rotatedDigits(int n) {
        int ans = 0;
        for (int i = 1; i <= n; ++i) {
            if (isGood(i)) {
                ++ans;
            }
        }
        return ans;
    }

    private boolean isGood(int x) {
        boolean changed = false;
        while (x > 0) {
            int d = x % 10;
            if (d == 3 || d == 4 || d == 7) {
                return false;
            }
            if (d == 2 || d == 5 || d == 6 || d == 9) {
                changed = true;
            }
            x /= 10;
        }
        return changed;
    }

    // 解法二:按位合法 + 必变标记(字符串形式,对照实现)
    public int rotatedDigits2(int n) {
        int ans = 0;
        for (int i = 1; i <= n; ++i) {
            String s = Integer.toString(i);
            boolean valid = true;
            boolean changed = false;
            for (int j = 0; j < s.length(); ++j) {
                char c = s.charAt(j);
                if (c == '3' || c == '4' || c == '7') {
                    valid = false;
                    break;
                }
                if (c == '2' || c == '5' || c == '6' || c == '9') {
                    changed = true;
                }
            }
            if (valid && changed) {
                ++ans;
            }
        }
        return ans;
    }

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

        ++total; if (sol.rotatedDigits(10) == 4) ++pass; else System.out.println("FAIL 788-1");
        ++total; if (sol.rotatedDigits(1) == 0) ++pass; else System.out.println("FAIL 788-2");
        ++total; if (sol.rotatedDigits(2) == 1) ++pass; else System.out.println("FAIL 788-3");
        ++total; if (sol.rotatedDigits(100) == 40) ++pass; else System.out.println("FAIL 788-4");
        ++total; if (sol.rotatedDigits(857) == 247) ++pass; else System.out.println("FAIL 788-5");
        ++total; if (sol.rotatedDigits(10000) == 2320) ++pass; else System.out.println("FAIL 788-6");

        ++total; if (sol.rotatedDigits2(10) == 4) ++pass; else System.out.println("FAIL 788-7");
        ++total; if (sol.rotatedDigits2(100) == 40) ++pass; else System.out.println("FAIL 788-8");
        ++total; if (sol.rotatedDigits2(857) == 247) ++pass; else System.out.println("FAIL 788-9");
        ++total; if (sol.rotatedDigits2(10000) == 2320) ++pass; else System.out.println("FAIL 788-10");

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

复杂度分析

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

789. Escape The Ghosts(逃脱阻碍者)

题目描述

你在进行一个简化版的吃豆人游戏。你从 [0, 0] 点开始出发,你的目的地是 target = [x, y]。 地图上有一些阻碍者,以数组 ghosts 给出,第 i 个阻碍者从 ghosts[i] = [x_i, y_i] 出发。 所有输入均为整数坐标。 每一回合,你和阻碍者们可以同时向东、西、南、北四个方向移动,每次可以移动到距离原位置 1 个单位的新位置。当然,也可以选择不动。所有动作同时发生。 如果你可以在任何阻碍者抓住你之前到达目的地(阻碍者可以采取任意行动方式),则被视为逃脱成功。 如果你和阻碍者同时到达了一个位置(包括目的地)都不算是逃脱成功。 如果不管阻碍者怎么移动都可以成功逃脱时,输出 true;否则,输出 false。

示例 1

  • 输入:ghosts = [[1,0],[0,3]], target = [0,1]
  • 输出:true

示例 2

  • 输入:ghosts = [[1,0]], target = [2,0]
  • 输出:false

示例 3

  • 输入:ghosts = [[2,0]], target = [1,0]
  • 输出:false 提示:
  • 1 <= ghosts.length <= 100
  • -10^4 <= x_i, y_i <= 10^4
  • 同一位置可能有多个阻碍者
  • -10^4 <= x_target, y_target <= 10^4

解题思路

网格上从 A 到 B 的最短步数就是曼哈顿距离 |dx| + |dy|(可以原地等待/绕行,但不会更短)。 阻碍者能拦住你,当且仅当其到目标的距离 <= 你到目标的距离:它可以先直接赶到目标处 等你(网格中不存在真正的"必经之路"以外的拦截,最优拦截就是截住目标点)。 解法一(比较曼哈顿距离,主提交):只要存在某个阻碍者距离目标 <= 你距离目标, 则返回 false;否则返回 true。 解法二(BFS 双向可达对照):暴力枚举玩家每一步,验证是否存在一条路径其每一点都 严格早于任意阻碍者到达。仅适合小范围坐标,作为理解用。

代码实现

class Solution {

    // 解法一:曼哈顿距离比较
    public boolean escapeGhosts(int[][] ghosts, int[] target) {
        int me = dist(0, 0, target[0], target[1]);
        for (int[] g : ghosts) {
            if (dist(g[0], g[1], target[0], target[1]) <= me) {
                return false;
            }
        }
        return true;
    }

    private int dist(int x1, int y1, int x2, int y2) {
        return Math.abs(x1 - x2) + Math.abs(y1 - y2);
    }

    // 解法二:最坏情况验证(所有阻碍者一起堵截目标)——与解法一逻辑等价的显式表述
    public boolean escapeGhosts2(int[][] ghosts, int[] target) {
        // 玩家最坏情况下的最短到达时间
        int player = dist(0, 0, target[0], target[1]);
        // 最小编号的阻碍者到达目标的时间
        int minGhost = Integer.MAX_VALUE;
        for (int[] g : ghosts) {
            minGhost = Math.min(minGhost, dist(g[0], g[1], target[0], target[1]));
        }
        return player < minGhost;
    }

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

        int[][] g1 = {{1, 0}, {0, 3}};
        ++total; if (sol.escapeGhosts(g1, new int[] {0, 1})) ++pass; else System.out.println("FAIL 789-1");
        int[][] g2 = {{1, 0}};
        ++total; if (!sol.escapeGhosts(g2, new int[] {2, 0})) ++pass; else System.out.println("FAIL 789-2");
        int[][] g3 = {{2, 0}};
        ++total; if (!sol.escapeGhosts(g3, new int[] {1, 0})) ++pass; else System.out.println("FAIL 789-3");
        int[][] g4 = {{5, 5}};
        ++total; if (sol.escapeGhosts(g4, new int[] {1, 1})) ++pass; else System.out.println("FAIL 789-4");
        int[][] g5 = {{0, 1}};
        ++total; if (sol.escapeGhosts(g5, new int[] {1, 0})) ++pass; else System.out.println("FAIL 789-5");
        int[][] g6 = {{-1, 0}};
        ++total; if (sol.escapeGhosts(g6, new int[] {0, 1})) ++pass; else System.out.println("FAIL 789-6");

        ++total; if (sol.escapeGhosts2(g1, new int[] {0, 1})) ++pass; else System.out.println("FAIL 789-7");
        ++total; if (!sol.escapeGhosts2(g2, new int[] {2, 0})) ++pass; else System.out.println("FAIL 789-8");
        ++total; if (sol.escapeGhosts2(g5, new int[] {1, 0})) ++pass; else System.out.println("FAIL 789-9");

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

复杂度分析

解法时间复杂度空间复杂度
解法一O(m)O(1)
解法二O(V*E)(小范围对照)O(V)