LeetCode 651-675. 题解合集
文章目录
- [LeetCode 651-675. 题解合集](#LeetCode 651-675. 题解合集)
-
- [651. 4 Keys Keyboard(四个键的键盘)](#651. 4 Keys Keyboard(四个键的键盘))
- [652. Find Duplicate Subtrees(寻找重复的子树)](#652. Find Duplicate Subtrees(寻找重复的子树))
- [653. Two Sum IV - Input is a BST(两数之和 IV - 输入二叉搜索树)](#653. Two Sum IV - Input is a BST(两数之和 IV - 输入二叉搜索树))
- [654. Maximum Binary Tree(最大二叉树)](#654. Maximum Binary Tree(最大二叉树))
- [655. Print Binary Tree(输出二叉树)](#655. Print Binary Tree(输出二叉树))
- [656. Coin Path(成本最小路径)](#656. Coin Path(成本最小路径))
- [657. Robot Return to Origin(机器人能否返回原点)](#657. Robot Return to Origin(机器人能否返回原点))
- [658. Find K Closest Elements(找到 K 个最接近的元素)](#658. Find K Closest Elements(找到 K 个最接近的元素))
- [659. Split Array into Consecutive Subsequences(分割数组为连续子序列)](#659. Split Array into Consecutive Subsequences(分割数组为连续子序列))
- [660. Remove 9(移除 9)](#660. Remove 9(移除 9))
- [661. Image Smoother(图片平滑器)](#661. Image Smoother(图片平滑器))
- [662. Maximum Width of Binary Tree(二叉树最大宽度)](#662. Maximum Width of Binary Tree(二叉树最大宽度))
- [663. Equal Tree Partition(均匀树划分)](#663. Equal Tree Partition(均匀树划分))
- [664. Strange Printer(奇怪的打印机)](#664. Strange Printer(奇怪的打印机))
- [665. Non-decreasing Array(非递减数列)](#665. Non-decreasing Array(非递减数列))
- [666. Path Sum IV(路径总和 IV)](#666. Path Sum IV(路径总和 IV))
- [667. Beautiful Arrangement II(优美的排列 II)](#667. Beautiful Arrangement II(优美的排列 II))
- [668. Kth Smallest Number in Multiplication Table(乘法表中第k小的数)](#668. Kth Smallest Number in Multiplication Table(乘法表中第k小的数))
- [669. Trim a Binary Search Tree(修剪二叉搜索树)](#669. Trim a Binary Search Tree(修剪二叉搜索树))
- [670. Maximum Swap(最大交换)](#670. Maximum Swap(最大交换))
- [671. Second Minimum Node In a Binary Tree(二叉树中第二小的节点)](#671. Second Minimum Node In a Binary Tree(二叉树中第二小的节点))
- [672. Bulb Switcher II(灯泡开关 Ⅱ)](#672. Bulb Switcher II(灯泡开关 Ⅱ))
- [673. Number of Longest Increasing Subsequence(最长递增子序列的个数)](#673. Number of Longest Increasing Subsequence(最长递增子序列的个数))
- [674. Longest Continuous Increasing Subsequence(最长连续递增序列)](#674. Longest Continuous Increasing Subsequence(最长连续递增序列))
- [675. Cut Off Trees for Golf Event(为高尔夫比赛砍树)](#675. Cut Off Trees for Golf Event(为高尔夫比赛砍树))
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 递归地构建:
- 创建一个根节点,其值为
nums中的最大值。 - 递归地在最大值左边 的子数组前缀上构建左子树。
- 递归地在最大值右边 的子数组后缀 上构建右子树。
返回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 <= 10000 <= nums[i] <= 1000nums中的所有整数互不相同
解题思路
解法一(递归 + 线性找最大值,主提交):定义 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] <= 100coins[1] != -11 <= 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^4moves只包含字符'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.length1 <= arr.length <= 10^4arr按升序排列-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] <= 1000nums按非递减顺序排列
解题思路
解法一(哈希表 + 最小堆,主提交):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.lengthn == img[i].length1 <= m, n <= 2000 <= 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 <= 100s由小写英文字母组成
解题思路
解法一(区间动态规划,主提交):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.length1 <= 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 <= 15110 <= nums[i] <= 489nums表示深度小于 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^41 <= 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) |
669. Trim a Binary Search Tree(修剪二叉搜索树)
题目描述
给你二叉搜索树的根节点 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 <= 10000 <= 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.lengthn == forest[i].length1 <= m, n <= 500 <= 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) |