LeetCode989~1000题目解析

3 阅读8分钟

LeetCode 989-1000. 题解合集


989. Add to Array-Form of Integer(数组形式的整数加法)

题目描述

整数的数组形式 num 是按照从左到右的顺序表示其数字的数组。
例如,对于 num = 1321,数组形式是 [1,3,2,1]。
给定 num,整数的数组形式,和整数 k,返回整数 num + k 的数组形式。

示例 1

  • 输入:num = [1,2,0,0], k = 34
  • 输出:[1,2,3,4]
  • 解释:1200 + 34 = 1234

示例 2

  • 输入:num = [2,7,4], k = 181
  • 输出:[4,5,5]

示例 3

  • 输入:num = [2,1,5], k = 806
  • 输出:[1,0,2,1]
  • 解释:215 + 806 = 1021
    提示:
  • - 1 <= num.length <= 10^4
  • - 0 <= num[i] <= 9
  • - 1 <= k <= 10^4

解题思路

解法一(逐位模拟):
从 num 的最低位(数组末尾)开始,把 k 加到当前位:
while i >= 0 或 k > 0:
若 i >= 0,则 k += num[i];
结果位为 k % 10,累进 k /= 10,--i。
最后把结果反转(从低到高存 → 从高到低)。
注意 k 可能有多位(>10^4 时不会,但 k 可能比 num 长,循环条件保证覆盖)。
解法二(字符串/大数对照,供对照):
从最低位同步推进 num 与 k 的十进制表示,两数相加模拟,处理进位。
与解法一结果一致。

代码实现

public class Solution {
    // 解法一:逐位模拟(主解法,提交 LeetCode)
    public java.util.List<Integer> addToArrayForm(int[] num, int k) {
        java.util.List<Integer> ans = new java.util.ArrayList<>();
        int i = num.length - 1;
        while (i >= 0 || k > 0) {
            if (i >= 0) {
                k += num[i];
            }
            ans.add(k % 10);
            k /= 10;
            --i;
        }
        java.util.Collections.reverse(ans);
        return ans;
    }

    // 解法二:数组推进对照(独立实现,供对照验证)
    public java.util.List<Integer> addToArrayForm2(int[] num, int k) {
        // 把 k 拆成十进制位数组
        java.util.List<Integer> kk = new java.util.ArrayList<>();
        int t = k;
        while (t > 0) {
            kk.add(t % 10);
            t /= 10;
        }
        // 低位在前
        java.util.List<Integer> res = new java.util.ArrayList<>();
        int carry = 0;
        int p = num.length - 1, q = 0;
        while (p >= 0 || q < kk.size() || carry > 0) {
            int sum = carry;
            if (p >= 0) {
                sum += num[p];
                --p;
            }
            if (q < kk.size()) {
                sum += kk.get(q);
                ++q;
            }
            res.add(sum % 10);
            carry = sum / 10;
        }
        java.util.Collections.reverse(res);
        return res;
    }

    private static void check(int[] num, int k, int[] expected, int caseId) {
        java.util.List<Integer> got1 = new Solution().addToArrayForm(num.clone(), k);
        java.util.List<Integer> got2 = new Solution().addToArrayForm2(num.clone(), k);
        java.util.List<Integer> exp = new java.util.ArrayList<>();
        for (int x : expected) {
            exp.add(x);
        }
        if (!got1.equals(exp) || !got2.equals(exp)) {
            System.out.println("FAIL case" + caseId + " expected=" + exp
                    + " got1=" + got1 + " got2=" + got2);
            return;
        }
        System.out.println("ok case" + caseId);
    }

    public static void main(String[] args) {
        check(new int[] {1, 2, 0, 0}, 34, new int[] {1, 2, 3, 4}, 1);
        check(new int[] {2, 7, 4}, 181, new int[] {4, 5, 5}, 2);
        check(new int[] {2, 1, 5}, 806, new int[] {1, 0, 2, 1}, 3);
        check(new int[] {9, 9, 9}, 1, new int[] {1, 0, 0, 0}, 4);
        check(new int[] {0}, 10000, new int[] {1, 0, 0, 0, 0}, 5);
        check(new int[] {1, 2, 3}, 9, new int[] {1, 3, 2}, 6);
        check(new int[] {0}, 0, new int[] {0}, 7);
        System.out.println("== 0989 PASS ==");
    }
}

复杂度分析

解法一:

  • 时间复杂度:O(max(n, log k));
  • 空间复杂度:O(max(n, log k))(答案)。
    解法二:
  • 时间复杂度:O(max(n, log k));
  • 空间复杂度:O(max(n, log k))。

990. Satisfiability of Equality Equations(等式方程的可满足性)

题目描述

给定一个由表示变量之间关系的字符串方程组成的数组,每个字符串方程 equations[i] 的长度为 4,
并采用两种不同的形式之一:"a==b" 或 "a!=b"。在这里,a 和 b 是小写字母(不一定不同),
表示单字母变量名。
只有当可以将整数分配给变量名,以便满足所有给定的方程时才返回 true,否则返回 false。

示例 1

  • 输入:["a==b","b!=a"]
  • 输出:false

示例 2

  • 输入:["b==a","a==b"]
  • 输出:true

示例 3

  • 输入:["a==b","b==c","a==c"]
  • 输出:true

示例 4

  • 输入:["a==b","b!=c","c==a"]
  • 输出:false

示例 5

  • 输入:["c==c","b==d","x!=z"]
  • 输出:true
    提示:
  • - 1 <= equations.length <= 500
  • - equations[i].length == 4

解题思路

解法一(并查集):
相等关系具有传递性,先把所有 "==" 方程涉及的变量合并到同一连通分量;
再逐条检查 "!=" 方程,若两变量已在同一连通分量则矛盾,返回 false;
全部检查通过返回 true。
变量只有 26 个小写字母,用数组实现路径压缩并查集即可。
解法二(BFS 连通性对照):
先用 "==" 建图(26 节点邻接表),再对每条 "!=" 用 BFS 判断两端是否连通,
若连通则矛盾。与解法一结果一致。

代码实现

public class Solution {
    // 解法一:并查集(主解法,提交 LeetCode)
    private int[] p;

    public boolean equationsPossible(String[] equations) {
        p = new int[26];
        for (int i = 0; i < 26; ++i) {
            p[i] = i;
        }
        for (String e : equations) {
            if (e.charAt(1) == '=') {
                int a = e.charAt(0) - 'a';
                int b = e.charAt(3) - 'a';
                union(a, b);
            }
        }
        for (String e : equations) {
            if (e.charAt(1) == '!') {
                int a = e.charAt(0) - 'a';
                int b = e.charAt(3) - 'a';
                if (find(a) == find(b)) {
                    return false;
                }
            }
        }
        return true;
    }

    private int find(int x) {
        if (p[x] != x) {
            p[x] = find(p[x]);
        }
        return p[x];
    }

    private void union(int a, int b) {
        p[find(a)] = find(b);
    }

    // 解法二:BFS 连通性(对照验证)
    public boolean equationsPossibleBFS(String[] equations) {
        @SuppressWarnings("unchecked")
        java.util.List<Integer>[] g = new java.util.List[26];
        for (int i = 0; i < 26; ++i) {
            g[i] = new java.util.ArrayList<>();
        }
        for (String e : equations) {
            if (e.charAt(1) == '=') {
                int a = e.charAt(0) - 'a';
                int b = e.charAt(3) - 'a';
                g[a].add(b);
                g[b].add(a);
            }
        }
        for (String e : equations) {
            if (e.charAt(1) == '!') {
                int a = e.charAt(0) - 'a';
                int b = e.charAt(3) - 'a';
                if (connected(g, a, b)) {
                    return false;
                }
            }
        }
        return true;
    }

    private boolean connected(java.util.List<Integer>[] g, int s, int t) {
        boolean[] vis = new boolean[26];
        java.util.Queue<Integer> q = new java.util.LinkedList<>();
        q.offer(s);
        vis[s] = true;
        while (!q.isEmpty()) {
            int u = q.poll();
            if (u == t) {
                return true;
            }
            for (int v : g[u]) {
                if (!vis[v]) {
                    vis[v] = true;
                    q.offer(v);
                }
            }
        }
        return false;
    }

    private static void check(String[] equations, boolean expected, int caseId) {
        boolean got1 = new Solution().equationsPossible(equations);
        boolean got2 = new Solution().equationsPossibleBFS(equations);
        if (got1 != expected || got2 != expected) {
            System.out.println("FAIL case" + caseId + " expected=" + expected
                    + " got1=" + got1 + " got2=" + got2);
            return;
        }
        System.out.println("ok case" + caseId);
    }

    public static void main(String[] args) {
        check(new String[] {"a==b", "b!=a"}, false, 1);
        check(new String[] {"b==a", "a==b"}, true, 2);
        check(new String[] {"a==b", "b==c", "a==c"}, true, 3);
        check(new String[] {"a==b", "b!=c", "c==a"}, false, 4);
        check(new String[] {"c==c", "b==d", "x!=z"}, true, 5);
        check(new String[] {"a!=a"}, false, 6);
        check(new String[] {"a!=b", "b!=c", "a==c"}, true, 7);
        check(new String[] {"a==b"}, true, 8);
        System.out.println("== 0990 PASS ==");
    }
}

复杂度分析

解法一:

  • 时间复杂度:O(n * α(26)) ≈ O(n),n 为方程数;
  • 空间复杂度:O(26)。
    解法二:
  • 时间复杂度:O(n * 26);每次 BFS 都访问邻接表;
  • 空间复杂度:O(26^2)。

991. Broken Calculator(坏了的计算器)

题目描述

在显示着数字 startValue 的坏计算器上,我们可以执行以下两种操作:

  • 双倍(Double):将显示屏上的数字乘 2;
  • 递减(Decrement):将显示屏上的数字减 1。
    给定两个整数 startValue 和 target。返回显示数字 target 所需的最小操作数。

示例 1

  • 输入:startValue = 2, target = 3
  • 输出:2
  • 解释:先进行双倍运算,然后再进行递减运算 {2 -> 4 -> 3}。

示例 2

  • 输入:startValue = 5, target = 8
  • 输出:2
  • 解释:先递减,再双倍 {5 -> 4 -> 8}。

示例 3

  • 输入:startValue = 3, target = 10
  • 输出:3
  • 解释:先双倍,然后递减,再双倍 {3 -> 6 -> 5 -> 10}。
    提示:
  • - 1 <= startValue, target <= 10^9

解题思路

解法一(逆向贪心):
正向同时有乘 2 和减 1,dp/BFS 都会超时。逆向思考:从 target 变回 startValue,
逆向操作是「除以 2」(当 target 为偶数)与「加 1」。
贪心策略:

  • 若 target 为奇数,则最后一次操作必是「减 1」(正向),逆向为 target+1;
  • 若 target 为偶数,优先 target/2(除以 2 比逐个加 1 减少操作更快)。
    循环直到 target <= startValue,之后只需逐个减 1,操作数为 startValue - target。
    累计操作次数即为答案。
    解法二(BFS 小数据对照):
    对 startValue 做 BFS(乘 2 / 减 1),记录到 target 的最短操作数。仅用于小数据对照验证。

代码实现

public class Solution {
    // 解法一:逆向贪心(主解法,提交 LeetCode)
    public int brokenCalc(int startValue, int target) {
        int ans = 0;
        while (startValue < target) {
            if ((target & 1) == 1) {
                ++target;
            } else {
                target >>= 1;
            }
            ++ans;
        }
        ans += startValue - target;
        return ans;
    }

    // 解法二:BFS(对照验证,仅限小数据)
    public int brokenCalcBFS(int startValue, int target) {
        java.util.Queue<int[]> q = new java.util.LinkedList<>();
        java.util.Set<Integer> vis = new java.util.HashSet<>();
        q.offer(new int[] {startValue, 0});
        vis.add(startValue);
        while (!q.isEmpty()) {
            int[] cur = q.poll();
            int v = cur[0], d = cur[1];
            if (v == target) {
                return d;
            }
            // 乘 2:过快超出 target 时剪枝(target*2 内足够)
            if (v < target * 2 && !vis.contains(v * 2)) {
                vis.add(v * 2);
                q.offer(new int[] {v * 2, d + 1});
            }
            // 减 1
            if (v - 1 >= 0 && !vis.contains(v - 1)) {
                vis.add(v - 1);
                q.offer(new int[] {v - 1, d + 1});
            }
        }
        return -1;
    }

    private static void check(int s, int t, int expected, int caseId) {
        int got1 = new Solution().brokenCalc(s, t);
        if (got1 != expected) {
            System.out.println("FAIL case" + caseId + " expected=" + expected + " got1=" + got1);
            return;
        }
        if (t <= 40) {
            int got2 = new Solution().brokenCalcBFS(s, t);
            if (got2 != expected) {
                System.out.println("FAIL-BFS case" + caseId + " expected=" + expected + " got2=" + got2);
                return;
            }
        }
        System.out.println("ok case" + caseId);
    }

    public static void main(String[] args) {
        check(2, 3, 2, 1);
        check(5, 8, 2, 2);
        check(3, 10, 3, 3);
        check(1, 1000000000, 39, 4);
        check(1024, 1, 1023, 5);
        check(1, 2, 1, 6);
        check(3, 3, 0, 7);
        System.out.println("== 0991 PASS ==");
    }
}

复杂度分析

解法一:

  • 时间复杂度:O(log target),每次至少减半或加一后减半;
  • 空间复杂度:O(1)。 解法二:
  • 时间复杂度:O(2^d),d 为最短操作数(仅限小数据);
  • 空间复杂度:O(2^d)。

992. Subarrays with K Different Integers(K 个不同整数的子数组)

题目描述

给定一个正整数数组 nums 和一个整数 k,返回 nums 中「好子数组」的数目。 如果 nums 的某个子数组中不同整数的个数恰好为 k,则称 nums 的这个连续、不一定不同的子数组为「好子数组」。 例如,[1,2,3,1,2] 中有 3 个不同的整数:1,2,以及 3。 子数组是数组的连续部分。

示例 1

  • 输入:nums = [1,2,1,2,3], k = 2
  • 输出:7
  • 解释:恰好由 2 个不同整数组成的子数组:[1,2], [2,1], [1,2], [2,3], [1,2,1], [2,1,2], [1,2,1,2]

示例 2

  • 输入:nums = [1,2,1,3,4], k = 3
  • 输出:3
  • 解释:[1,2,1,3], [2,1,3], [1,3,4] 提示:
  • - 1 <= nums.length <= 2 * 10^4
  • - 1 <= nums[i], k <= nums.length

解题思路

解法一(双指针 + 「至多 k」差分): 恰好 k 种 = 至多 k 种 - 至多 k-1 种。 定义 f(k) 返回数组 pos:pos[i] 表示以 i 结尾、不同整数种数不超过 k 的最短窗口左端点 j。 滑动窗口维护 cnt[] 与当前窗口不同种数 s,当 s > k 时右移左指针直到 s <= k。 对每个右端点,好子数组数量 = withAtMost(k-1) 的左端点 - withAtMost(k) 的左端点, 求和即为答案。 解法二(暴力枚举对照): 枚举所有子数组,统计不同元素恰为 k 的数量。仅用于小数据对照验证。

代码实现

public class Solution {
    // 解法一:双指针 + 至多 k 差分(主解法,提交 LeetCode)
    public int subarraysWithKDistinct(int[] nums, int k) {
        return atMost(nums, k) - atMost(nums, k - 1);
    }

    // 不同整数种数不超过 k 的子数组数量
    private int atMost(int[] nums, int k) {
        int n = nums.length;
        int[] cnt = new int[n + 1];
        int s = 0; // 当前窗口不同种数
        int ans = 0;
        for (int i = 0, j = 0; i < n; ++i) {
            if (cnt[nums[i]]++ == 0) {
                ++s;
            }
            while (s > k) {
                if (--cnt[nums[j]] == 0) {
                    --s;
                }
                ++j;
            }
            ans += i - j + 1;
        }
        return ans;
    }

    // 解法二:暴力枚举(对照验证)
    public int subarraysWithKDistinctBrute(int[] nums, int k) {
        int n = nums.length;
        int ans = 0;
        for (int i = 0; i < n; ++i) {
            java.util.Set<Integer> set = new java.util.HashSet<>();
            for (int j = i; j < n; ++j) {
                set.add(nums[j]);
                if (set.size() == k) {
                    ++ans;
                } else if (set.size() > k) {
                    break;
                }
            }
        }
        return ans;
    }

    private static void check(int[] nums, int k, int expected, int caseId) {
        int got1 = new Solution().subarraysWithKDistinct(nums.clone(), k);
        int got2 = new Solution().subarraysWithKDistinctBrute(nums.clone(), k);
        if (got1 != expected || got2 != expected) {
            System.out.println("FAIL case" + caseId + " expected=" + expected
                    + " got1=" + got1 + " got2=" + got2);
            return;
        }
        System.out.println("ok case" + caseId);
    }

    public static void main(String[] args) {
        check(new int[] {1, 2, 1, 2, 3}, 2, 7, 1);
        check(new int[] {1, 2, 1, 3, 4}, 3, 3, 2);
        check(new int[] {1, 1, 1, 1}, 1, 10, 3);
        check(new int[] {1, 2}, 1, 2, 4);
        check(new int[] {1, 2, 1, 2}, 2, 6, 5);
        check(new int[] {1, 2, 3}, 3, 1, 6);
        check(new int[] {1, 1, 2, 2}, 2, 4, 7);
        System.out.println("== 0992 PASS ==");
    }
}

复杂度分析

解法一:

  • 时间复杂度:O(n);
  • 空间复杂度:O(n)。 解法二:
  • 时间复杂度:O(n^2);
  • 空间复杂度:O(n)。

993. Cousins in Binary Tree(二叉树的堂兄弟节点)

题目描述

在二叉树中,根节点位于深度 0 处,每个深度为 k 的节点的子节点位于深度 k+1 处。 如果二叉树的两个节点深度相同,但父节点不同,则它们是一对堂兄弟节点。 我们给出了具有唯一值的二叉树的根节点 root,以及树中两个不同节点的值 x 和 y。 只有与值 x 和 y 对应的节点是堂兄弟节点时,才返回 true。否则,返回 false。

示例 1

  • 输入:root = [1,2,3,4], x = 4, y = 3
  • 输出:false

示例 2

  • 输入:root = [1,2,3,null,4,null,5], x = 5, y = 4
  • 输出:true

示例 3

  • 输入:root = [1,2,3,null,4], x = 2, y = 3
  • 输出:false 提示:
    • 二叉树的节点数介于 2 到 100 之间
    • 每个节点的值都是唯一的、范围为 1 到 100 的整数

解题思路

解法一(BFS 层序): 堂兄弟 = 深度相同且父节点不同。层序遍历时同时记录每个节点的父节点与深度: 把 (node, parent) 入队,按层推进;遇到值 x 或 y 时记录 (depth, parent)。 遍历结束后比较两者的深度与父节点。 解法二(DFS 递归,供对照): 从根递归,携带当前深度与父节点;遇到 x/y 记录;遍历完整棵树后比较。

代码实现

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

public class Solution {
    // 解法一:BFS 层序(主解法,提交 LeetCode)
    public boolean isCousins(TreeNode root, int x, int y) {
        java.util.Queue<TreeNode[]> q = new java.util.LinkedList<>();
        q.offer(new TreeNode[] {root, null});
        int d1 = -1, d2 = -1;
        TreeNode p1 = null, p2 = null;
        int depth = 0;
        while (!q.isEmpty()) {
            int size = q.size();
            for (int t = 0; t < size; ++t) {
                TreeNode[] cur = q.poll();
                TreeNode node = cur[0], parent = cur[1];
                if (node.val == x) {
                    d1 = depth;
                    p1 = parent;
                } else if (node.val == y) {
                    d2 = depth;
                    p2 = parent;
                }
                if (node.left != null) {
                    q.offer(new TreeNode[] {node.left, node});
                }
                if (node.right != null) {
                    q.offer(new TreeNode[] {node.right, node});
                }
            }
            ++depth;
        }
        return d1 == d2 && p1 != p2;
    }

    // 解法二:DFS 递归(对照验证)
    private int xv, yv;
    private int dx, dy;
    private TreeNode px, py;

    public boolean isCousinsDFS(TreeNode root, int x, int y) {
        xv = x;
        yv = y;
        dfs(root, null, 0);
        return dx == dy && px != py;
    }

    private void dfs(TreeNode node, TreeNode parent, int depth) {
        if (node == null) {
            return;
        }
        if (node.val == xv) {
            dx = depth;
            px = parent;
        } else if (node.val == yv) {
            dy = depth;
            py = parent;
        }
        dfs(node.left, node, depth + 1);
        dfs(node.right, node, depth + 1);
    }

    private static TreeNode build(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 i = 1;
        while (!q.isEmpty() && i < arr.length) {
            TreeNode cur = q.poll();
            if (arr[i] != null) {
                cur.left = new TreeNode(arr[i]);
                q.offer(cur.left);
            }
            ++i;
            if (i < arr.length && arr[i] != null) {
                cur.right = new TreeNode(arr[i]);
                q.offer(cur.right);
            }
            ++i;
        }
        return root;
    }

    private static void check(Integer[] arr, int x, int y, boolean expected, int caseId) {
        TreeNode r1 = build(arr);
        TreeNode r2 = build(arr);
        boolean got1 = new Solution().isCousins(r1, x, y);
        boolean got2 = new Solution().isCousinsDFS(r2, x, y);
        if (got1 != expected || got2 != expected) {
            System.out.println("FAIL case" + caseId + " expected=" + expected
                    + " got1=" + got1 + " got2=" + got2);
            return;
        }
        System.out.println("ok case" + caseId);
    }

    public static void main(String[] args) {
        check(new Integer[] {1, 2, 3, 4}, 4, 3, false, 1);
        check(new Integer[] {1, 2, 3, null, 4, null, 5}, 5, 4, true, 2);
        check(new Integer[] {1, 2, 3, null, 4}, 2, 3, false, 3);
        check(new Integer[] {1, 2, 3, null, 4, null, 5}, 2, 3, false, 4); // 2 与 3 是兄弟
        check(new Integer[] {1, 2, 3, 4, 5, 6, 7}, 4, 7, true, 5);
        System.out.println("== 0993 PASS ==");
    }
}

复杂度分析

解法一:

  • 时间复杂度:O(n);
  • 空间复杂度:O(n),队列。 解法二:
  • 时间复杂度:O(n);
  • 空间复杂度:O(h),递归栈,h 为树高。

994. Rotting Oranges(腐烂的橘子)

题目描述

在给定的 m x n 网格 grid 中,每个单元格可以有以下三个值之一:

  • 值 0 代表空单元格;
  • 值 1 代表新鲜橘子;
  • 值 2 代表腐烂的橘子。 每分钟,腐烂的橘子周围 4 个方向上相邻的新鲜橘子都会腐烂。 返回直到单元格中没有新鲜橘子为止所必须经过的最小分钟数。如果不可能,返回 -1。

示例 1

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

示例 2

  • 输入:grid = [[2,1,1],[0,1,1],[1,0,1]]
  • 输出:-1
  • 解释:左下角的橘子永远不会腐烂……

示例 3

  • 输入:grid = [[0,2]]
  • 输出:0 提示:
  • - m == grid.length
  • - n == grid[i].length
  • - 1 <= m, n <= 10
    • grid[i][j] 仅为 0、1 或 2

解题思路

解法一(多源 BFS): 所有腐烂橘子同时向外扩散,每分钟感染四邻的新鲜橘子。这是无权图多源最短路径问题:

  1. 先统计新鲜橘子数 cnt,并把所有腐烂橘子坐标入队;
  2. 按层 BFS:轮数 ans 从 0 递增,每轮扩张队列中所有腐烂橘子的四邻; 感染一个新鲜橘子就把 cnt 减 1,若 cnt 变为 0 立即返回当前轮数;
  3. BFS 结束后若 cnt > 0,说明有新鲜橘子永远无法被感染,返回 -1,否则返回 0。 解法二(DFS 逐分钟模拟,供对照): 每分钟扫描网格把可被感染的橘子标记为腐烂,检查是否有新感染;反复直到无新感染 或新鲜橘已清零。与解法一结果一致,仅限小网格。

代码实现

public class Solution {
    // 解法一:多源 BFS(主解法,提交 LeetCode)
    public int orangesRotting(int[][] grid) {
        int m = grid.length, n = grid[0].length;
        java.util.Queue<int[]> q = new java.util.LinkedList<>();
        int fresh = 0;
        for (int i = 0; i < m; ++i) {
            for (int j = 0; j < n; ++j) {
                if (grid[i][j] == 1) {
                    ++fresh;
                } else if (grid[i][j] == 2) {
                    q.offer(new int[] {i, j});
                }
            }
        }
        if (fresh == 0) {
            return 0;
        }
        int[] dx = {-1, 0, 1, 0};
        int[] dy = {0, 1, 0, -1};
        int minutes = 0;
        while (!q.isEmpty()) {
            int size = q.size();
            for (int t = 0; t < size; ++t) {
                int[] cur = q.poll();
                for (int d = 0; d < 4; ++d) {
                    int nx = cur[0] + dx[d];
                    int ny = cur[1] + dy[d];
                    if (nx >= 0 && nx < m && ny >= 0 && ny < n && grid[nx][ny] == 1) {
                        grid[nx][ny] = 2;
                        --fresh;
                        q.offer(new int[] {nx, ny});
                    }
                }
            }
            ++minutes;
            if (fresh == 0) {
                return minutes;
            }
        }
        return -1;
    }

    // 解法二:逐分钟模拟(对照验证)
    public int orangesRottingSim(int[][] grid) {
        int m = grid.length, n = grid[0].length;
        int fresh = 0;
        for (int[] row : grid) {
            for (int x : row) {
                if (x == 1) {
                    ++fresh;
                }
            }
        }
        if (fresh == 0) {
            return 0;
        }
        int[] dx = {-1, 0, 1, 0};
        int[] dy = {0, 1, 0, -1};
        int minutes = 0;
        while (fresh > 0) {
            boolean changed = false;
            boolean[][] willRot = new boolean[m][n];
            for (int i = 0; i < m; ++i) {
                for (int j = 0; j < n; ++j) {
                    if (grid[i][j] == 2) {
                        for (int d = 0; d < 4; ++d) {
                            int nx = i + dx[d];
                            int ny = j + dy[d];
                            if (nx >= 0 && nx < m && ny >= 0 && ny < n && grid[nx][ny] == 1) {
                                willRot[nx][ny] = true;
                            }
                        }
                    }
                }
            }
            for (int i = 0; i < m; ++i) {
                for (int j = 0; j < n; ++j) {
                    if (willRot[i][j]) {
                        grid[i][j] = 2;
                        --fresh;
                        changed = true;
                    }
                }
            }
            if (!changed) {
                return -1;
            }
            ++minutes;
        }
        return minutes;
    }

    private static int[][] cloneGrid(int[][] g) {
        int[][] c = new int[g.length][];
        for (int i = 0; i < g.length; ++i) {
            c[i] = g[i].clone();
        }
        return c;
    }

    private static void check(int[][] g, int expected, int caseId) {
        int got1 = new Solution().orangesRotting(cloneGrid(g));
        int got2 = new Solution().orangesRottingSim(cloneGrid(g));
        if (got1 != expected || got2 != expected) {
            System.out.println("FAIL case" + caseId + " expected=" + expected
                    + " got1=" + got1 + " got2=" + got2);
            return;
        }
        System.out.println("ok case" + caseId);
    }

    public static void main(String[] args) {
        check(new int[][] {{2, 1, 1}, {1, 1, 0}, {0, 1, 1}}, 4, 1);
        check(new int[][] {{2, 1, 1}, {0, 1, 1}, {1, 0, 1}}, -1, 2);
        check(new int[][] {{0, 2}}, 0, 3);
        check(new int[][] {{1}}, -1, 4);
        check(new int[][] {{2}}, 0, 5);
        check(new int[][] {{1, 2}}, 1, 6);
        check(new int[][] {{0, 0, 0}}, 0, 7);
        System.out.println("== 0994 PASS ==");
    }
}

复杂度分析

解法一:

  • 时间复杂度:O(m*n),每格至多入队一次;
  • 空间复杂度:O(m*n),队列。 解法二:
  • 时间复杂度:O(T * m * n),T 为分钟数;
  • 空间复杂度:O(m*n)。

995. Minimum Number of K Consecutive Bit Flips(K 连续位的最小翻转次数)

题目描述

给定一个二进制数组 nums 和一个整数 k。 k 位翻转就是从 nums 中选择一个长度为 k 的子数组,同时把子数组中的每一个 0 都改成 1, 把子数组中的每一个 1 都改成 0。 返回数组中不存在 0 所需的最小 k 位翻转次数。如果不可能,则返回 -1。 子数组是数组的连续部分。

示例 1

  • 输入:nums = [0,1,0], K = 1
  • 输出:2
  • 解释:先翻转 A[0],然后翻转 A[2]。

示例 2

  • 输入:nums = [1,1,0], K = 2
  • 输出:-1

示例 3

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

解题思路

解法一(差分数组 + 贪心): 翻转顺序不影响最终结果。从左到右贪心:对于位置 i,若此前叠加的翻转次数与 nums[i] 的奇偶性相同,则 nums[i] 最终仍是 0,必须在 i 处开启一次长度为 k 的翻转。 用差分数组 d 维护「从 i 开始叠加的翻转次数」:开启翻转时 d[i]++、d[i+k]--, 当前位置累计次数 s += d[i]。若开启翻转后 i+k 越界则返回 -1。 时间复杂度 O(n)。 解法二(O(1) 空间滑动窗口,供对照): 与差分等价:用变量 flipped 记录当前位置是否被奇数个翻转覆盖。当 i >= k 且 nums[i-k] == -1(标记在 i-k 处开启过翻转)时,flipped ^= 1。若 flipped == nums[i] 则 当前位仍为 0,必须开翻:flipped ^= 1、标记 nums[i] = -1、答案 +1;i+k 越界返回 -1。

代码实现

public class Solution {
    // 解法一:差分数组(主解法,提交 LeetCode)
    public int minKBitFlips(int[] nums, int k) {
        int n = nums.length;
        int[] d = new int[n + 1];
        int ans = 0, s = 0;
        for (int i = 0; i < n; ++i) {
            s += d[i];
            if (s % 2 == nums[i]) {
                if (i + k > n) {
                    return -1;
                }
                ++d[i];
                --d[i + k];
                ++s;
                ++ans;
            }
        }
        return ans;
    }

    // 解法二:原地滑动窗口(对照验证)
    public int minKBitFlipsO1(int[] nums, int k) {
        int n = nums.length;
        int ans = 0, flipped = 0;
        for (int i = 0; i < n; ++i) {
            if (i >= k && nums[i - k] == -1) {
                flipped ^= 1;
            }
            if (flipped == nums[i]) {
                if (i + k > n) {
                    return -1;
                }
                flipped ^= 1;
                ++ans;
                nums[i] = -1;
            }
        }
        return ans;
    }

    private static void check(int[] nums, int k, int expected, int caseId) {
        int[] n1 = nums.clone();
        int[] n2 = nums.clone();
        int got1 = new Solution().minKBitFlips(n1, k);
        int got2 = new Solution().minKBitFlipsO1(n2, k);
        if (got1 != expected || got2 != expected) {
            System.out.println("FAIL case" + caseId + " expected=" + expected
                    + " got1=" + got1 + " got2=" + got2);
            return;
        }
        System.out.println("ok case" + caseId);
    }

    public static void main(String[] args) {
        check(new int[] {0, 1, 0}, 1, 2, 1);
        check(new int[] {1, 1, 0}, 2, -1, 2);
        check(new int[] {0, 0, 0, 1, 0, 1, 1, 0}, 3, 3, 3);
        check(new int[] {0}, 1, 1, 4);
        check(new int[] {1}, 1, 0, 5);
        check(new int[] {0, 0, 0}, 2, -1, 6);
        check(new int[] {1, 0, 0, 1}, 2, 1, 7);
        System.out.println("== 0995 PASS ==");
    }
}

复杂度分析

解法一:

  • 时间复杂度:O(n);
  • 空间复杂度:O(n),差分数组。 解法二:
  • 时间复杂度:O(n);
  • 空间复杂度:O(1),原地标记。

996. Number of Squareful Arrays(平方数组的数目)

题目描述

如果一个数组的任意两个相邻元素之和都是完全平方数,则该数组称为平方数组。 给定一个整数数组 nums,返回所有属于平方数组的 nums 的排列数量。 如果存在某个索引 i 使得 perm1[i] != perm2[i],则认为两个排列 perm1 和 perm2 不同。

示例 1

  • 输入:nums = [1,17,8]
  • 输出:2
  • 解释:[1,8,17] 和 [17,8,1] 是有效的排列。

示例 2

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

解题思路

解法一(状态压缩 DP): n <= 12,可以用位掩码表示已使用的数字。f[i][j] 表示已选集合为 i(二进制第 b 位为 1 表示已选 nums[b])、且当前排列最后一个下标是 j 的排列数。 初始 f[1<<j][j] = 1;转移:枚举下一个要追加的下标 k(不在集合内),若 nums[j] + nums[k] 是完全平方数,则 f[i|(1<<k)][k] += f[i][j]。 最终答案是 sum(f[(1<<n)-1][j])。由于相同数字的不同下标会产生重复排列, 需除以各数值出现次数的阶乘(乘法原理去重)。 解法二(回溯 + 集合去重,供对照): 从空排列开始 DFS 构造,每次挑选「与前一个数之和为平方数」且未被使用的下标; 相同数值在同一层只尝试第一个可用下标(用 Set 记录本层已尝试值)以达到去重。 计数即答案。仅适用于 n 较小。

代码实现

public class Solution {
    // 解法一:状态压缩 DP(主解法,提交 LeetCode)
    public int numSquarefulPerms(int[] nums) {
        int n = nums.length;
        int full = 1 << n;
        int[][] f = new int[full][n];
        for (int j = 0; j < n; ++j) {
            f[1 << j][j] = 1;
        }
        for (int mask = 1; mask < full; ++mask) {
            for (int j = 0; j < n; ++j) {
                if ((mask & (1 << j)) == 0 || f[mask][j] == 0) {
                    continue;
                }
                for (int k = 0; k < n; ++k) {
                    if ((mask & (1 << k)) != 0) {
                        continue;
                    }
                    long sum = (long) nums[j] + nums[k];
                    int root = (int) Math.sqrt(sum);
                    if ((long) root * root == sum) {
                        f[mask | (1 << k)][k] += f[mask][j];
                    }
                }
            }
        }
        long ans = 0;
        for (int j = 0; j < n; ++j) {
            ans += f[full - 1][j];
        }
        // 除以相同数值出现次数的阶乘(每种数值的重复下标排列属同一序列)
        java.util.Map<Integer, Integer> cnt = new java.util.HashMap<>();
        for (int x : nums) {
            cnt.merge(x, 1, Integer::sum);
        }
        int[] fact = new int[n + 1];
        fact[0] = 1;
        for (int i = 1; i <= n; ++i) {
            fact[i] = fact[i - 1] * i;
        }
        for (int v : cnt.values()) {
            ans /= fact[v];
        }
        return (int) ans;
    }

    // 解法二:回溯 + 集合去重(对照验证)
    private int n;
    private int[] a;
    private int count;

    public int numSquarefulPermsBacktrack(int[] nums) {
        n = nums.length;
        a = nums.clone();
        count = 0;
        java.util.Arrays.sort(a);
        boolean[] used = new boolean[n];
        dfs(0, -1, used);
        return count;
    }

    private void dfs(int depth, int last, boolean[] used) {
        if (depth == n) {
            ++count;
            return;
        }
        java.util.Set<Integer> seen = new java.util.HashSet<>();
        for (int i = 0; i < n; ++i) {
            if (used[i] || seen.contains(a[i])) {
                continue;
            }
            if (last != -1) {
                long sum = (long) last + a[i];
                int root = (int) Math.sqrt(sum);
                if ((long) root * root != sum) {
                    continue;
                }
            }
            seen.add(a[i]);
            used[i] = true;
            dfs(depth + 1, a[i], used);
            used[i] = false;
        }
    }

    private static void check(int[] nums, int expected, int caseId) {
        int got1 = new Solution().numSquarefulPerms(nums.clone());
        int got2 = new Solution().numSquarefulPermsBacktrack(nums.clone());
        if (got1 != expected || got2 != expected) {
            System.out.println("FAIL case" + caseId + " expected=" + expected
                    + " got1=" + got1 + " got2=" + got2);
            return;
        }
        System.out.println("ok case" + caseId);
    }

    public static void main(String[] args) {
        check(new int[] {1, 17, 8}, 2, 1);
        check(new int[] {2, 2, 2}, 1, 2);
        check(new int[] {1}, 1, 3);
        check(new int[] {0, 0}, 1, 4);
        check(new int[] {1, 1, 8, 1, 8}, 1, 5);
        check(new int[] {2, 2, 2, 2, 2}, 1, 6);
        check(new int[] {1, 8, 17, 8}, 6, 7);
        System.out.println("== 0996 PASS ==");
    }
}

复杂度分析

解法一:

  • 时间复杂度:O(2^n * n^2);
  • 空间复杂度:O(2^n * n)。 解法二:
  • 时间复杂度:O(n!),最坏全排列数;
  • 空间复杂度:O(n)。

997. Find the Town Judge(找到小镇的法官)

题目描述

小镇里有 n 个人,按从 1 到 n 的顺序编号。传言称,这些人中有一个暗地里是小镇法官。 如果小镇法官真的存在,那么:

  1. 小镇法官不会信任任何人;
  2. 每个人(除了小镇法官)都信任这位小镇法官;
  3. 只有一个人同时满足属性 1 和属性 2。 给你一个数组 trust,其中 trust[i] = [a_i, b_i] 表示编号为 a_i 的人信任编号为 b_i 的人。 如果小镇法官存在并且可以确定他的身份,请返回该法官的编号;否则,返回 -1。

示例 1

  • 输入:n = 2, trust = [[1,2]]
  • 输出:2

示例 2

  • 输入:n = 3, trust = [[1,3],[2,3]]
  • 输出:3

示例 3

  • 输入:n = 3, trust = [[1,3],[2,3],[3,1]]
  • 输出:-1 提示:
  • - 1 <= n <= 1000
  • - 0 <= trust.length <= 10^4
  • - trust[i].length == 2
    • trust 中的所有 trust[i] = [a_i, b_i] 互不相同
  • - a_i != b_i
  • - 1 <= a_i, b_i <= n

解题思路

解法一(出入度计数): 法官的特征是「不信任任何人」且「被其他所有 n-1 人信任」。 用 cnt1[i] 记录 i 信任的人数(出度),cnt2[i] 记录信任 i 的人数(入度)。 扫描 trust 后,若存在编号 i 满足 cnt1[i] == 0 且 cnt2[i] == n - 1,则 i 是法官。 由于「只有一个人同时满足属性 1 和属性 2」,满足条件的编号至多一个。 解法二(差值计数,供对照): 记 diff[i] = 被信任数 - 信任数(入度减出度)。法官满足 diff[i] == n - 1。 与解法一结果一致。

代码实现

public class Solution {
    // 解法一:出入度计数(主解法,提交 LeetCode)
    public int findJudge(int n, int[][] trust) {
        int[] cnt1 = new int[n + 1]; // 信任人数(出度)
        int[] cnt2 = new int[n + 1]; // 被信任人数(入度)
        for (int[] t : trust) {
            ++cnt1[t[0]];
            ++cnt2[t[1]];
        }
        for (int i = 1; i <= n; ++i) {
            if (cnt1[i] == 0 && cnt2[i] == n - 1) {
                return i;
            }
        }
        return -1;
    }

    // 解法二:差值计数(对照验证)
    public int findJudgeDiff(int n, int[][] trust) {
        int[] diff = new int[n + 1];
        for (int[] t : trust) {
            --diff[t[0]]; // 信任别人:出度 +
            ++diff[t[1]]; // 被别人信任:入度 +
        }
        for (int i = 1; i <= n; ++i) {
            if (diff[i] == n - 1) {
                return i;
            }
        }
        return -1;
    }

    private static void check(int n, int[][] trust, int expected, int caseId) {
        int got1 = new Solution().findJudge(n, trust);
        int got2 = new Solution().findJudgeDiff(n, trust);
        if (got1 != expected || got2 != expected) {
            System.out.println("FAIL case" + caseId + " expected=" + expected
                    + " got1=" + got1 + " got2=" + got2);
            return;
        }
        System.out.println("ok case" + caseId);
    }

    public static void main(String[] args) {
        check(2, new int[][] {{1, 2}}, 2, 1);
        check(3, new int[][] {{1, 3}, {2, 3}}, 3, 2);
        check(3, new int[][] {{1, 3}, {2, 3}, {3, 1}}, -1, 3);
        check(1, new int[][] {}, 1, 4);
        check(3, new int[][] {{1, 2}, {2, 3}}, -1, 5);
        check(3, new int[][] {{1, 3}, {2, 3}, {3, 2}}, -1, 6);
        check(4, new int[][] {{1, 3}, {1, 4}, {2, 3}, {2, 4}, {4, 3}}, 3, 7);
        System.out.println("== 0997 PASS ==");
    }
}

复杂度分析

解法一:

  • 时间复杂度:O(n + m),m 为 trust 长度;
  • 空间复杂度:O(n)。 解法二:
  • 时间复杂度:O(n + m);
  • 空间复杂度:O(n)。

998. Maximum Binary Tree II(最大二叉树 II)

题目描述

最大树定义:一棵树,并满足:其中每个节点的值都大于其子树中的任何其他值。 给你最大树的根节点 root 和一个整数 val。 就像之前的问题那样,给定的树是利用 Construct(a) 例程从列表 a(root = Construct(a))递归地构建的:

  • 如果 a 为空,返回 null;
  • 否则,令 a[i] 作为 a 的最大元素。创建一个值为 a[i] 的根节点 root;
  • root 的左子树将被构建为 Construct([a[0], a[1], ..., a[i-1]]);
  • root 的右子树将被构建为 Construct([a[i+1], a[i+2], ..., a[a.length-1]]);
  • 返回 root。 请注意,题目没有直接给出 a,只是给出一个根节点 root = Construct(a)。 假设 b 是 a 的副本,并在末尾附加值 val。题目数据保证 b 中的值互不相同。 返回 Construct(b)。

示例 1

  • 输入:root = [4,1,3,null,null,2], val = 5
  • 输出:[5,4,null,1,3,null,null,2]
  • 解释:a = [1,4,2,3], b = [1,4,2,3,5]

示例 2

  • 输入:root = [5,2,4,null,1], val = 3
  • 输出:[5,2,4,null,1,null,3]
  • 解释:a = [2,1,5,4], b = [2,1,5,4,3]

示例 3

  • 输入:root = [5,2,3,null,1], val = 4
  • 输出:[5,2,4,null,1,3]
  • 解释:a = [2,1,5,3], b = [2,1,5,3,4] 提示:
    • 树中节点数目在范围 [1, 100] 内
  • - 1 <= Node.val <= 100
    • 树中的所有值互不相同
  • - 1 <= val <= 100

解题思路

解法一(递归): 在最大树对应的数组末尾追加 val,再重建最大树。val 是最后追加的元素, 因此它要么成为新根(val 比根大),要么插入右子树,绝不进入左子树:

  • 若 root == null 或 root.val < val:新节点为根,原树作为左子树;
  • 否则 val 仍应插到右子树:root.right = insertIntoMaxTree(root.right, val)。 解法二(迭代沿右脊,供对照): 沿右子树找到「当前节点值 > val > 右孩子值(或右孩子为空)」的位置, 在该处把新节点挂上:新节点左孩子 = 当前位置的右节点,当前节点右孩子 = 新节点。 与递归结果一致,且无需递归栈。

代码实现

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

public class Solution {
    // 解法一:递归(主解法,提交 LeetCode)
    public TreeNode insertIntoMaxTree(TreeNode root, int val) {
        if (root == null || root.val < val) {
            return new TreeNode(val, root, null);
        }
        root.right = insertIntoMaxTree(root.right, val);
        return root;
    }

    // 解法二:迭代沿右脊(对照验证)
    public TreeNode insertIntoMaxTreeIter(TreeNode root, int val) {
        if (root.val < val) {
            return new TreeNode(val, root, null);
        }
        TreeNode cur = root;
        while (cur.right != null && cur.right.val > val) {
            cur = cur.right;
        }
        TreeNode node = new TreeNode(val);
        node.left = cur.right;
        cur.right = node;
        return root;
    }

    private static TreeNode build(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 i = 1;
        while (!q.isEmpty() && i < arr.length) {
            TreeNode cur = q.poll();
            if (arr[i] != null) {
                cur.left = new TreeNode(arr[i]);
                q.offer(cur.left);
            }
            ++i;
            if (i < arr.length && arr[i] != null) {
                cur.right = new TreeNode(arr[i]);
                q.offer(cur.right);
            }
            ++i;
        }
        return root;
    }

    private static Integer[] toLevel(TreeNode root) {
        java.util.List<Integer> res = new java.util.ArrayList<>();
        java.util.Queue<TreeNode> q = new java.util.LinkedList<>();
        q.offer(root);
        while (!q.isEmpty()) {
            TreeNode cur = q.poll();
            if (cur == null) {
                res.add(null);
                continue;
            }
            res.add(cur.val);
            q.offer(cur.left);
            q.offer(cur.right);
        }
        // 去掉尾部空位
        while (!res.isEmpty() && res.get(res.size() - 1) == null) {
            res.remove(res.size() - 1);
        }
        return res.toArray(new Integer[0]);
    }

    // 结构比较:两棵树逐节点判断值一致且左右孩子结构一致
    private static boolean sameTree(TreeNode a, TreeNode b) {
        if (a == null || b == null) {
            return a == b;
        }
        return a.val == b.val && sameTree(a.left, b.left) && sameTree(a.right, b.right);
    }

    private static void check(Integer[] arr, int val, Integer[] expected, int caseId) {
        TreeNode r1 = build(arr);
        TreeNode r2 = build(arr);
        TreeNode t1 = new Solution().insertIntoMaxTree(r1, val);
        TreeNode t2 = new Solution().insertIntoMaxTreeIter(r2, val);
        TreeNode exp = build(expected);
        boolean ok1 = sameTree(t1, exp);
        boolean ok2 = sameTree(t2, exp);
        if (!ok1 || !ok2) {
            System.out.println("FAIL case" + caseId + " expected=" + java.util.Arrays.toString(toLevel(exp))
                    + " got1=" + java.util.Arrays.toString(toLevel(t1))
                    + " got2=" + java.util.Arrays.toString(toLevel(t2)));
            return;
        }
        System.out.println("ok case" + caseId);
    }

    public static void main(String[] args) {
        check(new Integer[] {4, 1, 3, null, null, 2}, 5,
                new Integer[] {5, 4, null, 1, 3, null, null, 2}, 1);
        check(new Integer[] {5, 2, 4, null, 1}, 3,
                new Integer[] {5, 2, 4, null, 1, null, 3}, 2);
        check(new Integer[] {5, 2, 3, null, 1}, 4,
                new Integer[] {5, 2, 4, null, 1, 3}, 3);
        check(new Integer[] {1}, 2, new Integer[] {2, 1}, 4);
        check(new Integer[] {3, 1, 2}, 0, new Integer[] {3, 1, 2, null, null, null, 0}, 5);
        System.out.println("== 0998 PASS ==");
    }
}

复杂度分析

解法一:

  • 时间复杂度:O(n),最坏访问右脊全部节点;
  • 空间复杂度:O(h),递归栈,h 为右脊长度(最坏 O(n))。 解法二:
  • 时间复杂度:O(n);
  • 空间复杂度:O(1)。

999. Available Captures for Rook(可以被一步捕获的棋子数)

题目描述

给定一个 8 x 8 的棋盘,只有一个白色的车,用字符 'R' 表示。棋盘上还可能存在白色的象 'B' 以及黑色的卒 'p'。空方块用字符 '.' 表示。 车可以按水平或竖直方向(上,下,左,右)移动任意个方格直到它遇到另一个棋子或棋盘的边界。 如果它能够在一次移动中移动到棋子的方格,则能够吃掉棋子。 注意:车不能穿过其它棋子,比如象和卒。这意味着如果有其它棋子挡住了路径,车就不能够吃掉棋子。 返回白车攻击范围内兵的数量。

示例 1

  • 输入:[[".",".",".",".",".",".",".","."], [".",".",".","p",".",".",".","."], [".",".",".","R",".",".",".","p"], [".",".",".",".",".",".",".","."], [".",".",".",".",".",".",".","."], [".",".",".","p",".",".",".","."], [".",".",".",".",".",".",".","."], [".",".",".",".",".",".",".","."]]
  • 输出:3

示例 2

  • 输入:[[".",".",".",".",".",".",".","."], [".","p","p","p","p","p",".","."], [".","p","p","B","p","p",".","."], [".","p","B","R","B","p",".","."], [".","p","p","B","p","p",".","."], [".","p","p","p","p","p",".","."], [".",".",".",".",".",".",".","."], [".",".",".",".",".",".",".","."]]
  • 输出:0

示例 3

  • 输入:[[".",".",".",".",".",".",".","."], [".",".",".","p",".",".",".","."], [".",".",".","p",".",".",".","."], ["p","p",".","R",".","p","B","."], [".",".",".",".",".",".",".","."], [".",".",".","B",".",".",".","."], [".",".",".","p",".",".",".","."], [".",".",".",".",".",".",".","."]]
  • 输出:3 提示:
  • - board.length == 8
  • - board[i].length == 8
    • board[i][j] 可以是 'R','.','B' 或 'p'
    • 只有一个格子上存在 board[i][j] == 'R'

解题思路

解法一(四向模拟): 车可以沿上下左右四个方向直线移动,遇象 'B' 立即停下(象挡路),遇卒 'p' 可以吃掉 并停止该方向(吃到卒后不能再继续穿透)。步骤如下:

  1. 找到 'R' 的位置;
  2. 对四个方向分别沿边步进:越界或遇 'B' 停止;遇 'p' 计数 +1 并停止; '.' 则继续。 返回值即四个方向吃卒总数。 解法二(BFS 一步可达,供对照): 车在一次移动中能吃到兵等价于该兵在车所在行或列的直线上、且两者之间没有其他棋子。 对每个卒检查是否与车同行/同列且路径上无 'B'/'p' 阻挡。与解法一结果一致。

代码实现

public class Solution {
    // 解法一:四向模拟(主解法,提交 LeetCode)
    public int numRookCaptures(char[][] board) {
        int n = board.length;
        int rx = 0, ry = 0;
        outer:
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < n; ++j) {
                if (board[i][j] == 'R') {
                    rx = i;
                    ry = j;
                    break outer;
                }
            }
        }
        int[] dx = {-1, 0, 1, 0};
        int[] dy = {0, 1, 0, -1};
        int ans = 0;
        for (int d = 0; d < 4; ++d) {
            int x = rx + dx[d];
            int y = ry + dy[d];
            while (x >= 0 && x < n && y >= 0 && y < n) {
                char c = board[x][y];
                if (c == 'B') {
                    break;
                }
                if (c == 'p') {
                    ++ans;
                    break;
                }
                x += dx[d];
                y += dy[d];
            }
        }
        return ans;
    }

    // 解法二:逐卒检查直线(对照验证)
    public int numRookCapturesCheck(char[][] board) {
        int n = board.length;
        int rx = 0, ry = 0;
        outer:
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < n; ++j) {
                if (board[i][j] == 'R') {
                    rx = i;
                    ry = j;
                    break outer;
                }
            }
        }
        int ans = 0;
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < n; ++j) {
                if (board[i][j] != 'p') {
                    continue;
                }
                if (i == rx) {
                    // 同行:检查 (rx, min(ry,j)+1 .. max(ry,j)-1) 是否有棋子,且没有越过车时……车在端点
                    // 逻辑:直接判断与车之间无障碍
                    if (clearRow(board, rx, ry, j)) {
                        ++ans;
                    }
                } else if (j == ry) {
                    if (clearCol(board, ry, rx, i)) {
                        ++ans;
                    }
                }
            }
        }
        return ans;
    }

    private boolean clearRow(char[][] board, int row, int a, int b) {
        int lo = Math.min(a, b), hi = Math.max(a, b);
        for (int j = lo + 1; j < hi; ++j) {
            if (board[row][j] != '.') {
                return false;
            }
        }
        return true;
    }

    private boolean clearCol(char[][] board, int col, int a, int b) {
        int lo = Math.min(a, b), hi = Math.max(a, b);
        for (int i = lo + 1; i < hi; ++i) {
            if (board[i][col] != '.') {
                return false;
            }
        }
        return true;
    }

    private static char[][] str2board(String[] rows) {
        char[][] b = new char[8][8];
        for (int i = 0; i < 8; ++i) {
            b[i] = rows[i].toCharArray();
        }
        return b;
    }

    private static void check(String[] rows, int expected, int caseId) {
        char[][] b1 = str2board(rows);
        char[][] b2 = str2board(rows);
        int got1 = new Solution().numRookCaptures(b1);
        int got2 = new Solution().numRookCapturesCheck(b2);
        if (got1 != expected || got2 != expected) {
            System.out.println("FAIL case" + caseId + " expected=" + expected
                    + " got1=" + got1 + " got2=" + got2);
            return;
        }
        System.out.println("ok case" + caseId);
    }

    public static void main(String[] args) {
        check(new String[] {
                "........",
                "...p....",
                "...R...p",
                "........",
                "........",
                "...p....",
                "........",
                "........"
        }, 3, 1);
        check(new String[] {
                "........",
                ".ppppp..",
                ".ppBpp..",
                ".pBRBp..",
                ".ppBpp..",
                ".ppppp..",
                "........",
                "........"
        }, 0, 2);
        check(new String[] {
                "........",
                "...p....",
                "...p....",
                "pp.R.pB.",
                "........",
                "...B....",
                "...p....",
                "........"
        }, 3, 3);
        check(new String[] {
                "........",
                "........",
                "........",
                "...R....",
                "........",
                "........",
                "...p....",
                "........"
        }, 1, 4);
        check(new String[] {
                "........",
                "........",
                "........",
                "...R....",
                "........",
                "...B....",
                "...p....",
                "........"
        }, 0, 5);
        check(new String[] {
                "........",
                "........",
                "........",
                "...R....",
                "........",
                "........",
                "........",
                "........"
        }, 0, 6);
        System.out.println("== 0999 PASS ==");
    }
}

复杂度分析

解法一:

  • 时间复杂度:O(88 + 48) = O(1),棋盘固定大小;
  • 空间复杂度:O(1)。 解法二:
  • 时间复杂度:O(8*8 * 8) = O(1);
  • 空间复杂度:O(1)。

1000. Minimum Cost to Merge Stones(合并石头的最低成本)

题目描述

有 n 堆石头排成一排,第 i 堆中有 stones[i] 块石头。 每次移动需要将连续的 k 堆石头合并为一堆,而这次移动的成本为这 k 堆中石头的总数。 返回把所有石头合并成一堆的最低成本。如果无法合并成一堆,返回 -1。

示例 1

  • 输入:stones = [3,2,4,1], K = 2
  • 输出:20
  • 解释:合并 [3,2] 成本 5 → [5,4,1];合并 [4,1] 成本 5 → [5,5];合并 [5,5] 成本 10 → [10]。 总成本 20。

示例 2

  • 输入:stones = [3,2,4,1], K = 3
  • 输出:-1

示例 3

  • 输入:stones = [3,5,1,2,6], K = 3
  • 输出:25 提示:
  • - n == stones.length
  • - 1 <= n <= 30
  • - 1 <= stones[i] <= 100
  • - 2 <= k <= 30

解题思路

解法一(区间 DP + 前缀和): 每次把「连续 K 堆」合并为一堆,合并后总堆数减少 K-1。

  1. 可行性:最终合并成一堆 ⇔ (n-1) % (K-1) == 0,否则返回 -1。
  2. 定义 f[i][j][k] = 把区间 [i, j] 合并成 k 堆的最小成本,初始 f[i][i][1] = 0,其余 +∞。 转移(枚举断点 h):f[i][j][k] = min(f[i][h][1] + f[h+1][j][k-1])。 因为每次只能合并连续段,先使左段并成 1 堆、右段并成 k-1 堆,随后一并用掉一次合并。
  3. 当 k == K 时,把 K 堆并成 1 堆需要付出区间总和: f[i][j][1] = f[i][j][K] + (s[j] - s[i-1])。 按区间长度递增计算,答案为 f[1][n][1]。 解法二(记忆化搜索,供对照): 与解法一相同状态定义,用递归 + memo 实现,枚举断点方式一致,结果应与 DP 相同。

代码实现

public class Solution {
    // 解法一:区间 DP(主解法,提交 LeetCode)
    public int mergeStones(int[] stones, int K) {
        int n = stones.length;
        if ((n - 1) % (K - 1) != 0) {
            return -1;
        }
        int[] s = new int[n + 1];
        for (int i = 1; i <= n; ++i) {
            s[i] = s[i - 1] + stones[i - 1];
        }
        final int INF = 1 << 20;
        int[][][] f = new int[n + 1][n + 1][K + 1];
        for (int[][] g : f) {
            for (int[] row : g) {
                java.util.Arrays.fill(row, INF);
            }
        }
        for (int i = 1; i <= n; ++i) {
            f[i][i][1] = 0;
        }
        for (int len = 2; len <= n; ++len) {
            for (int i = 1; i + len - 1 <= n; ++i) {
                int j = i + len - 1;
                for (int k = 2; k <= K; ++k) {
                    for (int h = i; h < j; ++h) {
                        f[i][j][k] = Math.min(f[i][j][k],
                                f[i][h][1] + f[h + 1][j][k - 1]);
                    }
                }
                f[i][j][1] = f[i][j][K] + (s[j] - s[i - 1]);
            }
        }
        return f[1][n][1];
    }

    // 解法二:记忆化搜索(对照验证)
    private int K;
    private int[] ps;
    private int[][][] memo;

    public int mergeStonesMemo(int[] stones, int k) {
        int n = stones.length;
        if ((n - 1) % (k - 1) != 0) {
            return -1;
        }
        K = k;
        ps = new int[n + 1];
        for (int i = 1; i <= n; ++i) {
            ps[i] = ps[i - 1] + stones[i - 1];
        }
        memo = new int[n + 1][n + 1][K + 1];
        for (int[][] g : memo) {
            for (int[] row : g) {
                java.util.Arrays.fill(row, -1);
            }
        }
        return dfs(1, n, 1);
    }

    private int dfs(int i, int j, int piles) {
        if (i == j) {
            return piles == 1 ? 0 : INF;
        }
        if (piles < 1 || piles > K) {
            return INF;
        }
        if (memo[i][j][piles] != -1) {
            return memo[i][j][piles];
        }
        int best = INF;
        if (piles == 1) {
            // 先得到 K 堆,再合并为 1 堆
            int cost = dfs(i, j, K);
            best = cost + (ps[j] - ps[i - 1]);
        } else {
            for (int h = i; h < j; ++h) {
                int left = dfs(i, h, 1);
                int right = dfs(h + 1, j, piles - 1);
                best = Math.min(best, left + right);
            }
        }
        memo[i][j][piles] = best;
        return best;
    }

    private static final int INF = 1 << 20;

    private static void check(int[] stones, int k, int expected, int caseId) {
        int got1 = new Solution().mergeStones(stones.clone(), k);
        int got2 = new Solution().mergeStonesMemo(stones.clone(), k);
        if (got1 != expected || got2 != expected) {
            System.out.println("FAIL case" + caseId + " expected=" + expected
                    + " got1=" + got1 + " got2=" + got2);
            return;
        }
        System.out.println("ok case" + caseId);
    }

    public static void main(String[] args) {
        check(new int[] {3, 2, 4, 1}, 2, 20, 1);
        check(new int[] {3, 2, 4, 1}, 3, -1, 2);
        check(new int[] {3, 5, 1, 2, 6}, 3, 25, 3);
        check(new int[] {1}, 2, 0, 4);
        check(new int[] {1, 2}, 2, 3, 5);
        check(new int[] {1, 2, 3}, 2, 9, 6);
        check(new int[] {7}, 2, 0, 7);
        check(new int[] {1, 2, 3, 4}, 3, -1, 8); // (4-1)%(3-1)=3%2=1 → -1
        System.out.println("== 1000 PASS ==");
    }
}

复杂度分析

解法一:

  • 时间复杂度:O(n^3 * K);
  • 空间复杂度:O(n^2 * K)。 解法二:
  • 时间复杂度:O(n^3 * K)(状态同 DP);
  • 空间复杂度:O(n^2 * K)。