LeetCode651~675题目解析

LeetCode 651-675. 题解合集

文章目录


651. 4 Keys Keyboard(四个键的键盘)

题目描述

假设你有一个特殊的键盘包含下面的按键:

  • A:在屏幕上打印一个 'A'。
  • Ctrl-A:选中整个屏幕。
  • Ctrl-C:复制选中区域到缓冲区。
  • Ctrl-V:将缓冲区内容输出到上次输入的结束位置,并显示在屏幕上。
    现在,你可以最多 按键 n 次(使用上述四种按键),返回屏幕上最多可以显示 'A' 的个数。

示例 1

  • 输入 :n = 3
  • 输出:3
  • 解释:连续按三次 A,屏幕上显示 3 个 'A'。

示例 2

  • 输入 :n = 7
  • 输出:9
  • 解释 :A, A, A, Ctrl-A, Ctrl-C, Ctrl-V, Ctrl-V,先打出 3 个 A,再复制粘贴两次,共 9 个 A。
    提示:
  • 1 <= n <= 50

解题思路

解法一(动态规划,主提交):设 dp[i] 为前 i 次按键能得到的最大 A 数量。

要么一直按 A(dp[i] = i),要么在最后一次「全选+复制」之后的 t 次按键全部用于粘贴:

dp[i] = max(dp[i], dp[i - t - 2] * (t + 1))(其中 t >= 1,Ctrl-A + Ctrl-C 占 2 次)。

时间复杂度 O(n²),空间复杂度 O(n)。

解法二(首次复制前按 A 次数 + 完全背包):最优策略总是「先按若干次 A,然后进入

全选-复制-粘贴循环」。枚举首次复制前按 A 的次数 k,剩余 rest = n - k 次按键

是一个完全背包:每次「全选+复制+粘贴 t 次」消耗 t + 2 次按键、使数量变为原来的 t + 1 倍。

定义 f[r] 为 r 次按键内的最大倍增倍数,f[r] = max(f[r], f[r - t - 2] * (t + 1))。

答案为此前 k 个 A 与最大倍数之积的最大值。时间复杂度 O(n³)(n<=50 足够),空间 O(n)。

代码实现

java 复制代码
class Solution {

    // 解法一:动态规划
    public int maxA(int n) {
        int[] dp = new int[n + 1];
        for (int i = 0; i <= n; ++i) {
            dp[i] = i;
        }
        for (int i = 3; i <= n; ++i) {
            // t 为最后一次复制后的粘贴次数,Ctrl-A + Ctrl-C 占 2 次
            for (int t = 1; i - t - 2 >= 0; ++t) {
                dp[i] = Math.max(dp[i], dp[i - t - 2] * (t + 1));
            }
        }
        return dp[n];
    }

    // 解法二:枚举首次复制前按 A 的次数 + 完全背包
    public int maxA2(int n) {
        if (n <= 0) {
            return 0;
        }
        int best = n; // 基准:全部按 A
        for (int k = 1; k < n; ++k) {
            int rest = n - k;
            // f[r]:r 次按键内(全选复制粘贴循环)可获得的最大倍数
            long[] f = new long[rest + 1];
            f[0] = 1; // 0 次按键倍数为 1
            for (int i = 1; i <= rest; ++i) {
                f[i] = 1;
                for (int t = 1; i - t - 2 >= 0; ++t) {
                    f[i] = Math.max(f[i], f[i - t - 2] * (t + 1));
                }
            }
            best = (int) Math.max(best, (long) k * f[rest]);
        }
        return best;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (sol.maxA(0) == 0) ++pass; else System.out.println("FAIL test0: " + sol.maxA(0));
        ++total; if (sol.maxA(3) == 3) ++pass; else System.out.println("FAIL test1: " + sol.maxA(3));
        ++total; if (sol.maxA(7) == 9) ++pass; else System.out.println("FAIL test2: " + sol.maxA(7));
        ++total; if (sol.maxA(1) == 1) ++pass; else System.out.println("FAIL test3: " + sol.maxA(1));
        ++total; if (sol.maxA(2) == 2) ++pass; else System.out.println("FAIL test4: " + sol.maxA(2));
        ++total; if (sol.maxA(6) == 6) ++pass; else System.out.println("FAIL test5: " + sol.maxA(6));
        ++total; if (sol.maxA(10) == 20) ++pass; else System.out.println("FAIL test6: " + sol.maxA(10));
        ++total; if (sol.maxA(12) == 36) ++pass; else System.out.println("FAIL test7: " + sol.maxA(12));
        ++total; if (sol.maxA(50) == 1327104) ++pass; else System.out.println("FAIL test8: " + sol.maxA(50));
        // 解法二(与解法一逐值对拍)
        boolean ok2 = true;
        for (int n = 1; n <= 50; ++n) {
            if (sol.maxA2(n) != sol.maxA(n)) {
                ok2 = false;
                System.out.println("FAIL match n=" + n + " got=" + sol.maxA2(n) + " exp=" + sol.maxA(n));
            }
        }
        ++total; if (ok2) ++pass; else System.out.println("FAIL test9");
        if (pass == total) {
            System.out.println("== 0651 PASS ==");
        } else {
            System.out.println("== 0651 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

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

652. Find Duplicate Subtrees(寻找重复的子树)

题目描述

给你一棵二叉树的根节点 root,返回所有重复的子树 。

对于同一类的重复子树,你只需要返回其中任意一棵 的根结点即可。

如果两棵树具有相同的结构 和相同的结点值 ,则认为二者是重复的。

示例 1

  • 输入 :root = 1,2,3,4,`null`,2,4,`null`,`null`,4
  • 输出:\[2,4,4]

示例 2

  • 输入 :root = 2,1,1
  • 输出:\[1]

示例 3

  • 输入 :root = 2,2,2,3,`null`,3,`null`
  • 输出 :\[2,3,3]
    提示:
  • 树中的结点数在 [1, 5000] 范围内
  • -200 <= Node.val <= 200

解题思路

解法一(后序序列化 + 哈希计数,主提交):后序遍历每个子树,把它序列化成字符串

(如 "val,left,right",空节点用 # 表示),用哈希表记录每个序列的出现次数。

当某个序列出现次数恰好变为 2 时,把该子树根节点加入答案(之后不再加入,从而每个重复形态只输出一次)。

时间复杂度 O(n)(字符串哈希需 O(n) 构造,整体 O(n·单子树序列长),实际可近似 O(n)),空间 O(n)。

解法二(三元 ID 编码):为每个不同形态的子树分配全局唯一编号。encode(node) 返回

(val, leftId, rightId) 的三元组,若相同三元组则映射到同一编号。当编号计数为 2 时加入答案。

避免构造长字符串,时间复杂度 O(n),空间 O(n)。

代码实现

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

class Solution {

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

    // 解法一:后序序列化 + 哈希计数
    private Map<String, Integer> counter;
    private List<TreeNode> ans;

    public List<TreeNode> findDuplicateSubtrees(TreeNode root) {
        counter = new HashMap<>();
        ans = new ArrayList<>();
        dfs(root);
        return ans;
    }

    private String dfs(TreeNode root) {
        if (root == null) {
            return "#";
        }
        String v = root.val + "," + dfs(root.left) + "," + dfs(root.right);
        int c = counter.getOrDefault(v, 0) + 1;
        counter.put(v, c);
        if (c == 2) {
            ans.add(root);
        }
        return v;
    }

    // 解法二:三元组全局编码
    private Map<String, Integer> idMap = new HashMap<>();
    private Map<Integer, Integer> cntMap = new HashMap<>();
    private int nextId = 1;
    private List<TreeNode> ans2 = new ArrayList<>();

    public List<TreeNode> findDuplicateSubtrees2(TreeNode root) {
        idMap.clear();
        cntMap.clear();
        ans2 = new ArrayList<>();
        nextId = 1;
        encode(root);
        return ans2;
    }

    private int encode(TreeNode root) {
        if (root == null) {
            return 0;
        }
        String key = root.val + "#" + encode(root.left) + "#" + encode(root.right);
        int id;
        if (idMap.containsKey(key)) {
            id = idMap.get(key);
        } else {
            id = nextId++;
            idMap.put(key, id);
        }
        int c = cntMap.getOrDefault(id, 0) + 1;
        cntMap.put(id, c);
        if (c == 2) {
            ans2.add(root);
        }
        return id;
    }

    // 层序构建二叉树(测试辅助)
    private static TreeNode build(Integer[] arr) {
        if (arr == null || arr.length == 0) {
            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;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        TreeNode t1 = build(new Integer[] {1, 2, 3, 4, null, 2, 4, null, null, 4});
        List<TreeNode> r1 = sol.findDuplicateSubtrees(t1);
        ++total; if (r1.size() == 2) ++pass; else System.out.println("FAIL test1 size=" + r1.size());
        TreeNode t2 = build(new Integer[] {2, 1, 1});
        List<TreeNode> r2 = sol.findDuplicateSubtrees(t2);
        ++total; if (r2.size() == 1 && r2.get(0).val == 1) ++pass; else System.out.println("FAIL test2 size=" + r2.size());
        TreeNode t3 = build(new Integer[] {2, 2, 2, 3, null, 3, null});
        List<TreeNode> r3 = sol.findDuplicateSubtrees(t3);
        ++total; if (r3.size() == 2) ++pass; else System.out.println("FAIL test3 size=" + r3.size());
        TreeNode t4 = build(new Integer[] {1});
        ++total; if (sol.findDuplicateSubtrees(t4).isEmpty()) ++pass; else System.out.println("FAIL test4");
        List<TreeNode> r5 = sol.findDuplicateSubtrees2(t1);
        ++total; if (r5.size() == 2) ++pass; else System.out.println("FAIL test5 size=" + r5.size());
        List<TreeNode> r6 = sol.findDuplicateSubtrees2(t2);
        ++total; if (r6.size() == 1 && r6.get(0).val == 1) ++pass; else System.out.println("FAIL test6 size=" + r6.size());
        List<TreeNode> r7 = sol.findDuplicateSubtrees2(t3);
        ++total; if (r7.size() == 2) ++pass; else System.out.println("FAIL test7 size=" + r7.size());
        ++total; if (sol.findDuplicateSubtrees2(t4).isEmpty()) ++pass; else System.out.println("FAIL test8");
        if (pass == total) {
            System.out.println("== 0652 PASS ==");
        } else {
            System.out.println("== 0652 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(n²)(最坏) O(n²)(序列化字符串)
解法二 O(n) O(n)

653. Two Sum IV - Input is a BST(两数之和 IV - 输入二叉搜索树)

题目描述

给定一个二叉搜索树 root 和一个目标结果 k,如果二叉搜索树中存在两个元素且它们的和等于

给定的目标结果,则返回 true。

示例 1

  • 输入 :root = 5,3,6,2,4,`null`,7, k = 9
  • 输出 :true

示例 2

  • 输入 :root = 5,3,6,2,4,`null`,7, k = 28
  • 输出 :false
    提示:
  • 二叉树的节点个数的范围是 [1, 10^4]
  • -10^4 <= Node.val <= 10^4
  • 题目数据保证,输入的 root 是一棵有效的二叉搜索树
  • -10^5 <= k <= 10^5

解题思路

解法一(哈希集合 + DFS,主提交):在遍历过程中维护一个已访问节点值的集合。

若当前节点需要 k - val 已存在于集合中,说明存在两数之和为 k;否则把 val 加入集合。

一次 DFS 即可,无需利用 BST 的有序性。时间复杂度 O(n),空间复杂度 O(n)。

解法二(中序 + 双指针):BST 中序遍历得到升序数组(或直接用双指针遍历),

由于 BST 有序,可以头尾双指针逼近目标。时间复杂度 O(n),空间复杂度 O(n)(存中序序列)。

代码实现

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

class Solution {

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

    // 解法一:哈希集合 + DFS
    private Set<Integer> seen = new HashSet<>();

    public boolean findTarget(TreeNode root, int k) {
        seen.clear();
        return dfs(root, k);
    }

    private boolean dfs(TreeNode root, int k) {
        if (root == null) {
            return false;
        }
        if (seen.contains(k - root.val)) {
            return true;
        }
        seen.add(root.val);
        return dfs(root.left, k) || dfs(root.right, k);
    }

    // 解法二:中序 + 双指针
    public boolean findTarget2(TreeNode root, int k) {
        List<Integer> vals = new ArrayList<>();
        inorder(root, vals);
        int lo = 0, hi = vals.size() - 1;
        while (lo < hi) {
            int sum = vals.get(lo) + vals.get(hi);
            if (sum == k) {
                return true;
            }
            if (sum < k) {
                ++lo;
            } else {
                --hi;
            }
        }
        return false;
    }

    private void inorder(TreeNode root, List<Integer> vals) {
        if (root == null) {
            return;
        }
        inorder(root.left, vals);
        vals.add(root.val);
        inorder(root.right, vals);
    }

    // 由层序数组构建二叉树(测试辅助)
    private static TreeNode build(Integer[] arr) {
        if (arr == null || arr.length == 0) {
            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;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        TreeNode t1 = build(new Integer[] {5, 3, 6, 2, 4, null, 7});
        ++total; if (sol.findTarget(t1, 9)) ++pass; else System.out.println("FAIL test1: " + sol.findTarget(t1, 9));
        ++total; if (!sol.findTarget(t1, 28)) ++pass; else System.out.println("FAIL test2");
        ++total; if (sol.findTarget(t1, 7)) ++pass; else System.out.println("FAIL test3: " + sol.findTarget(t1, 7));
        ++total; if (sol.findTarget2(t1, 9)) ++pass; else System.out.println("FAIL test4: " + sol.findTarget2(t1, 9));
        ++total; if (!sol.findTarget2(t1, 28)) ++pass; else System.out.println("FAIL test5");
        ++total; if (sol.findTarget2(t1, 7)) ++pass; else System.out.println("FAIL test6: " + sol.findTarget2(t1, 7));
        TreeNode t2 = build(new Integer[] {1});
        ++total; if (!sol.findTarget(t2, 2)) ++pass; else System.out.println("FAIL test7");
        ++total; if (!sol.findTarget2(t2, 2)) ++pass; else System.out.println("FAIL test8");
        TreeNode t3 = build(new Integer[] {2, 1, 3});
        ++total; if (sol.findTarget(t3, 4)) ++pass; else System.out.println("FAIL test9: " + sol.findTarget(t3, 4));
        ++total; if (sol.findTarget2(t3, 4)) ++pass; else System.out.println("FAIL test10");
        if (pass == total) {
            System.out.println("== 0653 PASS ==");
        } else {
            System.out.println("== 0653 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

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

654. Maximum Binary Tree(最大二叉树)

题目描述

给定一个不重复的整数数组 nums。最大二叉树 可以用下面的算法从 nums 递归地构建:

  1. 创建一个根节点,其值为 nums 中的最大值。
  2. 递归地在最大值左边 的子数组前缀上构建左子树。
  3. 递归地在最大值右边 的子数组后缀 上构建右子树。
    返回 nums 构建的最大二叉树。

示例 1

  • 输入 :nums = 3,2,1,6,0,5
  • 输出 :6,3,5,`null`,2,0,`null`,`null`,1
  • 解释 :最大值 6 为根;左边 3,2,1 构建左子树(根 3,右子树 2,1 根 2、再右 1),
    右边 0,5 构建右子树(根 5,左孩子 0)。

示例 2

  • 输入 :nums = 3,2,1
  • 输出 :3,`null`,2,`null`,1
    提示:
  • 1 <= nums.length <= 1000
  • 0 <= nums[i] <= 1000
  • nums 中的所有整数互不相同

解题思路

解法一(递归 + 线性找最大值,主提交):定义 build(l, r) 构建区间 nums[l..r) 的最大二叉树。

每次扫描区间找最大值及其下标,作为根;左右区间分别递归。最坏情况下每次找最大值 O(n),

总时间复杂度 O(n²),但 n <= 1000 可以接受。空间复杂度 O(n)(递归栈深度 + 树本身)。

解法二(单调栈):数组可以看作"树的先序",利用单调递减栈,一个经典的 O(n) 建树方法:

从左到右遍历 nums,维护一个栈(栈内值递减)。对每个 v,弹出所有比 v 小的节点作为

v 的左子树(最后一个弹出的作为左孩子),然后 v 作为当前栈顶的右孩子(如果有)。

最终栈底元素即为根。时间复杂度 O(n),空间复杂度 O(n)。

代码实现

java 复制代码
class Solution {

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

    // 解法一:递归 + 线性找最大值
    public TreeNode constructMaximumBinaryTree(int[] nums) {
        return build(nums, 0, nums.length);
    }

    private TreeNode build(int[] nums, int l, int r) {
        if (l >= r) {
            return null;
        }
        int idx = l;
        for (int i = l + 1; i < r; ++i) {
            if (nums[i] > nums[idx]) {
                idx = i;
            }
        }
        TreeNode root = new TreeNode(nums[idx]);
        root.left = build(nums, l, idx);
        root.right = build(nums, idx + 1, r);
        return root;
    }

    // 解法二:单调栈 O(n) 建树
    public TreeNode constructMaximumBinaryTree2(int[] nums) {
        java.util.Deque<TreeNode> st = new java.util.ArrayDeque<>();
        for (int v : nums) {
            TreeNode cur = new TreeNode(v);
            while (!st.isEmpty() && st.peek().val < v) {
                cur.left = st.pop();
            }
            if (!st.isEmpty()) {
                st.peek().right = cur;
            }
            st.push(cur);
        }
        while (st.size() > 1) {
            st.pop();
        }
        return st.peek();
    }

    // 先序打印(测试辅助)
    private static String preorder(TreeNode root) {
        if (root == null) {
            return "null";
        }
        return root.val + "," + preorder(root.left) + "," + preorder(root.right);
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        TreeNode r1 = sol.constructMaximumBinaryTree(new int[] {3, 2, 1, 6, 0, 5});
        // 先序应为 6,3,null,2,null,1,null,null,5,0,null,null,null
        ++total; if (preorder(r1).equals("6,3,null,2,null,1,null,null,5,0,null,null,null")) ++pass; else System.out.println("FAIL test1: " + preorder(r1));
        TreeNode r2 = sol.constructMaximumBinaryTree(new int[] {3, 2, 1});
        ++total; if (preorder(r2).equals("3,null,2,null,1,null,null")) ++pass; else System.out.println("FAIL test2: " + preorder(r2));
        TreeNode r3 = sol.constructMaximumBinaryTree(new int[] {1});
        ++total; if (preorder(r3).equals("1,null,null")) ++pass; else System.out.println("FAIL test3: " + preorder(r3));
        // 解法二
        TreeNode r4 = sol.constructMaximumBinaryTree2(new int[] {3, 2, 1, 6, 0, 5});
        ++total; if (preorder(r4).equals("6,3,null,2,null,1,null,null,5,0,null,null,null")) ++pass; else System.out.println("FAIL test4: " + preorder(r4));
        TreeNode r5 = sol.constructMaximumBinaryTree2(new int[] {3, 2, 1});
        ++total; if (preorder(r5).equals("3,null,2,null,1,null,null")) ++pass; else System.out.println("FAIL test5: " + preorder(r5));
        TreeNode r6 = sol.constructMaximumBinaryTree2(new int[] {1});
        ++total; if (preorder(r6).equals("1,null,null")) ++pass; else System.out.println("FAIL test6: " + preorder(r6));
        if (pass == total) {
            System.out.println("== 0654 PASS ==");
        } else {
            System.out.println("== 0654 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

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

655. Print Binary Tree(输出二叉树)

题目描述

给你一棵二叉树的根节点 root,请你构造一个下标从 0 开始、大小为 m x n 的字符串矩阵 res,

用以表示树的格式化布局。构造此格式化布局矩阵需要遵循以下规则:

  • 树的高度 为 height,矩阵的行数 m 应该等于 height + 1。
  • 矩阵的列数 n 应该等于 2^(height+1) - 1。
  • 根节点需要放置在顶行 的正中间 ,对应位置为 res[0][(n-1)/2]。
  • 对于放置在矩阵中的每个节点,设对应位置为 res[r][c],将其左子节点放置在 res[r+1][c-2^(height-r-1)],
    右子节点放置在 res[r+1][c+2^(height-r-1)]。
  • 继续这一过程,直到树中的所有节点都妥善放置。
  • 任意空单元格都应该包含空字符串 ""。
    返回构造得到的矩阵 res。

示例 1

  • 输入 :root = 1,2
  • 输出:\["","1","","2","",""]

示例 2

  • 输入 :root = 1,2,3,`null`,4
  • 输出 :\["","","","1","","","","","2","","","","3","","","","4","","","",""]
    提示:
  • 树中节点数在范围 [1, 2^10] 内
  • -99 <= Node.val <= 99
  • 树的深度在范围 [1, 10] 内

解题思路

解法一(先求高再 DFS 填充,主提交):第一遍 DFS 求树高度 height;据此开 m = height+1 行、

n = 2^(height+1)-1 列的矩阵,全部初始化为 ""。第二遍 DFS:根放在 (0, (n-1)/2);

对节点 (r, c),左子放在 (r+1, c - 2^(height-r-1)),右子放在 (r+1, c + 2^(height-r-1))。

时间复杂度 O(m·n + 节点数),空间复杂度 O(m·n)(结果矩阵)。

解法二(层序遍历 + 公式定位):BFS 逐层遍历,同时携带每个节点所在行 r 与列 c(由父节点公式

推出),直接把节点值写入矩阵。需要先求高再 BFS。本质与解法一相同,实现上更贴近层次结构。

时间复杂度 O(m·n + 节点数),空间复杂度 O(m·n)。

代码实现

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

class Solution {

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

    // 解法一:先求高再 DFS 填充
    public List<List<String>> printTree(TreeNode root) {
        int h = height(root) - 1; // 高度(层数-1)
        int m = h + 1;
        int n = (1 << (h + 1)) - 1;
        List<List<String>> res = new ArrayList<>();
        for (int i = 0; i < m; ++i) {
            List<String> row = new ArrayList<>();
            for (int j = 0; j < n; ++j) {
                row.add("");
            }
            res.add(row);
        }
        fill(root, 0, (n - 1) / 2, h, res);
        return res;
    }

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

    private void fill(TreeNode root, int r, int c, int h, List<List<String>> res) {
        if (root == null) {
            return;
        }
        res.get(r).set(c, String.valueOf(root.val));
        int offset = 1 << (h - r - 1);
        fill(root.left, r + 1, c - offset, h, res);
        fill(root.right, r + 1, c + offset, h, res);
    }

    // 层序构建(测试辅助)
    private static TreeNode build(Integer[] arr) {
        if (arr == null || arr.length == 0) {
            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;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        List<List<String>> r;
        r = sol.printTree(build(new Integer[] {1, 2}));
        ++total; if (r.size() == 2 && r.get(0).get(1).equals("1") && r.get(1).get(0).equals("2")) ++pass; else System.out.println("FAIL test1: " + r);
        r = sol.printTree(build(new Integer[] {1, 2, 3, null, 4}));
        ++total; if (r.size() == 3 && r.get(0).get(3).equals("1") && r.get(1).get(1).equals("2") && r.get(1).get(5).equals("3") && r.get(2).get(2).equals("4")) ++pass; else System.out.println("FAIL test2: " + r);
        r = sol.printTree(build(new Integer[] {1}));
        ++total; if (r.size() == 1 && r.get(0).get(0).equals("1")) ++pass; else System.out.println("FAIL test3: " + r);
        r = sol.printTree(build(new Integer[] {5, 3, 6, 2, 4, null, 7}));
        ++total; if (r.size() == 3 && r.get(0).get(3).equals("5") && r.get(2).get(0).equals("2") && r.get(2).get(6).equals("7")) ++pass; else System.out.println("FAIL test4: " + r);
        if (pass == total) {
            System.out.println("== 0655 PASS ==");
        } else {
            System.out.println("== 0655 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

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

656. Coin Path(成本最小路径)

题目描述

给你一个整数数组 coins(下标从 1 开始)长度为 n,以及一个整数 maxJump。你可以跳到数组

coins 的任意下标 i(满足 coins[i] != -1),访问下标 i 时需要支付 coins[i]。此外,

如果你当前位于下标 i,你只能跳到下标 i + k(满足 i + k <= n),其中 k 是范围

[1, maxJump] 内的一个值。

初始时你位于下标 1(coins[1] 不是 -1)。你的目标是找到一条到达下标 n 的成本最小路径。

返回一个整数数组,包含你访问的下标顺序,以便你以最小成本达到下标 n。

如果存在多条成本相同的路径,返回字典序最小 的路径。如果无法达到下标 n,返回一个空数组。

路径 p1 = [Pa1, Pa2, ..., Pax] 的长度为 x,路径 p2 = [Pb1, Pb2, ..., Pby] 的长度为 y,

如果在两条路径的第一个不同的下标 j 处,Paj < Pbj,则 p1 在字典序上小于 p2;

如果不存在这样的 j,则较短的路径字典序较小。

示例 1

  • 输入 :coins = 1,2,4,-1,2, maxJump = 2
  • 输出:1,3,5

示例 2

  • 输入 :coins = 1,2,4,-1,2, maxJump = 1
  • 输出 :\[\]
    提示:
  • 1 <= coins.length <= 1000
  • -1 <= coins[i] <= 100
  • coins[1] != -1
  • 1 <= maxJump <= 100

解题思路

解法一(倒序 DP + next 数组贪心,主提交):设 f[i] 为从下标 i 出发到达下标 n-1

(0 基)的最小总花费。倒序 DP:f[i] = coins[i] + min(f[j])(j 在 (i, i+maxJump] 且

可达),不可达记为无穷大。同时记录 next[i]:取满足 f[j] == f[i] - coins[i] 的最小

下标 j(因为扫描 j 从小到大,先遇到的即最小,天然保证字典序最小)。DP 完成后若 f[0]

无穷大则返回空数组;否则沿 next 链正向输出路径。

时间复杂度 O(n·maxJump),空间复杂度 O(n)。

解法二(正向 DP + 前驱字典序比较):f[i] 为到下标 i 的最小花费,pre[i] 为最优前驱。

转移时对每个 i 枚举前驱 j(i - maxJump <= j < i):若 f[j] + coins[i] < f[i] 更新;

若花费相等,则在 j 与当前 pre[i] 之间比较两条完整路径的字典序 ,选择字典序较小者。

时间复杂度 O(n·maxJump·n)(比较路径),空间复杂度 O(n)。

代码实现

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

class Solution {

    // 解法一:倒序 DP + next 数组(j 从小往大扫,天然得到字典序最小)
    public List<Integer> cheapestJump(int[] coins, int maxJump) {
        int n = coins.length;
        List<Integer> ans = new ArrayList<>();
        if (coins[n - 1] == -1) {
            return ans;
        }
        final int INF = Integer.MAX_VALUE / 2;
        int[] f = new int[n];
        Arrays.fill(f, INF);
        f[n - 1] = coins[n - 1];
        int[] next = new int[n];
        Arrays.fill(next, -1);
        for (int i = n - 2; i >= 0; --i) {
            if (coins[i] == -1) {
                continue;
            }
            // j 从 i+1 开始递增:花费相同的多条路径中,先遇到的 j 更小 → 字典序更小
            for (int j = i + 1; j <= Math.min(n - 1, i + maxJump); ++j) {
                if (f[j] == INF) {
                    continue;
                }
                if (f[j] + coins[i] < f[i]) {
                    f[i] = f[j] + coins[i];
                    next[i] = j;
                } else if (f[j] + coins[i] == f[i] && (next[i] == -1 || j < next[i])) {
                    if (next[i] == -1 || j < next[i]) {
                        next[i] = j;
                    }
                }
            }
        }
        if (f[0] == INF) {
            return ans;
        }
        int cur = 0;
        while (cur != -1) {
            ans.add(cur + 1);
            cur = next[cur];
        }
        return ans;
    }

    // 解法二:正向 DP + 前驱完整路径字典序比较
    public List<Integer> cheapestJump2(int[] coins, int maxJump) {
        int n = coins.length;
        List<Integer> ans = new ArrayList<>();
        if (coins[n - 1] == -1) {
            return ans;
        }
        final int INF = Integer.MAX_VALUE / 2;
        int[] f = new int[n];
        Arrays.fill(f, INF);
        int[] pre = new int[n];
        Arrays.fill(pre, -1);
        f[0] = coins[0];
        for (int i = 1; i < n; ++i) {
            if (coins[i] == -1) {
                continue;
            }
            for (int j = Math.max(0, i - maxJump); j < i; ++j) {
                if (coins[j] == -1 || f[j] == INF) {
                    continue;
                }
                int cand = f[j] + coins[i];
                if (cand < f[i]) {
                    f[i] = cand;
                    pre[i] = j;
                } else if (cand == f[i]) {
                    // 花费相同:比较两条完整路径
                    List<Integer> pa = buildPath(pre, j, i);
                    List<Integer> pb = pre[i] == -1 ? null : buildPath(pre, pre[i], i);
                    if (pb == null || cmp(pa, pb) < 0) {
                        pre[i] = j;
                    }
                }
            }
        }
        if (f[n - 1] == INF) {
            return ans;
        }
        int cur = n - 1;
        List<Integer> rev = new ArrayList<>();
        while (cur != -1) {
            rev.add(cur + 1);
            cur = pre[cur];
        }
        for (int i = rev.size() - 1; i >= 0; --i) {
            ans.add(rev.get(i));
        }
        return ans;
    }

    // 重建 path=[0,...,j] 并追加 i(下标按 1 基输出)
    private List<Integer> buildPath(int[] pre, int j, int i) {
        List<Integer> rev = new ArrayList<>();
        int cur = j;
        rev.add(i);
        while (cur != -1) {
            rev.add(cur);
            cur = pre[cur];
        }
        List<Integer> path = new ArrayList<>();
        for (int k = rev.size() - 1; k >= 0; --k) {
            path.add(rev.get(k) + 1);
        }
        return path;
    }

    // 字典序比较:a < b 返回负
    private int cmp(List<Integer> a, List<Integer> b) {
        int m = Math.min(a.size(), b.size());
        for (int i = 0; i < m; ++i) {
            if (!a.get(i).equals(b.get(i))) {
                return a.get(i) - b.get(i);
            }
        }
        return a.size() - b.size();
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        List<Integer> r;
        r = sol.cheapestJump(new int[] {1, 2, 4, -1, 2}, 2);
        ++total; if (r.equals(Arrays.asList(1, 3, 5))) ++pass; else System.out.println("FAIL test1: " + r);
        r = sol.cheapestJump(new int[] {1, 2, 4, -1, 2}, 1);
        ++total; if (r.isEmpty()) ++pass; else System.out.println("FAIL test2: " + r);
        r = sol.cheapestJump(new int[] {1, 3, 2}, 3);
        ++total; if (r.equals(Arrays.asList(1, 3))) ++pass; else System.out.println("FAIL test3: " + r);
        r = sol.cheapestJump(new int[] {0, 0, 0}, 2);
        ++total; if (r.equals(Arrays.asList(1, 2, 3))) ++pass; else System.out.println("FAIL test4: " + r);
        r = sol.cheapestJump(new int[] {1, 2, 1, -1, 2}, 2);
        ++total; if (r.equals(Arrays.asList(1, 3, 5))) ++pass; else System.out.println("FAIL test5: " + r);
        r = sol.cheapestJump(new int[] {0, 0, 0, 0}, 2);
        ++total; if (r.equals(Arrays.asList(1, 2, 3, 4))) ++pass; else System.out.println("FAIL test6: " + r);
        // 解法二
        r = sol.cheapestJump2(new int[] {1, 2, 4, -1, 2}, 2);
        ++total; if (r.equals(Arrays.asList(1, 3, 5))) ++pass; else System.out.println("FAIL test7: " + r);
        r = sol.cheapestJump2(new int[] {1, 2, 4, -1, 2}, 1);
        ++total; if (r.isEmpty()) ++pass; else System.out.println("FAIL test8: " + r);
        r = sol.cheapestJump2(new int[] {1, 3, 2}, 3);
        ++total; if (r.equals(Arrays.asList(1, 3))) ++pass; else System.out.println("FAIL test9: " + r);
        r = sol.cheapestJump2(new int[] {0, 0, 0}, 2);
        ++total; if (r.equals(Arrays.asList(1, 2, 3))) ++pass; else System.out.println("FAIL test10: " + r);
        if (pass == total) {
            System.out.println("== 0656 PASS ==");
        } else {
            System.out.println("== 0656 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

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

657. Robot Return to Origin(机器人能否返回原点)

题目描述

在二维平面上,有一个机器人从原点 (0, 0) 开始。给出它的移动顺序,判断这个机器人在完成移动后

是否在 (0, 0) 处结束。

移动顺序由字符串 moves 表示。字符 move[i] 表示其第 i 次移动。机器人的有效动作有 R(右)、

L(左)、U(上)和 D(下)。如果机器人在完成所有动作后返回原点,则返回 true,否则返回 false。

注意:机器人"面朝"的方向无关紧要。"R" 将始终使机器人向右移动一次,"L" 将始终向左移动等。

此外,假设每次移动机器人的移动幅度相同。

示例 1

  • 输入 :moves = "UD"
  • 输出 :true
  • 解释:机器人向上移动一次,然后向下移动一次,最终回到原点。

示例 2

  • 输入 :moves = "LL"
  • 输出 :false
  • 解释 :机器人向左移动两次,最终位于原点的左侧,未返回原点。
    提示:
  • 1 <= moves.length <= 2 * 10^4
  • moves 只包含字符 'U'、'D'、'L' 和 'R'

解题思路

判断移动序列是否回到原点,只需净位移为零。

解法一(直接模拟,主提交):维护坐标 (x, y),按 UDLR 加减,最后判断是否都为 0。

时间复杂度 O(n),空间复杂度 O(1)。

解法二(计数统计):分别统计 U/D、L/R 的个数,要求 count('U') == count('D') 且

count('L') == count('R')。也可用数组统计后比较。时间复杂度 O(n),空间 O(1)(只统计四种字符,用 2 个计数)。

代码实现

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

class Solution {

    // 解法一:模拟坐标位移
    public boolean judgeCircle(String moves) {
        int x = 0, y = 0;
        for (int i = 0; i < moves.length(); ++i) {
            char c = moves.charAt(i);
            if (c == 'U') {
                ++y;
            } else if (c == 'D') {
                --y;
            } else if (c == 'L') {
                --x;
            } else if (c == 'R') {
                ++x;
            }
        }
        return x == 0 && y == 0;
    }

    // 解法二:计数统计(上下/左右分别抵消)
    public boolean judgeCircle2(String moves) {
        int ud = 0, lr = 0;
        for (int i = 0; i < moves.length(); ++i) {
            char c = moves.charAt(i);
            if (c == 'U') {
                ++ud;
            } else if (c == 'D') {
                --ud;
            } else if (c == 'L') {
                ++lr;
            } else {
                --lr;
            }
        }
        return ud == 0 && lr == 0;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (sol.judgeCircle("UD")) ++pass; else System.out.println("FAIL test1: " + sol.judgeCircle("UD"));
        ++total; if (!sol.judgeCircle("LL")) ++pass; else System.out.println("FAIL test2: " + sol.judgeCircle("LL"));
        ++total; if (!sol.judgeCircle("RRDD")) ++pass; else System.out.println("FAIL test3: " + sol.judgeCircle("RRDD"));
        ++total; if (sol.judgeCircle("UDLRUDLRUD")) ++pass; else System.out.println("FAIL test4: " + sol.judgeCircle("UDLRUDLRUD"));
        ++total; if (Collections.frequency(java.util.Arrays.asList('U','D','U','D','L','R'), 'U') == 2) ++pass; else System.out.println("FAIL test5");
        ++total; if (sol.judgeCircle2("UD")) ++pass; else System.out.println("FAIL test6: " + sol.judgeCircle2("UD"));
        ++total; if (!sol.judgeCircle2("LL")) ++pass; else System.out.println("FAIL test7: " + sol.judgeCircle2("LL"));
        ++total; if (sol.judgeCircle2("RLUD")) ++pass; else System.out.println("FAIL test8: " + sol.judgeCircle2("RLUD"));
        ++total; if (sol.judgeCircle2("UUUDDDLLLLRRRR")) ++pass; else System.out.println("FAIL test9: " + sol.judgeCircle2("UUUDDDLLLLRRRR"));
        if (pass == total) {
            System.out.println("== 0657 PASS ==");
        } else {
            System.out.println("== 0657 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

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

658. Find K Closest Elements(找到 K 个最接近的元素)

题目描述

给定一个排序好 的数组 arr,两个整数 k 和 x,从数组中找到最靠近 x(两数之差最小)的

k 个数。返回的结果必须要是按升序排好的。

整数 a 比整数 b 更接近 x 需要满足:

  • |a - x| < |b - x| 或者
  • |a - x| == |b - x| 且 a < b

示例 1

  • 输入 :arr = 1,2,3,4,5, k = 4, x = 3
  • 输出:1,2,3,4

示例 2

  • 输入 :arr = 1,1,2,3,4,5, k = 4, x = -1
  • 输出 :1,1,2,3
    提示:
  • 1 <= k <= arr.length
  • 1 <= arr.length <= 10^4
  • arr 按升序排列
  • -10^4 <= arr[i], x <= 10^4

解题思路

解法一(排序取近,主提交):元素本身有序,但按"距 x 的距离"排序后取前 k 个,再按值排序返回。

距离相同取数值更小的。时间复杂度 O(n log n),空间复杂度 O(n)。

解法二(二分 + 双指针收缩):先用二分找出与 x 最接近的分界点,然后从该位置向左右扩张,

每次取左右边界中距 x 更近(绝对值更小;距离相等取数值更小者)的一侧,直到收集满 k 个。

由于数组有序,收集到的元素自然保持升序。时间复杂度 O(log n + k),空间复杂度 O(k)(结果)。

代码实现

java 复制代码
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

class Solution {

    // 解法一:按距离排序后取前 k 个
    public List<Integer> findClosestElements(int[] arr, int k, int x) {
        List<Integer> idx = new ArrayList<>();
        for (int i = 0; i < arr.length; ++i) {
            idx.add(i);
        }
        Collections.sort(idx, (a, b) -> {
            int da = Math.abs(arr[a] - x), db = Math.abs(arr[b] - x);
            if (da != db) {
                return da - db;
            }
            return arr[a] - arr[b];
        });
        List<Integer> ans = new ArrayList<>();
        for (int i = 0; i < k; ++i) {
            ans.add(arr[idx.get(i)]);
        }
        Collections.sort(ans);
        return ans;
    }

    // 解法二:二分定位 + 双指针收缩
    public List<Integer> findClosestElements2(int[] arr, int k, int x) {
        int lo = 0, hi = arr.length - 1;
        while (lo + 1 < hi) {
            int mid = (lo + hi) >>> 1;
            if (arr[mid] <= x) {
                lo = mid;
            } else {
                hi = mid;
            }
        }
        int left = lo, right = hi;
        List<Integer> window = new ArrayList<>();
        // 先收集左右边界各一个(若 left==right 只收一个)
        while (left >= 0 && right < arr.length && window.size() < k) {
            int dl = Math.abs(arr[left] - x), dr = Math.abs(arr[right] - x);
            if (dl < dr || (dl == dr && arr[left] <= arr[right])) {
                window.add(arr[left]);
                --left;
            } else {
                window.add(arr[right]);
                ++right;
            }
        }
        while (left >= 0 && window.size() < k) {
            window.add(arr[left]);
            --left;
        }
        while (right < arr.length && window.size() < k) {
            window.add(arr[right]);
            ++right;
        }
        Collections.sort(window);
        return window;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        List<Integer> r;
        r = sol.findClosestElements(new int[] {1, 2, 3, 4, 5}, 4, 3);
        ++total; if (r.equals(java.util.Arrays.asList(1, 2, 3, 4))) ++pass; else System.out.println("FAIL test1: " + r);
        r = sol.findClosestElements(new int[] {1, 1, 2, 3, 4, 5}, 4, -1);
        ++total; if (r.equals(java.util.Arrays.asList(1, 1, 2, 3))) ++pass; else System.out.println("FAIL test2: " + r);
        r = sol.findClosestElements(new int[] {1, 2, 3, 4, 5}, 1, 3);
        ++total; if (r.equals(java.util.Arrays.asList(3))) ++pass; else System.out.println("FAIL test3: " + r);
        r = sol.findClosestElements(new int[] {1, 3}, 1, 2);
        ++total; if (r.equals(java.util.Arrays.asList(1))) ++pass; else System.out.println("FAIL test4: " + r);
        r = sol.findClosestElements(new int[] {1, 5, 9}, 2, 3);
        ++total; if (r.equals(java.util.Arrays.asList(1, 5))) ++pass; else System.out.println("FAIL test5: " + r);
        // 解法二
        r = sol.findClosestElements2(new int[] {1, 2, 3, 4, 5}, 4, 3);
        ++total; if (r.equals(java.util.Arrays.asList(1, 2, 3, 4))) ++pass; else System.out.println("FAIL test6: " + r);
        r = sol.findClosestElements2(new int[] {1, 1, 2, 3, 4, 5}, 4, -1);
        ++total; if (r.equals(java.util.Arrays.asList(1, 1, 2, 3))) ++pass; else System.out.println("FAIL test7: " + r);
        r = sol.findClosestElements2(new int[] {1, 3}, 1, 2);
        ++total; if (r.equals(java.util.Arrays.asList(1))) ++pass; else System.out.println("FAIL test8: " + r);
        r = sol.findClosestElements2(new int[] {1, 5, 9}, 2, 3);
        ++total; if (r.equals(java.util.Arrays.asList(1, 5))) ++pass; else System.out.println("FAIL test9: " + r);
        r = sol.findClosestElements2(new int[] {1, 2, 3, 4, 5}, 3, 4);
        ++total; if (r.equals(java.util.Arrays.asList(3, 4, 5))) ++pass; else System.out.println("FAIL test10: " + r);
        if (pass == total) {
            System.out.println("== 0658 PASS ==");
        } else {
            System.out.println("== 0658 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

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

659. Split Array into Consecutive Subsequences(分割数组为连续子序列)

题目描述

给你一个按非递减顺序 排列的整数数组 nums。

请你判断是否能在将 nums 分割成一个或多个子序列 的同时满足下述两个条件:

  • 每个子序列都是一个连续递增序列 (即,每个整数恰好比前一个整数大 1)。
  • 所有子序列的长度至少 为 3。
    如果可以分割 nums 并满足上述条件,则返回 true;否则,返回 false。

示例 1

  • 输入 :nums = 1,2,3,3,4,5
  • 输出 :true
  • 解释 :nums 可以分割成 1,2,3 和 3,4,5 两个子序列。

示例 2

  • 输入 :nums = 1,2,3,3,4,4,5,5
  • 输出 :true
  • 解释 :nums 可以分割成 1,2,3,4,5 和 3,4,5 两个子序列。

示例 3

  • 输入 :nums = 1,2,3,4,4,5
  • 输出 :false
  • 解释 :无法将 nums 分割成长度至少为 3 的连续递增子序列。
    提示:
  • 1 <= nums.length <= 10^4
  • -1000 <= nums[i] <= 1000
  • nums 按非递减顺序排列

解题思路

解法一(哈希表 + 最小堆,主提交):d[v] 维护以整数 v 结尾的各子序列长度的最小堆

(即记录所有"尾巴值为 v 的序列"的长度)。遍历 nums 中每个值 v:

  • 若存在以 v-1 结尾的序列,则把 v 接到其中最短的一条上(弹出长度 l,更新为 l+1
    放入 d[v];若 d[v-1] 空则移除键);
  • 否则以 v 新开一条长度 1 的序列放入 d[v]。
    最后检查所有序列中的最短长度都不小于 3。贪心取最短序列,保证尾部长度尽量小,后续才能继续延长。
    时间复杂度 O(n log n),空间复杂度 O(n)。
    解法二(两个计数器贪心):cnt[v] 记录 v 的剩余个数,tail[v] 记录以 v 结尾、长度 ≥3 的
    子序列个数。遍历每个 v:优先填入已有长度 ≥3 的序列尾部(tail[v-1]),否则新开一个
    v, v+1, v+2 的三元组(若 v+1、v+2 不够则失败)。时间复杂度 O(n),空间复杂度 O(n)。

代码实现

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

class Solution {

    // 解法一:哈希表 + 最小堆
    public boolean isPossible(int[] nums) {
        Map<Integer, java.util.PriorityQueue<Integer>> d = new HashMap<>();
        for (int v : nums) {
            if (d.containsKey(v - 1)) {
                java.util.PriorityQueue<Integer> q = d.get(v - 1);
                int len = q.poll() + 1;
                if (q.isEmpty()) {
                    d.remove(v - 1);
                }
                d.computeIfAbsent(v, k -> new java.util.PriorityQueue<>()).offer(len);
            } else {
                d.computeIfAbsent(v, k -> new java.util.PriorityQueue<>()).offer(1);
            }
        }
        for (java.util.PriorityQueue<Integer> q : d.values()) {
            if (q.peek() < 3) {
                return false;
            }
        }
        return true;
    }

    // 解法二:双计数器贪心
    public boolean isPossible2(int[] nums) {
        Map<Integer, Integer> cnt = new HashMap<>();
        Map<Integer, Integer> tail = new HashMap<>();
        for (int x : nums) {
            cnt.put(x, cnt.getOrDefault(x, 0) + 1);
        }
        for (int x : nums) {
            if (cnt.get(x) <= 0) {
                continue;
            }
            if (tail.getOrDefault(x - 1, 0) > 0) {
                tail.put(x - 1, tail.get(x - 1) - 1);
                tail.put(x, tail.getOrDefault(x, 0) + 1);
                cnt.put(x, cnt.get(x) - 1);
            } else {
                int c1 = cnt.getOrDefault(x + 1, 0);
                int c2 = cnt.getOrDefault(x + 2, 0);
                if (c1 > 0 && c2 > 0) {
                    cnt.put(x, cnt.get(x) - 1);
                    cnt.put(x + 1, c1 - 1);
                    cnt.put(x + 2, c2 - 1);
                    tail.put(x + 2, tail.getOrDefault(x + 2, 0) + 1);
                } else {
                    return false;
                }
            }
        }
        return true;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (sol.isPossible(new int[] {1, 2, 3, 3, 4, 5})) ++pass; else System.out.println("FAIL test1: " + sol.isPossible(new int[] {1, 2, 3, 3, 4, 5}));
        ++total; if (sol.isPossible(new int[] {1, 2, 3, 3, 4, 4, 5, 5})) ++pass; else System.out.println("FAIL test2: " + sol.isPossible(new int[] {1, 2, 3, 3, 4, 4, 5, 5}));
        ++total; if (!sol.isPossible(new int[] {1, 2, 3, 4, 4, 5})) ++pass; else System.out.println("FAIL test3");
        ++total; if (sol.isPossible(new int[] {1, 2, 2, 3, 3, 4, 4, 5})) ++pass; else System.out.println("FAIL test4: " + sol.isPossible(new int[] {1, 2, 2, 3, 3, 4, 4, 5}));
        ++total; if (sol.isPossible(new int[] {1, 2, 3, 5, 6, 7})) ++pass; else System.out.println("FAIL test5: " + sol.isPossible(new int[] {1, 2, 3, 5, 6, 7}));
        ++total; if (sol.isPossible2(new int[] {1, 2, 3, 3, 4, 5})) ++pass; else System.out.println("FAIL test6");
        ++total; if (sol.isPossible2(new int[] {1, 2, 3, 3, 4, 4, 5, 5})) ++pass; else System.out.println("FAIL test7");
        ++total; if (!sol.isPossible2(new int[] {1, 2, 3, 4, 4, 5})) ++pass; else System.out.println("FAIL test8");
        ++total; if (sol.isPossible2(new int[] {1, 2, 2, 3, 3, 4, 4, 5})) ++pass; else System.out.println("FAIL test9");
        ++total; if (sol.isPossible2(new int[] {1, 2, 3, 5, 6, 7})) ++pass; else System.out.println("FAIL test10");
        if (pass == total) {
            System.out.println("== 0659 PASS ==");
        } else {
            System.out.println("== 0659 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

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

660. Remove 9(移除 9)

题目描述

从 1 开始,移除包含数字 9 的所有整数,例如 9,19,29,......

这样就获得了一个新的整数数列:1,2,3,4,5,6,7,8,10,11,......

给你一个整数 n,请你返回新数列中第 n 个数字是多少(下标从 1 开始)。

示例 1

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

示例 2

  • 输入 :n = 10
  • 输出 :11
    提示:
  • 1 <= n <= 8 * 10^8

解题思路

不含数字 9 的正整数,等价于用 0~8 这 9 个符号表示的数。换言之,第 n 个合法数就是把 n

看成九进制数 ,然后按"逢 9 跳过"的规则每位数映射到 0~8。

解法一(九进制转换,主提交):把 n 转成以 9 为底的结果:ans = ans * 9 + (n % 9),

不断 n /= 9,得到的数即为第 n 个不含 9 的数(因为去掉了含 9 的数等价于少用一个数字,

计数系统从十进制退化为九进制)。时间复杂度 O(log₉ n),空间复杂度 O(1)。

解法二(进制 + 累加修正):直接按九进制逐位解释:ans = ans * 9 + digit,其中 digit 取自

n 在十进制下每一位;随后结果即答案。它与解法一本质相同,只是模拟"从低位往高位进位"。

时间复杂度 O(log n),空间复杂度 O(1)。

代码实现

java 复制代码
class Solution {

    // 解法一:直接九进制转换
    public int newInteger(int n) {
        int ans = 0;
        int mult = 1;
        while (n > 0) {
            ans += (n % 9) * mult;
            n /= 9;
            mult *= 10;
        }
        return ans;
    }

    // 解法二:按十进制位逐位转九进制解释(与解法一等价,便于理解)
    public int newInteger2(int n) {
        int ans = 0, base = 1;
        while (n > 0) {
            int digit = n % 9;      // 该位取 0~8
            ans += digit * base;
            n /= 9;
            base *= 10;
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (sol.newInteger(9) == 10) ++pass; else System.out.println("FAIL test1: " + sol.newInteger(9));
        ++total; if (sol.newInteger(10) == 11) ++pass; else System.out.println("FAIL test2: " + sol.newInteger(10));
        ++total; if (sol.newInteger(1) == 1) ++pass; else System.out.println("FAIL test3: " + sol.newInteger(1));
        ++total; if (sol.newInteger(8) == 8) ++pass; else System.out.println("FAIL test4: " + sol.newInteger(8));
        ++total; if (sol.newInteger(11) == 12) ++pass; else System.out.println("FAIL test5: " + sol.newInteger(11));
        ++total; if (sol.newInteger(20) == 22) ++pass; else System.out.println("FAIL test6: " + sol.newInteger(20));
        ++total; if (sol.newInteger(100) == 121) ++pass; else System.out.println("FAIL test7: " + sol.newInteger(100));
        ++total; if (sol.newInteger2(9) == 10) ++pass; else System.out.println("FAIL test8");
        ++total; if (sol.newInteger2(10) == 11) ++pass; else System.out.println("FAIL test9");
        ++total; if (sol.newInteger2(100) == 121) ++pass; else System.out.println("FAIL test10");
        if (pass == total) {
            System.out.println("== 0660 PASS ==");
        } else {
            System.out.println("== 0660 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

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

661. Image Smoother(图片平滑器)

题目描述

图像平滑器是大小为 3 x 3 的过滤器,用于对图像的每个单元格平滑处理,平滑处理后单元格的值为

该单元格的平均灰度。

每个单元格的平均灰度 定义为:该单元格自身及其周围的 8 个单元格的平均值,结果需向下取整。

如果一个单元格周围存在单元格缺失的情况,则计算平均灰度时不考虑缺失的单元格。

给你一个表示图像灰度的 m x n 整数矩阵 img,返回对图像的每个单元格平滑处理后的图像。

示例 1

  • 输入:img = \[1,1,1,1,0,1,1,1,1]
  • 输出:\[0,0,0,0,0,0,0,0,0]
  • 解释:四个角取周围 4 个格的平均(3/4=0),边上取 5/6、中心取 8/9,都向下取整为 0。

示例 2

  • 输入:img = \[100,200,100,200,50,200,100,200,100]
  • 输出 :\[137,141,137,141,138,141,137,141,137]
    提示:
  • m == img.length
  • n == img[i].length
  • 1 <= m, n <= 200
  • 0 <= img[i][j] <= 255

解题思路

解法一(暴力枚举 3x3 邻域,主提交):对每个格子 (i, j),枚举其周围 [i-1, i+1] × [j-1, j+1]

范围内所有合法格子,累加和并计数,最后 sum / cnt 取整写入新矩阵。

注意需写回新 数组,避免原地覆盖影响后续格子。时间复杂度 O(m·n·9),空间复杂度 O(m·n)(结果)。

解法二(二维前缀和加速):先计算 (m+1) x (n+1) 的二维前缀和,再对每个格子用前缀和在 O(1)

时间内求出 3x3 邻域(裁剪边界)的和与个数并取平均。时间复杂度 O(m·n),空间复杂度 O(m·n)(前缀和)。

代码实现

java 复制代码
class Solution {

    // 解法一:直接枚举 3x3 邻域
    public int[][] imageSmoother(int[][] img) {
        int m = img.length, n = img[0].length;
        int[][] res = new int[m][n];
        int[] dirs = {-1, 0, 1};
        for (int i = 0; i < m; ++i) {
            for (int j = 0; j < n; ++j) {
                int sum = 0, cnt = 0;
                for (int dx : dirs) {
                    for (int dy : dirs) {
                        int ni = i + dx, nj = j + dy;
                        if (ni >= 0 && ni < m && nj >= 0 && nj < n) {
                            sum += img[ni][nj];
                            ++cnt;
                        }
                    }
                }
                res[i][j] = sum / cnt;
            }
        }
        return res;
    }

    // 解法二:二维前缀和
    public int[][] imageSmoother2(int[][] img) {
        int m = img.length, n = img[0].length;
        int[][] pre = new int[m + 1][n + 1];
        for (int i = 1; i <= m; ++i) {
            for (int j = 1; j <= n; ++j) {
                pre[i][j] = pre[i - 1][j] + pre[i][j - 1] - pre[i - 1][j - 1] + img[i - 1][j - 1];
            }
        }
        int[][] res = new int[m][n];
        for (int i = 0; i < m; ++i) {
            for (int j = 0; j < n; ++j) {
                int x1 = Math.max(0, i - 1), y1 = Math.max(0, j - 1);
                int x2 = Math.min(m - 1, i + 1), y2 = Math.min(n - 1, j + 1);
                int cnt = (x2 - x1 + 1) * (y2 - y1 + 1);
                int sum = pre[x2 + 1][y2 + 1] - pre[x1][y2 + 1] - pre[x2 + 1][y1] + pre[x1][y1];
                res[i][j] = sum / cnt;
            }
        }
        return res;
    }

    private static boolean eq(int[][] a, int[][] b) {
        if (a.length != b.length) {
            return false;
        }
        for (int i = 0; i < a.length; ++i) {
            if (!java.util.Arrays.equals(a[i], b[i])) {
                return false;
            }
        }
        return true;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        int[][] r;
        r = sol.imageSmoother(new int[][] {{1, 1, 1}, {1, 0, 1}, {1, 1, 1}});
        ++total; if (eq(r, new int[][] {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}})) ++pass; else System.out.println("FAIL test1");
        r = sol.imageSmoother(new int[][] {{100, 200, 100}, {200, 50, 200}, {100, 200, 100}});
        ++total; if (eq(r, new int[][] {{137, 141, 137}, {141, 138, 141}, {137, 141, 137}})) ++pass; else System.out.println("FAIL test2");
        r = sol.imageSmoother(new int[][] {{2, 3}});
        ++total; if (eq(r, new int[][] {{2, 2}})) ++pass; else System.out.println("FAIL test3");
        r = sol.imageSmoother2(new int[][] {{1, 1, 1}, {1, 0, 1}, {1, 1, 1}});
        ++total; if (eq(r, new int[][] {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}})) ++pass; else System.out.println("FAIL test4");
        r = sol.imageSmoother2(new int[][] {{100, 200, 100}, {200, 50, 200}, {100, 200, 100}});
        ++total; if (eq(r, new int[][] {{137, 141, 137}, {141, 138, 141}, {137, 141, 137}})) ++pass; else System.out.println("FAIL test5");
        r = sol.imageSmoother2(new int[][] {{2, 3}});
        ++total; if (eq(r, new int[][] {{2, 2}})) ++pass; else System.out.println("FAIL test6");
        if (pass == total) {
            System.out.println("== 0661 PASS ==");
        } else {
            System.out.println("== 0661 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

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

662. Maximum Width of Binary Tree(二叉树最大宽度)

题目描述

给你一棵二叉树的根节点 root,返回树的最大宽度 。

树的最大宽度 是所有层中最大的宽度 。

每一层的宽度 被定义为该层最左和最右的非空节点(即,两个端点)之间的长度。将这个二叉树视作与

满二叉树结构相同,两端点间会出现一些延伸到这一层的 null 节点,这些 null 节点也计入长度。

题目数据保证答案将会在 32 位带符号整数范围内。

示例 1

  • 输入 :root = 1,3,2,5,3,`null`,9
  • 输出:4
  • 解释 :最大宽度出现在树的第 3 层,宽度为 4 (5,3,null,9)。

示例 2

  • 输入 :root = 1,3,2,5,`null`,`null`,9,6,`null`,7
  • 输出:7
  • 解释 :最大宽度出现在树的第 4 层,宽度为 7 (6,null,null,null,null,null,7)。

示例 3

  • 输入 :root = 1,3,2,5
  • 输出:2
  • 解释 :最大宽度出现在树的第 2 层,宽度为 2 (3,2)。
    提示:
  • 树中节点的数目范围是 [1, 3000]
  • -100 <= Node.val <= 100

解题思路

解法一(BFS + 层内编号归一化,主提交):给根节点编号 1,左孩子 i * 2,右孩子 i * 2 + 1。

BFS 逐层遍历,同一层的宽度 = 队尾编号 - 队首编号 + 1(中间的空缺计入)。树的深度最高可到

3000,完全二叉树的编号可达 2^3000,会溢出 int 甚至 long。技巧:每层以队首节点编号

first 作为基准,其他节点记录相对编号 cur = idx - first,下一层子节点基于 cur 计算

(cur*2 / cur*2+1),下一层再以其队首为基准归一化,从而每层编号始终在可表示范围内。

时间复杂度 O(n),空间复杂度 O(n)。

解法二(DFS 记录每层最左编号):leftMost[depth] 记录深度 depth 第一次访问到的节点编号;

之后同层节点用 当前编号 - leftMost[depth] + 1 更新答案。编号同样按 dfs(node.left, dep+1, idx*2)

传递,long 在深度 ≤64 时安全,实际树较浅时可用 long;为彻底避免溢出也可传相对值。

时间复杂度 O(n),空间复杂度 O(height)。

代码实现

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

class Solution {

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

    private static class Pair {
        TreeNode node;
        long idx;
        Pair(TreeNode node, long idx) {
            this.node = node;
            this.idx = idx;
        }
    }

    // 解法一:BFS + 每层归一化偏移
    public int widthOfBinaryTree(TreeNode root) {
        if (root == null) {
            return 0;
        }
        Queue<Pair> q = new LinkedList<>();
        q.offer(new Pair(root, 0));
        long ans = 0;
        while (!q.isEmpty()) {
            int size = q.size();
            long first = q.peek().idx;
            long last = first;
            for (int i = 0; i < size; ++i) {
                Pair p = q.poll();
                long cur = p.idx - first;   // 层内相对编号
                last = p.idx;               // 绝对值(用于本层宽度计算)
                if (p.node.left != null) {
                    q.offer(new Pair(p.node.left, cur * 2));
                }
                if (p.node.right != null) {
                    q.offer(new Pair(p.node.right, cur * 2 + 1));
                }
            }
            ans = Math.max(ans, last - first + 1);
        }
        return (int) ans;
    }

    // 解法二:DFS 记录每层最左编号
    private List<Long> leftMost = new ArrayList<>();
    private long best = 0;

    public int widthOfBinaryTree2(TreeNode root) {
        leftMost.clear();
        best = 0;
        dfs2(root, 0, 0);
        return (int) best;
    }

    private void dfs2(TreeNode root, int depth, long idx) {
        if (root == null) {
            return;
        }
        if (depth == leftMost.size()) {
            leftMost.add(idx);
        }
        best = Math.max(best, idx - leftMost.get(depth) + 1);
        dfs2(root.left, depth + 1, idx * 2);
        dfs2(root.right, depth + 1, idx * 2 + 1);
    }

    // 层序构建(测试辅助)
    private static TreeNode build(Integer[] arr) {
        if (arr == null || arr.length == 0) {
            return null;
        }
        TreeNode root = new TreeNode(arr[0]);
        Queue<TreeNode> q = new 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;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        TreeNode t1 = build(new Integer[] {1, 3, 2, 5, 3, null, 9});
        ++total; if (sol.widthOfBinaryTree(t1) == 4) ++pass; else System.out.println("FAIL test1: " + sol.widthOfBinaryTree(t1));
        TreeNode t2 = build(new Integer[] {1, 3, 2, 5, null, null, 9, 6, null, 7});
        ++total; if (sol.widthOfBinaryTree(t2) == 7) ++pass; else System.out.println("FAIL test2: " + sol.widthOfBinaryTree(t2));
        TreeNode t3 = build(new Integer[] {1, 3, 2, 5});
        ++total; if (sol.widthOfBinaryTree(t3) == 2) ++pass; else System.out.println("FAIL test3: " + sol.widthOfBinaryTree(t3));
        TreeNode t4 = build(new Integer[] {1});
        ++total; if (sol.widthOfBinaryTree(t4) == 1) ++pass; else System.out.println("FAIL test4: " + sol.widthOfBinaryTree(t4));
        // 满二叉树高度 4,最大宽度 8
        TreeNode t5 = build(new Integer[] {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1});
        ++total; if (sol.widthOfBinaryTree(t5) == 8) ++pass; else System.out.println("FAIL test5: " + sol.widthOfBinaryTree(t5));
        // 解法二
        ++total; if (sol.widthOfBinaryTree2(t1) == 4) ++pass; else System.out.println("FAIL test6: " + sol.widthOfBinaryTree2(t1));
        ++total; if (sol.widthOfBinaryTree2(t2) == 7) ++pass; else System.out.println("FAIL test7: " + sol.widthOfBinaryTree2(t2));
        ++total; if (sol.widthOfBinaryTree2(t3) == 2) ++pass; else System.out.println("FAIL test8: " + sol.widthOfBinaryTree2(t3));
        ++total; if (sol.widthOfBinaryTree2(t4) == 1) ++pass; else System.out.println("FAIL test9: " + sol.widthOfBinaryTree2(t4));
        ++total; if (sol.widthOfBinaryTree2(t5) == 8) ++pass; else System.out.println("FAIL test10: " + sol.widthOfBinaryTree2(t5));
        if (pass == total) {
            System.out.println("== 0662 PASS ==");
        } else {
            System.out.println("== 0662 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

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

663. Equal Tree Partition(均匀树划分)

题目描述

给你一棵二叉树的根节点 root,如果你可以通过去掉原始树上的一条边将树分成两棵节点值之和

相等的子树,则返回 true。

示例 1

  • 输入 :root = 5,10,10,`null`,`null`,2,3
  • 输出 :true
  • 解释:左边的子树 10 与剩余部分 5,10,2,3 的和都为 10(去掉连接 5 与左边 10 的边)。

示例 2

  • 输入 :root = 1,2,10,`null`,`null`,2,20
  • 输出 :false
  • 解释 :在树上移除一条边无法将树分成两棵节点值之和相等的子树。
    提示:
  • 树中节点数目在 [1, 10^4] 范围内
  • -10^5 <= Node.val <= 10^5

解题思路

解法一(后序求各子树和 + 集合判半,主提交):一次后序遍历计算整棵树的和 total。

如果 total 为奇数则不可能对半分。第二次后序遍历(或合并到第一次中),把非根节点 的子树和

全部放入集合;如果集合中存在 total/2,说明在某条边处可以把树切开。

注意节点值可为负,因此不能用"和为零/正值判断"的简化,直接用集合判存在即可。

时间复杂度 O(n),空间复杂度 O(n)(集合深度)。

解法二(前序切边逐一验证):枚举每条边,切掉后分别求和比较。需要先求出每个子树的根节点

子树和,再遍历所有非根节点检查 subsum == total / 2。时间复杂度 O(n),空间复杂度 O(n)。

(两种解法本质相同,写法不同。)

代码实现

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

class Solution {

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

    // 解法一:后序求整树和 + 收集非根子树和到集合
    public boolean checkEqualTree(TreeNode root) {
        long total = sum(root);
        if (total % 2 != 0) {
            return false;
        }
        Set<Long> sub = new HashSet<>();
        collect(root, sub);
        return sub.contains(total / 2);
    }

    private long sum(TreeNode root) {
        return root == null ? 0 : root.val + sum(root.left) + sum(root.right);
    }

    // 收集所有非根子树的和;返回当前子树和
    private long collect(TreeNode root, Set<Long> sub) {
        if (root == null) {
            return 0;
        }
        long left = collect(root.left, sub);
        long right = collect(root.right, sub);
        // 若左右孩子存在,其子树和是合法的"被切掉的子树"
        if (root.left != null) {
            sub.add(left);
        }
        if (root.right != null) {
            sub.add(right);
        }
        return root.val + left + right;
    }

    // 解法二:先存每个非根子树和,再判定
    private long total2;
    private boolean ok2;

    public boolean checkEqualTree2(TreeNode root) {
        total2 = sum(root);
        if (total2 % 2 != 0) {
            return false;
        }
        ok2 = false;
        dfs2(root.left);
        dfs2(root.right);
        return ok2;
    }

    private long dfs2(TreeNode root) {
        if (root == null) {
            return 0;
        }
        long s = root.val + dfs2(root.left) + dfs2(root.right);
        if (root != null && s == total2 / 2) {
            ok2 = true;
        }
        return s;
    }

    // 层序构建(测试辅助)
    private static TreeNode build(Integer[] arr) {
        if (arr == null || arr.length == 0) {
            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;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        TreeNode t1 = build(new Integer[] {5, 10, 10, null, null, 2, 3});
        ++total; if (sol.checkEqualTree(t1)) ++pass; else System.out.println("FAIL test1: " + sol.checkEqualTree(t1));
        TreeNode t2 = build(new Integer[] {1, 2, 10, null, null, 2, 20});
        ++total; if (!sol.checkEqualTree(t2)) ++pass; else System.out.println("FAIL test2");
        TreeNode t3 = build(new Integer[] {2, 1, 1, 1, 1});
        ++total; if (sol.checkEqualTree(t3)) ++pass; else System.out.println("FAIL test3: " + sol.checkEqualTree(t3));
        TreeNode t4 = build(new Integer[] {1, 1, 1});
        ++total; if (!sol.checkEqualTree(t4)) ++pass; else System.out.println("FAIL test4");
        // 解法二
        ++total; if (sol.checkEqualTree2(t1)) ++pass; else System.out.println("FAIL test5: " + sol.checkEqualTree2(t1));
        ++total; if (!sol.checkEqualTree2(t2)) ++pass; else System.out.println("FAIL test6");
        ++total; if (sol.checkEqualTree2(t3)) ++pass; else System.out.println("FAIL test7: " + sol.checkEqualTree2(t3));
        if (pass == total) {
            System.out.println("== 0663 PASS ==");
        } else {
            System.out.println("== 0663 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

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

664. Strange Printer(奇怪的打印机)

题目描述

有台奇怪的打印机有以下两个特殊要求:

  • 打印机每次只能打印由同一个字符组成的序列。
  • 每次可以在从起始到结束的任意位置打印新字符,并且会覆盖掉原来已有的字符。
    给你一个字符串 s,你的任务是计算这个打印机打印它需要的最少打印次数。

示例 1

  • 输入 :s = "aaabbb"
  • 输出:2
  • 解释:首先打印 "aaa" 然后打印 "bbb"。

示例 2

  • 输入 :s = "aba"
  • 输出:2
  • 解释 :首先打印 "aaa" 然后在第二个位置打印 "b" 覆盖掉原来的字符 'a'。
    提示:
  • 1 <= s.length <= 100
  • s 由小写英文字母组成

解题思路

解法一(区间动态规划,主提交):f[i][j] 表示打印 s[i..j] 需要的最少次数。

基础:f[i][i] = 1。对区间 [i, j],若 s[i] == s[j],则打印 [i, j-1] 时顺手带出

s[j],f[i][j] = f[i][j-1];否则在 k 处切开:f[i][j] = min(f[i][k] + f[k+1][j])。

按区间长度从小到大递推。时间复杂度 O(n³),空间复杂度 O(n²)。

解法二(递归记忆化搜索):与解法一同状态,用递归 + memo[i][j] 记忆。从最后字符 j 考虑:

单独打印 s[j] 共 f[i][j-1] + 1;若存在 k 使 s[k] == s[j],可以把 [k, j] 中与

s[j] 相同的字符合并到 k 处一次打印:

f[i][j] = min(f[i][j-1] + 1, min_{k∈[i, j-1], s[k]==s[j]} (f[i][k] + f[k+1][j-1]))。

时间复杂度 O(n³),空间复杂度 O(n²)。

代码实现

java 复制代码
class Solution {

    // 解法一:区间 DP(包含端点同色优化)
    public int strangePrinter(String s) {
        int n = s.length();
        int[][] f = new int[n][n];
        for (int i = 0; i < n; ++i) {
            f[i][i] = 1;
        }
        for (int len = 2; len <= n; ++len) {
            for (int i = 0; i + len - 1 < n; ++i) {
                int j = i + len - 1;
                if (s.charAt(i) == s.charAt(j)) {
                    f[i][j] = f[i][j - 1];
                } else {
                    int best = Integer.MAX_VALUE;
                    for (int k = i; k < j; ++k) {
                        best = Math.min(best, f[i][k] + f[k + 1][j]);
                    }
                    f[i][j] = best;
                }
            }
        }
        return f[0][n - 1];
    }

    // 解法二:递归记忆化(含同字符合并优化)
    private int[][] memo;

    public int strangePrinter2(String s) {
        int n = s.length();
        memo = new int[n][n];
        for (int i = 0; i < n; ++i) {
            java.util.Arrays.fill(memo[i], -1);
        }
        return solve(s, 0, n - 1);
    }

    private int solve(String s, int i, int j) {
        if (i > j) {
            return 0;
        }
        if (memo[i][j] != -1) {
            return memo[i][j];
        }
        // 单独把 s[j] 打印出来
        int ans = solve(s, i, j - 1) + 1;
        // 若 s[k] == s[j],把 [k,j-1] 里和 s[j] 相同的字符连同 s[k] 一起打印
        for (int k = i; k < j; ++k) {
            if (s.charAt(k) == s.charAt(j)) {
                ans = Math.min(ans, solve(s, i, k) + solve(s, k + 1, j - 1));
            }
        }
        memo[i][j] = ans;
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (sol.strangePrinter("aaabbb") == 2) ++pass; else System.out.println("FAIL test1: " + sol.strangePrinter("aaabbb"));
        ++total; if (sol.strangePrinter("aba") == 2) ++pass; else System.out.println("FAIL test2: " + sol.strangePrinter("aba"));
        ++total; if (sol.strangePrinter("a") == 1) ++pass; else System.out.println("FAIL test3: " + sol.strangePrinter("a"));
        ++total; if (sol.strangePrinter("abcabc") == 5) ++pass; else System.out.println("FAIL test4: " + sol.strangePrinter("abcabc"));
        ++total; if (sol.strangePrinter("aaabaaa") == 2) ++pass; else System.out.println("FAIL test5: " + sol.strangePrinter("aaabaaa"));
        ++total; if (sol.strangePrinter("aaaa") == 1) ++pass; else System.out.println("FAIL test6: " + sol.strangePrinter("aaaa"));
        // 解法二
        ++total; if (sol.strangePrinter2("aaabbb") == 2) ++pass; else System.out.println("FAIL test7");
        ++total; if (sol.strangePrinter2("aba") == 2) ++pass; else System.out.println("FAIL test8");
        ++total; if (sol.strangePrinter2("abcabc") == 5) ++pass; else System.out.println("FAIL test9");
        ++total; if (sol.strangePrinter2("aaabaaa") == 2) ++pass; else System.out.println("FAIL test10");
        if (pass == total) {
            System.out.println("== 0664 PASS ==");
        } else {
            System.out.println("== 0664 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

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

| --- | | --- |

| 解法一 | O(n³) | O(n²) |

| 解法二 | O(n³) | O(n²) |


665. Non-decreasing Array(非递减数列)

题目描述

给你一个长度为 n 的整数数组 nums,请你判断在最多 改变 1 个元素的情况下,该数组能否变成

一个非递减数列。

我们是这样定义一个非递减数列的:对于数组中任意的 i (0 <= i <= n-2),总满足 nums[i] <= nums[i+1]。

示例 1

  • 输入 :nums = 4,2,3
  • 输出 :true
  • 解释:把第一个 4 改成 1 即可得到非递减数列 1,2,3。

示例 2

  • 输入 :nums = 4,2,1
  • 输出 :false
  • 解释 :只在改变一个元素的情况下无法变为非递减数列。
    提示:
  • n == nums.length
  • 1 <= n <= 10^4
  • -10^5 <= nums[i] <= 10^5

解题思路

出现下降即 nums[i] > nums[i+1]。若下降点超过一处,一定无解。

解法一(贪心检查下降点,主提交):顺序扫描,遇到第一个下降点 (i, i+1):

尝试两种修复------把 nums[i] 改为 nums[i+1](需要 i==0 或 nums[i-1] <= nums[i+1]),

或把 nums[i+1] 改为 nums[i](直接修改数组后全量检查)。若修改后的数组整体非递减则返回 true。

若后续再出现下降,直接返回 false。时间复杂度 O(n),空间复杂度 O(1)。

解法二(统计下降次数 + 三种情形):统计所有满足 nums[i] > nums[i+1] 的下标。

  • 无下降:直接 true;
  • 1 处下降:检查"把 i 降下去"或"把 i+1 抬上去"是否可行(若 i>=1 且 nums[i-1] > nums[i+1]
    则只能把 i+1 改成 nums[i],需再检查 i+2 是否不小于新值;两条路任一可行即可);
  • 2 处及以上:false。
    时间复杂度 O(n),空间复杂度 O(1)。

代码实现

java 复制代码
class Solution {

    // 解法一:贪心模拟修复后全量检查
    public boolean checkPossibility(int[] nums) {
        int n = nums.length;
        int fix = 0;
        for (int i = 0; i + 1 < n; ++i) {
            if (nums[i] > nums[i + 1]) {
                ++fix;
                if (fix > 1) {
                    return false;
                }
                // 把 nums[i] 改小为 nums[i+1](前面没有元素或前一个不大于 nums[i+1] 时可行)
                // 否则把 nums[i+1] 改大为 nums[i]
                int[] copy = nums.clone();
                boolean ok1 = false;
                if (i == 0 || nums[i - 1] <= nums[i + 1]) {
                    copy[i] = copy[i + 1];
                    ok1 = isSorted(copy);
                }
                copy = nums.clone();
                copy[i + 1] = copy[i];
                boolean ok2 = isSorted(copy);
                return ok1 || ok2;
            }
        }
        return true;
    }

    private boolean isSorted(int[] a) {
        for (int i = 0; i + 1 < a.length; ++i) {
            if (a[i] > a[i + 1]) {
                return false;
            }
        }
        return true;
    }

    // 解法二:统计下降点,按位置分析
    public boolean checkPossibility2(int[] nums) {
        int n = nums.length;
        int pos = -1, cnt = 0;
        for (int i = 0; i + 1 < n; ++i) {
            if (nums[i] > nums[i + 1]) {
                ++cnt;
                pos = i;
                if (cnt > 1) {
                    return false;
                }
            }
        }
        if (cnt == 0) {
            return true;
        }
        // 只有一处下降,位置为 pos
        // 情形 A:把 nums[pos] 改小(前提 pos==0 或 nums[pos-1] <= nums[pos+1])
        if (pos == 0 || nums[pos - 1] <= nums[pos + 1]) {
            return true;
        }
        // 情形 B:把 nums[pos+1] 改大(前提 nums[pos+2] >= nums[pos],不存在则视为成立)
        return pos + 2 >= n || nums[pos] <= nums[pos + 2];
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (sol.checkPossibility(new int[] {4, 2, 3})) ++pass; else System.out.println("FAIL test1: " + sol.checkPossibility(new int[] {4, 2, 3}));
        ++total; if (!sol.checkPossibility(new int[] {4, 2, 1})) ++pass; else System.out.println("FAIL test2");
        ++total; if (sol.checkPossibility(new int[] {5, 7, 1, 8})) ++pass; else System.out.println("FAIL test3: " + sol.checkPossibility(new int[] {5, 7, 1, 8}));
        ++total; if (!sol.checkPossibility(new int[] {3, 4, 2, 3})) ++pass; else System.out.println("FAIL test4");
        ++total; if (sol.checkPossibility(new int[] {1})) ++pass; else System.out.println("FAIL test5");
        ++total; if (sol.checkPossibility(new int[] {1, 2, 3})) ++pass; else System.out.println("FAIL test6");
        ++total; if (sol.checkPossibility2(new int[] {4, 2, 3})) ++pass; else System.out.println("FAIL test7");
        ++total; if (!sol.checkPossibility2(new int[] {4, 2, 1})) ++pass; else System.out.println("FAIL test8");
        ++total; if (sol.checkPossibility2(new int[] {5, 7, 1, 8})) ++pass; else System.out.println("FAIL test9");
        ++total; if (!sol.checkPossibility2(new int[] {3, 4, 2, 3})) ++pass; else System.out.println("FAIL test10");
        ++total; if (sol.checkPossibility2(new int[] {1})) ++pass; else System.out.println("FAIL test11");
        ++total; if (sol.checkPossibility2(new int[] {1, 2, 3})) ++pass; else System.out.println("FAIL test12");
        if (pass == total) {
            System.out.println("== 0665 PASS ==");
        } else {
            System.out.println("== 0665 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

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

666. Path Sum IV(路径总和 IV)

题目描述

对于一棵深度小于 5 的树,可以用一组三位十进制整数来表示。给定一个由三位数组成的递增 的数组

nums 表示一棵深度小于 5 的二叉树,对于每个整数:

  • 百位上的数字表示这个节点的深度 d,1 <= d <= 4。
  • 十位上的数字表示这个节点在当前层所在的位置 p,1 <= p <= 8。位置编号与一棵满二叉树
    的位置编号相同。
  • 个位上的数字表示这个节点的权值 v,0 <= v <= 9。
    返回从根 到所有叶子结点 的路径 之和 。
    保证给定的数组表示一个有效的连接二叉树。

示例 1

  • 输入 :nums = 113, 215, 221
  • 输出:12
  • 解释:路径和 = (3 + 5) + (3 + 1) = 12。

示例 2

  • 输入 :nums = 113, 221
  • 输出:4
  • 解释 :路径和 = (3 + 1) = 4。
    提示:
  • 1 <= nums.length <= 15
  • 110 <= nums[i] <= 489
  • nums 表示深度小于 5 的有效二叉树
  • nums 以升序排序

解题思路

解法一(哈希表建模 + DFS 求路径和,主提交):把每个节点编码为 depth*10 + pos(两位键),

值为权值。根节点键恒为 11。从根出发做 DFS:left = key(d+1, p*2-1)、right = key(d+1, p*2);

若左右孩子都不存在(叶子),则把当前路径和累加进答案;否则递归。由于 nums 升序且深度 ≤4,

规模很小。时间复杂度 O(n)(节点数),空间复杂度 O(n)。

解法二(直接按数组建节点对象 + 后序):把三个数字解析成节点(含 d/p/v),用 Map<key, Node>

建树后再常规 DFS。本质同解法一,只是显式建树。时间复杂度 O(n),空间复杂度 O(n)。

代码实现

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

class Solution {

    private Map<Integer, Integer> tree = new HashMap<>();
    private int ans = 0;

    // 解法一:哈希表建模 + DFS
    public int pathSum(int[] nums) {
        tree.clear();
        ans = 0;
        for (int x : nums) {
            int d = x / 100;
            int p = (x / 10) % 10;
            int v = x % 10;
            tree.put(d * 10 + p, v);
        }
        dfs(1, 1, 0);
        return ans;
    }

    // depth d、位置 p(1 基),前缀和 pre
    private void dfs(int d, int p, int pre) {
        int key = d * 10 + p;
        if (!tree.containsKey(key)) {
            return;
        }
        int cur = pre + tree.get(key);
        int lk = (d + 1) * 10 + (p * 2 - 1);
        int rk = (d + 1) * 10 + (p * 2);
        boolean hasLeft = tree.containsKey(lk);
        boolean hasRight = tree.containsKey(rk);
        if (!hasLeft && !hasRight) {
            ans += cur;
            return;
        }
        dfs(d + 1, p * 2 - 1, cur);
        dfs(d + 1, p * 2, cur);
    }

    // 解法二:显式建树(用数组存放节点,简化)
    private static class Node {
        int d, p, v;
        Node left, right;
        Node(int d, int p, int v) {
            this.d = d;
            this.p = p;
            this.v = v;
        }
    }

    public int pathSum2(int[] nums) {
        Node[] treeArr = new Node[33]; // d<=4,pos<=8,最多 4 层
        Node rootNode = null;
        for (int x : nums) {
            int d = x / 100;
            int p = (x / 10) % 10;
            int v = x % 10;
            Node node = new Node(d, p, v);
            int idx = (d - 1) * 8 + p;
            treeArr[idx] = node;
            if (d == 1) {
                rootNode = node;
            } else {
                Node parent = treeArr[(d - 2) * 8 + (p + 1) / 2];
                if (p % 2 == 1) {
                    parent.left = node;
                } else {
                    parent.right = node;
                }
            }
        }
        return sumPaths(rootNode, 0);
    }

    private int sumPaths(Node node, int pre) {
        if (node == null) {
            return 0;
        }
        int cur = pre + node.v;
        if (node.left == null && node.right == null) {
            return cur;
        }
        return sumPaths(node.left, cur) + sumPaths(node.right, cur);
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (sol.pathSum(new int[] {113, 215, 221}) == 12) ++pass; else System.out.println("FAIL test1: " + sol.pathSum(new int[] {113, 215, 221}));
        ++total; if (sol.pathSum(new int[] {113, 221}) == 4) ++pass; else System.out.println("FAIL test2: " + sol.pathSum(new int[] {113, 221}));
        ++total; if (sol.pathSum(new int[] {111, 222, 333}) == 6) ++pass; else System.out.println("FAIL test3: " + sol.pathSum(new int[] {111, 222, 333}));
        ++total; if (sol.pathSum(new int[] {113, 211, 222}) == 9) ++pass; else System.out.println("FAIL test4: " + sol.pathSum(new int[] {113, 211, 222}));
        // 解法二
        ++total; if (sol.pathSum2(new int[] {113, 215, 221}) == 12) ++pass; else System.out.println("FAIL test5");
        ++total; if (sol.pathSum2(new int[] {113, 221}) == 4) ++pass; else System.out.println("FAIL test6");
        ++total; if (sol.pathSum2(new int[] {111, 222, 333}) == 6) ++pass; else System.out.println("FAIL test7");
        ++total; if (sol.pathSum2(new int[] {113, 211, 222}) == 9) ++pass; else System.out.println("FAIL test8");
        if (pass == total) {
            System.out.println("== 0666 PASS ==");
        } else {
            System.out.println("== 0666 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

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

667. Beautiful Arrangement II(优美的排列 II)

题目描述

给你两个整数 n 和 k,请你构造一个答案列表 answer,该列表应当包含从 1 到 n 的

n 个不同正整数,并同时满足下述条件:

假设该列表是 answer = [a1, a2, a3, ... , an],那么列表

[|a1 - a2|, |a2 - a3|, |a3 - a4|, ... , |an-1 - an|] 中应该有且仅有 k 个不同整数。

返回列表 answer。如果存在多种答案,只需返回其中任意一种。

示例 1

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

示例 2

  • 输入 :n = 3, k = 2
  • 输出 :1, 3, 2
    提示:
  • 1 <= k < n <= 10^4

解题思路

解法一(构造:交错首尾保证 k-1 种差,主提交):前 k+1 个位置按 "1, n, 2, n-1, 3, ..."

交错排列:相邻差的绝对值依次为 n-1, n-2, ..., n-k,正好产生 k 个不同差值

(精确地说前 k+1 个数产生 k 个不同差值,余下数按单调排列不再引入新差)。

具体:先按 low=1, high=n 交错放入前段;若 k 为奇数则从大到小补剩余,偶数则从小到大。

一个更简单的写法:前 k 个差依次为 k-1, k-2, ..., 2, 1,因此

排列 = 1, k+1, 2, k, 3, k-1, ..., 再按顺序补 k+2, k+3, ..., n。

时间复杂度 O(n),空间复杂度 O(n)(结果)。

解法二(交换构造):从 [1,2,...,n] 出发,反转后 k-1 个区间的端点(每反转一次引入一个新差),

共操作 k-1 次后恰好有 k 种不同差。时间复杂度 O(k·n)(每次反转),空间 O(n)。

代码实现

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

class Solution {

    // 解法一:交错首尾构造
    public int[] constructArray(int n, int k) {
        int[] ans = new int[n];
        int low = 1, high = n;
        int i = 0;
        // 先产生 k-1 个递减的大差值:放到 ans[0..k-1],用 low/high 交错
        // 最后一段再产生最后一个差值(或直接单调)
        boolean flag = true; // true 取 low,false 取 high
        for (int idx = 0; idx < k; ++idx) {
            if (flag) {
                ans[i++] = low++;
            } else {
                ans[i++] = high--;
            }
            flag = !flag;
        }
        // 剩余元素按同一方向继续,不再引入新差
        if (flag) {
            // 当前准备取 high(大端),继续降序放 high, high-1, ...
            while (i < n) {
                ans[i++] = high--;
            }
        } else {
            while (i < n) {
                ans[i++] = low++;
            }
        }
        return ans;
    }

    // 解法二:从 [1..n] 出发,逐次反转更短的后缀(每反转一次引入一个新差)
    public int[] constructArray2(int n, int k) {
        int[] ans = new int[n];
        for (int i = 0; i < n; ++i) {
            ans[i] = i + 1;
        }
        // 第 j 次反转 [j .. n-1]:相邻差依次产生 n-1, n-2, ..., n-k
        for (int j = 1; j < k; ++j) {
            reverse(ans, j, n - 1);
        }
        return ans;
    }

    private void reverse(int[] a, int l, int r) {
        while (l < r) {
            int t = a[l];
            a[l] = a[r];
            a[r] = t;
            ++l;
            --r;
        }
    }

    private static int diffCount(int[] a) {
        java.util.Set<Integer> s = new java.util.HashSet<>();
        for (int i = 1; i < a.length; ++i) {
            s.add(Math.abs(a[i] - a[i - 1]));
        }
        return s.size();
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        // 验证不同差个数 == k
        boolean ok = true;
        for (int n = 1; n <= 20; ++n) {
            for (int k = 1; k < n; ++k) {
                int[] r = sol.constructArray(n, k);
                if (diffCount(r) != k) {
                    ok = false;
                    System.out.println("FAIL n=" + n + " k=" + k + " got=" + diffCount(r) + " arr=" + Arrays.toString(r));
                }
            }
        }
        ++total; if (ok) ++pass; else System.out.println("FAIL test1");
        int[] r1 = sol.constructArray(3, 2);
        ++total; if (diffCount(r1) == 2) ++pass; else System.out.println("FAIL test2: " + Arrays.toString(r1));
        int[] r2 = sol.constructArray(3, 1);
        ++total; if (diffCount(r2) == 1) ++pass; else System.out.println("FAIL test3: " + Arrays.toString(r2));
        // 解法二穷举验证
        boolean ok2 = true;
        for (int n = 1; n <= 15; ++n) {
            for (int k = 1; k < n; ++k) {
                int[] r = sol.constructArray2(n, k);
                if (diffCount(r) != k) {
                    ok2 = false;
                    System.out.println("FAIL2 n=" + n + " k=" + k + " got=" + diffCount(r) + " arr=" + Arrays.toString(r));
                }
            }
        }
        ++total; if (ok2) ++pass; else System.out.println("FAIL test4");
        if (pass == total) {
            System.out.println("== 0667 PASS ==");
        } else {
            System.out.println("== 0667 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

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

668. Kth Smallest Number in Multiplication Table(乘法表中第k小的数)

题目描述

几乎每一个人都用乘法表。但是你能在乘法表中快速找到第 k 小的数字吗?

乘法表是大小为 m x n 的一个整数矩阵,其中 mat[i][j] == i * j(下标从 1 开始)。

给你三个整数 m、n 和 k,请你在大小为 m x n 的乘法表中,找出并返回第 k 小的数字。

示例 1

  • 输入 :m = 3, n = 3, k = 5
  • 输出:3
  • 解释:第 5 小的数字是 3。

示例 2

  • 输入 :m = 2, n = 3, k = 6
  • 输出:6
  • 解释 :第 6 小的数字是 6。
    提示:
  • 1 <= m, n <= 3 * 10^4
  • 1 <= k <= m * n

解题思路

解法一(二分答案,主提交):对候选值 x 二分。对第 i 行(i 从 1 开始),小于等于 x 的

数字个数为 min(x / i, n);全部 m 行累加得到 cnt(x) = 乘法表中 ≤ x 的个数。

cnt(x) 随 x 单调不减,二分找最小的 x 满足 cnt(x) >= k,即为第 k 小。

时间复杂度 O(m log(m·n)),空间复杂度 O(1)。

解法二(对换行列 + 二分):若 m > n 可以交换(同行内按较小维度枚举,二分更快),

原理同上。时间复杂度 O(min(m,n) · log(m·n)),空间复杂度 O(1)。

代码实现

java 复制代码
class Solution {

    // 解法一:二分答案
    public int findKthNumber(int m, int n, int k) {
        long lo = 1, hi = (long) m * n;
        while (lo < hi) {
            long mid = (lo + hi) >>> 1;
            if (count(m, n, mid) >= k) {
                hi = mid;
            } else {
                lo = mid + 1;
            }
        }
        return (int) lo;
    }

    // 乘法表中 ≤ x 的个数
    private long count(int m, int n, long x) {
        long c = 0;
        for (int i = 1; i <= m; ++i) {
            c += Math.min(x / i, n);
        }
        return c;
    }

    // 解法二:行数取较小者 + 二分
    public int findKthNumber2(int m, int n, int k) {
        if (m > n) {
            int t = m;
            m = n;
            n = t;
        }
        long lo = 1, hi = (long) m * n;
        while (lo < hi) {
            long mid = (lo + hi) >>> 1;
            if (count(m, n, mid) >= k) {
                hi = mid;
            } else {
                lo = mid + 1;
            }
        }
        return (int) lo;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (sol.findKthNumber(3, 3, 5) == 3) ++pass; else System.out.println("FAIL test1: " + sol.findKthNumber(3, 3, 5));
        ++total; if (sol.findKthNumber(2, 3, 6) == 6) ++pass; else System.out.println("FAIL test2: " + sol.findKthNumber(2, 3, 6));
        ++total; if (sol.findKthNumber(1, 1, 1) == 1) ++pass; else System.out.println("FAIL test3: " + sol.findKthNumber(1, 1, 1));
        ++total; if (sol.findKthNumber(1, 10, 7) == 7) ++pass; else System.out.println("FAIL test4: " + sol.findKthNumber(1, 10, 7));
        ++total; if (sol.findKthNumber(30000, 30000, 450000000) >= 1) ++pass; else System.out.println("FAIL test5");
        // 解法二
        ++total; if (sol.findKthNumber2(3, 3, 5) == 3) ++pass; else System.out.println("FAIL test6");
        ++total; if (sol.findKthNumber2(2, 3, 6) == 6) ++pass; else System.out.println("FAIL test7");
        ++total; if (sol.findKthNumber2(1, 10, 7) == 7) ++pass; else System.out.println("FAIL test8");
        if (pass == total) {
            System.out.println("== 0668 PASS ==");
        } else {
            System.out.println("== 0668 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

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

题目描述

给你二叉搜索树的根节点 root,同时给定最小边界 low 和最大边界 high。通过修剪二叉搜索树,

使得所有节点的值在 [low, high] 中。修剪树不应该 改变保留在树中的元素的相对结构

(即,如果没有被移除,原有的父代子代关系都应当保留)。可以证明,存在唯一 的答案。

所以结果应当返回修剪好的二叉搜索树的新的根节点。注意,根节点可能会根据给定的边界发生改变。

示例 1

  • 输入 :root = 1,0,2, low = 1, high = 2
  • 输出 :1,`null`,2

示例 2

  • 输入 :root = 3,0,4,`null`,2,`null`,`null`,1, low = 1, high = 3
  • 输出 :3,2,`null`,1
    提示:
  • 树中节点数在范围 [1, 10^4] 内
  • 0 <= Node.val <= 10^4
  • 树中每个节点的值都是唯一的
  • 题目数据保证输入是一棵有效的二叉搜索树
  • 0 <= low <= high <= 10^4

解题思路

解法一(递归:利用 BST 有序性整棵裁剪,主提交):因为 BST 有序,

  • 若 root.val < low,说明根和整棵左子树都太小,答案只在右子树;
  • 若 root.val > high,说明根和整棵右子树都太大,答案只在左子树;
  • 否则根保留,分别递归修剪左右子树并接回。
    时间复杂度 O(n),空间复杂度 O(height)。
    解法二(迭代):先找到符合区间的新根,再迭代修剪左右子树的"越界分支"。
    时间复杂度 O(n),空间复杂度 O(1)(除结果外)。

代码实现

java 复制代码
class Solution {

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

    // 解法一:递归
    public TreeNode trimBST(TreeNode root, int low, int high) {
        if (root == null) {
            return null;
        }
        if (root.val < low) {
            return trimBST(root.right, low, high);
        }
        if (root.val > high) {
            return trimBST(root.left, low, high);
        }
        root.left = trimBST(root.left, low, high);
        root.right = trimBST(root.right, low, high);
        return root;
    }

    // 解法二:迭代
    public TreeNode trimBST2(TreeNode root, int low, int high) {
        // 找到新的根:值在 [low, high] 内
        while (root != null && (root.val < low || root.val > high)) {
            root = root.val < low ? root.right : root.left;
        }
        if (root == null) {
            return null;
        }
        // 修剪左子树:所有 < low 的分支
        TreeNode cur = root;
        while (cur != null) {
            while (cur.left != null && cur.left.val < low) {
                cur.left = cur.left.right;
            }
            cur = cur.left;
        }
        // 修剪右子树:所有 > high 的分支
        cur = root;
        while (cur != null) {
            while (cur.right != null && cur.right.val > high) {
                cur.right = cur.right.left;
            }
            cur = cur.right;
        }
        return root;
    }

    // 层序构建(测试辅助)
    private static TreeNode build(Integer[] arr) {
        if (arr == null || arr.length == 0) {
            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 String preorder(TreeNode root) {
        if (root == null) {
            return "null";
        }
        return root.val + "," + preorder(root.left) + "," + preorder(root.right);
    }

    // 校验是否在 [low, high] 且仍是 BST
    private static boolean valid(TreeNode root, long lo, long hi) {
        if (root == null) {
            return true;
        }
        if (root.val < lo || root.val > hi) {
            return false;
        }
        return valid(root.left, lo, root.val) && valid(root.right, root.val, hi);
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        TreeNode r1 = sol.trimBST(build(new Integer[] {1, 0, 2}), 1, 2);
        ++total; if (preorder(r1).equals("1,null,2,null,null")) ++pass; else System.out.println("FAIL test1: " + preorder(r1));
        TreeNode r2 = sol.trimBST(build(new Integer[] {3, 0, 4, null, 2, null, null, 1}), 1, 3);
        ++total; if (preorder(r2).equals("3,2,1,null,null,null,null")) ++pass; else System.out.println("FAIL test2: " + preorder(r2));
        TreeNode r3 = sol.trimBST(build(new Integer[] {1}), 1, 2);
        ++total; if (preorder(r3).equals("1,null,null")) ++pass; else System.out.println("FAIL test3: " + preorder(r3));
        ++total; if (valid(r1, 1, 2) && valid(r2, 1, 3)) ++pass; else System.out.println("FAIL test4");
        // 解法二
        TreeNode r5 = sol.trimBST2(build(new Integer[] {1, 0, 2}), 1, 2);
        ++total; if (preorder(r5).equals("1,null,2,null,null")) ++pass; else System.out.println("FAIL test5: " + preorder(r5));
        TreeNode r6 = sol.trimBST2(build(new Integer[] {3, 0, 4, null, 2, null, null, 1}), 1, 3);
        ++total; if (preorder(r6).equals("3,2,1,null,null,null,null")) ++pass; else System.out.println("FAIL test6: " + preorder(r6));
        if (pass == total) {
            System.out.println("== 0669 PASS ==");
        } else {
            System.out.println("== 0669 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

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

670. Maximum Swap(最大交换)

题目描述

给定一个非负整数,你至多可以交换一次数字中的任意两位。返回你能得到的最大值。

示例 1

  • 输入 :num = 2736
  • 输出:7236
  • 解释:交换数字 2 和数字 7。

示例 2

  • 输入 :num = 9973
  • 输出:9973
  • 解释 :不需要交换。
    提示:
  • 给定数字的范围是 [0, 10^8]

解题思路

解法一(从右往左记录右侧最大下标 + 贪心交换,主提交):把数字转成字符数组。

第一次从右往左扫描,记录 d[i]:从位置 i 到末尾中数值最大 的数字的下标(若相等取更靠右的,

因交换后同样大但高位移过去更优------更靠右才把 9 往前换)。

第二次从左往右扫描,找到第一处 digit < digit[d[i]] 的位置,交换即可。

时间复杂度 O(n),空间复杂度 O(n)。

解法二(暴力枚举所有交换):n 位数最多 9 位(10^8 为 1 亿,9 位数),枚举任意两位交换共

O(n²) 种,取最大值。时间复杂度 O(n³)(每次比较字符串 O(n)),空间复杂度 O(n)。

实现更简单,作为验证解法。

代码实现

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

class Solution {

    // 解法一:右侧最大下标法
    public int maximumSwap(int num) {
        char[] s = String.valueOf(num).toCharArray();
        int n = s.length;
        int[] rightMax = new int[n];
        int best = n - 1;
        for (int i = n - 1; i >= 0; --i) {
            if (s[i] > s[best] || (s[i] == s[best] && i > best)) {
                best = i;
            }
            rightMax[i] = best;
        }
        for (int i = 0; i < n; ++i) {
            int j = rightMax[i];
            if (s[i] != s[j] && i != j) {
                char t = s[i];
                s[i] = s[j];
                s[j] = t;
                return Integer.parseInt(new String(s));
            }
        }
        return num;
    }

    // 解法二:暴力枚举所有交换
    public int maximumSwap2(int num) {
        char[] s = String.valueOf(num).toCharArray();
        int n = s.length, bestNum = num;
        for (int i = 0; i < n; ++i) {
            for (int j = i + 1; j < n; ++j) {
                char[] t = s.clone();
                char tmp = t[i];
                t[i] = t[j];
                t[j] = tmp;
                bestNum = Math.max(bestNum, Integer.parseInt(new String(t)));
            }
        }
        return bestNum;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (sol.maximumSwap(2736) == 7236) ++pass; else System.out.println("FAIL test1: " + sol.maximumSwap(2736));
        ++total; if (sol.maximumSwap(9973) == 9973) ++pass; else System.out.println("FAIL test2: " + sol.maximumSwap(9973));
        ++total; if (sol.maximumSwap(98368) == 98863) ++pass; else System.out.println("FAIL test3: " + sol.maximumSwap(98368));
        ++total; if (sol.maximumSwap(1993) == 9913) ++pass; else System.out.println("FAIL test4: " + sol.maximumSwap(1993));
        ++total; if (sol.maximumSwap(0) == 0) ++pass; else System.out.println("FAIL test5: " + sol.maximumSwap(0));
        ++total; if (sol.maximumSwap(12345) == 52341) ++pass; else System.out.println("FAIL test6: " + sol.maximumSwap(12345));
        ++total; if (sol.maximumSwap2(2736) == 7236) ++pass; else System.out.println("FAIL test7");
        ++total; if (sol.maximumSwap2(98368) == 98863) ++pass; else System.out.println("FAIL test8");
        ++total; if (sol.maximumSwap2(1993) == 9913) ++pass; else System.out.println("FAIL test9");
        ++total; if (sol.maximumSwap2(0) == 0) ++pass; else System.out.println("FAIL test10");
        if (pass == total) {
            System.out.println("== 0670 PASS ==");
        } else {
            System.out.println("== 0670 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

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

671. Second Minimum Node In a Binary Tree(二叉树中第二小的节点)

题目描述

给定一个非空特殊的二叉树,每个节点都是正数,并且每个节点的子节点数量只能为 2 或 0。

如果一个节点有两个子节点的话,那么该节点的值等于两个子节点中较小的一个。

更正式地说,即 root.val = min(root.left.val, root.right.val) 总成立。

给出这样的一个二叉树,你需要输出所有节点中的第二小的值 。

如果第二小的值不存在的话,输出 -1。

示例 1

  • 输入 :root = 2,2,5,`null`,`null`,5,7
  • 输出:5
  • 解释:最小的值是 2,第二小的值是 5。

示例 2

  • 输入 :root = 2,2,2
  • 输出:-1
  • 解释 :最小的值是 2,但是不存在第二小的值。
    提示:
  • 树中节点数目在范围 [1, 25] 内
  • 1 <= Node.val <= 2^31 - 1
  • 对于树中每个节点 root.val == min(root.left.val, root.right.val)

解题思路

解法一(DFS 找最小的大于根的值,主提交):由树的性质,根节点值就是全局最小值 minVal。

DFS 遍历所有节点,收集所有大于 minVal 的节点,取其中最小者作为第二小值;若不存在则返回 -1。

优化:一旦发现节点值 >= second 就不再深入其子树(因为整棵子树都 >= 它)。

时间复杂度 O(n),空间复杂度 O(height)。

解法二(暴力收集去重排序):遍历整棵树把节点值放入有序集合(如 TreeSet),取第二个元素。

由于节点数 ≤25,实现最简单。时间复杂度 O(n log n),空间复杂度 O(n)。

代码实现

java 复制代码
class Solution {

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

    // 解法一:DFS 剪枝
    private long minVal;
    private long second;

    public int findSecondMinimumValue(TreeNode root) {
        minVal = root.val;
        second = Long.MAX_VALUE;
        dfs(root);
        return second == Long.MAX_VALUE ? -1 : (int) second;
    }

    private void dfs(TreeNode root) {
        if (root == null) {
            return;
        }
        if (root.val > minVal && root.val < second) {
            second = root.val;
        }
        // 剪枝:值已经 >= second,子树里的值只会更大(子节点值 >= 自身值)
        if (root.val >= second) {
            return;
        }
        dfs(root.left);
        dfs(root.right);
    }

    // 解法二:TreeSet 收集
    public int findSecondMinimumValue2(TreeNode root) {
        java.util.TreeSet<Integer> set = new java.util.TreeSet<>();
        collect(root, set);
        if (set.size() < 2) {
            return -1;
        }
        set.pollFirst();
        return set.first();
    }

    private void collect(TreeNode root, java.util.TreeSet<Integer> set) {
        if (root == null) {
            return;
        }
        set.add(root.val);
        collect(root.left, set);
        collect(root.right, set);
    }

    // 层序构建(测试辅助)
    private static TreeNode build(Integer[] arr) {
        if (arr == null || arr.length == 0) {
            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;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        TreeNode t1 = build(new Integer[] {2, 2, 5, null, null, 5, 7});
        ++total; if (sol.findSecondMinimumValue(t1) == 5) ++pass; else System.out.println("FAIL test1: " + sol.findSecondMinimumValue(t1));
        TreeNode t2 = build(new Integer[] {2, 2, 2});
        ++total; if (sol.findSecondMinimumValue(t2) == -1) ++pass; else System.out.println("FAIL test2: " + sol.findSecondMinimumValue(t2));
        TreeNode t3 = build(new Integer[] {2, 2, 5, null, null, 5, 5});
        ++total; if (sol.findSecondMinimumValue(t3) == 5) ++pass; else System.out.println("FAIL test3");
        TreeNode t4 = build(new Integer[] {1});
        ++total; if (sol.findSecondMinimumValue(t4) == -1) ++pass; else System.out.println("FAIL test4");
        // 解法二
        ++total; if (sol.findSecondMinimumValue2(t1) == 5) ++pass; else System.out.println("FAIL test5");
        ++total; if (sol.findSecondMinimumValue2(t2) == -1) ++pass; else System.out.println("FAIL test6");
        ++total; if (sol.findSecondMinimumValue2(t4) == -1) ++pass; else System.out.println("FAIL test7");
        if (pass == total) {
            System.out.println("== 0671 PASS ==");
        } else {
            System.out.println("== 0671 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

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

672. Bulb Switcher II(灯泡开关 Ⅱ)

题目描述

房间中有 n 只已经打开的灯泡,编号从 1 到 n。墙上挂着 4 个开关。

这 4 个开关各自都具有不同的功能,其中:

  • 开关 1:反转当前所有灯的状态(即开变为关,关变为开)
  • 开关 2:反转编号为偶数的灯的状态(即 0, 2, 4, ...)
  • 开关 3:反转编号为奇数的灯的状态(即 1, 3, ...)
  • 开关 4:反转编号为 j = 3k + 1 的灯的状态,其中 k = 0, 1, 2, ...(即 1, 4, 7, 10, ...)
    你必须恰好 按压开关 presses 次。每次按压,你都需要从 4 个开关中选出一个来执行按压操作。
    给你两个整数 n 和 presses,执行完所有按压之后,返回不同可能状态的数量。

示例 1

  • 输入 :n = 1, presses = 1
  • 输出:2
  • 解释:状态可以是:按压开关 1 → 关,按压开关 2 → 开。

示例 2

  • 输入 :n = 2, presses = 1
  • 输出:3

示例 3

  • 输入 :n = 3, presses = 1
  • 输出 :4
    提示:
  • 1 <= n <= 1000
  • 0 <= presses <= 1000

解题思路

关键观察:每个开关按偶数次等价于没按(奇偶性),因此只有"按开关"与"不按开关"两种状态。

进一步,灯 i 与灯 i+6 的状态永远相同(因为 4 个开关对编号的周期性正好是 6:开关 2 与

i mod 2 有关、开关 3 同、开关 4 与 i mod 3 有关,最小公倍数 6),所以只需考虑前

min(n, 6) 盏灯的 6 位二进制状态即可。

解法一(奇偶掩码枚举 + 去重,主提交):枚举 4 个开关的按压奇偶性(16 种组合),每种组合对应

的按压次数 = 选中开关个数;按奇偶组合模拟前 min(n, 6) 盏灯的状态(异或 1),把 6 位状态

压缩为整数去重。若按压次数与 presses 同奇偶且不超过 presses(可以按无效的"开-关"补足),

该状态可达。最后返回去重集合大小。时间复杂度 O(16·6),空间复杂度 O(16)。

解法二(找规律打表):n 较大时结果只与 presses 有关,且随 presses 增大稳定在 8。

可以直接按 n 与 presses 分类讨论:

  • presses == 0 → 1;
  • n == 1 → presses >= 1 ? 2 : 1;
  • n == 2 → presses == 1 ? 3 : (presses == 2 ? 4 : (presses >= 3 ? 4 : 1));
  • n >= 3 → presses == 1 ? 4 : (presses == 2 ? 7 : (presses >= 3 ? 8 : 1))。
    时间复杂度 O(1),空间复杂度 O(1)。(作为对照解法保留。)

代码实现

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

class Solution {

    // 解法一:奇偶掩码枚举 + 前 6 盏灯去重
    public int flipLights(int n, int presses) {
        int len = Math.min(n, 6);
        Set<Integer> states = new HashSet<>();
        for (int mask = 0; mask < 16; ++mask) {
            int used = Integer.bitCount(mask);
            if (used > presses || ((presses - used) % 2) != 0) {
                continue;
            }
            // 初始全 1(二进制位 i 表示第 i 盏灯,1=亮)
            int state = (1 << len) - 1;
            // 开关 1:全部反转(mask 位 0)
            if ((mask & 1) != 0) {
                state ^= ((1 << len) - 1);
            }
            if ((mask & (1 << 1)) != 0) {
                // 开关 2:偶数位置(1-indexed 偶数 → 0-indexed 奇数位)
                for (int i = 0; i < len; ++i) {
                    if (i % 2 == 1) {
                        state ^= (1 << i);
                    }
                }
            }
            if ((mask & (1 << 2)) != 0) {
                // 开关 3:奇数位置(1-indexed 奇数 → 0-indexed 偶数位)
                for (int i = 0; i < len; ++i) {
                    if (i % 2 == 0) {
                        state ^= (1 << i);
                    }
                }
            }
            if ((mask & (1 << 3)) != 0) {
                // 开关 4:编号 1,4,7,...(0-indexed 0,3,6,...)
                for (int i = 0; i < len; ++i) {
                    if (i % 3 == 0) {
                        state ^= (1 << i);
                    }
                }
            }
            states.add(state);
        }
        return states.size();
    }

    // 解法二:分类讨论(打表规律)
    public int flipLights2(int n, int presses) {
        if (presses == 0) {
            return 1;
        }
        if (n == 1) {
            return 2;
        }
        if (n == 2) {
            if (presses == 1) {
                return 3;
            }
            return 4;
        }
        // n >= 3
        if (presses == 1) {
            return 4;
        }
        if (presses == 2) {
            return 7;
        }
        return 8;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (sol.flipLights(1, 1) == 2) ++pass; else System.out.println("FAIL test1: " + sol.flipLights(1, 1));
        ++total; if (sol.flipLights(2, 1) == 3) ++pass; else System.out.println("FAIL test2: " + sol.flipLights(2, 1));
        ++total; if (sol.flipLights(3, 1) == 4) ++pass; else System.out.println("FAIL test3: " + sol.flipLights(3, 1));
        ++total; if (sol.flipLights(3, 2) == 7) ++pass; else System.out.println("FAIL test4: " + sol.flipLights(3, 2));
        ++total; if (sol.flipLights(3, 3) == 8) ++pass; else System.out.println("FAIL test5: " + sol.flipLights(3, 3));
        ++total; if (sol.flipLights(3, 0) == 1) ++pass; else System.out.println("FAIL test6: " + sol.flipLights(3, 0));
        ++total; if (sol.flipLights(2, 2) == 4) ++pass; else System.out.println("FAIL test7: " + sol.flipLights(2, 2));
        ++total; if (sol.flipLights(4, 1000) == 8) ++pass; else System.out.println("FAIL test8: " + sol.flipLights(4, 1000));
        // 解法二与解法一对拍
        boolean ok = true;
        for (int n = 1; n <= 20; ++n) {
            for (int p = 0; p <= 15; ++p) {
                if (sol.flipLights2(n, p) != sol.flipLights(n, p)) {
                    ok = false;
                    System.out.println("FAIL match n=" + n + " p=" + p + " got=" + sol.flipLights2(n, p) + " exp=" + sol.flipLights(n, p));
                }
            }
        }
        ++total; if (ok) ++pass; else System.out.println("FAIL test9");
        if (pass == total) {
            System.out.println("== 0672 PASS ==");
        } else {
            System.out.println("== 0672 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

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

673. Number of Longest Increasing Subsequence(最长递增子序列的个数)

题目描述

给定一个未排序的整数数组 nums,返回最长递增子序列的个数 。

注意这个数列必须是严格递增的。

示例 1

  • 输入 :nums = 1,3,5,4,7
  • 输出:2
  • 解释:有两个最长递增子序列,分别是 1,3,4,7 和 1,3,5,7。

示例 2

  • 输入 :nums = 2,2,2,2,2
  • 输出:5
  • 解释 :最长递增子序列的长度是 1,并且存在 5 个子序列的长度为 1,因此输出 5。
    提示:
  • 1 <= nums.length <= 2000
  • -10^6 <= nums[i] <= 10^6

解题思路

解法一(双 DP 数组,主提交):dp[i] 为以 nums[i] 结尾的最长递增子序列长度,cnt[i] 为其条数。

对每个 i,遍历 j < i:

  • 若 nums[j] < nums[i]:当 dp[j] + 1 > dp[i] 时更新长度并把条数置为 cnt[j];
    当 dp[j] + 1 == dp[i] 时累加条数 cnt[i] += cnt[j]。
  • 若没有任何 j 满足,则 cnt[i] = 1。
    最后找到全局最大长度 mx,把所有 dp[i] == mx 的 cnt[i] 相加。
    注意条数可能很大(如全相等时是 n),用 int 在本题范围内够用(结果 ≤ 2^31-1 前提下),
    为稳妥用 long 累加再转回 int。时间复杂度 O(n²),空间复杂度 O(n)。
    解法二(树状数组/线段树优化):把 nums 离散化后用树状数组维护前缀的(长度, 条数)最大值/条数和,
    使转移降为 O(log n)。时间复杂度 O(n log n),空间复杂度 O(n)。
    实现较复杂,此处作为思路记录;主解法给出 O(n²) 实现(n≤2000 足够)。

代码实现

java 复制代码
class Solution {

    // 解法一:dp + cnt 双数组
    public int findNumberOfLIS(int[] nums) {
        int n = nums.length;
        int[] dp = new int[n];
        int[] cnt = new int[n];
        int mx = 0;
        for (int i = 0; i < n; ++i) {
            dp[i] = 1;
            cnt[i] = 1;
            for (int j = 0; j < i; ++j) {
                if (nums[j] < nums[i]) {
                    if (dp[j] + 1 > dp[i]) {
                        dp[i] = dp[j] + 1;
                        cnt[i] = cnt[j];
                    } else if (dp[j] + 1 == dp[i]) {
                        cnt[i] += cnt[j];
                    }
                }
            }
            mx = Math.max(mx, dp[i]);
        }
        int ans = 0;
        for (int i = 0; i < n; ++i) {
            if (dp[i] == mx) {
                ans += cnt[i];
            }
        }
        return ans;
    }

    // 解法二思路:线段树/树状数组(此处实现 O(n²) 的等价写法占总解法,解法二以说明为主)
    // 为满足"多解法必须给代码",这里给出基于 Fenwick 的实现。
    private int[] bitLen;
    private int[] bitCnt;
    private int m;

    public int findNumberOfLIS2(int[] nums) {
        // 离散化
        int[] sorted = nums.clone();
        java.util.Arrays.sort(sorted);
        m = 0;
        for (int i = 0; i < sorted.length; ++i) {
            if (i == 0 || sorted[i] != sorted[i - 1]) {
                sorted[m++] = sorted[i];
            }
        }
        bitLen = new int[m + 1];
        bitCnt = new int[m + 1];
        int gmax = 0, gcnt = 0;
        for (int x : nums) {
            // 严格递增:查询严格小于 x 的最大值 → rank(x)-1
            int idx = rank(sorted, x);
            int[] pre = query(idx - 1);
            int len = pre[0] + 1;
            int cnt = pre[0] == 0 ? 1 : pre[1];
            update(idx, len, cnt);
            if (len > gmax) {
                gmax = len;
                gcnt = cnt;
            } else if (len == gmax) {
                gcnt += cnt;
            }
        }
        return gcnt;
    }

    // 返回 x 在 sorted 前 m 个中的排名(1 基)
    private int rank(int[] sorted, int x) {
        int lo = 0, hi = m - 1;
        while (lo <= hi) {
            int mid = (lo + hi) >>> 1;
            if (sorted[mid] == x) {
                return mid + 1;
            }
            if (sorted[mid] < x) {
                lo = mid + 1;
            } else {
                hi = mid - 1;
            }
        }
        return lo + 1;
    }

    private void update(int i, int len, int cnt) {
        for (; i <= m; i += i & (-i)) {
            if (len > bitLen[i]) {
                bitLen[i] = len;
                bitCnt[i] = cnt;
            } else if (len == bitLen[i]) {
                bitCnt[i] += cnt;
            }
        }
    }

    private int[] query(int i) {
        int len = 0, cnt = 0;
        for (; i > 0; i -= i & (-i)) {
            if (bitLen[i] > len) {
                len = bitLen[i];
                cnt = bitCnt[i];
            } else if (bitLen[i] == len) {
                cnt += bitCnt[i];
            }
        }
        return new int[] {len, cnt};
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (sol.findNumberOfLIS(new int[] {1, 3, 5, 4, 7}) == 2) ++pass; else System.out.println("FAIL test1: " + sol.findNumberOfLIS(new int[] {1, 3, 5, 4, 7}));
        ++total; if (sol.findNumberOfLIS(new int[] {2, 2, 2, 2, 2}) == 5) ++pass; else System.out.println("FAIL test2: " + sol.findNumberOfLIS(new int[] {2, 2, 2, 2, 2}));
        ++total; if (sol.findNumberOfLIS(new int[] {1}) == 1) ++pass; else System.out.println("FAIL test3: " + sol.findNumberOfLIS(new int[] {1}));
        ++total; if (sol.findNumberOfLIS(new int[] {1, 2, 3, 4, 5}) == 1) ++pass; else System.out.println("FAIL test4: " + sol.findNumberOfLIS(new int[] {1, 2, 3, 4, 5}));
        ++total; if (sol.findNumberOfLIS(new int[] {1, 2, 4, 3, 5, 4, 7, 2}) == 3) ++pass; else System.out.println("FAIL test5: " + sol.findNumberOfLIS(new int[] {1, 2, 4, 3, 5, 4, 7, 2}));
        // 解法二
        ++total; if (sol.findNumberOfLIS2(new int[] {1, 3, 5, 4, 7}) == 2) ++pass; else System.out.println("FAIL test6");
        ++total; if (sol.findNumberOfLIS2(new int[] {2, 2, 2, 2, 2}) == 5) ++pass; else System.out.println("FAIL test7");
        ++total; if (sol.findNumberOfLIS2(new int[] {1, 2, 3, 4, 5}) == 1) ++pass; else System.out.println("FAIL test8");
        ++total; if (sol.findNumberOfLIS2(new int[] {1, 2, 4, 3, 5, 4, 7, 2}) == 3) ++pass; else System.out.println("FAIL test9");
        if (pass == total) {
            System.out.println("== 0673 PASS ==");
        } else {
            System.out.println("== 0673 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

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

674. Longest Continuous Increasing Subsequence(最长连续递增序列)

题目描述

给定一个未经排序的整数数组,找到最长且连续递增 的子序列,并返回该序列的长度。

连续递增的子序列可以由两个下标 l 和 r(l < r)确定,如果对于每个 l <= i < r,

都有 nums[i] < nums[i+1],那么子序列 [nums[l], nums[l+1], ..., nums[r-1], nums[r]] 就是连续递增子序列。

示例 1

  • 输入 :nums = 1,3,5,4,7
  • 输出:3
  • 解释:最长连续递增序列是 1,3,5,长度为 3。尽管 1,3,5,7 也是升序子序列,但它不是连续的。

示例 2

  • 输入 :nums = 2,2,2,2,2
  • 输出:1
  • 解释 :最长连续递增序列是 2,长度为 1。
    提示:
  • 1 <= nums.length <= 10^4
  • -10^9 <= nums[i] <= 10^9

解题思路

连续递增子数组必须相邻,只需一次扫描。

解法一(一次扫描维护当前段长度,主提交):从左到右遍历,若 nums[i] > nums[i-1] 则当前段

len + 1,否则重置为 1;随时更新全局最大。时间复杂度 O(n),空间复杂度 O(1)。

解法二(双指针窗口):用左右指针维护当前递增窗口;遇到下降时左指针跳到右指针处重新计数。

时间复杂度 O(n),空间复杂度 O(1)。

代码实现

java 复制代码
class Solution {

    // 解法一:单次扫描
    public int findLengthOfLCIS(int[] nums) {
        int ans = 1, len = 1;
        for (int i = 1; i < nums.length; ++i) {
            if (nums[i] > nums[i - 1]) {
                ++len;
                if (len > ans) {
                    ans = len;
                }
            } else {
                len = 1;
            }
        }
        return ans;
    }

    // 解法二:双指针
    public int findLengthOfLCIS2(int[] nums) {
        int n = nums.length, ans = 1, left = 0;
        for (int right = 1; right < n; ++right) {
            if (nums[right] <= nums[right - 1]) {
                left = right;
            }
            ans = Math.max(ans, right - left + 1);
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (sol.findLengthOfLCIS(new int[] {1, 3, 5, 4, 7}) == 3) ++pass; else System.out.println("FAIL test1: " + sol.findLengthOfLCIS(new int[] {1, 3, 5, 4, 7}));
        ++total; if (sol.findLengthOfLCIS(new int[] {2, 2, 2, 2, 2}) == 1) ++pass; else System.out.println("FAIL test2: " + sol.findLengthOfLCIS(new int[] {2, 2, 2, 2, 2}));
        ++total; if (sol.findLengthOfLCIS(new int[] {1}) == 1) ++pass; else System.out.println("FAIL test3: " + sol.findLengthOfLCIS(new int[] {1}));
        ++total; if (sol.findLengthOfLCIS(new int[] {1, 3, 5, 7}) == 4) ++pass; else System.out.println("FAIL test4: " + sol.findLengthOfLCIS(new int[] {1, 3, 5, 7}));
        ++total; if (sol.findLengthOfLCIS(new int[] {3, 3, 3, 1, 2, 3}) == 3) ++pass; else System.out.println("FAIL test5: " + sol.findLengthOfLCIS(new int[] {3, 3, 3, 1, 2, 3}));
        ++total; if (sol.findLengthOfLCIS2(new int[] {1, 3, 5, 4, 7}) == 3) ++pass; else System.out.println("FAIL test6");
        ++total; if (sol.findLengthOfLCIS2(new int[] {2, 2, 2, 2, 2}) == 1) ++pass; else System.out.println("FAIL test7");
        ++total; if (sol.findLengthOfLCIS2(new int[] {1, 3, 5, 7}) == 4) ++pass; else System.out.println("FAIL test8");
        ++total; if (sol.findLengthOfLCIS2(new int[] {3, 3, 3, 1, 2, 3}) == 3) ++pass; else System.out.println("FAIL test9");
        if (pass == total) {
            System.out.println("== 0674 PASS ==");
        } else {
            System.out.println("== 0674 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

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

675. Cut Off Trees for Golf Event(为高尔夫比赛砍树)

题目描述

你被请来给一个要举办高尔夫比赛的树林砍树。树林由一个 m x n 的矩阵表示,在这个矩阵中:

  • 0 表示障碍,无法触碰
  • 1 表示地面,可以行走
  • 比 1 大的数表示有树的单元格,可以行走,数值表示树的高度
    每一步,你都可以向上、下、左、右四个方向之一移动一个单位,如果你站的地方有一棵树,那么你可以
    决定是否要砍倒它。
    你需要按照树的高度从低向高砍掉所有的树,每砍过一颗树,该单元格的值变为 1(即变为地面)。
    你将从 (0, 0) 点开始工作,返回你砍完所有树需要走的最小步数。如果你无法砍完所有的树,返回 -1。
    可以保证的是,没有两棵树的高度是相同的,并且你至少需要砍倒一棵树。

示例 1

  • 输入:forest = \[1,2,3,0,0,4,7,6,5]
  • 输出:6

示例 2

  • 输入:forest = \[1,2,3,0,0,0,7,6,5]
  • 输出:-1
  • 解释:由于中间一行被障碍阻塞,无法访问最下面一行中的树。

示例 3

  • 输入:forest = \[2,3,4,0,0,5,8,7,6]
  • 输出:6
  • 解释 :可以按与示例 1 相同的路径来砍掉所有的树。(0,0) 位置的树可以直接砍去,不用算步数。
    提示:
  • m == forest.length
  • n == forest[i].length
  • 1 <= m, n <= 50
  • 0 <= forest[i][j] <= 10^9

解题思路

解法一(排序 + BFS 分段最短路,主提交):树高度互异,必须按高度递增依次砍树。把高度 >1 的

坐标收集起来按高度排序,得到访问序列。从 (0, 0) 出发,对序列中相邻两点做 BFS 求最短距离,

累加所有段距离;任何一段不可达则返回 -1。

注意起点 (0,0) 本身可能是一棵树,直接视为第一棵被砍(题目示例 3 说明不计步数)。

时间复杂度 O(树数 · m·n),空间复杂度 O(m·n)。

解法二(A* 估价函数加速):在相邻点之间用 A* 而不是纯 BFS:优先队列按"已走步数 + 曼哈顿距离"

排序,终点首次出队即为最短。网格小(50×50)时 BFS 已足够;A* 在障碍多、点距远时更快。

时间复杂度 O(树数 · m·n log(m·n)),空间复杂度 O(m·n)。

代码实现

java 复制代码
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.PriorityQueue;

class Solution {

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

    // 解法一:排序 + BFS
    public int cutOffTree(List<List<Integer>> forest) {
        int m = forest.size(), n = forest.get(0).size();
        List<int[]> trees = new ArrayList<>();
        for (int i = 0; i < m; ++i) {
            for (int j = 0; j < n; ++j) {
                int v = forest.get(i).get(j);
                if (v > 1) {
                    trees.add(new int[] {v, i, j});
                }
            }
        }
        Collections.sort(trees, (a, b) -> a[0] - b[0]);
        int sx = 0, sy = 0;
        int total = 0;
        for (int[] t : trees) {
            int d = bfs(forest, sx, sy, t[1], t[2]);
            if (d == -1) {
                return -1;
            }
            total += d;
            sx = t[1];
            sy = t[2];
        }
        return total;
    }

    private int bfs(List<List<Integer>> forest, int sx, int sy, int tx, int ty) {
        int m = forest.size(), n = forest.get(0).size();
        if (sx == tx && sy == ty) {
            return 0;
        }
        boolean[][] vis = new boolean[m][n];
        java.util.Queue<int[]> q = new java.util.ArrayDeque<>();
        q.offer(new int[] {sx, sy});
        vis[sx][sy] = true;
        int steps = 0;
        while (!q.isEmpty()) {
            ++steps;
            int size = q.size();
            for (int k = 0; k < size; ++k) {
                int[] cur = q.poll();
                for (int d = 0; d < 4; ++d) {
                    int nx = cur[0] + DX[d], ny = cur[1] + DY[d];
                    if (nx < 0 || nx >= m || ny < 0 || ny >= n || vis[nx][ny]) {
                        continue;
                    }
                    int v = forest.get(nx).get(ny);
                    if (v == 0) {
                        continue;
                    }
                    if (nx == tx && ny == ty) {
                        return steps;
                    }
                    vis[nx][ny] = true;
                    q.offer(new int[] {nx, ny});
                }
            }
        }
        return -1;
    }

    // 解法二:A*(启发式搜索)
    public int cutOffTree2(List<List<Integer>> forest) {
        int m = forest.size(), n = forest.get(0).size();
        List<int[]> trees = new ArrayList<>();
        for (int i = 0; i < m; ++i) {
            for (int j = 0; j < n; ++j) {
                int v = forest.get(i).get(j);
                if (v > 1) {
                    trees.add(new int[] {v, i, j});
                }
            }
        }
        Collections.sort(trees, (a, b) -> a[0] - b[0]);
        int sx = 0, sy = 0, total = 0;
        for (int[] t : trees) {
            int d = aStar(forest, sx, sy, t[1], t[2]);
            if (d == -1) {
                return -1;
            }
            total += d;
            sx = t[1];
            sy = t[2];
        }
        return total;
    }

    private int aStar(List<List<Integer>> forest, int sx, int sy, int tx, int ty) {
        int m = forest.size(), n = forest.get(0).size();
        if (sx == tx && sy == ty) {
            return 0;
        }
        int[][] dist = new int[m][n];
        for (int i = 0; i < m; ++i) {
            java.util.Arrays.fill(dist[i], Integer.MAX_VALUE);
        }
        // f = g + h(h=曼哈顿距离);最小 f 优先
        PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[0] - b[0]);
        dist[sx][sy] = 0;
        pq.offer(new int[] {Math.abs(sx - tx) + Math.abs(sy - ty), sx, sy});
        while (!pq.isEmpty()) {
            int[] cur = pq.poll();
            int cx = cur[1], cy = cur[2];
            if (cx == tx && cy == ty) {
                return dist[cx][cy];
            }
            for (int d = 0; d < 4; ++d) {
                int nx = cx + DX[d], ny = cy + DY[d];
                if (nx < 0 || nx >= m || ny < 0 || ny >= n || forest.get(nx).get(ny) == 0) {
                    continue;
                }
                int nd = dist[cx][cy] + 1;
                if (nd < dist[nx][ny]) {
                    dist[nx][ny] = nd;
                    pq.offer(new int[] {nd + Math.abs(nx - tx) + Math.abs(ny - ty), nx, ny});
                }
            }
        }
        return -1;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        List<List<Integer>> f1 = new ArrayList<>();
        f1.add(java.util.Arrays.asList(1, 2, 3));
        f1.add(java.util.Arrays.asList(0, 0, 4));
        f1.add(java.util.Arrays.asList(7, 6, 5));
        ++total; if (sol.cutOffTree(f1) == 6) ++pass; else System.out.println("FAIL test1: " + sol.cutOffTree(f1));
        List<List<Integer>> f2 = new ArrayList<>();
        f2.add(java.util.Arrays.asList(1, 2, 3));
        f2.add(java.util.Arrays.asList(0, 0, 0));
        f2.add(java.util.Arrays.asList(7, 6, 5));
        ++total; if (sol.cutOffTree(f2) == -1) ++pass; else System.out.println("FAIL test2: " + sol.cutOffTree(f2));
        List<List<Integer>> f3 = new ArrayList<>();
        f3.add(java.util.Arrays.asList(2, 3, 4));
        f3.add(java.util.Arrays.asList(0, 0, 5));
        f3.add(java.util.Arrays.asList(8, 7, 6));
        ++total; if (sol.cutOffTree(f3) == 6) ++pass; else System.out.println("FAIL test3: " + sol.cutOffTree(f3));
        List<List<Integer>> f4 = new ArrayList<>();
        f4.add(java.util.Arrays.asList(1, 2));
        f4.add(java.util.Arrays.asList(3, 4));
        ++total; if (sol.cutOffTree(f4) == 4) ++pass; else System.out.println("FAIL test4: " + sol.cutOffTree(f4));
        // 解法二
        ++total; if (sol.cutOffTree2(f1) == 6) ++pass; else System.out.println("FAIL test5: " + sol.cutOffTree2(f1));
        ++total; if (sol.cutOffTree2(f2) == -1) ++pass; else System.out.println("FAIL test6");
        ++total; if (sol.cutOffTree2(f3) == 6) ++pass; else System.out.println("FAIL test7: " + sol.cutOffTree2(f3));
        if (pass == total) {
            System.out.println("== 0675 PASS ==");
        } else {
            System.out.println("== 0675 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法 时间复杂度 空间复杂度
解法一 O(T·m·n) O(m·n)
解法二 O(T·m·n log(m·n)) O(m·n)
相关推荐
frjc1 小时前
Spring WebFlux SSE 长连接,可以替换为 Java 21 虚拟线程吗?
java·人工智能·spring·ai·ai编程
Xin7702 小时前
LeetCode 78. 子集(回溯法)
leetcode
INGNIGHT2 小时前
151 · 买卖股票的最佳时机 III(序列dp)
数据结构·算法·leetcode
知守观2 小时前
从 4 套 PDF 库的混战,聊聊老项目的依赖管理与“静默失败”
java·后端·设计模式
深盾科技_Virbox2 小时前
换机或重装后软件许可怎么恢复:先核对哪四项
java·服务器·数据库
Zhuo1Joy2 小时前
中间件挂了怎么办:兜底的边界与取舍
java
用户072367066602 小时前
基于 JavaAgent 的无侵入 Mock:让联调和链路测试不再等下游
java·后端
圣保罗的大教堂2 小时前
leetcode 3644. 排序排列 中等
leetcode