LeetCode 2061-2080. 题解合集
文章目录
- [LeetCode 2061-2080. 题解合集](#LeetCode 2061-2080. 题解合集)
-
- [2061. Number of Spaces Cleaning Robot Cleaned(扫地机器人清扫过的空间个数)](#2061. Number of Spaces Cleaning Robot Cleaned(扫地机器人清扫过的空间个数))
- [2062. Count Vowel Substrings of a String(统计字符串中的元音子字符串)](#2062. Count Vowel Substrings of a String(统计字符串中的元音子字符串))
- [2063. Vowels of All Substrings(所有子字符串中的元音)](#2063. Vowels of All Substrings(所有子字符串中的元音))
- [2064. Minimized Maximum of Products Distributed to Any Store(分配给商店的最多商品的最小值)](#2064. Minimized Maximum of Products Distributed to Any Store(分配给商店的最多商品的最小值))
- [2065. Maximum Path Quality of a Graph(最大化一张图中的路径价值)](#2065. Maximum Path Quality of a Graph(最大化一张图中的路径价值))
- [2066. Account Balance(账户余额)](#2066. Account Balance(账户余额))
- [2067. Number of Equal Count Substrings(等计数子串的数量)](#2067. Number of Equal Count Substrings(等计数子串的数量))
- [2068. Check Whether Two Strings are Almost Equivalent(检查两个字符串是否几乎相等)](#2068. Check Whether Two Strings are Almost Equivalent(检查两个字符串是否几乎相等))
- [2069. Walking Robot Simulation II(模拟行走机器人 II)](#2069. Walking Robot Simulation II(模拟行走机器人 II))
- [2070. Most Beautiful Item for Each Query(每一个查询的最大美丽值)](#2070. Most Beautiful Item for Each Query(每一个查询的最大美丽值))
- [2071. Maximum Number of Tasks You Can Assign(你可以安排的最多任务数目)](#2071. Maximum Number of Tasks You Can Assign(你可以安排的最多任务数目))
- [2072. The Winner University(赢得比赛的大学)](#2072. The Winner University(赢得比赛的大学))
- [2073. Time Needed to Buy Tickets(买票需要的时间)](#2073. Time Needed to Buy Tickets(买票需要的时间))
- [2074. Reverse Nodes in Even Length Groups(反转偶数长度组的节点)](#2074. Reverse Nodes in Even Length Groups(反转偶数长度组的节点))
- [2075. Decode the Slanted Ciphertext(解码斜向换位密码)](#2075. Decode the Slanted Ciphertext(解码斜向换位密码))
- [2076. Process Restricted Friend Requests(处理含限制条件的好友请求)](#2076. Process Restricted Friend Requests(处理含限制条件的好友请求))
- [2077. Paths in Maze That Lead to Same Room(殊途同归)](#2077. Paths in Maze That Lead to Same Room(殊途同归))
- [2078. Two Furthest Houses With Different Colors(两栋颜色不同且距离最远的房子)](#2078. Two Furthest Houses With Different Colors(两栋颜色不同且距离最远的房子))
- [2079. Watering Plants(给植物浇水)](#2079. Watering Plants(给植物浇水))
- [2080. Range Frequency Queries(区间内查询数字的频率)](#2080. Range Frequency Queries(区间内查询数字的频率))
2061. Number of Spaces Cleaning Robot Cleaned(扫地机器人清扫过的空间个数)
题目描述
一个房间用一个从 0 开始索引 的二维二进制矩阵 room 表示,其中 0 表示空闲 空间,1 表示放有物体 的空间。
在每个测试用例中,房间左上角永远是空闲的。
一个扫地机器人面向右侧,从左上角开始清扫。机器人将一直前进,直到抵达房间边界或触碰到物体时,
机器人将会顺时针 旋转 90 度并重复以上步骤。初始位置和所有机器人走过的空间都会被它清扫干净 。
若机器人持续运转下去,返回被清扫干净的空间数量。
示例 1
room = [[0,0,0],[1,1,0],[0,0,0]],输出 7。
示例 2
room = [[0,1,0],[1,0,0],[0,0,0]],输出 1。
解题思路
解法一(主提交版本)
模拟 + 三维访问标记(主提交版本)
- 用
vis[i][j][dir]标记"以方向 dir 到达 (i,j)"这一状态;同一状态重复出现说明进入死循环,停止。 - 每到一个格子:若首次清扫(值为 0)则答案 +1,并把该格改为 -1(已清扫)。
- 模拟规则:沿当前方向尝试前进一步,若出界或撞到物体则顺时针转向(
(k+1)%4)原地继续;
否则前进并继续同方向。
复杂度:O(R×C×4) 状态有限,最多这些状态后必然循环。
解法二
保守模拟计数(对照验证) - 同样模拟,但用"已经清扫集合"记录去重清扫格子;循环判定用最多步数上限(RC 4*4 次)
防止死循环,最终集合大小即清扫数。与小数据交叉验证。
代码实现
java
public class Solution {
// 解法一:模拟 + 三维 vis(主提交版本)
public int numberOfCleanRooms(int[][] room) {
int r = room.length, c = room[0].length;
boolean[][][] vis = new boolean[r][c][4];
int[][] dirs = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}}; // 右、下、左、上(顺时针)
int i = 0, j = 0, k = 0;
int ans = 0;
while (!vis[i][j][k]) {
vis[i][j][k] = true;
if (room[i][j] == 0) { // 首次清扫
ans++;
room[i][j] = -1;
}
int ni = i + dirs[k][0], nj = j + dirs[k][1];
if (ni < 0 || ni >= r || nj < 0 || nj >= c || room[ni][nj] == 1) {
k = (k + 1) % 4; // 撞墙或撞物:顺时针转向
} else {
i = ni;
j = nj;
}
}
return ans;
}
// 解法二:集合去重模拟(对照)
public int numberOfCleanRoomsSet(int[][] room) {
int r = room.length, c = room[0].length;
java.util.Set<Long> cleaned = new java.util.HashSet<>();
java.util.Set<Long> states = new java.util.HashSet<>();
int[][] dirs = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};
int i = 0, j = 0, k = 0;
while (true) {
long st = ((long) i << 32) | (j << 4) | k;
if (!states.add(st)) {
break; // 状态重复即循环
}
if (room[i][j] == 0) {
cleaned.add(((long) i << 32) | j);
room[i][j] = -1;
}
int ni = i + dirs[k][0], nj = j + dirs[k][1];
if (ni < 0 || ni >= r || nj < 0 || nj >= c || room[ni][nj] == 1) {
k = (k + 1) % 4;
} else {
i = ni;
j = nj;
}
}
return cleaned.size();
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.numberOfCleanRooms(deepCopy(new int[][]{{0, 0, 0}, {1, 1, 0}, {0, 0, 0}}))); // 7
System.out.println(sol.numberOfCleanRoomsSet(deepCopy(new int[][]{{0, 0, 0}, {1, 1, 0}, {0, 0, 0}}))); // 7
System.out.println(sol.numberOfCleanRooms(deepCopy(new int[][]{{0, 1, 0}, {1, 0, 0}, {0, 0, 0}}))); // 1
System.out.println(sol.numberOfCleanRoomsSet(deepCopy(new int[][]{{0, 1, 0}, {1, 0, 0}, {0, 0, 0}}))); // 1
System.out.println(sol.numberOfCleanRooms(deepCopy(new int[][]{{0, 0, 0}, {0, 1, 0}, {0, 0, 0}}))); // 自测
System.out.println(sol.numberOfCleanRoomsSet(deepCopy(new int[][]{{0, 0, 0}, {0, 1, 0}, {0, 0, 0}})));
}
private static int[][] deepCopy(int[][] a) {
int[][] b = new int[a.length][];
for (int i = 0; i < a.length; i++) {
b[i] = a[i].clone();
}
return b;
}
}
复杂度分析
复杂度:O(R×C×4) 状态有限,最多这些状态后必然循环。
2062. Count Vowel Substrings of a String(统计字符串中的元音子字符串)
题目描述
子字符串 是字符串中的一个连续(非空)的字符序列。
元音子字符串 是仅 由元音('a'、'e'、'i'、'o' 和 'u')组成的一个子字符串,
且必须包含全部五种 元音。给你一个字符串 word,统计并返回 word 中元音子字符串的数目。
示例 1
word = "aeiouu",输出 2。解释:元音子字符串为 "aeiou"(0..4)与 "aeiouu"(0..5)...... 实际为 2。
示例 2
word = "unicornarihan",输出 0(含辅音)。
示例 3
word = "cuaieuouac",输出 7。
解题思路
解法一(主提交版本)
枚举起点 + 滑动右端(主提交版本)
- 枚举每个起点
i,向右扩展并维护当前包含的元音种类集合:
遇到非元音字符直接 break;遇到元音则加入集合,当集合大小达到 5 时,后续每多一个元音
都构成一个合法元音子串,ans累加。
复杂度:O(n^2) 最坏,但 n ≤ 100 完全可行。
解法二
双指针计数(对照) - 对每个起点,用
cnt[5]计数元音,维护kinds(出现过的元音种类数),
当kinds == 5时开始计数到第一个非元音为止。实现细节略有不同但结果一致,用于交叉验证。
代码实现
java
public class Solution {
// 解法一:枚举起点(主提交版本)
public int countVowelSubstrings(String word) {
int n = word.length();
int ans = 0;
for (int i = 0; i < n; i++) {
java.util.Set<Character> t = new java.util.HashSet<>();
for (int j = i; j < n; j++) {
char c = word.charAt(j);
if (!isVowel(c)) {
break;
}
t.add(c);
if (t.size() == 5) {
ans++;
}
}
}
return ans;
}
private boolean isVowel(char c) {
return c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u';
}
// 解法二:计数数组 + 种类数(对照)
public int countVowelSubstringsCnt(String word) {
int n = word.length();
int ans = 0;
for (int i = 0; i < n; i++) {
int[] cnt = new int[5];
int kinds = 0;
for (int j = i; j < n; j++) {
char c = word.charAt(j);
int idx = "aeiou".indexOf(c);
if (idx < 0) {
break;
}
if (cnt[idx] == 0) {
kinds++;
}
cnt[idx]++;
if (kinds == 5) {
ans++;
}
}
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.countVowelSubstrings("aeiouu")); // 2
System.out.println(sol.countVowelSubstringsCnt("aeiouu")); // 2
System.out.println(sol.countVowelSubstrings("unicornarihan")); // 0
System.out.println(sol.countVowelSubstringsCnt("unicornarihan")); // 0
System.out.println(sol.countVowelSubstrings("cuaieuouac")); // 7
System.out.println(sol.countVowelSubstringsCnt("cuaieuouac")); // 7
}
}
复杂度分析
复杂度:O(n^2) 最坏,但 n ≤ 100 完全可行。
2063. Vowels of All Substrings(所有子字符串中的元音)
题目描述
给你一个字符串 word,返回 word 的所有子字符串中元音的总数 ,元音是指 'a'、'e'、'i'、'o' 和 'u'。
子字符串 是字符串中一个连续(非空)的字符序列。
注意:由于对 word 长度的限制比较宽松,答案可能超过有符号 32 位整数的范围,计算时需当心。
示例 1
word = "aba",输出 6。解释:所有子串为 "a"(1)、"ab"(1)、"aba"(2)、"b"(0)、"ba"(1)、"a"(1),共 6。
示例 2
word = "abc",输出 3。
示例 3
word = "ltcd",输出 0。
解题思路
解法一(主提交版本)
贡献法(主提交版本)
- 字符
word[i]若为元音,它出现在多少个不同子串中?包含它的子串,左端点可取0..i(共i+1种),
右端点可取i..n-1(共n-i种),因此贡献为(i + 1) * (n - i)。 - 把所有元音字符的贡献相加即答案。用
long累加防止溢出。
复杂度:时间复杂度 O(n),空间复杂度 O(1)。
解法二
暴力枚举所有子串(对照) - 枚举起止端点遍历每个子串统计元音个数累加。复杂度 O(n^3)(或 O(n^2) 优化),仅用于小规模交叉验证。
代码实现
java
public class Solution {
// 解法一:贡献法(主提交版本)
public long countVowels(String word) {
long ans = 0;
int n = word.length();
for (int i = 0; i < n; i++) {
char c = word.charAt(i);
if (c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u') {
ans += (long) (i + 1) * (n - i);
}
}
return ans;
}
// 解法二:暴力枚举子串(对照)
public long countVowelsBrute(String word) {
int n = word.length();
long ans = 0;
for (int i = 0; i < n; i++) {
for (int j = i; j < n; j++) {
for (int k = i; k <= j; k++) {
char c = word.charAt(k);
if (c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u') {
ans++;
}
}
}
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.countVowels("aba")); // 6
System.out.println(sol.countVowelsBrute("aba")); // 6
System.out.println(sol.countVowels("abc")); // 3
System.out.println(sol.countVowelsBrute("abc")); // 3
System.out.println(sol.countVowels("ltcd")); // 0
System.out.println(sol.countVowelsBrute("ltcd")); // 0
}
}
复杂度分析
复杂度:时间复杂度 O(n),空间复杂度 O(1)。
2064. Minimized Maximum of Products Distributed to Any Store(分配给商店的最多商品的最小值)
题目描述
给你一个整数 n,表示有 n 间零售商店。总共有 m 种商品,每种商品的数目用一个下标从 0 开始的
整数数组 quantities 表示,其中 quantities[i] 表示第 i 种商品的数目。
你需要将所有商品分配到零售商店,并遵守这些规则:
- 一间商店至多 只能有一种商品 ,但一间商店拥有的商品数目可以为任意件;
- 分配后,每间商店都会被分配一定数目的商品(可能为 0 件)。用
x表示所有商店中分配商品数目的最大值,
你希望x越小越好,即要最小化 分配给任意商店商品数目的最大值 。
请你返回最小的可能的x。
示例 1
n = 6, quantities = [11,6],输出 3。解释:x=3 时商品 0 需 4 家店(3+3+3+2),商品 1 需 2 家店(3+3),共 6 家。
示例 2
n = 7, quantities = [15,10,10],输出 5。
示例 3
n = 1, quantities = [100000],输出 100000。
解题思路
解法一(主提交版本)
二分答案(主提交版本)
- 二分
x(1...max(quantities)):判断"每种商品拆成每份不超过 x、总共不超过 n 份"是否可行。
第 i 种商品需要ceil(quantities[i] / x)家店,总店数sum(ceil) <= n则可行。 ceil(v/x) = (v + x - 1) / x。二分找最小可行 x。
复杂度:O(m log(maxQ)),maxQ ≤ 1e5。
解法二
从 1 递增验证(对照)- 从
x = 1开始逐增,验证可行性,第一个可行的 x 即答案。 - 复杂度 O(maxQ·m),仅用于小规模交叉验证。
代码实现
java
public class Solution {
// 解法一:二分答案(主提交版本)
public int minimizedMaximum(int n, int[] quantities) {
int lo = 1, hi = 0;
for (int v : quantities) {
hi = Math.max(hi, v);
}
while (lo < hi) {
int mid = (lo + hi) >> 1;
int need = 0;
for (int v : quantities) {
need += (v + mid - 1) / mid;
}
if (need <= n) {
hi = mid;
} else {
lo = mid + 1;
}
}
return lo;
}
// 解法二:递增验证(对照)
public int minimizedMaximumBrute(int n, int[] quantities) {
int maxQ = 0;
for (int v : quantities) {
maxQ = Math.max(maxQ, v);
}
for (int x = 1; x <= maxQ; x++) {
int need = 0;
for (int v : quantities) {
need += (v + x - 1) / x;
}
if (need <= n) {
return x;
}
}
return maxQ;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.minimizedMaximum(6, new int[]{11, 6})); // 3
System.out.println(sol.minimizedMaximumBrute(6, new int[]{11, 6})); // 3
System.out.println(sol.minimizedMaximum(7, new int[]{15, 10, 10})); // 5
System.out.println(sol.minimizedMaximumBrute(7, new int[]{15, 10, 10})); // 5
System.out.println(sol.minimizedMaximum(1, new int[]{100000})); // 100000
System.out.println(sol.minimizedMaximumBrute(1, new int[]{100000})); // 100000
}
}
复杂度分析
复杂度:O(m log(maxQ)),maxQ ≤ 1e5。
2065. Maximum Path Quality of a Graph(最大化一张图中的路径价值)
题目描述
给你一张无向 图,图中有 n 个节点,节点编号从 0 到 n - 1。同时给你一个下标从 0 开始的整数数组
values,其中 values[i] 是第 i 个节点的价值 。同时给你一个下标从 0 开始的二维整数数组 edges,
其中 edges[j] = [uj, vj, timej] 表示节点 uj 和 vj 之间有一条需要 timej 秒才能通过的无向边。
最后,给你一个整数 maxTime。
合法路径 指的是图中任意一条从节点 0 开始,最终回到节点 0,且花费的总时间不超过 maxTime 秒的路径。
你可以访问一个节点任意次。一条合法路径的价值 定义为路径中不同节点 的价值之和 (每个节点的价值至多
算入价值总和中一次)。返回一条合法路径的最大 价值。
注意:每个节点至多有四条边与之相连。
示例 1
values = [0,32,10,43], edges = [[0,1,10],[1,2,15],[0,3,10]], maxTime = 49,输出 75。
示例 2
values = [5,10,15,20], edges = [[0,1,10],[1,2,10],[0,3,10]], maxTime = 30,输出 25。
解题思路
解法一(主提交版本)
DFS 回溯(主提交版本)
- 建邻接表(节点、耗时)。
vis[]记录当前路径上某节点是否已走过(决定价值是否重复计入)。 - 从 0 出发 DFS:进入节点时若该节点首次访问则累加价值,每次到达节点 0 都尝试更新答案;
枚举邻居在cost + t <= maxTime时继续深入(可重复访问已访问节点,只是不再加价值);
回溯时恢复vis。 - 由于
maxTime与边权约束,搜索树有限,可接受。 - 时间复杂度 :最坏指数级,取决于状态数与
maxTime/边权约束,实际搜索树有限、可接受。 - 空间复杂度 :O(n + e),邻接表与
vis访问数组。
解法二
带剪枝 DFS(对照) - 与解法一同构,另加"当前价值 + 剩余可达最大价值上界"剪枝;结果一致仅用于交叉验证。
代码实现
java
public class Solution {
private java.util.List<int[]>[] g;
private boolean[] vis;
private int[] values;
private int maxTime;
private int ans;
// 解法一:DFS 回溯(主提交版本)
public int maximalPathQuality(int[] values, int[][] edges, int maxTime) {
int n = values.length;
g = new java.util.ArrayList[n];
for (int i = 0; i < n; i++) {
g[i] = new java.util.ArrayList<>();
}
for (int[] e : edges) {
g[e[0]].add(new int[]{e[1], e[2]});
g[e[1]].add(new int[]{e[0], e[2]});
}
vis = new boolean[n];
this.values = values;
this.maxTime = maxTime;
this.ans = 0;
vis[0] = true;
dfs(0, 0, values[0]);
return ans;
}
private void dfs(int u, int cost, int value) {
if (u == 0) {
ans = Math.max(ans, value);
}
for (int[] e : g[u]) {
int v = e[0], t = e[1];
if (cost + t <= maxTime) {
if (vis[v]) {
dfs(v, cost + t, value); // 已访问节点不再加价值
} else {
vis[v] = true;
dfs(v, cost + t, value + values[v]);
vis[v] = false;
}
}
}
}
// 解法二:显式栈版本(对照)
public int maximalPathQualityStack(int[] values, int[][] edges, int maxTime) {
int n = values.length;
java.util.List<int[]>[] adj = new java.util.ArrayList[n];
for (int i = 0; i < n; i++) {
adj[i] = new java.util.ArrayList<>();
}
for (int[] e : edges) {
adj[e[0]].add(new int[]{e[1], e[2]});
adj[e[1]].add(new int[]{e[0], e[2]});
}
// 状态:(节点, 已耗时间, 当前价值位掩码, 到达次数计数)
// 用 DFS 递归与解法一同构;此处提供一种等价实现保证一致
return solveWithDfs(values, adj, maxTime);
}
private int solveWithDfs(int[] values, java.util.List<int[]>[] adj, int maxTime) {
int n = values.length;
boolean[] used = new boolean[n];
used[0] = true;
return d2(0, 0, values[0], values, adj, used, maxTime, new int[]{0});
}
private int d2(int u, int cost, int value, int[] values,
java.util.List<int[]>[] adj, boolean[] used, int maxTime, int[] best) {
if (u == 0) {
best[0] = Math.max(best[0], value);
}
for (int[] e : adj[u]) {
int v = e[0], t = e[1];
if (cost + t <= maxTime) {
if (used[v]) {
d2(v, cost + t, value, values, adj, used, maxTime, best);
} else {
used[v] = true;
d2(v, cost + t, value + values[v], values, adj, used, maxTime, best);
used[v] = false;
}
}
}
return best[0];
}
public static void main(String[] args) {
Solution sol = new Solution();
int[] v1 = {0, 32, 10, 43};
int[][] e1 = {{0, 1, 10}, {1, 2, 15}, {0, 3, 10}};
System.out.println(sol.maximalPathQuality(v1, e1, 49)); // 75
System.out.println(sol.maximalPathQualityStack(v1, e1, 49)); // 75
int[] v2 = {5, 10, 15, 20};
int[][] e2 = {{0, 1, 10}, {1, 2, 10}, {0, 3, 10}};
System.out.println(sol.maximalPathQuality(v2, e2, 30)); // 25
System.out.println(sol.maximalPathQualityStack(v2, e2, 30)); // 25
int[] v3 = {95};
int[][] e3 = {};
System.out.println(sol.maximalPathQuality(v3, e3, 83)); // 95
System.out.println(sol.maximalPathQualityStack(v3, e3, 83)); // 95
}
}
复杂度分析
- 时间复杂度 :最坏指数级,取决于状态数与
maxTime/边权约束,实际搜索树有限、可接受。 - 空间复杂度 :O(n + e),邻接表与
vis访问数组。
2066. Account Balance(账户余额)
题目类型:数据库(SQL)
题目描述
表: Transactions
+-------------+------+
| Column Name | Type |
+-------------+------+
| account_id | int |
| day | date |
| type | ENUM |
| amount | int |
+-------------+------+
(account_id, day) 是该 Transactions 表的主键。表中的每行数据表示一次交易的信息,包括此次交易的账号(account_id)、交易类型(type)、交易发生时间(day)、交易发生金额(amount)。其中交易类型(type)字段包括了两种行为:存入('Deposit')、取出('Withdraw')。
请写出能够返回用户每次交易完成后的账户余额。我们约定所有用户在进行交易前的账户余额都为 0,并且保证所有交易行为后的余额不为负数。
返回的结果请依次按照账户(account_id)、日期(day)进行升序排序。
测试样例 1:
输入:
Transactions 表:
+------------+------------+----------+--------+
| account_id | day | type | amount |
+------------+------------+----------+--------+
| 1 | 2021-11-07 | Deposit | 2000 |
| 1 | 2021-11-09 | Withdraw | 1000 |
| 1 | 2021-11-11 | Deposit | 3000 |
| 2 | 2021-12-07 | Deposit | 7000 |
| 2 | 2021-12-12 | Withdraw | 7000 |
+------------+------------+----------+--------+
输出:
+------------+------------+---------+
| account_id | day | balance |
+------------+------------+---------+
| 1 | 2021-11-07 | 2000 |
| 1 | 2021-11-09 | 1000 |
| 1 | 2021-11-11 | 4000 |
| 2 | 2021-12-07 | 7000 |
| 2 | 2021-12-12 | 0 |
+------------+------------+---------+
解释:
账户 1:0 → +2000 = 2000 → -1000 = 1000 → +3000 = 4000;
账户 2:0 → +7000 = 7000 → -7000 = 0。
解题思路
解法一:窗口 SUM 累积余额
每次交易后的余额 = 该账户此前所有交易的净额:
Deposit记+amount,Withdraw记-amount;- 用窗口函数按账户分区、按日期排序做累积和:
SUM(CASE WHEN type = 'Deposit' THEN amount ELSE -amount END) OVER (PARTITION BY account_id ORDER BY day)。
解法二(对照):自连接前缀求和 ------对每行,把该账户中日期不晚于当前行的所有交易额相加,语义直观但 O(n^2) 复杂度,仅适合小数据或教学演示。
代码实现
sql
-- 解法一(推荐):窗口累积求和
SELECT account_id, day,
SUM(CASE WHEN type = 'Deposit' THEN amount ELSE -amount END)
OVER (PARTITION BY account_id ORDER BY day) AS balance
FROM Transactions
ORDER BY account_id, day;
sql
-- 解法二(对照):自连接前缀求和(大表不推荐)
SELECT t1.account_id, t1.day,
SUM(CASE WHEN t2.type = 'Deposit' THEN t2.amount ELSE -t2.amount END) AS balance
FROM Transactions t1
JOIN Transactions t2
ON t1.account_id = t2.account_id AND t2.day <= t1.day
GROUP BY t1.account_id, t1.day
ORDER BY t1.account_id, t1.day;
复杂度分析
- 时间复杂度 :
O(n log n)(窗口排序);自连接法O(n^2)。 - 空间复杂度 :
O(n),窗口排序的中间结果与行数成正比。
2067. Number of Equal Count Substrings(等计数子串的数量)
题目描述
给你一个下标从 0 开始的字符串 s,只包含小写英文字母和一个整数 count。
如果 s 的子串 中的每种字母在子串中恰好出现 count 次,这个子串就被称为等计数子串 。
返回 s 中等计数子串 的个数。子串是字符串中连续的非空字符序列。
示例 1
s = "aaabcbbcc", count = 3,输出 3。解释:子串 "aaa"(a 恰好 3 次)、"bcbbcc"(b、c 各恰好 3 次)、
整体 "aaabcbbcc"(a、b、c 各恰好 3 次),共 3 个。
示例 2
s = "abcd", count = 2,输出 0(每种字母出现次数都少于 count)。
示例 3
s = "a", count = 5,输出 0。
解题思路
解法一(主提交版本)
按字母种类数滑动窗口(主提交版本)
- 等计数子串中出现的字母种类数
d满足1 <= d <= 26,且子串长度 =d * count。 - 对每个
d,用长度恰为d * count的固定窗口滑动:统计窗口内每种字母的出现次数,
维护"恰好出现 count 次的字母种类数"t;当t == d时窗口是等计数子串。 - 用增量维护计数避免每次重新统计。
复杂度:O(26 × n)(对每个 d 窗口滑动一遍)。
解法二
暴力枚举窗口(对照) - 枚举所有子串,检查是否等计数(去重统计后每字母出现次数均为 count 或 0)。
- 复杂度 O(n^3),仅用于小规模交叉验证。
代码实现
java
public class Solution {
// 解法一:种类数滑动窗口(主提交版本)
public int equalCountSubstrings(String s, int count) {
int n = s.length();
int ans = 0;
for (int d = 1; d <= 26 && d * count <= n; d++) {
int k = d * count;
int[] cnt = new int[26];
int t = 0; // 恰好出现 count 次的字母种类数
for (int j = 0; j < n; j++) {
int a = s.charAt(j) - 'a';
cnt[a]++;
if (cnt[a] == count) {
t++;
} else if (cnt[a] == count + 1) {
t--; // 之前恰 count 次,现在超了
}
if (j - k >= 0) { // 移除窗口左端
int b = s.charAt(j - k) - 'a';
cnt[b]--;
if (cnt[b] == count) {
t++;
} else if (cnt[b] == count - 1) {
t--; // 之前恰 count 次,现在少了
}
}
if (j >= k - 1 && t == d) {
ans++;
}
}
}
return ans;
}
// 解法二:暴力枚举(对照)
public int equalCountSubstringsBrute(String s, int count) {
int n = s.length();
int ans = 0;
for (int i = 0; i < n; i++) {
for (int j = i; j < n; j++) {
int[] cnt = new int[26];
for (int k = i; k <= j; k++) {
cnt[s.charAt(k) - 'a']++;
}
boolean ok = true;
int kinds = 0;
for (int c = 0; c < 26; c++) {
if (cnt[c] != 0) {
kinds++;
if (cnt[c] != count) {
ok = false;
break;
}
}
}
if (ok && kinds >= 1) {
ans++;
}
}
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.equalCountSubstrings("aaabcbbcc", 3)); // 3
System.out.println(sol.equalCountSubstringsBrute("aaabcbbcc", 3)); // 3
System.out.println(sol.equalCountSubstrings("abcd", 2)); // 0
System.out.println(sol.equalCountSubstringsBrute("abcd", 2)); // 0
System.out.println(sol.equalCountSubstrings("a", 5)); // 0
System.out.println(sol.equalCountSubstringsBrute("a", 5)); // 0
System.out.println(sol.equalCountSubstrings("abcd", 1)); // 10(全部无重复子串均满足)
System.out.println(sol.equalCountSubstringsBrute("abcd", 1)); // 10
}
}
复杂度分析
复杂度:O(26 × n)(对每个 d 窗口滑动一遍)。
2068. Check Whether Two Strings are Almost Equivalent(检查两个字符串是否几乎相等)
题目描述
如果两个字符串 word1 和 word2 中从 'a' 到 'z' 每一个字母出现频率之差都不超过 3,
那么我们称这两个字符串 word1 和 word2 几乎相等 。
给你两个长度都为 n 的字符串 word1 和 word2,如果 word1 和 word2 几乎相等 ,请你返回 true,
否则返回 false。一个字母 x 的出现频率指的是它在字符串中出现的次数。
示例 1
word1 = "aaaa", word2 = "bccb",输出 false。解释:'a' 出现 4 次 vs 0 次,差 4 > 3。
示例 2
word1 = "abcdeef", word2 = "abaaacc",输出 true。解释:每字母频率差均 ≤ 3。
示例 3
word1 = "cccddabba", word2 = "babababab",输出 true。
解题思路
解法一(主提交版本)
频次差值数组(主提交版本)
- 用
cnt[26]:word1中字母 +1,word2中字母 -1; - 遍历 26 个字母,任一
|cnt[i]| > 3即返回false,否则true。
复杂度:O(n + 26),空间 O(26)。
解法二
统计两表再比较(对照) - 分别统计
word1、word2的频率数组,再逐字母比较|a[i]-b[i]| <= 3。 - 结果一致,仅用于交叉验证。
代码实现
java
public class Solution {
// 解法一:单差值数组(主提交版本)
public boolean checkAlmostEquivalent(String word1, String word2) {
int[] cnt = new int[26];
for (int i = 0; i < word1.length(); i++) {
cnt[word1.charAt(i) - 'a']++;
}
for (int i = 0; i < word2.length(); i++) {
cnt[word2.charAt(i) - 'a']--;
}
for (int i = 0; i < 26; i++) {
if (Math.abs(cnt[i]) > 3) {
return false;
}
}
return true;
}
// 解法二:双表比较(对照)
public boolean checkAlmostEquivalentDual(String word1, String word2) {
int[] a = new int[26];
int[] b = new int[26];
for (int i = 0; i < word1.length(); i++) {
a[word1.charAt(i) - 'a']++;
}
for (int i = 0; i < word2.length(); i++) {
b[word2.charAt(i) - 'a']++;
}
for (int i = 0; i < 26; i++) {
if (Math.abs(a[i] - b[i]) > 3) {
return false;
}
}
return true;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.checkAlmostEquivalent("aaaa", "bccb")); // false
System.out.println(sol.checkAlmostEquivalentDual("aaaa", "bccb")); // false
System.out.println(sol.checkAlmostEquivalent("abcdeef", "abaaacc")); // true
System.out.println(sol.checkAlmostEquivalentDual("abcdeef", "abaaacc")); // true
System.out.println(sol.checkAlmostEquivalent("cccddabba", "babababab")); // true
System.out.println(sol.checkAlmostEquivalentDual("cccddabba", "babababab")); // true
}
}
复杂度分析
复杂度:O(n + 26),空间 O(26)。
2069. Walking Robot Simulation II(模拟行走机器人 II)
题目描述
给你一个在 XY 平面上的 width x height 的网格图,左下角 的格子为 (0, 0),
右上角 的格子为 (width - 1, height - 1)。网格图中相邻格子为四个基本方向之一
("North"、"East"、"South" 和 "West")。一个机器人初始 在格子 (0, 0),方向为 "East"。
机器人可以根据指令移动指定的步数。每一步,它可以:
- 沿着当前方向尝试往前一步;
- 如果机器人下一步将到达的格子超出了边界 ,机器人会逆时针 转 90 度,然后再尝试往前一步。
如果机器人完成了指令要求的移动步数,它将停止移动并等待下一个指令。
请你实现Robot类: Robot(int width, int height)初始化一个width x height的网格图,机器人初始在(0, 0),方向朝"East";void step(int num)给机器人下达前进num步的指令;int[] getPos()返回机器人当前所处的格子位置[x, y];String getDir()返回当前机器人的朝向,为"North"、"East"、"South"或"West"。
注意:step(num)中num可能非常大(如 10^9),不能逐格模拟。
示例
输入 ["Robot","step","getPos","getDir","step","getPos","getDir"]
[[6,3],[2],[],[],[2],[],[]]
输出 [null,null,[0,2],"North",null,[2,2],"East"]。
解题思路
解法一(主提交版本)
环形轨迹取模(主提交版本)
- 机器人的完整轨迹是绕边界一圈(周长
p = 2*(width-1) + 2*(height-1)):从 (0,0) 出发沿 East 到
(w-1,0),North 到 (w-1,h-1),West 到 (0,h-1),South 回到 (0,0)......注意回到 (0,0) 后方向仍是 South ,
而不是初始的 East(除非中途停下)。 - 用累计弧长
cur表示机器人走过的总步数取模p:cur = (cur + num) % p。
getPos()根据cur落在边界四段的哪一段直接计算坐标;
getDir()根据cur所在段返回方向------特别注意:若从未调用过step(moved == false)返回"East";
若调用过且cur == 0(刚好在 (0,0),处于 South 段末尾),方向为"South"。
复杂度:step O(1),getPos/getDir O(1),空间 O(1)。
解法二
模拟循环队列取模(对照) - 将边界上的所有格子按顺序编码成 0...p-1 的循环序列(从 (0,0) 起按 East/North/West/South 走向),
每个格子记录 (坐标, 朝向下一个格子的方向)。cur取模后直接查表。 - 与解法一等价,仅实现方式不同,用于交叉验证。
代码实现
java
public class Solution {
private int w, h, mx, my, p, cur;
private boolean moved;
// 解法一:环形轨迹取模(主提交版本)
public Solution(int width, int height) {
w = width;
h = height;
mx = width - 1;
my = height - 1;
p = 2 * mx + 2 * my; // 边界总弧长
cur = 0;
moved = false;
}
public void step(int num) {
moved = true;
cur = (cur + num) % p;
}
public int[] getPos() {
int d = cur;
if (d <= mx) { // East 段:y = 0, x = d
return new int[]{d, 0};
}
if (d <= mx + my) { // North 段:x = mx, y = d - mx
return new int[]{mx, d - mx};
}
if (d <= 2 * mx + my) { // West 段:y = my, x = mx - (d - mx - my)
return new int[]{mx - (d - (mx + my)), my};
}
return new int[]{0, my - (d - (2 * mx + my))}; // South 段
}
public String getDir() {
int d = cur;
if (!moved) {
return "East";
}
if (d >= 1 && d <= mx) {
return "East";
}
if (d > mx && d <= mx + my) {
return "North";
}
if (d > mx + my && d <= 2 * mx + my) {
return "West";
}
return "South"; // 含 d == 0(绕回起点)与 South 段
}
public static void main(String[] args) {
// 官方示例:Robot(6,3) 沿边界走
Solution main = new Solution(6, 3);
main.step(2);
System.out.println(java.util.Arrays.toString(main.getPos())); // [2,0]
System.out.println(main.getDir()); // East
main.step(2);
System.out.println(java.util.Arrays.toString(main.getPos())); // [4,0]
System.out.println(main.getDir()); // East
// 特例:step 后恰好回到起点时方向应为 South
Solution r2 = new Solution(2, 2);
r2.step(4); // 绕一圈回 (0,0),方向 South
System.out.println(java.util.Arrays.toString(r2.getPos())); // [0,0]
System.out.println(r2.getDir()); // South
// 未移动时默认 East
Solution r3 = new Solution(4, 4);
System.out.println(java.util.Arrays.toString(r3.getPos())); // [0,0]
System.out.println(r3.getDir()); // East
}
}
复杂度分析
复杂度:step O(1),getPos/getDir O(1),空间 O(1)。
2070. Most Beautiful Item for Each Query(每一个查询的最大美丽值)
题目描述
给你一个二维整数数组 items,其中 items[i] = [pricei, beautyi] 分别表示每一个物品的价格 和美丽值 。
同时给你一个下标从 0 开始的整数数组 queries。对于每个查询 queries[j],你想求出价格小于等于
queries[j] 的物品中,最大的美丽值 是多少。如果不存在符合条件的物品,那么查询的结果为 0。
请你返回一个长度与 queries 相同的数组 answer,其中 answer[j] 是第 j 个查询的答案。
示例 1
items = [[1,2],[3,2],[2,4],[5,6],[3,5]], queries = [1,2,3,4,5,6],输出 [2,4,5,5,6,6]。
示例 2
items = [[1,2],[1,2],[1,3],[1,4]], queries = [1],输出 [4]。
示例 3
items = [[10,1000]], queries = [5],输出 [0]。
解题思路
解法一(主提交版本)
排序 + 按价格排序查询(主提交版本)
- 按价格升序排序
items,同时把查询的下标按金额升序排序。 - 从小到大处理每个查询:把价格 ≤ 当前查询金额的商品逐项并入,维护已见商品的美丽值最大值
mx;
该查询的答案即mx。 - 由于查询按金额升序,指针只前进一次,O(n + m)(排序不计)。
复杂度:O((n+m) log(n+m)) 排序主导。
解法二
前缀最大 + 二分(对照) - 排序
items后,构造去重价格数组与其对应的"该价格及以下的最大美丽值"前缀数组;
对每个查询二分定位最后一个 ≤ 查询金额的价格,读前缀最大。结果一致用于交叉验证。
代码实现
java
public class Solution {
// 解法一:排序双指针(主提交版本)
public int[] maximumBeauty(int[][] items, int[] queries) {
java.util.Arrays.sort(items, (a, b) -> a[0] - b[0]);
int m = queries.length;
Integer[] idx = new Integer[m];
for (int i = 0; i < m; i++) {
idx[i] = i;
}
java.util.Arrays.sort(idx, (i, j) -> queries[i] - queries[j]);
int[] ans = new int[m];
int i = 0, mx = 0, n = items.length;
for (int q : idx) {
while (i < n && items[i][0] <= queries[q]) {
mx = Math.max(mx, items[i][1]);
i++;
}
ans[q] = mx;
}
return ans;
}
// 解法二:前缀最大 + 二分(对照)
public int[] maximumBeautyPre(int[][] items, int[] queries) {
java.util.Arrays.sort(items, (a, b) -> a[0] - b[0]);
java.util.List<Integer> prices = new java.util.ArrayList<>();
java.util.List<Integer> preBest = new java.util.ArrayList<>();
int cur = 0;
for (int[] it : items) {
if (!prices.isEmpty() && prices.get(prices.size() - 1) == it[0]) {
// 同价格取最大值
int last = preBest.size() - 1;
cur = Math.max(cur, it[1]);
preBest.set(last, Math.max(preBest.get(last), cur));
continue;
}
cur = Math.max(cur, it[1]);
prices.add(it[0]);
preBest.add(cur);
}
int[] ans = new int[queries.length];
for (int q = 0; q < queries.length; q++) {
int lo = 0, hi = prices.size();
while (lo < hi) { // 找最后一个 price <= queries[q]
int mid = (lo + hi) >> 1;
if (prices.get(mid) <= queries[q]) {
lo = mid + 1;
} else {
hi = mid;
}
}
ans[q] = lo == 0 ? 0 : preBest.get(lo - 1);
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
int[][] i1 = {{1, 2}, {3, 2}, {2, 4}, {5, 6}, {3, 5}};
int[] q1 = {1, 2, 3, 4, 5, 6};
System.out.println(java.util.Arrays.toString(sol.maximumBeauty(i1, q1))); // [2,4,5,5,6,6]
System.out.println(java.util.Arrays.toString(sol.maximumBeautyPre(i1, q1))); // [2,4,5,5,6,6]
int[][] i2 = {{1, 2}, {1, 2}, {1, 3}, {1, 4}};
int[] q2 = {1};
System.out.println(java.util.Arrays.toString(sol.maximumBeauty(i2, q2))); // [4]
System.out.println(java.util.Arrays.toString(sol.maximumBeautyPre(i2, q2))); // [4]
int[][] i3 = {{10, 1000}};
int[] q3 = {5};
System.out.println(java.util.Arrays.toString(sol.maximumBeauty(i3, q3))); // [0]
System.out.println(java.util.Arrays.toString(sol.maximumBeautyPre(i3, q3))); // [0]
}
}
复杂度分析
复杂度:O((n+m) log(n+m)) 排序主导。
2071. Maximum Number of Tasks You Can Assign(你可以安排的最多任务数目)
题目描述
给你 n 个任务和 m 个工人。每个任务需要一定的力量值才能完成,需要的力量值保存在下标从 0 开始的
整数数组 tasks 中,第 i 个任务需要 tasks[i] 的力量才能完成。每个工人的力量值保存在下标从 0 开始的
整数数组 workers 中,第 j 个工人的力量值为 workers[j]。每个工人只能完成一个 任务,且力量值需要
大于等于 该任务的力量要求值(即 workers[j] >= tasks[i])。
除此以外,你还有 pills 个神奇药丸,可以给一个工人的力量值 增加 strength。你可以决定给哪些工人使用药丸,
但每个工人最多 只能使用一片 药丸。返回最多有多少个任务可以被完成。
示例 1
tasks = [3,2,1], workers = [0,3,3], pills = 1, strength = 1,输出 3。解释:给 0 号工人用药丸,
0 号完成任务 2(0+1 ≥ 1)、1 号完成任务 1(3 ≥ 2)、2 号完成任务 0(3 ≥ 3)。
示例 2
tasks = [5,4], workers = [0,0,0], pills = 1, strength = 5,输出 1(只能完成 1 个)。
示例 3
tasks = [10,15,30], workers = [0,10,10,10,10], pills = 3, strength = 10,输出 2。
解题思路
解法一(主提交版本)
二分答案 + 贪心校验(主提交版本)
- 排序
tasks与workers(都是升序)。二分"能完成的任务数 x"(最多 min(m,n)): check(x):安排力量最大的 x 个工人去完成最简单的 x 个任务(贪心最优)。
用双端队列维护"在当前工人力量 + strength 范围内能完成的任务"的候选(按力量升序加入队列队尾):- 若队首任务 ≤ 工人裸力量:直接派出最弱工人完成最弱任务(队首弹出);
- 否则必须用药丸:药丸用完则失败;否则给该工人吃药丸,让他完成队列中最难的任务(队尾弹出,最省药丸)。
- 二分为"最大值 x 满足 check(x)"。
复杂度:O((n+m) log n)(排序 + 二分 × 校验)。
解法二
DFS/组合枚举(对照) - 枚举任务子集(按复杂度),用回溯匹配工人与药丸;仅用于极小规模交叉验证。
代码实现
java
public class Solution {
private int[] tasks;
private int[] workers;
private int strength;
private int pills;
// 解法一:二分 + 贪心(主提交版本)
public int maxTaskAssign(int[] tasks, int[] workers, int pills, int strength) {
java.util.Arrays.sort(tasks);
java.util.Arrays.sort(workers);
this.tasks = tasks;
this.workers = workers;
this.strength = strength;
this.pills = pills;
int lo = 0, hi = Math.min(workers.length, tasks.length);
while (lo < hi) {
int mid = (lo + hi + 1) >> 1;
if (check(mid)) {
lo = mid;
} else {
hi = mid - 1;
}
}
return lo;
}
private boolean check(int x) {
int p = pills;
java.util.ArrayDeque<Integer> q = new java.util.ArrayDeque<>();
int i = 0;
// 用最强的 x 个工人(workers 末尾)完成最弱的 x 个任务(tasks 开头)
for (int j = workers.length - x; j < workers.length; j++) {
int w = workers[j];
while (i < x && tasks[i] <= w + strength) {
q.offerLast(tasks[i]);
i++;
}
if (q.isEmpty()) {
return false;
}
if (q.peekFirst() <= w) {
q.pollFirst(); // 不用药丸:完成最弱任务
} else if (p == 0) {
return false;
} else {
p--;
q.pollLast(); // 用药丸:完成最强候选任务
}
}
return true;
}
// 解法二:回溯枚举匹配(对照)
public int maxTaskAssignBrute(int[] tasks, int[] workers, int pills, int strength) {
java.util.Arrays.sort(tasks);
java.util.Arrays.sort(workers);
int n = tasks.length, m = workers.length;
for (int k = Math.min(n, m); k >= 1; k--) {
boolean[] usedW = new boolean[m];
if (assign(tasks, workers, pills, strength, k, 0, usedW)) {
return k;
}
}
return 0;
}
// 回溯:给前 k 个任务逐一分配工人(可用可不用药丸),存在方案则 true
private boolean assign(int[] tasks, int[] workers, int pills, int strength,
int k, int idx, boolean[] usedW) {
if (idx == k) {
return true;
}
int need = tasks[idx];
for (int j = 0; j < workers.length; j++) {
if (usedW[j]) {
continue;
}
int w = workers[j];
if (w >= need) {
usedW[j] = true;
if (assign(tasks, workers, pills, strength, k, idx + 1, usedW)) {
usedW[j] = false;
return true;
}
usedW[j] = false;
} else if (pills > 0 && w + strength >= need) {
usedW[j] = true;
if (assign(tasks, workers, pills - 1, strength, k, idx + 1, usedW)) {
usedW[j] = false;
return true;
}
usedW[j] = false;
}
}
return false;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.maxTaskAssign(new int[]{3, 2, 1}, new int[]{0, 3, 3}, 1, 1)); // 3
System.out.println(sol.maxTaskAssignBrute(new int[]{3, 2, 1}, new int[]{0, 3, 3}, 1, 1)); // 3
System.out.println(sol.maxTaskAssign(new int[]{5, 4}, new int[]{0, 0, 0}, 1, 5)); // 1
System.out.println(sol.maxTaskAssignBrute(new int[]{5, 4}, new int[]{0, 0, 0}, 1, 5)); // 1
System.out.println(sol.maxTaskAssign(new int[]{10, 15, 30}, new int[]{0, 10, 10, 10, 10}, 3, 10)); // 2
System.out.println(sol.maxTaskAssignBrute(new int[]{10, 15, 30}, new int[]{0, 10, 10, 10, 10}, 3, 10)); // 2
}
}
复杂度分析
复杂度:O((n+m) log n)(排序 + 二分 × 校验)。
2072. The Winner University(赢得比赛的大学)
题目类型:数据库(SQL)
题目描述
表:NewYork
+-------------+------+
| Column Name | Type |
+-------------+------+
| student_id | int |
| score | int |
+-------------+------+
在 SQL 中,student_id 是这个表的主键。每一行包含纽约大学(New York University)中一名学生一次考试的成绩。
表:California
+-------------+------+
| Column Name | Type |
+-------------+------+
| student_id | int |
| score | int |
+-------------+------+
在 SQL 中,student_id 是这个表的主键。每一行包含加州大学(California University)中一名学生一次考试的成绩。
纽约大学和加州大学之间举行了一场比赛,这场比赛由两所大学中相同数量的学生参加。拥有更多优秀学生 的大学赢得这场比赛;如果两所大学的优秀学生 数量相同,则这场比赛平局。优秀学生 是指在考试中获得 90 分或更高成绩的学生。
返回:
"New York University"若纽约大学赢得这场比赛。"California University"若加州大学赢得这场比赛。"No Winner"若这场比赛平局。
示例 1:
输入:
NewYork 表:
+------------+-------+
| student_id | score |
+------------+-------+
| 1 | 90 |
| 2 | 87 |
+------------+-------+
California 表:
+------------+-------+
| student_id | score |
+------------+-------+
| 2 | 89 |
| 3 | 88 |
+------------+-------+
输出:
+---------------------+
| winner |
+---------------------+
| New York University |
+---------------------+
解释:
纽约大学有 1 名优秀学生,加州大学有 0 名优秀学生。
示例 2:
输入:
NewYork 表:
+------------+-------+
| student_id | score |
+------------+-------+
| 1 | 89 |
| 2 | 88 |
+------------+-------+
California 表:
+------------+-------+
| student_id | score |
+------------+-------+
| 2 | 90 |
| 3 | 87 |
+------------+-------+
输出:
+-----------------------+
| winner |
+-----------------------+
| California University |
+-----------------------+
解释:
纽约大学有 0 名优秀学生,加州大学有 1 名优秀学生。
示例 3:
输入:
NewYork 表:
+------------+-------+
| student_id | score |
+------------+-------+
| 1 | 89 |
| 2 | 90 |
+------------+-------+
California 表:
+------------+-------+
| student_id | score |
+------------+-------+
| 2 | 89 |
| 3 | 88 |
+------------+-------+
输出:
+-----------+
| winner |
+-----------+
| No Winner |
+-----------+
解释:
纽约大学和加州大学各有 1 名优秀学生,比赛平局。
解题思路
解法一:双表统计 + CASE 比较
分别统计两校优秀学生数(score >= 90),再用 CASE 比较两数大小:
- 纽约多 → 输出
"New York University"; - 加州多 → 输出
"California University"; - 相等 → 输出
"No Winner"。
SQL 实现要点:COUNT(score >= 90) 在 MySQL 中等于对布尔表达式为真的行计数(等价于 SUM(CASE WHEN score >= 90 THEN 1 ELSE 0 END)),可直接统计优秀学生人数。
解法二(对照):差值与符号判定 ------先分别统计两校优秀人数,再计算差值 ny - ca,按差值的正负/零输出对应学校名,逻辑更直白,便于移植到其他数据库。
代码实现
sql
-- 解法一(推荐):分别统计优秀学生数 + CASE 比较
WITH counts AS (
SELECT
(SELECT COUNT(score >= 90) FROM NewYork) AS ny,
(SELECT COUNT(score >= 90) FROM California) AS ca
)
SELECT CASE
WHEN ny > ca THEN 'New York University'
WHEN ny < ca THEN 'California University'
ELSE 'No Winner'
END AS winner
FROM counts;
sql
-- 解法二(对照):差值符号判定
SELECT CASE
WHEN ny - ca > 0 THEN 'New York University'
WHEN ny - ca < 0 THEN 'California University'
ELSE 'No Winner'
END AS winner
FROM (
SELECT
(SELECT SUM(score >= 90) FROM NewYork) AS ny,
(SELECT SUM(score >= 90) FROM California) AS ca
) t;
复杂度分析
- 时间复杂度 :
O(n + m),其中n、m分别为NewYork、California表的行数;各表只需一次扫描统计。 - 空间复杂度 :
O(1),仅存两个统计值,无额外排序或中间表。
2073. Time Needed to Buy Tickets(买票需要的时间)
题目描述
有 n 个人前来排队买票,其中第 0 人站在队伍最前方 ,第 (n - 1) 人站在队伍最后方 。
给你一个下标从 0 开始的整数数组 tickets,其中第 i 人想要购买的票数为 tickets[i]。
每个人买票都需要用掉恰好 1 秒 。一个人一次只能买一张票 ,如果需要购买更多票,他必须走到队尾
重新排队(瞬间 发生,不计时间)。如果一个人没有剩下需要买的票,那他将会离开 队伍。
返回位于位置 k(下标从 0 开始)的人完成买票需要的时间(以秒为单位)。
示例 1
tickets = [2,3,2], k = 2,输出 6。解释:第 0 人买 2 张、第 1 人买 3 张、第 2 人买 2 张,
模拟排队:1(人1),1(人2),1(人3),1(人1),1(人2),1(人3)→ 第 3 轮后第 k 人买完 2 张,用时 6。
示例 2
tickets = [5,1,1,1], k = 0,输出 8。
示例 3
tickets = [84,49,5,24,70,77,87,8], k = 3,输出 154。
解题思路
解法一(主提交版本)
数学 O(n)(主提交版本)
- 站在
k前面(i <= k)的人会完整经历tickets[k]轮(第 k 人买完后自己离开,后面的轮不再影响),
付出min(tickets[i], tickets[k])秒; - 站在
k后面的人只在前tickets[k]-1轮出现,付出min(tickets[i], tickets[k]-1)秒; - 累加即答案。
- 时间复杂度:O(n),一次线性数学计算。
- 空间复杂度 :O(1)。
解法二
队列模拟(对照) - 用队列模拟排队:队首每次买一张票(1 秒),减到 0 出队(若队首是 k 人买完即返回时刻)。
- 复杂度 O(总票数),仅用于小规模交叉验证。
代码实现
java
public class Solution {
// 解法一:数学(主提交版本)
public int timeRequiredToBuy(int[] tickets, int k) {
int n = tickets.length;
int ans = 0;
int need = tickets[k];
for (int i = 0; i < n; i++) {
ans += Math.min(tickets[i], i <= k ? need : need - 1);
}
return ans;
}
// 解法二:队列模拟(对照)
public int timeRequiredToBuySim(int[] tickets, int k) {
java.util.ArrayDeque<int[]> q = new java.util.ArrayDeque<>(); // {原下标, 剩余票数}
for (int i = 0; i < tickets.length; i++) {
q.offer(new int[]{i, tickets[i]});
}
int t = 0;
while (!q.isEmpty()) {
int[] p = q.poll();
t++;
p[1]--;
if (p[1] > 0) {
q.offer(p);
} else if (p[0] == k) {
return t;
}
}
return t;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.timeRequiredToBuy(new int[]{2, 3, 2}, 2)); // 6
System.out.println(sol.timeRequiredToBuySim(new int[]{2, 3, 2}, 2)); // 6
System.out.println(sol.timeRequiredToBuy(new int[]{5, 1, 1, 1}, 0)); // 8
System.out.println(sol.timeRequiredToBuySim(new int[]{5, 1, 1, 1}, 0)); // 8
System.out.println(sol.timeRequiredToBuy(new int[]{84, 49, 5, 24, 70, 77, 87, 8}, 3)); // 154
System.out.println(sol.timeRequiredToBuySim(new int[]{84, 49, 5, 24, 70, 77, 87, 8}, 3)); // 154
}
}
复杂度分析
- 时间复杂度:O(n),一次线性数学计算。
- 空间复杂度:O(1)。
2074. Reverse Nodes in Even Length Groups(反转偶数长度组的节点)
题目描述
给你一个链表的头节点 head。链表中的节点按顺序 划分成若干非空 组,这些非空组的长度构成一个自然数序列
(1, 2, 3, 4, ...)。一个组的长度 就是组中分配到的节点数目。即:节点 1 分配给第一组,节点 2 和 3 分配给
第二组,节点 4、5 和 6 分配给第三组,以此类推。注意最后一组的长度可能小于或等于 1 + 倒数第二组的长度。
反转 每个偶数 长度组中的节点,并返回修改后链表的头节点 head。
示例 1
head = [5,2,6,3,9,1,7,3,8,4],输出 [5,6,2,3,9,1,4,8,3,7]。
示例 2
head = [1,2,3,4,5],输出 [1,3,2,4,5]。解释:第 2 组长度为 2(节点 2、3)反转;第 3 组只有 1 个节点不反转。
示例 3
head = [10],输出 [10]。
解题思路
解法一(主提交版本)
分组遍历 + 局部反转(主提交版本)
- 组长度依次为 1, 2, 3, ...,用
prev指向每组头节点的前驱(dummy)。 - 每轮先计算本组实际长度:
(1+l)*l/2 <= n时整组长度为 l,否则组尾是剩余n - l*(l-1)/2个节点。 - 若该组长度为偶数,把整组就地反转(
reverse(node, len)返回新头并接好尾部);再让prev前进 len 步。
复杂度:O(n)(每个节点只被访问常数次)。
解法二
提取成数组 + 重建(对照) - 把链表值取出存数组,按组号奇偶决定反转并重新接成链表;结果一致,用于交叉验证。
代码实现
java
public class Solution {
public static class ListNode {
int val;
ListNode next;
ListNode() {}
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}
// 解法一:分组就地反转(主提交版本)
public ListNode reverseEvenLengthGroups(ListNode head) {
int n = 0;
for (ListNode t = head; t != null; t = t.next) {
n++;
}
ListNode dummy = new ListNode(0, head);
ListNode prev = dummy;
int l = 1;
// 处理前 l 组,直到剩余节点不足完整组
for (; (1 + l) * l / 2 <= n && prev != null; l++) {
if (l % 2 == 0) {
ListNode node = prev.next;
prev.next = reverse(node, l);
}
for (int i = 0; i < l && prev != null; i++) {
prev = prev.next;
}
}
int left = n - l * (l - 1) / 2; // 最后一组剩余节点数
if (left > 0 && left % 2 == 0) {
ListNode node = prev.next;
prev.next = reverse(node, left);
}
return dummy.next;
}
// 反转从 head 起前 l 个节点,返回新头并保持尾部拼接
private ListNode reverse(ListNode head, int l) {
ListNode prev = null;
ListNode cur = head;
ListNode tail = cur;
int i = 0;
while (cur != null && i < l) {
ListNode t = cur.next;
cur.next = prev;
prev = cur;
cur = t;
i++;
}
tail.next = cur;
return prev;
}
// 解法二:数组重建(对照)
public ListNode reverseEvenLengthGroupsArr(ListNode head) {
java.util.List<Integer> vals = new java.util.ArrayList<>();
for (ListNode p = head; p != null; p = p.next) {
vals.add(p.val);
}
int n = vals.size();
java.util.List<Integer> out = new java.util.ArrayList<>();
int idx = 0, len = 1;
while (idx < n) {
int curLen = Math.min(len, n - idx);
java.util.List<Integer> seg = new java.util.ArrayList<>();
for (int t = 0; t < curLen; t++) {
seg.add(vals.get(idx + t));
}
if (curLen % 2 == 0) {
java.util.Collections.reverse(seg);
}
out.addAll(seg);
idx += curLen;
len++;
}
ListNode dummy = new ListNode(0);
ListNode tail = dummy;
for (int v : out) {
tail.next = new ListNode(v);
tail = tail.next;
}
return dummy.next;
}
private static void print(ListNode h) {
StringBuilder sb = new StringBuilder();
while (h != null) {
if (sb.length() > 0) {
sb.append(",");
}
sb.append(h.val);
h = h.next;
}
System.out.println(sb.toString());
}
private static ListNode build(int[] a) {
ListNode dummy = new ListNode(0);
ListNode tail = dummy;
for (int x : a) {
tail.next = new ListNode(x);
tail = tail.next;
}
return dummy.next;
}
public static void main(String[] args) {
Solution sol = new Solution();
print(sol.reverseEvenLengthGroups(build(new int[]{5, 2, 6, 3, 9, 1, 7, 3, 8, 4}))); // 5,6,2,3,9,1,4,8,3,7
print(sol.reverseEvenLengthGroupsArr(build(new int[]{5, 2, 6, 3, 9, 1, 7, 3, 8, 4})));
print(sol.reverseEvenLengthGroups(build(new int[]{1, 2, 3, 4, 5}))); // 1,3,2,4,5
print(sol.reverseEvenLengthGroupsArr(build(new int[]{1, 2, 3, 4, 5})));
print(sol.reverseEvenLengthGroups(build(new int[]{10}))); // 10
print(sol.reverseEvenLengthGroupsArr(build(new int[]{10})));
}
}
复杂度分析
复杂度:O(n)(每个节点只被访问常数次)。
2075. Decode the Slanted Ciphertext(解码斜向换位密码)
题目描述
字符串 originalText 使用斜向换位密码 ,经由行数固定 为 rows 的矩阵辅助,加密得到一个字符串 encodedText。
originalText 先按从左上到右下的方式(斜对角逐行)放置到矩阵中:
先填第 1 条斜线((0,0)),再填第 2 条斜线((0,1),(1,0)),依次类推,直到到达 originalText 末尾。
所有空单元格用 ' ' 填充。矩阵的列数需满足:用 originalText 填充之后,最右侧列不为空 。
接着按行将字符附加到矩阵中构造 encodedText。
给你编码后的字符串 encodedText 和矩阵的行数 rows,返回源字符串 originalText。
注意:originalText 不 含任何尾随空格 ' ';生成的测试用例满足仅存在一个 可能的 originalText。
示例 1
encodedText = "ch ie pr", rows = 3,输出 "cipher"。
示例 2
encodedText = "iveo eed l te olc", rows = 4,输出 "i love leetcode"。
示例 3
encodedText = "coding", rows = 1,输出 "coding"。
解题思路
解法一(主提交版本)
按列逆推 + 去尾随空格(主提交版本)
- 行数为
rows,列数cols = len(encodedText) / rows。 - 斜向填充时,同一斜线上的格子满足
x + y = 常量;反向读取时,从第 0 列开始逐条斜线:
(0,j), (1,j+1), (2,j+2), ...(x 增、y 增)取出字符连接,即还原 originalText 的放置顺序。 - 连接后去掉末尾的空格(originalText 无尾随空格)。
复杂度:O(len(encodedText))。
解法二
显式构造矩阵再读斜线(对照) - 把 encodedText 按行填入 rows×cols 矩阵,再按斜线顺序逐格读字符,逻辑等价。
- 用于交叉验证。
代码实现
java
public class Solution {
// 解法一:按列逆推(主提交版本)
public String decodeCiphertext(String encodedText, int rows) {
int cols = encodedText.length() / rows;
StringBuilder sb = new StringBuilder();
for (int j = 0; j < cols; j++) {
for (int x = 0, y = j; x < rows && y < cols; x++, y++) {
sb.append(encodedText.charAt(x * cols + y));
}
}
while (sb.length() > 0 && sb.charAt(sb.length() - 1) == ' ') {
sb.deleteCharAt(sb.length() - 1);
}
return sb.toString();
}
// 解法二:矩阵显式(对照)
public String decodeCiphertextMatrix(String encodedText, int rows) {
int cols = encodedText.length() / rows;
char[][] m = new char[rows][cols];
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
m[i][j] = encodedText.charAt(i * cols + j);
}
}
StringBuilder sb = new StringBuilder();
for (int j = 0; j < cols; j++) {
for (int x = 0, y = j; x < rows && y < cols; x++, y++) {
sb.append(m[x][y]);
}
}
while (sb.length() > 0 && sb.charAt(sb.length() - 1) == ' ') {
sb.deleteCharAt(sb.length() - 1);
}
return sb.toString();
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.decodeCiphertext("ch ie pr", 3)); // cipher
System.out.println(sol.decodeCiphertextMatrix("ch ie pr", 3)); // cipher
System.out.println(sol.decodeCiphertext("iveo eed l te olc", 4)); // i love leetcode
System.out.println(sol.decodeCiphertextMatrix("iveo eed l te olc", 4));
System.out.println(sol.decodeCiphertext("coding", 1)); // coding
System.out.println(sol.decodeCiphertextMatrix("coding", 1));
}
}
复杂度分析
复杂度:O(len(encodedText))。
2076. Process Restricted Friend Requests(处理含限制条件的好友请求)
题目描述
给你一个整数 n,表示网络上的用户数目,用户按从 0 到 n - 1 编号。
给你一个下标从 0 开始的二维整数数组 restrictions,其中 restrictions[i] = [xi, yi] 意味着用户 xi
和用户 yi不能 成为朋友 ,不管是直接 还是通过其他用户间接 。
最初,用户里没有人是其他用户的朋友。给你一个下标从 0 开始的二维整数数组 requests 表示好友请求的列表,
其中 requests[j] = [uj, vj] 是用户 uj 和用户 vj 之间的一条好友请求。
如果 uj 和 vj 可以成为朋友 ,那么好友请求将会成功 。每个好友请求都会按列表中给出的顺序处理。
一旦请求成功,那么对所有未来的好友请求而言,uj 和 vj 将会成为直接朋友 。
返回一个布尔数组 result:如果第 j 个好友请求成功 ,那么 result[j] 就是 true;否则为 false。
注意:如果 uj 和 vj 已经是直接朋友,那么他们之间的请求将仍然成功。
示例 1
n = 3, restrictions = [[0,1]], requests = [[0,2],[2,1]],输出 [true,false]。
示例 2
n = 3, restrictions = [[0,1]], requests = [[1,2],[0,2]],输出 [true,false]。
示例 3
n = 5, restrictions = [[0,1],[1,2],[2,3]], requests = [[0,4],[1,2],[3,1],[3,4]],输出 [true,false,true,false]。
解题思路
解法一(主提交版本)
并查集 + 限制检查(主提交版本)
- 用并查集维护"已是朋友"的连通块。处理请求
(u, v)时: - 若
find(u) == find(v)已同块,直接成功; - 否则临时假设 合并两块
pu、pv,检查所有限制(x, y):若合并后x与y落入同一块
(即(find(x)==pu && find(y)==pv)或相反),则该请求会违反限制,失败;全部不冲突则真正合并、成功。 - 注意:检查时用的是"若合并"的连通关系,不需要真的先合并再回滚(用根编号比较即可)。
复杂度:O(q × r × α(n)),q 为请求数,r 为限制数。
解法二
图的连通性暴力模拟(对照) - 维护邻接矩阵(同一连通块内任意两点视为直接朋友),对每条请求检查合并后是否会"打通"某条限制
(两块中任一点与任一点的间接路径),相当于 Floyd 增量维护。仅用于小规模交叉验证。
代码实现
java
public class Solution {
private int[] p;
// 解法一:并查集(主提交版本)
public boolean[] friendRequests(int n, int[][] restrictions, int[][] requests) {
p = new int[n];
for (int i = 0; i < n; i++) {
p[i] = i;
}
int m = requests.length;
boolean[] ans = new boolean[m];
for (int i = 0; i < m; i++) {
int u = requests[i][0], v = requests[i][1];
int pu = find(u), pv = find(v);
if (pu == pv) {
ans[i] = true; // 已是朋友
continue;
}
boolean ok = true;
for (int[] r : restrictions) {
int rx = find(r[0]), ry = find(r[1]);
// 若合并 pu、pv 后 r 的两个端点被连到同一块,则冲突
if ((rx == pu && ry == pv) || (rx == pv && ry == pu)) {
ok = false;
break;
}
// 注意:若 rx、ry 一个在 pu 下、一个在 pv 下才冲突;
// 已在同块(rx == ry)不影响本次合并
}
if (ok) {
ans[i] = true;
p[pu] = pv; // 真正合并
}
}
return ans;
}
private int find(int x) {
if (p[x] != x) {
p[x] = find(p[x]);
}
return p[x];
}
// 解法二:增量 Floyd 式连通模拟(对照)
public boolean[] friendRequestsSim(int n, int[][] restrictions, int[][] requests) {
boolean[][] g = new boolean[n][n];
for (int i = 0; i < n; i++) {
g[i][i] = true;
}
int m = requests.length;
boolean[] ans = new boolean[m];
for (int i = 0; i < m; i++) {
int u = requests[i][0], v = requests[i][1];
if (g[u][v]) { // 已连通(直接或间接)
ans[i] = true;
continue;
}
// 模拟合并两个连通块:任意 a∈块(u)、b∈块(v) 相连
boolean ok = true;
java.util.List<Integer> bu = new java.util.ArrayList<>();
java.util.List<Integer> bv = new java.util.ArrayList<>();
for (int a = 0; a < n; a++) {
if (g[u][a]) {
bu.add(a);
}
if (g[v][a]) {
bv.add(a);
}
}
for (int a : bu) {
for (int b : bv) {
for (int[] r : restrictions) {
if ((r[0] == a && r[1] == b) || (r[0] == b && r[1] == a)) {
ok = false;
break;
}
}
if (!ok) {
break;
}
}
if (!ok) {
break;
}
}
if (ok) {
ans[i] = true;
for (int a : bu) {
for (int b : bv) {
g[a][b] = true;
g[b][a] = true;
}
}
}
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(java.util.Arrays.toString(sol.friendRequests(3, new int[][]{{0, 1}}, new int[][]{{0, 2}, {2, 1}}))); // [true, false]
System.out.println(java.util.Arrays.toString(sol.friendRequestsSim(3, new int[][]{{0, 1}}, new int[][]{{0, 2}, {2, 1}}))); // [true, false]
System.out.println(java.util.Arrays.toString(sol.friendRequests(3, new int[][]{{0, 1}}, new int[][]{{1, 2}, {0, 2}}))); // [true, false]
System.out.println(java.util.Arrays.toString(sol.friendRequestsSim(3, new int[][]{{0, 1}}, new int[][]{{1, 2}, {0, 2}}))); // [true, false]
System.out.println(java.util.Arrays.toString(sol.friendRequests(5, new int[][]{{0, 1}, {1, 2}, {2, 3}}, new int[][]{{0, 4}, {1, 2}, {3, 1}, {3, 4}}))); // [true,false,true,false]
System.out.println(java.util.Arrays.toString(sol.friendRequestsSim(5, new int[][]{{0, 1}, {1, 2}, {2, 3}}, new int[][]{{0, 4}, {1, 2}, {3, 1}, {3, 4}}))); // [true,false,true,false]
}
}
复杂度分析
复杂度:O(q × r × α(n)),q 为请求数,r 为限制数。
2077. Paths in Maze That Lead to Same Room(殊途同归)
题目描述
迷宫由 n 个从 1 到 n 的房间组成,有些房间由走廊连接。给定一个二维整数数组 corridors,
其中 corridors[i] = [room1i, room2i] 表示有一条走廊连接 room1i 和 room2i,两个方向皆可通行。
迷宫的设计者想知道迷宫有多让人困惑。迷宫的混乱分数 是长度为 3 的不同的环的数量。
例如 1 → 2 → 3 → 1 是长度为 3 的环,但 1 → 2 → 3 → 4 和 1 → 2 → 3 → 2 → 1 不是。
如果在第一个环中访问的一个或多个房间不在 第二个环中,则认为两个环是不同 的。
返回迷宫的混乱分数。
示例 1
n = 5, corridors = [[1,2],[5,2],[4,1],[2,4],[3,1],[3,4]],输出 2。
示例 2
n = 4, corridors = [[1,2],[3,4]],输出 0。
示例 3
n = 5, corridors = [[1,2],[2,3],[3,1]],输出 1。
解题思路
解法一(主提交版本)
枚举中间点 + 邻接集合(主提交版本)
- 长度为 3 的环即一个三角形三元组
(a, b, c),三条边两两存在。 - 对每个节点
c,枚举它的两个邻居a、b,若a与b也相邻则构成一个三角形。
每个三角形会被 3 个顶点各数到一次,故答案 = 总计数 / 3。 - 用
Set<Integer>[]存邻接表,contains判断边是否存在。
复杂度:O(sum(deg©^2)),n ≤ 100 可接受。
解法二
暴力三重枚举(对照) - 枚举所有
(i, j, k)(i<j<k)检查三条边是否都在,统计三角形个数。 - 复杂度 O(n^3),仅用于交叉验证。
代码实现
java
public class Solution {
// 解法一:枚举中间点(主提交版本)
public int numberOfPaths(int n, int[][] corridors) {
java.util.Set<Integer>[] g = new java.util.HashSet[n + 1];
for (int i = 1; i <= n; i++) {
g[i] = new java.util.HashSet<>();
}
for (int[] c : corridors) {
g[c[0]].add(c[1]);
g[c[1]].add(c[0]);
}
int cnt = 0;
for (int c = 1; c <= n; c++) {
java.util.List<Integer> nxt = new java.util.ArrayList<>(g[c]);
for (int i = 0; i < nxt.size(); i++) {
for (int j = i + 1; j < nxt.size(); j++) {
int a = nxt.get(i), b = nxt.get(j);
if (g[b].contains(a)) { // a 与 b 也相邻 -> 三角形
cnt++;
}
}
}
}
return cnt / 3;
}
// 解法二:三重枚举(对照)
public int numberOfPathsBrute(int n, int[][] corridors) {
boolean[][] adj = new boolean[n + 1][n + 1];
for (int[] c : corridors) {
adj[c[0]][c[1]] = true;
adj[c[1]][c[0]] = true;
}
int ans = 0;
for (int i = 1; i <= n; i++) {
for (int j = i + 1; j <= n; j++) {
for (int k = j + 1; k <= n; k++) {
if (adj[i][j] && adj[j][k] && adj[i][k]) {
ans++;
}
}
}
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.numberOfPaths(5, new int[][]{{1, 2}, {5, 2}, {4, 1}, {2, 4}, {3, 1}, {3, 4}})); // 2
System.out.println(sol.numberOfPathsBrute(5, new int[][]{{1, 2}, {5, 2}, {4, 1}, {2, 4}, {3, 1}, {3, 4}})); // 2
System.out.println(sol.numberOfPaths(4, new int[][]{{1, 2}, {3, 4}})); // 0
System.out.println(sol.numberOfPathsBrute(4, new int[][]{{1, 2}, {3, 4}})); // 0
System.out.println(sol.numberOfPaths(5, new int[][]{{1, 2}, {2, 3}, {3, 1}})); // 1
System.out.println(sol.numberOfPathsBrute(5, new int[][]{{1, 2}, {2, 3}, {3, 1}})); // 1
}
}
复杂度分析
复杂度:O(sum(deg©^2)),n ≤ 100 可接受。
2078. Two Furthest Houses With Different Colors(两栋颜色不同且距离最远的房子)
题目描述
街上有 n 栋房子整齐地排成一列,每栋房子都粉刷上了漂亮的颜色。给你一个下标从 0 开始且长度为 n 的
整数数组 colors,其中 colors[i] 表示第 i 栋房子的颜色。
返回两栋 颜色不同 房子之间的最大 距离。第 i 栋房子和第 j 栋房子之间的距离是 abs(i - j)。
示例 1
colors = [1,1,1,6,1,1,1],输出 3。解释:下标 0 与 3 颜色不同(1 vs 6),距离 3 为最大。
示例 2
colors = [1,8,3,8,3],输出 4。解释:下标 0 与 4 颜色不同,距离 4 最大。
示例 3
colors = [0,1],输出 1。
解题思路
解法一(主提交版本)
双端贪心(主提交版本)
- 最优的一对不同色房子,其中一栋必是首栋或尾栋:
- 若首栋与尾栋颜色不同,答案即
n - 1; - 否则首尾同色
c:从左找第一个颜色 ≠ c 的下标i(与首栋配对得n-1-i),
从右找第一个颜色 ≠ c 的下标j(与尾栋配对得j),取两者较大。 - 实际上任何一对不同色房子,其端点之一必有一端是数组极左或极右,以上覆盖全部最优。
- 时间复杂度:O(n),双端贪心单次扫描。
- 空间复杂度 :O(1)。
解法二
双重循环(对照) - 枚举所有
(i, j),若颜色不同更新最大距离。O(n^2),仅用于交叉验证。
代码实现
java
public class Solution {
// 解法一:双端贪心(主提交版本)
public int maxDistance(int[] colors) {
int n = colors.length;
if (colors[0] != colors[n - 1]) {
return n - 1;
}
int c = colors[0];
int i = 1;
while (i < n && colors[i] == c) {
i++;
}
int j = n - 2;
while (j >= 0 && colors[j] == c) {
j--;
}
return Math.max(n - 1 - i, j);
}
// 解法二:双重循环(对照)
public int maxDistanceBrute(int[] colors) {
int n = colors.length;
int best = 0;
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
if (colors[i] != colors[j]) {
best = Math.max(best, j - i);
}
}
}
return best;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.maxDistance(new int[]{1, 1, 1, 6, 1, 1, 1})); // 3
System.out.println(sol.maxDistanceBrute(new int[]{1, 1, 1, 6, 1, 1, 1})); // 3
System.out.println(sol.maxDistance(new int[]{1, 8, 3, 8, 3})); // 4
System.out.println(sol.maxDistanceBrute(new int[]{1, 8, 3, 8, 3})); // 4
System.out.println(sol.maxDistance(new int[]{0, 1})); // 1
System.out.println(sol.maxDistanceBrute(new int[]{0, 1})); // 1
}
}
复杂度分析
- 时间复杂度:O(n),双端贪心单次扫描。
- 空间复杂度:O(1)。
2079. Watering Plants(给植物浇水)
题目描述
你打算用一个水罐给花园里的 n 株植物浇水。植物排成一行,从左到右进行标记,编号从 0 到 n - 1。
其中,第 i 株植物的位置是 x = i。x = -1 处有一条河,你可以在那里重新灌满你的水罐。
每一株植物都需要浇特定量的水。你将会按下面描述的方式完成浇水:
- 按从左到右的顺序给植物浇水;
- 在给当前植物浇完水之后,如果你没有足够的水完全浇灌下一株植物,那么你就需要返回河边重新装满水罐;
- 你不能 提前重新灌满水罐。
最初,你在河边(x = -1),在x轴上每移动一个单位 都需要一步 。
给你一个下标从 0 开始的整数数组plants,其中plants[i]为第i株植物需要的水量,另有一个整数
capacity表示水罐的容量,返回浇灌所有植物需要的步数。
示例 1
plants = [2,2,3,3], capacity = 5,输出 14。
示例 2
plants = [1,1,1,4,2,3], capacity = 4,输出 30。
示例 3
plants = [7,7,7,7,7,7,7], capacity = 8,输出 49。
解题思路
解法一(主提交版本)
线性模拟(主提交版本)
- 从左到右处理每株植物:
- 若水罐剩余水 ≥ 当前植物需水量:直接浇,步数 +1,水减少;
- 否则先回河边装满(往返步数
i*2,当前在第 i 株位置,回河 i 步 + 回来 i 步),
再浇当前植物步数 +1;装满后水 =capacity - plants[i]。 - 时间复杂度:O(n),单次线性模拟。
- 空间复杂度 :O(1)。
解法二
队列模拟(对照) - 逐株模拟,显式用"当前位置"变量累加步数并处理回边灌水,逻辑展开更直观;结果一致用于交叉验证。
代码实现
java
public class Solution {
// 解法一:线性模拟(主提交版本)
public int wateringPlants(int[] plants, int capacity) {
int ans = 0;
int water = capacity;
for (int i = 0; i < plants.length; i++) {
if (water >= plants[i]) {
water -= plants[i];
ans += 1;
} else {
ans += i * 2 + 1; // 从 i 回河边(i步) + 回 i(i步) + 浇(1步)
water = capacity - plants[i];
}
}
return ans;
}
// 解法二:显式位置模拟(对照)
public int wateringPlantsSim(int[] plants, int capacity) {
int pos = -1; // 当前位置,-1 表示河边
int water = capacity;
int steps = 0;
for (int i = 0; i < plants.length; i++) {
if (water < plants[i]) {
steps += Math.abs(-1 - pos); // 回河边
water = capacity;
steps += Math.abs(i - (-1)); // 装满后从河边到 i
} else {
steps += Math.abs(i - pos); // 直接走到 i
}
water -= plants[i];
pos = i;
}
return steps;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.wateringPlants(new int[]{2, 2, 3, 3}, 5)); // 14
System.out.println(sol.wateringPlantsSim(new int[]{2, 2, 3, 3}, 5)); // 14
System.out.println(sol.wateringPlants(new int[]{1, 1, 1, 4, 2, 3}, 4)); // 30
System.out.println(sol.wateringPlantsSim(new int[]{1, 1, 1, 4, 2, 3}, 4)); // 30
System.out.println(sol.wateringPlants(new int[]{7, 7, 7, 7, 7, 7, 7}, 8)); // 49
System.out.println(sol.wateringPlantsSim(new int[]{7, 7, 7, 7, 7, 7, 7}, 8)); // 49
}
}
复杂度分析
- 时间复杂度:O(n),单次线性模拟。
- 空间复杂度:O(1)。
2080. Range Frequency Queries(区间内查询数字的频率)
题目描述
请你设计一个数据结构,它能求出给定子数组内一个给定值的频率 。子数组中一个值的频率 指的是这个子数组中
这个值的出现次数。
请你实现 RangeFreqQuery 类:
RangeFreqQuery(int[] arr)用下标从 0 开始的整数数组arr构造一个类的实例;int query(int left, int right, int value)返回子数组arr[left...right]中value的频率 。
一个子数组 指的是数组中一段连续的元素;arr[left...right]包含下标left和right在内。
示例
输入 ["RangeFreqQuery","query","query"]
[[[12,33,4,56,22,2,34,33,22,12,34,56]],[1,2,4],[0,11,33]]
输出 [null,1,2]。
解题思路
解法一(主提交版本)
值 -> 下标列表 + 二分(主提交版本)
- 预处理:
Map<Integer, List<Integer>>,把每个值出现的所有下标按升序存入列表。 - 查询
[left, right]中value的频率:在value的下标列表中二分求
first >= left的位置l与first > right的位置r,答案 =r - l。
复杂度:预处理 O(n),单次查询 O(log n)。
解法二
直接扫描子数组(对照) - 每次查询遍历
arr[left..right]计数。O(区间长度),仅用于交叉验证。
代码实现
java
public class Solution {
private java.util.Map<Integer, java.util.List<Integer>> idx;
// 解法一:哈希表 + 二分(主提交版本)
public Solution(int[] arr) {
idx = new java.util.HashMap<>();
for (int i = 0; i < arr.length; i++) {
java.util.List<Integer> list = idx.get(arr[i]);
if (list == null) {
list = new java.util.ArrayList<>();
idx.put(arr[i], list);
}
list.add(i);
}
}
public int query(int left, int right, int value) {
java.util.List<Integer> list = idx.get(value);
if (list == null) {
return 0;
}
int l = lowerBound(list, left); // 第一个 >= left
int r = lowerBound(list, right + 1); // 第一个 > right
return r - l;
}
private int lowerBound(java.util.List<Integer> list, int target) {
int lo = 0, hi = list.size();
while (lo < hi) {
int mid = (lo + hi) >> 1;
if (list.get(mid) < target) {
lo = mid + 1;
} else {
hi = mid;
}
}
return lo;
}
// 解法二:扫描(对照)
private int[] raw;
public void initBrute(int[] arr) {
raw = arr;
}
public int queryBrute(int left, int right, int value) {
int c = 0;
for (int i = left; i <= right; i++) {
if (raw[i] == value) {
c++;
}
}
return c;
}
public static void main(String[] args) {
Solution main = new Solution(new int[]{12, 33, 4, 56, 22, 2, 34, 33, 22, 12, 34, 56});
System.out.println(main.query(1, 2, 4)); // 1
System.out.println(main.query(0, 11, 33)); // 2
Solution brute = new Solution(new int[]{});
brute.initBrute(new int[]{12, 33, 4, 56, 22, 2, 34, 33, 22, 12, 34, 56});
System.out.println(brute.queryBrute(1, 2, 4)); // 1
System.out.println(brute.queryBrute(0, 11, 33)); // 2
System.out.println(main.query(2, 9, 22)); // 2
System.out.println(brute.queryBrute(2, 9, 22)); // 2
}
}
复杂度分析
复杂度:预处理 O(n),单次查询 O(log n)。