LeetCode 851-863. 题解合集
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 穷”的直接邻居的答案,邻居
入度归零后入队,逐步把最优信息传播到传播链末端。
代码实现
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] > arr[i + 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),实现直观但常数更差。
代码实现
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) 按位置升序排好,从终点
方向往回扫描,遇到「到达时间比右侧已记录的最大到达时间还大」的车就计为新车队。思路与
解法一相同,仅遍历顺序相反。
代码实现
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 求最少步数,
逻辑等价、代码更短,两种实现结果一致。
代码实现
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) 遍历全部已坐座位,仅适合小规模数据。
代码实现
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,线性时间、常数空间。
代码实现
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) 量级,仅适合小数据。
代码实现
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次。
代码实现
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 时需互为“换位”。实现略繁琐但思路与 解法一一致,作为验证交叉。
代码实现
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 元;该写法把“优先花大额”显式表达为分支条件,逻辑等价。
代码实现
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)。
解法二(按位直接求解,对照实现):由于每列独立且可翻转,先把数组复制一份并真正执行
行翻转,再用计数求列最优;实现直接、便于对照,但多一次拷贝。
代码实现
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) 仅作思路 正确性参考,不适合大数据(本地仅用小用例验证)。
代码实现
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 的结点加入答案(注意不要重复经过路径上的下一个结点)。
代码实现
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) |