LeetCode 2241-2260. 题解合集
文章目录
- [LeetCode 2241-2260. 题解合集](#LeetCode 2241-2260. 题解合集)
-
- [2241. Design an ATM Machine(设计一个 ATM 机器)](#2241. Design an ATM Machine(设计一个 ATM 机器))
- [2242. Maximum Score of a Node Sequence(节点序列的最大得分)](#2242. Maximum Score of a Node Sequence(节点序列的最大得分))
- [2243. Calculate Digit Sum of a String(计算字符串的数字和)](#2243. Calculate Digit Sum of a String(计算字符串的数字和))
- [2244. Minimum Rounds to Complete All Tasks(完成所有任务需要的最少轮数)](#2244. Minimum Rounds to Complete All Tasks(完成所有任务需要的最少轮数))
- [2245. Maximum Trailing Zeros in a Cornered Path(转角路径的乘积中最多能有几个尾随零)](#2245. Maximum Trailing Zeros in a Cornered Path(转角路径的乘积中最多能有几个尾随零))
- [2246. Longest Path With Different Adjacent Characters(相邻字符不同的最长路径)](#2246. Longest Path With Different Adjacent Characters(相邻字符不同的最长路径))
- [2247. Maximum Cost of Trip With K Highways(K 条高速公路的最大旅行费用)](#2247. Maximum Cost of Trip With K Highways(K 条高速公路的最大旅行费用))
- [2248. Intersection of Multiple Arrays(多个数组求交集)](#2248. Intersection of Multiple Arrays(多个数组求交集))
- [2249. Count Lattice Points Inside a Circle(统计圆内格点数目)](#2249. Count Lattice Points Inside a Circle(统计圆内格点数目))
- [2250. Count Number of Rectangles Containing Each Point(统计包含每个点的矩形数目)](#2250. Count Number of Rectangles Containing Each Point(统计包含每个点的矩形数目))
- [2251. Number of Flowers in Full Bloom(花期内花的数目)](#2251. Number of Flowers in Full Bloom(花期内花的数目))
- [2252. Dynamic Pivoting of a Table(表的动态旋转)](#2252. Dynamic Pivoting of a Table(表的动态旋转))
- [2253. Dynamic Unpivoting of a Table(动态取消表的旋转)](#2253. Dynamic Unpivoting of a Table(动态取消表的旋转))
- [2254. Design Video Sharing Platform(设计视频共享平台)](#2254. Design Video Sharing Platform(设计视频共享平台))
- [2255. Count Prefixes of a Given String(统计是给定字符串前缀的字符串数目)](#2255. Count Prefixes of a Given String(统计是给定字符串前缀的字符串数目))
- [2256. Minimum Average Difference(最小平均差)](#2256. Minimum Average Difference(最小平均差))
- [2257. Count Unguarded Cells in the Grid(统计网格图中没有被保卫的格子数)](#2257. Count Unguarded Cells in the Grid(统计网格图中没有被保卫的格子数))
- [2258. Escape the Spreading Fire(逃离火灾)](#2258. Escape the Spreading Fire(逃离火灾))
- [2259. Remove Digit From Number to Maximize Result(移除指定数字得到的最大结果)](#2259. Remove Digit From Number to Maximize Result(移除指定数字得到的最大结果))
- [2260. Minimum Consecutive Cards to Pick Up(必须拿起的最小连续卡牌数)](#2260. Minimum Consecutive Cards to Pick Up(必须拿起的最小连续卡牌数))
2241. Design an ATM Machine(设计一个 ATM 机器)
题目描述
一个 ATM 机器,存有 5 种面值的钞票:20、50、100、200 和 500 美元。初始时,ATM 机是空的。用户可以用它存或者取任意数目的钱。取款时,机器会优先取较大 数额的钱。请你实现 ATM 类:ATM() 初始化 ATM 对象;void deposit(int[] banknotesCount) 分别存入 $20、$50、$100、$200 和 $500 钞票的数目;int[] withdraw(int amount) 返回一个长度为 5 的数组,分别表示 $20、$50、$100、$200 和 $500 钞票的数目,并且更新 ATM 机里取款后钞票的剩余数量;如果无法取出指定数额的钱,返回 [-1](这种情况下不取出任何钞票)。
示例 1:
输入:["ATM", "deposit", "withdraw", "deposit", "withdraw", "withdraw"]
参数:[[], [[0,0,1,2,1]], [600], [[0,1,0,1,1]], [600], [550]]
输出:[null, null, [0,0,1,0,1], null, [-1], [0,1,0,0,1]]
解释:先存 1 张 $100、2 张 $200 和 1 张 $500;取 600 返回 1 张 $100 和 1 张 $500;再存 1 张 $50、1 张 $200 和 1 张 $500 后取 600 被拒绝;最后取 550 返回 1 张 $50 和 1 张 $500。
约束:
-
banknotesCount.length == 5 -
0 <= banknotesCount[i] <= 10⁹ -
1 <= amount <= 10⁹ -
withdraw调用总次数不超过10⁵ -
deposit与withdraw调用总次数不超过10⁵
解题思路
核心思想 :贪心 + 存量数组。面值固定为 {20, 50, 100, 200, 500},取款时从大面值到小面值尽量多取,若最后剩余金额为 0 说明可精确凑出,才真正扣减存量。
步骤:
- 用
long[] cnt记录五种面值钞票的张数(张数可达10⁹,累加可能溢出int,故用long)。 deposit直接累加对应面值张数。withdraw(amount):从面值500开始,每张面值最多取min(cnt[i], amount / vals[i])张,amount同步减少。- 若
amount != 0说明凑不出,返回[-1]且不动存量;否则扣减存量并返回五张数的数组。
注意 :贪心在大面值优先下可能"看似能取实则取不了"(如示例中的 $600),这正是题目要求的行为------优先取较大数额、取不出就整单拒绝。
代码实现
java
import java.util.Arrays;
/**
* 2241. Design an ATM Machine 设计一个 ATM 机器
*
* 一个 ATM 机器,存有 5 种面值的钞票:20 、50 、100 、200 和 500 美元。初始时 ATM 机是空的,
* 用户可以用它存或者取任意数目的钱。取款时机器会优先取较大数额的钱。实现 ATM 类:
* - ATM() 初始化 ATM 对象。
* - void deposit(int[] banknotesCount) 分别存入 20、50、100、200 和 500 美元钞票的数目。
* - int[] withdraw(int amount) 返回一个长度为 5 的数组,分别表示各面值取出的钞票数目,并更新 ATM 机
* 里取款后钞票的剩余数量。如果无法取出指定数额的钱,返回 [-1](此时不取出任何钞票)。
*
* 算法思路:用长度为 5 的数组 cnt 记录每种面值钞票的存量,面值数组 vals = {20,50,100,200,500}。
* 取款时从大面值到小面值贪心:每张面值最多取 min(存量, amount / 面值) 张,累加到答案并扣减 amount;
* 若最后 amount == 0 说明可精确凑出,才真正从存量中扣减;否则返回 [-1] 且不修改存量。
*
* 时间复杂度:deposit O(1),withdraw O(1);空间复杂度:O(1)
*/
public class Solution {
private static final int[] VALS = {20, 50, 100, 200, 500};
private long[] cnt = new long[5];
/** 分别存入各类面值的钞票数目 */
public void deposit(int[] banknotesCount) {
for (int i = 0; i < 5; i++) {
cnt[i] += banknotesCount[i];
}
}
/** 取款 amount,返回各面值取出的张数;无法凑出时返回 [-1] */
public int[] withdraw(int amount) {
long[] take = new long[5];
// 从大面值到小面值贪心
for (int i = 4; i >= 0; i--) {
long need = Math.min(cnt[i], amount / VALS[i]);
amount -= need * VALS[i];
take[i] = need;
}
if (amount != 0) {
return new int[]{-1};
}
// 凑出成功才真正扣减存量
for (int i = 0; i < 5; i++) {
cnt[i] -= take[i];
}
return new int[]{(int) take[0], (int) take[1], (int) take[2], (int) take[3], (int) take[4]};
}
public static void main(String[] args) {
Solution at = new Solution();
at.deposit(new int[]{0, 0, 1, 2, 1});
// 预期 [0,0,1,0,1]
System.out.println(Arrays.toString(at.withdraw(600)));
at.deposit(new int[]{0, 1, 0, 1, 1});
// 预期 [-1]
System.out.println(Arrays.toString(at.withdraw(600)));
// 预期 [0,1,0,0,1]
System.out.println(Arrays.toString(at.withdraw(550)));
Solution at2 = new Solution();
at2.deposit(new int[]{0, 1, 0, 1, 1});
// 预期 [0,0,0,0,1]:500 面值
System.out.println(Arrays.toString(at2.withdraw(550)));
// 预期 [-1]
System.out.println(Arrays.toString(at2.withdraw(600)));
}
}
复杂度分析
- 解法:贪心取款(主提交版本)
- 时间复杂度:O(1) --- 固定 5 种面值
- 空间复杂度:O(1) --- 常量尺寸数组
2242. Maximum Score of a Node Sequence(节点序列的最大得分)
题目描述
给你一个 n 个节点的无向图 ,节点编号为 0 到 n - 1。给你一个下标从 0 开始的整数数组 scores,其中 scores[i] 是第 i 个节点的分数。同时给你一个二维整数数组 edges,其中 edges[i] = [ai, bi] 表示节点 ai 和 bi 之间有一条无向 边。一个合法的节点序列满足:序列中每相邻 节点之间有边相连,且序列中没有节点出现超过一次。节点序列的分数定义为序列中节点分数之和 。返回一个长度为 4 的合法节点序列的最大分数,如果不存在这样的序列返回 -1。
示例 1:
输入:scores = [5,2,9,8,4], edges = [[0,1],[1,2],[2,3],[0,2],[1,3],[2,4]]
输出:24
解释:节点序列 [0,1,2,3] 的分数为 5 + 2 + 9 + 8 = 24,可证无更高。
示例 2:
输入:scores = [9,20,6,4,11,12], edges = [[0,3],[5,3],[2,4],[1,3]]
输出:-1
解释:不存在长度为 4 的合法序列。
约束:
-
n == scores.length -
4 <= n <= 10⁵ -
1 <= scores[i] <= 10⁶ -
1 <= edges.length <= 10⁵ -
edges[i].length == 2 -
0 <= ai, bi <= n - 1 -
ai != bi -
无重边
解题思路
核心思想 :枚举中边 + 邻居 Top3。任意长度为 4 的合法序列形如 a - u - v - b,其中 (u, v) 是一条边。固定这条中边后,a 应从 u 的邻居里选分数最大者,b 应从 v 的邻居里选分数最大者,同时保证四个节点互不相同。
步骤:
- 建邻接表,邻居存为
(节点, 分数)。 - 每个节点的邻居按分数降序 排序,每个节点只保留前 3 个 邻居------因为需要考虑的排除项最多 2 个(
v与另一个端点),Top3 足够包含最优选择。 - 枚举每条边
(u, v),双重遍历u的 Top3 邻居a与v的 Top3 邻居b,跳过b == u、a == v、a == b的冲突组合,更新scores[a] + scores[u] + scores[v] + scores[b]的最大值。
复杂度:排序 O(n log n)(按度),枚举 O(|E| × 9),总体 O(n log n + |E|)。
代码实现
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* 2242. Maximum Score of a Node Sequence 节点序列的最大得分
*
* 给你一个 n 个节点的无向图,节点编号为 0 到 n - 1。给你一个下标从 0 开始的整数数组 scores,
* 其中 scores[i] 是第 i 个节点的分数。同时给你一个二维整数数组 edges,其中 edges[i] = [ai, bi]
* 表示节点 ai 和 bi 之间有一条无向边。一个节点序列满足:序列中每相邻节点之间有边相连,且序列中
* 没有节点出现超过一次。节点序列的分数定义为序列中节点分数之和。返回一个长度为 4 的合法节点序列
* 的最大分数,如果不存在这样的序列返回 -1。
*
* 算法思路:枚举序列中间的一条边 (u, v),序列形式为 a - u - v - b。为让分数最大,a 取 u 的邻居中
* 分数最大的且不是 v 的节点,b 取 v 的邻居中分数最大的且不是 u 的节点,同时要保证 a != b。
* 对每个节点只保留分数最大的 3 个邻居(因为只需考虑最多两个"排除项",保留 top3 足够)。
*
* 时间复杂度:O(n + |E| * 3^2) ≈ O(n + |E|),空间复杂度:O(n + |E|)
*/
public class Solution {
public int maximumScore(int[] scores, int[][] edges) {
int n = scores.length;
List<int[]>[] graph = new List[n];
for (int i = 0; i < n; i++) {
graph[i] = new ArrayList<>();
}
for (int[] e : edges) {
int a = e[0], b = e[1];
graph[a].add(new int[]{b, scores[b]});
graph[b].add(new int[]{a, scores[a]});
}
// 每个节点的邻居按分数降序排序,之后每个节点最多使用前 3 个邻居
for (int i = 0; i < n; i++) {
graph[i].sort((x, y) -> y[1] - x[1]);
if (graph[i].size() > 3) {
graph[i] = new ArrayList<>(graph[i].subList(0, 3));
}
}
int ans = -1;
int m = edges.length;
// 枚举中边 (u, v),端点分列两侧
for (int[] e : edges) {
int u = e[0], v = e[1];
// 尝试 u 选一个邻居 a,v 选一个邻居 b,四个节点两两不同且各不相同
for (int[] pa : graph[u]) {
int a = pa[0];
if (a == v) continue;
for (int[] pb : graph[v]) {
int b = pb[0];
if (b == u || b == a) continue;
ans = Math.max(ans, scores[a] + scores[u] + scores[v] + scores[b]);
}
}
}
return ans;
}
public static void main(String[] args) {
Solution s = new Solution();
// 示例 1:预期 24
System.out.println(s.maximumScore(new int[]{5, 2, 9, 8, 4},
new int[][]{{0, 1}, {1, 2}, {2, 3}, {0, 2}, {1, 3}, {2, 4}}));
// 示例 2:预期 -1
System.out.println(s.maximumScore(new int[]{9, 20, 6, 4, 11, 12},
new int[][]{{0, 3}, {5, 3}, {2, 4}, {1, 3}}));
// 自测:两个节点一条边,长度为 4 序列不存在,预期 -1
System.out.println(s.maximumScore(new int[]{1, 2}, new int[][]{{0, 1}}));
}
}
复杂度分析
- 解法:枚举中边 + 邻居 Top3(主提交版本)
- 时间复杂度:O(n log n + |E|) --- 排序 + 枚举
- 空间复杂度:O(n + |E|) --- 邻接表
2243. Calculate Digit Sum of a String(计算字符串的数字和)
题目描述
给你一个由若干数字(0 - 9)组成的字符串 s 和一个整数 k。如果 s 的长度大于 k,则可以执行一轮操作:将 s 拆分 成长度为 k 的若干连续数字组 (最后一个数字组的长度可以小于 k),用表示每个数字组中所有数字之和的字符串来替换 对应的数字组(例如 "346" 会替换为 "13",因为 3 + 4 + 6 = 13),然后合并 所有组以形成一个新字符串。如果新字符串的长度大于 k 则重复。返回在完成所有轮操作后的 s。
示例 1:
输入:s = "11111222223", k = 3
输出:"135"
解释:第一轮 "111"、"112"、"222"、"23" → "3465";第二轮 "346"、"5" → "135"。
示例 2:
输入:s = "00000000", k = 3
输出:"000"
解释:每组分组的数字和都为 0,最终合并为 "000"。
约束:
-
1 <= s.length <= 100 -
2 <= k <= 100 -
s[i]为数字0-9
解题思路
核心思想 :按题意循环模拟。每轮把 s 按每 k 个字符一组,求组内数字之和(可能超过 9,转成多位数串),拼接成新串,直到长度 <= k 为止。
步骤:
while (s.length() > k)循环。- 内层循环以
i += k步进,取出[i, min(i+k, len))一段求数字和,追加到StringBuilder。 - 以拼接结果替换
s。
复杂度:每轮 O(len),轮数随长度快速收缩(每位数字和 ≤ 9k,长度大致减半),总体远小于 O(n²),最坏 O(n²) 也满足 n ≤ 100。
代码实现
java
/**
* 2243. Calculate Digit Sum of a String 计算字符串的数字和
*
* 给你一个由若干数字(0 - 9)组成的字符串 s 和一个整数 k。如果 s 的长度大于 k,则可以执行一轮
* 操作:把 s 拆分成若干长度为 k 的连续数字组(最后一组长度可以小于 k),用表示每个数字组中所有
* 数字之和的字符串替换对应的数字组,然后合并所有组形成新字符串。重复直到 s 的长度 <= k,返回
* 完成所有轮操作后的 s。
*
* 算法思路:按题意循环模拟。每次把 s 按 k 切成组,每组求数字和(可能 >9 需转成多位字符串),
* 拼接为新串,若长度仍大于 k 则继续,否则返回。
*
* 时间复杂度:每轮 O(n),轮数约为 log_k(n),总体 O(n * log n);空间复杂度:O(n)
*/
public class Solution {
public String digitSum(String s, int k) {
while (s.length() > k) {
StringBuilder sb = new StringBuilder();
// 按每段 k 个字符分组
for (int i = 0; i < s.length(); i += k) {
int sum = 0;
for (int j = i; j < Math.min(s.length(), i + k); j++) {
sum += s.charAt(j) - '0';
}
sb.append(sum);
}
s = sb.toString();
}
return s;
}
public static void main(String[] args) {
Solution s = new Solution();
// 示例 1:预期 "135"
System.out.println(s.digitSum("11111222223", 3));
// 示例 2:预期 "000"
System.out.println(s.digitSum("00000000", 3));
// 自测:长度 <= k 直接返回,预期 "123"
System.out.println(s.digitSum("123", 5));
}
}
复杂度分析
- 解法:循环模拟(主提交版本)
- 时间复杂度:O(n²) 最坏(n ≤ 100,每轮 O(len))
- 空间复杂度:O(n) --- 每次拼接新串
2244. Minimum Rounds to Complete All Tasks(完成所有任务需要的最少轮数)
题目描述
给你一个下标从 0 开始的整数数组 tasks,其中 tasks[i] 表示任务的难度级别。在每一轮中,你可以完成 2 个或者 3 个相同难度级别 的任务。返回完成所有任务需要的最少 轮数,如果无法完成所有任务返回 -1。
示例 1:
输入:tasks = [2,2,3,3,2,4,4,4,4,4]
输出:4
解释:难度 2 的 3 个一轮、难度 3 的 2 个一轮、难度 4 的 3+2 两轮,共 4 轮。
示例 2:
输入:tasks = [2,3,3]
输出:-1
解释:难度 2 只有 1 个任务,无法凑成 2 或 3 个一轮。
约束:
-
1 <= tasks.length <= 10⁵ -
1 <= tasks[i] <= 10⁹
解题思路
核心思想 :统计个数 + 贪心凑轮。设某难度任务数为 cnt,每轮只能完成 2 或 3 个相同难度的任务,因此:
cnt == 1:无法完成,整体返回-1;cnt >= 2:最优轮数 =cnt / 3 + (cnt % 3 == 0 ? 0 : 1)。
为什么这个公式对 :尽量用 3 个一轮(cnt / 3 轮),余数分三种情况------余 0 刚好;余 1 时把最后一组 3 拆成两个 2(总轮数 +1);余 2 时多来一组 2(+1)。cnt = 2 时 0 余 2,2 / 3 + 1 = 1 轮,正确。
步骤:
HashMap统计每种难度数量。- 对每个
cnt累加轮数,遇到1直接返回-1。
复杂度:O(n) 时间、O(n) 空间。
代码实现
java
import java.util.HashMap;
import java.util.Map;
/**
* 2244. Minimum Rounds to Complete All Tasks 完成所有任务需要的最少轮数
*
* 给你一个下标从 0 开始的整数数组 tasks,其中 tasks[i] 表示任务的难度级别。在每一轮中,你可以
* 完成 2 个或者 3 个相同难度级别的任务。返回完成所有任务需要的最少轮数,如果无法完成所有任务
* 返回 -1。
*
* 算法思路:统计每个难度级别的任务个数 cnt。对每个 cnt:
* - cnt == 1 时无法完成(1 不能拆成 2 或 3),返回 -1;
* - cnt >= 2 时,最优策略是尽量多用 3:先 cnt / 3 轮取 3 个,剩余 cnt % 3 若为 0 则刚好;
* 若余 1,则把最后一组 3 拆成两个 2(即总轮数 +1);若余 2,则再来一轮 2。
* 即轮数 = cnt / 3 + (cnt % 3 == 0 ? 0 : 1)(cnt >= 2 时恒成立)。
*
* 时间复杂度:O(n),空间复杂度:O(n)
*/
public class Solution {
public int minimumRounds(int[] tasks) {
Map<Integer, Integer> cnt = new HashMap<>();
for (int t : tasks) {
cnt.put(t, cnt.getOrDefault(t, 0) + 1);
}
int ans = 0;
for (int c : cnt.values()) {
if (c == 1) {
return -1;
}
// c >= 2:每 3 个一组(除最后)
ans += c / 3;
if (c % 3 != 0) {
ans++;
}
}
return ans;
}
public static void main(String[] args) {
Solution s = new Solution();
// 示例 1:预期 4
System.out.println(s.minimumRounds(new int[]{2, 2, 3, 3, 2, 4, 4, 4, 4, 4}));
// 示例 2:预期 -1
System.out.println(s.minimumRounds(new int[]{2, 3, 3}));
// 自测:2 个任务一轮,预期 1
System.out.println(s.minimumRounds(new int[]{1, 1}));
// 自测:4 个任务拆 2+2,预期 2
System.out.println(s.minimumRounds(new int[]{1, 1, 1, 1}));
}
}
复杂度分析
- 解法:贪心凑轮(主提交版本)
- 时间复杂度:O(n)
- 空间复杂度:O(n) --- 哈希表
2245. Maximum Trailing Zeros in a Cornered Path(转角路径的乘积中最多能有几个尾随零)
题目描述
给你一个二维整数数组 grid,大小为 m x n,其中每个单元格都含一个正整数。转角路径 定义为:包含至多一个弯的一组相邻单元。具体而言,路径应该完全向水平方向 或者向竖直方向 移动过弯(如果存在弯),而不能访问之前访问过的单元格。在过弯之后,路径应当完全朝另一个 方向行进。一条路径的乘积 定义为路径上所有值的乘积。请从 grid 中找出一条乘积中尾随零数目最多的转角路径,并返回该路径中尾随零的数目。
示例 1:
输入:grid = [[23,17,15,3,20],[8,1,20,27,11],[9,4,6,2,21],[40,9,1,10,6],[22,7,4,5,3]]
输出:3
解释:路径 15 * 20 * 6 * 1 * 10 = 18000,共 3 个尾随零。
示例 2:
输入:grid = [[4,3,2],[7,6,1],[8,8,8]]
输出:0
解释:不存在乘积含尾随零的转角路径。
约束:
-
m == grid.length -
n == grid[i].length -
1 <= m, n <= 10⁵(即m * n <= 10⁵) -
1 <= grid[i][j] <= 1000
解题思路
核心思想 :尾随零 = min(因子2的个数, 因子5的个数)。转角路径本质是"一条水平线段 + 一条竖直线段"以某格为角点拼接,共有 4 种形状:左+上、左+下、右+上、右+下。
步骤:
- 对每个格求因子
2、5的个数c2[i][j]、c5[i][j]。 - 分别做行前缀和 (每个单元格前缀)与列前缀和,支持 O(1) 求任意行段/列段内因子数。
- 枚举每个格子作为角点
(i, j),尝试四种方向组合;行段与列段都包含角点自身,合并时减去一次角点的因子数避免重复计数。 - 对四种组合取
min(因子2总数, 因子5总数)的最大值。
复杂度:O(m × n) 时间与空间。
代码实现
java
/**
* 2245. Maximum Trailing Zeros in a Cornered Path 转角路径的乘积中最多能有几个尾随零
*
* 给你一个二维整数数组 grid,大小为 m x n,其中每个单元格都含一个正整数。转角路径定义为:
* 包含至多一个弯的一组相邻单元,路径完全向水平方向或竖直方向移动过弯(如果存在弯),不能访问
* 之前访问过的单元格。一条路径的乘积定义为路径上所有值的乘积。请找出一条乘积中尾随零数目最多的
* 转角路径,返回该路径中尾随零的数目。
*
* 算法思路:尾随零 = min(因子2的个数, 因子5的个数)。转角路径只有 4 种形状:横+竖的上下左右
* 组合,即 (左段+上段)、(左段+下段)、(右段+上段)、(右段+下段),且都经过某个"转角"单元格。
* 预处理每行/每列的因子 2、5 的前缀和,然后枚举每个单元格作为转角,4 种方向组合取最大。
*
* 时间复杂度:O(m * n),空间复杂度:O(m * n)
*/
public class Solution {
public int maxTrailingZeros(int[][] grid) {
int m = grid.length, n = grid[0].length;
// 因子 2、5 个数矩阵
int[][] c2 = new int[m][n];
int[][] c5 = new int[m][n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
int v = grid[i][j];
while (v % 2 == 0) { c2[i][j]++; v /= 2; }
while (v % 5 == 0) { c5[i][j]++; v /= 5; }
}
}
// 行前缀和:row2[i][j] = 第 i 行前 j 列的 2 因子数(j 为前缀长度 0..n)
int[][] row2 = new int[m][n + 1];
int[][] row5 = new int[m][n + 1];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
row2[i][j + 1] = row2[i][j] + c2[i][j];
row5[i][j + 1] = row5[i][j] + c5[i][j];
}
}
// 列前缀和:col2[j][i] = 第 j 列前 i 行的因子数
int[][] col2 = new int[n][m + 1];
int[][] col5 = new int[n][m + 1];
for (int j = 0; j < n; j++) {
for (int i = 0; i < m; i++) {
col2[j][i + 1] = col2[j][i] + c2[i][j];
col5[j][i + 1] = col5[j][i] + c5[i][j];
}
}
int ans = 0;
// 枚举转角单元格 (i, j)
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
// 行段:左段 [0..j] 或右段 [j..n-1];列段:上段 [0..i] 或下段 [i..m-1]
// 注意角点自身会被行段与列段重复计算一次
for (int lr = 0; lr < 2; lr++) { // 0 左段,1 右段
for (int ud = 0; ud < 2; ud++) { // 0 上段,1 下段
// 行段 2 因子数
int r2 = lr == 0 ? row2[i][j + 1] : row2[i][n] - row2[i][j];
int r5 = lr == 0 ? row5[i][j + 1] : row5[i][n] - row5[i][j];
// 列段 2 因子数(含角点,故需减去一次角点去重)
int c2v = ud == 0 ? col2[j][i + 1] : col2[j][m] - col2[j][i];
int c5v = ud == 0 ? col5[j][i + 1] : col5[j][m] - col5[j][i];
// 角点被算两次,减去一次
int t2 = r2 + c2v - c2[i][j];
int t5 = r5 + c5v - c5[i][j];
ans = Math.max(ans, Math.min(t2, t5));
}
}
}
}
return ans;
}
public static void main(String[] args) {
Solution s = new Solution();
// 示例 1:预期 3
System.out.println(s.maxTrailingZeros(new int[][]{
{23, 17, 15, 3, 20},
{8, 1, 20, 27, 11},
{9, 4, 6, 2, 21},
{40, 9, 1, 10, 6},
{22, 7, 4, 5, 3}}));
// 示例 2:预期 0
System.out.println(s.maxTrailingZeros(new int[][]{
{4, 3, 2},
{7, 6, 1},
{8, 8, 8}}));
}
}
复杂度分析
- 解法:因子分解 + 行/列前缀和枚举(主提交版本)
- 时间复杂度:O(m × n) --- 单遍预处理 + 枚举
- 空间复杂度:O(m × n) --- 前缀和数组
2246. Longest Path With Different Adjacent Characters(相邻字符不同的最长路径)
题目描述
给你一棵树 (连通、无向、无环图),根节点是节点 0,这棵树由编号从 0 到 n - 1 的 n 个节点组成。用下标从 0 开始、长度为 n 的数组 parent 表示这棵树,其中 parent[i] 是节点 i 的父节点,parent[0] == -1。另给你一个字符串 s,长度也是 n,其中 s[i] 表示分配给节点 i 的字符。找出路径上任意一对相邻节点都没有分配到相同字符的最长路径,返回该路径的长度(节点个数)。
示例 1:
输入:parent = [-1,0,0,1,1,2], s = "abacbe"
输出:3
解释:最长路径 0 -> 1 -> 3,长度 3。
示例 2:
输入:parent = [-1,0,0,0], s = "aabc"
输出:3
解释:最长路径 2 -> 0 -> 3,长度 3。
约束:
-
n == parent.length == s.length -
1 <= n <= 10⁵ -
0 <= parent[i] <= n - 1对所有i >= 1 -
parent[0] == -1 -
parent表示一棵有效树 -
s由小写英文字母组成
解题思路
核心思想 :树形 DP。以某个节点为路径"拐点"的最长合法路径,由它向下最长的两条可接续链(子节点字符与它不同)合并而成。对每个节点,DFS 返回"以该节点为端点向下延伸的最大合法链长(节点数)",并在归过程中更新全局答案。
步骤:
- 由
parent建树(父 → 子边)。 dfs(u):遍历子节点v,递归得到子链长len;若s[u] == s[v],该子链不能接续(但可作为独立路径考虑),否则用len更新两条最长链max1、max2。- 过
u的候选路径节点数 =max1 + max2 + 1,更新全局ans。 - 返回
max1 + 1作为父节点视角下的子链长。
复杂度:O(n) 时间、O(n) 空间。
代码实现
java
import java.util.ArrayList;
import java.util.List;
/**
* 2246. Longest Path With Different Adjacent Characters 相邻字符不同的最长路径
*
* 给你一棵树(连通、无向、无环图),根节点是节点 0,由整数数组 parent 表示,parent[i] 是节点 i
* 的父节点,parent[0] == -1。另给字符串 s,s[i] 表示节点 i 的字符。找出路径上任意一对相邻节点
* 都没有分配到相同字符的最长路径,返回该路径的长度(节点数)。
*
* 算法思路:树形 DP。对每个节点维护"从该节点向下、经过不同字符节点"的最长链长度(以节点为端点
* 计)。对每个节点合并子节点传递上来的最长两条链(子节点字符相同则不能接续),
* 以该节点为拐点的候选路径 = 最长链 + 次长链 - 1(节点数),全局最大值即为答案。
*
* 时间复杂度:O(n),空间复杂度:O(n)
*/
public class Solution {
private List<Integer>[] g;
private String s;
private int ans;
public int longestPath(int[] parent, String s) {
this.s = s;
int n = parent.length;
g = new List[n];
for (int i = 0; i < n; i++) {
g[i] = new ArrayList<>();
}
for (int i = 1; i < n; i++) {
g[parent[i]].add(i);
}
ans = 1;
dfs(0);
return ans;
}
/** 返回以节点 u 为端点(向下)且相邻字符互不相同的最长链长度(节点数) */
private int dfs(int u) {
// 两条最长的"可接续"链(子节点字符与 u 不同的情况下)
int max1 = 0, max2 = 0;
for (int v : g[u]) {
int len = dfs(v); // 子链节点数
if (s.charAt(u) == s.charAt(v)) {
// 父字符与子相同,该子链不能作为 u 的延续,但可以单独成为候选路径
continue;
}
if (len > max1) {
max2 = max1;
max1 = len;
} else if (len > max2) {
max2 = len;
}
}
// 过 u 的候选路径 = 最长两条链合并 + u 自身
ans = Math.max(ans, max1 + max2 + 1);
return max1 + 1;
}
public static void main(String[] args) {
Solution s = new Solution();
// 示例 1:预期 3
System.out.println(s.longestPath(new int[]{-1, 0, 0, 1, 1, 2}, "abacbe"));
// 示例 2:预期 3
System.out.println(s.longestPath(new int[]{-1, 0, 0, 0}, "aabc"));
// 自测:单节点,预期 1
System.out.println(s.longestPath(new int[]{-1}, "z"));
}
}
复杂度分析
- 解法:树形 DP(主提交版本)
- 时间复杂度:O(n) --- 每个节点一次
- 空间复杂度:O(n) --- 树 + 递归栈
2247. Maximum Cost of Trip With K Highways(K 条高速公路的最大旅行费用)
题目描述
一系列高速公路连接从 0 到 n - 1 的 n 个城市。给定一个二维整数数组 highways,其中 highways[i] = [city1i, city2i, tolli] 表示有一条高速公路连接 city1i 和 city2i,允许一辆汽车从 city1i 前往 city2i,反之亦然 ,费用为 tolli。给你一个整数 k,你要正好 经过 k 条公路。你可以从任何一个城市出发,但在旅途中每个城市最多 只能访问一次。返回旅行的最大费用,如果没有符合要求的行程则返回 -1。
示例 1:
输入:n = 5, highways = [[0,1,4],[2,1,3],[1,4,11],[3,2,3],[3,4,2]], k = 3
输出:17
解释:路径 0 -> 1 -> 4 -> 3 费用 4 + 11 + 2 = 17,可证为最大。
示例 2:
输入:n = 4, highways = [[0,1,3],[2,3,2]], k = 2
输出:-1
解释:不存在长度为 2 的有效行程。
约束:
-
2 <= n <= 15 -
0 <= highways.length <= 50 -
highways[i].length == 3 -
0 <= city1i, city2i <= n - 1 -
city1i != city2i -
0 <= tolli <= 100 -
1 <= k <= 50 -
无重复高速公路
解题思路
核心思想 :状态压缩记忆化搜索(bitmask DP)。n <= 15,可用一个 int 位掩码表示已访问城市集合。状态 (u, mask) 表示当前在城市 u 且已访问集合为 mask 时,走完剩余边能获得的最大费用;已经走过 k 条边(mask 中城市数为 k + 1)时返回 0。
步骤:
- 若
k + 1 > n直接返回-1(简单路径最多n - 1条边)。 - 建无向邻接表
(neighbor, toll)。 dfs(u, mask):Integer.bitCount(mask) == k + 1时返回0;否则枚举未访问邻居v,递归w + dfs(v, mask | (1 << v)),取最大值;不可达记-1。- 以每个城市为起点(
mask = 1 << i)调用,取全局最大。
复杂度:状态数 O(n × 2ⁿ),每个状态枚举度数,总 O(n² × 2ⁿ),n ≤ 15 时可行。
代码实现
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* 2247. Maximum Cost of Trip With K Highways K 条高速公路的最大旅行费用
*
* 一系列高速公路连接从 0 到 n - 1 的 n 个城市。给定一个二维整数数组 highways,其中
* highways[i] = [city1i, city2i, tolli] 表示有一条高速公路连接 city1i 和 city2i,允许车辆
* 双向通行,费用为 tolli。给你一个整数 k,你要正好经过 k 条公路。你可以从任何一个城市出发,
* 但在旅途中每个城市最多只能访问一次。返回旅行的最大费用,如果没有符合要求的行程则返回 -1。
*
* 算法思路:位掩码状态压缩记忆化搜索。状态 (u, mask) 表示当前在城市 u、已访问城市集合为 mask
* 时,还能得到的最大费用。当 mask 中城市个数 == k + 1(已经走过 k 条边)时返回 0;否则枚举 u
* 的未访问邻居 v,递归取 max(toll + dfs(v, mask | (1 << v)))。每个城市都可作为起点,
* 取所有 dfs(i, 1 << i) 的最大值。
*
* 时间复杂度:O(n * 2^n)(n <= 15),空间复杂度:O(n * 2^n)
*/
public class Solution {
private List<int[]>[] graph;
private int[][] mem;
private int k;
public int maximumCost(int n, int[][] highways, int k) {
this.k = k;
if (k + 1 > n) return -1;
graph = new List[n];
for (int i = 0; i < n; i++) {
graph[i] = new ArrayList<>();
}
for (int[] h : highways) {
int u = h[0], v = h[1], w = h[2];
graph[u].add(new int[]{v, w});
graph[v].add(new int[]{u, w});
}
mem = new int[n][1 << n];
for (int[] row : mem) {
Arrays.fill(row, -1);
}
int ans = -1;
for (int i = 0; i < n; i++) {
ans = Math.max(ans, dfs(i, 1 << i));
}
return ans;
}
/** 深度优先搜索:在城市 u、已访问集合 mask 时,继续走完 k 条边可得到的最大费用 */
private int dfs(int u, int mask) {
if (Integer.bitCount(mask) == k + 1) {
return 0;
}
if (mem[u][mask] != -1) {
return mem[u][mask];
}
int res = -1;
for (int[] e : graph[u]) {
int v = e[0], w = e[1];
if ((mask >> v & 1) == 1) {
continue;
}
int next = dfs(v, mask | (1 << v));
if (next != -1) {
res = Math.max(res, w + next);
}
}
return mem[u][mask] = res;
}
public static void main(String[] args) {
Solution s = new Solution();
// 示例 1:预期 17
System.out.println(s.maximumCost(5,
new int[][]{{0, 1, 4}, {2, 1, 3}, {1, 4, 11}, {3, 2, 3}, {3, 4, 2}}, 3));
// 示例 2:预期 -1
System.out.println(s.maximumCost(4,
new int[][]{{0, 1, 3}, {2, 3, 2}}, 2));
// 自测:k+1 > n,预期 -1
System.out.println(s.maximumCost(2,
new int[][]{{0, 1, 5}}, 2));
}
}
复杂度分析
- 解法:bitmask DP 记忆化搜索(主提交版本)
- 时间复杂度:O(n² × 2ⁿ)(n ≤ 15)
- 空间复杂度:O(n × 2ⁿ)
2248. Intersection of Multiple Arrays(多个数组求交集)
题目描述
给你一个二维整数数组 nums,其中 nums[i] 是由不同 正整数组成的一个非空数组。按升序排列 返回一个数组,数组中的每个元素在 nums 所有数组中都出现过。
示例 1:
输入:nums = [[3,1,2,4,5],[1,2,3,4],[3,4,5,6]]
输出:[3,4]
解释:只有 3 和 4 出现在所有数组中。
示例 2:
输入:nums = [[1,2,3],[4,5,6]]
输出:[]
解释:无公共元素。
约束:
-
1 <= nums.length <= 1000 -
1 <= sum(nums[i].length) <= 1000 -
1 <= nums[i][j] <= 1000 -
nums[i]中的所有值互不相同
解题思路
核心思想 :计数数组。值域 [1, 1000],用一个 int[1001] 统计每个值出现的数组个数,最后输出计数等于数组总数的值。
步骤:
cnt[v]++遍历所有数组元素(每组内元素互异,无需额外去重)。- 升序遍历
0到1000,若cnt[v] == nums.length输出v。
复杂度:O(总元素数 + 1000) 时间、O(1001) 空间。
代码实现
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* 2248. Intersection of Multiple Arrays 多个数组求交集
*
* 给你一个二维整数数组 nums,其中 nums[i] 是由不同正整数组成的一个非空数组。按升序返回一个数组,
* 数组中的每个元素在 nums 所有数组中都出现过。
*
* 算法思路:由于元素值可能较大(<= 1000),直接用数组计数。对每个数组去重自增计数,
* 最后统计计数 == 数组个数 nums.length 的元素,按升序输出。
*
* 时间复杂度:O(总元素个数),空间复杂度:O(maxVal)
*/
public class Solution {
public List<Integer> intersection(int[][] nums) {
int[] cnt = new int[1001];
int m = nums.length;
for (int[] arr : nums) {
for (int v : arr) {
cnt[v]++;
}
}
List<Integer> ans = new ArrayList<>();
for (int v = 0; v <= 1000; v++) {
if (cnt[v] == m) {
ans.add(v);
}
}
return ans;
}
public static void main(String[] args) {
Solution s = new Solution();
// 示例 1:预期 [3,4]
System.out.println(s.intersection(new int[][]{{3, 1, 2, 4, 5}, {1, 2, 3, 4}, {3, 4, 5, 6}}));
// 示例 2:预期 []
System.out.println(s.intersection(new int[][]{{1, 2, 3}, {4, 5, 6}}));
}
}
复杂度分析
- 解法:计数数组(主提交版本)
- 时间复杂度:O(总元素数 + 1000)
- 空间复杂度:O(1001)
2249. Count Lattice Points Inside a Circle(统计圆内格点数目)
题目描述
给你一个二维整数数组 circles,其中 circles[i] = [xi, yi, ri] 表示网格上圆心为 (xi, yi) 且半径为 ri 的第 i 个圆。返回出现在至少一个 圆内的格点数目 。格点 指整数坐标对应的点,圆周上的点也被视为出现在圆内。
示例 1:
输入:circles = [[2,2,1]]
输出:5
解释:圆内格点为 (1,2)、(2,1)、(2,2)、(2,3)、(3,2),共 5 个。
示例 2:
输入:circles = [[2,2,2],[3,4,1]]
输出:16
解释:两个圆内共有 16 个格点。
约束:
-
1 <= circles.length <= 200 -
circles[i].length == 3 -
1 <= xi, yi <= 100 -
1 <= ri <= min(xi, yi)
解题思路
核心思想 :暴力枚举所有候选格点 + 集合去重。对每个圆,扫描其外接正方形(x ∈ [xi - ri, xi + ri],y ∈ [yi - ri, yi + ri])内的所有整数点,若满足 (x-xi)² + (y-yi)² <= ri² 则属于该圆。圆可能重叠,用编码集合去重。
步骤:
- 对每个圆四重枚举外接正方形内的整数点,判断距离
<= ri²。 - 命中则把
x * 10000 + y加入HashSet(坐标绝对值 < 10000,编码安全)。 - 返回集合大小。
复杂度 :每个圆枚举 (2r+1)² 个点,r ≤ 100,最多 200 个圆,运算量约 8×10⁶,可行;空间 O(去重点数)。
代码实现
java
import java.util.HashSet;
import java.util.Set;
/**
* 2249. Count Lattice Points Inside a Circle 统计圆内格点数目
*
* 给你一个二维整数数组 circles,其中 circles[i] = [xi, yi, ri] 表示圆心为 (xi, yi) 且半径为
* ri 的第 i 个圆。返回出现在至少一个圆内的格点数目。格点指整数坐标对应的点,圆周上的点也算。
*
* 算法思路:枚举所有可能落在圆内的整数点。对每个圆,x 范围 [xi - ri, xi + ri],y 范围
* [yi - ri, yi + ri],判断 (x-xi)² + (y-yi)² <= ri² 是否成立。由于可能有多个圆重叠,使用
* HashSet 去重,坐标编码为 x * 10000 + y(坐标绝对值 <= 300,编码安全)。每个圆最多枚举
* (2r+1)² 个点,r <= 100,总枚举量 <= 200 个圆 * 201² 约 8×10⁶,可接受。
*
* 时间复杂度:O(sum(ri²)),空间复杂度:O(枚举点数)
*/
public class Solution {
public int countLatticePoints(int[][] circles) {
Set<Integer> set = new HashSet<>();
for (int[] c : circles) {
int x = c[0], y = c[1], r = c[2];
// 扫描该圆的外接正方形内的所有整数点
for (int i = x - r; i <= x + r; i++) {
for (int j = y - r; j <= y + r; j++) {
int dx = i - x, dy = j - y;
if (dx * dx + dy * dy <= r * r) {
set.add(i * 10000 + j); // 坐标去重编码
}
}
}
}
return set.size();
}
public static void main(String[] args) {
Solution s = new Solution();
// 示例 1:预期 5
System.out.println(s.countLatticePoints(new int[][]{{2, 2, 1}}));
// 示例 2:预期 16
System.out.println(s.countLatticePoints(new int[][]{{2, 2, 2}, {3, 4, 1}}));
// 自测:两个圆重叠,预期 12(r=1 的圆 5 个点,r=1 圆心(2,3) 与 (2,2) 圆相切)
System.out.println(s.countLatticePoints(new int[][]{{2, 2, 1}, {2, 4, 1}}));
}
}
复杂度分析
- 解法:外接正方形枚举 + 集合去重(主提交版本)
- 时间复杂度:O(∑(2r+1)²) ≈ 8×10⁶
- 空间复杂度:O(去重格点数)
2250. Count Number of Rectangles Containing Each Point(统计包含每个点的矩形数目)
题目描述
给你一个二维整数数组 rectangles,其中 rectangles[i] = [li, hi] 表示第 i 个矩形长为 li 高为 hi。再给你一个二维整数数组 points,其中 points[j] = [xj, yj] 是坐标为 (xj, yj) 的一个点。第 i 个矩形的左下角 在 (0, 0) 处,右上角 在 (li, hi)。返回整数数组 count,其中 count[j] 是包含 第 j 个点的矩形数目。如果 0 <= xj <= li 且 0 <= yj <= hi,则该矩形包含该点;点刚好在矩形边上也被视为被包含。
示例 1:
输入:rectangles = [[1,2],[2,3],[2,5]], points = [[2,1],[1,4]]
输出:[2,1]
解释:点 (2,1) 被第 2、3 个矩形包含;点 (1,4) 只被第 3 个矩形包含。
示例 2:
输入:rectangles = [[1,1],[2,2],[3,3]], points = [[1,3],[1,1]]
输出:[1,3]
解释:点 (1,3) 只被第 3 个矩形包含;点 (1,1) 被全部 3 个矩形包含。
约束:
-
1 <= rectangles.length, points.length <= 10⁵ -
rectangles[i].length == points[j].length == 2 -
1 <= li, xj <= 10⁹ -
1 <= hi, yj <= 100 -
所有
rectangles互不相同 -
xj升序给出
解题思路
核心思想 :按高度分组 + 二分。高度 hi 最大仅 100,把矩形按高度分组,每组内存放长度并升序排序。对点 (x, y),只需枚举高度 h >= y 的分组,二分查长度 >= x 的矩形个数并累加。
步骤:
groups[h].add(l):按高度分组收集矩形长度。- 每组升序排序。
- 对每个点
(x, y),对h ∈ [y, 100]的各组做lowerBound(x)(第一个长度 >= x 的下标),计数 =size - lowerBound。 - 累加后填回答案数组。
复杂度:排序 O(n log n),每个点枚举最多 101 组 × 每组 O(log n) 二分,O(m × 101 × log n),可行。
代码实现
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* 2250. Count Number of Rectangles Containing Each Point 统计包含每个点的矩形数目
*
* 给你一个二维整数数组 rectangles,其中 rectangles[i] = [li, hi] 表示第 i 个矩形长为 li 高为 hi。
* 再给你一个二维整数数组 points,其中 points[j] = [xj, yj] 是坐标为 (xj, yj) 的一个点。第 i 个
* 矩形的左下角在 (0, 0),右上角在 (li, hi)。返回整数数组 count,count[j] 是包含第 j 个点的矩形
* 数目。点在矩形的边上也被视为被包含。
*
* 算法思路:高度 hi 范围有限(<= 100),按高度分组:groups[h] 存放所有高度为 h 的矩形长度。
* 对每个点 (x, y),枚举所有高度 h >= y 的分组,在分组二分查找长度 >= x 的矩形个数并累加。
* 这样避免了对每个点遍历所有矩形 O(n*m) 的开销。
*
* 时间复杂度:O((n + m) * maxHi * log n),空间复杂度:O(n)
*/
public class Solution {
public int[] countRectangles(int[][] rectangles, int[][] points) {
// 高度分组,每组升序存放长度
List<List<Integer>> groups = new ArrayList<>();
for (int h = 0; h <= 100; h++) {
groups.add(new ArrayList<>());
}
for (int[] r : rectangles) {
groups.get(r[1]).add(r[0]);
}
for (int h = 0; h <= 100; h++) {
groups.get(h).sort(Integer::compareTo);
}
int m = points.length;
int[] ans = new int[m];
for (int j = 0; j < m; j++) {
int x = points[j][0], y = points[j][1];
int cnt = 0;
// 高度必须 >= 点的 y 坐标
for (int h = y; h <= 100; h++) {
List<Integer> list = groups.get(h);
cnt += list.size() - lowerBound(list, x);
}
ans[j] = cnt;
}
return ans;
}
/** 在升序 list 中查找第一个 >= x 的下标 */
private int lowerBound(List<Integer> list, int x) {
int lo = 0, hi = list.size();
while (lo < hi) {
int mid = (lo + hi) >>> 1;
if (list.get(mid) < x) {
lo = mid + 1;
} else {
hi = mid;
}
}
return lo;
}
public static void main(String[] args) {
Solution s = new Solution();
// 示例 1:预期 [2,1]
System.out.println(Arrays.toString(s.countRectangles(
new int[][]{{1, 2}, {2, 3}, {2, 5}}, new int[][]{{2, 1}, {1, 4}})));
// 示例 2:预期 [1,3]
System.out.println(Arrays.toString(s.countRectangles(
new int[][]{{1, 1}, {2, 2}, {3, 3}}, new int[][]{{1, 3}, {1, 1}})));
}
}
复杂度分析
- 解法:按高度分组 + 排序二分(主提交版本)
- 时间复杂度:O(n log n + m × 101 × log n)
- 空间复杂度:O(n)
2251. Number of Flowers in Full Bloom(花期内花的数目)
题目描述
给你一个下标从 0 开始的二维整数数组 flowers,其中 flowers[i] = [starti, endi] 表示第 i 朵花的花期 从 starti 到 endi(都包含 )。同时给你一个下标从 0 开始大小为 n 的整数数组 people,people[i] 是第 i 个人来看花的时间。返回大小为 n 的整数数组 answer,其中 answer[i] 是第 i 个人到达时在花期内花的数目。
示例 1:
输入:flowers = [[1,6],[3,7],[9,12],[4,13]], people = [2,3,7,11]
输出:[1,2,2,2]
解释:时刻 2 只有花期 [1,6] 一朵花,时刻 3 有 [1,6]、[3,7] 两朵,等等。
示例 2:
输入:flowers = [[1,10],[3,3]], people = [3,3,2]
输出:[2,2,1]
解释:时刻 3 两朵都开,时刻 2 只有 [1,10]。
约束:
-
1 <= flowers.length <= 10⁵ -
flowers[i].length == 2 -
1 <= starti <= endi <= 10⁹ -
1 <= people.length <= 10⁵ -
1 <= people[i] <= 10⁹
解题思路
核心思想 :排序 + 二分计数。对每个人的到达时刻 t:"花期开始 <= t" 的花数减去"花期结束 < t"(已凋谢)的花数,即为 t 时刻开放的花数。做法:分别对开始时间、结束时间排序,用二分快速求两个计数。
步骤:
starts= 所有starti排序,ends= 所有endi排序。- 对每个人
t:open = upperBound(starts, t)(starts <= t的个数);closed = lowerBound(ends, t)(ends < t的个数)。 answer[i] = open - closed。
复杂度:排序 O(n log n),每个点两次 O(log n) 二分,总 O((n + m) log n) 时间、O(n) 空间。(也可用差分 + 双指针,本质相同。)
代码实现
java
import java.util.Arrays;
/**
* 2251. Number of Flowers in Full Bloom 花期内花的数目
*
* 给你一个下标从 0 开始的二维整数数组 flowers,其中 flowers[i] = [starti, endi] 表示第 i 朵花
* 的花期从 starti 到 endi(都包含)。同时给你一个大小为 n 的整数数组 people,people[i] 是第 i
* 个人来看花的时间。返回大小为 n 的整数数组 answer,answer[i] 是第 i 个人到达时在花期内花的数目。
*
* 算法思路:差分 + 排序。把每个花期拆成 start 处 +1、end+1 处 -1 的差分事件,所有事件按时间排序;
* 再把 people 按时间排序并记录原下标。双指针扫描:在每个人的时间点累加到该时间为止的差分和即为
* 该时刻开放的花数,按原下标填回答案数组。
*
* 时间复杂度:O((n + m) log(n + m)),空间复杂度:O(n + m)
*/
public class Solution {
public int[] fullBloomFlowers(int[][] flowers, int[] people) {
int n = flowers.length;
int[] starts = new int[n];
int[] ends = new int[n];
for (int i = 0; i < n; i++) {
starts[i] = flowers[i][0];
ends[i] = flowers[i][1];
}
Arrays.sort(starts);
Arrays.sort(ends);
int m = people.length;
int[] ans = new int[m];
// 对每个人,对 starts 找 <= t 的个数,对 ends 找 < t 的个数,相减
for (int i = 0; i < m; i++) {
int t = people[i];
int open = upperBound(starts, t); // 花期已开始(<=t)的花数
int closed = lowerBound(ends, t); // 花期已结束(<t)的花数,即 end < t
ans[i] = open - closed;
}
return ans;
}
/** 升序数组中第一个 > target 的下标(即 <= target 的个数) */
private int upperBound(int[] arr, int target) {
int lo = 0, hi = arr.length;
while (lo < hi) {
int mid = (lo + hi) >>> 1;
if (arr[mid] <= target) {
lo = mid + 1;
} else {
hi = mid;
}
}
return lo;
}
/** 升序数组中第一个 >= target 的下标(即 < target 的个数) */
private int lowerBound(int[] arr, int target) {
int lo = 0, hi = arr.length;
while (lo < hi) {
int mid = (lo + hi) >>> 1;
if (arr[mid] < target) {
lo = mid + 1;
} else {
hi = mid;
}
}
return lo;
}
public static void main(String[] args) {
Solution s = new Solution();
// 示例 1:预期 [1,2,2,2]
System.out.println(Arrays.toString(s.fullBloomFlowers(
new int[][]{{1, 6}, {3, 7}, {9, 12}, {4, 13}}, new int[]{2, 3, 7, 11})));
// 示例 2:预期 [2,2,1]
System.out.println(Arrays.toString(s.fullBloomFlowers(
new int[][]{{1, 10}, {3, 3}}, new int[]{3, 3, 2})));
}
}
复杂度分析
- 解法:排序 + 二分计数(主提交版本)
- 时间复杂度:O(n log n + m log n)
- 空间复杂度:O(n)
2252. Dynamic Pivoting of a Table(表的动态旋转)
题目类型:数据库(SQL)
题目描述
表: Products
+-------------+---------+
| Column Name | Type |
+-------------+---------+
| product_id | int |
| store | varchar |
| price | int |
+-------------+---------+
(product_id, store) 是该表的主键。该表的每一行都表示商店 store 中商品 product_id 的价格。表中最多有 30 个不同的商店。实现 PivotProducts 过程来重新组织 Products 表,使每行都有一个商品的 id 及其在每个商店中的价格;如果商品不在某商店出售,价格应为 null。表的列应该包含每个商店,并按字典顺序排序 。过程应该在重新组织表之后返回它,以任意顺序返回结果表。
示例 1:
输入:Products 表如上(商品 1 在 Shop、LC_Store 销售;商品 2 在 Nozama、Souq;商品 3 在 Shop、Souq)
输出:
+------------+----------+--------+------+------+
| product_id | LC_Store | Nozama | Shop | Souq |
+------------+----------+--------+------+------+
| 1 | 100 | null | 110 | null |
| 2 | null | 200 | null | 190 |
| 3 | null | null | 1000 | 1900 |
+------------+----------+--------+------+------+
解释:商店按字典序 LC_Store、Nozama、Shop、Souq 排列。
约束:
-
(product_id, store)是主键 -
表中最多
30个不同商店 -
需要动态透视:商店列表不固定
解题思路
核心思想 :动态 SQL 生成透视列。商店列表不固定,先在运行时用 GROUP_CONCAT 拼出形如 ``MAX(IF(store='Shop', price, NULL)) AS Shop``` 的列表达式(按字典序 ORDER BY store),再 PREPARE+EXECUTE` 执行。
步骤:
SET SESSION group_concat_max_len = 1000000:防止商店较多时拼接结果被截断。SELECT GROUP_CONCAT(... ORDER BY store) INTO @sql FROM Products生成列片段。- 拼上
SELECT product_id, <列片段> FROM Products GROUP BY product_id。 PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;。
代码实现
sql
-- 2252. Dynamic Pivoting of a Table 表的动态旋转
--
-- Table: Products
-- +-------------+---------+
-- | Column Name | Type |
-- +-------------+---------+
-- | product_id | int |
-- | store | varchar |
-- | price | int |
-- +-------------+---------+
-- (product_id, store) 是该表的主键。每行表示商店 store 中商品 product_id 的价格。
-- 表中最多有 30 个不同的商店。要求实现 PivotProducts 过程,把 Products 表透视(pivot)重组:
-- 每行一个商品 id,列是每个商店(按字典序排序),商品不在某店出售则价格为 null。
--
-- 思路:商店列表不是写死的,需要动态 SQL。先用 GROUP_CONCAT 在运行时拼出
-- "MAX(IF(store='Shop', price, NULL)) AS Shop, ..." 这样的列表达式(商店按字典序),
-- 再 PREPARE + EXECUTE 执行拼装好的动态查询。
CREATE PROCEDURE PivotProducts()
BEGIN
-- 增大 GROUP_CONCAT 上限,避免商店多时结果被截断
SET SESSION group_concat_max_len = 1000000;
SET @sql = NULL;
SELECT GROUP_CONCAT(
CONCAT('MAX(IF(store = ''', store, ''', price, NULL)) AS `', store, '`')
ORDER BY store)
INTO @sql
FROM Products;
-- 汇总每个商品在每家商店的价格
SET @sql = CONCAT(
'SELECT product_id, ', @sql,
' FROM Products GROUP BY product_id ORDER BY product_id');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
END;
复杂度分析
- 解法:动态 SQL 拼列 + PREPARE/EXECUTE(主提交版本)
- 时间复杂度:O(S + P)(S 为商店数,P 为行数)
- 空间复杂度:O(S) --- 拼接字符串
2253. Dynamic Unpivoting of a Table(动态取消表的旋转)
题目类型:数据库(SQL)
题目描述
表: Products
+-------------+---------+
| Column Name | Type |
+-------------+---------+
| product_id | int |
| store_name1 | int |
| ... | int |(至多 30 家店)
+-------------+---------+
product_id 是该表的主键。表中每行表示该商品在 n 个不同商店中的价格;若商店中没有该商品,该列价格为 null。不同测试用例的商店名称可能不同,至少有 1 家店、最多 30 家店。实现 UnpivotProducts 过程来重新组织 Products 表,使每一行都有商品的 id、销售该商品的商店名称以及该商品在该商店中的价格。如果某商品在某商店不可用,则不要 在结果表中包含该组合的行。结果应有 3 列:product_id、store 和 price,以任意顺序返回。
示例 1:
输入:Products 表如上(宽表:1 在 LC_Store 100、Shop 110;2 在 Nozama 200、Souq 190;3 在 Shop 1000、Souq 1900)
输出:
+------------+----------+-------+
| product_id | store | price |
+------------+----------+-------+
| 1 | LC_Store | 100 |
| 1 | Shop | 110 |
| 2 | Nozama | 200 |
| 2 | Souq | 190 |
| 3 | Shop | 1000 |
| 3 | Souq | 1900 |
+------------+----------+-------+
约束:
-
product_id是主键 -
至少有 1 家店、至多 30 家店
-
需要动态逆透视:商店列名不固定
解题思路
核心思想 :动态 SQL 生成逆透视查询。从 INFORMATION_SCHEMA.COLUMNS 查出表内所有非 product_id 的列名,对每一列拼一条 SELECT product_id, '列名' AS store, 列名 AS price FROM Products WHERE 列名 IS NOT NULL,用 UNION ALL 串接后动态执行。
步骤:
SET SESSION group_concat_max_len = 1000000。SELECT GROUP_CONCAT(..., SEPARATOR ' UNION ALL ') INTO @sql FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'Products' AND COLUMN_NAME <> 'product_id' ORDER BY ORDINAL_POSITION。- 每条子句对当前店铺列生成一行价格非 null 的记录。
PREPARE+EXECUTE+DEALLOCATE PREPARE。
代码实现
sql
-- 2253. Dynamic Unpivoting of a Table 动态取消表的旋转
--
-- Table: Products
-- +-------------+---------+
-- | Column Name | Type |
-- +-------------+---------+
-- | product_id | int |
-- | store_name1 | int |
-- | ... | ... (至多 30 家店)
-- +-------------+---------+
-- product_id 是主键,每行表示某商品在若干商店的价格(null 表示该店无该商品)。
-- 要求实现 UnpivotProducts 过程,把宽表逆透视(unpivot)为长表:
-- 每行 (product_id, store, price),只保留价格非 null 的组合。
--
-- 思路:商店列名不是写死的,需动态 SQL。从 INFORMATION_SCHEMA.COLUMNS 查出全部
-- 非 product_id 的列名,用 UNION ALL 拼出每个店列的无 null 行:
-- SELECT product_id, 'shop1' AS store, shop1 AS price FROM Products WHERE shop1 IS NOT NULL
-- 再用 PREPARE + EXECUTE 执行。
CREATE PROCEDURE UnpivotProducts()
BEGIN
SET SESSION group_concat_max_len = 1000000;
SET @sql = NULL;
SELECT GROUP_CONCAT(
CONCAT(
'SELECT product_id, ''', COLUMN_NAME, ''' AS store, `',
COLUMN_NAME, '` AS price FROM Products WHERE `',
COLUMN_NAME, '` IS NOT NULL')
SEPARATOR ' UNION ALL ')
INTO @sql
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'Products' AND COLUMN_NAME <> 'product_id'
ORDER BY ORDINAL_POSITION;
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
END;
复杂度分析
- 解法:动态 SQL UNION ALL + PREPARE/EXECUTE(主提交版本)
- 时间复杂度:O(S × P)
- 空间复杂度:O(S) --- 拼接字符串
2254. Design Video Sharing Platform(设计视频共享平台)
题目描述
实现 VideoSharingPlatform 类:upload(String video) 上传视频,返回最小可用 videoId(从 0 开始,视频删除后其 id 可复用);remove(int videoId) 删除视频(不存在则忽略);watch(int videoId, int startMinute, int endMinute) 若视频存在则观看数 +1 并返回从 startMinute 到 min(endMinute, len-1)(含边界)的子串,否则返回 "-1";like(int videoId) 点赞数 +1(不存在则忽略);dislike(int videoId) 点踩数 +1(不存在则忽略);getLikesAndDislikes(int videoId) 返回 [点赞数, 点踩数],不存在返回 [-1];getViews(int videoId) 返回观看数,不存在返回 -1。
示例 1:
输入:["VideoSharingPlatform", "upload", "upload", "remove", "remove", "upload", "watch", "watch", "like", "dislike", "dislike", "getLikesAndDislikes", "getViews"]
参数:[[], ["123"], ["456"], [4], [0], ["789"], [1, 0, 5], [1, 0, 1], [1], [1], [1], [1], [1]]
输出:[null, 0, 1, null, null, 0, "456", "45", null, null, null, [1, 2], 2]
解释:删除 0 后重新上传复用 id 0;watch(1,0,5) 截取 "456",watch(1,0,1) 截取 "45";点赞 1 次、点踩 2 次;观看 2 次。
示例 2:
输入:["VideoSharingPlatform", "remove", "watch", "like", "dislike", "getLikesAndDislikes", "getViews"]
参数:[[], [0], [0, 0, 1], [0], [0], [0], [0]]
输出:[null, null, "-1", null, null, [-1], -1]
解释:视频 0 不存在,所有操作都按"不存在"处理。
约束:
-
1 <= video.length <= 10⁴ -
0 <= videoId, startMinute <= 1⁰⁵ -
videoId、startMinute、endMinute均为整数 -
startMinute <= endMinute -
upload、remove、watch、like、dislike、getLikesAndDislikes、getViews总调用次数不超过10⁵ -
video只含数字字符
解题思路
核心思想 :哈希表存储 + 最小堆复用 id。用 Map<Integer, Video> 保存当前存在的视频;用 PriorityQueue 记录删除后被释放的最小 id;全局自增 nextId 分配新 id。上传时优先复用堆顶最小 id。
步骤:
upload:堆非空则取堆顶复用,否则用nextId++;存入视频对象。remove:从 Map 移除并offer进堆(存在才做)。watch:存在则观看数 +1,截取[start, min(end, len-1)];否则返回"-1"。- 其余操作依题意对 Map 判空处理。
复杂度:upload O(log n)(堆操作),其余 O(1);空间 O(在存视频数)。
代码实现
java
import java.util.HashMap;
import java.util.Map;
import java.util.PriorityQueue;
/**
* 2254. Design Video Sharing Platform 设计视频共享平台
*
* 实现 VideoSharingPlatform 类:
* - upload(String video) 上传视频,返回最小可用 videoId(从 0 开始,删除后可复用)。
* - remove(int videoId) 删除视频(不存在则忽略)。
* - watch(int videoId, int startMinute, int endMinute) 观看数 +1,返回从 startMinute 到
* min(endMinute, len-1) 的子串;不存在返回 "-1"。
* - like(int videoId) / dislike(int videoId) 点赞 / 点踩(不存在则忽略)。
* - getLikesAndDislikes(int videoId) 返回 [赞, 踩];不存在返回 [-1]。
* - getViews(int videoId) 返回观看数;不存在返回 -1。
*
* 算法思路:用 Map<Integer, Video> 存当前存在的视频,PriorityQueue 管理被删除后待复用的最小
* videoId(贪心取最小);自增 id 作为全局递增分配器。upload 时优先从队列取复用 id,否则用下一个
* 新 id。
*
* 时间复杂度:upload O(log n)、其余 O(1);空间复杂度:O(视频数)
*/
public class Solution {
public static void main(String[] args) {
VideoSharingPlatform p = new VideoSharingPlatform();
System.out.println(p.upload("123")); // 0
System.out.println(p.upload("456")); // 1
p.remove(4); // 忽略(不存在)
p.remove(0); // 删除 0
System.out.println(p.upload("789")); // 0(复用)
System.out.println(p.watch(1, 0, 5)); // "456"
System.out.println(p.watch(1, 0, 1)); // "45"
p.like(1);
p.dislike(1);
p.dislike(1);
System.out.println(java.util.Arrays.toString(p.getLikesAndDislikes(1))); // [1,2]
System.out.println(p.getViews(1)); // 2
VideoSharingPlatform p2 = new VideoSharingPlatform();
p2.remove(0);
System.out.println(p2.watch(0, 0, 1)); // "-1"
p2.like(0);
p2.dislike(0);
System.out.println(java.util.Arrays.toString(p2.getLikesAndDislikes(0))); // [-1]
System.out.println(p2.getViews(0)); // -1
}
}
class VideoSharingPlatform {
private static class Video {
String content;
int views, likes, dislikes;
Video(String content) {
this.content = content;
}
}
private Map<Integer, Video> videos = new HashMap<>();
private PriorityQueue<Integer> reusable = new PriorityQueue<>();
private int nextId = 0;
public int upload(String video) {
int id;
if (!reusable.isEmpty()) {
id = reusable.poll(); // 复用被删除的最小 id
} else {
id = nextId++;
}
videos.put(id, new Video(video));
return id;
}
public void remove(int videoId) {
if (videos.remove(videoId) != null) {
reusable.offer(videoId);
}
}
public String watch(int videoId, int startMinute, int endMinute) {
Video v = videos.get(videoId);
if (v == null) {
return "-1";
}
v.views++;
int end = Math.min(endMinute, v.content.length() - 1);
return v.content.substring(startMinute, end + 1);
}
public void like(int videoId) {
Video v = videos.get(videoId);
if (v != null) {
v.likes++;
}
}
public void dislike(int videoId) {
Video v = videos.get(videoId);
if (v != null) {
v.dislikes++;
}
}
public int[] getLikesAndDislikes(int videoId) {
Video v = videos.get(videoId);
if (v == null) {
return new int[]{-1};
}
return new int[]{v.likes, v.dislikes};
}
public int getViews(int videoId) {
Video v = videos.get(videoId);
return v == null ? -1 : v.views;
}
}
复杂度分析
- 解法:哈希表 + 最小堆复用 id(主提交版本)
- 时间复杂度:upload O(log n),其余 O(1)
- 空间复杂度:O(在存视频数)
2255. Count Prefixes of a Given String(统计是给定字符串前缀的字符串数目)
题目描述
给你一个字符串数组 words 和一个字符串 s,其中 words[i] 和 s 只包含小写英文字母 。返回 words 中是字符串 s 前缀 的字符串数目。相同字符串出现多次应计数多次。
示例 1:
输入:words = ["a","b","c","ab","bc","abc"], s = "abc"
输出:3
解释:"a"、"ab"、"abc" 是 "abc" 的前缀。
示例 2:
输入:words = ["a","a"], s = "aa"
输出:2
解释:两个 "a" 都计一次。
约束:
-
1 <= words.length <= 1000 -
1 <= words[i].length, s.length <= 10 -
words[i]和s只含小写英文字母
解题思路
核心思想 :直接调用 String.startsWith。每个 word 只要 s.startsWith(word) 就计数 +1。
步骤:
- 遍历
words。 s.startsWith(w)为真则ans++。
复杂度:O(sum |word|) 时间、O(1) 空间。
代码实现
java
/**
* 2255. Count Prefixes of a Given String 统计是给定字符串前缀的字符串数目
*
* 给你一个字符串数组 words 和一个字符串 s,其中 words[i] 和 s 只包含小写英文字母。
* 返回 words 中是字符串 s 前缀的字符串数目。相同字符串出现多次应计数多次。
*
* 算法思路:遍历每个 word,若 s.startsWith(word) 则计数 +1。直接利用 String.startsWith。
*
* 时间复杂度:O(sum(|word|)),空间复杂度:O(1)
*/
public class Solution {
public int countPrefixes(String[] words, String s) {
int ans = 0;
for (String w : words) {
if (s.startsWith(w)) {
ans++;
}
}
return ans;
}
public static void main(String[] args) {
Solution s = new Solution();
// 示例 1:预期 3
System.out.println(s.countPrefixes(new String[]{"a", "b", "c", "ab", "bc", "abc"}, "abc"));
// 示例 2:预期 2
System.out.println(s.countPrefixes(new String[]{"a", "a"}, "aa"));
// 自测:空串前缀(题目保证非空,这里验证行为):预期 2
System.out.println(s.countPrefixes(new String[]{"a", "aa", "ab", "abcd"}, "abc"));
}
}
复杂度分析
- 解法:遍历 + startsWith(主提交版本)
- 时间复杂度:O(∑|word|)
- 空间复杂度:O(1)
2256. Minimum Average Difference(最小平均差)
题目描述
给你一个下标从 0 开始长度为 n 的整数数组 nums。下标 i 处的平均差 指的是 nums 中前 i + 1 个元素平均值和后 n - i - 1 个元素平均值的绝对差 。两个平均值都需要向下取整 到最近的整数。返回产生最小平均差 的下标;如果有多个结果相等,返回最小 的下标。注意 :0 个元素的平均值视为 0。
示例 1:
输入:nums = [2,5,3,9,5,3]
输出:3
解释:下标 3 处前后平均分别为 19/4=4 与 8/2=4,差 0 最小。
示例 2:
输入:nums = [0]
输出:0
解释:唯一下标,差 |0 - 0| = 0。
约束:
-
1 <= nums.length <= 10⁵ -
0 <= nums[i] <= 10⁵
解题思路
核心思想 :前缀和一趟扫描。先求总和 total,再从左往右维护前缀和 pre;下标 i 处左侧平均 = pre / (i+1),右侧平均 =(i == n-1 时为 0,否则 (total - pre) / (n-i-1)),整除即向下取整。平均差 = 两者绝对差,维护最小差值及其下标。
步骤:
- 求
total(long防溢出)。 - 遍历:累积
pre,计算diff = |pre/(i+1) - (total-pre)/(n-i-1)|。 diff < best时更新best与idx(严格小于保证取最小下标)。
复杂度:O(n) 时间、O(1) 空间。
代码实现
java
/**
* 2256. Minimum Average Difference 最小平均差
*
* 给你一个下标从 0 开始长度为 n 的整数数组 nums。下标 i 处的平均差指的是 nums 中前 i + 1 个元素
* 平均值和后 n - i - 1 个元素平均值的绝对差。两个平均值都需要向下取整到最近的整数。
* 返回产生最小平均差的下标,若有多个取最小的下标。注意:0 个元素的平均值视为 0。
*
* 算法思路:先求总和 total。从左到右用前缀和 pre 维护前 i+1 个元素的和,则左侧平均值
* left = pre / (i+1)(整除),右侧平均值 right = (i == n-1) ? 0 : (total - pre) / (n-i-1)。
* 平均差 = |left - right|,维护最小值和对应下标。
*
* 时间复杂度:O(n),空间复杂度:O(1)
*/
public class Solution {
public int minimumAverageDifference(int[] nums) {
int n = nums.length;
long total = 0;
for (int v : nums) {
total += v;
}
long pre = 0;
long best = Long.MAX_VALUE;
int idx = 0;
for (int i = 0; i < n; i++) {
pre += nums[i];
long left = pre / (i + 1);
long right = (i == n - 1) ? 0 : (total - pre) / (n - i - 1);
long diff = Math.abs(left - right);
if (diff < best) {
best = diff;
idx = i;
}
}
return idx;
}
public static void main(String[] args) {
Solution s = new Solution();
// 示例 1:预期 3
System.out.println(s.minimumAverageDifference(new int[]{2, 5, 3, 9, 5, 3}));
// 示例 2:预期 0
System.out.println(s.minimumAverageDifference(new int[]{0}));
// 自测:预期 0(左右平均差为 |1-2|=1 vs |1-1|=0... 实际算 [1,2,3,4,5])
System.out.println(s.minimumAverageDifference(new int[]{1, 2, 3, 4, 5}));
}
}
复杂度分析
- 解法:前缀和扫描(主提交版本)
- 时间复杂度:O(n)
- 空间复杂度:O(1)
2257. Count Unguarded Cells in the Grid(统计网格图中没有被保卫的格子数)
题目描述
给你两个整数 m 和 n 表示一个下标从 0 开始的 m x n 网格图。同时给二维数组 guards(guards[i] = [rowi, coli] 表示第 i 个警卫的位置)和 walls(walls[j] = [rowj, colj] 表示第 j 座墙的位置)。一个警卫能看到 4 个坐标轴方向(东、南、西、北)的所有 格子,除非被墙或另一个警卫挡住 视线。如果一个格子能被至少 一个警卫看到,则说它被保卫 了。返回空格子中有多少格子没被保卫。
示例 1:
输入:m = 4, n = 6, guards = [[0,0],[1,1],[2,3]], walls = [[0,1],[2,2],[1,4]]
输出:7
解释:网格中共 7 个空格未被保卫。
示例 2:
输入:m = 3, n = 3, guards = [[1,1]], walls = [[0,1],[1,0],[2,1],[1,2]]
输出:4
解释:警卫四面都被墙挡住,其余空格全未保卫。
约束:
-
1 <= m, n <= 10⁵(m * n <= 10⁵) -
0 <= guards.length, walls.length <= m * n -
1 <= guards.length + walls.length <= m * n -
警卫与墙位置互不重复
解题思路
核心思想 :网格标记 + 方向扩散。用 int 矩阵标记:墙 = 2、警卫 = 1、被保卫 = 3、空格 = 0。对每个警卫向 4 个方向直线扩散,遇到墙或另一个警卫停止,途中的空格全部标记为被保卫;最后统计仍为 0 的格子数。
步骤:
- 先标记墙为 2、警卫为 1。
- 对每个警卫,沿 4 个方向 while 循环标记(
g[r][c]非墙非警卫时置 3)。 - 统计值为 0 的格子。
复杂度:每个格子最多被扫常数次,O(m × n) 时间、O(m × n) 空间。
代码实现
java
/**
* 2257. Count Unguarded Cells in the Grid 统计网格图中没有被保卫的格子数
*
* 给你两个整数 m 和 n 表示一个下标从 0 开始的 m x n 网格图。同时给你 guards[i] = [rowi, coli]
* 表示第 i 个警卫所在位置,walls[j] = [rowj, colj] 表示第 j 座墙所在位置。一个警卫能看到 4 个
* 坐标轴方向的所有格子,除非视线被墙或另一个警卫挡住。如果一个格子能被至少一个警卫看到,则被保卫。
* 返回空格子中有多少格子没被保卫。
*
* 算法思路:标记网格:墙 = 2,警卫 = 1。然后对每个警卫向 4 个方向扩散,遇到墙或警卫停止,
* 途中标记为"被保卫"(3)。最后统计值为 0(空格且未被保卫)的单元格数量。
*
* 时间复杂度:O(m * n),空间复杂度:O(m * n)
*/
public class Solution {
public int countUnguarded(int m, int n, int[][] guards, int[][] walls) {
int[][] g = new int[m][n]; // 0 空, 1 警卫, 2 墙, 3 被保卫
for (int[] w : walls) {
g[w[0]][w[1]] = 2;
}
for (int[] gu : guards) {
g[gu[0]][gu[1]] = 1;
}
// 四方向
int[][] dirs = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
for (int[] gu : guards) {
for (int[] d : dirs) {
int r = gu[0] + d[0], c = gu[1] + d[1];
// 沿方向扩散直到出界、墙或另一个警卫
while (r >= 0 && r < m && c >= 0 && c < n && g[r][c] != 2 && g[r][c] != 1) {
g[r][c] = 3;
r += d[0];
c += d[1];
}
}
}
int ans = 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (g[i][j] == 0) {
ans++;
}
}
}
return ans;
}
public static void main(String[] args) {
Solution s = new Solution();
// 示例 1:预期 7
System.out.println(s.countUnguarded(4, 6,
new int[][]{{0, 0}, {1, 1}, {2, 3}},
new int[][]{{0, 1}, {2, 2}, {1, 4}}));
// 示例 2:预期 4
System.out.println(s.countUnguarded(3, 3,
new int[][]{{1, 1}},
new int[][]{{0, 1}, {1, 0}, {2, 1}, {1, 2}}));
}
}
复杂度分析
- 解法:网格标记 + 四向扩散(主提交版本)
- 时间复杂度:O(m × n)
- 空间复杂度:O(m × n)
2258. Escape the Spreading Fire(逃离火灾)
题目描述
给你一个下标从 0 开始大小为 m x n 的二维整数数组 grid,表示一个网格图,每个格子为 0(草地)、1(着火的格子)或 2(墙,你跟火都不能通过)。一开始你在最左上角的格子 (0, 0),要到达最右下角的安全屋 (m - 1, n - 1)。每一分钟,你可以移动到相邻 的草地格子;每次你移动之后 ,着火的格子会扩散到所有不是墙的相邻 格子。返回在初始位置可以停留的最多 分钟数,且停留完后仍能安全到达安全屋;如果无法实现返回 -1;如果不管停留多久总是 能到达,返回 10⁹。注意:到达安全屋后火马上到安全屋,视为安全到达。
示例 1:
输入:grid = [[0,2,0,0,0,0,0],[0,0,0,2,2,1,0],[0,2,0,0,1,2,0],[0,0,2,2,2,0,2],[0,0,0,0,0,0,0]]
输出:3
解释:停留 3 分钟后仍可安全到达安全屋,超过 3 分钟不行。
示例 2:
输入:grid = [[0,0,0,0],[0,1,2,0],[0,2,0,0]]
输出:-1
解释:火会蔓延到所有可达格子,无法安全到达。
示例 3:
输入:grid = [[0,0,0],[2,2,0],[1,2,0]]
输出:1000000000
解释:火被墙围住,永远无法到达安全屋路径。
约束:
-
m == grid.length -
n == grid[i].length -
1 <= m, n <= 300 -
grid[i][j]为0、1或2 -
grid[0][0] == grid[m-1][n-1] == 0
解题思路
核心思想:多源 BFS 求着火时间 + 二分等待时间。
步骤:
- 多源 BFS(所有
1同时出发)求每个格子的着火时间fire[i][j],不可达记为INF。 - BFS 求人从
(0,0)到每个格子的最短时间human[i][j]。 - 若人原本到不了终点返回
-1;若起点永远不着火(fire[0][0] == INF)返回10⁹。 - 二分等待分钟数
wait(上界fire[0][0] - 1):人在时刻wait出发,到达中间格子(r,c)的实际时刻 =human[r][c] + wait,必须严格小于fire[r][c];终点允许<=(同时到达算安全)。用 BFS 验证wait是否可行,二分最大可行值。
复杂度:两次 BFS + 二分内 BFS,总 O(m×n × log(maxFire))。
代码实现
java
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Queue;
/**
* 2258. Escape the Spreading Fire 逃离火灾
*
* 网格图 grid,0 草地、1 着火的格子、2 墙。一开始在 (0,0),要到达安全屋 (m-1,n-1)。每分钟
* 可以先移动一步,然后火扩散到相邻的草地(不能穿墙)。返回在初始位置可以停留的最多分钟数,
* 且停留后仍能安全到达安全屋;无法到达返回 -1;无论停留多久总能到达返回 10⁹。
* 注意:到达安全屋后火马上到,也算安全到达。
*
* 算法思路:
* 1. 多源 BFS 求每个格子的着火时间 fire[i][j](无法到达的格子记为 INF)。
* 2. BFS 求人从 (0,0) 到达每个格子的最早时间 human[i][j](不带等待)。
* 3. 二分"停留分钟数 wait":人在时刻 wait 从起点出发(实际到达时间 human+wait),要满足
* 对每个中间格子 t < fire[t],且到终点时 t <= fire[终点](安全屋允许同时到达)。
* 用一条 BFS 验证 wait 是否可行。对 wait 二分上界取 fire[0][0] 或一个充分大的值。
*
* 时间复杂度:O(m*n),空间复杂度:O(m*n)
*/
public class Solution {
private static final int INF = Integer.MAX_VALUE / 2;
public int maximumMinutes(int[][] grid) {
int m = grid.length, n = grid[0].length;
int[][] fire = new int[m][n];
for (int[] row : fire) {
Arrays.fill(row, INF);
}
Queue<int[]> q = new ArrayDeque<>();
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == 1) {
fire[i][j] = 0;
q.offer(new int[]{i, j});
}
}
}
bfsFire(fire, q, m, n, grid);
// 人的可达性(不带等待,纯按最短时间)
int[][] human = new int[m][n];
for (int[] row : human) {
Arrays.fill(row, INF);
}
human[0][0] = 0;
q.clear();
q.offer(new int[]{0, 0});
bfsHuman(human, q, m, n, grid);
// 若人根本到不了终点 / 起点,返回 -1 或 INF 处理
if (human[m - 1][n - 1] == INF) {
return -1;
}
// 若火根本烧不到终点 / 起点,人可以无限等(只要起点不被烧)
if (fire[0][0] == INF) {
return 1000000000;
}
int lo = -1, hi = fire[0][0]; // hi 取 fire[0][0]:若停留 fire[0][0] 分钟起点已被烧
while (lo < hi) {
int mid = (lo + hi + 1) >>> 1;
if (canReach(grid, human, fire, m, n, mid)) {
lo = mid;
} else {
hi = mid - 1;
}
}
return lo;
}
/** 多源 BFS 扩散火 */
private void bfsFire(int[][] fire, Queue<int[]> q, int m, int n, int[][] grid) {
int[][] dirs = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
while (!q.isEmpty()) {
int[] cur = q.poll();
for (int[] d : dirs) {
int r = cur[0] + d[0], c = cur[1] + d[1];
if (r < 0 || r >= m || c < 0 || c >= n || grid[r][c] == 2 || fire[r][c] != INF) {
continue;
}
fire[r][c] = fire[cur[0]][cur[1]] + 1;
q.offer(new int[]{r, c});
}
}
}
/** BFS 求人的最早到达时间 */
private void bfsHuman(int[][] human, Queue<int[]> q, int m, int n, int[][] grid) {
int[][] dirs = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
while (!q.isEmpty()) {
int[] cur = q.poll();
for (int[] d : dirs) {
int r = cur[0] + d[0], c = cur[1] + d[1];
if (r < 0 || r >= m || c < 0 || c >= n || grid[r][c] == 2 || human[r][c] != INF) {
continue;
}
human[r][c] = human[cur[0]][cur[1]] + 1;
q.offer(new int[]{r, c});
}
}
}
/** 验证停留 wait 分钟后从起点出发能否安全到达终点 */
private boolean canReach(int[][] grid, int[][] human, int[][] fire, int m, int n, int wait) {
// 起点在 wait 分钟后已被烧,则不可能
if (wait >= fire[0][0]) {
return false;
}
boolean[][] vis = new boolean[m][n];
Queue<int[]> q = new ArrayDeque<>();
q.offer(new int[]{0, 0});
vis[0][0] = true;
int[][] dirs = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
while (!q.isEmpty()) {
int[] cur = q.poll();
if (cur[0] == m - 1 && cur[1] == n - 1) {
return true;
}
for (int[] d : dirs) {
int r = cur[0] + d[0], c = cur[1] + d[1];
if (r < 0 || r >= m || c < 0 || c >= n || grid[r][c] == 2 || vis[r][c]) {
continue;
}
int t = human[cur[0]][cur[1]] + 1 + wait; // 实际到达时刻
if (r == m - 1 && c == n - 1) {
// 终点允许与火同时到达
if (t <= fire[r][c]) {
vis[r][c] = true;
q.offer(new int[]{r, c});
}
} else {
// 中间格必须在火到达之前
if (t < fire[r][c]) {
vis[r][c] = true;
q.offer(new int[]{r, c});
}
}
}
}
return false;
}
public static void main(String[] args) {
Solution s = new Solution();
// 示例 1:预期 3
System.out.println(s.maximumMinutes(new int[][]{
{0, 2, 0, 0, 0, 0, 0},
{0, 0, 0, 2, 2, 1, 0},
{0, 2, 0, 0, 1, 2, 0},
{0, 0, 2, 2, 2, 0, 2},
{0, 0, 0, 0, 0, 0, 0}}));
// 示例 2:预期 -1
System.out.println(s.maximumMinutes(new int[][]{
{0, 0, 0, 0},
{0, 1, 2, 0},
{0, 2, 0, 0}}));
// 示例 3:预期 1000000000
System.out.println(s.maximumMinutes(new int[][]{
{0, 0, 0},
{2, 2, 0},
{1, 2, 0}}));
}
}
复杂度分析
- 解法:多源 BFS 着火时间 + 二分等待(主提交版本)
- 时间复杂度:O(m × n × log(maxFire))
- 空间复杂度:O(m × n)
2259. Remove Digit From Number to Maximize Result(移除指定数字得到的最大结果)
题目描述
给你一个表示某个正整数的字符串 number 和一个字符 digit。从 number 中恰好 移除一个 等于 digit 的字符后,返回按十进制 表示最大 的结果字符串。生成的测试用例保证 digit 在 number 中出现至少一次。
示例 1:
输入:number = "123", digit = '3'
输出:"12"
解释:数字串中只有一个 '3',移除后为 "12"。
示例 2:
输入:number = "1231", digit = '1'
输出:"231"
解释:移除第一个 '1' 得 "231",移除第二个得 "123",取较大者。
示例 3:
输入:number = "551", digit = '5'
输出:"51"
解释:移除任一 '5' 结果都是 "51"。
约束:
-
2 <= number.length <= 100 -
number由数字组成 -
digit是'0'到'9'的数字 -
digit在number中至少出现一次
解题思路
核心思想 :枚举试删 + 字符串比较。数字串长度相同,字典序比较即数值比较。对每个等于 digit 的位置试删一次,保留字典序最大的结果。
步骤:
- 遍历
number,对每个等于digit的字符构造substring(0,i) + substring(i+1)。 - 用
compareTo与当前最优比较,更新。
复杂度:O(n²)(n ≤ 100),空间 O(n)。
进阶写法的贪心 :也可只删『digit 且后一位更大』的第一个位置,否则删最后一次出现的该字符,O(n) 实现;本题枚举更直观。
代码实现
java
/**
* 2259. Remove Digit From Number to Maximize Result 移除指定数字得到的最大结果
*
* 给你一个表示某个正整数的字符串 number 和一个字符 digit。从 number 中恰好移除一个等于 digit
* 的字符后,返回按十进制表示最大的结果字符串。测试用例保证 digit 在 number 中出现至少一次。
*
* 算法思路:贪心 + 字符串比较。从左到右扫描,若当前字符等于 digit 且是最后一次出现(或
* 它后面紧邻的字符更大------实际上对于相同长度的数字串,字典序比较即数值比较),更通用的做法:
* 对每个等于 digit 的位置试删一次,保留字典序(即数值)最大的结果。因 length 最多 100,
* O(n²) 完全可行,简单不易错。
*
* 时间复杂度:O(n²),空间复杂度:O(n)
*/
public class Solution {
public String removeDigit(String number, char digit) {
String best = "";
for (int i = 0; i < number.length(); i++) {
if (number.charAt(i) == digit) {
String cand = number.substring(0, i) + number.substring(i + 1);
// 字符串比较等价于数值比较(长度相同)
if (cand.compareTo(best) > 0) {
best = cand;
}
}
}
return best;
}
public static void main(String[] args) {
Solution s = new Solution();
// 示例 1:预期 "12"
System.out.println(s.removeDigit("123", '3'));
// 示例 2:预期 "231"
System.out.println(s.removeDigit("1231", '1'));
// 示例 3:预期 "51"
System.out.println(s.removeDigit("551", '5'));
// 自测:预期 "52"(删大的那一个)
System.out.println(s.removeDigit("552", '5'));
}
}
复杂度分析
- 解法:枚举试删 + 字符串比较(主提交版本)
- 时间复杂度:O(n²)(n ≤ 100)
- 空间复杂度:O(n)
2260. Minimum Consecutive Cards to Pick Up(必须拿起的最小连续卡牌数)
题目描述
给你一个整数数组 cards,其中 cards[i] 表示第 i 张卡牌的值 。如果两张卡牌的值相同,则认为这一对卡牌匹配 。返回必须拿起的最小连续卡牌数,以使在拿起的卡牌中有一对匹配的卡牌;如果无法得到一对匹配的卡牌返回 -1。
示例 1:
输入:cards = [3,4,2,3,4,7]
输出:4
解释:拿起 [3,4,2,3] 包含一对 3;[4,2,3,4] 也是最优方案。
示例 2:
输入:cards = [1,0,5,3]
输出:-1
解释:没有任何值出现两次。
约束:
-
1 <= cards.length <= 10⁵ -
0 <= cards[i] <= 10⁶
解题思路
核心思想 :哈希表记录最近下标。用 Map<值, 最近出现下标> 扫描数组,遇到已出现的值,连续区间长度 = i - last + 1,更新全局最小值,然后刷新该值最近下标。若从未遇到重复,返回 -1。
步骤:
Map<Integer,Integer> last记录每个值最近一次出现的位置。- 遍历
cards:若last中存在cards[i],ans = min(ans, i - prev + 1);更新last。 - 返回
ans或-1。
复杂度:O(n) 时间、O(n) 空间。
代码实现
java
import java.util.HashMap;
import java.util.Map;
/**
* 2260. Minimum Consecutive Cards to Pick Up 必须拿起的最小连续卡牌数
*
* 给你一个整数数组 cards,cards[i] 表示第 i 张卡牌的值。如果两张卡牌的值相同,则认为这一对卡牌
* 匹配。返回必须拿起的最小连续卡牌数,使拿起的卡牌中有一对匹配;如果无法得到,返回 -1。
*
* 算法思路:滑动窗口 + 哈希表。用 Map<值, 最近出现下标> 记录每个值最近一次出现的位置;
* 遍历数组,若当前值之前出现过,则窗口大小 = i - last + 1,更新答案最小值;然后更新该值的
* 最近下标为 i。
*
* 时间复杂度:O(n),空间复杂度:O(n)
*/
public class Solution {
public int minimumCardPickup(int[] cards) {
Map<Integer, Integer> last = new HashMap<>();
int ans = Integer.MAX_VALUE;
for (int i = 0; i < cards.length; i++) {
Integer prev = last.get(cards[i]);
if (prev != null) {
ans = Math.min(ans, i - prev + 1);
}
last.put(cards[i], i);
}
return ans == Integer.MAX_VALUE ? -1 : ans;
}
public static void main(String[] args) {
Solution s = new Solution();
// 示例 1:预期 4
System.out.println(s.minimumCardPickup(new int[]{3, 4, 2, 3, 4, 7}));
// 示例 2:预期 -1
System.out.println(s.minimumCardPickup(new int[]{1, 0, 5, 3}));
// 自测:预期 2
System.out.println(s.minimumCardPickup(new int[]{1, 1}));
}
}
复杂度分析
- 解法:哈希表记录最近下标(主提交版本)
- 时间复杂度:O(n)
- 空间复杂度:O(n)