LeetCode 2321-2340. 题解合集
文章目录
- [LeetCode 2321-2340. 题解合集](#LeetCode 2321-2340. 题解合集)
-
- [2321. Maximum Score Of Spliced Array(拼接数组的最大分数)](#2321. Maximum Score Of Spliced Array(拼接数组的最大分数))
- [2322. Minimum Score After Removals on a Tree(从树中删除边的最小分数)](#2322. Minimum Score After Removals on a Tree(从树中删除边的最小分数))
- [2323. Find Minimum Time to Finish All Jobs II(完成所有工作的最短时间 II 🔒)](#2323. Find Minimum Time to Finish All Jobs II(完成所有工作的最短时间 II 🔒))
- [2324. Product Sales Analysis IV(产品销售分析 IV 🔒)](#2324. Product Sales Analysis IV(产品销售分析 IV 🔒))
- [2325. Decode the Message(解密消息)](#2325. Decode the Message(解密消息))
- [2326. Spiral Matrix IV(螺旋矩阵 IV)](#2326. Spiral Matrix IV(螺旋矩阵 IV))
- [2327. Number of People Aware of a Secret(知道秘密的人数)](#2327. Number of People Aware of a Secret(知道秘密的人数))
- [2328. Number of Increasing Paths in a Grid(网格图中递增路径的数目)](#2328. Number of Increasing Paths in a Grid(网格图中递增路径的数目))
- [2329. Product Sales Analysis V(产品销售分析 Ⅴ 🔒)](#2329. Product Sales Analysis V(产品销售分析 Ⅴ 🔒))
- [2330. Valid Palindrome IV(验证回文串 IV 🔒)](#2330. Valid Palindrome IV(验证回文串 IV 🔒))
- [2331. Evaluate Boolean Binary Tree(计算布尔二叉树的值)](#2331. Evaluate Boolean Binary Tree(计算布尔二叉树的值))
- [2332. The Latest Time to Catch a Bus(坐上公交的最晚时间)](#2332. The Latest Time to Catch a Bus(坐上公交的最晚时间))
- [2333. Minimum Sum of Squared Difference(最小差值平方和)](#2333. Minimum Sum of Squared Difference(最小差值平方和))
- [2334. Subarray With Elements Greater Than Varying Threshold(元素值大于变化阈值的子数组)](#2334. Subarray With Elements Greater Than Varying Threshold(元素值大于变化阈值的子数组))
- [2335. Minimum Amount of Time to Fill Cups(装满杯子需要的最短总时长)](#2335. Minimum Amount of Time to Fill Cups(装满杯子需要的最短总时长))
- [2336. Smallest Number in Infinite Set(无限集中的最小数字)](#2336. Smallest Number in Infinite Set(无限集中的最小数字))
- [2337. Move Pieces to Obtain a String(移动片段得到字符串)](#2337. Move Pieces to Obtain a String(移动片段得到字符串))
- [2338. Count the Number of Ideal Arrays(统计理想数组的数目)](#2338. Count the Number of Ideal Arrays(统计理想数组的数目))
- [2339. All the Matches of the League(联赛的所有比赛 🔒)](#2339. All the Matches of the League(联赛的所有比赛 🔒))
- [2340. Minimum Adjacent Swaps to Make a Valid Array(生成有效数组的最少交换次数 🔒)](#2340. Minimum Adjacent Swaps to Make a Valid Array(生成有效数组的最少交换次数 🔒))
2321. Maximum Score Of Spliced Array(拼接数组的最大分数)
题目描述
给你两个下标从 0 开始的整数数组 nums1 和 nums2 ,长度都是 n 。可以选择两个整数 left 和 right(0 <= left <= right < n),接着交换 两个子数组 nums1[left...right] 和 nums2[left...right]。可以执行该操作一次 或不执行。数组的分数 取 sum(nums1) 和 sum(nums2) 中的最大值,返回可能的最大分数。
示例 1:
输入:nums1 = [60,60,60], nums2 = [10,90,10]
输出:210
解释:选择 left = 1 和 right = 1,得到 nums1 = [60,90,60] 和 nums2 = [10,60,10],分数为 max(210, 80) = 210。
示例 2:
输入:nums1 = [20,40,20,70,30], nums2 = [50,20,50,40,20]
输出:220
示例 3:
输入:nums1 = [7,11,13], nums2 = [1,1,1]
输出:31
约束:
-
n == nums1.length == nums2.length -
1 <= n <= 10⁵ -
1 <= nums1[i], nums2[i] <= 10⁴
解题思路
核心思想 :Kadane 最大子段和。交换子数组后 nums1 的和变化量 = sum(nums2[l..r]) - sum(nums1[l..r]),即在差数组 d = nums2 - nums1 上求最大子段和 (可不交换则取 0)。分别以求 nums2 - nums1 与 nums1 - nums2 的最大子段和来放大 nums1 或 nums2,加到各自原和上取较大者为答案。
复杂度:O(n) 时间、O(1) 空间。
代码实现
java
/**
* LeetCode 2321. Maximum Score Of Spliced Array(拼接数组的最大分数)
*
* 题目描述:给你两个下标从 0 开始的整数数组 nums1 和 nums2 ,长度都是 n 。
* 你可以选择两个整数 left 和 right (0 <= left <= right < n),接着交换两个子数组
* nums1[left...right] 和 nums2[left...right]。你可以选择执行上述操作一次或不执行任何操作。
* 数组的分数取 sum(nums1) 和 sum(nums2) 中的最大值,返回可能的最大分数。
*
* 思路:交换子数组后,nums1 的和变化量 = sum(nums2[l..r]) - sum(nums1[l..r]),
* 即差数组 d = nums2 - nums1 的某个子数组和。要让最终分数最大,分两种方向:
* ① 尽力让 nums1 变大:在 nums2 - nums1 上求最大子段和(可空则取 0),加到 sum(nums1);
* ② 尽力让 nums2 变大:在 nums1 - nums2 上求最大子段和(可空则取 0),加到 sum(nums2)。
* 两者取较大者即为答案(Kadane 算法)。
*
* 复杂度:时间 O(n),空间 O(1)。
*/
class Solution {
public int maximumsSplicedArray(int[] nums1, int[] nums2) {
int n = nums1.length;
int sum1 = 0, sum2 = 0;
for (int i = 0; i < n; i++) {
sum1 += nums1[i];
sum2 += nums2[i];
}
// 方向 ①:交换后让 nums1 尽可能大
int best = 0, cur = 0;
for (int i = 0; i < n; i++) {
cur = Math.max(0, cur + nums2[i] - nums1[i]);
best = Math.max(best, cur);
}
int ans = sum1 + best;
// 方向 ②:交换后让 nums2 尽可能大
best = 0;
cur = 0;
for (int i = 0; i < n; i++) {
cur = Math.max(0, cur + nums1[i] - nums2[i]);
best = Math.max(best, cur);
}
ans = Math.max(ans, sum2 + best);
return ans;
}
// 本地验证
public static void main(String[] args) {
Solution sol = new Solution();
// 示例 1:预期 210
System.out.println(sol.maximumsSplicedArray(new int[]{60, 60, 60}, new int[]{10, 90, 10}));
// 示例 2:预期 220
System.out.println(sol.maximumsSplicedArray(new int[]{20, 40, 20, 70, 30}, new int[]{50, 20, 50, 40, 20}));
// 示例 3:预期 31
System.out.println(sol.maximumsSplicedArray(new int[]{7, 11, 13}, new int[]{1, 1, 1}));
}
}
复杂度分析
- 解法:Kadane 最大子段和(主提交版本)
- 时间复杂度:O(n)
- 空间复杂度:O(1)
2322. Minimum Score After Removals on a Tree(从树中删除边的最小分数)
题目描述
存在一棵无向连通树,编号 0 到 n - 1 的 n 个节点、n - 1 条边。nums[i] 表示第 i 个节点的值,edges[i] = [ai, bi] 表示一条边。删除两条不同 的边形成三个连通组件,分别计算每个组件所有节点值的异或 值,最大异或值与最小异或值的差值 即为该方案的分数。返回所有方案中的最小分数。
示例 1:
输入:nums = [1,5,5,4,11], edges = [[0,1],[1,2],[1,3],[3,4]]
输出:9
解释:组件值 [5,4,11] 异或 10、[1] 异或 1、[5] 异或 5,分数 10 - 1 = 9。
示例 2:
输入:nums = [5,5,2,4,4,2], edges = [[0,1],[1,2],[5,2],[4,3],[1,3]]
输出:0
约束:
-
n == nums.length -
3 <= n <= 1000 -
1 <= nums[i] <= 10⁸ -
edges.length == n - 1 -
edges表示一棵有效的树
解题思路
核心思想 :预处理子树异或 + 祖先判定 + 枚举。以 0 为根 DFS,预处理 sub[u](子树异或和)与时间戳 in[u]/out[u](O(1) 判定祖先关系)。删两条边等效于选两个非根子树根 a、b:若 a 是 b 祖先,三组件异或为 sub[b]、sub[a]^sub[b]、total^sub[a];互不包含时为 sub[a]、sub[b]、total^sub[a]^sub[b]。枚举所有 (a,b) 求最大异或与最小异或之差的最小值。
复杂度:O(n²) 时间、O(n) 空间。
代码实现
java
/**
* LeetCode 2322. Minimum Score After Removals on a Tree(从树中删除边的最小分数)
*
* 题目描述:存在一棵无向连通树,树中有编号从 0 到 n - 1 的 n 个节点,以及 n - 1 条边。
* 给你一个下标从 0 开始的整数数组 nums ,nums[i] 表示第 i 个节点的值;edges[i] = [ai, bi]
* 表示一条边。删除树中两条不同的边以形成三个连通组件,分别计算每个组件中所有节点值的
* 异或值,三种异或值中最大值与最小值的差即为该方案的分数。返回所有方案中的最小分数。
*
* 思路:以节点 0 为根做一次 DFS,预处理:
* ① sub[u] = 以 u 为根的子树的异或和;
* ② 时间戳 in[u] / out[u],用于 O(1) 判断祖先-后代关系(u 是 v 的祖先 ⟺ in[u]<in[v] 且 out[v]<out[u])。
* 删掉两条边等价于选定两条边的"下端"(非根)子树根 a、b(a != b,均不为 0):
* - 若 a 是 b 的祖先(a 包含 b):三个组件异或为 sub[b]、sub[a]^sub[b]、total^sub[a];
* - 若 b 是 a 的祖先:对称处理;
* - 否则(互不包含):三个组件异或为 sub[a]、sub[b]、total^sub[a]^sub[b]。
* 枚举 O(n²) 对 (a, b) 求最大异或与最小异或之差的最小值。
*
* 复杂度:时间 O(n²),空间 O(n)。
*/
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
class Solution {
private List<Integer>[] g;
private int[] sub;
private int[] in, out;
private int clock;
public int minimumScore(int[] nums, int[][] edges) {
int n = nums.length;
g = new List[n];
for (int i = 0; i < n; i++) {
g[i] = new ArrayList<>();
}
for (int[] e : edges) {
g[e[0]].add(e[1]);
g[e[1]].add(e[0]);
}
sub = new int[n];
in = new int[n];
out = new int[n];
clock = 0;
dfs(0, -1, nums);
int total = sub[0];
int ans = Integer.MAX_VALUE;
// a、b 枚举非根子树(根的父边不存在,不能删)
for (int a = 1; a < n; a++) {
for (int b = a + 1; b < n; b++) {
int x, y, z;
if (isAnc(a, b)) {
x = sub[b];
y = sub[a] ^ sub[b];
z = total ^ sub[a];
} else if (isAnc(b, a)) {
x = sub[a];
y = sub[b] ^ sub[a];
z = total ^ sub[b];
} else {
x = sub[a];
y = sub[b];
z = total ^ sub[a] ^ sub[b];
}
int mx = Math.max(x, Math.max(y, z));
int mn = Math.min(x, Math.min(y, z));
ans = Math.min(ans, mx - mn);
}
}
return ans;
}
private void dfs(int u, int fa, int[] nums) {
in[u] = ++clock;
sub[u] = nums[u];
for (int v : g[u]) {
if (v != fa) {
dfs(v, u, nums);
sub[u] ^= sub[v];
}
}
out[u] = ++clock;
}
private boolean isAnc(int u, int v) {
return in[u] < in[v] && out[v] < out[u];
}
// 本地验证
public static void main(String[] args) {
Solution sol = new Solution();
// 示例 1:预期 9
System.out.println(sol.minimumScore(new int[]{1, 5, 5, 4, 11},
new int[][]{{0, 1}, {1, 2}, {1, 3}, {3, 4}}));
// 示例 2:预期 0
System.out.println(sol.minimumScore(new int[]{5, 5, 2, 4, 4, 2},
new int[][]{{0, 1}, {1, 2}, {5, 2}, {4, 3}, {1, 3}}));
// 自测:n=3 链 [0]-[1]-[2],删除两条边是唯一方案
// nums=[1,2,3]:total=0,sub[1]=2^3=1,sub[2]=3 → 组件 {0}=1,{1}=2,{2}=3 → 差 2
System.out.println(sol.minimumScore(new int[]{1, 2, 3}, new int[][]{{0, 1}, {1, 2}}));
}
}
复杂度分析
- 解法:子树异或 + 祖先判定 + 枚举(主提交版本)
- 时间复杂度:O(n²)
- 空间复杂度:O(n)
2323. Find Minimum Time to Finish All Jobs II(完成所有工作的最短时间 II 🔒)
题目描述
给你两个下标从 0 开始的整数数组 jobs 和 workers(等长):jobs[i] 是完成第 i 个工作所需的时间,workers[j] 是第 j 个工人每天可以工作的时间。每项工作应该正好 分配给一个工人,每个工人只能完成一项 工作。返回分配后完成所有作业所需的最少天数。
示例 1:
输入: jobs = [5,2,4], workers = [1,7,5]
输出: 2
解释:把第 2 个工人分给第 0 个工作(1 天)、第 0 个工人分给第 1 个工作(2 天)、第 1 个工人分给第 2 个工作(1 天),共需 2 天。
示例 2:
输入: jobs = [3,18,15,9], workers = [6,5,1,3]
输出: 3
约束:
-
n == jobs.length == workers.length -
1 <= n <= 10⁵ -
1 <= jobs[i], workers[i] <= 10⁵
解题思路
核心思想 :贪心 + 排序(交换论证)。把"较小的工作"配给"日均工作量更大的工人"不会让最大耗时变大,故最优解为 jobs、workers 各自升序后按下标一一配对。每个配对所需天数 = ⌈jobs[i]/workers[i]⌉ = (jobs[i] + workers[i] - 1) / workers[i],总用时 = 各配对天数的最大值。
复杂度:O(n log n) 时间、O(1) 空间。
代码实现
java
/**
* LeetCode 2323. Find Minimum Time to Finish All Jobs II(完成所有工作的最短时间 II)
*
* 题目描述:给你两个下标从 0 开始的整数数组 jobs 和 workers(长度相等),jobs[i] 是完成
* 第 i 个工作所需的时间,workers[j] 是第 j 个工人每天可以工作的时间。每项工作应该正好
* 分配给一个工人,每个工人只能完成一项工作。返回分配后完成所有作业所需的最少天数。
*
* 思路:贪心 + 排序(交换论证)。若把"较小的工作"分配给"较强(日均工作量更大)的工人、
* 较大的工作分配给较弱的工人,则交换两个工人的分配不会使最大耗时变大,甚至会变小。
* 因此最优解是把 jobs 升序、workers 升序后按下标一一配对(弱工人做小工作、强工人做大工作),
* 每个配对需要的天数 = ceil(jobs[i]/workers[i]) = (jobs[i]+workers[i]-1)/workers[i],
* 所有工作同时进行,总用时即为所有配对天数的最大值。
*
* 复杂度:时间 O(n log n)(排序主导),空间 O(1)。
*/
import java.util.Arrays;
class Solution {
public int minimumTime(int[] jobs, int[] workers) {
Arrays.sort(jobs);
Arrays.sort(workers);
int ans = 0;
for (int i = 0; i < jobs.length; i++) {
ans = Math.max(ans, (jobs[i] + workers[i] - 1) / workers[i]);
}
return ans;
}
// 本地验证
public static void main(String[] args) {
Solution sol = new Solution();
// 示例 1:预期 2
System.out.println(sol.minimumTime(new int[]{5, 2, 4}, new int[]{1, 7, 5}));
// 示例 2:预期 3
System.out.println(sol.minimumTime(new int[]{3, 18, 15, 9}, new int[]{6, 5, 1, 3}));
// 自测:一工人一工作
System.out.println(sol.minimumTime(new int[]{10}, new int[]{3}));
}
}
复杂度分析
- 解法:贪心 + 排序配对(主提交版本)
- 时间复杂度:O(n log n)
- 空间复杂度:O(1)
2324. Product Sales Analysis IV(产品销售分析 IV 🔒)
题目类型:数据库(SQL)
题目描述
表 Sales:sale_id(唯一)、product_id(Product 外键)、user_id、quantity;表 Product:product_id(唯一)、price。编写解决方案,为每个用户获取其消费最多 的产品 id;若同一用户在多个产品上花费了同样最多的钱,返回这些产品的全部。结果可按任意顺序返回。
示例 1:
输入:
Sales 表:
| sale_id | product_id | user_id | quantity |
| 1 | 1 | 101 | 10 |
| 2 | 3 | 101 | 7 |
| 3 | 1 | 102 | 9 |
| 4 | 2 | 102 | 6 |
| 5 | 3 | 102 | 10 |
| 6 | 1 | 102 | 6 |
Product 表:
| product_id | price |
| 1 | 10 |
| 2 | 25 |
| 3 | 15 |
输出:
| user_id | product_id |
| 101 | 3 |
| 102 | 1 |
| 102 | 2 |
| 102 | 3 |
解释:用户 101 在产品 3 花费 105 最多;用户 102 在产品 1、2、3 花费均为 150 并列最多。
约束:
-
1 <= Sales.length <= 10⁵ -
1 <= Product.length <= 10⁵ -
消费额按
SUM(quantity * price)计算
解题思路
核心思想 :CTE 求每人每产品花费 + RANK() 窗口取并列第一。先 JOIN 并按 (user_id, product_id) 分组求 amount = SUM(quantity * price);再按 user_id 分区、amount 降序排名,rn = 1 即消费最多的产品(含并列)。
SQL 要点 :WITH 公共表表达式;RANK() OVER (PARTITION BY user_id ORDER BY amount DESC)。
代码实现
sql
-- LeetCode 2324. Product Sales Analysis IV(产品销售分析 IV)
-- 题目描述:Sales 表(sale_id 唯一,product_id 为外键,记录用户购买某产品的数量);
-- Product 表(product_id 唯一,记录产品单价)。请为每个用户获取其消费最多的产品 id,
-- 若同一用户在多个产品上花费相同且最多,返回这些产品的全部。
--
-- 思路:先把每个用户在每个产品上的总花费算出来(JOIN + GROUP BY + SUM(quantity*price)),
-- 再用窗口函数 RANK() 按用户分区、按花费降序排名,rn = 1 即为该用户消费最多的产品(并列返回)。
WITH t AS (
SELECT s.user_id, s.product_id,
SUM(s.quantity * p.price) AS amount
FROM Sales s
JOIN Product p ON s.product_id = p.product_id
GROUP BY s.user_id, s.product_id
)
SELECT user_id, product_id
FROM (
SELECT user_id, product_id,
RANK() OVER (PARTITION BY user_id ORDER BY amount DESC) AS rn
FROM t
) x
WHERE rn = 1;
复杂度分析
- 解法:CTE + RANK 窗口函数(主提交版本)
- 时间复杂度:O(n log n)
- 空间复杂度:O(n)
2325. Decode the Message(解密消息)
题目描述
给你字符串 key 和 message,分别表示一个加密密钥和一段加密消息。解密步骤:使用 key 中 26 个英文小写字母第一次出现 的顺序作为替换表中字母的顺序,将替换表与普通英文字母表对齐形成对照表,再按对照表替换 message 中的每个字母,空格 ' ' 保持不变。key 保证包含字母表中每个字母至少一次。返回解密后的消息。
示例 1:
输入:key = "the quick brown fox jumps over the lazy dog", message = "vkbs bs t suepuv"
输出:"this is a secret"
示例 2:
输入:key = "eljuxhpwnyrdgtqkviszcfmabo", message = "zwx hnfx lqantp mnoeius ycgk vcnjrdb"
输出:"the five boxing wizards jump quickly"
约束:
-
26 <= key.length <= 2000 -
key由小写英文字母及空格组成 -
key包含英文字母表中每个字符至少一次 -
1 <= message.length <= 2000 -
message由小写英文字母和空格组成
解题思路
核心思想 :映射表。扫描 key 跳过空格,按"第一次出现"顺序给字符编号(第 i 个出现 → 'a' + i),存为 map[26];再遍历 message,字母按映射替换、空格原样保留。
复杂度:O(n) 时间、O(1) 空间。
代码实现
java
/**
* LeetCode 2325. Decode the Message(解密消息)
*
* 题目描述:给你字符串 key 和 message ,分别表示一个加密密钥和一段加密消息。
* 解密步骤:使用 key 中 26 个英文小写字母第一次出现的顺序作为替换表中字母的顺序,
* 将替换表与普通英文字母表对齐形成对照表,再按对照表替换 message 中的每个字母,
* 空格 ' ' 保持不变。key 保证包含字母表中每个字母至少一次。
*
* 思路:先扫描 key,忽略空格,按"第一次出现"的顺序给字符编号(0..25),
* 构建映射 f[keyChar] = 对应英文字母(第 i 个出现 → 'a'+i)。
* 再遍历 message,字母按 f 替换,空格原样保留。
*
* 复杂度:时间 O(n)(n = message 长度),空间 O(1)(仅 26 个小写字母映射)。
*/
import java.util.Arrays;
class Solution {
public String decodeMessage(String key, String message) {
char[] map = new char[26];
Arrays.fill(map, '\0');
int idx = 0;
for (char c : key.toCharArray()) {
if (c != ' ' && map[c - 'a'] == '\0') {
map[c - 'a'] = (char) ('a' + idx++);
}
}
StringBuilder sb = new StringBuilder();
for (char c : message.toCharArray()) {
if (c == ' ') {
sb.append(' ');
} else {
sb.append(map[c - 'a']);
}
}
return sb.toString();
}
// 本地验证
public static void main(String[] args) {
Solution sol = new Solution();
// 示例 1:预期 "this is a secret"
System.out.println(sol.decodeMessage(
"the quick brown fox jumps over the lazy dog",
"vkbs bs t suepuv"));
// 示例 2:预期 "the five boxing wizards jump quickly"
System.out.println(sol.decodeMessage(
"eljuxhpwnyrdgtqkviszcfmabo",
"zwx hnfx lqantp mnoeius ycgk vcnjrdb"));
}
}
复杂度分析
- 解法:映射表(主提交版本)
- 时间复杂度:O(n)
- 空间复杂度:O(1)
2326. Spiral Matrix IV(螺旋矩阵 IV)
题目描述
给你两个整数 m 和 n 表示矩阵维数,另给你一个整数链表的头节点 head。生成一个大小为 m x n 的螺旋矩阵,矩阵包含链表中的所有整数,从矩阵左上角 开始、顺时针 按螺旋 顺序填充,若有剩余空格则用 -1 填充。返回生成的矩阵。
示例 1:
输入:m = 3, n = 5, head = [3,0,2,6,8,1,7,9,4,2,5,5,0]
输出:[[3,0,2,6,8],[5,0,-1,-1,1],[5,2,4,9,7]]
示例 2:
输入:m = 1, n = 4, head = [0,1,2]
输出:[[0,1,2,-1]]
约束:
-
1 <= m, n <= 10⁵ -
1 <= m * n <= 10⁵ -
链表中节点数目在
[1, m * n]内 -
0 <= Node.val <= 1000
解题思路
核心思想 :边界收缩的螺旋遍历。用 top/bottom/left/right 四个边界控制方向,顺时针依次走右、下、左、上四条边,每走完一条边收缩对应边界;每填一格令链表指针前移,链表走完则剩余格子保持 -1。
复杂度:O(m × n) 时间、O(m × n) 空间(结果矩阵)。
代码实现
java
/**
* LeetCode 2326. Spiral Matrix IV(螺旋矩阵 IV)
*
* 题目描述:给你两个整数 m 和 n 表示矩阵维数,另给你一个整数链表的头节点 head 。
* 生成一个大小为 m x n 的螺旋矩阵,矩阵包含链表中的所有整数,从矩阵左上角开始、
* 顺时针按螺旋顺序填充,剩余空格用 -1 填充。返回生成的矩阵。
*
* 思路:经典螺旋遍历。用上、下、左、右四个边界(top/bottom/left/right)控制方向,
* 顺时针依次走右、下、左、上四条边,每走完一条边收缩对应边界;
* 每填一个格子就令链表指针前移,链表走完则剩余格子全部填 -1。
*
* 复杂度:时间 O(m * n),空间 O(m * n)(结果矩阵本身)。
*/
class Solution {
public int[][] spiralMatrix(int m, int n, ListNode head) {
int[][] ans = new int[m][n];
for (int[] row : ans) {
java.util.Arrays.fill(row, -1);
}
int top = 0, bottom = m - 1, left = 0, right = n - 1;
while (head != null && top <= bottom && left <= right) {
// 向右
for (int j = left; j <= right && head != null; j++) {
ans[top][j] = head.val;
head = head.next;
}
top++;
// 向下
for (int i = top; i <= bottom && head != null; i++) {
ans[i][right] = head.val;
head = head.next;
}
right--;
// 向左
for (int j = right; j >= left && head != null; j--) {
ans[bottom][j] = head.val;
head = head.next;
}
bottom--;
// 向上
for (int i = bottom; i >= top && head != null; i--) {
ans[i][left] = head.val;
head = head.next;
}
left++;
}
return ans;
}
// 本地验证
public static void main(String[] args) {
Solution sol = new Solution();
// 示例 1:预期 [[3,0,2,6,8],[5,0,-1,-1,1],[5,2,4,9,7]]
ListNode head1 = build(new int[]{3, 0, 2, 6, 8, 1, 7, 9, 4, 2, 5, 5, 0});
printMatrix(sol.spiralMatrix(3, 5, head1));
// 示例 2:预期 [[0,1,2,-1]]
ListNode head2 = build(new int[]{0, 1, 2});
printMatrix(sol.spiralMatrix(1, 4, head2));
}
private static ListNode build(int[] arr) {
ListNode dummy = new ListNode(0), cur = dummy;
for (int v : arr) {
cur.next = new ListNode(v);
cur = cur.next;
}
return dummy.next;
}
private static void printMatrix(int[][] mat) {
StringBuilder sb = new StringBuilder("[");
for (int i = 0; i < mat.length; i++) {
if (i > 0) sb.append(",");
sb.append("[");
for (int j = 0; j < mat[i].length; j++) {
if (j > 0) sb.append(",");
sb.append(mat[i][j]);
}
sb.append("]");
}
sb.append("]");
System.out.println(sb);
}
}
/**
* 链表节点定义(本地测试用,提交时并入题解即可)
*/
class ListNode {
int val;
ListNode next;
ListNode() {}
ListNode(int val) {
this.val = val;
}
ListNode(int val, ListNode next) {
this.val = val;
this.next = next;
}
}
复杂度分析
- 解法:边界收缩螺旋遍历(主提交版本)
- 时间复杂度:O(m × n)
- 空间复杂度:O(m × n)
2327. Number of People Aware of a Secret(知道秘密的人数)
题目描述
在第 1 天,有一个人发现了一个秘密。给定 delay------每个人会在发现秘密后的 delay 天之后,每天 给一个新的人分享秘密;forget------每个人在发现秘密 forget 天之后会忘记(忘记当天及之后不能分享)。给定 n,返回第 n 天结束时知道秘密的人数,对 10⁹ + 7 取余。
示例 1:
输入:n = 6, delay = 2, forget = 4
输出:5
示例 2:
输入:n = 4, delay = 1, forget = 3
输出:6
约束:
-
2 <= n <= 1000 -
1 <= delay < forget <= n
解题思路
核心思想 :线性递推 + 前缀和。dp[i] 表示第 i 天新增 知道秘密的人数(第 i 天新被分享的人)。第 i 天能分享的人 = 在 [i - forget + 1, i - delay] 区间内新增的人(已过 delay 且未到 forget),用前缀和 O(1) 求区间和。第 n 天结束时仍记得的人 = [n - forget + 1, n] 区间新增人数之和。
复杂度:O(n) 时间、O(n) 空间。
代码实现
java
/**
* LeetCode 2327. Number of People Aware of a Secret(知道秘密的人数)
*
* 题目描述:在第 1 天一个人发现了一个秘密。给定整数 delay(发现秘密 delay 天后每天分享
* 给一个新的人)和 forget(发现秘密 forget 天后忘记,忘记当天及之后不再分享)。
* 给定 n,返回第 n 天结束时知道秘密的人数,结果对 10⁹+7 取余。
*
* 思路:按天递推,统计每天"新增知道秘密的人数"。
* 设 share[i] = 第 i 天会分享秘密的人数(即已到 delay 且未到 forget 的人)。
* 记 dp[i] = 第 i 天新增知道秘密的人数(第 i 天新被分享的人),则有:
* newShare = sum(dp[max(1,i-forget+1) .. i-delay]),即前 forget-1 天内新增且已满 delay 的人。
* 用前缀和加速区间求和。第 n 天结束知道秘密的人数 = sum(dp[n-forget+1 .. n])(还没忘记的人)。
*
* 复杂度:时间 O(n),空间 O(n)。
*/
class Solution {
private static final int MOD = 1_000_000_007;
public int peopleAwareOfSecret(int n, int delay, int forget) {
long[] dp = new long[n + 1]; // dp[i] = 第 i 天新增知道秘密的人数
long[] pre = new long[n + 1]; // 前缀和
dp[1] = 1;
pre[1] = 1;
for (int i = 2; i <= n; i++) {
// 第 i 天能分享的人 = i-delay-1 到 i-forget 之间新增的人(已过 delay、未到 forget)
int l = Math.max(1, i - forget + 1);
int r = i - delay;
if (r >= 1 && r >= l) {
dp[i] = (pre[r] - pre[l - 1] + MOD) % MOD;
}
pre[i] = (pre[i - 1] + dp[i]) % MOD;
}
// 第 n 天结束时仍然知道秘密的人:n-forget+1 到 n 天新增的人
int l = Math.max(1, n - forget + 1);
return (int) ((pre[n] - pre[l - 1] + MOD) % MOD);
}
// 本地验证
public static void main(String[] args) {
Solution sol = new Solution();
// 示例 1:预期 5
System.out.println(sol.peopleAwareOfSecret(6, 2, 4));
// 示例 2:预期 6
System.out.println(sol.peopleAwareOfSecret(4, 1, 3));
}
}
复杂度分析
- 解法:线性递推 + 前缀和(主提交版本)
- 时间复杂度:O(n)
- 空间复杂度:O(n)
2328. Number of Increasing Paths in a Grid(网格图中递增路径的数目)
题目描述
给你一个 m x n 的整数网格图 grid,可以从一个格子移动到 4 个方向相邻的任意一个格子。返回从任意 格子出发、到达任意 格子、且路径中数字严格递增 的路径数目(对 10⁹ + 7 取余)。若两条路径访问过的格子不完全相同,视为两条不同路径。
示例 1:
输入:grid = [[1,1],[3,4]]
输出:8
解释:长度为 1 的路径 4 条;长度为 2 的 [1->3]、[1->4]、[3->4] 共 3 条;长度为 3 的 [1->3->4] 1 条,合计 8。
示例 2:
输入:grid = [[1],[2]]
输出:3
约束:
-
m == grid.length、n == grid[i].length -
1 <= m, n <= 1000 -
1 <= m * n <= 10⁵ -
1 <= grid[i][j] <= 10⁵
解题思路
核心思想 :记忆化搜索。f(i,j) = 以 (i,j) 为起点的严格递增路径数(至少含自身,初值 1)。若相邻格 (ni,nj) 满足 grid[ni][nj] > grid[i][j],则 f(i,j) += f(ni,nj);因只能走向更大的格子不会成环。答案 = 所有格子 f(i,j) 之和。
复杂度:O(m × n) 时间、O(m × n) 空间。
代码实现
java
/**
* LeetCode 2328. Number of Increasing Paths in a Grid(网格图中递增路径的数目)
*
* 题目描述:给你一个 m x n 的整数网格图 grid,可从一格移动到 4 个方向相邻的任意一格。
* 返回从任意格子出发、达到任意格子、且路径中数字是严格递增的路径数量(对 10⁹+7 取余)。
* 如果两条路径访问过的格子不完全相同,视为两条不同路径。
*
* 思路:记忆化搜索。f(i,j) 表示以 (i,j) 为起点的严格递增路径条数(至少包含自身,初值 1),
* 若相邻格 (ni,nj) 满足 grid[ni][nj] > grid[i][j],则 f(i,j) += f(ni,nj)。
* 由于只能走向更大的格子,不会成环,递归天然终止;答案 = 所有格子的 f(i,j) 之和。
*
* 复杂度:时间 O(m * n),空间 O(m * n)。
*/
class Solution {
private static final int MOD = 1_000_000_007;
private int[][] g;
private long[][] memo;
private int m, n;
private int[][] dirs = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
public int countPaths(int[][] grid) {
m = grid.length;
n = grid[0].length;
g = grid;
memo = new long[m][n];
long ans = 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
ans = (ans + dfs(i, j)) % MOD;
}
}
return (int) ans;
}
private long dfs(int i, int j) {
if (memo[i][j] != 0) {
return memo[i][j];
}
long res = 1; // 单格路径
for (int[] d : dirs) {
int ni = i + d[0], nj = j + d[1];
if (ni >= 0 && ni < m && nj >= 0 && nj < n && g[ni][nj] > g[i][j]) {
res = (res + dfs(ni, nj)) % MOD;
}
}
return memo[i][j] = res;
}
// 本地验证
public static void main(String[] args) {
Solution sol = new Solution();
// 示例 1:预期 8
System.out.println(sol.countPaths(new int[][]{{1, 1}, {3, 4}}));
// 示例 2:预期 3
System.out.println(sol.countPaths(new int[][]{{1}, {2}}));
}
}
复杂度分析
- 解法:记忆化搜索(主提交版本)
- 时间复杂度:O(m × n)
- 空间复杂度:O(m × n)
2329. Product Sales Analysis V(产品销售分析 Ⅴ 🔒)
题目类型:数据库(SQL)
题目描述
表 Sales:sale_id(唯一)、product_id(Product 外键)、user_id、quantity;表 Product:product_id(唯一)、price。编写解决方案,获取每个用户的消费额 spending,按消费额递减 排序,消费额相等时按 user_id 递增排序。
示例 1:
输入:
Sales 表:
| sale_id | product_id | user_id | quantity |
| 1 | 1 | 101 | 10 |
| 2 | 2 | 101 | 1 |
| 3 | 3 | 102 | 3 |
| 4 | 3 | 102 | 2 |
| 5 | 2 | 103 | 3 |
Product 表:
| product_id | price |
| 1 | 10 |
| 2 | 25 |
| 3 | 15 |
输出:
| user_id | spending |
| 101 | 125 |
| 102 | 75 |
| 103 | 75 |
解释:用户 101 消费 10×10 + 1×25 = 125;102 与 103 同为 75,按 user_id 升序排列。
约束:
-
1 <= Sales.length <= 10⁵ -
1 <= Product.length <= 10⁵ -
sale_id与product_id分别唯一
解题思路
核心思想 :JOIN + 分组聚合。SUM(s.quantity * p.price) 即用户消费额 spending,GROUP BY user_id 后 ORDER BY spending DESC, user_id ASC 即可。
SQL 要点 :聚合函数 SUM;多字段 ORDER BY。
代码实现
sql
-- LeetCode 2329. Product Sales Analysis V(产品销售分析 Ⅴ)
-- 题目描述:Sales 表(sale_id 唯一,product_id 外键,记录用户购买某产品的数量与对应销售);
-- Product 表(product_id 唯一,记录产品单价)。请获取每个用户的消费额 spending,
-- 按消费额递减排序,消费额相等时按 user_id 递增排序。
--
-- 思路:JOIN 后按 user_id 分组,SUM(quantity * price) 即消费额,
-- ORDER BY spending DESC, user_id ASC 满足排序要求。
SELECT s.user_id, SUM(s.quantity * p.price) AS spending
FROM Sales s
JOIN Product p ON s.product_id = p.product_id
GROUP BY s.user_id
ORDER BY spending DESC, s.user_id ASC;
复杂度分析
- 解法:JOIN + 分组聚合(主提交版本)
- 时间复杂度:O(n log n)
- 空间复杂度:O(n)
2330. Valid Palindrome IV(验证回文串 IV 🔒)
题目描述
给你一个下标从 0 开始、仅由小写英文字母组成的字符串 s 。在一步操作中,你可以将 s 中的任一字符更改为其他任何字符。如果你能在恰 执行一到两步 操作后使 s 变成一个回文 ,则返回 true,否则返回 false。
示例 1:
输入: s = "abcdba"
输出: true
解释:将 s[2] 变成 'd',得到 "abddba",一步操作即可。
示例 2:
输入: s = "aa"
输出: true
解释:两步操作改出 "bb" 亦可。
示例 3:
输入: s = "abcdef"
输出: false
约束:
-
1 <= s.length <= 10⁵ -
s仅由小写英文字母组成
解题思路
核心思想 :双指针统计不匹配对数。回文要求镜像位置字符相等,s[i] 与 s[n-1-i] 不相等的一对就需要一次修改。统计所有镜像对中不相等的数量 cnt,cnt <= 2 即可在两步内成回文(cnt = 0 时已有回文,随便改一对镜像字符并配平即可恰好用两步)。
复杂度:O(n) 时间、O(1) 空间。
代码实现
java
/**
* LeetCode 2330. Valid Palindrome IV(验证回文串 IV)
*
* 题目描述:给你一个下标从 0 开始、仅由小写英文字母组成的字符串 s 。在一步操作中,
* 你可以将 s 中的任一字符更改为其他任何字符。如果你能在恰执行一到两步操作后使 s 变成
* 一个回文,则返回 true,否则返回 false。
*
* 思路:双指针。回文要求镜像位置字符相等。统计 s[i] 与 s[n-1-i] 不相等的镜像对数量 cnt:
* 每一对不相等都需要一次修改即可配对,因此只要 cnt <= 2 就说明可在不超过两步内成为回文;
* 又因题面要求"恰一到两步"(0 步不合法,但 cnt=0 表示已是回文,此时随便改一对镜像字符
* 并保持回文构型也能恰好用两步实现),故直接判断 cnt <= 2 即可。
*
* 复杂度:时间 O(n),空间 O(1)。
*/
class Solution {
public boolean makePalindrome(String s) {
int cnt = 0;
int l = 0, r = s.length() - 1;
while (l < r) {
if (s.charAt(l) != s.charAt(r)) {
cnt++;
}
l++;
r--;
}
return cnt <= 2;
}
// 本地验证
public static void main(String[] args) {
Solution sol = new Solution();
// 示例 1:预期 true
System.out.println(sol.makePalindrome("abcdba"));
// 示例 2:预期 true
System.out.println(sol.makePalindrome("aa"));
// 示例 3:预期 false
System.out.println(sol.makePalindrome("abcdef"));
}
}
复杂度分析
- 解法:双指针计数(主提交版本)
- 时间复杂度:O(n)
- 空间复杂度:O(1)
2331. Evaluate Boolean Binary Tree(计算布尔二叉树的值)
题目描述
给你一棵完整二叉树 的根,特征如下:叶子节点值为 0 或 1(0 表示 False、1 表示 True);非叶子节点值为 2 或 3(2 表示逻辑或 OR、3 表示逻辑与 AND)。叶子节点值为其本身;否则先计算两个孩子节点的值,再按运算符运算。返回根节点的布尔运算值。
示例 1:
输入:root = [2,1,3,null,null,0,1]
输出:true
解释:AND 节点值为 False AND True = False;OR 节点值为 True OR False = True。
示例 2:
输入:root = [0]
输出:false
约束:
-
树中节点数目在
[1, 1000]之间 -
0 <= Node.val <= 3 -
每个节点的孩子数为 0 或 2
-
叶子节点值为
0或1,非叶子节点值为2或3
解题思路
核心思想 :递归后序求值。叶子节点(无孩子)直接返回 val == 1;内部节点先递归求左右孩子布尔值,再按 val == 2 做 OR、val == 3 做 AND 合并。
复杂度:O(n) 时间、O(h) 空间(递归栈)。
代码实现
java
/**
* LeetCode 2331. Evaluate Boolean Binary Tree(计算布尔二叉树的值)
*
* 题目描述:给你一棵完整二叉树的根,这棵树有以下特征:
* - 叶子节点值为 0 或 1,其中 0 表示 False,1 表示 True;
* - 非叶子节点值为 2 或 3,其中 2 表示 OR,3 表示 AND。
* 计算一个节点值的方式:叶子节点值为其本身;否则计算两个孩子节点值后按运算符运算。
* 返回根节点的布尔运算值。
*
* 思路:递归。叶子节点(无孩子)直接返回 val == 1;
* 内部节点先递归求左右孩子的布尔值,再按 val 为 2(OR)或 3(AND)合并。
*
* 复杂度:时间 O(n)(n 为节点数),空间 O(h)(h 为树高,递归栈)。
*/
class Solution {
public boolean evaluateTree(TreeNode root) {
if (root.left == null && root.right == null) {
return root.val == 1;
}
boolean l = evaluateTree(root.left);
boolean r = evaluateTree(root.right);
return root.val == 2 ? (l || r) : (l && r);
}
// 本地验证
public static void main(String[] args) {
Solution sol = new Solution();
// 示例 1:root = [2,1,3,null,null,0,1],预期 true
TreeNode root1 = new TreeNode(2,
new TreeNode(1),
new TreeNode(3, new TreeNode(0), new TreeNode(1)));
System.out.println(sol.evaluateTree(root1));
// 示例 2:root = [0],预期 false
System.out.println(sol.evaluateTree(new TreeNode(0)));
// 自测:[3,1,1] → AND(true,true)=true
System.out.println(sol.evaluateTree(new TreeNode(3, new TreeNode(1), new TreeNode(1))));
}
}
/**
* 二叉树节点定义(本地测试用,提交时由 LeetCode 环境提供)
*/
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode() {}
TreeNode(int val) {
this.val = val;
}
TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}
复杂度分析
- 解法:递归后序求值(主提交版本)
- 时间复杂度:O(n)
- 空间复杂度:O(h)
2332. The Latest Time to Catch a Bus(坐上公交的最晚时间)
题目描述
给定 n 辆公交车 buses(出发时间互不相同)、m 位乘客 passengers(到达时间互不相同)与容量 capacity。乘客排队搭乘下一辆有座位 的公交车:到达时间 y <= 出发时间 x 且车没满即可上车,最早 到达的乘客优先上车。返回可以搭乘公交车的最晚到达时间(不能跟别的乘客同时刻到达)。
示例 1:
输入:buses = [10,20], passengers = [2,17,18,19], capacity = 2
输出:16
解释:第 1 辆车载第 1 位乘客;第 2 辆车载你和第 2 位乘客,你须在第 2 位乘客(17 点)前到达,即 16 点。
示例 2:
输入:buses = [20,30,10], passengers = [19,13,26,4,25,11,21], capacity = 2
输出:20
约束:
-
n == buses.length、m == passengers.length -
1 <= n, m, capacity <= 10⁵ -
2 <= buses[i], passengers[i] <= 10⁹ -
buses、passengers元素互不相同
解题思路
核心思想 :排序 + 贪心模拟。两个数组排序后,按车逐辆模拟乘客上车:记录每辆车的实载人数与最后一位上车乘客。若车未满员,候选时间 = 发车时间;若满员,候选时间 = 最后上车乘客时间 - 1。随后循环把候选时间下调到不撞任何乘客时刻(乘客时间互异,用集合判重),取所有车候选的最大值。
复杂度:O((n + m) log(n + m)) 时间、O(m) 空间。
代码实现
java
/**
* LeetCode 2332. The Latest Time to Catch a Bus(坐上公交的最晚时间)
*
* 题目描述:给定 n 辆公交车(buses[i] 为出发时间,互不相同)与 m 位乘客(passengers[j]
* 为到达时间,互不相同),以及公交车容量 capacity。每位乘客排队搭乘下一辆有座位的公交车,
* 到达时间 y <= 出发时间 x 且有座位即可上车,最早到达的乘客优先上车。
* 返回你可以搭乘公交车的最晚到达时间(不能与任何乘客同一时刻到达)。
*
* 思路:排序 + 模拟。
* 1. buses、passengers 排序;
* 2. 用指针 i 顺序分配乘客:对每辆公交车,在容量允许范围内,把"到达时间 <= bus"的乘客依次上车;
* 3. 记录每辆公交车上车的最后一位乘客,以及该车是否满员:
* - 若车未满员,最晚可上车时间 noLater = 发车时间 bus(但需不撞乘客时刻);
* - 若已满员,最晚可上车时间 = 该车最后一位上车乘客到达时间 - 1。
* 4. noLater 需要避开所有乘客的时刻:从 noLater 往前找第一个不在乘客集合中的时刻。
* 因为乘客到达时间互不相同,用集合判重。
*
* 复杂度:时间 O((n+m) log(n+m)),空间 O(m)。
*/
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
class Solution {
public int latestTimeCatchTheBus(int[] buses, int[] passengers, int capacity) {
Arrays.sort(buses);
Arrays.sort(passengers);
Set<Integer> set = new HashSet<>();
for (int p : passengers) {
set.add(p);
}
int i = 0; // 下一个待上车乘客下标
int last = 0; // 之后用到的"最后上车乘客"时间(实际在用)
int latest = 0; // 答案候选
for (int bus : buses) {
int cnt = 0;
int lastPassenger = 0;
// 上到容量上限或没乘客可上(或乘客晚于发车)
while (cnt < capacity && i < passengers.length && passengers[i] <= bus) {
lastPassenger = passengers[i];
i++;
cnt++;
}
int candidate;
if (cnt < capacity) {
// 车没坐满:发车时间点上车即可
candidate = bus;
} else {
// 满员:必须比最后上车的乘客早至少 1 分钟
candidate = lastPassenger - 1;
}
// 避开与乘客同时到达:往前找第一个空闲时刻
while (candidate >= 1 && set.contains(candidate)) {
candidate--;
}
latest = Math.max(latest, candidate);
last = lastPassenger; // 保留(本解法未用)
}
return latest;
}
// 本地验证
public static void main(String[] args) {
Solution sol = new Solution();
// 示例 1:预期 16
System.out.println(sol.latestTimeCatchTheBus(new int[]{10, 20}, new int[]{2, 17, 18, 19}, 2));
// 示例 2:预期 20
System.out.println(sol.latestTimeCatchTheBus(new int[]{20, 30, 10}, new int[]{19, 13, 26, 4, 25, 11, 21}, 2));
// 自测:单辆车容量 1,乘客 2 点
System.out.println(sol.latestTimeCatchTheBus(new int[]{5}, new int[]{2}, 1));
}
}
复杂度分析
- 解法:排序 + 贪心模拟(主提交版本)
- 时间复杂度:O((n + m) log(n + m))
- 空间复杂度:O(m)
2333. Minimum Sum of Squared Difference(最小差值平方和)
题目描述
给定长度 n 的数组 nums1、nums2,差值平方和 = Σ (nums1[i] - nums2[i])²。可以把 nums1 中任意元素 +1/-1 至多 k1 次、nums2 中任意元素 +1/-1 至多 k2 次(元素可变为负整数)。返回修改后的最小差值平方和。
示例 1:
输入:nums1 = [1,2,3,4], nums2 = [2,10,20,19], k1 = 0, k2 = 0
输出:579
解释:差值平方和 (1-2)² + (2-10)² + (3-20)² + (4-19)² = 579。
示例 2:
输入:nums1 = [1,4,10,12], nums2 = [5,8,6,9], k1 = 1, k2 = 1
输出:43
约束:
-
n == nums1.length == nums2.length -
1 <= n <= 10⁵ -
0 <= nums1[i], nums2[i] <= 10⁵ -
0 <= k1, k2 <= 10⁹
解题思路
核心思想 :贪心削减最大差值。令 d[i] = |nums1[i] - nums2[i]|,一次操作(对任一数组)等价于把某个 d[i] 减 1(d[i] > 0 时)。平方和 Σd[i]² 为凸函数:削减越大差值收益 2d - 1 越大。用大根堆每次削减当前最大差值,直到预算 k = k1 + k2 耗尽或全部为 0。
复杂度:O(n + k log n) 时间、O(n) 空间。
代码实现
java
/**
* LeetCode 2333. Minimum Sum of Squared Difference(最小差值平方和)
*
* 题目描述:给定长度 n 的数组 nums1、nums2,差值平方和 = Σ (nums1[i] - nums2[i])²。
* 你可以把 nums1 中任意元素 +1 或 -1 至多 k1 次,把 nums2 中任意元素 +1 或 -1 至多 k2 次
* (元素可变为负整数)。返回修改后的最小差值平方和。
*
* 思路:令 d[i] = |nums1[i] - nums2[i]|,一次操作(无论对哪个数组)等价于把某个 d[i] 减 1
* (当 d[i] > 0 时);d[i] = 0 时操作只会增加平方和不划算。因此可用总预算 k = k1 + k2
* 去削减 d[i]。平方和 Σ d[i]² 是凸函数:削减越大的 d[i],
* 收益 (d[i]² - (d[i]-1)²) = 2*d[i]-1 越大。用大根堆(或频次数组)每次削减当前最大的 d[i],
* 直到预算耗尽或全部为 0。
*
* 复杂度:时间 O(n + V log V)(V 为差值上界,用频次桶可做到 O(n + V + k·log),
* 大根堆实现简单直观),空间 O(n)。
*/
import java.util.PriorityQueue;
class Solution {
public long minSumSquareDiff(int[] nums1, int[] nums2, int k1, int k2) {
int n = nums1.length;
PriorityQueue<Integer> pq = new PriorityQueue<>((a, b) -> b - a);
long sum = 0;
for (int i = 0; i < n; i++) {
int d = Math.abs(nums1[i] - nums2[i]);
sum += (long) d * d;
if (d > 0) {
pq.offer(d);
}
}
long k = (long) k1 + k2;
while (k > 0 && !pq.isEmpty()) {
int d = pq.poll();
// 每次削减当前最大差值
int dec = 1;
if (dec > k) {
dec = (int) k;
}
int newD = d - dec;
// 更新平方和
sum -= (long) d * d - (long) newD * newD;
if (newD > 0) {
pq.offer(newD);
}
k -= dec;
}
return sum;
}
// 本地验证
public static void main(String[] args) {
Solution sol = new Solution();
// 示例 1:预期 579
System.out.println(sol.minSumSquareDiff(new int[]{1, 2, 3, 4}, new int[]{2, 10, 20, 19}, 0, 0));
// 示例 2:预期 43
System.out.println(sol.minSumSquareDiff(new int[]{1, 4, 10, 12}, new int[]{5, 8, 6, 9}, 1, 1));
}
}
复杂度分析
- 解法:贪心最大堆削减(主提交版本)
- 时间复杂度:O(n + k log n)
- 空间复杂度:O(n)
2334. Subarray With Elements Greater Than Varying Threshold(元素值大于变化阈值的子数组)
题目描述
给你一个整数数组 nums 和一个整数 threshold。找到长度为 k 的 nums 子数组,满足数组中每个元素都大于 threshold / k。返回满足要求的任意 子数组的大小;若没有,返回 -1。
示例 1:
输入:nums = [1,3,4,3,1], threshold = 6
输出:3
解释:子数组 [3,4,3] 每个元素都大于 6/3 = 2。
示例 2:
输入:nums = [6,5,6,5,8], threshold = 7
输出:1
解释:[8] 满足 8 > 7/1 = 7;返回任意合法长度均可。
约束:
-
1 <= nums.length <= 10⁵ -
1 <= nums[i], threshold <= 10⁹
解题思路
核心思想 :单调栈定最小值管辖区间。对每个下标 i:用单调栈求 left[i](左侧第一个 < nums[i] 的下标)与 right[i](右侧第一个 <= nums[i] 的下标,用 <= 防止等值重复计算),则 nums[i] 作为最小值的最大区间长度为 len = right[i] - left[i] - 1。条件 min > threshold / len 等价于 nums[i] * len > threshold,成立即可返回 len。
复杂度:O(n) 时间、O(n) 空间。
代码实现
java
/**
* LeetCode 2334. Subarray With Elements Greater Than Varying Threshold
* (元素值大于变化阈值的子数组)
*
* 题目描述:给你一个整数数组 nums 和一个整数 threshold。找到长度为 k 的 nums 子数组,
* 满足数组中每个元素都大于 threshold / k。返回满足要求的任意子数组的大小;若没有,返回 -1。
*
* 思路:枚举区间最小值的作用范围(单调栈经典用法)。
* 对每个下标 i,用单调栈求出:
* - left[i] = 左侧第一个 < nums[i] 的下标(则 (left[i], i] 内 nums[i] 是最小值之一);
* - right[i] = 右侧第一个 <= nums[i] 的下标(为避免重复取等值,用 <= 划定右界)。
* 于是以 nums[i] 为最小值的最大子数组长度为 len = right[i] - left[i] - 1,
* 若 num[i] * len > threshold(等价于 min > threshold / len),则该子数组合法,
* 记录 len 并取最大(任意合法长度均可,题目允许返回任意合法子数组大小)。
*
* 复杂度:时间 O(n)(两遍单调栈),空间 O(n)。
*/
import java.util.ArrayDeque;
import java.util.Deque;
class Solution {
public int validSubarraySize(int[] nums, int threshold) {
int n = nums.length;
int[] left = new int[n];
int[] right = new int[n];
Deque<Integer> stack = new ArrayDeque<>();
// 左侧第一个小于 nums[i] 的下标
for (int i = 0; i < n; i++) {
while (!stack.isEmpty() && nums[stack.peek()] >= nums[i]) {
stack.pop();
}
left[i] = stack.isEmpty() ? -1 : stack.peek();
stack.push(i);
}
stack.clear();
// 右侧第一个 <= nums[i] 的下标(用 <= 去重,避免相等元素重复计算)
for (int i = n - 1; i >= 0; i--) {
while (!stack.isEmpty() && nums[stack.peek()] > nums[i]) {
stack.pop();
}
right[i] = stack.isEmpty() ? n : stack.peek();
stack.push(i);
}
for (int i = 0; i < n; i++) {
int len = right[i] - left[i] - 1;
// 每个元素都 > threshold / len ⟺ nums[i] * len > threshold
if (nums[i] > threshold / (double) len) {
return len;
}
}
return -1;
}
// 本地验证
public static void main(String[] args) {
Solution sol = new Solution();
// 示例 1:预期 3
System.out.println(sol.validSubarraySize(new int[]{1, 3, 4, 3, 1}, 6));
// 示例 2:预期 1([8])
System.out.println(sol.validSubarraySize(new int[]{6, 5, 6, 5, 8}, 7));
// 自测:无合法子数组 → -1
System.out.println(sol.validSubarraySize(new int[]{1, 1}, 3));
}
}
复杂度分析
- 解法:单调栈定区间 + 判定(主提交版本)
- 时间复杂度:O(n)
- 空间复杂度:O(n)
2335. Minimum Amount of Time to Fill Cups(装满杯子需要的最短总时长)
题目描述
一台饮水机可制备冷水、温水、热水。每秒钟可以装满 2 杯不同类型 的水或 1 杯任意类型的水。amount[0]、amount[1]、amount[2] 分别表示需要装满冷水、温水、热水的杯子数量。返回装满所有杯子所需的最少秒数。
示例 1:
输入:amount = [1,4,2]
输出:4
示例 2:
输入:amount = [5,4,4]
输出:7
示例 3:
输入:amount = [5,0,0]
输出:5
约束:
-
amount.length == 3 -
0 <= amount[i] <= 100
解题思路
核心思想 :贪心分类讨论。记总量 s = sum(amount)、最大值 mx = max(amount):若 2 × mx <= s,说明最大项不超过其余两项之和,可全程两杯两杯装,秒数 = ⌈s/2⌉;否则先把其余数量以"最大值 + 另一项"配对消耗掉,剩余 mx - (s - mx) 杯只能单独装,秒数 = mx。合并为 ans = max(mx, (s + 1) / 2)。
复杂度:O(1) 时间、O(1) 空间。
代码实现
java
/**
* LeetCode 2335. Minimum Amount of Time to Fill Cups(装满杯子需要的最短总时长)
*
* 题目描述:一台饮水机可制备冷水、温水、热水。每秒钟可装满 2 杯不同类型的水或 1 杯
* 任意类型的水。给定长度 3 的数组 amount,amount[0]、amount[1]、amount[2] 分别表示需要
* 装满冷水、温水、热水的杯子数量。返回装满所有杯子所需的最少秒数。
*
* 思路:贪心。每秒钟优先装满数量最多的两类水(各一杯),最后一杯单独装。
* 记总量 s = sum(amount),最大值 mx = max(amount):
* - 若 mx * 2 <= s,说明最大项不超过其他两项之和,可以一直两个两个地装,秒数 = ceil(s/2);
* - 否则,前 s - mx 秒每轮消耗最大值与另一项各 1,剩余 mx - (s - mx) 杯只能单独装,
* 秒数 = mx。
* 合并两式:ans = max(mx, ceil(s/2))。
*
* 复杂度:时间 O(1),空间 O(1)。
*/
import java.util.Arrays;
class Solution {
public int fillCups(int[] amount) {
int sum = amount[0] + amount[1] + amount[2];
int mx = Math.max(amount[0], Math.max(amount[1], amount[2]));
return Math.max(mx, (sum + 1) / 2);
}
// 本地验证
public static void main(String[] args) {
Solution sol = new Solution();
// 示例 1:预期 4
System.out.println(sol.fillCups(new int[]{1, 4, 2}));
// 示例 2:预期 7
System.out.println(sol.fillCups(new int[]{5, 4, 4}));
// 示例 3:预期 5
System.out.println(sol.fillCups(new int[]{5, 0, 0}));
}
}
复杂度分析
- 解法:贪心分类讨论(主提交版本)
- 时间复杂度:O(1)
- 空间复杂度:O(1)
2336. Smallest Number in Infinite Set(无限集中的最小数字)
题目描述
现有一个包含所有正整数的集合 [1, 2, 3, 4, 5, ...]。实现 SmallestInfiniteSet 类:SmallestInfiniteSet() 初始化对象以包含所有 正整数;int popSmallest() 移除并返回该无限集中的最小整数;void addBack(int num) 如果正整数 num 不 存在于无限集中,则将其添加到该集合中。
示例 1:
输入
["SmallestInfiniteSet", "addBack", "popSmallest", "popSmallest", "popSmallest", "addBack", "popSmallest", "popSmallest", "popSmallest"]
[[], [2], [], [], [], [1], [], [], []]
输出
[null, null, 1, 2, 3, null, 1, 4, 5]
解释:addBack(2) 时 2 尚在集合中不生效;依次弹出 1、2、3;addBack(1) 后最小数回到 1,随后弹出 1、4、5。
约束:
-
1 <= num <= 1000 -
最多调用
popSmallest和addBack方法共计1000次
解题思路
核心思想 :游标 + 最小堆 + 哈希去重。cur 记录尚未弹出的下一个正整数;小根堆存 addBack 回来的数(必然 < cur),Set 去重。popSmallest():堆非空且堆顶 < cur 则弹堆顶,否则返回 cur++。addBack(num):num < cur 且不在堆中才入堆(>= cur 表示本就在集合中无需处理)。
复杂度:单次操作 O(log P) 时间、O§ 空间(P 为复活数个数)。
代码实现
java
/**
* LeetCode 2336. Smallest Number in Infinite Set(无限集中的最小数字)
*
* 题目描述:现有一个包含所有正整数的集合 [1, 2, 3, 4, 5, ...]。实现 SmallestInfiniteSet 类:
* - SmallestInfiniteSet() 初始化对象以包含所有正整数;
* - int popSmallest() 移除并返回该无限集中的最小整数;
* - void addBack(int num) 如果正整数 num 不存在于无限集中,则将其添加进去。
*
* 思路:维护"最小未弹出游标 cur" + 一个最小堆(保存被 addBack 回来的数)+ 哈希集合去重。
* - popSmallest():若堆非空且堆顶 < cur,弹出堆顶;否则返回 cur++。
* (堆中元素只可能是 < cur 的复活数;若 >= cur 说明原本就还在集合中,无需处理)
* - addBack(num):num >= cur 表示它本就在集合中,直接忽略;否则若未在堆中则入堆。
*
* 复杂度:单次操作 O(log P)(P 为复活数个数),空间 O(P)。
*/
import java.util.HashSet;
import java.util.PriorityQueue;
import java.util.Set;
class SmallestInfiniteSet {
private int cur = 1; // 下一个"原装"最小数
private PriorityQueue<Integer> heap; // 被 addBack 回来的数(均 < cur)
private Set<Integer> inHeap; // 去重
public SmallestInfiniteSet() {
heap = new PriorityQueue<>();
inHeap = new HashSet<>();
}
public int popSmallest() {
if (!heap.isEmpty() && heap.peek() < cur) {
int v = heap.poll();
inHeap.remove(v);
return v;
}
return cur++;
}
public void addBack(int num) {
if (num < cur && inHeap.add(num)) {
heap.offer(num);
}
}
// 本地验证
public static void main(String[] args) {
// 示例:预期 [null, null, 1, 2, 3, null, 1, 4, 5]
SmallestInfiniteSet s = new SmallestInfiniteSet();
s.addBack(2); // 2 本就在集合中,不做变更
System.out.println(s.popSmallest()); // 1
System.out.println(s.popSmallest()); // 2
System.out.println(s.popSmallest()); // 3
s.addBack(1); // 添加 1 回集合
System.out.println(s.popSmallest()); // 1
System.out.println(s.popSmallest()); // 4
System.out.println(s.popSmallest()); // 5
}
}
复杂度分析
- 解法:游标 + 最小堆 + 哈希去重(主提交版本)
- 时间复杂度:O(log P)
- 空间复杂度:O§
2337. Move Pieces to Obtain a String(移动片段得到字符串)
题目描述
给你两个字符串 start 和 target,长度均为 n,仅由字符 'L'、'R' 和 '_' 组成。'L' 只在其左侧紧邻空位时才能向左 移动,'R' 只在其右侧紧邻空位时才能向右 移动,'_' 表示可被占据的空位。判断在移动 start 中的片段任意次之后能否得到 target。
示例 1:
输入:start = "_L__R__R_", target = "L______RR"
输出:true
示例 2:
输入:start = "R_L_", target = "__LR"
输出:false
示例 3:
输入:start = "_R", target = "R_"
输出:false
约束:
-
n == start.length == target.length -
1 <= n <= 10⁵ -
start和target由字符'L'、'R'和'_'组成
解题思路
核心思想 :双指针按序比对片段。两字符串忽略 '_' 后,L/R 的相对顺序必须完全一致;且每个 'L' 在 target 中位置必须 <= 在 start 中的位置(只能左移),每个 'R' 必须 >=(只能右移)。两指针各自跳过 '_' 逐位验证即可。
复杂度:O(n) 时间、O(1) 空间。
代码实现
java
/**
* LeetCode 2337. Move Pieces to Obtain a String(移动片段得到字符串)
*
* 题目描述:给你两个字符串 start 和 target,长度均为 n,仅由字符 'L'、'R' 和 '_' 组成。
* 'L' 片段只有在其左侧紧邻空位(_)时才能向左移动,'R' 片段只有在其右侧紧邻空位时才能
* 向右移动。判断在移动 start 中的片段任意次之后能否得到 target。
*
* 思路:两个字符串忽略 '_' 后,L/R 的相对顺序必须完全一致;
* 且对于每个片段:
* - 'L' 只能左移,所以在 target 中的位置必须 <= 在 start 中的位置;
* - 'R' 只能右移,所以在 target 中的位置必须 >= 在 start 中的位置。
* 双指针同时扫描 start 与 target(跳过 '_'),逐位验证即可。
*
* 复杂度:时间 O(n),空间 O(1)。
*/
class Solution {
public boolean canChange(String start, String target) {
int n = start.length();
int i = 0, j = 0;
while (i < n || j < n) {
while (i < n && start.charAt(i) == '_') {
i++;
}
while (j < n && target.charAt(j) == '_') {
j++;
}
if (i == n || j == n) {
return i == n && j == n; // 必须同时结束
}
char c1 = start.charAt(i);
char c2 = target.charAt(j);
if (c1 != c2) {
return false; // L/R 相对顺序不一致
}
if (c1 == 'L' && i < j) {
return false; // L 只能向左
}
if (c1 == 'R' && i > j) {
return false; // R 只能向右
}
i++;
j++;
}
return true;
}
// 本地验证
public static void main(String[] args) {
Solution sol = new Solution();
// 示例 1:预期 true
System.out.println(sol.canChange("_L__R__R_", "L______RR"));
// 示例 2:预期 false
System.out.println(sol.canChange("R_L_", "__LR"));
// 示例 3:预期 false
System.out.println(sol.canChange("_R", "R_"));
}
}
复杂度分析
- 解法:双指针按序比对(主提交版本)
- 时间复杂度:O(n)
- 空间复杂度:O(1)
2338. Count the Number of Ideal Arrays(统计理想数组的数目)
题目描述
给定整数 n 与 maxValue。长度为 n 的数组 arr 是理想数组 当且仅当:每个 arr[i] ∈ [1, maxValue],且每个 arr[i] 都能被 arr[i-1] 整除(0 < i < n)。返回长度为 n 的不同 理想数组的数目(对 10⁹ + 7 取余)。
示例 1:
输入:n = 2, maxValue = 5
输出:10
解释:以 1 开头 5 个、以 2 开头 2 个([2,2]、[2,4])、以 3/4/5 开头各 1 个,共 10。
示例 2:
输入:n = 5, maxValue = 3
输出:11
约束:
-
2 <= n <= 10⁴ -
1 <= maxValue <= 10⁴
解题思路
核心思想 :倍数链 + 组合计数。数组中不同值组成一条"严格递增的倍数链",链长最多 log2(maxValue)+1 ≈ 14(每换一个不同值至少乘以 2)。ways[val][L] 表示以 val 结尾、含 L 个不同值的链数,由 val 的真因子链转移得到(O(√val) 枚举因子)。把长度 n 的位置分配给链上的 L 个不同值(保序可重复)的方案数为 C(n-1, L-1),答案 = Σ_val Σ_L ways[val][L] × C(n-1, L-1),组合数用杨辉三角预处理。
复杂度:O(maxValue × √maxValue + n × log maxValue) 时间、O(maxValue × log maxValue) 空间。
代码实现
java
/**
* LeetCode 2338. Count the Number of Ideal Arrays(统计理想数组的数目)
*
* 题目描述:给定整数 n 与 maxValue。长度为 n 的整数数组 arr 是理想数组当且仅当:
* 每个 arr[i] ∈ [1, maxValue],且每个 arr[i] 都能被 arr[i-1] 整除(0 < i < n)。
* 返回长度为 n 的不同理想数组数目(对 10⁹+7 取余)。
*
* 思路:任何理想数组中,不同值出现的次数构成"链状倍数结构"。
* 由于每个位置的值必须被前一个整除,数组中不同的值组成一条严格递增的倍数链
* (除去重复值)。关键观察:链中不同值的个数最多为 log2(maxValue)+1 ≈ 14,
* 因为每换一个不同的值至少乘以 2。
* 记 cnt[i] = i 的倍数链方案(不含 1;1 与任何数都整除)。具体做法:
* - dp[x]:以 x 结尾的、由不同值组成的倍数链在长度为 len 时的划分方案数不好直接做;
* 这里用更简洁的组合计数:对每个"末位值" val,枚举链长 L(不同值的个数),
* cnt2[val][L] = 从 1..maxValue 中选 L 个不同值构成一条链且末尾恰为 val 的链形态数,
* 可由递归/DP 按"上一个值是 val 的真因子"传递。然后长度为 n 的数组把 n 个位置分配
* 给链上的 L 个不同值(保持相对顺序,允许重复),方案数 = C(n-1, L-1)。
* 总答案 = Σ_val Σ_L cnt2[val][L] * C(n-1, L-1)(加上全 1 数组:L=1 → C(n-1,0)=1,已包含)。
*
* 复杂度:时间 O(maxValue * log(maxValue) * log(n) ),空间 O(maxValue * log(maxValue))。
*/
class Solution {
private static final int MOD = 1_000_000_007;
public int idealArrays(int n, int maxValue) {
int maxLen = 0;
int t = maxValue;
while (t > 0) {
maxLen++;
t >>= 1;
}
// comb[n-1][L-1] 组合数(杨辉三角)
long[][] comb = new long[n][maxLen + 1];
for (int i = 0; i < n; i++) {
comb[i][0] = 1;
for (int j = 1; j <= Math.min(i, maxLen - 1); j++) {
comb[i][j] = (comb[i - 1][j - 1] + (j <= i - 1 ? comb[i - 1][j] : 0)) % MOD;
}
}
// ways[val][L] = 以 val 结尾的、含 L 个不同值的链数
long[][] ways = new long[maxValue + 1][maxLen + 1];
long[] sumLen = new long[maxLen + 1]; // sumLen[L] = Σ_val ways[val][L]
for (int val = 1; val <= maxValue; val++) {
ways[val][1] = 1; // 单值链
sumLen[1] = (sumLen[1] + 1) % MOD;
// 由因子转移:枚举 val 的真因子的链,长度 +1
for (int d = 1; d * d <= val; d++) {
if (val % d == 0) {
if (d != val) {
addFrom(ways, sumLen, val, d, maxLen);
}
int d2 = val / d;
if (d2 != val && d2 != d) {
addFrom(ways, sumLen, val, d2, maxLen);
}
}
}
}
long ans = 0;
for (int L = 1; L <= maxLen; L++) {
// 把 n 个位置分配给 L 个不同值:C(n-1, L-1)
ans = (ans + sumLen[L] * comb[n - 1][L - 1]) % MOD;
}
return (int) ans;
}
private void addFrom(long[][] ways, long[] sumLen, int val, int d, int maxLen) {
for (int L = 1; L < maxLen; L++) {
if (ways[d][L] > 0) {
ways[val][L + 1] = (ways[val][L + 1] + ways[d][L]) % MOD;
sumLen[L + 1] = (sumLen[L + 1] + ways[d][L]) % MOD;
}
}
}
// 本地验证
public static void main(String[] args) {
Solution sol = new Solution();
// 示例 1:预期 10
System.out.println(sol.idealArrays(2, 5));
// 示例 2:预期 11
System.out.println(sol.idealArrays(5, 3));
}
}
复杂度分析
- 解法:倍数链 DP + 组合计数(主提交版本)
- 时间复杂度:O(maxValue × √maxValue + n × log maxValue)
- 空间复杂度:O(maxValue × log maxValue)
2339. All the Matches of the League(联赛的所有比赛 🔒)
题目类型:数据库(SQL)
题目描述
表 Teams:team_name(唯一)表示所有球队名称。每两支球队进行两场 比赛,一场以其中一支为主队 home_team、另一场以另一支为主队 away_team。编写解决方案,按任意顺序返回联赛的所有比赛。
示例 1:
输入:
Teams 表:
| team_name |
| Leetcode FC |
| Ahly SC |
| Real Madrid |
输出:
| home_team | away_team |
| Real Madrid | Leetcode FC |
| Real Madrid | Ahly SC |
| Leetcode FC | Real Madrid |
| Leetcode FC | Ahly SC |
| Ahly SC | Real Madrid |
| Ahly SC | Leetcode FC |
解释:每两支球队各自互为一次主客场的比赛全部列出。
约束:
-
1 <= Teams.length <= 10⁵ -
team_name唯一
解题思路
核心思想 :自连接交叉生成主客对阵。把 Teams 表自我连接两次(a 为主队、b 为客队),约束 a.team_name <> b.team_name,即得到每两支球队两个方向的主客场比赛。
SQL 要点 :表自身 JOIN(别名区分);不等值连接条件。
代码实现
sql
-- LeetCode 2339. All the Matches of the League(联赛的所有比赛)
-- 题目描述:Teams 表(team_name 唯一)给出所有球队名称。每两支球队进行两场比赛,
-- 一场以其中一支为主队(home_team)、另一支以另一支为主队(away_team)。
-- 按任意顺序返回所有比赛的 (home_team, away_team)。
--
-- 思路:交叉连接生成有序对------把 Teams 表自身 JOIN 两次,约束主队名 != 客队名,
-- 即为联赛的全部主客场对阵。
SELECT a.team_name AS home_team, b.team_name AS away_team
FROM Teams a
JOIN Teams b ON a.team_name <> b.team_name;
复杂度分析
- 解法:表自连接交叉生成(主提交版本)
- 时间复杂度:O(n²)
- 空间复杂度:O(n²)
2340. Minimum Adjacent Swaps to Make a Valid Array(生成有效数组的最少交换次数 🔒)
题目描述
给定一个下标从 0 开始的整数数组 nums。nums 上的相邻 元素可以进行交换。一个有效 数组必须满足:最大的元素(若多个则为任意一个)位于数组最右端 ;最小的元素(若多个则为任意一个)位于数组最左侧 。返回使 nums 成为有效数组所需的最少交换次数。
示例 1:
输入: nums = [3,4,5,5,3,1]
输出: 6
解释:将最左最小值 1(下标 5)移到最左需 5 次、最右最大值 5(下标 2)移到最右需 1 次,路径交叉减去 1 次,共 6 次。
示例 2:
输入: nums = [9]
输出: 0
约束:
-
1 <= nums.length <= 10⁵ -
1 <= nums[i] <= 10⁵
解题思路
核心思想 :贪心定位 + 计数。相邻交换的最小次数 = "最左最小值下标 l"移到最左所需的 l 次 + "最右最大值下标 r"移到最右所需的 n - 1 - r 次(重复值取最左/最右可最小化步数)。若 l > r(最小在最大右侧),两者移动路径交叉,会重复计 1 次交换,需减 1。
复杂度:O(n) 时间、O(1) 空间。
代码实现
java
/**
* LeetCode 2340. Minimum Adjacent Swaps to Make a Valid Array(生成有效数组的最少交换次数)
*
* 题目描述:给定一个下标从 0 开始的整数数组 nums。nums 上的相邻元素可以进行交换。
* 一个有效数组满足:最大的元素(若多个则任意一个)位于数组最右端;最小的元素(若多个
* 则任意一个)位于数组最左端。返回使 nums 成为有效数组所需的最少相邻交换次数。
*
* 思路:相邻交换等价于"冒泡移动"。要最小化次数:
* - 取"最左侧的最小值"下标 l(若有多个最小值取最左,这样移到左端步数最少);
* - 取"最右侧的最大值"下标 r(若有多个最大值取最右,移到右端步数最少)。
* 把最小值移到最左需要 l 次交换,把最大值移到最右需要 (n-1-r) 次交换。
* 若 l > r(最小值在最大值的右侧),两者路径交叉,会在某一次交换中彼此多走一步
* 再互相抵消,实际总次数要减 1。
*
* 复杂度:时间 O(n),空间 O(1)。
*/
class Solution {
public int minimumSwaps(int[] nums) {
int n = nums.length;
int l = 0; // 最左侧最小值下标
int r = 0; // 最右侧最大值下标
for (int i = 0; i < n; i++) {
if (nums[i] < nums[l]) {
l = i;
}
if (nums[i] >= nums[r]) {
r = i; // 取最右侧的最大值
}
}
if (l == r) {
return 0;
}
int swaps = l + (n - 1 - r);
if (l > r) {
swaps--; // 移动路径交叉,有一次交换同时推进两个目标
}
return swaps;
}
// 本地验证
public static void main(String[] args) {
Solution sol = new Solution();
// 示例 1:预期 6
System.out.println(sol.minimumSwaps(new int[]{3, 4, 5, 5, 3, 1}));
// 示例 2:预期 0
System.out.println(sol.minimumSwaps(new int[]{9}));
}
}
复杂度分析
- 解法:贪心定位 + 计数(主提交版本)
- 时间复杂度:O(n)
- 空间复杂度:O(1)