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)
相关推荐
量化分析码农1 小时前
股票历史数据的复权参数对照:多级别返回的字段与数值差异
java·javascript·后端
猫猫不是喵喵.1 小时前
从0到1创建一个微服务项目
java·数据库·微服务·1024程序员节
瑞雪兆丰年兮1 小时前
JavaWeb-day13-SpringBootWeb事务管理&AOP
java·开发语言·springboot·javaweb·事务·web·aop
曲径海盗1 小时前
Try-Except怎么用,3个追问解答在自动化下的应用
java·jvm·oracle
Cosolar2 小时前
极简智库:快速搭建你的开源项目教程阅读平台,让 docs/ 目录被更多人看见
java·架构·github
nice_lcj5203 小时前
【从零理解 Spring AI · 16】Agent 与工作流:什么该写死,什么该让模型决定
java·人工智能·后端·spring ai·ai应用开发
北京云帆互联科技3 小时前
云帆培训考试系统更新说明:v9.3.0
java·学习
学长毕设4 小时前
基于SpringBoot的景点管理系统-附源码
java·spring boot·后端
sxlishaobin5 小时前
Maven Setting详解
java·学习·maven
知守观5 小时前
一个方法两个触发口:反编译 XXL-Job 后,我看清了 inner/job 子包的裸奔现状
java·spring boot·微服务