LeetCode 864-875. 题解合集
864. Shortest Path to Get All Keys(获取所有钥匙的最短路径)
题目描述
给你一个二维网格 grid,其中:
'.'代表一个空房间;'#'代表一堵墙;'@'是起点;- 小写字母代表钥匙;
- 大写字母代表锁。
我们从起点开始出发,一次移动是指向四个基本方向之一行走一个单位空间。一些网格区域有锁。
如果我们有对应的钥匙,我们才能通过锁的区域。开始时我们没有任何钥匙。
每次经过一个钥匙格,我们都可以把它拾起来,并锁上它对应的门(即该钥匙格标记为已收集)。
请你返回获取所有钥匙所需要的移动的最少次数。如果无法到达,请返回-1。
示例 1
- 输入:
grid= ["@.a..","###.#","b.A.B"] - 输出:8
- 解释:目标是获得所有钥匙,而不是打开所有锁。
示例 2
- 输入:
grid= ["@..aA","..B#.","....b"] - 输出:6
提示: m == grid.lengthn == grid[i].length1 <= m, n <= 30grid[i][j]只含有'.'、'#'、'@'、'a'-'f'以及'A'-'F'- 钥匙的数目范围是
[1, 6] - 每个钥匙都对应至少一个锁,每个锁也对应一把钥匙
- 钥匙和锁的字母互不相同
解题思路
最短路径同时依赖“位置”与“当前已收集的钥匙集合”,把状态扩展为 (x, y, mask) 做 BFS,
其中 mask 为 6 位二进制位表示 a~f 六把钥匙是否已拿到。
解法一(状态压缩 BFS,主提交):先扫描网格找到起点、统计钥匙总数 K(每把钥匙对应
一个位)。BFS 状态 (x, y, mask);从起点出发,四方向移动:
- 越界或撞墙跳过;
- 遇到锁(大写字母)时,必须已持有对应钥匙(
mask >> (c-'A') & 1 == 1)才能进入; - 遇到钥匙(小写字母)时更新
mask,若mask == (1<<K)-1立即返回步数。
用dist[x][y][mask]去重,保证每个状态至多扩展一次。
解法二(优先队列 Dijkstra 视角,对照实现):把移动代价视为 1,用带(x,y,mask)状态的
堆化队列做 BFS,本质上相同;此处通过自定义对象入队记录步数展示另一种状态组织方式。
代码实现
import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.Map;
import java.util.Queue;
class Solution {
private static final int[] DX = {-1, 1, 0, 0};
private static final int[] DY = {0, 0, -1, 1};
// 解法一:状态压缩 BFS(推荐提交)
public int shortestPathAllKeys(String[] grid) {
int m = grid.length, n = grid[0].length();
int sx = 0, sy = 0, keyMask = 0;
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
char c = grid[i].charAt(j);
if (c == '@') {
sx = i;
sy = j;
} else if (c >= 'a' && c <= 'f') {
keyMask |= 1 << (c - 'a');
}
}
}
boolean[][][] vis = new boolean[m][n][64];
Queue<int[]> q = new ArrayDeque<>(); // {x, y, mask}
q.offer(new int[]{sx, sy, 0});
vis[sx][sy][0] = true;
int step = 0;
while (!q.isEmpty()) {
int size = q.size();
for (int round = 0; round < size; ++round) {
int[] cur = q.poll();
int x = cur[0], y = cur[1], mask = cur[2];
if (mask == keyMask) {
return step;
}
for (int d = 0; d < 4; ++d) {
int nx = x + DX[d], ny = y + DY[d];
if (nx < 0 || nx >= m || ny < 0 || ny >= n) {
continue;
}
char c = grid[nx].charAt(ny);
if (c == '#') {
continue;
}
int nmask = mask;
if (c >= 'A' && c <= 'F') {
if ((mask & (1 << (c - 'A'))) == 0) {
continue; // 没有对应钥匙不能通过
}
} else if (c >= 'a' && c <= 'f') {
nmask |= 1 << (c - 'a');
}
if (!vis[nx][ny][nmask]) {
vis[nx][ny][nmask] = true;
q.offer(new int[]{nx, ny, nmask});
}
}
}
++step;
}
return -1;
}
// 解法二:带步数的状态队列(对照实现)
public int shortestPathAllKeys2(String[] grid) {
int m = grid.length, n = grid[0].length();
int sx = 0, sy = 0, keyMask = 0;
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
char c = grid[i].charAt(j);
if (c == '@') {
sx = i;
sy = j;
} else if (c >= 'a' && c <= 'f') {
keyMask |= 1 << (c - 'a');
}
}
}
Map<String, Integer> dist = new HashMap<>();
Queue<int[]> q = new ArrayDeque<>(); // {x, y, mask, dist}
q.offer(new int[]{sx, sy, 0, 0});
dist.put(sx + "," + sy + "," + 0, 0);
int ans = Integer.MAX_VALUE;
while (!q.isEmpty()) {
int[] cur = q.poll();
int x = cur[0], y = cur[1], mask = cur[2], d = cur[3];
if (mask == keyMask) {
ans = Math.min(ans, d);
continue;
}
for (int dir = 0; dir < 4; ++dir) {
int nx = x + DX[dir], ny = y + DY[dir];
if (nx < 0 || nx >= m || ny < 0 || ny >= n) {
continue;
}
char c = grid[nx].charAt(ny);
if (c == '#') {
continue;
}
int nmask = mask;
if (c >= 'A' && c <= 'F') {
if ((mask & (1 << (c - 'A'))) == 0) {
continue;
}
} else if (c >= 'a' && c <= 'f') {
nmask |= 1 << (c - 'a');
}
String key = nx + "," + ny + "," + nmask;
Integer old = dist.get(key);
if (old == null || old > d + 1) {
dist.put(key, d + 1);
q.offer(new int[]{nx, ny, nmask, d + 1});
}
}
}
return ans == Integer.MAX_VALUE ? -1 : ans;
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
String[][] grids = {
{"@.a..", "###.#", "b.A.B"}, // 官方示例 1
{"@..aA", "..B#.", "....b"}, // 官方示例 2
{"@Aa"}, // 官方示例 3:钥匙被锁挡在墙后,不可达
{"@a.", "b##", ".c#"}, // 无锁,收集 3 把钥匙需 5 步
{"@.a..A.b"} // 需先拿 a 再过 A 拿 b,共 7 步
};
int[] exp = {8, 6, -1, 5, 7};
for (int t = 0; t < grids.length; ++t) {
++total;
int g1 = s.shortestPathAllKeys(grids[t]);
int g2 = s.shortestPathAllKeys2(grids[t]);
if (g1 == exp[t] && g2 == exp[t]) {
++pass;
} else {
System.out.println("FAIL 864-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0864 PASS ==");
} else {
System.out.println("== 0864 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(m * n * 2^K) | O(m * n * 2^K) |
| 解法二 | O(m * n * 2^K) | O(m * n * 2^K) |
865. Smallest Subtree with all the Deepest Nodes(具有所有最深节点的最小子树)
题目描述
给定一个根为 root 的二叉树,每个节点的深度是它到根的最短距离。
返回包含原树中所有最深节点的最小子树。如果一个节点是整棵树的叶子节点且它的深度最大,
那么它自己就是包含所有最深节点的最小子树。
示例 1
- 输入:
root= [3,5,1,6,2,0,8,null,null,7,4] - 输出:[2,7,4]
- 解释:最深节点为 7 和 4(深度 3),包含它们的最小子树是以 2 为根的子树。
示例 2
- 输入:
root= [1] - 输出:[1]
示例 3
- 输入:
root= [0,1,3,null,2] - 输出:[2]
- 解释:节点 2 是唯一的叶子节点,深度 2 最大,所以它自己就是答案。
提示: - 树中节点数量在
[1, 500]范围内 0 <= Node.val <= 500- 每个节点的值都是独一无二的
解题思路
所有最深节点的最近公共祖先就是答案。可以自底向上判定:哪个子树“同时包含所有最深节点”。
解法一(两遍 DFS:最大深度 + 定位 LCA,主提交):第一遍求出整棵树最大深度 maxDepth;
第二遍从根开始找“同时包含 maxDepth 层节点的最深子树”:若当前节点深度为 maxDepth,
它本身就是答案;否则递归左右子树,若左右子树都存在深度 maxDepth 的节点,当前节点就是
答案,否则返回包含最深节点的那个子树。
解法二(递归返回 (深度, 子树),对照实现):每个节点返回 (maxDepth, 该深度对应的子树);
若左右子树最大深度相同且都达到全局最大,则合并返回当前节点;否则返回更深一侧的结果。
只需一遍 DFS。
代码实现
import java.util.ArrayDeque;
import java.util.Queue;
class Solution {
// 解法一:最大深度 + LCA 定位(推荐提交)
public TreeNode subtreeWithAllDeepest(TreeNode root) {
int maxDepth = depth(root);
return find(root, 1, maxDepth);
}
private int depth(TreeNode node) {
if (node == null) {
return 0;
}
return 1 + Math.max(depth(node.left), depth(node.right));
}
// 返回包含所有“深度 == maxDepth”节点的最小子树
private TreeNode find(TreeNode node, int d, int maxDepth) {
if (node == null) {
return null;
}
if (d == maxDepth) {
return node; // 当前节点就是最深节点
}
TreeNode l = find(node.left, d + 1, maxDepth);
TreeNode r = find(node.right, d + 1, maxDepth);
if (l != null && r != null) {
return node; // 左右都有最深节点,交汇于此
}
return l != null ? l : r;
}
// 解法二:一遍 DFS 返回 (深度, 子树)(对照实现)
public TreeNode subtreeWithAllDeepest2(TreeNode root) {
// 返回二元组:deepest(root) = {最大深度, 覆盖全部最深节点的子树}
return post(root).node;
}
private Result post(TreeNode node) {
if (node == null) {
return new Result(0, null);
}
Result l = post(node.left);
Result r = post(node.right);
if (l.depth == r.depth) {
return new Result(l.depth + 1, node); // 两侧同深,当前节点成为 LCA
}
if (l.depth > r.depth) {
return new Result(l.depth + 1, l.node);
}
return new Result(r.depth + 1, r.node);
}
private static class Result {
int depth;
TreeNode node;
Result(int depth, TreeNode node) {
this.depth = depth;
this.node = node;
}
}
// 二叉树节点定义
static class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) {
val = x;
}
}
// 本地测试辅助:层序建树
private static TreeNode build(Integer[] vals) {
if (vals == null || vals.length == 0) {
return null;
}
TreeNode root = new TreeNode(vals[0]);
Queue<TreeNode> q = new ArrayDeque<>();
q.offer(root);
int i = 1;
while (!q.isEmpty() && i < vals.length) {
TreeNode cur = q.poll();
if (vals[i] != null) {
cur.left = new TreeNode(vals[i]);
q.offer(cur.left);
}
++i;
if (i < vals.length && vals[i] != null) {
cur.right = new TreeNode(vals[i]);
q.offer(cur.right);
}
++i;
}
return root;
}
// 树结构转层序字符串(便于对比;LinkedList 允许 null)
private static String dump(TreeNode root) {
if (root == null) {
return "[]";
}
StringBuilder sb = new StringBuilder("[");
Queue<TreeNode> q = new java.util.LinkedList<>();
q.offer(root);
while (!q.isEmpty()) {
TreeNode cur = q.poll();
if (cur == null) {
sb.append("null,");
continue;
}
sb.append(cur.val).append(",");
q.offer(cur.left);
q.offer(cur.right);
}
// 去掉尾部 null
String s = sb.toString();
while (s.endsWith("null,")) {
s = s.substring(0, s.length() - 5);
}
return s.substring(0, s.length() - 1) + "]";
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
++total;
{
TreeNode root = build(new Integer[]{3, 5, 1, 6, 2, 0, 8, null, null, 7, 4});
if (dump(s.subtreeWithAllDeepest(root)).equals("[2,7,4]")
&& dump(s.subtreeWithAllDeepest2(root)).equals("[2,7,4]")) {
++pass;
} else {
System.out.println("FAIL 865-1: " + dump(s.subtreeWithAllDeepest(root)));
}
}
++total;
{
TreeNode root = build(new Integer[]{1});
if (dump(s.subtreeWithAllDeepest(root)).equals("[1]")) {
++pass;
} else {
System.out.println("FAIL 865-2");
}
}
++total;
{
TreeNode root = build(new Integer[]{0, 1, 3, null, 2});
TreeNode got = s.subtreeWithAllDeepest(root);
if (got != null && got.val == 2 && s.subtreeWithAllDeepest2(root).val == 2) {
++pass;
} else {
System.out.println("FAIL 865-3: " + (got == null ? "null" : got.val));
}
}
++total;
{
TreeNode root = build(new Integer[]{1, 2, 3, 4});
if (dump(s.subtreeWithAllDeepest(root)).equals("[4]")) {
++pass;
} else {
System.out.println("FAIL 865-4: " + dump(s.subtreeWithAllDeepest(root)));
}
}
if (pass == total) {
System.out.println("== 0865 PASS ==");
} else {
System.out.println("== 0865 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n) | O(h)(树高,递归栈) |
| 解法二 | O(n) | O(h) |
866. Prime Palindrome(回文质数)
题目描述
返回大于或等于 n 的最小回文质数。
一个整数如果它本身是回文数(正读反读相同),并且是质数,那么它就是回文质数。
示例 1
- 输入:
n= 6 - 输出:7
示例 2
- 输入:
n= 8 - 输出:11
示例 3
- 输入:
n= 13 - 输出:101
提示: 1 <= n <= 10^8- 答案确保是一个小于
2 * 10^8的 32 位有符号整数
解题思路
直接枚举每个整数判断回文再判断质数可行但较慢;利用“偶数长度的回文数一定被 11 整除
(除 11 本身)”,因此长度大于 1 的回文质数必为奇数位。可从低到高枚举回文数的“上半部分”,
构造出完整回文后检查是否 >= n 且为质数,保证答案按数值升序遍历。
解法一(枚举回文数,主提交):从 1 位开始向上枚举奇数位长度,对每个长度枚举“前一半”
的数字 i(最小为 10^(len-1) 的数量级),镜像构造回文,跳过偶数长度(除 11),逐个判断
质数,第一个 >= n 且为质数的即是答案。
解法二(枚举整数 + 改进质数判断,对照实现):从 n 开始逐个整数试探,若该数是回文且是
质数则返回;通过数字转字符串判回文、质数判断只试到 sqrt。在题目范围内通常足够快,
但最坏情况可能多尝试一些数字,思路更直接。
代码实现
class Solution {
// 解法一:枚举回文数(推荐提交)
public int primePalindrome(int n) {
if (n <= 2) {
return 2;
}
if (n == 3) {
return 3;
}
// 2、3、5、7、11 特判
if (n <= 5) {
return 5;
}
if (n <= 7) {
return 7;
}
if (n <= 11) {
return 11;
}
// 构造长度大于等于从 n 的位数开始(答案位数 >= n 的位数)
for (int len = String.valueOf(n).length(); ; ++len) {
if (len % 2 == 0) {
// 偶数位回文数(除 11)均为 11 的倍数,跳过
if (len == 2) {
continue; // 11 已特判
}
continue;
}
int half = (len + 1) / 2; // 前半部分长度
long lo = (long) Math.pow(10, half - 1);
long hi = (long) Math.pow(10, half); // 前半部分范围 [lo, hi)
for (long prefix = lo; prefix < hi; ++prefix) {
long pal = mirror(prefix);
if (pal < n) {
continue;
}
if (isPrime(pal)) {
return (int) pal;
}
}
}
}
// 由前半部分构造奇数位回文数
private long mirror(long prefix) {
String s = Long.toString(prefix);
String rev = new StringBuilder(s).reverse().toString();
return Long.parseLong(s + rev.substring(1));
}
private boolean isPrime(long x) {
if (x < 2) {
return false;
}
if (x == 2 || x == 3) {
return true;
}
if (x % 2 == 0 || x % 3 == 0) {
return false;
}
for (long i = 5; i * i <= x; i += 6) {
if (x % i == 0 || x % (i + 2) == 0) {
return false;
}
}
return true;
}
// 解法二:从 n 逐个枚举(对照实现)
public int primePalindrome2(int n) {
if (n <= 2) {
return 2;
}
for (int x = n; ; ++x) {
// 偶数位回文数跳过(除 11)
if (x > 11 && String.valueOf(x).length() % 2 == 0) {
// 跳到下一个奇数位长度起点:10^(digits)
int digits = String.valueOf(x).length();
x = (int) Math.pow(10, digits) - 1; // 循环 ++ 后为 10^digits
continue;
}
if (isPalindrome(x) && isPrime(x)) {
return x;
}
}
}
private boolean isPalindrome(int x) {
String s = Integer.toString(x);
int l = 0, r = s.length() - 1;
while (l < r) {
if (s.charAt(l) != s.charAt(r)) {
return false;
}
++l;
--r;
}
return true;
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
int[] ns = {6, 8, 13, 1, 2, 3, 11, 100, 9989900};
int[] exp = {7, 11, 101, 2, 2, 3, 11, 101, 100030001};
for (int t = 0; t < ns.length; ++t) {
++total;
int g1 = s.primePalindrome(ns[t]);
int g2 = s.primePalindrome2(ns[t]);
if (g1 == exp[t] && g2 == exp[t]) {
++pass;
} else {
System.out.println("FAIL 866-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0866 PASS ==");
} else {
System.out.println("== 0866 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(10^(L/2) * sqrt(10^L)),L 为答案位数 | O(1) |
| 解法二 | O(答案与 n 的差 * sqrt(答案)) | O(1) |
867. Transpose Matrix(转置矩阵)
题目描述
给你一个二维整数数组 matrix,返回 matrix 的转置矩阵。
矩阵的转置是指将矩阵的主对角线翻转,交换矩阵的行索引与列索引。
示例 1
- 输入:
matrix= [[1,2,3],[4,5,6],[7,8,9]] - 输出:[[1,4,7],[2,5,8],[3,6,9]]
示例 2
- 输入:
matrix= [[1,2,3],[4,5,6]] - 输出:[[1,4],[2,5],[3,6]]
提示: m == matrix.lengthn == matrix[i].length1 <= m, n <= 10001 <= m * n <= 10^5-10^9 <= matrix[i][j] <= 10^9
解题思路
转置就是把 matrix[i][j] 放到 res[j][i]。原矩阵是 m×n,转置后是 n×m。
解法一(直接构造,主提交):申请 n x m 的新数组,双重循环赋值。时间 O(mn),空间 O(mn)。
解法二(逐列填充,对照实现):逐列处理原矩阵,把第 j 列的所有元素拷贝为新矩阵第 j
行。逻辑等价,便于理解列优先的视角。
代码实现
class Solution {
// 解法一:直接构造(推荐提交)
public int[][] transpose(int[][] matrix) {
int m = matrix.length, n = matrix[0].length;
int[][] res = new int[n][m];
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
res[j][i] = matrix[i][j];
}
}
return res;
}
// 解法二:逐列填充(对照实现)
public int[][] transpose2(int[][] matrix) {
int m = matrix.length, n = matrix[0].length;
int[][] res = new int[n][m];
for (int j = 0; j < n; ++j) {
for (int i = 0; i < m; ++i) {
res[j][i] = matrix[i][j];
}
}
return res;
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
int[][][] mats = {
{{1, 2, 3}, {4, 5, 6}, {7, 8, 9}},
{{1, 2, 3}, {4, 5, 6}},
{{1}},
{{1, 2}, {3, 4}, {5, 6}}
};
int[][][] exps = {
{{1, 4, 7}, {2, 5, 8}, {3, 6, 9}},
{{1, 4}, {2, 5}, {3, 6}},
{{1}},
{{1, 3, 5}, {2, 4, 6}}
};
for (int t = 0; t < mats.length; ++t) {
++total;
if (java.util.Arrays.deepEquals(s.transpose(mats[t]), exps[t])
&& java.util.Arrays.deepEquals(s.transpose2(mats[t]), exps[t])) {
++pass;
} else {
System.out.println("FAIL 867-" + (t + 1));
}
}
if (pass == total) {
System.out.println("== 0867 PASS ==");
} else {
System.out.println("== 0867 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(m * n) | O(m * n) |
| 解法二 | O(m * n) | O(m * n) |
868. Binary Gap(二进制间距)
题目描述
给定一个正整数 n,找到并返回 n 的二进制表示中两个相邻 1 之间的最长距离。如果不存在两个
相邻的 1,则返回 0。
如果只有 0 将两个 1 分隔开(可能不存在 0),则认为这两个 1 彼此相邻。两个 1 之间的距离是
它们二进制表示中位置差值的绝对值。例如,"1001" 中的两个 1 之间的距离为 3。
示例 1
- 输入:
n= 22 - 输出:2
- 解释:22 的二进制是 "10110"。
在 22 的二进制表示中,有三个 1,组成两对相邻的 1。
第一对相邻的 1 中,两个 1 之间的距离为 2。
第二对相邻的 1 中,两个 1 之间的距离为 1。
答案取两个距离之中最大的,也就是 2。
示例 2
- 输入:
n= 8 - 输出:0
- 解释:8 的二进制是 "1000"。在 8 的二进制表示中没有相邻的两个 1,所以返回 0。
示例 3
- 输入:
n= 5 - 输出:2
- 解释:5 的二进制是 "101",两个相邻的 1 之间的距离为 2。
提示: 1 <= n <= 10^9
解题思路
相邻两个 1 的间距是它们在二进制串中的位置差。逐位扫描二进制位,记录上一个 1 的位置。
解法一(位扫描,主提交):用 Integer 从最低位到最高位依次检查:若当前位为 1,且之前
出现过 1,则用当前位置与上次位置的差更新答案;再用 n >>= 1 或直接用 position 递增
模拟位权。
解法二(toString 法,对照实现):Integer.toBinaryString(n) 得到二进制串,扫描找相邻 1
的位置差最大值;思路直观,借助字符串 API。
代码实现
class Solution {
// 解法一:位扫描(推荐提交)
public int binaryGap(int n) {
int prev = -1;
int ans = 0;
for (int i = 0; i < 31; ++i) {
if (((n >> i) & 1) == 1) {
if (prev != -1) {
ans = Math.max(ans, i - prev);
}
prev = i;
}
}
return ans;
}
// 解法二:二进制字符串法(对照实现)
public int binaryGap2(int n) {
String s = Integer.toBinaryString(n);
int prev = -1;
int ans = 0;
for (int i = 0; i < s.length(); ++i) {
if (s.charAt(i) == '1') {
if (prev != -1) {
ans = Math.max(ans, i - prev);
}
prev = i;
}
}
return ans;
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
int[] ns = {22, 8, 5, 1, 6, 0};
int[] exp = {2, 0, 2, 0, 1, 0};
for (int t = 0; t < ns.length; ++t) {
++total;
if (s.binaryGap(ns[t]) == exp[t] && s.binaryGap2(ns[t]) == exp[t]) {
++pass;
} else {
System.out.println("FAIL 868-" + (t + 1) + ": got " + s.binaryGap(ns[t]) + " exp " + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0868 PASS ==");
} else {
System.out.println("== 0868 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(log n) | O(1) |
| 解法二 | O(log n) | O(log n)(字符串) |
869. Reordered Power of 2(重新排序得到 2 的幂)
题目描述
给定正整数 n,我们按任何顺序(包括原始顺序)将数字重新排序,注意其前导数字不能为零。
如果我们可以通过上述方式得到 2 的幂,返回 true;否则返回 false。
示例 1
- 输入:
n= 1 - 输出:
true
示例 2
- 输入:
n= 10 - 输出:
false
示例 3
- 输入:
n= 46 - 输出:
true
提示: 1 <= n <= 10^9
解题思路
数字重排只改变数位顺序,不改变各数字的出现次数。因此只需比较 n 的数位频次是否与
某个 2 的幂完全一致。由于 n <= 10^9,只需枚举不超过 30 个 2 的幂。
解法一(频次数组比较,主提交):把 n 转成 10 位长频次数组(每位数字计数),枚举
1 << k(k = 0..30)检查其数位频次是否与 n 相同,相同返回 true,否则 false。
解法二(排序字符串比较,对照实现):把 n 的十进制字符串排序与所有 2 的幂的十进制
字符串排序比对,借助排序避免手写频次数组,思路等价。
代码实现
import java.util.HashSet;
import java.util.Set;
class Solution {
// 解法一:频次数组比较(推荐提交)
public boolean reorderedPowerOf2(int n) {
int[] cnt = count(n);
for (int k = 0; k < 31; ++k) {
if (same(cnt, count(1 << k))) {
return true;
}
}
return false;
}
private int[] count(int x) {
int[] c = new int[10];
String s = Integer.toString(x);
for (int i = 0; i < s.length(); ++i) {
++c[s.charAt(i) - '0'];
}
return c;
}
private boolean same(int[] a, int[] b) {
for (int i = 0; i < 10; ++i) {
if (a[i] != b[i]) {
return false;
}
}
return true;
}
// 解法二:排序字符串比较(对照实现)
public boolean reorderedPowerOf2_2(int n) {
String target = sortedStr(n);
for (int k = 0; k < 31; ++k) {
if (target.equals(sortedStr(1 << k))) {
return true;
}
}
return false;
}
private String sortedStr(int x) {
char[] cs = Integer.toString(x).toCharArray();
java.util.Arrays.sort(cs);
return new String(cs);
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
int[] ns = {1, 10, 46, 24, 679, 111, 1000, 1024};
boolean[] exp = {true, false, true, false, false, false, false, true};
for (int t = 0; t < ns.length; ++t) {
++total;
if (s.reorderedPowerOf2(ns[t]) == exp[t]
&& s.reorderedPowerOf2_2(ns[t]) == exp[t]) {
++pass;
} else {
System.out.println("FAIL 869-" + (t + 1) + ": got " + s.reorderedPowerOf2(ns[t]) + " exp " + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0869 PASS ==");
} else {
System.out.println("== 0869 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(31 * 10)(数位最多 10 位) | O(1) |
| 解法二 | O(31 * d log d),d 为数位 | O(d) |
870. Advantage Shuffle(优势洗牌)
题目描述
给定两个长度相等的数组 nums1 和 nums2,nums1 相对于 nums2 的优势可以用满足
nums1[i] > nums2[i] 的索引 i 的数目来描述。
返回 nums1 的任意排列,使其相对于 nums2 的优势最大化。
示例 1
- 输入:nums1 = [2,7,11,15], nums2 = [1,10,4,11]
- 输出:[2,11,7,15]
示例 2
- 输入:nums1 = [12,24,8,32], nums2 = [13,25,32,11]
- 输出:[24,32,8,12]
提示: 1 <= nums1.length <= 10^50 <= nums1[i] <= 10^90 <= nums2[i] <= 10^9nums1.length == nums2.length
解题思路
与“田忌赛马”同理:用最小代价去消耗对方最强的牌,用强牌去对抗对方比自己弱的牌。
解法一(排序 + 双指针,主提交):把 nums1 升序排序,nums2 的下标按值升序排序。
用双指针处理已排序的 nums1:对于 nums2 中最小的尚未匹配的元素,若 nums1[l] 能赢它
(nums1[l] > nums2[i])就配对并 l++;否则把 nums1[l] 这张“废牌”配给 nums2 中
当前最大的元素(消耗对方强牌),l++、r--。这样最大化胜场数。
解法二(TreeMap 贪心,对照实现):把 nums1 放进 TreeMap 计数;对每个 nums2[i],
用 higherKey(nums2[i]) 找比它大的最小牌,有则用,没有则用当前最小牌(放弃本局)。
思路直观,O(n log n)。
代码实现
import java.util.Arrays;
class Solution {
// 解法一:排序 + 双指针(推荐提交)
public int[] advantageCount(int[] nums1, int[] nums2) {
int n = nums1.length;
Arrays.sort(nums1);
Integer[] idx = new Integer[n];
for (int i = 0; i < n; ++i) {
idx[i] = i;
}
// nums2 的索引按值升序
Arrays.sort(idx, (a, b) -> Integer.compare(nums2[a], nums2[b]));
int[] ans = new int[n];
int left = 0, right = n - 1;
for (int i = n - 1; i >= 0; --i) {
int j = idx[i]; // 当前 nums2 中最大元素的下标
if (nums1[right] > nums2[j]) {
ans[j] = nums1[right--]; // 用最大的牌赢过对方最大值
} else {
ans[j] = nums1[left++]; // 赢不了则用最小的牌消耗对方最大值
}
}
return ans;
}
// 解法二:TreeMap 贪心(对照实现)
public int[] advantageCount2(int[] nums1, int[] nums2) {
int n = nums1.length;
java.util.TreeMap<Integer, Integer> map = new java.util.TreeMap<>();
for (int x : nums1) {
map.put(x, map.getOrDefault(x, 0) + 1);
}
int[] ans = new int[n];
for (int i = 0; i < n; ++i) {
Integer better = map.higherKey(nums2[i]);
if (better == null) {
better = map.firstKey(); // 放弃本局,用最小的牌
}
ans[i] = better;
if (map.get(better) == 1) {
map.remove(better);
} else {
map.put(better, map.get(better) - 1);
}
}
return ans;
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
// 期待值不定,改为验证得分(优势数)是否达到最优
int[][] n1s = {
{2, 7, 11, 15}, {12, 24, 8, 32}, {1, 1, 1, 2}, {2, 4, 6}, {1, 2, 3}
};
int[][] n2s = {
{1, 10, 4, 11}, {13, 25, 32, 11}, {1, 1, 1, 1}, {3, 1, 2}, {1, 2, 3}
};
int[] best = {4, 3, 1, 3, 2};
for (int t = 0; t < n1s.length; ++t) {
++total;
int score1 = score(s.advantageCount(n1s[t], n2s[t]), n2s[t]);
int score2 = score(s.advantageCount2(n1s[t], n2s[t]), n2s[t]);
if (score1 == best[t] && score2 == best[t]) {
++pass;
} else {
System.out.println("FAIL 870-" + (t + 1) + ": score " + score1 + "/" + score2 + " best " + best[t]);
}
}
if (pass == total) {
System.out.println("== 0870 PASS ==");
} else {
System.out.println("== 0870 FAIL ==");
System.exit(1);
}
}
private static int score(int[] res, int[] nums2) {
int cnt = 0;
for (int i = 0; i < res.length; ++i) {
if (res[i] > nums2[i]) {
++cnt;
}
}
return cnt;
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n log n) | O(n) |
| 解法二 | O(n log n) | O(n) |
871. Minimum Number of Refueling Stops(最低加油次数)
题目描述
汽车从起点出发驶向目的地,该目的地位于出发位置东面 target 英里处。
沿途有加油站,用数组 stations 表示。其中 stations[i] = [position_i, fuel_i] 表示第 i
个加油站位于出发位置东面 position_i 英里处,并且有 fuel_i 升汽油。
假设汽车油箱的容量是无限的,其中最初有 startFuel 升燃料。它每行驶 1 英里就会用掉 1 升
汽油。当汽车到达加油站时,它可能停下来加油,将所有汽油从加油站转移到汽车中。
为了到达目的地,汽车所必要的最低加油次数是多少?如果无法到达目的地,则返回 -1。
注意:如果汽车到达加油站时剩余燃料为 0,它仍然可以在那里加油。如果汽车到达目的地时
剩余燃料为 0,仍然认为它已经到达目的地。
示例 1
- 输入:
target= 1,startFuel= 1, stations = [] - 输出:0
示例 2
- 输入:
target= 100,startFuel= 1, stations = [[10,100]] - 输出:-1
示例 3
- 输入:
target= 100,startFuel= 10, stations = [[10,60],[20,30],[30,30],[60,40]] - 输出:2
- 解释:在 10 和 60 两个加油站各加一次油。
提示: 1 <= target, startFuel <= 10^90 <= stations.length <= 5001 <= position_i < position_{i+1} < target1 <= fuel_i < 10^9
解题思路
贪心 + “后悔”思想:把已经经过的加油站的油量放入大根堆,当油不够开到下一站时,从堆中取
出最大油量“补加”,尽可能用最少的次数获得最多的油。
解法一(贪心 + 大根堆,主提交):把终点也当作一个 position = target, fuel = 0 的站点
参与扫描。维护当前位置 pre、当前油量 fuel、大根堆 pq(存已经过的站点的油量)。
依次推进:每到一个站点 pos,fuel -= pos - pre;若 fuel < 0,从 pq 中反复弹出
最大油量加入 fuel 并计数,直到 fuel >= 0;若 pq 空仍不够则返回 -1;把该站油量入堆,
更新 pre。遍历结束返回加油次数。
解法二(动态规划,对照实现):dp[i] 表示加油 i 次能到达的最远距离,初始 dp[0] = startFuel。
遍历每个加油站,从大到小更新 dp[j + 1] = max(dp[j+1], dp[j] + fuel_i)(前提 dp[j] >= position_i),
第一个 dp[i] >= target 的 i 就是答案。O(n^2)。
代码实现
import java.util.PriorityQueue;
class Solution {
// 解法一:贪心 + 大根堆(推荐提交)
public int minRefuelStops(int target, int startFuel, int[][] stations) {
PriorityQueue<Integer> pq = new PriorityQueue<>((a, b) -> Integer.compare(b, a));
int ans = 0;
int pre = 0;
int fuel = startFuel;
int n = stations.length;
for (int i = 0; i <= n; ++i) {
int pos = i < n ? stations[i][0] : target;
int dist = pos - pre;
fuel -= dist;
while (fuel < 0 && !pq.isEmpty()) {
fuel += pq.poll();
++ans;
}
if (fuel < 0) {
return -1;
}
if (i < n) {
pq.offer(stations[i][1]);
pre = pos;
}
}
return ans;
}
// 解法二:动态规划(对照实现)
public int minRefuelStops2(int target, int startFuel, int[][] stations) {
int n = stations.length;
long[] dp = new long[n + 1]; // dp[i] = 加油 i 次能到的最远距离
dp[0] = startFuel;
for (int i = 0; i < n; ++i) {
for (int j = i; j >= 0; --j) {
if (dp[j] >= stations[i][0]) {
dp[j + 1] = Math.max(dp[j + 1], dp[j] + stations[i][1]);
}
}
}
for (int i = 0; i <= n; ++i) {
if (dp[i] >= target) {
return i;
}
}
return -1;
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
++total;
if (s.minRefuelStops(1, 1, new int[][]{}) == 0
&& s.minRefuelStops2(1, 1, new int[][]{}) == 0) {
++pass;
} else {
System.out.println("FAIL 871-1");
}
++total;
if (s.minRefuelStops(100, 1, new int[][]{{10, 100}}) == -1
&& s.minRefuelStops2(100, 1, new int[][]{{10, 100}}) == -1) {
++pass;
} else {
System.out.println("FAIL 871-2");
}
++total;
if (s.minRefuelStops(100, 10, new int[][]{{10, 60}, {20, 30}, {30, 30}, {60, 40}}) == 2
&& s.minRefuelStops2(100, 10, new int[][]{{10, 60}, {20, 30}, {30, 30}, {60, 40}}) == 2) {
++pass;
} else {
System.out.println("FAIL 871-3: " + s.minRefuelStops(100, 10, new int[][]{{10, 60}, {20, 30}, {30, 30}, {60, 40}}));
}
++total;
if (s.minRefuelStops(1000, 1, new int[][]{}) == -1
&& s.minRefuelStops2(1000, 1, new int[][]{}) == -1) {
++pass;
} else {
System.out.println("FAIL 871-4");
}
++total;
if (s.minRefuelStops(100, 50, new int[][]{{10, 60}, {20, 100}, {30, 30}}) == 1
&& s.minRefuelStops2(100, 50, new int[][]{{10, 60}, {20, 100}, {30, 30}}) == 1) {
++pass;
} else {
System.out.println("FAIL 871-5: " + s.minRefuelStops(100, 50, new int[][]{{10, 60}, {20, 100}, {30, 30}}));
}
if (pass == total) {
System.out.println("== 0871 PASS ==");
} else {
System.out.println("== 0871 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n log n) | O(n) |
| 解法二 | O(n^2) | O(n) |
872. Leaf-Similar Trees(叶子相似的树)
题目描述
请考虑一棵二叉树上所有的叶子,这些叶子的值按从左到右的顺序排列形成一个叶值序列。
如果有两棵二叉树的叶值序列是相同,那么我们就认为它们是叶相似的。
如果给定的两个根结点分别为 root1 和 root2 的树是叶相似的,则返回 true;否则返回
false。
示例 1
- 输入:root1 = [3,5,1,6,2,9,8,
null,null,7,4], root2 = [3,5,1,6,7,4,2,null,null,null,null,null,null,9,8] - 输出:
true
示例 2
- 输入:root1 = [1,2,3], root2 = [1,3,2]
- 输出:
false
提示: - 给定的两棵树结点数在
[1, 200]范围内 - 给定的两棵树上的值在
[0, 200]范围内
解题思路
叶值序列是按“从左到右”的先序(中序亦可,叶子顺序天然一致)收集叶子节点得到的结果。
解法一(DFS 收集两棵树的叶子,主提交):递归遍历,左右孩子都为空即为叶子,按先序顺序
(先左后右)收集;得到两个序列直接相等判断。
解法二(迭代栈 DFS,对照实现):用显式栈模拟先序遍历收集叶子,避免递归栈深度占用;
两棵树的叶子序列分别收集后比较。实现不依赖系统栈,思路与解法一相同。
代码实现
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
class Solution {
// 解法一:DFS 递归收集(推荐提交)
public boolean leafSimilar(TreeNode root1, TreeNode root2) {
List<Integer> l1 = new ArrayList<>();
List<Integer> l2 = new ArrayList<>();
dfs(root1, l1);
dfs(root2, l2);
return l1.equals(l2);
}
private void dfs(TreeNode node, List<Integer> leaves) {
if (node == null) {
return;
}
if (node.left == null && node.right == null) {
leaves.add(node.val);
return;
}
dfs(node.left, leaves);
dfs(node.right, leaves);
}
// 解法二:迭代栈(对照实现)
public boolean leafSimilar2(TreeNode root1, TreeNode root2) {
return collect(root1).equals(collect(root2));
}
private List<Integer> collect(TreeNode root) {
List<Integer> leaves = new ArrayList<>();
if (root == null) {
return leaves;
}
ArrayDeque<TreeNode> stack = new ArrayDeque<>();
stack.push(root);
while (!stack.isEmpty()) {
TreeNode cur = stack.pop();
if (cur.left == null && cur.right == null) {
leaves.add(cur.val);
continue;
}
// 先右后左,保证弹出时先处理左子树
if (cur.right != null) {
stack.push(cur.right);
}
if (cur.left != null) {
stack.push(cur.left);
}
}
return leaves;
}
// 二叉树节点定义
static class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) {
val = x;
}
}
// 本地测试辅助:层序建树
private static TreeNode build(Integer[] vals) {
if (vals == null || vals.length == 0) {
return null;
}
TreeNode root = new TreeNode(vals[0]);
Queue<TreeNode> q = new ArrayDeque<>();
q.offer(root);
int i = 1;
while (!q.isEmpty() && i < vals.length) {
TreeNode cur = q.poll();
if (vals[i] != null) {
cur.left = new TreeNode(vals[i]);
q.offer(cur.left);
}
++i;
if (i < vals.length && vals[i] != null) {
cur.right = new TreeNode(vals[i]);
q.offer(cur.right);
}
++i;
}
return root;
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
++total;
{
TreeNode r1 = build(new Integer[]{3, 5, 1, 6, 2, 9, 8, null, null, 7, 4});
TreeNode r2 = build(new Integer[]{3, 5, 1, 6, 7, 4, 2, null, null, null, null, null, null, 9, 8});
if (s.leafSimilar(r1, r2) && s.leafSimilar2(r1, r2)) {
++pass;
} else {
System.out.println("FAIL 872-1");
}
}
++total;
{
TreeNode r1 = build(new Integer[]{1, 2, 3});
TreeNode r2 = build(new Integer[]{1, 3, 2});
if (!s.leafSimilar(r1, r2) && !s.leafSimilar2(r1, r2)) {
++pass;
} else {
System.out.println("FAIL 872-2");
}
}
++total;
{
TreeNode r1 = build(new Integer[]{1});
TreeNode r2 = build(new Integer[]{1, 2, 3});
if (s.leafSimilar(r1, r2) == false && s.leafSimilar2(r1, r2) == false) {
++pass;
} else {
System.out.println("FAIL 872-3");
}
}
++total;
{
TreeNode r1 = build(new Integer[]{1, null, 2});
TreeNode r2 = build(new Integer[]{1, 2});
if (s.leafSimilar(r1, r2) && s.leafSimilar2(r1, r2)) {
++pass;
} else {
System.out.println("FAIL 872-4");
}
}
if (pass == total) {
System.out.println("== 0872 PASS ==");
} else {
System.out.println("== 0872 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n1 + n2) | O(h1 + h2)(递归深度) |
| 解法二 | O(n1 + n2) | O(h1 + h2)(显式栈) |
873. Length of Longest Fibonacci Subsequence(最长的斐波那契子序列的长度)
题目描述
如果序列 x_1, x_2, ..., x_n 满足下列条件,就说它是斐波那契式的:
n >= 3- 对于所有
i + 2 <= n,都有x_i + x_{i+1} == x_{i+2}
给定一个严格递增的正整数数组形成序列arr,找到arr中最长的斐波那契式的子序列的长度。
如果不存在,返回0。
子序列是通过从另一个序列arr中删除任意数量的元素(包括删除 0 个元素)得到的,同时不改变
剩余元素顺序。例如,[3, 5, 8]是[3, 4, 5, 6, 7, 8]的子序列。
示例 1
- 输入:
arr= [1,2,3,4,5,6,7,8] - 输出: 5
- 解释: 最长的斐波那契式子序列为 [1,2,3,5,8]。
示例 2
- 输入:
arr= [1,3,7,11,12,14,18] - 输出: 3
- 解释: 最长的斐波那契式子序列有 [1,11,12]、[3,11,14] 以及 [7,11,18]。
提示: 3 <= arr.length <= 10001 <= arr[i] < arr[i + 1] <= 10^9
解题思路
斐波那契式子序列每新增一项由前两项决定,可以把状态定义为「以 (arr[i], arr[j]) 结尾的
子序列长度」,其中 i < j 且 arr[j] 是最后一项。
解法一(动态规划 + 值到下标映射,主提交):用 dp[i][j] 表示以 arr[i] 和 arr[j]
为最后两项的最长斐波那契式子序列长度(dp[i][j] >= 2 表示至少两项)。枚举最右侧的
j 与中间的 i,若 k = arr[j] - arr[i] 存在于 arr 且 k < arr[i](值为 k 的下标
p 满足 p < i,即 k 在原序列中更靠左),则 dp[i][j] = dp[p][i] + 1,否则 = 2;
全局取最大,若最大 < 3 则返回 0。实现时用一维滚动以省内存(相邻两项 -> 长度)。
解法二(哈希表逐步扩展,对照实现):枚举每个可能的起始对 (i, j),用哈希表查找
arr[i] + arr[j] 是否存在,存在则继续向后扩展,统计长度。以每个起始对为起点,
O(n^2) 起始对,每对的扩展步数 O(n),最坏 O(n^3) 但实际很快;作为对照思路。
代码实现
import java.util.HashMap;
import java.util.Map;
class Solution {
// 解法一:动态规划(推荐提交)
public int lenLongestFibSubseq(int[] arr) {
int n = arr.length;
Map<Integer, Integer> pos = new HashMap<>();
for (int i = 0; i < n; ++i) {
pos.put(arr[i], i);
}
int[][] dp = new int[n][n]; // dp[i][j]: 以 arr[i], arr[j] 结尾(i<j)的最长长度
int ans = 0;
for (int j = 0; j < n; ++j) {
for (int i = 0; i < j; ++i) {
int need = arr[j] - arr[i];
Integer p = pos.get(need);
if (p != null && p < i) {
dp[i][j] = dp[p][i] + 1;
} else {
dp[i][j] = 2;
}
ans = Math.max(ans, dp[i][j]);
}
}
return ans < 3 ? 0 : ans;
}
// 解法二:哈希表逐步扩展(对照实现)
public int lenLongestFibSubseq2(int[] arr) {
int n = arr.length;
Map<Integer, Integer> pos = new HashMap<>();
for (int i = 0; i < n; ++i) {
pos.put(arr[i], i);
}
int ans = 0;
for (int i = 0; i < n; ++i) {
for (int j = i + 1; j < n; ++j) {
int a = arr[i], b = arr[j];
int len = 2;
while (true) {
Integer k = pos.get(a + b);
if (k == null) {
break;
}
int tmp = b;
b = a + b;
a = tmp;
++len;
}
if (len >= 3) {
ans = Math.max(ans, len);
}
}
}
return ans;
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
int[][] arrs = {
{1, 2, 3, 4, 5, 6, 7, 8},
{1, 3, 7, 11, 12, 14, 18},
{1, 2, 3},
{1, 2},
{2, 4, 7, 8, 11, 12, 14, 15, 18, 19}
};
int[] exp = {5, 3, 3, 0, 4};
for (int t = 0; t < arrs.length; ++t) {
++total;
int g1 = s.lenLongestFibSubseq(arrs[t]);
int g2 = s.lenLongestFibSubseq2(arrs[t]);
if (g1 == exp[t] && g2 == exp[t]) {
++pass;
} else {
System.out.println("FAIL 873-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0873 PASS ==");
} else {
System.out.println("== 0873 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n^2) | O(n^2)(或 O(n) 滚动) |
| 解法二 | O(n^3) 最坏 | O(n) |
874. Walking Robot Simulation(模拟行走机器人)
题目描述
机器人在一个无限大小的 XY 网格平面上行走,从点 (0, 0) 处开始出发,面向北方。该机器人
可以接收以下三种类型的命令 commands:
-2:向左转 90 度;-1:向右转 90 度;1 <= x <= 9:向前移动x个单位长度。
在网格上有一些格子被视为障碍物obstacles。第i个障碍物位于网格点
obstacles[i] = (x_i, y_i)。
机器人无法走到障碍物上,它将会停在障碍物的前一个网格方块上,但仍然可以继续尝试进行该路线
的其余部分。
返回从原点到机器人所有经过的路径点(坐标为整数)的最大欧氏距离的平方。
示例 1
- 输入:commands = [4,-1,3], obstacles = []
- 输出:25
- 解释:机器人开始位于 (0, 0):
- 向北移动 4 个单位,到达 (0, 4)。
- 右转。
- 向东移动 3 个单位,到达 (3, 4)。
距离原点最远的是 (3, 4) ,距离为 3^2 + 4^2 = 25。
示例 2
- 输入:commands = [4,-1,4,-2,4], obstacles = [[2,4]]
- 输出:65
- 解释:机器人开始位于 (0, 0):
- 向北移动 4 个单位,到达 (0, 4)。
- 右转。
- 向东移动 1 个单位,然后被位于 (2, 4) 的障碍物阻挡,停在 (1, 4)。
- 左转。
- 向北走 4 个单位,到达 (1, 8)。
距离原点最远的是 (1, 8) ,距离为 1^2 + 8^2 = 65。
提示:
1 <= commands.length <= 10^4commands[i]是-2、-1或者是范围[1, 9]内的一个整数0 <= obstacles.length <= 10^4-3 * 10^4 <= x_i, y_i <= 3 * 10^4- 答案保证小于
2^31
解题思路
机器人朝四个方向移动,方向用 0=北、1=东、2=南、3=西 表示;-1 右转相当于
dir = (dir + 1) % 4,-2 左转相当于 dir = (dir + 3) % 4。移动时逐格检查前方是否
有障碍物(障碍物集合用哈希存储坐标编码),有则停在该格前。
解法一(方向数组 + HashSet 障碍判定,主提交):四个方向向量
{{0,1},{1,0},{0,-1},{-1,0}},移动时每步试探下一格,若在障碍集合中则停止本次前进,
否则真正移动并更新最远距离平方。
解法二(坐标编码 String,对照实现):把障碍物编码为 x + "," + y 的字符串放入 Set,
方向切换拆成独立 if 分支;逻辑等价,但字符串比较比 int 编码略慢,仅作对照。
代码实现
import java.util.HashSet;
import java.util.Set;
class Solution {
private static final int[][] DIRS = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};
// 解法一:方向数组 + int 编码障碍(推荐提交)
public int robotSim(int[] commands, int[][] obstacles) {
Set<Integer> block = new HashSet<>();
for (int[] o : obstacles) {
block.add(encode(o[0], o[1]));
}
int x = 0, y = 0, dir = 0; // 0=北 1=东 2=南 3=西
int ans = 0;
for (int c : commands) {
if (c == -1) {
dir = (dir + 1) % 4;
} else if (c == -2) {
dir = (dir + 3) % 4;
} else {
int dx = DIRS[dir][0], dy = DIRS[dir][1];
for (int step = 0; step < c; ++step) {
int nx = x + dx, ny = y + dy;
if (block.contains(encode(nx, ny))) {
break;
}
x = nx;
y = ny;
}
ans = Math.max(ans, x * x + y * y);
}
}
return ans;
}
private int encode(int x, int y) {
// 坐标范围 +-30000,偏移后编码到 64 位内唯一
return (x + 30000) * 100000 + (y + 30000);
}
// 解法二:字符串编码障碍(对照实现)
public int robotSim2(int[] commands, int[][] obstacles) {
Set<String> block = new HashSet<>();
for (int[] o : obstacles) {
block.add(o[0] + "," + o[1]);
}
int x = 0, y = 0, dir = 0;
int ans = 0;
for (int c : commands) {
if (c == -2) {
dir = dir == 0 ? 3 : dir - 1;
} else if (c == -1) {
dir = (dir + 1) % 4;
} else {
int dx = DIRS[dir][0], dy = DIRS[dir][1];
for (int step = 0; step < c; ++step) {
int nx = x + dx, ny = y + dy;
if (block.contains(nx + "," + ny)) {
break;
}
x = nx;
y = ny;
}
ans = Math.max(ans, x * x + y * y);
}
}
return ans;
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
++total;
if (s.robotSim(new int[]{4, -1, 3}, new int[][]{}) == 25) {
++pass;
} else {
System.out.println("FAIL 874-1: " + s.robotSim(new int[]{4, -1, 3}, new int[][]{}));
}
++total;
if (s.robotSim(new int[]{4, -1, 4, -2, 4}, new int[][]{{2, 4}}) == 65) {
++pass;
} else {
System.out.println("FAIL 874-2: " + s.robotSim(new int[]{4, -1, 4, -2, 4}, new int[][]{{2, 4}}));
}
++total;
if (s.robotSim(new int[]{6, -2, 8}, new int[][]{}) == 100) {
++pass;
} else {
System.out.println("FAIL 874-3: " + s.robotSim(new int[]{6, -2, 8}, new int[][]{}));
}
++total;
if (s.robotSim2(new int[]{4, -1, 3}, new int[][]{}) == 25
&& s.robotSim2(new int[]{4, -1, 4, -2, 4}, new int[][]{{2, 4}}) == 65) {
++pass;
} else {
System.out.println("FAIL 874-4");
}
if (pass == total) {
System.out.println("== 0874 PASS ==");
} else {
System.out.println("== 0874 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n * m),m 为单次最大步数(9) + O(k) 建集 | O(k) |
| 解法二 | O(n * m) + O(k) | O(k) |
875. Koko Eating Bananas(爱吃香蕉的珂珂)
题目描述
珂珂喜欢吃香蕉。这里有 n 堆香蕉,第 i 堆中有 piles[i] 根香蕉。警卫已经离开了,
将在 h 小时后回来。
珂珂可以决定她吃香蕉的速度 k(单位:根/小时)。每个小时,她将会选择一堆香蕉,从中吃掉
k 根。如果这堆香蕉少于 k 根,她将吃掉这堆的所有香蕉,然后这一小时内不会再吃更多的
香蕉。
珂珂喜欢慢慢吃,但仍然想在警卫回来前吃掉所有的香蕉。
返回她可以在 h 小时内吃掉所有香蕉的最小速度 k(k 为整数)。
示例 1
- 输入:piles = [3,6,7,11],
h= 8 - 输出:4
示例 2
- 输入:piles = [30,11,23,4,20],
h= 5 - 输出:30
示例 3
- 输入:piles = [30,11,23,4,20],
h= 6 - 输出:23
提示: 1 <= piles.length <= 10^4piles.length <= h <= 10^91 <= piles[i] <= 10^9
解题思路
吃完全部香蕉所需小时数随速度 k 单调递减,因此可对速度二分查找第一个使总耗时
<= h 的 k。每堆耗时 ceil(piles[i] / k)。
解法一(二分答案,主提交):二分区间 [1, max(piles)]。ok(k) 判断以速度 k 是否能在
h 小时内吃完:对每堆累加 (p + k - 1) / k,注意用 long 防溢出;总耗时 <= h 即合法。
二分取合法区间左边界,即为最小速度。
解法二(暴力从 1 递增,对照实现):从 k = 1 开始逐个测试(每堆沿用上面的耗时公式),
找到第一个合法速度。最坏情况接近 max(piles) 时很慢,仅作正确性对照。
代码实现
import java.util.Arrays;
class Solution {
// 解法一:二分答案(推荐提交)
public int minEatingSpeed(int[] piles, int h) {
int lo = 1, hi = 0;
for (int p : piles) {
hi = Math.max(hi, p);
}
while (lo < hi) {
int mid = lo + (hi - lo) / 2;
if (canFinish(piles, h, mid)) {
hi = mid;
} else {
lo = mid + 1;
}
}
return lo;
}
private boolean canFinish(int[] piles, int h, int k) {
long hours = 0;
for (int p : piles) {
hours += (p + k - 1L) / k;
if (hours > h) {
return false;
}
}
return true;
}
// 解法二:递增枚举(对照实现)
public int minEatingSpeed2(int[] piles, int h) {
int k = 1;
while (true) {
long hours = 0;
for (int p : piles) {
hours += (p + k - 1L) / k;
if (hours > h) {
break;
}
}
if (hours <= h) {
return k;
}
++k;
}
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
++total;
if (s.minEatingSpeed(new int[]{3, 6, 7, 11}, 8) == 4) {
++pass;
} else {
System.out.println("FAIL 875-1: " + s.minEatingSpeed(new int[]{3, 6, 7, 11}, 8));
}
++total;
if (s.minEatingSpeed(new int[]{30, 11, 23, 4, 20}, 5) == 30) {
++pass;
} else {
System.out.println("FAIL 875-2: " + s.minEatingSpeed(new int[]{30, 11, 23, 4, 20}, 5));
}
++total;
if (s.minEatingSpeed(new int[]{30, 11, 23, 4, 20}, 6) == 23) {
++pass;
} else {
System.out.println("FAIL 875-3: " + s.minEatingSpeed(new int[]{30, 11, 23, 4, 20}, 6));
}
++total;
if (s.minEatingSpeed(new int[]{1, 1, 1}, 3) == 1) {
++pass;
} else {
System.out.println("FAIL 875-4");
}
++total;
if (s.minEatingSpeed2(new int[]{3, 6, 7, 11}, 8) == 4
&& s.minEatingSpeed2(new int[]{30, 11, 23, 4, 20}, 6) == 23) {
++pass;
} else {
System.out.println("FAIL 875-5");
}
if (pass == total) {
System.out.println("== 0875 PASS ==");
} else {
System.out.println("== 0875 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n log max(piles)) | O(1) |
| 解法二 | O(max(piles) * n) | O(1) |