LeetCode 814-825. 题解合集
814. Binary Tree Pruning(二叉树剪枝)
题目描述
给你二叉树的根结点 root,此外树的每个结点的值要么是 0,要么是 1。
返回移除了所有不包含 1 的子树的原二叉树。
节点 node 的子树为 node 本身加上所有 node 的后代。
示例 1
- 输入:
root= [1,null,0,0,1] - 输出:[1,
null,0,null,1]
示例 2
- 输入:
root= [1,0,1,0,0,0,1] - 输出:[1,
null,1,null,1]
示例 3
- 输入:
root= [1,1,0,1,1,0,1,0] - 输出:[1,1,0,1,1,
null,1]
提示: - 树中节点的数目在范围 [1, 200] 内
Node.val为 0 或 1
解题思路
需要删去所有不含 1 的子树。自顶向下无法在见到子树之前判断,应先递归处理左右孩子,再根据
子树是否已被清空决定当前结点是否保留。
解法一(后序递归,主提交):后序返回后,若结点值为 0 且左右皆空,则整棵子树可删。
解法二(非递归后序 + 标记的对照实现):显式栈后序遍历,处理后结点与孩子,便于理解递归
展开过程。
代码实现
class Solution {
// 解法一:后序递归(推荐提交)
public TreeNode pruneTree(TreeNode root) {
if (root == null) {
return null;
}
root.left = pruneTree(root.left);
root.right = pruneTree(root.right);
if (root.val == 0 && root.left == null && root.right == null) {
return null;
}
return root;
}
// 解法二:非递归后序(对照实现)
public TreeNode pruneTree2(TreeNode root) {
java.util.Deque<TreeNode> st = new java.util.ArrayDeque<>();
java.util.Set<TreeNode> done = new java.util.HashSet<>();
TreeNode cur = root;
TreeNode last = null;
while (cur != null || !st.isEmpty()) {
while (cur != null) {
st.push(cur);
cur = cur.left;
}
TreeNode top = st.peek();
if (top.right != null && !done.contains(top.right)) {
cur = top.right;
} else {
st.pop();
// 剪枝:若该结点值为 0 且孩子都被删除(或原本为空)
boolean keep = top.val == 1 || top.left != null || top.right != null;
if (!keep) {
// 需要通知父结点:本子树已删除
if (!st.isEmpty()) {
TreeNode parent = st.peek();
if (parent.left == top) {
parent.left = null;
} else {
parent.right = null;
}
} else {
root = null;
}
}
done.add(top);
last = top;
cur = null;
}
}
return root;
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
// 示例 1: [1,null,0,0,1] -> [1,null,0,null,1]
TreeNode t1 = TreeNode.fromArray(new Integer[]{1, null, 0, 0, 1});
TreeNode r1 = sol.pruneTree(t1);
++total; if ("1,null,0,null,1".equals(TreeNode.toLeetCodeString(r1))) ++pass; else System.out.println("FAIL 814-1: " + TreeNode.toLeetCodeString(r1));
// 示例 2: [1,0,1,0,0,0,1] -> [1,null,1,null,1]
TreeNode t2 = TreeNode.fromArray(new Integer[]{1, 0, 1, 0, 0, 0, 1});
TreeNode r2 = sol.pruneTree(t2);
++total; if ("1,null,1,null,1".equals(TreeNode.toLeetCodeString(r2))) ++pass; else System.out.println("FAIL 814-2: " + TreeNode.toLeetCodeString(r2));
// 示例 3: [1,1,0,1,1,0,1,0] -> [1,1,0,1,1,null,1]
TreeNode t3 = TreeNode.fromArray(new Integer[]{1, 1, 0, 1, 1, 0, 1, 0});
TreeNode r3 = sol.pruneTree(t3);
++total; if ("1,1,0,1,1,null,1".equals(TreeNode.toLeetCodeString(r3))) ++pass; else System.out.println("FAIL 814-3: " + TreeNode.toLeetCodeString(r3));
// 全 0 -> null
TreeNode t4 = TreeNode.fromArray(new Integer[]{0, 0, 0});
TreeNode r4 = sol.pruneTree(t4);
++total; if (r4 == null) ++pass; else System.out.println("FAIL 814-4");
// 解法二
TreeNode t5 = TreeNode.fromArray(new Integer[]{1, null, 0, 0, 1});
TreeNode r5 = sol.pruneTree2(t5);
++total; if ("1,null,0,null,1".equals(TreeNode.toLeetCodeString(r5))) ++pass; else System.out.println("FAIL 814-5: " + TreeNode.toLeetCodeString(r5));
TreeNode t6 = TreeNode.fromArray(new Integer[]{1, 0, 1, 0, 0, 0, 1});
TreeNode r6 = sol.pruneTree2(t6);
++total; if ("1,null,1,null,1".equals(TreeNode.toLeetCodeString(r6))) ++pass; else System.out.println("FAIL 814-6: " + TreeNode.toLeetCodeString(r6));
if (pass == total) {
System.out.println("== 0814 PASS ==");
} else {
System.out.println("== 0814 FAIL ==");
System.exit(1);
}
}
private static class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode() {
}
TreeNode(int val) {
this.val = val;
}
TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
static TreeNode fromArray(Integer[] arr) {
if (arr == null || arr.length == 0 || arr[0] == null) {
return null;
}
TreeNode root = new TreeNode(arr[0]);
java.util.Queue<TreeNode> q = new java.util.LinkedList<>();
q.offer(root);
int idx = 1;
while (!q.isEmpty() && idx < arr.length) {
TreeNode cur = q.poll();
if (idx < arr.length && arr[idx] != null) {
cur.left = new TreeNode(arr[idx]);
q.offer(cur.left);
}
++idx;
if (idx < arr.length && arr[idx] != null) {
cur.right = new TreeNode(arr[idx]);
q.offer(cur.right);
}
++idx;
}
return root;
}
static String toLeetCodeString(TreeNode root) {
if (root == null) {
return "";
}
java.util.List<String> tokens = new java.util.ArrayList<>();
tokens.add(String.valueOf(root.val));
java.util.Queue<TreeNode> q = new java.util.LinkedList<>();
q.offer(root);
while (!q.isEmpty()) {
TreeNode cur = q.poll();
if (cur.left != null) {
tokens.add(String.valueOf(cur.left.val));
q.offer(cur.left);
} else {
tokens.add("null");
}
if (cur.right != null) {
tokens.add(String.valueOf(cur.right.val));
q.offer(cur.right);
} else {
tokens.add("null");
}
}
while (!tokens.isEmpty() && "null".equals(tokens.get(tokens.size() - 1))) {
tokens.remove(tokens.size() - 1);
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < tokens.size(); ++i) {
if (i > 0) {
sb.append(",");
}
sb.append(tokens.get(i));
}
return sb.toString();
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n) | O(h) |
| 解法二 | O(n) | O(n) |
815. Bus Routes(公交路线)
题目描述
给你一个数组 routes,表示一系列公交线路,其中每个 routes[i] 表示一条公交线路,第 i
辆公交车将会在上面循环行驶。例如,路线 routes[0] = [1, 5, 7] 表示第 0 辆公交车会一直按
序列 1 -> 5 -> 7 -> 1 -> ... 这样的车站路线行驶。
现在从 source 车站出发(初始时不在公交车上),要前往 target 车站。期间仅可乘坐公交车。
求出最少乘坐的公交车数量。如果不可能到达终点车站,返回 -1。
示例 1
- 输入:routes = [[1,2,7],[3,6,7]], source = 1,
target= 6 - 输出:2
示例 2
- 输入:routes = [[7,12],[4,5,15],[6],[15,19],[9,12,13]], source = 15,
target= 12 - 输出:-1
提示: 1 <= routes.length <= 5001 <= routes[i].length <= 10^5sum(routes[i].length) <= 10^50 <= routes[i][j] < 10^60 <= source, target < 10^6
解题思路
每次乘坐一辆公交可到达该线路上的任意站点,求最少乘坐次数。按站点逐步走边会把同一辆车
重复展开;站点与线路规模较大,应把「换乘」作为 BFS 的一层。
解法一(BFS 换乘分层,主提交):先建站点到线路的倒排。队列中存站点与已乘车次数,每条线路
只展开一次,把它的全部未访问站点入队。这样每辆车至多处理一遍。
解法二(双层 BFS,对照实现):先按「线路-线路是否共站」建图再跑 BFS,思路不同但同样线性。
代码实现
class Solution {
// 解法一:BFS 换乘分层(推荐提交)
public int numBusesToDestination(int[][] routes, int source, int target) {
if (source == target) {
return 0;
}
// 站点 -> 线路
java.util.Map<Integer, java.util.List<Integer>> stopToBus = new java.util.HashMap<>();
for (int i = 0; i < routes.length; ++i) {
for (int stop : routes[i]) {
stopToBus.computeIfAbsent(stop, k -> new java.util.ArrayList<>()).add(i);
}
}
if (!stopToBus.containsKey(source) || !stopToBus.containsKey(target)) {
return -1;
}
// 队列:站点 + 已乘次数
java.util.ArrayDeque<int[]> q = new java.util.ArrayDeque<>();
boolean[] visBus = new boolean[routes.length];
java.util.Set<Integer> visStop = new java.util.HashSet<>();
q.offer(new int[]{source, 0});
visStop.add(source);
while (!q.isEmpty()) {
int[] cur = q.poll();
int stop = cur[0], cnt = cur[1];
if (stop == target) {
return cnt;
}
for (int bus : stopToBus.get(stop)) {
if (visBus[bus]) {
continue;
}
visBus[bus] = true;
for (int next : routes[bus]) {
if (!visStop.contains(next)) {
visStop.add(next);
q.offer(new int[]{next, cnt + 1});
}
}
}
}
return -1;
}
// 解法二:线路图 BFS(对照实现)
public int numBusesToDestination2(int[][] routes, int source, int target) {
if (source == target) {
return 0;
}
int m = routes.length;
java.util.Map<Integer, java.util.List<Integer>> stopToBus = new java.util.HashMap<>();
for (int i = 0; i < m; ++i) {
for (int stop : routes[i]) {
stopToBus.computeIfAbsent(stop, k -> new java.util.ArrayList<>()).add(i);
}
}
if (!stopToBus.containsKey(source) || !stopToBus.containsKey(target)) {
return -1;
}
boolean[][] share = new boolean[m][m];
for (int i = 0; i < m; ++i) {
java.util.Set<Integer> s = new java.util.HashSet<>();
for (int stop : routes[i]) {
s.add(stop);
}
for (int j = i + 1; j < m; ++j) {
for (int stop : routes[j]) {
if (s.contains(stop)) {
share[i][j] = share[j][i] = true;
break;
}
}
}
}
int[] dist = new int[m];
java.util.Arrays.fill(dist, -1);
java.util.ArrayDeque<Integer> q = new java.util.ArrayDeque<>();
boolean[] startBus = new boolean[m];
for (int b : stopToBus.get(source)) {
startBus[b] = true;
dist[b] = 1;
q.offer(b);
}
while (!q.isEmpty()) {
int b = q.poll();
for (int nb = 0; nb < m; ++nb) {
if (share[b][nb] && dist[nb] == -1) {
dist[nb] = dist[b] + 1;
q.offer(nb);
}
}
}
int ans = Integer.MAX_VALUE;
for (int b : stopToBus.get(target)) {
if (dist[b] != -1) {
ans = Math.min(ans, dist[b]);
}
}
return ans == Integer.MAX_VALUE ? -1 : ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if (sol.numBusesToDestination(new int[][]{{1, 2, 7}, {3, 6, 7}}, 1, 6) == 2) ++pass; else System.out.println("FAIL 815-1");
++total; if (sol.numBusesToDestination(new int[][]{{7, 12}, {4, 5, 15}, {6}, {15, 19}, {9, 12, 13}}, 15, 12) == -1) ++pass; else System.out.println("FAIL 815-2");
++total; if (sol.numBusesToDestination(new int[][]{{1, 2}, {2, 3}}, 1, 3) == 2) ++pass; else System.out.println("FAIL 815-3");
++total; if (sol.numBusesToDestination(new int[][]{{1, 2}, {2, 3}}, 1, 1) == 0) ++pass; else System.out.println("FAIL 815-4");
++total; if (sol.numBusesToDestination(new int[][]{{1, 2, 3}}, 1, 3) == 1) ++pass; else System.out.println("FAIL 815-5");
++total; if (sol.numBusesToDestination(new int[][]{{1}, {2}, {3}}, 1, 3) == -1) ++pass; else System.out.println("FAIL 815-6");
++total; if (sol.numBusesToDestination2(new int[][]{{1, 2, 7}, {3, 6, 7}}, 1, 6) == 2) ++pass; else System.out.println("FAIL 815-7");
++total; if (sol.numBusesToDestination2(new int[][]{{7, 12}, {4, 5, 15}, {6}, {15, 19}, {9, 12, 13}}, 15, 12) == -1) ++pass; else System.out.println("FAIL 815-8");
++total; if (sol.numBusesToDestination2(new int[][]{{1, 2}, {2, 3}}, 1, 3) == 2) ++pass; else System.out.println("FAIL 815-9");
++total; if (sol.numBusesToDestination2(new int[][]{{1, 2, 3}}, 1, 3) == 1) ++pass; else System.out.println("FAIL 815-10");
if (pass == total) {
System.out.println("== 0815 PASS ==");
} else {
System.out.println("== 0815 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(L) | O(L) |
| 解法二 | O(L + m^2)(m 为线路数) | O(L + m^2) |
816. Ambiguous Coordinates(模糊坐标)
题目描述
我们有一些二维坐标,如 "(1, 3)" 或 "(2, 0.5)",然后我们移除所有逗号,小数点和空格,得到
一个字符串 S。返回所有可能的原始字符串到一个列表中。
原始的坐标表示法不会存在多余的零,所以不会出现类似于 "00", "0.0", "0.00", "1.0", "001",
"00.01" 或一些其他更小的数来表示坐标。此外,一个小数点前至少存在一个数,所以也不会出现
".1" 形式的数字。
最后返回的列表可以是任意顺序的。而且注意返回的两个数字中间(逗号之后)都有一个空格。
示例 1
- 输入:"(123)"
- 输出: ["(1, 23)", "(12, 3)", "(1.2, 3)", "(1, 2.3)"]
示例 2
- 输入:"(00011)"
- 输出: ["(0.001, 1)", "(0, 0.011)"]
示例 3
- 输入:"(0123)"
- 输出: ["(0, 123)", "(0, 12.3)", "(0, 1.23)", "(0.1, 23)", "(0.1, 2.3)", "(0.12, 3)"]
示例 4
- 输入:"(100)"
- 输出: ["(10, 0)"]
提示: 4 <= S.length <= 12- S[0] = "(", S[S.length - 1] = ")",且字符串 S 中的其他元素都是数字
解题思路
在括号内插入逗号与可选小数点,枚举所有合法坐标。串长不超过 12,枚举逗号位置后再枚举
小数点位置即可。
解法一(分段合法写法 + 组合,主提交):对左半 [1,i) 与右半 [i,n-1) 分别生成所有合法写法,
再两两组合成 "(x, y)"。
解法二(整体枚举 + 去重,对照实现):三重循环直接枚举逗号与小数点位置,用集合去重后输出。
代码实现
class Solution {
// 解法一:分段合法写法组合(推荐提交)
public java.util.List<String> ambiguousCoordinates(String s) {
int n = s.length();
java.util.List<String> ans = new java.util.ArrayList<>();
for (int i = 2; i < n - 1; ++i) {
java.util.List<String> xs = valid(s, 1, i);
java.util.List<String> ys = valid(s, i, n - 1);
for (String x : xs) {
for (String y : ys) {
ans.add("(" + x + ", " + y + ")");
}
}
}
return ans;
}
// 返回 s[from,to) 的所有合法数字写法
private java.util.List<String> valid(String s, int from, int to) {
java.util.List<String> res = new java.util.ArrayList<>();
for (int k = 1; k <= to - from; ++k) {
String l = s.substring(from, from + k);
String r = s.substring(from + k, to);
// 左半不能以 0 开头(除非只有 0);右半不能以 0 结尾
boolean okL = l.length() == 1 || l.charAt(0) != '0';
boolean okR = r.isEmpty() || r.charAt(r.length() - 1) != '0';
if (okL && okR) {
StringBuilder sb = new StringBuilder(l);
if (!r.isEmpty()) {
sb.append('.').append(r);
}
res.add(sb.toString());
}
}
return res;
}
// 解法二:整体枚举 + 去重
public java.util.List<String> ambiguousCoordinates2(String s) {
int n = s.length();
java.util.TreeSet<String> set = new java.util.TreeSet<>();
for (int i = 2; i < n - 1; ++i) { // 逗号在 i 之前
for (String x : splits(s.substring(1, i))) {
for (String y : splits(s.substring(i, n - 1))) {
set.add("(" + x + ", " + y + ")");
}
}
}
return new java.util.ArrayList<>(set);
}
private java.util.List<String> splits(String t) {
java.util.List<String> res = new java.util.ArrayList<>();
for (int k = 1; k <= t.length(); ++k) {
String l = t.substring(0, k);
String r = t.substring(k);
if (isValid(l, r)) {
res.add(r.isEmpty() ? l : l + "." + r);
}
}
return res;
}
private boolean isValid(String l, String r) {
if (l.length() > 1 && l.charAt(0) == '0') {
return false;
}
if (!r.isEmpty() && r.charAt(r.length() - 1) == '0') {
return false;
}
return true;
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if (sol.ambiguousCoordinates("(123)").size() == 4) ++pass; else System.out.println("FAIL 816-1: " + sol.ambiguousCoordinates("(123)"));
++total; if (sol.ambiguousCoordinates("(00011)").size() == 2) ++pass; else System.out.println("FAIL 816-2: " + sol.ambiguousCoordinates("(00011)"));
++total; if (sol.ambiguousCoordinates("(0123)").size() == 6) ++pass; else System.out.println("FAIL 816-3: " + sol.ambiguousCoordinates("(0123)"));
++total; if (sol.ambiguousCoordinates("(100)").size() == 1) ++pass; else System.out.println("FAIL 816-4: " + sol.ambiguousCoordinates("(100)"));
++total; if (sol.ambiguousCoordinates("(123)").contains("(1, 23)")) ++pass; else System.out.println("FAIL 816-5");
++total; if (sol.ambiguousCoordinates2("(123)").size() == 4) ++pass; else System.out.println("FAIL 816-6");
++total; if (sol.ambiguousCoordinates2("(00011)").size() == 2) ++pass; else System.out.println("FAIL 816-7");
++total; if (sol.ambiguousCoordinates2("(0123)").size() == 6) ++pass; else System.out.println("FAIL 816-8");
++total; if (sol.ambiguousCoordinates2("(100)").size() == 1) ++pass; else System.out.println("FAIL 816-9");
if (pass == total) {
System.out.println("== 0816 PASS ==");
} else {
System.out.println("== 0816 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n^3) | O(n^3)(结果集) |
| 解法二 | O(n^3) | O(n^3) |
817. Linked List Components(链表组件)
题目描述
给你一个链表的头节点 head,该链表包含唯一的整数值,以及一个整数数组 nums,它是链表值的
一个子集。
返回列表 nums 中组件的个数,这里对组件的定义为:链表中一段最长连续结点的值(该值必须在
列表 nums 中)构成的集合。
示例 1
- 输入:
head= [0,1,2,3],nums= [0,1,3] - 输出: 2
- 解释: 链表中,0 和 1 是相连接的,且
nums中不包含 2,所以 [0, 1] 是nums的一个组件,同理
[3] 也是一个组件,故返回 2。
示例 2
- 输入:
head= [0,1,2,3,4],nums= [0,3,1,4] - 输出: 2
提示: - 链表中节点数为
n 1 <= n <= 10^40 <= Node.val < nNode.val中所有值不同1 <= nums.length <= nnums中所有值不同
解题思路
连通分量是 nums 中值在链表上连续出现的极大段。n <= 10^4,把 nums 放入集合后沿链表扫一遍即可。
解法一(哈希集合 + 一次遍历,主提交):跳过不在集合中的结点;每遇到一段在集合中的连续结点
计为一个分量,并滑过整段。
解法二(前驱标记法,对照实现):记录每个 in-set 结点且其前驱不在集合中的个数,等价于
分量的起点数。
代码实现
class Solution {
// 解法一:一次遍历(推荐提交)
public int numComponents(ListNode head, int[] nums) {
java.util.Set<Integer> set = new java.util.HashSet<>();
for (int v : nums) {
set.add(v);
}
int ans = 0;
ListNode cur = head;
while (cur != null) {
if (set.contains(cur.val)) {
++ans;
while (cur != null && set.contains(cur.val)) {
cur = cur.next;
}
} else {
cur = cur.next;
}
}
return ans;
}
// 解法二:分量起点计数
public int numComponents2(ListNode head, int[] nums) {
java.util.Set<Integer> set = new java.util.HashSet<>();
for (int v : nums) {
set.add(v);
}
int ans = 0;
boolean inSet = false;
ListNode cur = head;
while (cur != null) {
if (set.contains(cur.val)) {
if (!inSet) {
++ans; // 新分量起点
inSet = true;
}
} else {
inSet = false;
}
cur = cur.next;
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
ListNode h1 = ListNode.fromArray(new int[]{0, 1, 2, 3});
++total; if (sol.numComponents(h1, new int[]{0, 1, 3}) == 2) ++pass; else System.out.println("FAIL 817-1");
ListNode h2 = ListNode.fromArray(new int[]{0, 1, 2, 3, 4});
++total; if (sol.numComponents(h2, new int[]{0, 3, 1, 4}) == 2) ++pass; else System.out.println("FAIL 817-2");
ListNode h3 = ListNode.fromArray(new int[]{0, 1, 2});
++total; if (sol.numComponents(h3, new int[]{0, 1, 2}) == 1) ++pass; else System.out.println("FAIL 817-3");
ListNode h4 = ListNode.fromArray(new int[]{0});
++total; if (sol.numComponents(h4, new int[]{0}) == 1) ++pass; else System.out.println("FAIL 817-4");
++total; if (sol.numComponents2(h1, new int[]{0, 1, 3}) == 2) ++pass; else System.out.println("FAIL 817-5");
++total; if (sol.numComponents2(h2, new int[]{0, 3, 1, 4}) == 2) ++pass; else System.out.println("FAIL 817-6");
++total; if (sol.numComponents2(h3, new int[]{0, 1, 2}) == 1) ++pass; else System.out.println("FAIL 817-7");
if (pass == total) {
System.out.println("== 0817 PASS ==");
} else {
System.out.println("== 0817 FAIL ==");
System.exit(1);
}
}
}
class ListNode {
int val;
ListNode next;
ListNode() {
}
ListNode(int val) {
this.val = val;
}
ListNode(int val, ListNode next) {
this.val = val;
this.next = next;
}
static ListNode fromArray(int[] arr) {
if (arr == null || arr.length == 0) {
return null;
}
ListNode dummy = new ListNode(0);
ListNode tail = dummy;
for (int v : arr) {
tail.next = new ListNode(v);
tail = tail.next;
}
return dummy.next;
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n) | O(m) |
| 解法二 | O(n) | O(m) |
818. Race Car(赛车)
题目描述
你的赛车可以从位置 0 开始,并且速度为 +1,在一条无限长的数轴上行驶。赛车也可以向负方向
行驶。赛车可以按照由加速指令 'A' 和倒车指令 'R' 组成的指令序列自动行驶。
- 当收到指令 'A' 时,赛车这样行驶:position += speed; speed *= 2
- 当收到指令 'R' 时,赛车这样行驶:如果速度为正数,那么 speed = -1;否则 speed = 1。
当前所处位置不变。
例如,在执行指令 "AAR" 后,赛车位置变化为 0 -> 1 -> 3 -> 3,速度变化为 1 -> 2 -> 4 -> -1。
给你一个目标位置target,返回能到达目标位置的最短指令序列的长度。
示例 1
- 输入:
target= 3 - 输出:2
- 解释:最短指令序列是 "AA"。
示例 2
- 输入:
target= 6 - 输出:5
- 解释:最短指令序列是 "AAARA"。
提示: 1 <= target <= 10^4
解题思路
指令序列对应在数轴上加速或掉头,目标位置可达 10^4。连续 k 次 A 到达 2^k-1,任何目标都可
由「越过再回头」或「中途回头再加速」拼成。
解法一(动态规划,主提交):dp[i] 为到达位置 i 的最少指令。若 i = 2^k-1 则恰好 k 次 A;
否则比较「冲到 2^k-1 再回头」以及在第 k-1 次加速后提前回头走 j 步再折返的代价,取最小。
解法二(BFS 状态搜索,对照实现):状态 (pos, speed) 展开,A/R 两种转移,逐层搜索最短长度;
数据规模小,可交叉验证。
代码实现
class Solution {
// 解法一:动态规划(推荐提交)
public int racecar(int target) {
int[] dp = new int[target + 1];
for (int i = 1; i <= target; ++i) {
int k = 32 - Integer.numberOfLeadingZeros(i); // i 的二进制位数
if (i == (1 << k) - 1) {
dp[i] = k;
continue;
}
// 先冲到 2^k - 1 再回头
dp[i] = dp[(1 << k) - 1 - i] + k + 1;
// 冲到 2^(k-1) - 1 后掉头,走 j 次再掉头
for (int j = 0; j < k; ++j) {
dp[i] = Math.min(dp[i],
dp[i - (1 << (k - 1)) + (1 << j)] + k - 1 + j + 2);
}
}
return dp[target];
}
// 解法二:BFS(对照实现)
public int racecar2(int target) {
if (target == 0) {
return 0;
}
java.util.ArrayDeque<int[]> q = new java.util.ArrayDeque<>();
java.util.Set<Long> vis = new java.util.HashSet<>();
q.offer(new int[]{0, 1}); // pos, speed
long key0 = pack(0, 1);
vis.add(key0);
int steps = 0;
while (!q.isEmpty()) {
int sz = q.size();
for (int t = 0; t < sz; ++t) {
int[] cur = q.poll();
int pos = cur[0], speed = cur[1];
// A
int np = pos + speed;
if (np == target) {
return steps + 1;
}
// 允许范围限制,避免无限扩张
if (Math.abs(np - target) < (1 << 14)) {
long nk = pack(np, speed * 2);
if (!vis.contains(nk)) {
vis.add(nk);
q.offer(new int[]{np, speed * 2});
}
}
// R
int ns = speed > 0 ? -1 : 1;
long nk2 = pack(pos, ns);
if (!vis.contains(nk2)) {
vis.add(nk2);
q.offer(new int[]{pos, ns});
}
}
++steps;
}
return -1;
}
private long pack(int pos, int speed) {
return ((long) (pos + 30000) << 20) | (speed & 0xFFFFF);
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if (sol.racecar(3) == 2) ++pass; else System.out.println("FAIL 818-1");
++total; if (sol.racecar(6) == 5) ++pass; else System.out.println("FAIL 818-2");
++total; if (sol.racecar(1) == 1) ++pass; else System.out.println("FAIL 818-3");
++total; if (sol.racecar(2) == 4) ++pass; else System.out.println("FAIL 818-4");
++total; if (sol.racecar(4) == 5) ++pass; else System.out.println("FAIL 818-5");
++total; if (sol.racecar(7) == 3) ++pass; else System.out.println("FAIL 818-6");
++total; if (sol.racecar(5) == 7) ++pass; else System.out.println("FAIL 818-7");
++total; if (sol.racecar2(3) == 2) ++pass; else System.out.println("FAIL 818-8");
++total; if (sol.racecar2(6) == 5) ++pass; else System.out.println("FAIL 818-9");
++total; if (sol.racecar2(1) == 1) ++pass; else System.out.println("FAIL 818-10");
++total; if (sol.racecar2(4) == 5) ++pass; else System.out.println("FAIL 818-11");
if (pass == total) {
System.out.println("== 0818 PASS ==");
} else {
System.out.println("== 0818 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n log n) | O(n) |
| 解法二 | O(状态数) | O(状态数) |
819. Most Common Word(最常见的单词)
题目描述
给你一个字符串 paragraph 和一个表示禁用词的字符串数组 banned,返回出现频率最高的非
禁用词。题目数据保证至少存在一个非禁用词,且答案唯一。
paragraph 中的单词不区分大小写,答案应以小写形式返回。
注意:单词不包含标点符号。
示例 1
- 输入:paragraph = "Bob hit
aball, the hit BALL flew far after it was hit.", banned = ["hit"] - 输出: "ball"
示例 2
- 输入:paragraph = "
a.", banned = [] - 输出: "
a"
提示: 1 <= paragraph.length <= 1000- paragraph 由英文字母、空格 ' '、和以下符号组成:"!?',;."
0 <= banned.length <= 1001 <= banned[i].length <= 10- banned[i] 仅由小写英文字母组成
解题思路
要在段落中统计单词频率并排除禁用表,同时标点视为分隔符。段落长度 <= 1000,先规范化再计数。
解法一(正则取词 + 哈希计数,主提交):段落转小写后按连续字母取出单词,统计出现次数,
找出不在禁用集中的最高频词。
解法二(双指针切词 + 哈希计数,对照实现):不用正则,手动扫描字母段,逻辑等价。
代码实现
class Solution {
// 解法一:正则取词 + 哈希计数(推荐提交)
public String mostCommonWord(String paragraph, String[] banned) {
java.util.Set<String> bannedWords = new java.util.HashSet<>();
for (String w : banned) {
bannedWords.add(w);
}
java.util.Map<String, Integer> cnt = new java.util.HashMap<>();
java.util.regex.Matcher matcher =
java.util.regex.Pattern.compile("[a-z]+").matcher(paragraph.toLowerCase());
while (matcher.find()) {
String word = matcher.group();
if (!bannedWords.contains(word)) {
cnt.put(word, cnt.getOrDefault(word, 0) + 1);
}
}
String ans = null;
int max = -1;
for (java.util.Map.Entry<String, Integer> e : cnt.entrySet()) {
if (e.getValue() > max) {
max = e.getValue();
ans = e.getKey();
}
}
return ans;
}
// 解法二:双指针切词(对照实现)
public String mostCommonWord2(String paragraph, String[] banned) {
java.util.Set<String> bannedWords = new java.util.HashSet<>();
for (String w : banned) {
bannedWords.add(w);
}
java.util.Map<String, Integer> cnt = new java.util.HashMap<>();
String s = paragraph.toLowerCase();
int n = s.length();
int i = 0;
while (i < n) {
while (i < n && !isLetter(s.charAt(i))) {
++i;
}
int j = i;
while (j < n && isLetter(s.charAt(j))) {
++j;
}
if (i < j) {
String word = s.substring(i, j);
if (!bannedWords.contains(word)) {
cnt.put(word, cnt.getOrDefault(word, 0) + 1);
}
}
i = j;
}
String ans = null;
int max = -1;
for (java.util.Map.Entry<String, Integer> e : cnt.entrySet()) {
if (e.getValue() > max) {
max = e.getValue();
ans = e.getKey();
}
}
return ans;
}
private boolean isLetter(char c) {
return c >= 'a' && c <= 'z';
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if ("ball".equals(sol.mostCommonWord("Bob hit a ball, the hit BALL flew far after it was hit.", new String[]{"hit"}))) ++pass; else System.out.println("FAIL 819-1");
++total; if ("a".equals(sol.mostCommonWord("a.", new String[]{}))) ++pass; else System.out.println("FAIL 819-2");
++total; if ("ball".equals(sol.mostCommonWord("Bob. hIt, baLl", new String[]{"bob", "hit"}))) ++pass; else System.out.println("FAIL 819-3");
++total; if ("a".equals(sol.mostCommonWord("a a a b b", new String[]{"b"}))) ++pass; else System.out.println("FAIL 819-4");
++total; if ("b".equals(sol.mostCommonWord("a a b b b", new String[]{"a"}))) ++pass; else System.out.println("FAIL 819-5");
++total; if ("ball".equals(sol.mostCommonWord2("Bob hit a ball, the hit BALL flew far after it was hit.", new String[]{"hit"}))) ++pass; else System.out.println("FAIL 819-6");
++total; if ("a".equals(sol.mostCommonWord2("a.", new String[]{}))) ++pass; else System.out.println("FAIL 819-7");
++total; if ("a".equals(sol.mostCommonWord2("a a a b b", new String[]{"b"}))) ++pass; else System.out.println("FAIL 819-8");
if (pass == total) {
System.out.println("== 0819 PASS ==");
} else {
System.out.println("== 0819 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n + m) | O(n + m) |
| 解法二 | O(n + m) | O(n + m) |
820. Short Encoding of Words(单词的压缩编码)
题目描述
单词数组 words 的有效编码由任意助记字符串 s 和下标数组 indices 组成,且满足:
words.length == indices.length- 助记字符串
s以 '#' 字符结尾 - 对于每个下标 indices[i],
s的一个从 indices[i] 开始、到下一个 '#' 字符结束(但不包括 '#')
的子字符串恰好与words[i]相等
给你一个单词数组words,返回成功对words进行编码的最小助记字符串s的长度。
示例 1
- 输入:
words= ["time", "me", "bell"] - 输出:10
- 解释:一组有效编码为
s= "time#bell#" 和 indices = [0, 2, 5]。
示例 2
- 输入:
words= ["t"] - 输出:2
提示: 1 <= words.length <= 20001 <= words[i].length <= 7words[i]仅由小写字母组成
解题思路
编码是若干单词加 # 的拼接,较短词若是较长词的后缀则可被其编码覆盖。
解法一(倒序 Trie,主提交):把单词倒序插入 Trie,公共后缀会落在同一条路径上。只有叶子
对应的单词需要单独编码,长度贡献为单词长加一;非叶子已被更长单词覆盖。
解法二(排序去后缀,对照实现):把每个单词反转,按字典序排序。排序后相邻的单词若互为
前后缀,则较短的可被覆盖,只统计不被覆盖的部分。
代码实现
class Solution {
// 解法一:倒序维护 (去重后可复用)(推荐提交)
public int minimumLengthEncoding(String[] words) {
java.util.Set<String> set = new java.util.HashSet<>(java.util.Arrays.asList(words));
// 如果一个词是另一个词的后缀,删掉短的
java.util.List<String> list = new java.util.ArrayList<>(set);
for (String w : list) {
for (int k = 1; k < w.length(); ++k) {
set.remove(w.substring(k));
}
}
int ans = 0;
for (String w : set) {
ans += w.length() + 1;
}
return ans;
}
// 解法二:倒序 Trie,DFS 统计叶子深度(题解主流思路)
private static class TrieNode {
TrieNode[] children = new TrieNode[26];
}
public int minimumLengthEncoding2(String[] words) {
TrieNode root = new TrieNode();
for (String w : words) {
TrieNode cur = root;
for (int i = w.length() - 1; i >= 0; --i) {
int idx = w.charAt(i) - 'a';
if (cur.children[idx] == null) {
cur.children[idx] = new TrieNode();
}
cur = cur.children[idx];
}
}
return dfsSum(root, 1); // 深度从 1 开始(计入 '#')
}
private int dfsSum(TrieNode cur, int depth) {
boolean isLeaf = true;
int ans = 0;
for (int i = 0; i < 26; ++i) {
if (cur.children[i] != null) {
isLeaf = false;
ans += dfsSum(cur.children[i], depth + 1);
}
}
if (isLeaf) {
ans += depth;
}
return ans;
}
// 解法三:反转 + 排序去重(对照实现)
public int minimumLengthEncoding3(String[] words) {
java.util.List<String> rev = new java.util.ArrayList<>();
for (String w : words) {
rev.add(new StringBuilder(w).reverse().toString());
}
java.util.Collections.sort(rev);
int ans = 0;
for (int i = 0; i < rev.size(); ++i) {
if (i + 1 < rev.size() && rev.get(i + 1).startsWith(rev.get(i))) {
continue; // 是下一个的后缀(反转后是前缀),可覆盖
}
ans += rev.get(i).length() + 1;
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if (sol.minimumLengthEncoding(new String[]{"time", "me", "bell"}) == 10) ++pass; else System.out.println("FAIL 820-1");
++total; if (sol.minimumLengthEncoding(new String[]{"t"}) == 2) ++pass; else System.out.println("FAIL 820-2");
++total; if (sol.minimumLengthEncoding(new String[]{"time", "time", "time"}) == 5) ++pass; else System.out.println("FAIL 820-3");
++total; if (sol.minimumLengthEncoding(new String[]{"me", "time"}) == 5) ++pass; else System.out.println("FAIL 820-4");
++total; if (sol.minimumLengthEncoding(new String[]{"fe", "x", "be"}) == 8) ++pass; else System.out.println("FAIL 820-5");
++total; if (sol.minimumLengthEncoding2(new String[]{"time", "me", "bell"}) == 10) ++pass; else System.out.println("FAIL 820-6");
++total; if (sol.minimumLengthEncoding2(new String[]{"t"}) == 2) ++pass; else System.out.println("FAIL 820-7");
++total; if (sol.minimumLengthEncoding2(new String[]{"me", "time"}) == 5) ++pass; else System.out.println("FAIL 820-8");
++total; if (sol.minimumLengthEncoding3(new String[]{"time", "me", "bell"}) == 10) ++pass; else System.out.println("FAIL 820-9");
++total; if (sol.minimumLengthEncoding3(new String[]{"t"}) == 2) ++pass; else System.out.println("FAIL 820-10");
++total; if (sol.minimumLengthEncoding3(new String[]{"me", "time"}) == 5) ++pass; else System.out.println("FAIL 820-11");
if (pass == total) {
System.out.println("== 0820 PASS ==");
} else {
System.out.println("== 0820 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(sum(len(w))) | O(sum(len(w))) |
| 解法二 | O(sum(len(w)) + L log L) | O(L) |
821. Shortest Distance to a Character(字符的最短距离)
题目描述
给你一个字符串 s 和一个字符 c,且 c 是 s 中出现过的字符。
返回一个整数数组 answer,其中 answer.length == s.length 且 answer[i] 是 s 中从下标
i 到离它最近的字符 c 的距离。
两个下标 i 和 j 之间的距离为 abs(i - j),其中 abs 是绝对值函数。
示例 1
- 输入:
s= "loveleetcode",c= "e" - 输出:[3,2,1,0,1,0,0,1,2,2,1,0]
示例 2
- 输入:
s= "aaab",c= "b" - 输出:[3,2,1,0]
提示: 1 <= s.length <= 10^4s[i]和c均为小写英文字母- 题目数据保证
c在s中至少出现一次
解题思路
对每个位置求到最近字符 c 的距离。最近的 c 只可能来自左侧或右侧。
解法一(两次遍历,主提交):从左向右记录最近的 c 下标,再从右向左扫一遍取更近者,
两遍线性扫描。
解法二(记录出现位置 + 二分,对照实现):先收集 c 的所有位置,对每个下标二分查找最近的
c 位置,逻辑直观。
代码实现
class Solution {
// 解法一:两次遍历(推荐提交)
public int[] shortestToChar(String s, char c) {
int n = s.length();
int[] ans = new int[n];
java.util.Arrays.fill(ans, Integer.MAX_VALUE);
int pre = Integer.MIN_VALUE / 2;
for (int i = 0; i < n; ++i) {
if (s.charAt(i) == c) {
pre = i;
}
ans[i] = Math.min(ans[i], i - pre);
}
int suf = Integer.MAX_VALUE / 2;
for (int i = n - 1; i >= 0; --i) {
if (s.charAt(i) == c) {
suf = i;
}
ans[i] = Math.min(ans[i], suf - i);
}
return ans;
}
// 解法二:位置收集 + 二分(对照实现)
public int[] shortestToChar2(String s, char c) {
java.util.List<Integer> pos = new java.util.ArrayList<>();
for (int i = 0; i < s.length(); ++i) {
if (s.charAt(i) == c) {
pos.add(i);
}
}
int[] ans = new int[s.length()];
for (int i = 0; i < s.length(); ++i) {
int idx = lowerBound(pos, i);
int best = Integer.MAX_VALUE;
if (idx < pos.size()) {
best = Math.min(best, pos.get(idx) - i);
}
if (idx > 0) {
best = Math.min(best, i - pos.get(idx - 1));
}
ans[i] = best;
}
return ans;
}
private int lowerBound(java.util.List<Integer> a, int key) {
int lo = 0, hi = a.size();
while (lo < hi) {
int mid = (lo + hi) >>> 1;
if (a.get(mid) < key) {
lo = mid + 1;
} else {
hi = mid;
}
}
return lo;
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if (java.util.Arrays.equals(sol.shortestToChar("loveleetcode", 'e'), new int[]{3, 2, 1, 0, 1, 0, 0, 1, 2, 2, 1, 0})) ++pass; else System.out.println("FAIL 821-1");
++total; if (java.util.Arrays.equals(sol.shortestToChar("aaab", 'b'), new int[]{3, 2, 1, 0})) ++pass; else System.out.println("FAIL 821-2");
++total; if (java.util.Arrays.equals(sol.shortestToChar("a", 'a'), new int[]{0})) ++pass; else System.out.println("FAIL 821-3");
++total; if (java.util.Arrays.equals(sol.shortestToChar("abaa", 'b'), new int[]{1, 0, 1, 2})) ++pass; else System.out.println("FAIL 821-4");
++total; if (java.util.Arrays.equals(sol.shortestToChar2("loveleetcode", 'e'), new int[]{3, 2, 1, 0, 1, 0, 0, 1, 2, 2, 1, 0})) ++pass; else System.out.println("FAIL 821-5");
++total; if (java.util.Arrays.equals(sol.shortestToChar2("aaab", 'b'), new int[]{3, 2, 1, 0})) ++pass; else System.out.println("FAIL 821-6");
++total; if (java.util.Arrays.equals(sol.shortestToChar2("abaa", 'b'), new int[]{1, 0, 1, 2})) ++pass; else System.out.println("FAIL 821-7");
if (pass == total) {
System.out.println("== 0821 PASS ==");
} else {
System.out.println("== 0821 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n) | O(1)(除答案数组) |
| 解法二 | O(n log m) | O(m) |
822. Card Flipping Game(翻转卡片游戏)
题目描述
在桌子上有 n 张卡片,每张卡片的正面和背面都写着一个正数(正面与背面上的数有可能不一样)。
我们可以先翻转任意张卡片,然后选择其中一张卡片。
如果选中的那张卡片背面的数字 x 与任意一张卡片的正面的数字都不同,那么这个数字是我们想要
的数字。
哪个数是这些想要的数字中最小的数(找到这些数中的最小值)呢?如果没有一个数字符合要求的,
输出 0。
其中,fronts[i] 和 backs[i] 分别代表第 i 张卡片的正面和背面的数字。
如果我们通过翻转卡片来交换正面与背面上的数,那么当初在正面的数就变成背面的数,背面的数就
变成正面的数。
示例 1
- 输入:fronts = [1,2,4,4,7], backs = [1,3,4,1,3]
- 输出:2
示例 2
- 输入:fronts = [1], backs = [1]
- 输出:0
提示: n == fronts.length == backs.length1 <= n <= 10001 <= fronts[i], backs[i] <= 2000
解题思路
翻牌后正面朝上的数若在某张牌的正反面同时出现,则该数不可能成为答案。n <= 1000,先找出
所有正反相同的数,它们永远不能作为最终正面。
解法一(哈希集合,主提交):所有正反面相同的数加入集合;在其余出现过的数字中取最小者,
若没有合法数字则返回 0。
解法二(布尔数组标记 + 两次扫描,对照实现):数值上界 2000,用布尔数组模拟集合,
逻辑与解法一同源。
代码实现
class Solution {
// 解法一:哈希集合(推荐提交)
public int flipgame(int[] fronts, int[] backs) {
java.util.Set<Integer> forbidden = new java.util.HashSet<>();
int n = fronts.length;
for (int i = 0; i < n; ++i) {
if (fronts[i] == backs[i]) {
forbidden.add(fronts[i]);
}
}
int ans = Integer.MAX_VALUE;
for (int i = 0; i < n; ++i) {
if (!forbidden.contains(fronts[i])) {
ans = Math.min(ans, fronts[i]);
}
if (!forbidden.contains(backs[i])) {
ans = Math.min(ans, backs[i]);
}
}
return ans == Integer.MAX_VALUE ? 0 : ans;
}
// 解法二:布尔数组标记(对照实现)
public int flipgame2(int[] fronts, int[] backs) {
boolean[] forbidden = new boolean[2001];
int n = fronts.length;
for (int i = 0; i < n; ++i) {
if (fronts[i] == backs[i]) {
forbidden[fronts[i]] = true;
}
}
int ans = 0;
for (int i = 0; i < n; ++i) {
if (!forbidden[fronts[i]]) {
ans = (ans == 0) ? fronts[i] : Math.min(ans, fronts[i]);
}
if (!forbidden[backs[i]]) {
ans = (ans == 0) ? backs[i] : Math.min(ans, backs[i]);
}
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if (sol.flipgame(new int[]{1, 2, 4, 4, 7}, new int[]{1, 3, 4, 1, 3}) == 2) ++pass; else System.out.println("FAIL 822-1");
++total; if (sol.flipgame(new int[]{1}, new int[]{1}) == 0) ++pass; else System.out.println("FAIL 822-2");
++total; if (sol.flipgame(new int[]{1, 2}, new int[]{1, 3}) == 2) ++pass; else System.out.println("FAIL 822-3");
++total; if (sol.flipgame(new int[]{1, 1}, new int[]{2, 2}) == 1) ++pass; else System.out.println("FAIL 822-4");
++total; if (sol.flipgame(new int[]{2, 3}, new int[]{2, 2}) == 3) ++pass; else System.out.println("FAIL 822-5");
++total; if (sol.flipgame2(new int[]{1, 2, 4, 4, 7}, new int[]{1, 3, 4, 1, 3}) == 2) ++pass; else System.out.println("FAIL 822-6");
++total; if (sol.flipgame2(new int[]{1}, new int[]{1}) == 0) ++pass; else System.out.println("FAIL 822-7");
++total; if (sol.flipgame2(new int[]{2, 3}, new int[]{2, 2}) == 3) ++pass; else System.out.println("FAIL 822-8");
if (pass == total) {
System.out.println("== 0822 PASS ==");
} else {
System.out.println("== 0822 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n) | O(n) |
| 解法二 | O(n + V) | O(V) |
823. Binary Trees With Factors(带因子的二叉树)
题目描述
给出一个含有不重复整数元素的数组 arr,每个整数 arr[i] 均大于 1。
用这些整数来构建二叉树,每个整数可以使用任意次数。其中:每个非叶结点的值应等于它的两个
子结点的值的乘积。
满足条件的二叉树一共有多少个?答案可能很大,返回对 10^9 + 7 取余的结果。
示例 1
- 输入:
arr= [2, 4] - 输出: 3
- 解释: 可以得到这些二叉树: [2], [4], [4, 2, 2]
示例 2
- 输入:
arr= [2, 4, 5, 10] - 输出: 7
- 解释: 可以得到这些二叉树: [2], [4], [5], [10], [4, 2, 2], [10, 2, 5], [10, 5, 2]
提示: 1 <= arr.length <= 10002 <= arr[i] <= 10^9arr中的所有值互不相同
解题思路
以数组中的数为结点、且孩子之积等于父结点,统计可构成的带标号二叉树。数组长度 <= 1000,
值互不相同,应按值从小到大构造,以便孩子先于父亲算完。
解法一(排序 + 动态规划,主提交):f[i] 为以 arr[i] 为根的方案数,枚举左孩子 b,若 a/b
也在数组中则累加 f[b] * f[c]。每个结点至少有一种单结点树,最后对 f 求和取模。
解法二(记忆化搜索,对照实现):对每个数递归计算其方案数,逻辑与 DP 等价。
代码实现
class Solution {
private static final int MOD = 1_000_000_007;
// 解法一:排序 + DP(推荐提交)
public int numFactoredBinaryTrees(int[] arr) {
java.util.Arrays.sort(arr);
int n = arr.length;
long[] f = new long[n];
java.util.Arrays.fill(f, 1);
java.util.Map<Integer, Integer> idx = new java.util.HashMap<>();
for (int i = 0; i < n; ++i) {
idx.put(arr[i], i);
}
for (int i = 0; i < n; ++i) {
for (int j = 0; j < i; ++j) {
int b = arr[j];
if (arr[i] % b == 0) {
int c = arr[i] / b;
Integer k = idx.get(c);
if (k != null) {
f[i] = (f[i] + f[j] * f[k]) % MOD;
}
}
}
}
long ans = 0;
for (long v : f) {
ans = (ans + v) % MOD;
}
return (int) ans;
}
// 解法二:记忆化搜索(对照实现)
private java.util.Map<Integer, Long> memo;
private java.util.Set<Integer> all;
public int numFactoredBinaryTrees2(int[] arr) {
all = new java.util.HashSet<>();
for (int v : arr) {
all.add(v);
}
memo = new java.util.HashMap<>();
long ans = 0;
for (int v : arr) {
ans = (ans + count(v)) % MOD;
}
return (int) ans;
}
private long count(int v) {
Long cached = memo.get(v);
if (cached != null) {
return cached;
}
long ways = 1; // 单结点
for (int b : all) {
if (v % b == 0 && b * 1L * b <= v) { // 只枚举 b <= sqrt,另一侧乘 2 处理
int c = v / b;
if (c != b) {
if (all.contains(c)) {
ways = (ways + count(b) * count(c) * 2) % MOD;
}
} else {
if (all.contains(c)) {
ways = (ways + count(b) * count(c)) % MOD;
}
}
}
}
memo.put(v, ways);
return ways;
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if (sol.numFactoredBinaryTrees(new int[]{2, 4}) == 3) ++pass; else System.out.println("FAIL 823-1");
++total; if (sol.numFactoredBinaryTrees(new int[]{2, 4, 5, 10}) == 7) ++pass; else System.out.println("FAIL 823-2");
++total; if (sol.numFactoredBinaryTrees(new int[]{2}) == 1) ++pass; else System.out.println("FAIL 823-3");
++total; if (sol.numFactoredBinaryTrees(new int[]{2, 3, 6}) == 5) ++pass; else System.out.println("FAIL 823-4");
++total; if (sol.numFactoredBinaryTrees(new int[]{18, 3, 6, 2}) == 12) ++pass; else System.out.println("FAIL 823-5");
++total; if (sol.numFactoredBinaryTrees2(new int[]{2, 4}) == 3) ++pass; else System.out.println("FAIL 823-6");
++total; if (sol.numFactoredBinaryTrees2(new int[]{2, 4, 5, 10}) == 7) ++pass; else System.out.println("FAIL 823-7");
++total; if (sol.numFactoredBinaryTrees2(new int[]{2, 3, 6}) == 5) ++pass; else System.out.println("FAIL 823-8");
if (pass == total) {
System.out.println("== 0823 PASS ==");
} else {
System.out.println("== 0823 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n^2) | O(n) |
| 解法二 | O(n^2) | O(n) |
824. Goat Latin(山羊拉丁文)
题目描述
给你一个由若干单词组成的句子 sentence,单词间由空格分隔。每个单词仅由大写和小写英文字母
组成。
请你将句子转换为“山羊拉丁文(Goat Latin)”(一种类似于猪拉丁文 - Pig Latin 的虚构语言)。
山羊拉丁文的规则如下:
- 如果单词以元音开头('
a', 'e', 'i', 'o', 'u'),在单词后添加 "ma"。
例如,单词 "apple" 变为 "applema"。 - 如果单词以辅音字母开头(即,非元音字母),移除第一个字符并将它放到末尾,之后再添加 "ma"。
例如,单词 "goat" 变为 "oatgma"。 - 根据单词在句子中的索引,在单词最后添加与索引相同数量的字母 '
a',索引从 1 开始。
例如,在第一个单词后添加 "a",在第二个单词后添加 "aa",以此类推。
返回将 sentence 转换为山羊拉丁文后的句子。
示例 1
- 输入:sentence = "I speak Goat Latin"
- 输出:"Imaa peaksmaaa
oatGmaaaaatinLmaaaaa"
示例 2
- 输入:sentence = "The quick brown fox jumped over the lazy dog"
- 输出:"
heTmaauickqmaaa rownbmaaaa oxfmaaaaa umpedjmaaaaaa overmaaaaaaa hetmaaaaaaaa azylmaaaaaaaaa ogdmaaaaaaaaaa"
提示: 1 <= sentence.length <= 150- sentence 由英文字母和空格组成
- sentence 不含前导或尾随空格
- sentence 中的所有单词由单个空格分隔
解题思路
按规则改写每个单词:辅音开头则把首字母移到末尾,再统一加 ma 以及与下标相应个数的 a。
解法一(逐词处理 + 字符串拼接,主提交):用小写判断元音,第 i 个单词(从 1 计)追加 i 个 a,
最后用空格拼回。
解法二(StringBuilder 单次扫描,对照实现):不 split,直接扫描单词并构造,减少中间串。
代码实现
class Solution {
private static final java.util.Set<Character> VOWELS =
new java.util.HashSet<>(java.util.Arrays.asList('a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U'));
// 解法一:split 逐词处理(推荐提交)
public String toGoatLatin(String sentence) {
String[] words = sentence.split(" ");
StringBuilder ans = new StringBuilder();
for (int i = 0; i < words.length; ++i) {
String w = words[i];
StringBuilder t = new StringBuilder();
if (VOWELS.contains(w.charAt(0))) {
t.append(w);
} else {
t.append(w.substring(1)).append(w.charAt(0));
}
t.append("ma");
for (int j = 0; j <= i; ++j) {
t.append('a');
}
if (i > 0) {
ans.append(' ');
}
ans.append(t);
}
return ans.toString();
}
// 解法二:单次扫描(对照实现)
public String toGoatLatin2(String sentence) {
StringBuilder ans = new StringBuilder();
int idx = 1;
int i = 0, n = sentence.length();
while (i < n) {
int j = i;
while (j < n && sentence.charAt(j) != ' ') {
++j;
}
String w = sentence.substring(i, j);
StringBuilder t = new StringBuilder();
if (VOWELS.contains(w.charAt(0))) {
t.append(w);
} else {
t.append(w.substring(1)).append(w.charAt(0));
}
t.append("ma");
for (int k = 0; k < idx; ++k) {
t.append('a');
}
if (ans.length() > 0) {
ans.append(' ');
}
ans.append(t);
++idx;
i = j + 1;
}
return ans.toString();
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if ("Imaa peaksmaaa oatGmaaaa atinLmaaaaa".equals(sol.toGoatLatin("I speak Goat Latin"))) ++pass; else System.out.println("FAIL 824-1");
++total; if ("heTmaa uickqmaaa rownbmaaaa oxfmaaaaa umpedjmaaaaaa overmaaaaaaa hetmaaaaaaaa azylmaaaaaaaaa ogdmaaaaaaaaaa".equals(sol.toGoatLatin("The quick brown fox jumped over the lazy dog"))) ++pass; else System.out.println("FAIL 824-2");
++total; if ("amaa".equals(sol.toGoatLatin("a"))) ++pass; else System.out.println("FAIL 824-3");
++total; if ("applemaa applemaaa".equals(sol.toGoatLatin("apple apple"))) ++pass; else System.out.println("FAIL 824-4");
++total; if ("eachwordmaa".equals(sol.toGoatLatin("eachword"))) ++pass; else System.out.println("FAIL 824-5");
++total; if ("bmaa".equals(sol.toGoatLatin("b"))) ++pass; else System.out.println("FAIL 824-6");
++total; if ("Imaa peaksmaaa oatGmaaaa atinLmaaaaa".equals(sol.toGoatLatin2("I speak Goat Latin"))) ++pass; else System.out.println("FAIL 824-7");
++total; if ("heTmaa uickqmaaa rownbmaaaa oxfmaaaaa umpedjmaaaaaa overmaaaaaaa hetmaaaaaaaa azylmaaaaaaaaa ogdmaaaaaaaaaa".equals(sol.toGoatLatin2("The quick brown fox jumped over the lazy dog"))) ++pass; else System.out.println("FAIL 824-8");
++total; if ("amaa".equals(sol.toGoatLatin2("a"))) ++pass; else System.out.println("FAIL 824-9");
if (pass == total) {
System.out.println("== 0824 PASS ==");
} else {
System.out.println("== 0824 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n^2)(拼接) | O(n) |
| 解法二 | O(n^2)(拼接) | O(n) |
825. Friends Of Appropriate Ages(适龄的朋友)
题目描述
在社交媒体网站上有 n 个用户。给你一个整数数组 ages,其中 ages[i] 是第 i 个用户的年龄。
如果下述任意一个条件为真,那么用户 x 将不会向用户 y(x != y)发送好友请求:
1. ages[y] <= 0.5 * ages[x] + 7
2. ages[y] > ages[x]
3. ages[y] > 100 && ages[x] < 100
否则,x 将会向 y 发送一条好友请求。
注意,如果 x 向 y 发送一条好友请求,y 不必也向 x 发送一条好友请求。另外,用户不会向自己发送
好友请求。
返回在该社交媒体网站上产生的好友请求总数。
示例 1
- 输入:ages = [16,16]
- 输出:2
示例 2
- 输入:ages = [16,17,18]
- 输出:2
示例 3
- 输入:ages = [20,30,100,110,120]
- 输出:3 提示:
n == ages.length1 <= n <= 2 * 10^41 <= ages[i] <= 120
解题思路
好友请求的条件只依赖两个年龄,且年龄范围仅 1..120,而 n 可达 2*10^4。两两枚举人会重复 计算相同年龄对。 解法一(计数 + 枚举年龄对,主提交):先计数每个年龄的人数,再枚举年龄对 (ax, ay) 判断是否 满足三个不等式。同一年龄内部要去掉自己给自己发请求,贡献为 x * (y - [ax == ay])。 解法二(排序 + 双指针,对照实现):对年龄排序,对每个 x 用二分/边界确定满足条件 y 的区间, 统计区间内人数后减去自身。
代码实现
class Solution {
// 解法一:计数 + 枚举年龄对(推荐提交)
public int numFriendRequests(int[] ages) {
int[] cnt = new int[121];
for (int a : ages) {
++cnt[a];
}
int ans = 0;
for (int ax = 1; ax <= 120; ++ax) {
for (int ay = 1; ay <= 120; ++ay) {
if (canSend(ax, ay)) {
if (ax == ay) {
ans += cnt[ax] * (cnt[ax] - 1);
} else {
ans += cnt[ax] * cnt[ay];
}
}
}
}
return ans;
}
// x 是否可以向 y 发送请求
private boolean canSend(int ax, int ay) {
if (ay <= 0.5 * ax + 7) {
return false;
}
if (ay > ax) {
return false;
}
if (ay > 100 && ax < 100) {
return false;
}
return true;
}
// 解法二:排序 + 二分(对照实现)
public int numFriendRequests2(int[] ages) {
java.util.Arrays.sort(ages);
int n = ages.length;
int ans = 0;
for (int i = 0; i < n; ++i) {
int ax = ages[i];
// y 必须满足 ay > 0.5*ax + 7 且 ay <= ax
int lo = upperBound(ages, (int) (0.5 * ax + 7)); // 第一个 > 0.5*ax+7 的下标
int hi = upperBound(ages, ax) - 1; // 最后一个 <= ax 的下标
if (lo <= hi) {
ans += hi - lo + 1;
// 减去自己给自己发的一条(自己一定在区间内)
--ans;
}
}
return ans;
}
private int upperBound(int[] a, int key) {
int lo = 0, hi = a.length;
while (lo < hi) {
int mid = (lo + hi) >>> 1;
if (a[mid] <= key) {
lo = mid + 1;
} else {
hi = mid;
}
}
return lo;
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if (sol.numFriendRequests(new int[]{16, 16}) == 2) ++pass; else System.out.println("FAIL 825-1");
++total; if (sol.numFriendRequests(new int[]{16, 17, 18}) == 2) ++pass; else System.out.println("FAIL 825-2");
++total; if (sol.numFriendRequests(new int[]{20, 30, 100, 110, 120}) == 3) ++pass; else System.out.println("FAIL 825-3");
++total; if (sol.numFriendRequests(new int[]{101, 56, 69, 48, 30}) == 4) ++pass; else System.out.println("FAIL 825-4");
++total; if (sol.numFriendRequests(new int[]{73, 106, 39, 6, 26, 15, 30, 100, 71, 35, 46, 112, 6, 60, 110}) == 29) ++pass; else System.out.println("FAIL 825-5");
++total; if (sol.numFriendRequests2(new int[]{16, 16}) == 2) ++pass; else System.out.println("FAIL 825-6");
++total; if (sol.numFriendRequests2(new int[]{16, 17, 18}) == 2) ++pass; else System.out.println("FAIL 825-7");
++total; if (sol.numFriendRequests2(new int[]{20, 30, 100, 110, 120}) == 3) ++pass; else System.out.println("FAIL 825-8");
++total; if (sol.numFriendRequests2(new int[]{101, 56, 69, 48, 30}) == 4) ++pass; else System.out.println("FAIL 825-9");
if (pass == total) {
System.out.println("== 0825 PASS ==");
} else {
System.out.println("== 0825 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n + 121^2) | O(121) |
| 解法二 | O(n log n) | O(n) |