LeetCode 776-789. 题解合集
776. Split BST(拆分二叉搜索树)
题目描述
给你一棵二叉搜索树(BST)的根结点 root 和一个整数 target。请将该树按要求拆分为两个子树:
其中第一个子树结点的值都必须小于等于给定的目标值;另一个子树结点的值都必须大于目标值;
树中并非一定要存在值为 target 的结点。
除此之外,树中大部分结构都需要保留,也就是说原始树中父节点 p 的任意子节点 c,假如拆分后
它们仍在同一个子树中,那么结点 p 应仍为 c 的父结点。
按顺序返回两个子树的根结点的数组。
示例 1
- 输入:
root= [4,2,6,1,3,5,7],target= 2 - 输出:[[2,1],[4,3,6,
null,null,5,7]]
示例 2
- 输入:
root= [1],target= 1 - 输出:[[1],[]]
提示: - 二叉搜索树节点个数在 [1, 50] 范围内
0 <= Node.val, target <= 1000
解题思路
解法一(递归分治,主提交):递归函数返回以当前节点为根的子树拆成的两个部分。
若 root.val <= target,则 root 连同其左子树整体属于「小树」,只需继续拆分右子树:
拆出的"较小部分"接在 root.right 上,root 本身作为小树的根;
若 root.val > target,对称地,整体属于「大树」,继续拆左子树,"较大部分"接在 root.left 上。
时间复杂度 O(h),空间复杂度 O(h)(h 为树高)。
解法二(迭代 + 哨兵拼接):用两个哨兵节点分别做小树大树的链尾,沿树游走:遇到 val <= target
的节点就挂到小树链尾并向右深入(其左子树天然属于小树),遇到 val > target 的节点就挂到
大树链尾并向左深入。每挂一个节点即断开对应方向的指针,最终哨兵的下一个节点即两棵树的根。
时间复杂度 O(h),空间复杂度 O(1)。
代码实现
import java.util.ArrayDeque;
import java.util.Deque;
class Solution {
private static class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int val) { this.val = val; }
}
// 解法一:递归分治
public TreeNode[] splitBST(TreeNode root, int target) {
if (root == null) {
return new TreeNode[] {null, null};
}
if (root.val <= target) {
TreeNode[] pair = splitBST(root.right, target);
root.right = pair[0];
return new TreeNode[] {root, pair[1]};
} else {
TreeNode[] pair = splitBST(root.left, target);
root.left = pair[1];
return new TreeNode[] {pair[0], root};
}
}
// 解法二:迭代 + 哨兵拼接
public TreeNode[] splitBST2(TreeNode root, int target) {
TreeNode small = new TreeNode(-1);
TreeNode large = new TreeNode(-1);
TreeNode ps = small, pl = large;
TreeNode cur = root;
while (cur != null) {
if (cur.val <= target) {
ps.right = cur; // 挂到小树链尾
ps = cur;
cur = cur.right; // 保存右孩子引用
ps.right = null; // 断开,下一轮继续拼接
} else {
pl.left = cur; // 挂到大树链尾
pl = cur;
cur = cur.left;
pl.left = null;
}
}
return new TreeNode[] {small.right, large.left};
}
// ===== 本地测试 =====
private static TreeNode fromArray(Integer[] a) {
if (a == null || a.length == 0) {
return null;
}
TreeNode root = new TreeNode(a[0]);
Deque<TreeNode> q = new ArrayDeque<>();
q.offer(root);
int i = 1;
while (!q.isEmpty() && i < a.length) {
TreeNode cur = q.poll();
if (a[i] != null) {
cur.left = new TreeNode(a[i]);
q.offer(cur.left);
}
++i;
if (i < a.length && a[i] != null) {
cur.right = new TreeNode(a[i]);
q.offer(cur.right);
}
++i;
}
return root;
}
private static String trace(TreeNode root) {
if (root == null) {
return "[]";
}
StringBuilder sb = new StringBuilder("[");
java.util.List<TreeNode> q = new java.util.ArrayList<>();
q.add(root);
for (int head = 0; head < q.size(); ++head) {
TreeNode cur = q.get(head);
if (cur == null) {
sb.append("null,");
continue;
}
sb.append(cur.val).append(',');
q.add(cur.left);
q.add(cur.right);
}
String s = sb.toString();
// 去掉尾部连续 null
while (s.endsWith("null,")) {
s = s.substring(0, s.length() - 5);
}
s = s.substring(0, s.length() - 1) + "]";
return s;
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
TreeNode t1 = fromArray(new Integer[] {4, 2, 6, 1, 3, 5, 7});
TreeNode[] r1 = sol.splitBST(t1, 2);
++total;
if ("[2,1]".equals(trace(r1[0])) && "[4,3,6,null,null,5,7]".equals(trace(r1[1]))) ++pass;
else System.out.println("FAIL 776-1 got " + trace(r1[0]) + " | " + trace(r1[1]));
TreeNode t2 = fromArray(new Integer[] {1});
TreeNode[] r2 = sol.splitBST(t2, 1);
++total;
if ("[1]".equals(trace(r2[0])) && "[]".equals(trace(r2[1]))) ++pass;
else System.out.println("FAIL 776-2 got " + trace(r2[0]) + " | " + trace(r2[1]));
// 迭代法同样验证
TreeNode u1 = fromArray(new Integer[] {4, 2, 6, 1, 3, 5, 7});
TreeNode[] r3 = sol.splitBST2(u1, 2);
++total;
if ("[2,1]".equals(trace(r3[0])) && "[4,3,6,null,null,5,7]".equals(trace(r3[1]))) ++pass;
else System.out.println("FAIL 776-3 got " + trace(r3[0]) + " | " + trace(r3[1]));
TreeNode u2 = fromArray(new Integer[] {5, 3, 8, 1, 4, 7, 10, null, null, null, null, null, null, 9, 11});
TreeNode[] r4 = sol.splitBST2(u2, 6);
++total;
if ("[5,3,null,1,4]".equals(trace(r4[0])) && "[8,7,10,null,null,9,11]".equals(trace(r4[1]))) ++pass;
else System.out.println("FAIL 776-4 got " + trace(r4[0]) + " | " + trace(r4[1]));
if (pass == total) {
System.out.println("== 0776 PASS ==");
} else {
System.out.println("== 0776 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(h) | O(h) |
| 解法二 | O(h) | O(1) |
777. Swap Adjacent in LR String(在LR字符串中交换相邻字符)
题目描述
在一个由 'L'、'R' 和 'X' 三个字符组成的字符串(例如 "RXXLRXRXL")中进行移动操作。
一次移动操作指用一个 "LX" 替换一个 "XL",或者用一个 "XR" 替换一个 "RX"。
现给定起始字符串 start 和结束字符串 result,请编写代码,当且仅当存在一系列移动操作
使得 start 可以转换成 result 时,返回 true。
示例 1
- 输入:
start= "RXXLRXRXL",result= "XRLXXRRLX" - 输出:
true - 解释:通过以下步骤我们可以将
start转化为result:
RXXLRXRXL -> XRXLRXRXL -> XRLXRXRXL -> XRLXXRRXL -> XRLXXRRLX
示例 2
- 输入:
start= "X",result= "L" - 输出:
false
提示: 1 <= start.length <= 10^4start.length == result.lengthstart和result都只包含 'L'、'R' 或 'X'
解题思路
观察操作本质:L 只能向左移动(XL -> LX),R 只能向右移动(RX -> XR),X 是空位。
因此两个字符串在去掉 'X' 后,L/R 的相对顺序必须完全一致;同时每个 L 在 result 中的位置
不能比其在 start 中的位置更靠右(L 只能左移),每个 R 的位置不能更靠左(R 只能右移)。
解法一(双指针一趟扫描,主提交):用两个指针各自跳过 'X',逐一对齐非 X 字符比较。
解法二(位置数组法):先过滤 X 比较序列(必要),再收集 start 与 result 中 L/R 的下标,
逐对校验方向合法(对照实现,思路更直白)。
代码实现
import java.util.ArrayList;
import java.util.List;
class Solution {
// 解法一:双指针一趟扫描
public boolean canTransform(String start, String result) {
int n = start.length();
if (n != result.length()) {
return false;
}
int i = 0, j = 0;
while (i < n || j < n) {
while (i < n && start.charAt(i) == 'X') {
++i;
}
while (j < n && result.charAt(j) == 'X') {
++j;
}
if (i == n || j == n) {
return i == n && j == n;
}
char a = start.charAt(i);
char b = result.charAt(j);
if (a != b) {
return false;
}
if (a == 'L' && i < j) {
return false; // L 只能向左,result 中位置更靠右则不合法
}
if (a == 'R' && i > j) {
return false; // R 只能向右,result 中位置更靠左则不合法
}
++i;
++j;
}
return true;
}
// 解法二:过滤 X + 位置校验
public boolean canTransform2(String start, String result) {
if (start.length() != result.length()) {
return false;
}
// 去掉 X 后序列必须一致
if (!strip(start).equals(strip(result))) {
return false;
}
// 收集每个 L/R 在两边字符串中的下标
List<int[]> aPos = collect(start);
List<int[]> bPos = collect(result);
for (int idx = 0; idx < aPos.size(); ++idx) {
int ch = aPos.get(idx)[0];
int a = aPos.get(idx)[1];
int b = bPos.get(idx)[1];
if (ch == 'L' && a < b) {
return false; // L 只能左移
}
if (ch == 'R' && a > b) {
return false; // R 只能右移
}
}
return true;
}
private String strip(String s) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < s.length(); ++i) {
if (s.charAt(i) != 'X') {
sb.append(s.charAt(i));
}
}
return sb.toString();
}
private List<int[]> collect(String s) {
List<int[]> list = new ArrayList<>();
for (int i = 0; i < s.length(); ++i) {
char c = s.charAt(i);
if (c == 'L' || c == 'R') {
list.add(new int[] {c, i});
}
}
return list;
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if (sol.canTransform("RXXLRXRXL", "XRLXXRRLX")) ++pass; else System.out.println("FAIL 777-1");
++total; if (!sol.canTransform("X", "L")) ++pass; else System.out.println("FAIL 777-2");
++total; if (!sol.canTransform("RXXLRXRXL", "XRLXXRRLXL")) ++pass; else System.out.println("FAIL 777-3");
++total; if (sol.canTransform("XXXL", "LXXX")) ++pass; else System.out.println("FAIL 777-4");
++total; if (!sol.canTransform("LXXX", "XXXL")) ++pass; else System.out.println("FAIL 777-5");
++total; if (!sol.canTransform("XXRXLX", "XRXXLX")) ++pass; else System.out.println("FAIL 777-6");
++total; if (sol.canTransform2("RXXLRXRXL", "XRLXXRRLX")) ++pass; else System.out.println("FAIL 777-7");
++total; if (sol.canTransform2("XXXL", "LXXX")) ++pass; else System.out.println("FAIL 777-8");
++total; if (!sol.canTransform2("LXXX", "XXXL")) ++pass; else System.out.println("FAIL 777-9");
if (pass == total) {
System.out.println("== 0777 PASS ==");
} else {
System.out.println("== 0777 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n) | O(1) |
| 解法二 | O(n) | O(n) |
778. Swim in Rising Water(水位上升的泳池中游泳)
题目描述
在一个 n x n 的整数矩阵 grid 中,每一个方格的值 grid[i][j] 表示位置 (i, j) 的平台高度。
当开始下雨时,在时间为 t 时,水池中的水位为 t。你可以从一个平台游向四周相邻的任意一个
平台,但是前提是此时水位必须同时淹没这两个平台。假定你可以瞬间移动无限距离,也就是默认在
方格内部游动是不耗时的。当然,在你游泳的时候你必须待在坐标方格里面。
你从坐标方格的左上平台 (0, 0) 出发。返回你到达坐标方格的右下平台 (n-1, n-1) 所需的最少时间。
示例 1
- 输入:
grid= [[0,2],[1,3]] - 输出:3
示例 2
- 输入:
grid= [[0,1,2,3,4],[24,23,22,21,5],[12,13,14,15,16],[11,17,18,19,20],[10,9,8,7,6]] - 输出:16
提示: n == grid.lengthn == grid[i].length1 <= n <= 500 <= grid[i][j] < n^2grid[i][j]中每个值均无重复
解题思路
本题的实质是:找一条从左上到右下的路径,使路径上最大的平台高度最小,答案即该最小最大值
(t 时刻水位为 t,能走通当且仅当路径上所有平台都 <= t)。
解法一(二分答案 + BFS,主提交):对时间 t 二分,check(t) 用 BFS 验证水位为 t 时
能否从 (0,0) 出发到达 (n-1,n-1)(只走平台高度 <= t 的格子)。答案区间为
[grid[0][0], n^2-1],单调可行,二分即可。
解法二(Dijkstra / 优先队列):把每个格子视为节点,边权为相邻两格平台高度的较大值,
用优先队列按"当前路径最大高度"从小到大扩展,最早到达右下角时的最大高度即答案。
代码实现
import java.util.ArrayDeque;
import java.util.PriorityQueue;
import java.util.Deque;
class Solution {
private static final int[][] DIRS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
// 解法一:二分答案 + BFS
public int swimInWater(int[][] grid) {
int n = grid.length;
int lo = grid[0][0], hi = n * n - 1;
while (lo < hi) {
int mid = (lo + hi) >>> 1;
if (canReach(grid, mid)) {
hi = mid;
} else {
lo = mid + 1;
}
}
return lo;
}
private boolean canReach(int[][] grid, int t) {
int n = grid.length;
if (grid[0][0] > t) {
return false;
}
boolean[][] seen = new boolean[n][n];
Deque<int[]> q = new ArrayDeque<>();
q.offer(new int[] {0, 0});
seen[0][0] = true;
while (!q.isEmpty()) {
int[] cur = q.poll();
if (cur[0] == n - 1 && cur[1] == n - 1) {
return true;
}
for (int[] d : DIRS) {
int x = cur[0] + d[0];
int y = cur[1] + d[1];
if (x >= 0 && x < n && y >= 0 && y < n && !seen[x][y] && grid[x][y] <= t) {
seen[x][y] = true;
q.offer(new int[] {x, y});
}
}
}
return false;
}
// 解法二:Dijkstra(优先队列维护路径最大高度最小)
public int swimInWater2(int[][] grid) {
int n = grid.length;
boolean[][] done = new boolean[n][n];
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[2] - b[2]);
pq.offer(new int[] {0, 0, grid[0][0]});
while (!pq.isEmpty()) {
int[] cur = pq.poll();
int x = cur[0], y = cur[1], h = cur[2];
if (done[x][y]) {
continue;
}
done[x][y] = true;
if (x == n - 1 && y == n - 1) {
return h;
}
for (int[] d : DIRS) {
int nx = x + d[0];
int ny = y + d[1];
if (nx >= 0 && nx < n && ny >= 0 && ny < n && !done[nx][ny]) {
pq.offer(new int[] {nx, ny, Math.max(h, grid[nx][ny])});
}
}
}
return -1; // 不会到达
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
int[][] g1 = {{0, 2}, {1, 3}};
++total; if (sol.swimInWater(g1) == 3) ++pass; else System.out.println("FAIL 778-1 got " + sol.swimInWater(g1));
int[][] g2 = {{0, 1, 2, 3, 4}, {24, 23, 22, 21, 5}, {12, 13, 14, 15, 16}, {11, 17, 18, 19, 20}, {10, 9, 8, 7, 6}};
++total; if (sol.swimInWater(g2) == 16) ++pass; else System.out.println("FAIL 778-2 got " + sol.swimInWater(g2));
int[][] g3 = {{3, 2}, {0, 1}};
++total; if (sol.swimInWater(g3) == 3) ++pass; else System.out.println("FAIL 778-3 got " + sol.swimInWater(g3));
int[][] g4 = {{0, 2}, {1, 3}};
++total; if (sol.swimInWater2(g4) == 3) ++pass; else System.out.println("FAIL 778-4 got " + sol.swimInWater2(g4));
int[][] g5 = {{0, 1, 2, 3, 4}, {24, 23, 22, 21, 5}, {12, 13, 14, 15, 16}, {11, 17, 18, 19, 20}, {10, 9, 8, 7, 6}};
++total; if (sol.swimInWater2(g5) == 16) ++pass; else System.out.println("FAIL 778-5 got " + sol.swimInWater2(g5));
int[][] g6 = {{3, 2}, {0, 1}};
++total; if (sol.swimInWater2(g6) == 3) ++pass; else System.out.println("FAIL 778-6 got " + sol.swimInWater2(g6));
if (pass == total) {
System.out.println("== 0778 PASS ==");
} else {
System.out.println("== 0778 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n^2 log n) | O(n^2) |
| 解法二 | O(n^2 log n) | O(n^2) |
779. K-th Symbol in Grammar(第K个语法符号)
题目描述
我们构建了一个包含 n 行(索引从 1 开始)的表。首先在第一行我们写上一个 0。
接下来的每一行,将前一行中的 0 替换为 01,1 替换为 10。
例如,对于 n = 3,第 1 行是 0,第 2 行是 01,第 3 行是 0110。
给定行数 n 和序数 k,返回第 n 行中第 k 个字符(k 从索引 1 开始)。
示例 1
- 输入:
n= 1,k= 1 - 输出:0
示例 2
- 输入:
n= 2,k= 1 - 输出:0
示例 3
- 输入:
n= 2,k= 2 - 输出:1
提示: 1 <= n <= 301 <= k <= 2^(n-1)
解题思路
第 n 行的前一半与第 n-1 行完全相同,后一半是第 n-1 行每位取反。
解法一(递归倒推,主提交):若 k 在前半段,ans(n,k) = ans(n-1,k);
若 k 在后半段,ans(n,k) = 1 - ans(n-1, k - 2^(n-2)),逐层向上直到 n=1。
解法二(位运算性质):把 k-1 写成二进制,第 n 行第 k 个字符等于 popcount(k-1) % 2。
原因:第 n 行的第 k 位由起点 0 经过 n-1 次"翻倍取反"得到,k 每进入后半段翻转一次,
翻转次数恰为 k-1 中 1 的个数。
代码实现
class Solution {
// 解法一:递归倒推
public int kthGrammar(int n, int k) {
if (n == 1) {
return 0;
}
int half = 1 << (n - 2); // 前一半长度 2^(n-2)
if (k <= half) {
return kthGrammar(n - 1, k);
}
return 1 - kthGrammar(n - 1, k - half);
}
// 解法二:位运算(popcount 奇偶性)
public int kthGrammar2(int n, int k) {
return Integer.bitCount(k - 1) & 1;
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if (sol.kthGrammar(1, 1) == 0) ++pass; else System.out.println("FAIL 779-1");
++total; if (sol.kthGrammar(2, 1) == 0) ++pass; else System.out.println("FAIL 779-2");
++total; if (sol.kthGrammar(2, 2) == 1) ++pass; else System.out.println("FAIL 779-3");
++total; if (sol.kthGrammar(3, 3) == 1) ++pass; else System.out.println("FAIL 779-4");
++total; if (sol.kthGrammar(3, 4) == 0) ++pass; else System.out.println("FAIL 779-5");
++total; if (sol.kthGrammar(4, 5) == 1) ++pass; else System.out.println("FAIL 779-6");
++total; if (sol.kthGrammar(30, 1 << 29) == 1) ++pass; else System.out.println("FAIL 779-7");
++total; if (sol.kthGrammar2(1, 1) == 0) ++pass; else System.out.println("FAIL 779-8");
++total; if (sol.kthGrammar2(2, 2) == 1) ++pass; else System.out.println("FAIL 779-9");
++total; if (sol.kthGrammar2(3, 4) == 0) ++pass; else System.out.println("FAIL 779-10");
++total; if (sol.kthGrammar2(4, 5) == 1) ++pass; else System.out.println("FAIL 779-11");
if (pass == total) {
System.out.println("== 0779 PASS ==");
} else {
System.out.println("== 0779 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n) | O(n) |
| 解法二 | O(log k) | O(1) |
780. Reaching Points(到达终点)
题目描述
给定四个整数 sx、sy、tx 和 ty,如果通过一系列的转换可以从起点 (sx, sy) 到达
终点 (tx, ty),则返回 true,否则返回 false。
从点 (x, y) 可以转换到 (x, x+y) 或者 (x+y, y)。
示例 1
- 输入:sx = 1, sy = 1, tx = 3, ty = 5
- 输出:
true - 解释:可以通过以下一系列转换从起点转换到终点:
(1, 1) -> (1, 2) -> (3, 2) -> (3, 5)
示例 2
- 输入:sx = 1, sy = 1, tx = 2, ty = 2
- 输出:
false
示例 3
- 输入:sx = 1, sy = 1, tx = 1, ty = 1
- 输出:
true
提示: 1 <= sx, sy, tx, ty <= 10^9
解题思路
正推(加法)是爆炸式的,需反向(减法/取模)思考:(tx, ty) 的上一状态只可能是
(tx - ty, ty)(若 tx > ty)或 (tx, ty - tx)(若 ty > tx)——即把较大坐标减去较小坐标。
解法一(递归 + 取模加速,主提交):当 tx > ty 时反复令 tx -= ty,若 ty == sy 则可直接用
取模一次算到底:(tx - sx) % ty == 0 即说明终可通过若干次减法到达;ty > tx 对称处理。
解法二(朴素反向减法):每步把较大的坐标减去较小的坐标,直到两边都 <= 起点,再判断是否相等。
正确但大数据下较慢,适合作为对照理解。
代码实现
class Solution {
// 解法一:反向 + 取模加速
public boolean reachingPoints(int sx, int sy, int tx, int ty) {
while (tx >= sx && ty >= sy) {
if (tx == sx && ty == sy) {
return true;
}
if (tx > ty) {
if (ty == sy) {
// 只剩横向减法,一次算到底
return (tx - sx) % ty == 0;
}
tx %= ty;
} else {
if (tx == sx) {
return (ty - sy) % tx == 0;
}
ty %= tx;
}
}
return false;
}
// 解法二:朴素反向减法(对照)
public boolean reachingPoints2(int sx, int sy, int tx, int ty) {
while (tx > sx || ty > sy) {
if (tx > ty) {
tx -= ty;
} else {
ty -= tx;
}
if (tx < sx || ty < sy) {
return false;
}
if (tx <= 0 || ty <= 0) {
return false;
}
}
return tx == sx && ty == sy;
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if (sol.reachingPoints(1, 1, 3, 5)) ++pass; else System.out.println("FAIL 780-1");
++total; if (!sol.reachingPoints(1, 1, 2, 2)) ++pass; else System.out.println("FAIL 780-2");
++total; if (sol.reachingPoints(1, 1, 1, 1)) ++pass; else System.out.println("FAIL 780-3");
++total; if (!sol.reachingPoints(1, 2, 2, 2)) ++pass; else System.out.println("FAIL 780-4");
++total; if (!sol.reachingPoints(2, 3, 4, 3)) ++pass; else System.out.println("FAIL 780-5");
++total; if (sol.reachingPoints(3, 3, 3, 3)) ++pass; else System.out.println("FAIL 780-6");
++total; if (sol.reachingPoints(1, 1, 1000000000, 1)) ++pass; else System.out.println("FAIL 780-7");
++total; if (!sol.reachingPoints2(1, 1, 2, 2)) ++pass; else System.out.println("FAIL 780-8");
++total; if (!sol.reachingPoints2(2, 3, 4, 3)) ++pass; else System.out.println("FAIL 780-9");
++total; if (sol.reachingPoints2(1, 1, 10, 9)) ++pass; else System.out.println("FAIL 780-10");
++total; if (!sol.reachingPoints2(1, 2, 2, 1)) ++pass; else System.out.println("FAIL 780-11");
if (pass == total) {
System.out.println("== 0780 PASS ==");
} else {
System.out.println("== 0780 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(log max(tx, ty)) | O(1) |
| 解法二 | O(max(tx, ty) / min) | O(1) |
781. Rabbits in Forest(森林中的兔子)
题目描述
森林中有未知数量的兔子。提问其中若干只兔子 "还有多少只其它兔子与你(指被提问的兔子)颜色相同?"
将答案收集到一个整数数组 answers 中,其中 answers[i] 是第 i 只兔子的回答。
给你数组 answers,返回森林中兔子的最少数量。
示例 1
- 输入:answers = [1,1,2]
- 输出:5
- 解释:两只回答了 "1" 的兔子可能有相同的颜色,设为红色。
之后回答了 "2" 的兔子不会是红色,否则他们的回答会相互矛盾。
设回答了 "2" 的兔子为蓝色。此外,森林中还应有另外 2 只蓝色兔子的回答没有包含在数组中。
因此森林中兔子的最少数量是 5 只:3 只回答的和 2 只没有回答的。
示例 2
- 输入:answers = [10,10,10]
- 输出:11
提示: 1 <= answers.length <= 10000 <= answers[i] < 1000
解题思路
若一只兔子回答 x,说明该颜色兔子总数至多为 x + 1 只(另有 x 只同类)。
回答同为 x 的兔子最多只有 x + 1 只能归为同一颜色(超过则必须另起一组)。
解法一(哈希表贪心计数,主提交):用 map 记录每个 x 已归入组中的兔子数(累计),
遇新兔子时若 x + 1 已满则开新组并把总数加上。更简单的方式:统计每个答案的出现次数
cnt[x],该答案需要 ceil(cnt[x] / (x+1)) 组,每组 x+1 只。
解法二(频率数组累加):因为 0 <= answers[i] < 1000,用数组计数后按组数公式求和。
代码实现
import java.util.HashMap;
import java.util.Map;
class Solution {
// 解法一:哈希表计数 + 组数公式
public int numRabbits(int[] answers) {
Map<Integer, Integer> cnt = new HashMap<>();
for (int a : answers) {
cnt.put(a, cnt.getOrDefault(a, 0) + 1);
}
int ans = 0;
for (Map.Entry<Integer, Integer> e : cnt.entrySet()) {
int x = e.getKey();
int c = e.getValue();
int groupSize = x + 1; // 每组最多 x+1 只
int groups = (c + groupSize - 1) / groupSize; // 向上取整
ans += groups * groupSize;
}
return ans;
}
// 解法二:频率数组
public int numRabbits2(int[] answers) {
int[] cnt = new int[1000];
for (int a : answers) {
cnt[a]++;
}
int ans = 0;
for (int x = 0; x < 1000; ++x) {
if (cnt[x] == 0) {
continue;
}
int groupSize = x + 1;
int groups = (cnt[x] + groupSize - 1) / groupSize;
ans += groups * groupSize;
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if (sol.numRabbits(new int[] {1, 1, 2}) == 5) ++pass; else System.out.println("FAIL 781-1");
++total; if (sol.numRabbits(new int[] {10, 10, 10}) == 11) ++pass; else System.out.println("FAIL 781-2");
++total; if (sol.numRabbits(new int[] {0, 0, 1, 1, 1}) == 6) ++pass; else System.out.println("FAIL 781-3");
++total; if (sol.numRabbits(new int[] {0}) == 1) ++pass; else System.out.println("FAIL 781-4");
++total; if (sol.numRabbits(new int[] {2, 2, 2, 2, 2}) == 6) ++pass; else System.out.println("FAIL 781-5");
++total; if (sol.numRabbits2(new int[] {1, 1, 2}) == 5) ++pass; else System.out.println("FAIL 781-6");
++total; if (sol.numRabbits2(new int[] {10, 10, 10}) == 11) ++pass; else System.out.println("FAIL 781-7");
++total; if (sol.numRabbits2(new int[] {2, 2, 2, 2, 2}) == 6) ++pass; else System.out.println("FAIL 781-8");
++total; if (sol.numRabbits2(new int[] {0, 0, 1, 1, 1}) == 6) ++pass; else System.out.println("FAIL 781-9");
if (pass == total) {
System.out.println("== 0781 PASS ==");
} else {
System.out.println("== 0781 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n) | O(n) |
| 解法二 | O(n + 1000) | O(1000) |
782. Transform to Chessboard(变为棋盘)
题目描述
一个 n x n 的二维网络 board 仅由 0 和 1 组成。每次移动,你能交换任意两列或是两行的位置。
返回将这个矩阵变为 "棋盘" 所需的最小移动次数。如果不存在可行的变换,输出 -1。
"棋盘" 是指任意一格的上下左右四个方向的值均与本身不同的矩阵。
示例 1
- 输入:
board= [[0,1,1,0],[0,1,1,0],[1,0,0,1],[1,0,0,1]] - 输出:2
- 解释:一种可行的变换方式如下,从左到右:
第一次移动交换了第一列和第二列。第二次移动交换了第二行和第三行。
示例 2
- 输入:
board= [[0,1],[1,0]] - 输出:0
示例 3
- 输入:
board= [[1,0],[1,0]] - 输出:-1
提示: n == board.lengthn == board[i].length2 <= n <= 30board[i][j]将只包含 0 或 1
解题思路
要能变成棋盘,矩阵必须满足两个充分必要条件:
- 所有行要么与第 0 行完全相同,要么与第 0 行完全相反(这样才能只需交换列得到棋盘);
- 第 0 行中 0 与 1 的个数差不超过 1(n 为偶数时相等,n 为奇数时相差 1),
且第 0 列也满足同样的条件(行、列角色对称)。
之后把行/列分别各自重排:目标列交错模式有两种(以 0 开头或以 1 开头),统计当前序列与
目标模式的失配次数,交换次数 = 失配数 / 2,取两种模式中的较小值。行与列的结果相加。
解法一(如上,主提交):先校验再分别统计行、列的最小交换次数。
解法二(对照暴力):小规模 n(<=8)直接 BFS 状态搜索验证,作为正确性对照。
代码实现
import java.util.HashMap;
import java.util.Map;
class Solution {
// 解法一:棋盘性质 + 分别统计行/列交换
public int movesToChessboard(int[][] board) {
int n = board.length;
// 1. 校验:每一行必须等于第 0 行或其反相
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
if (board[i][j] != (board[0][j] ^ board[i][0] ^ board[0][0])) {
return -1;
}
}
}
// 2. 无论怎么交换行/列,每行每列中 0、1 的个数必须满足棋盘约束
int rowOne = 0, colOne = 0;
for (int i = 0; i < n; ++i) {
rowOne += board[0][i]; // 第 0 行的 1 个数(所有行都一样)
colOne += board[i][0]; // 第 0 列的 1 个数
}
if (Math.abs(rowOne * 2 - n) > 1 || Math.abs(colOne * 2 - n) > 1) {
return -1;
}
// 3. 计算使第 0 行变为标准棋盘序列所需的最小交换次数
int rowSwaps = minSwaps(board[0], n);
// 4. 计算使第 0 列变为标准棋盘序列所需的最小交换次数
int[] col = new int[n];
for (int i = 0; i < n; ++i) {
col[i] = board[i][0];
}
int colSwaps = minSwaps(col, n);
return rowSwaps + colSwaps;
}
// 将长度为 n 的 0/1 序列变成 0101... 或 1010... 的最小交换次数(一次交换两列/两行)
private int minSwaps(int[] seq, int n) {
int costStart0 = 0; // 目标 0,1,0,1...
int costStart1 = 0; // 目标 1,0,1,0...
for (int i = 0; i < n; ++i) {
if (seq[i] != (i & 1)) {
costStart0++;
}
if (seq[i] != (1 - (i & 1))) {
costStart1++;
}
}
if ((n & 1) == 0) {
return Math.min(costStart0, costStart1) / 2;
}
// n 为奇数时,只能选 1 的个数更少的模式(奇数长度下 1 多的那个必然无法一次抵对)
return (costStart0 & 1) == 0 ? costStart0 / 2 : costStart1 / 2;
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
int[][] b1 = {{0, 1, 1, 0}, {0, 1, 1, 0}, {1, 0, 0, 1}, {1, 0, 0, 1}};
++total; if (sol.movesToChessboard(b1) == 2) ++pass; else System.out.println("FAIL 782-1 got " + sol.movesToChessboard(b1));
int[][] b2 = {{0, 1}, {1, 0}};
++total; if (sol.movesToChessboard(b2) == 0) ++pass; else System.out.println("FAIL 782-2 got " + sol.movesToChessboard(b2));
int[][] b3 = {{1, 0}, {1, 0}};
++total; if (sol.movesToChessboard(b3) == -1) ++pass; else System.out.println("FAIL 782-3 got " + sol.movesToChessboard(b3));
int[][] b4 = {{0, 1, 0}, {1, 0, 1}, {0, 1, 0}};
++total; if (sol.movesToChessboard(b4) == 0) ++pass; else System.out.println("FAIL 782-4 got " + sol.movesToChessboard(b4));
int[][] b5 = {{0, 0, 1}, {1, 1, 0}, {1, 1, 0}};
++total; if (sol.movesToChessboard(b5) == 2) ++pass; else System.out.println("FAIL 782-5 got " + sol.movesToChessboard(b5));
int[][] b6 = {{0, 1, 0, 0}, {1, 0, 1, 1}, {0, 1, 0, 0}, {1, 0, 1, 1}};
++total; if (sol.movesToChessboard(b6) == -1) ++pass; else System.out.println("FAIL 782-6 got " + sol.movesToChessboard(b6));
if (pass == total) {
System.out.println("== 0782 PASS ==");
} else {
System.out.println("== 0782 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n^2) | O(n) |
| 解法二 | O(n! * n^2)(仅小规模对照) | O(n!) |
783. Minimum Distance Between BST Nodes(二叉搜索树节点最小距离)
题目描述
给你一个二叉搜索树的根节点 root,返回树中任意两不同节点值之间的最小差值。
差值是一个正数,其数值等于两值之差的绝对值。
示例 1
- 输入:
root= [4,2,6,1,3] - 输出:1
示例 2
- 输入:
root= [1,0,48,null,null,12,49] - 输出:1
提示: - 树中节点的数目范围是 [2, 100]
0 <= Node.val <= 10^5- 注意:本题与 530 相同
解题思路
二叉搜索树中序遍历得到严格递增序列,最小差值一定出现在相邻两个元素之间。
解法一(中序遍历 + 维护前驱,主提交):递归中序遍历,记录上一个访问值,用当前值减
前驱更新最小差。
解法二(迭代中序遍历):用显式栈模拟中序遍历,逻辑等价、空间 O(h)。
代码实现
import java.util.ArrayDeque;
import java.util.Deque;
class Solution {
private static class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int val) { this.val = val; }
}
private int prev;
private int best;
// 解法一:递归中序遍历
public int minDiffInBST(TreeNode root) {
prev = -1;
best = Integer.MAX_VALUE;
dfs(root);
return best;
}
private void dfs(TreeNode node) {
if (node == null) {
return;
}
dfs(node.left);
if (prev >= 0) {
best = Math.min(best, node.val - prev);
}
prev = node.val;
dfs(node.right);
}
// 解法二:迭代中序遍历
public int minDiffInBST2(TreeNode root) {
int prevVal = -1;
int bestVal = Integer.MAX_VALUE;
Deque<TreeNode> stack = new ArrayDeque<>();
TreeNode cur = root;
while (cur != null || !stack.isEmpty()) {
while (cur != null) {
stack.push(cur);
cur = cur.left;
}
cur = stack.pop();
if (prevVal >= 0) {
bestVal = Math.min(bestVal, cur.val - prevVal);
}
prevVal = cur.val;
cur = cur.right;
}
return bestVal;
}
// ===== 本地测试 =====
private static TreeNode fromArray(Integer[] a) {
if (a == null || a.length == 0) {
return null;
}
TreeNode root = new TreeNode(a[0]);
Deque<TreeNode> q = new ArrayDeque<>();
q.offer(root);
int i = 1;
while (!q.isEmpty() && i < a.length) {
TreeNode cur = q.poll();
if (a[i] != null) {
cur.left = new TreeNode(a[i]);
q.offer(cur.left);
}
++i;
if (i < a.length && a[i] != null) {
cur.right = new TreeNode(a[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 = fromArray(new Integer[] {4, 2, 6, 1, 3});
++total; if (sol.minDiffInBST(t1) == 1) ++pass; else System.out.println("FAIL 783-1");
TreeNode t2 = fromArray(new Integer[] {1, 0, 48, null, null, 12, 49});
++total; if (sol.minDiffInBST(t2) == 1) ++pass; else System.out.println("FAIL 783-2");
TreeNode t3 = fromArray(new Integer[] {90, 69, null, 49, 89, null, 52});
++total; if (sol.minDiffInBST(t3) == 1) ++pass; else System.out.println("FAIL 783-3");
TreeNode u1 = fromArray(new Integer[] {4, 2, 6, 1, 3});
++total; if (sol.minDiffInBST2(u1) == 1) ++pass; else System.out.println("FAIL 783-4");
TreeNode u2 = fromArray(new Integer[] {1, 0, 48, null, null, 12, 49});
++total; if (sol.minDiffInBST2(u2) == 1) ++pass; else System.out.println("FAIL 783-5");
TreeNode u3 = fromArray(new Integer[] {56, 42, 71, 28, 49, 63, 84, null, null, null, null, 60, 66});
++total; if (sol.minDiffInBST2(u3) == 3) ++pass; else System.out.println("FAIL 783-6");
if (pass == total) {
System.out.println("== 0783 PASS ==");
} else {
System.out.println("== 0783 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n) | O(h) |
| 解法二 | O(n) | O(h) |
784. Letter Case Permutation(字母大小写全排列)
题目描述
给定一个字符串 s,通过将字符串 s 中的每个字母转变大小写,我们可以获得一个新的字符串。
返回所有可能得到的字符串集合。以任意顺序返回输出。
示例 1
- 输入:
s= "a1b2" - 输出:["a1b2", "
a1B2", "A1b2", "A1B2"]
示例 2
- 输入:
s= "3z4" - 输出:["3z4","3Z4"]
提示: 1 <= s.length <= 12s由小写英文字母、大写英文字母和数字组成
解题思路
每个字母有两种形态(原样或切换大小写),数字只有一种,总组合数 <= 2^12。
解法一(回溯 DFS,主提交):从左到右处理,遇字母分两支(小写/大写),数字直接追加深搜。
解法二(位掩码枚举):先收集所有字母的位置,共有 m 个字母,枚举 2^m 种掩码,
掩码第 i 位为 1 表示第 i 个字母用大写,为 0 用小写。
代码实现
import java.util.ArrayList;
import java.util.List;
class Solution {
private List<String> ans;
private char[] chars;
// 解法一:回溯 DFS
public List<String> letterCasePermutation(String s) {
ans = new ArrayList<>();
chars = s.toCharArray();
dfs(0);
return ans;
}
private void dfs(int idx) {
if (idx == chars.length) {
ans.add(new String(chars));
return;
}
char c = chars[idx];
if (Character.isLetter(c)) {
chars[idx] = Character.toLowerCase(c);
dfs(idx + 1);
chars[idx] = Character.toUpperCase(c);
dfs(idx + 1);
chars[idx] = c; // 回溯还原
} else {
dfs(idx + 1);
}
}
// 解法二:位掩码枚举
public List<String> letterCasePermutation2(String s) {
List<String> res = new ArrayList<>();
List<Integer> pos = new ArrayList<>();
StringBuilder base = new StringBuilder();
for (int i = 0; i < s.length(); ++i) {
char c = s.charAt(i);
if (Character.isLetter(c)) {
pos.add(i);
base.append(Character.toLowerCase(c));
} else {
base.append(c);
}
}
int m = pos.size();
for (int mask = 0; mask < (1 << m); ++mask) {
char[] arr = base.toString().toCharArray();
for (int b = 0; b < m; ++b) {
if ((mask & (1 << b)) != 0) {
arr[pos.get(b)] = Character.toUpperCase(arr[pos.get(b)]);
}
}
res.add(new String(arr));
}
return res;
}
private static boolean sameSet(List<String> a, List<String> b) {
if (a.size() != b.size()) {
return false;
}
java.util.Set<String> set = new java.util.HashSet<>(a);
for (String s : b) {
if (!set.contains(s)) {
return false;
}
}
return true;
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
List<String> r1 = sol.letterCasePermutation("a1b2");
++total; if (r1.size() == 4 && r1.contains("a1b2") && r1.contains("a1B2") && r1.contains("A1b2") && r1.contains("A1B2"))
++pass; else System.out.println("FAIL 784-1 " + r1);
List<String> r2 = sol.letterCasePermutation("3z4");
++total; if (r2.size() == 2 && r2.contains("3z4") && r2.contains("3Z4")) ++pass; else System.out.println("FAIL 784-2 " + r2);
List<String> r3 = sol.letterCasePermutation("12345");
++total; if (r3.size() == 1 && r3.get(0).equals("12345")) ++pass; else System.out.println("FAIL 784-3");
List<String> r4 = sol.letterCasePermutation2("a1b2");
++total; if (sameSet(sol.letterCasePermutation2("a1b2"), sol.letterCasePermutation("a1b2"))) ++pass; else System.out.println("FAIL 784-4");
++total; if (sameSet(sol.letterCasePermutation2("3z4"), sol.letterCasePermutation("3z4"))) ++pass; else System.out.println("FAIL 784-5");
++total; if (r4.size() == 4) ++pass; else System.out.println("FAIL 784-6");
List<String> r5 = sol.letterCasePermutation2("C");
++total; if (r5.size() == 2 && r5.contains("C") && r5.contains("c")) ++pass; else System.out.println("FAIL 784-7 " + r5);
if (pass == total) {
System.out.println("== 0784 PASS ==");
} else {
System.out.println("== 0784 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(2^m * n) | O(n)(递归深度)+ O(2^m * n)(答案) |
| 解法二 | O(2^m * n) | O(m) + O(2^m * n)(答案) |
785. Is Graph Bipartite(判断二分图)
题目描述
存在一个无向图,图中有 n 个节点。其中每个节点都有一个介于 0 到 n-1 之间的唯一编号。
给你一个二维数组 graph,其中 graph[u] 是一个节点数组,由节点 u 的邻接节点组成。
该无向图同时具有以下属性:
- 不存在自环,不存在平行边
- 如果
v在graph[u]内,那么u也应该在graph[v]内(该图是无向图) - 这个图可能不是连通图
二分图定义:如果能将一个图的节点集合分割成两个独立的子集 A 和 B,并使图中的每一条边
的两个节点一个来自 A 集合,一个来自 B 集合,就将这个图称为二分图。
如果图是二分图,返回true;否则,返回false。
示例 1
- 输入:
graph= [[1,2,3],[0,2],[0,1,3],[0,2]] - 输出:
false
示例 2
- 输入:
graph= [[1,3],[0,2],[1,3],[0,2]] - 输出:
true
提示: graph.length == n1 <= n <= 100
解题思路
染色法:给每个节点染色(0/1 两色),相邻节点必须异色;若出现同色相邻则不是二分图。
图可能不连通,需要对每个未染色节点启动一次遍历。
解法一(BFS 染色,主提交):队列逐层染色扩散。
解法二(DFS 染色 / 并查集):DFS 递归染色等价;并查集则把每个节点的所有邻居两两合并,
若某邻居与节点本身同属一个集合则矛盾。
代码实现
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Arrays;
class Solution {
// 解法一:BFS 染色
public boolean isBipartite(int[][] graph) {
int n = graph.length;
int[] color = new int[n]; // 0 未染色,1/-1 两种颜色
for (int i = 0; i < n; ++i) {
if (color[i] != 0) {
continue;
}
color[i] = 1;
Deque<Integer> q = new ArrayDeque<>();
q.offer(i);
while (!q.isEmpty()) {
int cur = q.poll();
for (int nb : graph[cur]) {
if (color[nb] == 0) {
color[nb] = -color[cur];
q.offer(nb);
} else if (color[nb] == color[cur]) {
return false;
}
}
}
}
return true;
}
// 解法二:DFS 染色
public boolean isBipartite2(int[][] graph) {
int n = graph.length;
int[] color = new int[n];
for (int i = 0; i < n; ++i) {
if (color[i] == 0 && !dfs(i, 1, color, graph)) {
return false;
}
}
return true;
}
private boolean dfs(int u, int c, int[] color, int[][] graph) {
if (color[u] != 0) {
return color[u] == c;
}
color[u] = c;
for (int v : graph[u]) {
if (!dfs(v, -c, color, graph)) {
return false;
}
}
return true;
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
int[][] g1 = {{1, 2, 3}, {0, 2}, {0, 1, 3}, {0, 2}};
++total; if (!sol.isBipartite(g1)) ++pass; else System.out.println("FAIL 785-1");
int[][] g2 = {{1, 3}, {0, 2}, {1, 3}, {0, 2}};
++total; if (sol.isBipartite(g2)) ++pass; else System.out.println("FAIL 785-2");
int[][] g3 = {{}, {2, 4, 6}, {1, 4, 8, 9}, {7, 8}, {1, 2, 8, 9}, {6, 9}, {1, 5, 7, 8, 9}, {3, 6, 9}, {2, 3, 4, 6, 9}, {2, 4, 5, 6, 7, 8}};
++total; if (!sol.isBipartite(g3)) ++pass; else System.out.println("FAIL 785-3");
int[][] g4 = {{}, {3}, {}, {1}, {}};
++total; if (sol.isBipartite(g4)) ++pass; else System.out.println("FAIL 785-4");
++total; if (!sol.isBipartite2(g1)) ++pass; else System.out.println("FAIL 785-5");
++total; if (sol.isBipartite2(g2)) ++pass; else System.out.println("FAIL 785-6");
++total; if (!sol.isBipartite2(g3)) ++pass; else System.out.println("FAIL 785-7");
++total; if (sol.isBipartite2(g4)) ++pass; else System.out.println("FAIL 785-8");
if (pass == total) {
System.out.println("== 0785 PASS ==");
} else {
System.out.println("== 0785 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n + e) | O(n) |
| 解法二 | O(n + e) / O(n alpha) | O(n) |
786. K-th Smallest Prime Fraction(第K个最小的质数分数)
题目描述
给你一个按递增顺序排序的数组 arr 和一个整数 k。数组 arr 由 1 和若干质数组成,
且其中所有整数互不相同。
对于每对满足 0 <= i < j < arr.length 的 i 和 j,可以得到分数 arr[i] / arr[j]。
那么第 k 个最小的分数是多少呢?以长度为 2 的整数数组返回你的答案,这里
answer[0] == arr[i] 且 answer[1] == arr[j]。
示例 1
- 输入:
arr= [1,2,3,5],k= 3 - 输出:[2,5]
- 解释:已构造好的分数,排序后如下所示:
1/5, 1/3, 2/5, 1/2, 3/5, 2/3
很明显第三个最小的分数是 2/5
示例 2
- 输入:
arr= [1,7],k= 1 - 输出:[1,7]
提示: 2 <= arr.length <= 10001 <= arr[i] <= 3 * 10^4arr[0] == 1arr[i]是一个质数,i> 0- 1 <=
k<=arr.length* (arr.length- 1) / 2 - 进阶:设计并实现时间复杂度小于
O(n^2)的算法解决此问题
解题思路
所有分数可组织成「每行 j 固定、i 增大则分数增大」的阶梯结构:对每个分母 arr[j],
分子 arr[i](i < j)越小分数越小,即每一列是单调递增序列。
解法一(优先队列归并,主提交):把每个分母 j 对应的最小分数 arr[0]/arr[j] 入堆,
每次弹出最小的,若该分母还能取下一个分子(i+1 < j),把 arr[i+1]/arr[j] 入堆。
弹出 k 次即得答案。时间 O(k log n),n <= 1000,满足进阶要求。
解法二(全量枚举排序):n <= 1000,最多约 5e5 个分数,全部枚举后按分数值排序取第 k 个
(对照实现,思路最直白,O(n^2 log n) 在本规模下可行)。
代码实现
import java.util.ArrayList;
import java.util.List;
import java.util.PriorityQueue;
class Solution {
// 解法一:优先队列归并
public int[] kthSmallestPrimeFraction(int[] arr, int k) {
int n = arr.length;
// 堆元素:[arr[i], arr[j], i, j]
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) ->
Long.compare((long) a[0] * b[1], (long) b[0] * a[1]));
for (int j = 1; j < n; ++j) {
pq.offer(new int[] {arr[0], arr[j], 0, j});
}
for (int t = 1; t < k; ++t) {
int[] cur = pq.poll();
int i = cur[2];
if (i + 1 < cur[3]) {
pq.offer(new int[] {arr[i + 1], arr[cur[3]], i + 1, cur[3]});
}
}
int[] top = pq.peek();
return new int[] {top[0], top[1]};
}
// 解法二:全量枚举排序
public int[] kthSmallestPrimeFraction2(int[] arr, int k) {
int n = arr.length;
List<int[]> list = new java.util.ArrayList<>();
for (int i = 0; i < n; ++i) {
for (int j = i + 1; j < n; ++j) {
list.add(new int[] {arr[i], arr[j]});
}
}
java.util.Collections.sort(list, (a, b) ->
Long.compare((long) a[0] * b[1], (long) b[0] * a[1]));
return list.get(k - 1);
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
int[] r1 = sol.kthSmallestPrimeFraction(new int[] {1, 2, 3, 5}, 3);
++total; if (r1[0] == 2 && r1[1] == 5) ++pass; else System.out.println("FAIL 786-1 " + r1[0] + "/" + r1[1]);
int[] r2 = sol.kthSmallestPrimeFraction(new int[] {1, 7}, 1);
++total; if (r2[0] == 1 && r2[1] == 7) ++pass; else System.out.println("FAIL 786-2 " + r2[0] + "/" + r2[1]);
int[] r3 = sol.kthSmallestPrimeFraction(new int[] {1, 2, 3, 5}, 6);
++total; if (r3[0] == 2 && r3[1] == 3) ++pass; else System.out.println("FAIL 786-3 " + r3[0] + "/" + r3[1]);
int[] r4 = sol.kthSmallestPrimeFraction(new int[] {1, 2, 3, 5}, 1);
++total; if (r4[0] == 1 && r4[1] == 5) ++pass; else System.out.println("FAIL 786-4 " + r4[0] + "/" + r4[1]);
int[] s1 = sol.kthSmallestPrimeFraction2(new int[] {1, 2, 3, 5}, 3);
++total; if (s1[0] == 2 && s1[1] == 5) ++pass; else System.out.println("FAIL 786-5 " + s1[0] + "/" + s1[1]);
int[] s2 = sol.kthSmallestPrimeFraction2(new int[] {1, 2, 3, 5}, 6);
++total; if (s2[0] == 2 && s2[1] == 3) ++pass; else System.out.println("FAIL 786-6 " + s2[0] + "/" + s2[1]);
int[] s3 = sol.kthSmallestPrimeFraction2(new int[] {1, 7}, 1);
++total; if (s3[0] == 1 && s3[1] == 7) ++pass; else System.out.println("FAIL 786-7 " + s3[0] + "/" + s3[1]);
if (pass == total) {
System.out.println("== 0786 PASS ==");
} else {
System.out.println("== 0786 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(k log n) | O(n) |
| 解法二 | O(n log(max/min)) | O(1) |
787. Cheapest Flights Within K Stops(K站中转内最便宜的航班)
题目描述
有 n 个城市通过一些航班连接。给你一个数组 flights,其中 flights[i] = [from_i, to_i, price_i],
表示该航班都从城市 from_i 开始,以价格 price_i 抵达 to_i。
现在给定所有的城市和航班,以及出发城市 src 和目的地 dst,你的任务是找到出一条
最多经过 k 站中转的路线,使得从 src 到 dst 的价格最便宜,并返回该价格。
如果不存在这样的路线,则输出 -1。
示例 1
- 输入:
n= 4, flights = [[0,1,100],[1,2,100],[2,0,100],[1,3,600],[2,3,200]],src= 0,dst= 3,k= 1 - 输出:700
示例 2
- 输入:
n= 3, flights = [[0,1,100],[1,2,100],[0,2,500]],src= 0,dst= 2,k= 1 - 输出:200
示例 3
- 输入:
n= 3, flights = [[0,1,100],[1,2,100],[0,2,500]],src= 0,dst= 2,k= 0 - 输出:500 提示:
2 <= n <= 1000 <= src, dst, k < nsrc != dst1 <= price_i <= 10^4
解题思路
带「最多 k 站中转」限制的最短路,不能用普通 Dijkstra 的永久标记,因为绕路也可能有
更少的边数(中转数)以达到更优的后继状态。
解法一(Bellman-Ford 分层松弛,主提交):dp[t][v] 表示经过至多 t 次飞行到达 v 的
最小花费。外层循环 n 次,每次用上一层的 dp 数组对每条边松弛,最终取 dp[k+1][dst]。
解法二(BFS 分层 DP):按站数逐层扩展,记录每层到达各站的最小花费,层数 <= k+1。
也可视为 SPFA 的思路;这里实现为「滚动数组 + 按层松弛」。
代码实现
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.PriorityQueue;
class Solution {
// 解法一:Bellman-Ford 分层松弛(滚动数组)
public int findCheapestPrice(int n, int[][] flights, int src, int dst, int k) {
int INF = Integer.MAX_VALUE / 2;
int[] dp = new int[n];
Arrays.fill(dp, INF);
dp[src] = 0;
// 最多 k+1 次飞行(k 站中转 = k+1 条边)
for (int step = 0; step <= k; ++step) {
int[] next = Arrays.copyOf(dp, n);
for (int[] f : flights) {
int u = f[0], v = f[1], w = f[2];
if (dp[u] + w < next[v]) {
next[v] = dp[u] + w;
}
}
dp = next;
}
return dp[dst] >= INF ? -1 : dp[dst];
}
// 解法二:按层扩展(显式队列,逐层松弛)
public int findCheapestPrice2(int n, int[][] flights, int src, int dst, int k) {
List<int[]>[] g = buildGraph(n, flights);
int INF = Integer.MAX_VALUE / 2;
int[] best = new int[n];
Arrays.fill(best, INF);
best[src] = 0;
List<Integer> layer = new ArrayList<>();
layer.add(src);
int stops = 0;
while (!layer.isEmpty() && stops <= k) {
List<Integer> nextLayer = new ArrayList<>();
int[] layerBest = new int[n];
Arrays.fill(layerBest, INF);
for (int u : layer) {
for (int[] e : g[u]) {
int v = e[0], w = e[1];
if (best[u] + w < layerBest[v]) {
if (layerBest[v] == INF) {
nextLayer.add(v);
}
layerBest[v] = best[u] + w;
}
}
}
for (int v : nextLayer) {
best[v] = Math.min(best[v], layerBest[v]);
}
layer = nextLayer;
++stops;
}
return best[dst] >= INF ? -1 : best[dst];
}
private List<int[]>[] buildGraph(int n, int[][] flights) {
List<int[]>[] g = new List[n];
for (int i = 0; i < n; ++i) {
g[i] = new ArrayList<>();
}
for (int[] f : flights) {
g[f[0]].add(new int[] {f[1], f[2]});
}
return g;
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
int[][] f1 = {{0, 1, 100}, {1, 2, 100}, {2, 0, 100}, {1, 3, 600}, {2, 3, 200}};
++total; if (sol.findCheapestPrice(4, f1, 0, 3, 1) == 700) ++pass; else System.out.println("FAIL 787-1");
int[][] f2 = {{0, 1, 100}, {1, 2, 100}, {0, 2, 500}};
++total; if (sol.findCheapestPrice(3, f2, 0, 2, 1) == 200) ++pass; else System.out.println("FAIL 787-2");
++total; if (sol.findCheapestPrice(3, f2, 0, 2, 0) == 500) ++pass; else System.out.println("FAIL 787-3");
int[][] f3 = {{0, 1, 10}, {1, 2, 10}, {2, 3, 10}, {0, 3, 100}};
++total; if (sol.findCheapestPrice(4, f3, 0, 3, 1) == 100) ++pass; else System.out.println("FAIL 787-4");
++total; if (sol.findCheapestPrice(4, f3, 0, 3, 2) == 30) ++pass; else System.out.println("FAIL 787-5");
++total; if (sol.findCheapestPrice(4, f3, 0, 3, 0) == 100) ++pass; else System.out.println("FAIL 787-6");
++total; if (sol.findCheapestPrice2(4, f1, 0, 3, 1) == 700) ++pass; else System.out.println("FAIL 787-7");
++total; if (sol.findCheapestPrice2(3, f2, 0, 2, 1) == 200) ++pass; else System.out.println("FAIL 787-8");
++total; if (sol.findCheapestPrice2(3, f2, 0, 2, 0) == 500) ++pass; else System.out.println("FAIL 787-9");
++total; if (sol.findCheapestPrice2(4, f3, 0, 3, 1) == 100) ++pass; else System.out.println("FAIL 787-10");
++total; if (sol.findCheapestPrice2(4, f3, 0, 3, 2) == 30) ++pass; else System.out.println("FAIL 787-11");
++total; if (sol.findCheapestPrice2(4, f3, 0, 3, 0) == 100) ++pass; else System.out.println("FAIL 787-12");
if (pass == total) {
System.out.println("== 0787 PASS ==");
} else {
System.out.println("== 0787 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(k * e) | O(n) |
| 解法二 | O(k * e) | O(n)(每层) |
788. Rotated Digits(旋转数字)
题目描述
我们称一个数 X 为好数,如果它的每位数字逐个地被旋转 180 度后,我们仍可以得到一个
有效的,且和 X 不同的数。要求每位数字都要被旋转。
如果一个数的每位数字被旋转以后仍然还是一个数字,则这个数是有效的。0、1 和 8 被旋转后
仍然是它们自己;2 和 5 可以互相旋转成对方;6 和 9 同理,除了这些以外其他的数字旋转以后
都不再是有效的数字。
现在我们有一个正整数 N,计算从 1 到 N 中有多少个数 X 是好数?
示例
- 输入:10
- 输出:4
- 解释:在 [1, 10] 中有四个好数:2, 5, 6, 9。 注意 1 和 10 不是好数,因为他们在旋转之后不变。 提示:
- N 的取值范围是 [1, 10000]
解题思路
「好数」= 每一位都只含 {0,1,2,5,6,8,9}(保证旋转有效)且至少含一个 {2,5,6,9}
(保证旋转后与自身不同)。含 3、4、7 的数直接不合法。
解法一(逐数检查,主提交):对 1..N 每个数逐位判断,合法且必变的计数。
解法二(三态数字 DP):把好数判定做成状态机(0-不合法 / 1-纯镜像 / 2-好数),
用数位 DP 或直接打表递推:dp[i] 记录 0..9 与 i 的映射后的状态。因为 N <= 10000,
也可直接遍历数字化判;此处实现一个基于字符替换的判定对照。
代码实现
class Solution {
// 解法一:逐数检查
public int rotatedDigits(int n) {
int ans = 0;
for (int i = 1; i <= n; ++i) {
if (isGood(i)) {
++ans;
}
}
return ans;
}
private boolean isGood(int x) {
boolean changed = false;
while (x > 0) {
int d = x % 10;
if (d == 3 || d == 4 || d == 7) {
return false;
}
if (d == 2 || d == 5 || d == 6 || d == 9) {
changed = true;
}
x /= 10;
}
return changed;
}
// 解法二:按位合法 + 必变标记(字符串形式,对照实现)
public int rotatedDigits2(int n) {
int ans = 0;
for (int i = 1; i <= n; ++i) {
String s = Integer.toString(i);
boolean valid = true;
boolean changed = false;
for (int j = 0; j < s.length(); ++j) {
char c = s.charAt(j);
if (c == '3' || c == '4' || c == '7') {
valid = false;
break;
}
if (c == '2' || c == '5' || c == '6' || c == '9') {
changed = true;
}
}
if (valid && changed) {
++ans;
}
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if (sol.rotatedDigits(10) == 4) ++pass; else System.out.println("FAIL 788-1");
++total; if (sol.rotatedDigits(1) == 0) ++pass; else System.out.println("FAIL 788-2");
++total; if (sol.rotatedDigits(2) == 1) ++pass; else System.out.println("FAIL 788-3");
++total; if (sol.rotatedDigits(100) == 40) ++pass; else System.out.println("FAIL 788-4");
++total; if (sol.rotatedDigits(857) == 247) ++pass; else System.out.println("FAIL 788-5");
++total; if (sol.rotatedDigits(10000) == 2320) ++pass; else System.out.println("FAIL 788-6");
++total; if (sol.rotatedDigits2(10) == 4) ++pass; else System.out.println("FAIL 788-7");
++total; if (sol.rotatedDigits2(100) == 40) ++pass; else System.out.println("FAIL 788-8");
++total; if (sol.rotatedDigits2(857) == 247) ++pass; else System.out.println("FAIL 788-9");
++total; if (sol.rotatedDigits2(10000) == 2320) ++pass; else System.out.println("FAIL 788-10");
if (pass == total) {
System.out.println("== 0788 PASS ==");
} else {
System.out.println("== 0788 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(N log N) | O(1) |
| 解法二 | O(N log N) | O(log N) |
789. Escape The Ghosts(逃脱阻碍者)
题目描述
你在进行一个简化版的吃豆人游戏。你从 [0, 0] 点开始出发,你的目的地是 target = [x, y]。
地图上有一些阻碍者,以数组 ghosts 给出,第 i 个阻碍者从 ghosts[i] = [x_i, y_i] 出发。
所有输入均为整数坐标。
每一回合,你和阻碍者们可以同时向东、西、南、北四个方向移动,每次可以移动到距离原位置
1 个单位的新位置。当然,也可以选择不动。所有动作同时发生。
如果你可以在任何阻碍者抓住你之前到达目的地(阻碍者可以采取任意行动方式),则被视为逃脱成功。
如果你和阻碍者同时到达了一个位置(包括目的地)都不算是逃脱成功。
如果不管阻碍者怎么移动都可以成功逃脱时,输出 true;否则,输出 false。
示例 1
- 输入:ghosts = [[1,0],[0,3]],
target= [0,1] - 输出:
true
示例 2
- 输入:ghosts = [[1,0]],
target= [2,0] - 输出:
false
示例 3
- 输入:ghosts = [[2,0]],
target= [1,0] - 输出:
false提示: 1 <= ghosts.length <= 100-10^4 <= x_i, y_i <= 10^4- 同一位置可能有多个阻碍者
-10^4 <= x_target, y_target <= 10^4
解题思路
网格上从 A 到 B 的最短步数就是曼哈顿距离 |dx| + |dy|(可以原地等待/绕行,但不会更短)。 阻碍者能拦住你,当且仅当其到目标的距离 <= 你到目标的距离:它可以先直接赶到目标处 等你(网格中不存在真正的"必经之路"以外的拦截,最优拦截就是截住目标点)。 解法一(比较曼哈顿距离,主提交):只要存在某个阻碍者距离目标 <= 你距离目标, 则返回 false;否则返回 true。 解法二(BFS 双向可达对照):暴力枚举玩家每一步,验证是否存在一条路径其每一点都 严格早于任意阻碍者到达。仅适合小范围坐标,作为理解用。
代码实现
class Solution {
// 解法一:曼哈顿距离比较
public boolean escapeGhosts(int[][] ghosts, int[] target) {
int me = dist(0, 0, target[0], target[1]);
for (int[] g : ghosts) {
if (dist(g[0], g[1], target[0], target[1]) <= me) {
return false;
}
}
return true;
}
private int dist(int x1, int y1, int x2, int y2) {
return Math.abs(x1 - x2) + Math.abs(y1 - y2);
}
// 解法二:最坏情况验证(所有阻碍者一起堵截目标)——与解法一逻辑等价的显式表述
public boolean escapeGhosts2(int[][] ghosts, int[] target) {
// 玩家最坏情况下的最短到达时间
int player = dist(0, 0, target[0], target[1]);
// 最小编号的阻碍者到达目标的时间
int minGhost = Integer.MAX_VALUE;
for (int[] g : ghosts) {
minGhost = Math.min(minGhost, dist(g[0], g[1], target[0], target[1]));
}
return player < minGhost;
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
int[][] g1 = {{1, 0}, {0, 3}};
++total; if (sol.escapeGhosts(g1, new int[] {0, 1})) ++pass; else System.out.println("FAIL 789-1");
int[][] g2 = {{1, 0}};
++total; if (!sol.escapeGhosts(g2, new int[] {2, 0})) ++pass; else System.out.println("FAIL 789-2");
int[][] g3 = {{2, 0}};
++total; if (!sol.escapeGhosts(g3, new int[] {1, 0})) ++pass; else System.out.println("FAIL 789-3");
int[][] g4 = {{5, 5}};
++total; if (sol.escapeGhosts(g4, new int[] {1, 1})) ++pass; else System.out.println("FAIL 789-4");
int[][] g5 = {{0, 1}};
++total; if (sol.escapeGhosts(g5, new int[] {1, 0})) ++pass; else System.out.println("FAIL 789-5");
int[][] g6 = {{-1, 0}};
++total; if (sol.escapeGhosts(g6, new int[] {0, 1})) ++pass; else System.out.println("FAIL 789-6");
++total; if (sol.escapeGhosts2(g1, new int[] {0, 1})) ++pass; else System.out.println("FAIL 789-7");
++total; if (!sol.escapeGhosts2(g2, new int[] {2, 0})) ++pass; else System.out.println("FAIL 789-8");
++total; if (sol.escapeGhosts2(g5, new int[] {1, 0})) ++pass; else System.out.println("FAIL 789-9");
if (pass == total) {
System.out.println("== 0789 PASS ==");
} else {
System.out.println("== 0789 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(m) | O(1) |
| 解法二 | O(V*E)(小范围对照) | O(V) |