LeetCode2321~2340题目解析

LeetCode 2321-2340. 题解合集

文章目录


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)

相关推荐
驭渊的小故事39 分钟前
Spring AOP 核心概念与通知类型详解
java·后端·spring
vipxieliang40 分钟前
ValidX vs 手写验证代码:开发效率对比实测
java·spring boot·后端
vipxieliang1 小时前
虚拟线程与 GraalVM:Spring Boot 3.x 云原生性能跃迁
java·spring boot·后端
Rain的Java大神之路1 小时前
面试官最烦的答案:“我用 Redis + Token”!接口幂等 8 种方案 + 四层防御,把重复扣款工单干到 0
java·后端·面试
Rainmicro2 小时前
【无标题】
java·数据库·算法
驭渊的小故事2 小时前
Spring AOP 入门:从 AOP 概念到第一个耗时统计切面
java·后端·spring
天天被压力2 小时前
【A股分钟级高频数据完全手册 #09】A股历史分时交易数据接口:普通分时怎么批量下
java·人工智能·python
天天被压力2 小时前
【A股分钟级高频数据完全手册 #10】股票实时行情数据接口Python:公开源实时交易取数
java·人工智能·python
apcipot_rain3 小时前
C++转Java日记(05)
java·开发语言·c++