LeetCode 851-875. 题解合集
文章目录
- [LeetCode 851-875. 题解合集](#LeetCode 851-875. 题解合集)
-
- [851. Loud and Rich(喧闹和富有)](#851. Loud and Rich(喧闹和富有))
- [852. Peak Index in a Mountain Array(山脉数组的峰顶索引)](#852. Peak Index in a Mountain Array(山脉数组的峰顶索引))
- [853. Car Fleet(车队)](#853. Car Fleet(车队))
- [854. K-Similar Strings(相似度为 K 的字符串)](#854. K-Similar Strings(相似度为 K 的字符串))
- [855. Exam Room(考场就座)](#855. Exam Room(考场就座))
- [856. Score of Parentheses(括号的分数)](#856. Score of Parentheses(括号的分数))
- [857. Minimum Cost to Hire K Workers(雇佣 K 名工人的最低成本)](#857. Minimum Cost to Hire K Workers(雇佣 K 名工人的最低成本))
- [858. Mirror Reflection(镜面反射)](#858. Mirror Reflection(镜面反射))
- [859. Buddy Strings(亲密字符串)](#859. Buddy Strings(亲密字符串))
- [860. Lemonade Change(柠檬水找零)](#860. Lemonade Change(柠檬水找零))
- [861. Score After Flipping Matrix(翻转矩阵后的得分)](#861. Score After Flipping Matrix(翻转矩阵后的得分))
- [862. Shortest Subarray with Sum at Least K(和至少为 K 的最短子数组)](#862. Shortest Subarray with Sum at Least K(和至少为 K 的最短子数组))
- [863. All Nodes Distance K in Binary Tree(二叉树中所有距离为 K 的结点)](#863. All Nodes Distance K in Binary Tree(二叉树中所有距离为 K 的结点))
- [864. Shortest Path to Get All Keys(获取所有钥匙的最短路径)](#864. Shortest Path to Get All Keys(获取所有钥匙的最短路径))
- [865. Smallest Subtree with all the Deepest Nodes(具有所有最深节点的最小子树)](#865. Smallest Subtree with all the Deepest Nodes(具有所有最深节点的最小子树))
- [866. Prime Palindrome(回文质数)](#866. Prime Palindrome(回文质数))
- [867. Transpose Matrix(转置矩阵)](#867. Transpose Matrix(转置矩阵))
- [868. Binary Gap(二进制间距)](#868. Binary Gap(二进制间距))
- [869. Reordered Power of 2(重新排序得到 2 的幂)](#869. Reordered Power of 2(重新排序得到 2 的幂))
- [870. Advantage Shuffle(优势洗牌)](#870. Advantage Shuffle(优势洗牌))
- [871. Minimum Number of Refueling Stops(最低加油次数)](#871. Minimum Number of Refueling Stops(最低加油次数))
- [872. Leaf-Similar Trees(叶子相似的树)](#872. Leaf-Similar Trees(叶子相似的树))
- [873. Length of Longest Fibonacci Subsequence(最长的斐波那契子序列的长度)](#873. Length of Longest Fibonacci Subsequence(最长的斐波那契子序列的长度))
- [874. Walking Robot Simulation(模拟行走机器人)](#874. Walking Robot Simulation(模拟行走机器人))
- [875. Koko Eating Bananas(爱吃香蕉的珂珂)](#875. Koko Eating Bananas(爱吃香蕉的珂珂))
851. Loud and Rich(喧闹和富有)
题目描述
有一组 n 个人作为实验对象,从 0 到 n - 1 编号,其中每个人都有不同数目的钱,
以及不同程度的安静值。为了方便起见,我们将编号为 x 的人简称为 "person x"。
给你一个数组 richer,其中 richer[i] = [a_i, b_i] 表示 person a_i 比 person
b_i 更有钱。另给你一个整数数组 quiet,其中 quiet[i] 是 person i 的安静值。
richer 中所给出的数据逻辑自洽(也就是说,在 person x 比 person y 更有钱的同时,
不会出现 person y 比 person x 更有钱的情况)。
现在,返回一个整数数组 answer 作为答案,其中 answer[x] = y 的前提是:在所有拥有的钱
肯定不少于 person x 的人中,person y 是最安静的人(也就是安静值 quiet[y] 最小的人)。
示例 1
- 输入:richer = \[1,0,2,1,3,1,3,7,4,3,5,3,6,3], quiet = 3,2,5,4,6,1,7,0
- 输出:5,5,2,5,4,5,6,7
- 解释 :
answer[0] = 5:
person 5 比 person 3 有钱,person 3 比 person 1 有钱,person 1 比 person 0 有钱。
所以是 person 5 比 person 0 有钱,输出 5(quiet 值最小的人)。
示例 2
- 输入:richer = \[\], quiet = 0
- 输出 :0
提示: n == quiet.length1 <= n <= 5000 <= quiet[i] < n0 <= richer.length <= n * (n - 1) / 2
解题思路
每个人 x 的答案,需要在「所有比 x 有钱或与 x 相同的人」中取安静值最小者。
解法一(记忆化 DFS,主提交):把"比 b 有钱"看成依赖关系,建图 g[b].add(a),递归
求 x 的答案 = x 与所有 g[x] 中节点的答案里安静值最小的人。每个人只算一次,记忆化。
解法二(拓扑排序):建图 g[a].add(b)(a 比 b 有钱),入度为 0 的点(最有钱的人,
答案就是自己)入队;弹出 u 时用 ans[u] 更新所有"比 u 穷"的直接邻居的答案,邻居
入度归零后入队,逐步把最优信息传播到传播链末端。
代码实现
java
import java.util.ArrayList;
import java.util.ArrayDeque;
import java.util.List;
import java.util.Queue;
class Solution {
private List<Integer>[] g;
private int[] quiet;
private int[] memo;
// 解法一:记忆化 DFS(推荐提交)
public int[] loudAndRich(int[][] richer, int[] quiet) {
int n = quiet.length;
this.quiet = quiet;
g = new List[n];
for (int i = 0; i < n; ++i) {
g[i] = new ArrayList<>();
}
for (int[] e : richer) {
// e[0] 比 e[1] 有钱:求 e[1] 的答案需要先知道 e[0]
g[e[1]].add(e[0]);
}
memo = new int[n];
java.util.Arrays.fill(memo, -1);
int[] ans = new int[n];
for (int i = 0; i < n; ++i) {
ans[i] = dfs(i);
}
return ans;
}
private int dfs(int x) {
if (memo[x] != -1) {
return memo[x];
}
int best = x;
for (int y : g[x]) {
int t = dfs(y);
if (quiet[t] < quiet[best]) {
best = t;
}
}
return memo[x] = best;
}
// 解法二:拓扑排序传播(对照实现)
public int[] loudAndRich2(int[][] richer, int[] quiet) {
int n = quiet.length;
this.quiet = quiet;
List<Integer>[] graph = new List[n];
for (int i = 0; i < n; ++i) {
graph[i] = new ArrayList<>();
}
int[] indeg = new int[n];
for (int[] e : richer) {
graph[e[0]].add(e[1]);
++indeg[e[1]];
}
int[] ans = new int[n];
Queue<Integer> q = new ArrayDeque<>();
for (int i = 0; i < n; ++i) {
ans[i] = i;
if (indeg[i] == 0) {
q.offer(i);
}
}
while (!q.isEmpty()) {
int u = q.poll();
for (int v : graph[u]) {
// u 比 v 有钱:v 的候选答案可以尝试 u 的答案
if (quiet[ans[u]] < quiet[ans[v]]) {
ans[v] = ans[u];
}
if (--indeg[v] == 0) {
q.offer(v);
}
}
}
return ans;
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
int[][] r1 = {{1, 0}, {2, 1}, {3, 1}, {3, 7}, {4, 3}, {5, 3}, {6, 3}};
int[] q1 = {3, 2, 5, 4, 6, 1, 7, 0};
++total;
if (java.util.Arrays.equals(s.loudAndRich(r1, q1), new int[]{5, 5, 2, 5, 4, 5, 6, 7})) {
++pass;
} else {
System.out.println("FAIL 851-1: " + java.util.Arrays.toString(s.loudAndRich(r1, q1)));
}
++total;
if (java.util.Arrays.equals(s.loudAndRich(new int[][]{}, new int[]{0}), new int[]{0})) {
++pass;
} else {
System.out.println("FAIL 851-2");
}
++total;
if (java.util.Arrays.equals(s.loudAndRich2(r1, q1), new int[]{5, 5, 2, 5, 4, 5, 6, 7})) {
++pass;
} else {
System.out.println("FAIL 851-3: " + java.util.Arrays.toString(s.loudAndRich2(r1, q1)));
}
++total;
if (java.util.Arrays.equals(s.loudAndRich2(new int[][]{}, new int[]{0}), new int[]{0})) {
++pass;
} else {
System.out.println("FAIL 851-4");
}
if (pass == total) {
System.out.println("== 0851 PASS ==");
} else {
System.out.println("== 0851 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n + m) | O(n + m) |
| 解法二 | O(n + m) | O(n + m) |
852. Peak Index in a Mountain Array(山脉数组的峰顶索引)
题目描述
符合下列属性的数组 arr 称为山脉数组:
arr.length >= 3
存在 i(0 < i < arr.length - 1)使得:
arr[0] < arr[1] < ... < arr[i - 1] < arr[i]
arr[i] > arr[i + 1] > ... > arr[arr.length - 1]
给你由整数组成的山脉数组 arr,返回任何满足 arr[0] < arr[1] < ... < arr[i - 1] < arr[i] > arri + 1> ... >arr[arr.length - 1] 的下标 i。
示例 1
- 输入 :
arr= 0,1,0 - 输出:1
示例 2
- 输入 :
arr= 0,2,1,0 - 输出:1
示例 3
- 输入 :
arr= 0,10,5,2 - 输出 :1
提示: 3 <= arr.length <= 10^50 <= arr[i] <= 10^6- 题目数据保证
arr是一个山脉数组
解题思路
山脉数组严格"先升后降",峰值左边 arr[i] < arr[i+1]、右边 arr[i] > arr[i+1],具有单调分界。
解法一(二分查找,主提交):在 [0, n-1] 上二分:mid 处若 arr[mid] < arr[mid+1],
说明峰值在右侧(mid 处于上升段),l = mid + 1;否则峰值在 mid 或左侧,r = mid。
循环结束后 l 即峰顶索引,时间复杂度 O(log n)。
解法二(线性扫描,对照实现):从 0 开始一路比较相邻元素,找到第一个
arr[i] > arr[i+1] 的位置就是峰顶,时间复杂度 O(n),实现直观但常数更差。
代码实现
java
class Solution {
// 解法一:二分查找(推荐提交)
public int peakIndexInMountainArray(int[] arr) {
int n = arr.length;
int l = 0, r = n - 1;
while (l < r) {
int mid = (l + r) >>> 1;
if (arr[mid] < arr[mid + 1]) {
l = mid + 1; // 处于上升段,峰顶在右侧
} else {
r = mid; // 处于下降段(或正好峰顶),峰顶在 mid 或左侧
}
}
return l;
}
// 解法二:线性扫描(对照实现)
public int peakIndexInMountainArray2(int[] arr) {
int i = 0;
while (arr[i] < arr[i + 1]) {
++i;
}
return i;
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
int[][] arrs = {
{0, 1, 0},
{0, 2, 1, 0},
{0, 10, 5, 2},
{24, 69, 100, 99, 79, 78, 67, 36, 26, 19},
{3, 5, 3, 2, 0}
};
int[] exp = {1, 1, 1, 2, 1};
for (int t = 0; t < arrs.length; ++t) {
++total;
if (s.peakIndexInMountainArray(arrs[t]) == exp[t]
&& s.peakIndexInMountainArray2(arrs[t]) == exp[t]) {
++pass;
} else {
System.out.println("FAIL 852-" + (t + 1) + ": got "
+ s.peakIndexInMountainArray(arrs[t]));
}
}
if (pass == total) {
System.out.println("== 0852 PASS ==");
} else {
System.out.println("== 0852 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(log n) | O(1) |
| 解法二 | O(n) | O(1) |
853. Car Fleet(车队)
题目描述
在一条单行道上,有 n 辆车开往同一目的地(目的地是几英里以外的 target 英里处)。
给定两个整数数组 position 和 speed,长度都是 n,其中 position[i] 是第 i 辆车的
位置,speed[i] 是第 i 辆车的速度(单位:英里/小时)。
一辆车永远不会超过前面的另一辆车,但它可以追上去,并以较慢车的速度在另一辆车旁边行驶。
车队 是指并排行驶的一辆或几辆汽车。车队的速度是车队中 最慢 的车的速度。
即便一辆车在 target 才赶上了一个车队,它们仍然会被视作是同一个车队。
返回到达目的地的车队数量。
示例 1
- 输入 :
target= 12, position = 10,8,0,5,3, speed = 2,4,1,1,3 - 输出:3
- 解释 :
从 10(速度为 2)和 8(速度为 4)开始的车会组成一个车队,它们在 12 相遇。
从 0(速度为 1)开始的车不会追上其它任何车,所以它自己是一个车队。
从 5(速度为 1)和 3(速度为 3)开始的车组成一个车队,在 6 相遇。
示例 2
- 输入 :
target= 10, position = 3, speed = 3 - 输出:1
示例 3
- 输入 :
target= 100, position = 0,2,4, speed = 4,2,1 - 输出 :1
提示: n == position.length == speed.length1 <= n <= 10^50 < target <= 10^60 <= position[i] < targetposition中每个值都不同0 < speed[i] <= 10^6
解题思路
每辆车单独行驶按自己的速度到达 target 所需时间 time = (target - position) / speed。
位置靠后的车若到达时间 ≤ 前车(距离 target 更近的车)的到达时间,就会追上并并入车
队;只有到达时间严格更大的车,才会形成新的车队。
解法一(按位置从近到远扫描,主提交):按 position 降序排序,记录前方车队所需的最长
时间 lead。遍历每辆车,若其到达时间 t > lead,说明它追不上前面的任何车,车队数加一
并更新 lead;否则并入当前车队。时间复杂度主要来自排序 O(n log n)。
解法二(升序排列后反向扫描,对照实现):把 (position, time) 按位置升序排好,从终点
方向往回扫描,遇到「到达时间比右侧已记录的最大到达时间还大」的车就计为新车队。思路与
解法一相同,仅遍历顺序相反。
代码实现
java
import java.util.Arrays;
class Solution {
// 解法一:按 position 降序扫描(推荐提交)
public int carFleet(int target, int[] position, int[] speed) {
int n = position.length;
Integer[] idx = new Integer[n];
for (int i = 0; i < n; ++i) {
idx[i] = i;
}
// 位置越接近 target 的越靠前
Arrays.sort(idx, (a, b) -> Integer.compare(position[b], position[a]));
int ans = 0;
double lead = 0; // 前方车队所需的最大时间
for (int i : idx) {
double t = (double) (target - position[i]) / speed[i];
if (t > lead) {
++ans;
lead = t;
}
}
return ans;
}
// 解法二:升序排列后从终点方向反向扫描(对照实现)
public int carFleet2(int target, int[] position, int[] speed) {
int n = position.length;
double[][] cars = new double[n][2]; // {position, time}
for (int i = 0; i < n; ++i) {
cars[i][0] = position[i];
cars[i][1] = (double) (target - position[i]) / speed[i];
}
Arrays.sort(cars, (a, b) -> Double.compare(a[0], b[0]));
int ans = 0;
double maxTime = 0; // 从终点往回已经记录的最大到达时间
for (int i = n - 1; i >= 0; --i) {
if (cars[i][1] > maxTime) {
++ans;
maxTime = cars[i][1];
}
}
return ans;
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
++total;
if (s.carFleet(12, new int[]{10, 8, 0, 5, 3}, new int[]{2, 4, 1, 1, 3}) == 3
&& s.carFleet2(12, new int[]{10, 8, 0, 5, 3}, new int[]{2, 4, 1, 1, 3}) == 3) {
++pass;
} else {
System.out.println("FAIL 853-1: " + s.carFleet(12, new int[]{10, 8, 0, 5, 3}, new int[]{2, 4, 1, 1, 3}));
}
++total;
if (s.carFleet(10, new int[]{3}, new int[]{3}) == 1
&& s.carFleet2(10, new int[]{3}, new int[]{3}) == 1) {
++pass;
} else {
System.out.println("FAIL 853-2");
}
++total;
if (s.carFleet(100, new int[]{0, 2, 4}, new int[]{4, 2, 1}) == 1
&& s.carFleet2(100, new int[]{0, 2, 4}, new int[]{4, 2, 1}) == 1) {
++pass;
} else {
System.out.println("FAIL 853-3");
}
++total;
if (s.carFleet(10, new int[]{6, 8}, new int[]{3, 2}) == 2
&& s.carFleet2(10, new int[]{6, 8}, new int[]{3, 2}) == 2) {
++pass;
} else {
System.out.println("FAIL 853-4");
}
if (pass == total) {
System.out.println("== 0853 PASS ==");
} else {
System.out.println("== 0853 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n log n) | O(n) |
| 解法二 | O(n log n) | O(n) |
854. K-Similar Strings(相似度为 K 的字符串)
题目描述
字符串 s1 和 s2 是 k 相似的(对于某些非负整数 k),如果我们可以通过交换字符串
s1 中两个字母的位置正好 k 次,使结果字符串等于 s2。
给定两个字谜游戏所需的字谜 s1 和 s2,返回 s1 和 s2 的相似度 k 的最小值。
示例 1
- 输入 :
s1= "ab",s2= "ba" - 输出:1
示例 2
- 输入 :
s1= "abc",s2= "bca" - 输出 :2
提示: 1 <= s1.length <= 20s2.length == s1.lengths1和s2只包含集合{'a', 'b', 'c', 'd', 'e', 'f'}中的小写字母- 题目数据保证两个
s1和s2是字谜(组成字母及个数相同,仅顺序不同)
解题思路
问题等价于求从 s1 出发,每次「交换任意两个不同下标的字符」,到达 s2 所需的最少交换
次数。每个状态是一个字符串,目标是最短路径,用 BFS。
解法一(BFS + 首个错位剪枝,主提交):每层取当前串,找到第一个与 s2 不同的位置 i,
只尝试把某个 j > i 处、字符恰为 s2[i] 且不在正确位置上的字符交换到 i。该剪枝保证
每次交换至少修复一个错位,分支显著缩小,每个串最多入队一次,层数即最少交换次数。
解法二(记忆化 DFS,对照实现):与 BFS 相同剪枝前提下,用递归 + memo 求最少步数,
逻辑等价、代码更短,两种实现结果一致。
代码实现
java
import java.util.ArrayDeque;
import java.util.HashSet;
import java.util.Queue;
import java.util.Set;
class Solution {
private String target;
private java.util.HashMap<String, Integer> memoMap;
// 解法一:BFS + 首个错位剪枝(推荐提交)
public int kSimilarity(String s1, String s2) {
if (s1.equals(s2)) {
return 0;
}
Queue<String> q = new ArrayDeque<>();
Set<String> seen = new HashSet<>();
q.offer(s1);
seen.add(s1);
int step = 0;
int n = s1.length();
char[] t = s2.toCharArray();
while (!q.isEmpty()) {
++step;
int size = q.size();
for (int round = 0; round < size; ++round) {
String cur = q.poll();
char[] cs = cur.toCharArray();
int i = 0;
while (cs[i] == t[i]) {
++i;
}
for (int j = i + 1; j < n; ++j) {
if (cs[j] == t[i] && cs[j] != t[j]) {
char tmp = cs[i];
cs[i] = cs[j];
cs[j] = tmp;
String next = new String(cs);
if (next.equals(s2)) {
return step;
}
if (!seen.contains(next)) {
seen.add(next);
q.offer(next);
}
tmp = cs[i];
cs[i] = cs[j];
cs[j] = tmp;
}
}
}
}
return -1;
}
// 解法二:记忆化 DFS(对照实现)
public int kSimilarity2(String s1, String s2) {
target = s2;
memoMap = new java.util.HashMap<>();
return dfs2(s1);
}
private int dfs2(String cur) {
if (cur.equals(target)) {
return 0;
}
Integer cached = memoMap.get(cur);
if (cached != null) {
return cached;
}
int n = target.length();
char[] cs = cur.toCharArray();
char[] t = target.toCharArray();
int i = 0;
while (cs[i] == t[i]) {
++i;
}
int best = Integer.MAX_VALUE;
for (int j = i + 1; j < n; ++j) {
if (cs[j] == t[i] && cs[j] != t[j]) {
char tmp = cs[i];
cs[i] = cs[j];
cs[j] = tmp;
int sub = dfs2(new String(cs));
if (sub != Integer.MAX_VALUE) {
best = Math.min(best, 1 + sub);
}
tmp = cs[i];
cs[i] = cs[j];
cs[j] = tmp;
}
}
memoMap.put(cur, best);
return best;
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
String[][] cases = {
{"ab", "ba"},
{"abc", "bca"},
{"abccaacceecdeea", "bcaacceeccdeaae"},
{"cdebcdeadedaaaebfbcf", "fdeceabceabddaefadcb"},
{"aaa", "aaa"}
};
int[] exp = {1, 2, 9, 9, 0};
for (int t = 0; t < cases.length; ++t) {
++total;
int got1 = s.kSimilarity(cases[t][0], cases[t][1]);
int got2 = s.kSimilarity2(cases[t][0], cases[t][1]);
if (got1 == exp[t] && got2 == exp[t]) {
++pass;
} else {
System.out.println("FAIL 854-" + (t + 1) + ": got1=" + got1 + " got2=" + got2 + " exp=" + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0854 PASS ==");
} else {
System.out.println("== 0854 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n^2 * 状态数) | O(状态数) |
| 解法二 | O(n^2 * 状态数) | O(状态数) |
855. Exam Room(考场就座)
题目描述
在考场里,有 n 个座位排成一行,编号为 0 到 n - 1。
当学生进入考场后,他必须坐在离最近的人最远的座位上。如果有多个这样的座位,他会坐在编号
最小的座位上。(另外,如果考场里没有人,那么学生就坐在 0 号座位上。)
设计一个模拟所述考场的类。
实现 ExamRoom 类:
ExamRoom(int n)用座位的数量n初始化考场对象。int seat()返回下一个学生将会入座的座位编号。void leave(int p)指定坐在座位p的学生将离开教室。保证座位p上会有一位学生。
示例 1
- 输入 :
"ExamRoom", "seat", "seat", "seat", "seat", "leave", "seat"
\[10\], \[\], \[\], \[\], \[\], \[4\], \[\]
- 输出 :`null`, 0, 9, 4, 2, `null`, 5
- 解释 :
ExamRoom examRoom = new ExamRoom(10);
examRoom.seat(); // 返回 0,房间里没有人,学生坐在 0 号座位。
examRoom.seat(); // 返回 9,学生最后坐在 9 号座位。
examRoom.seat(); // 返回 4,学生最后坐在 4 号座位。
examRoom.seat(); // 返回 2,学生最后坐在 2 号座位。
examRoom.leave(4);
examRoom.seat(); // 返回 5,学生最后坐在 5 号座位。
提示: 1 <= n <= 10^9- 保证有学生正坐在座位
p上 seat和leave最多被调用10^4次
解题思路
学生总是坐到「距离最近已坐者最远」的位置,等价于把空座位按相连区间维护,每次选择最佳
区间。由于 n 可达 10^9,不能按座位逐位扫描。
解法一(有序集合 + 哈希表,主提交):用 TreeSet 存所有「空区间」,区间 [l, r)
(-1 与 n 表示墙,两边端点在外侧空区间),比较器先比「该区间能提供的到最近者的距离
dist」降序、再比左端点升序。哈希表 left/right 分别记下:以 l 为左端点的区间的
右端点、以 r 为右端点的区间的左端点。seat() 取堆顶区间,坐到 dist 最大的位置
(端点区间坐到 0 或 n-1),拆成两个子区间;leave(p) 用两个哈希表找到 p 左右两个
区间并合并。所有操作 O(log n)。
解法二(有序集合存已坐者,对照实现):用 TreeSet<Integer> 存放当前已坐的座位,
seat() 时枚举相邻已坐者(含两侧墙 -1 与 n)求最大间隙;实现直观,但一次 seat 需要
O(n) 遍历全部已坐座位,仅适合小规模数据。
代码实现
java
import java.util.HashMap;
import java.util.Map;
import java.util.TreeSet;
class ExamRoom {
private TreeSet<int[]> ts; // 空区间集合,key 为左端点
private Map<Integer, Integer> left; // 区间左端点 -> 右端点
private Map<Integer, Integer> right;// 区间右端点 -> 左端点
private int n;
private int dist(int[] s) {
int l = s[0], r = s[1];
// 端点区间(贴着墙)能提供的最大距离 = 区间长度;中间区间 = 中点距离
return l == -1 || r == n ? r - l - 1 : (r - l) >> 1;
}
public ExamRoom(int n) {
this.n = n;
ts = new TreeSet<>((a, b) -> {
int d1 = dist(a), d2 = dist(b);
return d1 != d2 ? Integer.compare(d2, d1) : Integer.compare(a[0], b[0]);
});
left = new HashMap<>();
right = new HashMap<>();
add(new int[]{-1, n});
}
public int seat() {
int[] s = ts.first();
int l = s[0], r = s[1];
int p = (l + r) >> 1;
if (l == -1) {
p = 0;
} else if (r == n) {
p = n - 1;
}
del(s);
add(new int[]{l, p});
add(new int[]{p, r});
return p;
}
public void leave(int p) {
int l = right.get(p); // p 左侧空区间的左端点
int r = left.get(p); // p 右侧空区间的右端点
del(new int[]{l, p});
del(new int[]{p, r});
add(new int[]{l, r});
}
private void add(int[] s) {
ts.add(s);
left.put(s[0], s[1]);
right.put(s[1], s[0]);
}
private void del(int[] s) {
ts.remove(s);
left.remove(s[0]);
right.remove(s[1]);
}
// 解法二:有序集合存已坐者,枚举相邻座位找最大间隙(对照实现)
// 设计题两种实现分开写,避免字段冲突
static class ExamRoom2 {
private final TreeSet<Integer> seated;
private final int n;
public ExamRoom2(int n) {
this.n = n;
seated = new TreeSet<>();
seated.add(-1);
seated.add(n);
}
public int seat() {
int best = -1;
int bestDist = -1;
Integer prev = null;
for (int cur : seated) {
if (prev == null) {
prev = cur;
continue;
}
// prev 与 cur 之间的空区间 [prev+1, cur-1]
int d;
int p;
if (prev == -1) {
p = 0;
d = cur; // 到 0 号位的距离
} else if (cur == n) {
p = n - 1;
d = n - 1 - prev;
} else {
p = (prev + cur) >> 1;
d = Math.min(p - prev, cur - p);
}
if (d > bestDist || (d == bestDist && p < best)) {
bestDist = d;
best = p;
}
prev = cur;
}
seated.add(best);
return best;
}
public void leave(int p) {
seated.remove(p);
}
}
// ===== 本地测试 =====
public static void main(String[] args) {
int pass = 0, total = 0;
{ // 官方示例
ExamRoom room = new ExamRoom(10);
ExamRoom2 room2 = new ExamRoom2(10);
int[] expects = {0, 9, 4, 2};
int[] acts = new int[4], acts2 = new int[4];
for (int i = 0; i < 4; ++i) {
acts[i] = room.seat();
acts2[i] = room2.seat();
}
++total;
if (java.util.Arrays.equals(acts, expects) && java.util.Arrays.equals(acts2, expects)) {
++pass;
} else {
System.out.println("FAIL 855-1: " + java.util.Arrays.toString(acts));
}
room.leave(4);
room2.leave(4);
++total;
if (room.seat() == 5 && room2.seat() == 5) {
++pass;
} else {
System.out.println("FAIL 855-2: got " + room.seat());
}
}
{ // n=1
ExamRoom room = new ExamRoom(1);
++total;
if (room.seat() == 0) {
++pass;
} else {
System.out.println("FAIL 855-3a");
}
room.leave(0);
++total;
if (room.seat() == 0) {
++pass;
} else {
System.out.println("FAIL 855-3b");
}
}
{ // 连续 seat 到满 (n=8)
ExamRoom room = new ExamRoom(8);
int[] expects = {0, 7, 3, 5, 1, 2, 4, 6};
int[] acts = new int[8];
for (int i = 0; i < 8; ++i) {
acts[i] = room.seat();
}
++total;
if (java.util.Arrays.equals(acts, expects)) {
++pass;
} else {
System.out.println("FAIL 855-4: " + java.util.Arrays.toString(acts));
}
room.leave(0);
++total;
if (room.seat() == 0) {
++pass;
} else {
System.out.println("FAIL 855-5: got " + room.seat());
}
}
{ // 交替坐/离(混合操作)
ExamRoom room = new ExamRoom(6);
int[] seq1 = {room.seat(), room.seat()};
++total;
if (seq1[0] == 0 && seq1[1] == 5) {
++pass;
} else {
System.out.println("FAIL 855-6: " + java.util.Arrays.toString(seq1));
}
room.leave(0);
++total;
if (room.seat() == 0) {
++pass;
} else {
System.out.println("FAIL 855-7");
}
}
if (pass == total) {
System.out.println("== 0855 PASS ==");
} else {
System.out.println("== 0855 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
| --- | | --- |
856. Score of Parentheses(括号的分数)
题目描述
给定一个平衡括号字符串 s,按下述规则计算该字符串的分数:
()得 1 分。AB得 A + B 分,其中 A 和 B 是平衡括号字符串。(A)得 2 * A 分,其中 A 是平衡括号字符串。
示例 1
- 输入:"()"
- 输出:1
示例 2
- 输入:"(())"
- 输出:2
示例 3
- 输入:"()()"
- 输出:2
示例 4
- 输入:"(()(()))"
- 输出 :6
提示: s是平衡括号字符串,且只含有(和)2 <= s.length <= 50
解题思路
括号串天然形成嵌套结构,可用栈模拟"分数相加"。每个分数在括号闭合时翻倍。
解法一(栈,主提交):栈中保存「当前层累加的分数」。遇到 ( 压入 0 作为新层;
遇到 ) 弹出顶层分数 v:若 v == 0 表示 (),本层贡献 1;否则翻倍 2 * v。
把这一贡献加到新的栈顶(父层),若栈空则把该值累加到答案。所有括号遍历完即得总分。
解法二(深度计数):() 是最基本元素,其分数贡献为 1 << depth,其中 depth 是该
括号所在的嵌套深度。遍历时维护当前深度,遇到 () 直接累加 1 << depth,线性时间、常数空间。
代码实现
java
import java.util.ArrayDeque;
import java.util.Deque;
class Solution {
// 解法一:栈(推荐提交)
public int scoreOfParentheses(String s) {
Deque<Integer> stack = new ArrayDeque<>();
int ans = 0;
for (char c : s.toCharArray()) {
if (c == '(') {
stack.push(0);
} else {
int v = stack.pop();
if (v == 0) {
v = 1; // () 得 1 分
} else {
v <<= 1; // (A) 得 2 * A 分
}
if (stack.isEmpty()) {
ans += v;
} else {
stack.push(stack.pop() + v); // AB 得 A + B
}
}
}
return ans;
}
// 解法二:深度计数(对照实现)
public int scoreOfParentheses2(String s) {
int ans = 0;
int depth = 0;
for (int i = 0; i < s.length(); ++i) {
char c = s.charAt(i);
if (c == '(') {
++depth;
} else {
--depth;
if (i > 0 && s.charAt(i - 1) == '(') {
ans += 1 << depth; // 基本元素 () 的贡献
}
}
}
return ans;
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
String[] cs = {"()", "(())", "()()", "(()(()))", "((()))", "()(())"};
int[] exp = {1, 2, 2, 6, 4, 3};
for (int t = 0; t < cs.length; ++t) {
++total;
if (s.scoreOfParentheses(cs[t]) == exp[t] && s.scoreOfParentheses2(cs[t]) == exp[t]) {
++pass;
} else {
System.out.println("FAIL 856-" + (t + 1) + ": got "
+ s.scoreOfParentheses(cs[t]) + " exp " + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0856 PASS ==");
} else {
System.out.println("== 0856 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n) | O(n) |
| 解法二 | O(n) | O(1) |
857. Minimum Cost to Hire K Workers(雇佣 K 名工人的最低成本)
题目描述
有 n 名工人。给定两个数组 quality 和 wage,其中 quality[i] 表示第 i 名工人的工作
质量,其最低期望工资为 wage[i]。
现在我们想雇佣 k 名工人组成一个工资组。在雇佣一组 k 名工人时,我们必须按照下述规则
向他们支付工资:
- 对工资组中的每名工人,应当按其工作质量与同组其他工人的工作质量的比例来支付工资。
- 工资组中的每名工人至少应当得到他们的最低期望工资。
给定整数k,返回组成满足上述条件的付费群体所需的最小金额。与实际答案误差相差在
10^-5以内的答案将被接受。
示例 1
- 输入 :quality = 10,20,5, wage = 70,50,30,
k= 2 - 输出:105.00000
- 解释:我们向 0 号工人支付 70,向 2 号工人支付 35。
示例 2
- 输入 :quality = 3,1,10,10,1, wage = 4,8,2,2,7,
k= 3 - 输出:30.66667
- 解释 :我们向 0 号工人支付 4,向 2 号和 3 号分别支付 13.33333。
提示: n == quality.length == wage.length1 <= k <= n <= 10^41 <= quality[i], wage[i] <= 10^4
解题思路
若以某名工人 i 为「工资基准」(按比例支付时恰好等于其最低期望工资),则组内任意工人 j
的工资为 wage[i] / quality[i] * quality[j],且该值必须不小于 wage[j],即
wage[j] / quality[j] <= wage[i] / quality[i]。因此把工人按 ratio = wage / quality
升序排序后,最优组的「基准」一定是组内 ratio 最大的人,组内其余人只需在前缀里取。
解法一(排序 + 大根堆,主提交):按 ratio 升序排序后从左到右扫描,把 quality 累加进
大根堆,堆内始终保留扫描到的前 k 小质量;堆满 k 个时,用「当前基准的 ratio × 质量总和」
计算以当前工人为基准的组成本,取最小值。扫描完即得全局最优。
解法二(暴力枚举基准 + 前缀选 k 小质量,对照实现):对每个工人作为基准,在前 i 个工人中
选质量最小的 k - 1 个(当 i >= k - 1),计算总成本取最小。思路最直观,但每次选择需要
排序或堆,复杂度 O(n^2 log n) 量级,仅适合小数据。
代码实现
java
import java.util.Arrays;
import java.util.PriorityQueue;
class Solution {
// 解法一:排序 + 大根堆贪心(推荐提交)
public double mincostToHireWorkers(int[] quality, int[] wage, int k) {
int n = quality.length;
Integer[] idx = new Integer[n];
for (int i = 0; i < n; ++i) {
idx[i] = i;
}
// 按"单位质量期望工资"升序排序
Arrays.sort(idx, (a, b) -> Double.compare(
(double) wage[a] / quality[a], (double) wage[b] / quality[b]));
double ans = Double.MAX_VALUE;
int qSum = 0;
// 大根堆:保留已扫描工人的最大质量,便于弹出最大的以保留 k 个最小质量
PriorityQueue<Integer> pq = new PriorityQueue<>((a, b) -> Integer.compare(b, a));
for (int i = 0; i < n; ++i) {
int qi = quality[idx[i]];
pq.offer(qi);
qSum += qi;
if (pq.size() > k) {
qSum -= pq.poll();
}
if (pq.size() == k) {
double ratio = (double) wage[idx[i]] / quality[idx[i]];
ans = Math.min(ans, ratio * qSum);
}
}
return ans;
}
// 解法二:枚举基准 + 堆选 k-1 小质量(对照实现)
public double mincostToHireWorkers2(int[] quality, int[] wage, int k) {
int n = quality.length;
Integer[] idx = new Integer[n];
for (int i = 0; i < n; ++i) {
idx[i] = i;
}
Arrays.sort(idx, (a, b) -> Double.compare(
(double) wage[a] / quality[a], (double) wage[b] / quality[b]));
double ans = Double.MAX_VALUE;
for (int t = 0; t < n; ++t) {
int base = idx[t];
double ratio = (double) wage[base] / quality[base];
if (t + 1 < k) {
continue; // 前缀人数不足 k 人
}
// 在前 t 个工人(不含基准)中选质量最小的 k-1 个
PriorityQueue<Integer> pq = new PriorityQueue<>(); // 小根堆
for (int j = 0; j < t; ++j) {
pq.offer(quality[idx[j]]);
}
long qSum = quality[base];
for (int c = 0; c < k - 1; ++c) {
qSum += pq.poll();
}
ans = Math.min(ans, ratio * qSum);
}
return ans;
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
double eps = 1e-5;
++total;
if (Math.abs(s.mincostToHireWorkers(new int[]{10, 20, 5}, new int[]{70, 50, 30}, 2) - 105.0) < eps) {
++pass;
} else {
System.out.println("FAIL 857-1: " + s.mincostToHireWorkers(new int[]{10, 20, 5}, new int[]{70, 50, 30}, 2));
}
++total;
if (Math.abs(s.mincostToHireWorkers(new int[]{3, 1, 10, 10, 1}, new int[]{4, 8, 2, 2, 7}, 3) - 30.66667) < 1e-3) {
++pass;
} else {
System.out.println("FAIL 857-2: " + s.mincostToHireWorkers(new int[]{3, 1, 10, 10, 1}, new int[]{4, 8, 2, 2, 7}, 3));
}
++total;
if (Math.abs(s.mincostToHireWorkers(new int[]{2, 1, 3}, new int[]{3, 2, 4}, 2) - 6.0) < eps) {
++pass;
} else {
System.out.println("FAIL 857-3: " + s.mincostToHireWorkers(new int[]{2, 1, 3}, new int[]{3, 2, 4}, 2));
}
++total;
if (Math.abs(s.mincostToHireWorkers2(new int[]{10, 20, 5}, new int[]{70, 50, 30}, 2) - 105.0) < eps
&& Math.abs(s.mincostToHireWorkers2(new int[]{3, 1, 10, 10, 1}, new int[]{4, 8, 2, 2, 7}, 3) - 30.66667) < 1e-3) {
++pass;
} else {
System.out.println("FAIL 857-4");
}
if (pass == total) {
System.out.println("== 0857 PASS ==");
} else {
System.out.println("== 0857 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n log n) | O(n) |
| 解法二 | O(n^2 log n) | O(n) |
858. Mirror Reflection(镜面反射)
题目描述
有一个特殊的正方形房间,每面墙上都有一面镜子。除西南角以外,每个角落都放有一个接受器,
编号为 0、1 和 2。
正方形房间的墙壁长度为 p,一束激光从西南角射出,首先会与东墙相遇,入射点到射入点的
距离为 q(即激光在竖直方向上走了 q 个单位)。
返回光线最先到达的接受器的编号(保证光线最终会到达一个接收器)。
示例 1
- 输入 :
p= 2,q= 1 - 输出:2
- 解释 :此射线在反射后第一次到达接受器 2(东北角)。
提示: 1 <= q <= p <= 1000
解题思路
把镜子反射"展开":光线进入无数个上下、左右堆叠的镜像房间,横向坐标在 0 与 p 之间
来回折返,纵向整体持续上升 q。光线第 k 次撞墙时纵向坐标为 k * q,能碰到某个接收器
当且仅当纵向坐标是 p 的整数倍。
解法一(数学法,主提交):令 g = gcd(p, q),设 pp = (p/g) % 2、qq = (q/g) % 2
(p、q 同除最大公约数后看奇偶):
pp == 1 && qq == 1:到达左侧角落,接收器1;pp == 1:到达左侧墙且纵向在墙中(上下对称位置),接收器0;- 否则(
pp == 0):到达右侧角落,接收器2。
解法二(展开模拟,对照实现):把房间横向宽度翻倍为2p,直接由k计算
x = (p * k) % (2p)、y = (q * k) % (2p),若y == p(到达南北墙)且x == p则接收器
1,x == 0则接收器2;若y == 0且x == p则接收器0。最多迭代2p次。
代码实现
java
class Solution {
// 解法一:数学(约分看奇偶,推荐提交)
public int mirrorReflection(int p, int q) {
int g = gcd(p, q);
int pp = (p / g) % 2;
int qq = (q / g) % 2;
if (pp == 1 && qq == 1) {
return 1;
}
return pp == 1 ? 0 : 2;
}
private int gcd(int a, int b) {
return b == 0 ? a : gcd(b, a % b);
}
// 解法二:展开房间逐次模拟(对照实现)
public int mirrorReflection2(int p, int q) {
int mod = 2 * p;
for (int k = 1; k <= mod * 2; ++k) {
int x = (p * k) % mod;
int y = (q * k) % mod;
if (y == p) {
if (x == p) {
return 1;
}
if (x == 0) {
return 2;
}
} else if (y == 0) {
if (x == p) {
return 0;
}
}
if (x == 0 && y == 0) {
continue; // 回到起点(西南角),继续
}
}
return -1;
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
int[][] cases = {{2, 1}, {3, 1}, {3, 2}, {4, 2}, {5, 3}, {1000, 999}};
int[] exp = {2, 1, 0, 2, 1, 2};
for (int t = 0; t < cases.length; ++t) {
++total;
int g1 = s.mirrorReflection(cases[t][0], cases[t][1]);
int g2 = s.mirrorReflection2(cases[t][0], cases[t][1]);
if (g1 == exp[t] && g2 == exp[t]) {
++pass;
} else {
System.out.println("FAIL 858-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0858 PASS ==");
} else {
System.out.println("== 0858 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(log min(p, q)) | O(1) |
| 解法二 | O(p / gcd(p, q)) | O(1) |
859. Buddy Strings(亲密字符串)
题目描述
给你两个字符串 s 和 goal,只要我们可以通过交换 s 中的两个字母得到与 goal 相等
的结果,就返回 true;否则返回 false。
交换字母的定义是:取两个下标 i 和 j(下标从 0 开始)且满足 i != j,接着交换
s[i] 和 s[j]。
例如,在 "abcd" 中交换下标 0 和下标 2 的元素可以产生 "cbad"。
示例 1
- 输入 :
s= "ab", goal = "ba" - 输出 :
true
示例 2
- 输入 :
s= "ab", goal = "ab" - 输出 :
false
示例 3
- 输入 :
s= "aa", goal = "aa" - 输出 :
true
提示: 1 <= s.length, goal.length <= 2 * 10^4s和goal由小写英文字母组成
解题思路
按 s 与 goal 是否已经相等分两种情况。
解法一(分类讨论,主提交):若两串长度不等直接返回 false。
- 若
s == goal:需要交换两个不同下标且交换后不变,即存在某个字符出现至少两次,
s中有一对相同字符可交换,返回 true,否则 false。 - 若
s != goal:统计所有s[i] != goal[i]的下标。恰好两个位置不同,且
s[i] == goal[j] && s[j] == goal[i]才可由一次交换成功。
时间复杂度 O(n),空间 O(1)。
解法二(计数比对,对照实现):先检查长度相等与任一字符是否重复;再用两个长度 26 的
计数器分别统计s与goal的字符频次,若不相似则某些频次不同直接返回 false;最后按
不同位置的个数判断:为 0 时需有重复字符,为 2 时需互为"换位"。实现略繁琐但思路与
解法一一致,作为验证交叉。
代码实现
java
class Solution {
// 解法一:分类讨论(推荐提交)
public boolean buddyStrings(String s, String goal) {
int n = s.length();
if (n != goal.length()) {
return false;
}
if (s.equals(goal)) {
int[] cnt = new int[26];
for (int i = 0; i < n; ++i) {
int c = s.charAt(i) - 'a';
if (++cnt[c] >= 2) {
return true;
}
}
return false;
}
int first = -1, second = -1;
for (int i = 0; i < n; ++i) {
if (s.charAt(i) != goal.charAt(i)) {
if (first == -1) {
first = i;
} else if (second == -1) {
second = i;
} else {
return false; // 超过两处不同
}
}
}
return second != -1
&& s.charAt(first) == goal.charAt(second)
&& s.charAt(second) == goal.charAt(first);
}
// 解法二:计数比对(对照实现)
public boolean buddyStrings2(String s, String goal) {
int n = s.length();
if (n != goal.length()) {
return false;
}
int[] cs = new int[26];
int[] cg = new int[26];
boolean dup = false;
for (int i = 0; i < n; ++i) {
if (++cs[s.charAt(i) - 'a'] >= 2) {
dup = true;
}
++cg[goal.charAt(i) - 'a'];
}
for (int i = 0; i < 26; ++i) {
if (cs[i] != cg[i]) {
return false;
}
}
int diff = 0;
for (int i = 0; i < n; ++i) {
if (s.charAt(i) != goal.charAt(i)) {
++diff;
}
}
return diff == 2 || (diff == 0 && dup);
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
String[][] cases = {
{"ab", "ba"}, {"ab", "ab"}, {"aa", "aa"}, {"aaaaaaabc", "aaaaaaacb"},
{"", ""}, {"abcaa", "abcbb"}, {"a", "a"}, {"ab", "ab"}
};
boolean[] exp = {true, false, true, true, false, false, false, false};
for (int t = 0; t < cases.length; ++t) {
++total;
if (s.buddyStrings(cases[t][0], cases[t][1]) == exp[t]
&& s.buddyStrings2(cases[t][0], cases[t][1]) == exp[t]) {
++pass;
} else {
System.out.println("FAIL 859-" + (t + 1) + ": got "
+ s.buddyStrings(cases[t][0], cases[t][1]) + " exp " + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0859 PASS ==");
} else {
System.out.println("== 0859 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n) | O(1) |
| 解法二 | O(n) | O(1) |
860. Lemonade Change(柠檬水找零)
题目描述
在柠檬水摊上,每一杯柠檬水的售价为 5 美元。顾客排队购买你的产品,(按账单 bills
支付的顺序)一次购买一杯。
每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客
正确找零,也就是说净交易是每位顾客向你支付 5 美元。
注意,一开始你手头没有任何零钱。
给你一个整数数组 bills,其中 bills[i] 是第 i 位顾客付的账。如果你能给每位顾客正确
找零,返回 true,否则返回 false。
示例 1
- 输入:bills = 5,5,5,10,20
- 输出 :
true - 解释 :
前 3 位顾客那里,我们按顺序收取 3 张 5 美元的钞票。
第 4 位顾客那里,我们收取一张 10 美元的钞票,并找回 5 美元。
第 5 位顾客那里,我们找还一张 10 美元的钞票和一张 5 美元的钞票。
由于所有客户都得到了正确的找零,所以我们输出true。
示例 2
- 输入:bills = 5,5,10,10,20
- 输出 :
false - 解释 :
前 2 位顾客那里,我们按顺序收取 2 张 5 美元的钞票。
对于接下来的 2 位顾客,我们收取一张 10 美元的钞票,然后找回 5 美元。
对于最后一位顾客,我们无法退回 15 美元,因为我们现在只有两张 10 美元的钞票。
由于不是每位顾客都得到了正确的找零,所以答案是false。
提示: 1 <= bills.length <= 10^5bills[i]不是5就是10或是20
解题思路
找零时永远优先花掉大面额(10 美元),把 5 美元留给更需要的场合,这是最优贪心策略。
解法一(贪心模拟,主提交):维护 five、ten 两张面额的张数。
- 收 5 元:
five++; - 收 10 元:必须有 5 元,
five--,ten++; - 收 20 元:优先
ten--, five--(找 15),否则five -= 3,任何一档不足即返回 false。
20 元面值无法找回,只进不出。
解法二(计数数组递推,对照实现):用长度为 3 的数组计数。无论哪种找零顺序,核心仍是
20 元场景先花 10 元;该写法把"优先花大额"显式表达为分支条件,逻辑等价。
代码实现
java
class Solution {
// 解法一:贪心模拟(推荐提交)
public boolean lemonadeChange(int[] bills) {
int five = 0, ten = 0;
for (int b : bills) {
if (b == 5) {
++five;
} else if (b == 10) {
if (five == 0) {
return false;
}
--five;
++ten;
} else { // 20
if (ten > 0 && five > 0) {
--ten;
--five;
} else if (five >= 3) {
five -= 3;
} else {
return false;
}
}
}
return true;
}
// 解法二:计数数组(对照实现)
public boolean lemonadeChange2(int[] bills) {
int[] cnt = new int[3]; // 下标 0 -> 5 元,1 -> 10 元,2 -> 20 元
for (int b : bills) {
if (b == 5) {
++cnt[0];
} else if (b == 10) {
if (cnt[0] == 0) {
return false;
}
--cnt[0];
++cnt[1];
} else {
if (cnt[1] > 0 && cnt[0] > 0) {
--cnt[1];
--cnt[0];
} else if (cnt[0] >= 3) {
cnt[0] -= 3;
} else {
return false;
}
}
}
return true;
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
int[][] billsArr = {
{5, 5, 5, 10, 20},
{5, 5, 10, 10, 20},
{5, 5, 5, 20},
{5, 10, 5, 20},
{20},
{5}
};
boolean[] exp = {true, false, true, true, false, true};
for (int t = 0; t < billsArr.length; ++t) {
++total;
if (s.lemonadeChange(billsArr[t]) == exp[t]
&& s.lemonadeChange2(billsArr[t]) == exp[t]) {
++pass;
} else {
System.out.println("FAIL 860-" + (t + 1) + ": got "
+ s.lemonadeChange(billsArr[t]) + " exp " + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0860 PASS ==");
} else {
System.out.println("== 0860 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n) | O(1) |
| 解法二 | O(n) | O(1) |
861. Score After Flipping Matrix(翻转矩阵后的得分)
题目描述
给你一个大小为 m x n 的二元矩阵 grid,其中每个单元格的值为 0 或 1(下标从 0 开始
编号)。我们可以按下面的规则进行任意次操作:
- 选择一行翻转:这一行的
1变成0,0变成1。 - 选择一列翻转:这一列的
1变成0,0变成1。
每行解释为一个二进制数(从最高有效位开始),矩阵的得分是所有这些数字的总和。
在执行任意次操作后,返回矩阵可能获得的最高分数。
示例 1
- 输入 :
grid= \[0,0,1,1,1,0,1,0,1,1,0,0] - 输出:39
- 解释 :
0b1111 + 0b1001 + 0b1011 = 15 + 9 + 11 = 39
示例 2
- 输入 :
grid= \[0] - 输出 :1
提示: m == grid.lengthn == grid[i].length1 <= m, n <= 20grid[i][j]不是0就是1
解题思路
最高位(每行首位)的权重比该行其余所有位加起来还大,因此必然先把每行首位翻成 1;
之后列之间互不影响,对每一列,尽量让 1 的数量更多(若 1 少于 0 就翻该列)。
解法一(贪心,主提交):第一遍:对每行,若 grid[i][0] == 0 则整行翻转(等价于仅作
行翻转操作,不必真实翻转,先计算时直接把该行整体取反处理)。第二遍:对每列统计
grid[i][j] == 1(行首为 0 的行按取反计)的个数,取 max(ones, m - ones) 贡献到答案。
不实际修改矩阵,直接用公式累加:ans = sum over j of max(ones_j, m - ones_j) << (n-1-j)。
解法二(按位直接求解,对照实现):由于每列独立且可翻转,先把数组复制一份并真正执行
行翻转,再用计数求列最优;实现直接、便于对照,但多一次拷贝。
代码实现
java
class Solution {
// 解法一:贪心(不实际翻转,直接计算,推荐提交)
public int matrixScore(int[][] grid) {
int m = grid.length, n = grid[0].length;
int ans = 0;
for (int j = 0; j < n; ++j) {
int ones = 0;
for (int i = 0; i < m; ++i) {
// 若第 i 行首位是 0,该行会被翻转,因此该格实际值取反
ones += (grid[i][0] ^ grid[i][j]); // grid[i][0]==1 时按原值,==0 时按取反
}
ans += Math.max(ones, m - ones) << (n - 1 - j);
}
return ans;
}
// 解法二:真实翻转后统计(对照实现)
public int matrixScore2(int[][] grid) {
int m = grid.length, n = grid[0].length;
int[][] g = new int[m][n];
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
g[i][j] = grid[i][0] == 1 ? grid[i][j] : (1 - grid[i][j]);
}
}
int ans = 0;
for (int j = 0; j < n; ++j) {
int ones = 0;
for (int i = 0; i < m; ++i) {
ones += g[i][j];
}
if (ones * 2 < m) { // 该列 1 少,翻转整列
for (int i = 0; i < m; ++i) {
g[i][j] = 1 - g[i][j];
}
ones = m - ones;
}
ans += ones << (n - 1 - j);
}
return ans;
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
++total;
if (s.matrixScore(new int[][]{{0, 0, 1, 1}, {1, 0, 1, 0}, {1, 1, 0, 0}}) == 39) {
++pass;
} else {
System.out.println("FAIL 861-1: " + s.matrixScore(new int[][]{{0, 0, 1, 1}, {1, 0, 1, 0}, {1, 1, 0, 0}}));
}
++total;
if (s.matrixScore(new int[][]{{0}}) == 1) {
++pass;
} else {
System.out.println("FAIL 861-2");
}
++total;
if (s.matrixScore(new int[][]{{0, 1}, {1, 1}}) == 5) {
++pass;
} else {
System.out.println("FAIL 861-3: " + s.matrixScore(new int[][]{{0, 1}, {1, 1}}));
}
++total;
if (s.matrixScore2(new int[][]{{0, 0, 1, 1}, {1, 0, 1, 0}, {1, 1, 0, 0}}) == 39
&& s.matrixScore2(new int[][]{{0}}) == 1) {
++pass;
} else {
System.out.println("FAIL 861-4");
}
if (pass == total) {
System.out.println("== 0861 PASS ==");
} else {
System.out.println("== 0861 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(m n) | O(1) |
| 解法二 | O(m n) | O(m n) |
862. Shortest Subarray with Sum at Least K(和至少为 K 的最短子数组)
题目描述
给你一个整数数组 nums 和一个整数 k,找出 nums 中和至少为 k 的最短非空子数组,
并返回该子数组的长度。如果不存在这样的子数组,返回 -1。
子数组是数组中连续的一部分。
示例 1
- 输入 :
nums= 1,k= 1 - 输出:1
示例 2
- 输入 :
nums= 1,2,k= 4 - 输出:-1
示例 3
- 输入 :
nums= 2,-1,2,k= 3 - 输出 :3
提示: 1 <= nums.length <= 10^5-10^5 <= nums[i] <= 10^51 <= k <= 10^9
解题思路
和至少为 k 的子数组 [i, j] 等价于前缀和满足 pre[j] - pre[i] >= k,其中 i < j。
由于 nums 含负数,前缀和不单调,不能直接双指针,需要用单调队列维护候选起点。
解法一(前缀和 + 单调递增队列,主提交):构造前缀和数组 pre(pre[0] = 0)。遍历
j 从 0 到 n:
- 队首
i满足pre[j] - pre[i] >= k时,更新答案j - i,并把i弹出(i已经不会
为更远的j提供更短答案); - 维护队列前缀和严格递增:新下标
j入队前,弹出所有pre[j] <= pre[队尾]的元素
(队尾若不如j"小",永远不会成为更优起点)。
队列操作均摊 O(1),总复杂度 O(n)。
解法二(暴力前缀枚举,对照实现):枚举所有(i, j)对检查前缀和差,O(n^2) 仅作思路
正确性参考,不适合大数据(本地仅用小用例验证)。
代码实现
java
import java.util.ArrayDeque;
import java.util.Deque;
class Solution {
// 解法一:前缀和 + 单调递增队列(推荐提交)
public int shortestSubarray(int[] nums, int k) {
int n = nums.length;
long[] pre = new long[n + 1];
for (int i = 0; i < n; ++i) {
pre[i + 1] = pre[i] + nums[i];
}
Deque<Integer> dq = new ArrayDeque<>(); // 存前缀和下标,pre 严格递增
int ans = Integer.MAX_VALUE;
for (int j = 0; j <= n; ++j) {
while (!dq.isEmpty() && pre[j] - pre[dq.peekFirst()] >= k) {
ans = Math.min(ans, j - dq.pollFirst());
}
while (!dq.isEmpty() && pre[j] <= pre[dq.peekLast()]) {
dq.pollLast();
}
dq.offerLast(j);
}
return ans == Integer.MAX_VALUE ? -1 : ans;
}
// 解法二:暴力前缀枚举(对照实现,仅小数据)
public int shortestSubarray2(int[] nums, int k) {
int n = nums.length;
long[] pre = new long[n + 1];
for (int i = 0; i < n; ++i) {
pre[i + 1] = pre[i] + nums[i];
}
int ans = Integer.MAX_VALUE;
for (int i = 0; i <= n; ++i) {
for (int j = i + 1; j <= n; ++j) {
if (pre[j] - pre[i] >= k) {
ans = Math.min(ans, j - i);
break; // 更长的同起点不会更优
}
}
}
return ans == Integer.MAX_VALUE ? -1 : ans;
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
int[][] numsArr = {{1}, {1, 2}, {2, -1, 2}, {84, -37, 32, 40, 95}, {-1, -1, 1}, {17, 85, 93, -45, -21}};
int[] kArr = {1, 4, 3, 167, 1, 150};
int[] exp = {1, -1, 3, 3, 1, 2};
for (int t = 0; t < numsArr.length; ++t) {
++total;
int g1 = s.shortestSubarray(numsArr[t], kArr[t]);
int g2 = s.shortestSubarray2(numsArr[t], kArr[t]);
if (g1 == exp[t] && g2 == exp[t]) {
++pass;
} else {
System.out.println("FAIL 862-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0862 PASS ==");
} else {
System.out.println("== 0862 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n) | O(n) |
| 解法二 | O(n^2) | O(n) |
863. All Nodes Distance K in Binary Tree(二叉树中所有距离为 K 的结点)
题目描述
给定一个二叉树(具有根结点 root),一个目标结点 target,和一个整数值 k。
返回到目标结点 target 距离为 k 的所有结点的值的列表。答案可以以任何顺序返回。
示例 1
- 输入 :
root= 3,5,1,6,2,0,8,`null`,`null`,7,4,target= 5,k= 2 - 输出:7,4,1
- 解释 :所求结点为与目标结点(值为 5)距离为 2 的结点,值分别为 7、4 以及 1。
提示: - 树中节点数在
[1, 500]范围内 0 <= Node.val <= 500- 树中每个节点的值互不相同
- 目标节点
target是树上的节点 0 <= k <= 1000
解题思路
距离为 k 的结点可能分布在 target 的子树内,也可能通过父节点走到其他分支,因此需先建立
从子到父的映射,再以 target 为起点做广度优先搜索。
解法一(父指针 + BFS,主提交):先用一次 DFS 记录每个结点的父节点;从 target 出发做 BFS,
记录步数,第 k 层的所有结点即为答案。用 (node, distance) 入队并记录已访问。
解法二(DFS 求路径 + 向下计数,对照实现):找到从 root 到 target 的路径,路径上的每个
祖先结点到 target 的距离为 d;对相隔 d 的祖先,其"另一侧"子树中距离该祖先恰为
k - d 的结点加入答案(注意不要重复经过路径上的下一个结点)。
代码实现
java
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Queue;
class Solution {
// 解法一:父指针 + BFS(推荐提交)
public List<Integer> distanceK(TreeNode root, TreeNode target, int k) {
Map<TreeNode, TreeNode> parent = new HashMap<>();
dfsParent(root, null, parent);
Queue<TreeNode> q = new ArrayDeque<>();
q.offer(target);
Map<TreeNode, Integer> seen = new HashMap<>();
seen.put(target, 0);
List<Integer> ans = new ArrayList<>();
while (!q.isEmpty()) {
TreeNode cur = q.poll();
int d = seen.get(cur);
if (d == k) {
ans.add(cur.val);
continue;
}
TreeNode[] nxt = {cur.left, cur.right, parent.get(cur)};
for (TreeNode nb : nxt) {
if (nb != null && !seen.containsKey(nb)) {
seen.put(nb, d + 1);
q.offer(nb);
}
}
}
return ans;
}
private void dfsParent(TreeNode node, TreeNode par, Map<TreeNode, TreeNode> parent) {
if (node == null) {
return;
}
if (par != null) {
parent.put(node, par);
}
dfsParent(node.left, node, parent);
dfsParent(node.right, node, parent);
}
// 解法二:路径记录 + 祖先侧子树收集(对照实现)
public List<Integer> distanceK2(TreeNode root, TreeNode target, int k) {
List<TreeNode> path = new ArrayList<>();
findPath(root, target, path);
List<Integer> ans = new ArrayList<>();
int m = path.size();
for (int i = 0; i < m; ++i) {
TreeNode anc = path.get(i);
int distAnc = m - 1 - i; // anc 到 target 的距离
TreeNode blocked = i + 1 < m ? path.get(i + 1) : null; // 朝向 target 的子结点
collect(anc, blocked, k - distAnc, ans);
}
return ans;
}
private boolean findPath(TreeNode node, TreeNode target, List<TreeNode> path) {
if (node == null) {
return false;
}
path.add(node);
if (node == target) {
return true;
}
if (findPath(node.left, target, path) || findPath(node.right, target, path)) {
return true;
}
path.remove(path.size() - 1);
return false;
}
private void collect(TreeNode node, TreeNode blocked, int rem, List<Integer> ans) {
if (node == null || node == blocked) {
return;
}
if (rem == 0) {
ans.add(node.val);
return;
}
collect(node.left, blocked, rem - 1, ans);
collect(node.right, blocked, rem - 1, ans);
}
// 二叉树节点定义
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;
}
private static java.util.List<Integer> sortList(java.util.List<Integer> list) {
java.util.Collections.sort(list);
return list;
}
// ===== 本地测试 =====
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});
TreeNode target = find(root, 5);
List<Integer> got = sortList(new ArrayList<>(s.distanceK(root, target, 2)));
List<Integer> exp = new ArrayList<>(java.util.Arrays.asList(7, 4, 1));
java.util.Collections.sort(exp);
if (got.equals(exp)) {
++pass;
} else {
System.out.println("FAIL 863-1: " + got);
}
}
++total;
{
TreeNode root = build(new Integer[]{1});
TreeNode target = find(root, 1);
List<Integer> got = sortList(new ArrayList<>(s.distanceK(root, target, 3)));
if (got.equals(new ArrayList<>())) {
++pass;
} else {
System.out.println("FAIL 863-2: " + got);
}
}
++total;
{
TreeNode root = build(new Integer[]{3, 5, 1, 6, 2, 0, 8, null, null, 7, 4});
TreeNode target = find(root, 5);
List<Integer> got = sortList(new ArrayList<>(s.distanceK2(root, target, 2)));
List<Integer> exp = new ArrayList<>(java.util.Arrays.asList(7, 4, 1));
java.util.Collections.sort(exp);
if (got.equals(exp)) {
++pass;
} else {
System.out.println("FAIL 863-3: " + got);
}
}
++total;
{
TreeNode root = build(new Integer[]{1, 2, 3, 4, 5, 6});
TreeNode target = find(root, 2);
List<Integer> got = sortList(new ArrayList<>(s.distanceK(root, target, 1)));
List<Integer> exp = new ArrayList<>(java.util.Arrays.asList(1, 4, 5));
java.util.Collections.sort(exp);
if (got.equals(exp)) {
++pass;
} else {
System.out.println("FAIL 863-4: " + got);
}
}
if (pass == total) {
System.out.println("== 0863 PASS ==");
} else {
System.out.println("== 0863 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n) | O(n) |
| 解法二 | O(n) | O(n) |
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,本质上相同;此处通过自定义对象入队记录步数展示另一种状态组织方式。
代码实现
java
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。
代码实现
java
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。在题目范围内通常足够快,
但最坏情况可能多尝试一些数字,思路更直接。
代码实现
java
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
行。逻辑等价,便于理解列优先的视角。
代码实现
java
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。
代码实现
java
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 的幂的十进制
字符串排序比对,借助排序避免手写频次数组,思路等价。
代码实现
java
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)。
代码实现
java
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)。
代码实现
java
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,对照实现):用显式栈模拟先序遍历收集叶子,避免递归栈深度占用;
两棵树的叶子序列分别收集后比较。实现不依赖系统栈,思路与解法一相同。
代码实现
java
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
解题思路
斐波那契式子序列每新增一项由前两项决定,可以把状态定义为「以 (arri, arrj) 结尾的
子序列长度」,其中 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) 但实际很快;作为对照思路。
代码实现
java
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 编码略慢,仅作对照。
代码实现
java
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) 时很慢,仅作正确性对照。
代码实现
java
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) |