LeetCode2241~2260题目解析

LeetCode 2241-2260. 题解合集

文章目录


2241. Design an ATM Machine(设计一个 ATM 机器)

题目描述

一个 ATM 机器,存有 5 种面值的钞票:20、50、100、200 和 500 美元。初始时,ATM 机是空的。用户可以用它存或者取任意数目的钱。取款时,机器会优先取较大 数额的钱。请你实现 ATM 类:ATM() 初始化 ATM 对象;void deposit(int[] banknotesCount) 分别存入 $20、$50、$100、$200 和 $500 钞票的数目;int[] withdraw(int amount) 返回一个长度为 5 的数组,分别表示 $20、$50、$100、$200 和 $500 钞票的数目,并且更新 ATM 机里取款后钞票的剩余数量;如果无法取出指定数额的钱,返回 [-1](这种情况下不取出任何钞票)。

示例 1:

复制代码
输入:["ATM", "deposit", "withdraw", "deposit", "withdraw", "withdraw"]
参数:[[], [[0,0,1,2,1]], [600], [[0,1,0,1,1]], [600], [550]]
输出:[null, null, [0,0,1,0,1], null, [-1], [0,1,0,0,1]]
解释:先存 1 张 $100、2 张 $200 和 1 张 $500;取 600 返回 1 张 $100 和 1 张 $500;再存 1 张 $50、1 张 $200 和 1 张 $500 后取 600 被拒绝;最后取 550 返回 1 张 $50 和 1 张 $500。

约束:

  • banknotesCount.length == 5

  • 0 <= banknotesCount[i] <= 10⁹

  • 1 <= amount <= 10⁹

  • withdraw 调用总次数不超过 10⁵

  • deposit 与 withdraw 调用总次数不超过 10⁵

解题思路

核心思想 :贪心 + 存量数组。面值固定为 {20, 50, 100, 200, 500},取款时从大面值到小面值尽量多取,若最后剩余金额为 0 说明可精确凑出,才真正扣减存量。

步骤:

  1. 用 long[] cnt 记录五种面值钞票的张数(张数可达 10⁹,累加可能溢出 int,故用 long)。
  2. deposit 直接累加对应面值张数。
  3. withdraw(amount):从面值 500 开始,每张面值最多取 min(cnt[i], amount / vals[i]) 张,amount 同步减少。
  4. 若 amount != 0 说明凑不出,返回 [-1] 且不动存量;否则扣减存量并返回五张数的数组。

注意 :贪心在大面值优先下可能"看似能取实则取不了"(如示例中的 $600),这正是题目要求的行为------优先取较大数额、取不出就整单拒绝。

代码实现

java 复制代码
import java.util.Arrays;

/**
 * 2241. Design an ATM Machine 设计一个 ATM 机器
 *
 * 一个 ATM 机器,存有 5 种面值的钞票:20 、50 、100 、200 和 500 美元。初始时 ATM 机是空的,
 * 用户可以用它存或者取任意数目的钱。取款时机器会优先取较大数额的钱。实现 ATM 类:
 * - ATM() 初始化 ATM 对象。
 * - void deposit(int[] banknotesCount) 分别存入 20、50、100、200 和 500 美元钞票的数目。
 * - int[] withdraw(int amount) 返回一个长度为 5 的数组,分别表示各面值取出的钞票数目,并更新 ATM 机
 *   里取款后钞票的剩余数量。如果无法取出指定数额的钱,返回 [-1](此时不取出任何钞票)。
 *
 * 算法思路:用长度为 5 的数组 cnt 记录每种面值钞票的存量,面值数组 vals = {20,50,100,200,500}。
 * 取款时从大面值到小面值贪心:每张面值最多取 min(存量, amount / 面值) 张,累加到答案并扣减 amount;
 * 若最后 amount == 0 说明可精确凑出,才真正从存量中扣减;否则返回 [-1] 且不修改存量。
 *
 * 时间复杂度:deposit O(1),withdraw O(1);空间复杂度:O(1)
 */
public class Solution {

    private static final int[] VALS = {20, 50, 100, 200, 500};
    private long[] cnt = new long[5];

    /** 分别存入各类面值的钞票数目 */
    public void deposit(int[] banknotesCount) {
        for (int i = 0; i < 5; i++) {
            cnt[i] += banknotesCount[i];
        }
    }

    /** 取款 amount,返回各面值取出的张数;无法凑出时返回 [-1] */
    public int[] withdraw(int amount) {
        long[] take = new long[5];
        // 从大面值到小面值贪心
        for (int i = 4; i >= 0; i--) {
            long need = Math.min(cnt[i], amount / VALS[i]);
            amount -= need * VALS[i];
            take[i] = need;
        }
        if (amount != 0) {
            return new int[]{-1};
        }
        // 凑出成功才真正扣减存量
        for (int i = 0; i < 5; i++) {
            cnt[i] -= take[i];
        }
        return new int[]{(int) take[0], (int) take[1], (int) take[2], (int) take[3], (int) take[4]};
    }

    public static void main(String[] args) {
        Solution at = new Solution();
        at.deposit(new int[]{0, 0, 1, 2, 1});
        // 预期 [0,0,1,0,1]
        System.out.println(Arrays.toString(at.withdraw(600)));
        at.deposit(new int[]{0, 1, 0, 1, 1});
        // 预期 [-1]
        System.out.println(Arrays.toString(at.withdraw(600)));
        // 预期 [0,1,0,0,1]
        System.out.println(Arrays.toString(at.withdraw(550)));

        Solution at2 = new Solution();
        at2.deposit(new int[]{0, 1, 0, 1, 1});
        // 预期 [0,0,0,0,1]:500 面值
        System.out.println(Arrays.toString(at2.withdraw(550)));
        // 预期 [-1]
        System.out.println(Arrays.toString(at2.withdraw(600)));
    }
}

复杂度分析

  • 解法:贪心取款(主提交版本)
  • 时间复杂度:O(1) --- 固定 5 种面值
  • 空间复杂度:O(1) --- 常量尺寸数组

2242. Maximum Score of a Node Sequence(节点序列的最大得分)

题目描述

给你一个 n 个节点的无向图 ,节点编号为 0 到 n - 1。给你一个下标从 0 开始的整数数组 scores,其中 scores[i] 是第 i 个节点的分数。同时给你一个二维整数数组 edges,其中 edges[i] = [ai, bi] 表示节点 ai 和 bi 之间有一条无向 边。一个合法的节点序列满足:序列中每相邻 节点之间有边相连,且序列中没有节点出现超过一次。节点序列的分数定义为序列中节点分数之和 。返回一个长度为 4 的合法节点序列的最大分数,如果不存在这样的序列返回 -1。

示例 1:

复制代码
输入:scores = [5,2,9,8,4], edges = [[0,1],[1,2],[2,3],[0,2],[1,3],[2,4]]
输出:24
解释:节点序列 [0,1,2,3] 的分数为 5 + 2 + 9 + 8 = 24,可证无更高。

示例 2:

复制代码
输入:scores = [9,20,6,4,11,12], edges = [[0,3],[5,3],[2,4],[1,3]]
输出:-1
解释:不存在长度为 4 的合法序列。

约束:

  • n == scores.length

  • 4 <= n <= 10⁵

  • 1 <= scores[i] <= 10⁶

  • 1 <= edges.length <= 10⁵

  • edges[i].length == 2

  • 0 <= ai, bi <= n - 1

  • ai != bi

  • 无重边

解题思路

核心思想 :枚举中边 + 邻居 Top3。任意长度为 4 的合法序列形如 a - u - v - b,其中 (u, v) 是一条边。固定这条中边后,a 应从 u 的邻居里选分数最大者,b 应从 v 的邻居里选分数最大者,同时保证四个节点互不相同。

步骤:

  1. 建邻接表,邻居存为 (节点, 分数)。
  2. 每个节点的邻居按分数降序 排序,每个节点只保留前 3 个 邻居------因为需要考虑的排除项最多 2 个(v 与另一个端点),Top3 足够包含最优选择。
  3. 枚举每条边 (u, v),双重遍历 u 的 Top3 邻居 a 与 v 的 Top3 邻居 b,跳过 b == u、a == v、a == b 的冲突组合,更新 scores[a] + scores[u] + scores[v] + scores[b] 的最大值。

复杂度:排序 O(n log n)(按度),枚举 O(|E| × 9),总体 O(n log n + |E|)。

代码实现

java 复制代码
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * 2242. Maximum Score of a Node Sequence 节点序列的最大得分
 *
 * 给你一个 n 个节点的无向图,节点编号为 0 到 n - 1。给你一个下标从 0 开始的整数数组 scores,
 * 其中 scores[i] 是第 i 个节点的分数。同时给你一个二维整数数组 edges,其中 edges[i] = [ai, bi]
 * 表示节点 ai 和 bi 之间有一条无向边。一个节点序列满足:序列中每相邻节点之间有边相连,且序列中
 * 没有节点出现超过一次。节点序列的分数定义为序列中节点分数之和。返回一个长度为 4 的合法节点序列
 * 的最大分数,如果不存在这样的序列返回 -1。
 *
 * 算法思路:枚举序列中间的一条边 (u, v),序列形式为 a - u - v - b。为让分数最大,a 取 u 的邻居中
 * 分数最大的且不是 v 的节点,b 取 v 的邻居中分数最大的且不是 u 的节点,同时要保证 a != b。
 * 对每个节点只保留分数最大的 3 个邻居(因为只需考虑最多两个"排除项",保留 top3 足够)。
 *
 * 时间复杂度:O(n + |E| * 3^2) ≈ O(n + |E|),空间复杂度:O(n + |E|)
 */
public class Solution {

    public int maximumScore(int[] scores, int[][] edges) {
        int n = scores.length;
        List<int[]>[] graph = new List[n];
        for (int i = 0; i < n; i++) {
            graph[i] = new ArrayList<>();
        }
        for (int[] e : edges) {
            int a = e[0], b = e[1];
            graph[a].add(new int[]{b, scores[b]});
            graph[b].add(new int[]{a, scores[a]});
        }
        // 每个节点的邻居按分数降序排序,之后每个节点最多使用前 3 个邻居
        for (int i = 0; i < n; i++) {
            graph[i].sort((x, y) -> y[1] - x[1]);
            if (graph[i].size() > 3) {
                graph[i] = new ArrayList<>(graph[i].subList(0, 3));
            }
        }
        int ans = -1;
        int m = edges.length;
        // 枚举中边 (u, v),端点分列两侧
        for (int[] e : edges) {
            int u = e[0], v = e[1];
            // 尝试 u 选一个邻居 a,v 选一个邻居 b,四个节点两两不同且各不相同
            for (int[] pa : graph[u]) {
                int a = pa[0];
                if (a == v) continue;
                for (int[] pb : graph[v]) {
                    int b = pb[0];
                    if (b == u || b == a) continue;
                    ans = Math.max(ans, scores[a] + scores[u] + scores[v] + scores[b]);
                }
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        // 示例 1:预期 24
        System.out.println(s.maximumScore(new int[]{5, 2, 9, 8, 4},
                new int[][]{{0, 1}, {1, 2}, {2, 3}, {0, 2}, {1, 3}, {2, 4}}));
        // 示例 2:预期 -1
        System.out.println(s.maximumScore(new int[]{9, 20, 6, 4, 11, 12},
                new int[][]{{0, 3}, {5, 3}, {2, 4}, {1, 3}}));
        // 自测:两个节点一条边,长度为 4 序列不存在,预期 -1
        System.out.println(s.maximumScore(new int[]{1, 2}, new int[][]{{0, 1}}));
    }
}

复杂度分析

  • 解法:枚举中边 + 邻居 Top3(主提交版本)
  • 时间复杂度:O(n log n + |E|) --- 排序 + 枚举
  • 空间复杂度:O(n + |E|) --- 邻接表

2243. Calculate Digit Sum of a String(计算字符串的数字和)

题目描述

给你一个由若干数字(0 - 9)组成的字符串 s 和一个整数 k。如果 s 的长度大于 k,则可以执行一轮操作:将 s 拆分 成长度为 k 的若干连续数字组 (最后一个数字组的长度可以小于 k),用表示每个数字组中所有数字之和的字符串来替换 对应的数字组(例如 "346" 会替换为 "13",因为 3 + 4 + 6 = 13),然后合并 所有组以形成一个新字符串。如果新字符串的长度大于 k 则重复。返回在完成所有轮操作后的 s。

示例 1:

复制代码
输入:s = "11111222223", k = 3
输出:"135"
解释:第一轮 "111"、"112"、"222"、"23" → "3465";第二轮 "346"、"5" → "135"。

示例 2:

复制代码
输入:s = "00000000", k = 3
输出:"000"
解释:每组分组的数字和都为 0,最终合并为 "000"。

约束:

  • 1 <= s.length <= 100

  • 2 <= k <= 100

  • s[i] 为数字 0 - 9

解题思路

核心思想 :按题意循环模拟。每轮把 s 按每 k 个字符一组,求组内数字之和(可能超过 9,转成多位数串),拼接成新串,直到长度 <= k 为止。

步骤:

  1. while (s.length() > k) 循环。
  2. 内层循环以 i += k 步进,取出 [i, min(i+k, len)) 一段求数字和,追加到 StringBuilder。
  3. 以拼接结果替换 s。

复杂度:每轮 O(len),轮数随长度快速收缩(每位数字和 ≤ 9k,长度大致减半),总体远小于 O(n²),最坏 O(n²) 也满足 n ≤ 100。

代码实现

java 复制代码
/**
 * 2243. Calculate Digit Sum of a String 计算字符串的数字和
 *
 * 给你一个由若干数字(0 - 9)组成的字符串 s 和一个整数 k。如果 s 的长度大于 k,则可以执行一轮
 * 操作:把 s 拆分成若干长度为 k 的连续数字组(最后一组长度可以小于 k),用表示每个数字组中所有
 * 数字之和的字符串替换对应的数字组,然后合并所有组形成新字符串。重复直到 s 的长度 <= k,返回
 * 完成所有轮操作后的 s。
 *
 * 算法思路:按题意循环模拟。每次把 s 按 k 切成组,每组求数字和(可能 >9 需转成多位字符串),
 * 拼接为新串,若长度仍大于 k 则继续,否则返回。
 *
 * 时间复杂度:每轮 O(n),轮数约为 log_k(n),总体 O(n * log n);空间复杂度:O(n)
 */
public class Solution {

    public String digitSum(String s, int k) {
        while (s.length() > k) {
            StringBuilder sb = new StringBuilder();
            // 按每段 k 个字符分组
            for (int i = 0; i < s.length(); i += k) {
                int sum = 0;
                for (int j = i; j < Math.min(s.length(), i + k); j++) {
                    sum += s.charAt(j) - '0';
                }
                sb.append(sum);
            }
            s = sb.toString();
        }
        return s;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        // 示例 1:预期 "135"
        System.out.println(s.digitSum("11111222223", 3));
        // 示例 2:预期 "000"
        System.out.println(s.digitSum("00000000", 3));
        // 自测:长度 <= k 直接返回,预期 "123"
        System.out.println(s.digitSum("123", 5));
    }
}

复杂度分析

  • 解法:循环模拟(主提交版本)
  • 时间复杂度:O(n²) 最坏(n ≤ 100,每轮 O(len))
  • 空间复杂度:O(n) --- 每次拼接新串

2244. Minimum Rounds to Complete All Tasks(完成所有任务需要的最少轮数)

题目描述

给你一个下标从 0 开始的整数数组 tasks,其中 tasks[i] 表示任务的难度级别。在每一轮中,你可以完成 2 个或者 3 个相同难度级别 的任务。返回完成所有任务需要的最少 轮数,如果无法完成所有任务返回 -1。

示例 1:

复制代码
输入:tasks = [2,2,3,3,2,4,4,4,4,4]
输出:4
解释:难度 2 的 3 个一轮、难度 3 的 2 个一轮、难度 4 的 3+2 两轮,共 4 轮。

示例 2:

复制代码
输入:tasks = [2,3,3]
输出:-1
解释:难度 2 只有 1 个任务,无法凑成 2 或 3 个一轮。

约束:

  • 1 <= tasks.length <= 10⁵

  • 1 <= tasks[i] <= 10⁹

解题思路

核心思想 :统计个数 + 贪心凑轮。设某难度任务数为 cnt,每轮只能完成 2 或 3 个相同难度的任务,因此:

  • cnt == 1:无法完成,整体返回 -1;
  • cnt >= 2:最优轮数 = cnt / 3 + (cnt % 3 == 0 ? 0 : 1)。

为什么这个公式对 :尽量用 3 个一轮(cnt / 3 轮),余数分三种情况------余 0 刚好;余 1 时把最后一组 3 拆成两个 2(总轮数 +1);余 2 时多来一组 2(+1)。cnt = 2 时 0 余 2,2 / 3 + 1 = 1 轮,正确。

步骤:

  1. HashMap 统计每种难度数量。
  2. 对每个 cnt 累加轮数,遇到 1 直接返回 -1。

复杂度:O(n) 时间、O(n) 空间。

代码实现

java 复制代码
import java.util.HashMap;
import java.util.Map;

/**
 * 2244. Minimum Rounds to Complete All Tasks 完成所有任务需要的最少轮数
 *
 * 给你一个下标从 0 开始的整数数组 tasks,其中 tasks[i] 表示任务的难度级别。在每一轮中,你可以
 * 完成 2 个或者 3 个相同难度级别的任务。返回完成所有任务需要的最少轮数,如果无法完成所有任务
 * 返回 -1。
 *
 * 算法思路:统计每个难度级别的任务个数 cnt。对每个 cnt:
 * - cnt == 1 时无法完成(1 不能拆成 2 或 3),返回 -1;
 * - cnt >= 2 时,最优策略是尽量多用 3:先 cnt / 3 轮取 3 个,剩余 cnt % 3 若为 0 则刚好;
 *   若余 1,则把最后一组 3 拆成两个 2(即总轮数 +1);若余 2,则再来一轮 2。
 *   即轮数 = cnt / 3 + (cnt % 3 == 0 ? 0 : 1)(cnt >= 2 时恒成立)。
 *
 * 时间复杂度:O(n),空间复杂度:O(n)
 */
public class Solution {

    public int minimumRounds(int[] tasks) {
        Map<Integer, Integer> cnt = new HashMap<>();
        for (int t : tasks) {
            cnt.put(t, cnt.getOrDefault(t, 0) + 1);
        }
        int ans = 0;
        for (int c : cnt.values()) {
            if (c == 1) {
                return -1;
            }
            // c >= 2:每 3 个一组(除最后)
            ans += c / 3;
            if (c % 3 != 0) {
                ans++;
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        // 示例 1:预期 4
        System.out.println(s.minimumRounds(new int[]{2, 2, 3, 3, 2, 4, 4, 4, 4, 4}));
        // 示例 2:预期 -1
        System.out.println(s.minimumRounds(new int[]{2, 3, 3}));
        // 自测:2 个任务一轮,预期 1
        System.out.println(s.minimumRounds(new int[]{1, 1}));
        // 自测:4 个任务拆 2+2,预期 2
        System.out.println(s.minimumRounds(new int[]{1, 1, 1, 1}));
    }
}

复杂度分析

  • 解法:贪心凑轮(主提交版本)
  • 时间复杂度:O(n)
  • 空间复杂度:O(n) --- 哈希表

2245. Maximum Trailing Zeros in a Cornered Path(转角路径的乘积中最多能有几个尾随零)

题目描述

给你一个二维整数数组 grid,大小为 m x n,其中每个单元格都含一个正整数。转角路径 定义为:包含至多一个弯的一组相邻单元。具体而言,路径应该完全向水平方向 或者向竖直方向 移动过弯(如果存在弯),而不能访问之前访问过的单元格。在过弯之后,路径应当完全朝另一个 方向行进。一条路径的乘积 定义为路径上所有值的乘积。请从 grid 中找出一条乘积中尾随零数目最多的转角路径,并返回该路径中尾随零的数目。

示例 1:

复制代码
输入:grid = [[23,17,15,3,20],[8,1,20,27,11],[9,4,6,2,21],[40,9,1,10,6],[22,7,4,5,3]]
输出:3
解释:路径 15 * 20 * 6 * 1 * 10 = 18000,共 3 个尾随零。

示例 2:

复制代码
输入:grid = [[4,3,2],[7,6,1],[8,8,8]]
输出:0
解释:不存在乘积含尾随零的转角路径。

约束:

  • m == grid.length

  • n == grid[i].length

  • 1 <= m, n <= 10⁵(即 m * n <= 10⁵)

  • 1 <= grid[i][j] <= 1000

解题思路

核心思想 :尾随零 = min(因子2的个数, 因子5的个数)。转角路径本质是"一条水平线段 + 一条竖直线段"以某格为角点拼接,共有 4 种形状:左+上、左+下、右+上、右+下。

步骤:

  1. 对每个格求因子 2、5 的个数 c2[i][j]、c5[i][j]。
  2. 分别做行前缀和 (每个单元格前缀)与列前缀和,支持 O(1) 求任意行段/列段内因子数。
  3. 枚举每个格子作为角点 (i, j),尝试四种方向组合;行段与列段都包含角点自身,合并时减去一次角点的因子数避免重复计数。
  4. 对四种组合取 min(因子2总数, 因子5总数) 的最大值。

复杂度:O(m × n) 时间与空间。

代码实现

java 复制代码
/**
 * 2245. Maximum Trailing Zeros in a Cornered Path 转角路径的乘积中最多能有几个尾随零
 *
 * 给你一个二维整数数组 grid,大小为 m x n,其中每个单元格都含一个正整数。转角路径定义为:
 * 包含至多一个弯的一组相邻单元,路径完全向水平方向或竖直方向移动过弯(如果存在弯),不能访问
 * 之前访问过的单元格。一条路径的乘积定义为路径上所有值的乘积。请找出一条乘积中尾随零数目最多的
 * 转角路径,返回该路径中尾随零的数目。
 *
 * 算法思路:尾随零 = min(因子2的个数, 因子5的个数)。转角路径只有 4 种形状:横+竖的上下左右
 * 组合,即 (左段+上段)、(左段+下段)、(右段+上段)、(右段+下段),且都经过某个"转角"单元格。
 * 预处理每行/每列的因子 2、5 的前缀和,然后枚举每个单元格作为转角,4 种方向组合取最大。
 *
 * 时间复杂度:O(m * n),空间复杂度:O(m * n)
 */
public class Solution {

    public int maxTrailingZeros(int[][] grid) {
        int m = grid.length, n = grid[0].length;
        // 因子 2、5 个数矩阵
        int[][] c2 = new int[m][n];
        int[][] c5 = new int[m][n];
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                int v = grid[i][j];
                while (v % 2 == 0) { c2[i][j]++; v /= 2; }
                while (v % 5 == 0) { c5[i][j]++; v /= 5; }
            }
        }
        // 行前缀和:row2[i][j] = 第 i 行前 j 列的 2 因子数(j 为前缀长度 0..n)
        int[][] row2 = new int[m][n + 1];
        int[][] row5 = new int[m][n + 1];
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                row2[i][j + 1] = row2[i][j] + c2[i][j];
                row5[i][j + 1] = row5[i][j] + c5[i][j];
            }
        }
        // 列前缀和:col2[j][i] = 第 j 列前 i 行的因子数
        int[][] col2 = new int[n][m + 1];
        int[][] col5 = new int[n][m + 1];
        for (int j = 0; j < n; j++) {
            for (int i = 0; i < m; i++) {
                col2[j][i + 1] = col2[j][i] + c2[i][j];
                col5[j][i + 1] = col5[j][i] + c5[i][j];
            }
        }
        int ans = 0;
        // 枚举转角单元格 (i, j)
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                // 行段:左段 [0..j] 或右段 [j..n-1];列段:上段 [0..i] 或下段 [i..m-1]
                // 注意角点自身会被行段与列段重复计算一次
                for (int lr = 0; lr < 2; lr++) {          // 0 左段,1 右段
                    for (int ud = 0; ud < 2; ud++) {      // 0 上段,1 下段
                        // 行段 2 因子数
                        int r2 = lr == 0 ? row2[i][j + 1] : row2[i][n] - row2[i][j];
                        int r5 = lr == 0 ? row5[i][j + 1] : row5[i][n] - row5[i][j];
                        // 列段 2 因子数(含角点,故需减去一次角点去重)
                        int c2v = ud == 0 ? col2[j][i + 1] : col2[j][m] - col2[j][i];
                        int c5v = ud == 0 ? col5[j][i + 1] : col5[j][m] - col5[j][i];
                        // 角点被算两次,减去一次
                        int t2 = r2 + c2v - c2[i][j];
                        int t5 = r5 + c5v - c5[i][j];
                        ans = Math.max(ans, Math.min(t2, t5));
                    }
                }
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        // 示例 1:预期 3
        System.out.println(s.maxTrailingZeros(new int[][]{
                {23, 17, 15, 3, 20},
                {8, 1, 20, 27, 11},
                {9, 4, 6, 2, 21},
                {40, 9, 1, 10, 6},
                {22, 7, 4, 5, 3}}));
        // 示例 2:预期 0
        System.out.println(s.maxTrailingZeros(new int[][]{
                {4, 3, 2},
                {7, 6, 1},
                {8, 8, 8}}));
    }
}

复杂度分析

  • 解法:因子分解 + 行/列前缀和枚举(主提交版本)
  • 时间复杂度:O(m × n) --- 单遍预处理 + 枚举
  • 空间复杂度:O(m × n) --- 前缀和数组

2246. Longest Path With Different Adjacent Characters(相邻字符不同的最长路径)

题目描述

给你一棵树 (连通、无向、无环图),根节点是节点 0,这棵树由编号从 0 到 n - 1 的 n 个节点组成。用下标从 0 开始、长度为 n 的数组 parent 表示这棵树,其中 parent[i] 是节点 i 的父节点,parent[0] == -1。另给你一个字符串 s,长度也是 n,其中 s[i] 表示分配给节点 i 的字符。找出路径上任意一对相邻节点都没有分配到相同字符的最长路径,返回该路径的长度(节点个数)。

示例 1:

复制代码
输入:parent = [-1,0,0,1,1,2], s = "abacbe"
输出:3
解释:最长路径 0 -> 1 -> 3,长度 3。

示例 2:

复制代码
输入:parent = [-1,0,0,0], s = "aabc"
输出:3
解释:最长路径 2 -> 0 -> 3,长度 3。

约束:

  • n == parent.length == s.length

  • 1 <= n <= 10⁵

  • 0 <= parent[i] <= n - 1 对所有 i >= 1

  • parent[0] == -1

  • parent 表示一棵有效树

  • s 由小写英文字母组成

解题思路

核心思想 :树形 DP。以某个节点为路径"拐点"的最长合法路径,由它向下最长的两条可接续链(子节点字符与它不同)合并而成。对每个节点,DFS 返回"以该节点为端点向下延伸的最大合法链长(节点数)",并在归过程中更新全局答案。

步骤:

  1. 由 parent 建树(父 → 子边)。
  2. dfs(u):遍历子节点 v,递归得到子链长 len;若 s[u] == s[v],该子链不能接续(但可作为独立路径考虑),否则用 len 更新两条最长链 max1、max2。
  3. 过 u 的候选路径节点数 = max1 + max2 + 1,更新全局 ans。
  4. 返回 max1 + 1 作为父节点视角下的子链长。

复杂度:O(n) 时间、O(n) 空间。

代码实现

java 复制代码
import java.util.ArrayList;
import java.util.List;

/**
 * 2246. Longest Path With Different Adjacent Characters 相邻字符不同的最长路径
 *
 * 给你一棵树(连通、无向、无环图),根节点是节点 0,由整数数组 parent 表示,parent[i] 是节点 i
 * 的父节点,parent[0] == -1。另给字符串 s,s[i] 表示节点 i 的字符。找出路径上任意一对相邻节点
 * 都没有分配到相同字符的最长路径,返回该路径的长度(节点数)。
 *
 * 算法思路:树形 DP。对每个节点维护"从该节点向下、经过不同字符节点"的最长链长度(以节点为端点
 * 计)。对每个节点合并子节点传递上来的最长两条链(子节点字符相同则不能接续),
 * 以该节点为拐点的候选路径 = 最长链 + 次长链 - 1(节点数),全局最大值即为答案。
 *
 * 时间复杂度:O(n),空间复杂度:O(n)
 */
public class Solution {

    private List<Integer>[] g;
    private String s;
    private int ans;

    public int longestPath(int[] parent, String s) {
        this.s = s;
        int n = parent.length;
        g = new List[n];
        for (int i = 0; i < n; i++) {
            g[i] = new ArrayList<>();
        }
        for (int i = 1; i < n; i++) {
            g[parent[i]].add(i);
        }
        ans = 1;
        dfs(0);
        return ans;
    }

    /** 返回以节点 u 为端点(向下)且相邻字符互不相同的最长链长度(节点数) */
    private int dfs(int u) {
        // 两条最长的"可接续"链(子节点字符与 u 不同的情况下)
        int max1 = 0, max2 = 0;
        for (int v : g[u]) {
            int len = dfs(v);            // 子链节点数
            if (s.charAt(u) == s.charAt(v)) {
                // 父字符与子相同,该子链不能作为 u 的延续,但可以单独成为候选路径
                continue;
            }
            if (len > max1) {
                max2 = max1;
                max1 = len;
            } else if (len > max2) {
                max2 = len;
            }
        }
        // 过 u 的候选路径 = 最长两条链合并 + u 自身
        ans = Math.max(ans, max1 + max2 + 1);
        return max1 + 1;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        // 示例 1:预期 3
        System.out.println(s.longestPath(new int[]{-1, 0, 0, 1, 1, 2}, "abacbe"));
        // 示例 2:预期 3
        System.out.println(s.longestPath(new int[]{-1, 0, 0, 0}, "aabc"));
        // 自测:单节点,预期 1
        System.out.println(s.longestPath(new int[]{-1}, "z"));
    }
}

复杂度分析

  • 解法:树形 DP(主提交版本)
  • 时间复杂度:O(n) --- 每个节点一次
  • 空间复杂度:O(n) --- 树 + 递归栈

2247. Maximum Cost of Trip With K Highways(K 条高速公路的最大旅行费用)

题目描述

一系列高速公路连接从 0 到 n - 1 的 n 个城市。给定一个二维整数数组 highways,其中 highways[i] = [city1i, city2i, tolli] 表示有一条高速公路连接 city1i 和 city2i,允许一辆汽车从 city1i 前往 city2i,反之亦然 ,费用为 tolli。给你一个整数 k,你要正好 经过 k 条公路。你可以从任何一个城市出发,但在旅途中每个城市最多 只能访问一次。返回旅行的最大费用,如果没有符合要求的行程则返回 -1。

示例 1:

复制代码
输入:n = 5, highways = [[0,1,4],[2,1,3],[1,4,11],[3,2,3],[3,4,2]], k = 3
输出:17
解释:路径 0 -> 1 -> 4 -> 3 费用 4 + 11 + 2 = 17,可证为最大。

示例 2:

复制代码
输入:n = 4, highways = [[0,1,3],[2,3,2]], k = 2
输出:-1
解释:不存在长度为 2 的有效行程。

约束:

  • 2 <= n <= 15

  • 0 <= highways.length <= 50

  • highways[i].length == 3

  • 0 <= city1i, city2i <= n - 1

  • city1i != city2i

  • 0 <= tolli <= 100

  • 1 <= k <= 50

  • 无重复高速公路

解题思路

核心思想 :状态压缩记忆化搜索(bitmask DP)。n <= 15,可用一个 int 位掩码表示已访问城市集合。状态 (u, mask) 表示当前在城市 u 且已访问集合为 mask 时,走完剩余边能获得的最大费用;已经走过 k 条边(mask 中城市数为 k + 1)时返回 0。

步骤:

  1. 若 k + 1 > n 直接返回 -1(简单路径最多 n - 1 条边)。
  2. 建无向邻接表 (neighbor, toll)。
  3. dfs(u, mask):Integer.bitCount(mask) == k + 1 时返回 0;否则枚举未访问邻居 v,递归 w + dfs(v, mask | (1 << v)),取最大值;不可达记 -1。
  4. 以每个城市为起点(mask = 1 << i)调用,取全局最大。

复杂度:状态数 O(n × 2ⁿ),每个状态枚举度数,总 O(n² × 2ⁿ),n ≤ 15 时可行。

代码实现

java 复制代码
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * 2247. Maximum Cost of Trip With K Highways K 条高速公路的最大旅行费用
 *
 * 一系列高速公路连接从 0 到 n - 1 的 n 个城市。给定一个二维整数数组 highways,其中
 * highways[i] = [city1i, city2i, tolli] 表示有一条高速公路连接 city1i 和 city2i,允许车辆
 * 双向通行,费用为 tolli。给你一个整数 k,你要正好经过 k 条公路。你可以从任何一个城市出发,
 * 但在旅途中每个城市最多只能访问一次。返回旅行的最大费用,如果没有符合要求的行程则返回 -1。
 *
 * 算法思路:位掩码状态压缩记忆化搜索。状态 (u, mask) 表示当前在城市 u、已访问城市集合为 mask
 * 时,还能得到的最大费用。当 mask 中城市个数 == k + 1(已经走过 k 条边)时返回 0;否则枚举 u
 * 的未访问邻居 v,递归取 max(toll + dfs(v, mask | (1 << v)))。每个城市都可作为起点,
 * 取所有 dfs(i, 1 << i) 的最大值。
 *
 * 时间复杂度:O(n * 2^n)(n <= 15),空间复杂度:O(n * 2^n)
 */
public class Solution {

    private List<int[]>[] graph;
    private int[][] mem;
    private int k;

    public int maximumCost(int n, int[][] highways, int k) {
        this.k = k;
        if (k + 1 > n) return -1;
        graph = new List[n];
        for (int i = 0; i < n; i++) {
            graph[i] = new ArrayList<>();
        }
        for (int[] h : highways) {
            int u = h[0], v = h[1], w = h[2];
            graph[u].add(new int[]{v, w});
            graph[v].add(new int[]{u, w});
        }
        mem = new int[n][1 << n];
        for (int[] row : mem) {
            Arrays.fill(row, -1);
        }
        int ans = -1;
        for (int i = 0; i < n; i++) {
            ans = Math.max(ans, dfs(i, 1 << i));
        }
        return ans;
    }

    /** 深度优先搜索:在城市 u、已访问集合 mask 时,继续走完 k 条边可得到的最大费用 */
    private int dfs(int u, int mask) {
        if (Integer.bitCount(mask) == k + 1) {
            return 0;
        }
        if (mem[u][mask] != -1) {
            return mem[u][mask];
        }
        int res = -1;
        for (int[] e : graph[u]) {
            int v = e[0], w = e[1];
            if ((mask >> v & 1) == 1) {
                continue;
            }
            int next = dfs(v, mask | (1 << v));
            if (next != -1) {
                res = Math.max(res, w + next);
            }
        }
        return mem[u][mask] = res;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        // 示例 1:预期 17
        System.out.println(s.maximumCost(5,
                new int[][]{{0, 1, 4}, {2, 1, 3}, {1, 4, 11}, {3, 2, 3}, {3, 4, 2}}, 3));
        // 示例 2:预期 -1
        System.out.println(s.maximumCost(4,
                new int[][]{{0, 1, 3}, {2, 3, 2}}, 2));
        // 自测:k+1 > n,预期 -1
        System.out.println(s.maximumCost(2,
                new int[][]{{0, 1, 5}}, 2));
    }
}

复杂度分析

  • 解法:bitmask DP 记忆化搜索(主提交版本)
  • 时间复杂度:O(n² × 2ⁿ)(n ≤ 15)
  • 空间复杂度:O(n × 2ⁿ)

2248. Intersection of Multiple Arrays(多个数组求交集)

题目描述

给你一个二维整数数组 nums,其中 nums[i] 是由不同 正整数组成的一个非空数组。按升序排列 返回一个数组,数组中的每个元素在 nums 所有数组中都出现过。

示例 1:

复制代码
输入:nums = [[3,1,2,4,5],[1,2,3,4],[3,4,5,6]]
输出:[3,4]
解释:只有 3 和 4 出现在所有数组中。

示例 2:

复制代码
输入:nums = [[1,2,3],[4,5,6]]
输出:[]
解释:无公共元素。

约束:

  • 1 <= nums.length <= 1000

  • 1 <= sum(nums[i].length) <= 1000

  • 1 <= nums[i][j] <= 1000

  • nums[i] 中的所有值互不相同

解题思路

核心思想 :计数数组。值域 [1, 1000],用一个 int[1001] 统计每个值出现的数组个数,最后输出计数等于数组总数的值。

步骤:

  1. cnt[v]++ 遍历所有数组元素(每组内元素互异,无需额外去重)。
  2. 升序遍历 0 到 1000,若 cnt[v] == nums.length 输出 v。

复杂度:O(总元素数 + 1000) 时间、O(1001) 空间。

代码实现

java 复制代码
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * 2248. Intersection of Multiple Arrays 多个数组求交集
 *
 * 给你一个二维整数数组 nums,其中 nums[i] 是由不同正整数组成的一个非空数组。按升序返回一个数组,
 * 数组中的每个元素在 nums 所有数组中都出现过。
 *
 * 算法思路:由于元素值可能较大(<= 1000),直接用数组计数。对每个数组去重自增计数,
 * 最后统计计数 == 数组个数 nums.length 的元素,按升序输出。
 *
 * 时间复杂度:O(总元素个数),空间复杂度:O(maxVal)
 */
public class Solution {

    public List<Integer> intersection(int[][] nums) {
        int[] cnt = new int[1001];
        int m = nums.length;
        for (int[] arr : nums) {
            for (int v : arr) {
                cnt[v]++;
            }
        }
        List<Integer> ans = new ArrayList<>();
        for (int v = 0; v <= 1000; v++) {
            if (cnt[v] == m) {
                ans.add(v);
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        // 示例 1:预期 [3,4]
        System.out.println(s.intersection(new int[][]{{3, 1, 2, 4, 5}, {1, 2, 3, 4}, {3, 4, 5, 6}}));
        // 示例 2:预期 []
        System.out.println(s.intersection(new int[][]{{1, 2, 3}, {4, 5, 6}}));
    }
}

复杂度分析

  • 解法:计数数组(主提交版本)
  • 时间复杂度:O(总元素数 + 1000)
  • 空间复杂度:O(1001)

2249. Count Lattice Points Inside a Circle(统计圆内格点数目)

题目描述

给你一个二维整数数组 circles,其中 circles[i] = [xi, yi, ri] 表示网格上圆心为 (xi, yi) 且半径为 ri 的第 i 个圆。返回出现在至少一个 圆内的格点数目 。格点 指整数坐标对应的点,圆周上的点也被视为出现在圆内。

示例 1:

复制代码
输入:circles = [[2,2,1]]
输出:5
解释:圆内格点为 (1,2)、(2,1)、(2,2)、(2,3)、(3,2),共 5 个。

示例 2:

复制代码
输入:circles = [[2,2,2],[3,4,1]]
输出:16
解释:两个圆内共有 16 个格点。

约束:

  • 1 <= circles.length <= 200

  • circles[i].length == 3

  • 1 <= xi, yi <= 100

  • 1 <= ri <= min(xi, yi)

解题思路

核心思想 :暴力枚举所有候选格点 + 集合去重。对每个圆,扫描其外接正方形(x ∈ [xi - ri, xi + ri],y ∈ [yi - ri, yi + ri])内的所有整数点,若满足 (x-xi)² + (y-yi)² <= ri² 则属于该圆。圆可能重叠,用编码集合去重。

步骤:

  1. 对每个圆四重枚举外接正方形内的整数点,判断距离 <= ri²。
  2. 命中则把 x * 10000 + y 加入 HashSet(坐标绝对值 < 10000,编码安全)。
  3. 返回集合大小。

复杂度 :每个圆枚举 (2r+1)² 个点,r ≤ 100,最多 200 个圆,运算量约 8×10⁶,可行;空间 O(去重点数)。

代码实现

java 复制代码
import java.util.HashSet;
import java.util.Set;

/**
 * 2249. Count Lattice Points Inside a Circle 统计圆内格点数目
 *
 * 给你一个二维整数数组 circles,其中 circles[i] = [xi, yi, ri] 表示圆心为 (xi, yi) 且半径为
 * ri 的第 i 个圆。返回出现在至少一个圆内的格点数目。格点指整数坐标对应的点,圆周上的点也算。
 *
 * 算法思路:枚举所有可能落在圆内的整数点。对每个圆,x 范围 [xi - ri, xi + ri],y 范围
 * [yi - ri, yi + ri],判断 (x-xi)² + (y-yi)² <= ri² 是否成立。由于可能有多个圆重叠,使用
 * HashSet 去重,坐标编码为 x * 10000 + y(坐标绝对值 <= 300,编码安全)。每个圆最多枚举
 * (2r+1)² 个点,r <= 100,总枚举量 <= 200 个圆 * 201² 约 8×10⁶,可接受。
 *
 * 时间复杂度:O(sum(ri²)),空间复杂度:O(枚举点数)
 */
public class Solution {

    public int countLatticePoints(int[][] circles) {
        Set<Integer> set = new HashSet<>();
        for (int[] c : circles) {
            int x = c[0], y = c[1], r = c[2];
            // 扫描该圆的外接正方形内的所有整数点
            for (int i = x - r; i <= x + r; i++) {
                for (int j = y - r; j <= y + r; j++) {
                    int dx = i - x, dy = j - y;
                    if (dx * dx + dy * dy <= r * r) {
                        set.add(i * 10000 + j); // 坐标去重编码
                    }
                }
            }
        }
        return set.size();
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        // 示例 1:预期 5
        System.out.println(s.countLatticePoints(new int[][]{{2, 2, 1}}));
        // 示例 2:预期 16
        System.out.println(s.countLatticePoints(new int[][]{{2, 2, 2}, {3, 4, 1}}));
        // 自测:两个圆重叠,预期 12(r=1 的圆 5 个点,r=1 圆心(2,3) 与 (2,2) 圆相切)
        System.out.println(s.countLatticePoints(new int[][]{{2, 2, 1}, {2, 4, 1}}));
    }
}

复杂度分析

  • 解法:外接正方形枚举 + 集合去重(主提交版本)
  • 时间复杂度:O(∑(2r+1)²) ≈ 8×10⁶
  • 空间复杂度:O(去重格点数)

2250. Count Number of Rectangles Containing Each Point(统计包含每个点的矩形数目)

题目描述

给你一个二维整数数组 rectangles,其中 rectangles[i] = [li, hi] 表示第 i 个矩形长为 li 高为 hi。再给你一个二维整数数组 points,其中 points[j] = [xj, yj] 是坐标为 (xj, yj) 的一个点。第 i 个矩形的左下角 在 (0, 0) 处,右上角 在 (li, hi)。返回整数数组 count,其中 count[j] 是包含 第 j 个点的矩形数目。如果 0 <= xj <= li 且 0 <= yj <= hi,则该矩形包含该点;点刚好在矩形边上也被视为被包含。

示例 1:

复制代码
输入:rectangles = [[1,2],[2,3],[2,5]], points = [[2,1],[1,4]]
输出:[2,1]
解释:点 (2,1) 被第 2、3 个矩形包含;点 (1,4) 只被第 3 个矩形包含。

示例 2:

复制代码
输入:rectangles = [[1,1],[2,2],[3,3]], points = [[1,3],[1,1]]
输出:[1,3]
解释:点 (1,3) 只被第 3 个矩形包含;点 (1,1) 被全部 3 个矩形包含。

约束:

  • 1 <= rectangles.length, points.length <= 10⁵

  • rectangles[i].length == points[j].length == 2

  • 1 <= li, xj <= 10⁹

  • 1 <= hi, yj <= 100

  • 所有 rectangles 互不相同

  • xj 升序给出

解题思路

核心思想 :按高度分组 + 二分。高度 hi 最大仅 100,把矩形按高度分组,每组内存放长度并升序排序。对点 (x, y),只需枚举高度 h >= y 的分组,二分查长度 >= x 的矩形个数并累加。

步骤:

  1. groups[h].add(l):按高度分组收集矩形长度。
  2. 每组升序排序。
  3. 对每个点 (x, y),对 h ∈ [y, 100] 的各组做 lowerBound(x)(第一个长度 >= x 的下标),计数 = size - lowerBound。
  4. 累加后填回答案数组。

复杂度:排序 O(n log n),每个点枚举最多 101 组 × 每组 O(log n) 二分,O(m × 101 × log n),可行。

代码实现

java 复制代码
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * 2250. Count Number of Rectangles Containing Each Point 统计包含每个点的矩形数目
 *
 * 给你一个二维整数数组 rectangles,其中 rectangles[i] = [li, hi] 表示第 i 个矩形长为 li 高为 hi。
 * 再给你一个二维整数数组 points,其中 points[j] = [xj, yj] 是坐标为 (xj, yj) 的一个点。第 i 个
 * 矩形的左下角在 (0, 0),右上角在 (li, hi)。返回整数数组 count,count[j] 是包含第 j 个点的矩形
 * 数目。点在矩形的边上也被视为被包含。
 *
 * 算法思路:高度 hi 范围有限(<= 100),按高度分组:groups[h] 存放所有高度为 h 的矩形长度。
 * 对每个点 (x, y),枚举所有高度 h >= y 的分组,在分组二分查找长度 >= x 的矩形个数并累加。
 * 这样避免了对每个点遍历所有矩形 O(n*m) 的开销。
 *
 * 时间复杂度:O((n + m) * maxHi * log n),空间复杂度:O(n)
 */
public class Solution {

    public int[] countRectangles(int[][] rectangles, int[][] points) {
        // 高度分组,每组升序存放长度
        List<List<Integer>> groups = new ArrayList<>();
        for (int h = 0; h <= 100; h++) {
            groups.add(new ArrayList<>());
        }
        for (int[] r : rectangles) {
            groups.get(r[1]).add(r[0]);
        }
        for (int h = 0; h <= 100; h++) {
            groups.get(h).sort(Integer::compareTo);
        }
        int m = points.length;
        int[] ans = new int[m];
        for (int j = 0; j < m; j++) {
            int x = points[j][0], y = points[j][1];
            int cnt = 0;
            // 高度必须 >= 点的 y 坐标
            for (int h = y; h <= 100; h++) {
                List<Integer> list = groups.get(h);
                cnt += list.size() - lowerBound(list, x);
            }
            ans[j] = cnt;
        }
        return ans;
    }

    /** 在升序 list 中查找第一个 >= x 的下标 */
    private int lowerBound(List<Integer> list, int x) {
        int lo = 0, hi = list.size();
        while (lo < hi) {
            int mid = (lo + hi) >>> 1;
            if (list.get(mid) < x) {
                lo = mid + 1;
            } else {
                hi = mid;
            }
        }
        return lo;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        // 示例 1:预期 [2,1]
        System.out.println(Arrays.toString(s.countRectangles(
                new int[][]{{1, 2}, {2, 3}, {2, 5}}, new int[][]{{2, 1}, {1, 4}})));
        // 示例 2:预期 [1,3]
        System.out.println(Arrays.toString(s.countRectangles(
                new int[][]{{1, 1}, {2, 2}, {3, 3}}, new int[][]{{1, 3}, {1, 1}})));
    }
}

复杂度分析

  • 解法:按高度分组 + 排序二分(主提交版本)
  • 时间复杂度:O(n log n + m × 101 × log n)
  • 空间复杂度:O(n)

2251. Number of Flowers in Full Bloom(花期内花的数目)

题目描述

给你一个下标从 0 开始的二维整数数组 flowers,其中 flowers[i] = [starti, endi] 表示第 i 朵花的花期 从 starti 到 endi(都包含 )。同时给你一个下标从 0 开始大小为 n 的整数数组 people,people[i] 是第 i 个人来看花的时间。返回大小为 n 的整数数组 answer,其中 answer[i] 是第 i 个人到达时在花期内花的数目。

示例 1:

复制代码
输入:flowers = [[1,6],[3,7],[9,12],[4,13]], people = [2,3,7,11]
输出:[1,2,2,2]
解释:时刻 2 只有花期 [1,6] 一朵花,时刻 3 有 [1,6]、[3,7] 两朵,等等。

示例 2:

复制代码
输入:flowers = [[1,10],[3,3]], people = [3,3,2]
输出:[2,2,1]
解释:时刻 3 两朵都开,时刻 2 只有 [1,10]。

约束:

  • 1 <= flowers.length <= 10⁵

  • flowers[i].length == 2

  • 1 <= starti <= endi <= 10⁹

  • 1 <= people.length <= 10⁵

  • 1 <= people[i] <= 10⁹

解题思路

核心思想 :排序 + 二分计数。对每个人的到达时刻 t:"花期开始 <= t" 的花数减去"花期结束 < t"(已凋谢)的花数,即为 t 时刻开放的花数。做法:分别对开始时间、结束时间排序,用二分快速求两个计数。

步骤:

  1. starts = 所有 starti 排序,ends = 所有 endi 排序。
  2. 对每个人 t:open = upperBound(starts, t)(starts <= t 的个数);closed = lowerBound(ends, t)(ends < t 的个数)。
  3. answer[i] = open - closed。

复杂度:排序 O(n log n),每个点两次 O(log n) 二分,总 O((n + m) log n) 时间、O(n) 空间。(也可用差分 + 双指针,本质相同。)

代码实现

java 复制代码
import java.util.Arrays;

/**
 * 2251. Number of Flowers in Full Bloom 花期内花的数目
 *
 * 给你一个下标从 0 开始的二维整数数组 flowers,其中 flowers[i] = [starti, endi] 表示第 i 朵花
 * 的花期从 starti 到 endi(都包含)。同时给你一个大小为 n 的整数数组 people,people[i] 是第 i
 * 个人来看花的时间。返回大小为 n 的整数数组 answer,answer[i] 是第 i 个人到达时在花期内花的数目。
 *
 * 算法思路:差分 + 排序。把每个花期拆成 start 处 +1、end+1 处 -1 的差分事件,所有事件按时间排序;
 * 再把 people 按时间排序并记录原下标。双指针扫描:在每个人的时间点累加到该时间为止的差分和即为
 * 该时刻开放的花数,按原下标填回答案数组。
 *
 * 时间复杂度:O((n + m) log(n + m)),空间复杂度:O(n + m)
 */
public class Solution {

    public int[] fullBloomFlowers(int[][] flowers, int[] people) {
        int n = flowers.length;
        int[] starts = new int[n];
        int[] ends = new int[n];
        for (int i = 0; i < n; i++) {
            starts[i] = flowers[i][0];
            ends[i] = flowers[i][1];
        }
        Arrays.sort(starts);
        Arrays.sort(ends);
        int m = people.length;
        int[] ans = new int[m];
        // 对每个人,对 starts 找 <= t 的个数,对 ends 找 < t 的个数,相减
        for (int i = 0; i < m; i++) {
            int t = people[i];
            int open = upperBound(starts, t);      // 花期已开始(<=t)的花数
            int closed = lowerBound(ends, t);      // 花期已结束(<t)的花数,即 end < t
            ans[i] = open - closed;
        }
        return ans;
    }

    /** 升序数组中第一个 > target 的下标(即 <= target 的个数) */
    private int upperBound(int[] arr, int target) {
        int lo = 0, hi = arr.length;
        while (lo < hi) {
            int mid = (lo + hi) >>> 1;
            if (arr[mid] <= target) {
                lo = mid + 1;
            } else {
                hi = mid;
            }
        }
        return lo;
    }

    /** 升序数组中第一个 >= target 的下标(即 < target 的个数) */
    private int lowerBound(int[] arr, int target) {
        int lo = 0, hi = arr.length;
        while (lo < hi) {
            int mid = (lo + hi) >>> 1;
            if (arr[mid] < target) {
                lo = mid + 1;
            } else {
                hi = mid;
            }
        }
        return lo;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        // 示例 1:预期 [1,2,2,2]
        System.out.println(Arrays.toString(s.fullBloomFlowers(
                new int[][]{{1, 6}, {3, 7}, {9, 12}, {4, 13}}, new int[]{2, 3, 7, 11})));
        // 示例 2:预期 [2,2,1]
        System.out.println(Arrays.toString(s.fullBloomFlowers(
                new int[][]{{1, 10}, {3, 3}}, new int[]{3, 3, 2})));
    }
}

复杂度分析

  • 解法:排序 + 二分计数(主提交版本)
  • 时间复杂度:O(n log n + m log n)
  • 空间复杂度:O(n)

2252. Dynamic Pivoting of a Table(表的动态旋转)

题目类型:数据库(SQL)

题目描述

表: Products

复制代码
+-------------+---------+
| Column Name | Type    |
+-------------+---------+
| product_id  | int     |
| store       | varchar |
| price       | int     |
+-------------+---------+

(product_id, store) 是该表的主键。该表的每一行都表示商店 store 中商品 product_id 的价格。表中最多有 30 个不同的商店。实现 PivotProducts 过程来重新组织 Products 表,使每行都有一个商品的 id 及其在每个商店中的价格;如果商品不在某商店出售,价格应为 null。表的列应该包含每个商店,并按字典顺序排序 。过程应该在重新组织表之后返回它,以任意顺序返回结果表。

示例 1:

复制代码
输入:Products 表如上(商品 1 在 Shop、LC_Store 销售;商品 2 在 Nozama、Souq;商品 3 在 Shop、Souq)
输出:
+------------+----------+--------+------+------+
| product_id | LC_Store | Nozama | Shop | Souq |
+------------+----------+--------+------+------+
| 1          | 100      | null   | 110  | null |
| 2          | null     | 200    | null | 190  |
| 3          | null     | null   | 1000 | 1900 |
+------------+----------+--------+------+------+
解释:商店按字典序 LC_Store、Nozama、Shop、Souq 排列。

约束:

  • (product_id, store) 是主键

  • 表中最多 30 个不同商店

  • 需要动态透视:商店列表不固定

解题思路

核心思想 :动态 SQL 生成透视列。商店列表不固定,先在运行时用 GROUP_CONCAT 拼出形如 ``MAX(IF(store='Shop', price, NULL)) AS Shop``` 的列表达式(按字典序 ORDER BY store),再 PREPARE+EXECUTE` 执行。

步骤:

  1. SET SESSION group_concat_max_len = 1000000:防止商店较多时拼接结果被截断。
  2. SELECT GROUP_CONCAT(... ORDER BY store) INTO @sql FROM Products 生成列片段。
  3. 拼上 SELECT product_id, <列片段> FROM Products GROUP BY product_id。
  4. PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;。

代码实现

sql 复制代码
-- 2252. Dynamic Pivoting of a Table 表的动态旋转
--
-- Table: Products
-- +-------------+---------+
-- | Column Name | Type    |
-- +-------------+---------+
-- | product_id  | int     |
-- | store       | varchar |
-- | price       | int     |
-- +-------------+---------+
-- (product_id, store) 是该表的主键。每行表示商店 store 中商品 product_id 的价格。
-- 表中最多有 30 个不同的商店。要求实现 PivotProducts 过程,把 Products 表透视(pivot)重组:
-- 每行一个商品 id,列是每个商店(按字典序排序),商品不在某店出售则价格为 null。
--
-- 思路:商店列表不是写死的,需要动态 SQL。先用 GROUP_CONCAT 在运行时拼出
-- "MAX(IF(store='Shop', price, NULL)) AS Shop, ..." 这样的列表达式(商店按字典序),
-- 再 PREPARE + EXECUTE 执行拼装好的动态查询。

CREATE PROCEDURE PivotProducts()
BEGIN
    -- 增大 GROUP_CONCAT 上限,避免商店多时结果被截断
    SET SESSION group_concat_max_len = 1000000;

    SET @sql = NULL;
    SELECT GROUP_CONCAT(
               CONCAT('MAX(IF(store = ''', store, ''', price, NULL)) AS `', store, '`')
               ORDER BY store)
    INTO @sql
    FROM Products;

    -- 汇总每个商品在每家商店的价格
    SET @sql = CONCAT(
        'SELECT product_id, ', @sql,
        ' FROM Products GROUP BY product_id ORDER BY product_id');

    PREPARE stmt FROM @sql;
    EXECUTE stmt;
    DEALLOCATE PREPARE stmt;
END;

复杂度分析

  • 解法:动态 SQL 拼列 + PREPARE/EXECUTE(主提交版本)
  • 时间复杂度:O(S + P)(S 为商店数,P 为行数)
  • 空间复杂度:O(S) --- 拼接字符串

2253. Dynamic Unpivoting of a Table(动态取消表的旋转)

题目类型:数据库(SQL)

题目描述

表: Products

复制代码
+-------------+---------+
| Column Name | Type    |
+-------------+---------+
| product_id  | int     |
| store_name1 | int     |
| ...         | int     |(至多 30 家店)
+-------------+---------+

product_id 是该表的主键。表中每行表示该商品在 n 个不同商店中的价格;若商店中没有该商品,该列价格为 null。不同测试用例的商店名称可能不同,至少有 1 家店、最多 30 家店。实现 UnpivotProducts 过程来重新组织 Products 表,使每一行都有商品的 id、销售该商品的商店名称以及该商品在该商店中的价格。如果某商品在某商店不可用,则不要 在结果表中包含该组合的行。结果应有 3 列:product_id、store 和 price,以任意顺序返回。

示例 1:

复制代码
输入:Products 表如上(宽表:1 在 LC_Store 100、Shop 110;2 在 Nozama 200、Souq 190;3 在 Shop 1000、Souq 1900)
输出:
+------------+----------+-------+
| product_id | store    | price |
+------------+----------+-------+
| 1          | LC_Store | 100   |
| 1          | Shop     | 110   |
| 2          | Nozama   | 200   |
| 2          | Souq     | 190   |
| 3          | Shop     | 1000  |
| 3          | Souq     | 1900  |
+------------+----------+-------+

约束:

  • product_id 是主键

  • 至少有 1 家店、至多 30 家店

  • 需要动态逆透视:商店列名不固定

解题思路

核心思想 :动态 SQL 生成逆透视查询。从 INFORMATION_SCHEMA.COLUMNS 查出表内所有非 product_id 的列名,对每一列拼一条 SELECT product_id, '列名' AS store, 列名 AS price FROM Products WHERE 列名 IS NOT NULL,用 UNION ALL 串接后动态执行。

步骤:

  1. SET SESSION group_concat_max_len = 1000000。
  2. SELECT GROUP_CONCAT(..., SEPARATOR ' UNION ALL ') INTO @sql FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'Products' AND COLUMN_NAME <> 'product_id' ORDER BY ORDINAL_POSITION。
  3. 每条子句对当前店铺列生成一行价格非 null 的记录。
  4. PREPARE + EXECUTE + DEALLOCATE PREPARE。

代码实现

sql 复制代码
-- 2253. Dynamic Unpivoting of a Table 动态取消表的旋转
--
-- Table: Products
-- +-------------+---------+
-- | Column Name | Type    |
-- +-------------+---------+
-- | product_id  | int     |
-- | store_name1 | int     |
-- | ...         | ...     (至多 30 家店)
-- +-------------+---------+
-- product_id 是主键,每行表示某商品在若干商店的价格(null 表示该店无该商品)。
-- 要求实现 UnpivotProducts 过程,把宽表逆透视(unpivot)为长表:
-- 每行 (product_id, store, price),只保留价格非 null 的组合。
--
-- 思路:商店列名不是写死的,需动态 SQL。从 INFORMATION_SCHEMA.COLUMNS 查出全部
-- 非 product_id 的列名,用 UNION ALL 拼出每个店列的无 null 行:
--   SELECT product_id, 'shop1' AS store, shop1 AS price FROM Products WHERE shop1 IS NOT NULL
-- 再用 PREPARE + EXECUTE 执行。

CREATE PROCEDURE UnpivotProducts()
BEGIN
    SET SESSION group_concat_max_len = 1000000;

    SET @sql = NULL;
    SELECT GROUP_CONCAT(
               CONCAT(
                   'SELECT product_id, ''', COLUMN_NAME, ''' AS store, `',
                   COLUMN_NAME, '` AS price FROM Products WHERE `',
                   COLUMN_NAME, '` IS NOT NULL')
               SEPARATOR ' UNION ALL ')
    INTO @sql
    FROM INFORMATION_SCHEMA.COLUMNS
    WHERE TABLE_NAME = 'Products' AND COLUMN_NAME <> 'product_id'
    ORDER BY ORDINAL_POSITION;

    PREPARE stmt FROM @sql;
    EXECUTE stmt;
    DEALLOCATE PREPARE stmt;
END;

复杂度分析

  • 解法:动态 SQL UNION ALL + PREPARE/EXECUTE(主提交版本)
  • 时间复杂度:O(S × P)
  • 空间复杂度:O(S) --- 拼接字符串

2254. Design Video Sharing Platform(设计视频共享平台)

题目描述

实现 VideoSharingPlatform 类:upload(String video) 上传视频,返回最小可用 videoId(从 0 开始,视频删除后其 id 可复用);remove(int videoId) 删除视频(不存在则忽略);watch(int videoId, int startMinute, int endMinute) 若视频存在则观看数 +1 并返回从 startMinute 到 min(endMinute, len-1)(含边界)的子串,否则返回 "-1";like(int videoId) 点赞数 +1(不存在则忽略);dislike(int videoId) 点踩数 +1(不存在则忽略);getLikesAndDislikes(int videoId) 返回 [点赞数, 点踩数],不存在返回 [-1];getViews(int videoId) 返回观看数,不存在返回 -1。

示例 1:

复制代码
输入:["VideoSharingPlatform", "upload", "upload", "remove", "remove", "upload", "watch", "watch", "like", "dislike", "dislike", "getLikesAndDislikes", "getViews"]
参数:[[], ["123"], ["456"], [4], [0], ["789"], [1, 0, 5], [1, 0, 1], [1], [1], [1], [1], [1]]
输出:[null, 0, 1, null, null, 0, "456", "45", null, null, null, [1, 2], 2]
解释:删除 0 后重新上传复用 id 0;watch(1,0,5) 截取 "456",watch(1,0,1) 截取 "45";点赞 1 次、点踩 2 次;观看 2 次。

示例 2:

复制代码
输入:["VideoSharingPlatform", "remove", "watch", "like", "dislike", "getLikesAndDislikes", "getViews"]
参数:[[], [0], [0, 0, 1], [0], [0], [0], [0]]
输出:[null, null, "-1", null, null, [-1], -1]
解释:视频 0 不存在,所有操作都按"不存在"处理。

约束:

  • 1 <= video.length <= 10⁴

  • 0 <= videoId, startMinute <= 1⁰⁵

  • videoId、startMinute、endMinute 均为整数

  • startMinute <= endMinute

  • upload、remove、watch、like、dislike、getLikesAndDislikes、getViews 总调用次数不超过 10⁵

  • video 只含数字字符

解题思路

核心思想 :哈希表存储 + 最小堆复用 id。用 Map<Integer, Video> 保存当前存在的视频;用 PriorityQueue 记录删除后被释放的最小 id;全局自增 nextId 分配新 id。上传时优先复用堆顶最小 id。

步骤:

  1. upload:堆非空则取堆顶复用,否则用 nextId++;存入视频对象。
  2. remove:从 Map 移除并 offer 进堆(存在才做)。
  3. watch:存在则观看数 +1,截取 [start, min(end, len-1)];否则返回 "-1"。
  4. 其余操作依题意对 Map 判空处理。

复杂度:upload O(log n)(堆操作),其余 O(1);空间 O(在存视频数)。

代码实现

java 复制代码
import java.util.HashMap;
import java.util.Map;
import java.util.PriorityQueue;

/**
 * 2254. Design Video Sharing Platform 设计视频共享平台
 *
 * 实现 VideoSharingPlatform 类:
 * - upload(String video) 上传视频,返回最小可用 videoId(从 0 开始,删除后可复用)。
 * - remove(int videoId) 删除视频(不存在则忽略)。
 * - watch(int videoId, int startMinute, int endMinute) 观看数 +1,返回从 startMinute 到
 *   min(endMinute, len-1) 的子串;不存在返回 "-1"。
 * - like(int videoId) / dislike(int videoId) 点赞 / 点踩(不存在则忽略)。
 * - getLikesAndDislikes(int videoId) 返回 [赞, 踩];不存在返回 [-1]。
 * - getViews(int videoId) 返回观看数;不存在返回 -1。
 *
 * 算法思路:用 Map<Integer, Video> 存当前存在的视频,PriorityQueue 管理被删除后待复用的最小
 * videoId(贪心取最小);自增 id 作为全局递增分配器。upload 时优先从队列取复用 id,否则用下一个
 * 新 id。
 *
 * 时间复杂度:upload O(log n)、其余 O(1);空间复杂度:O(视频数)
 */
public class Solution {

    public static void main(String[] args) {
        VideoSharingPlatform p = new VideoSharingPlatform();
        System.out.println(p.upload("123"));  // 0
        System.out.println(p.upload("456"));  // 1
        p.remove(4);                          // 忽略(不存在)
        p.remove(0);                          // 删除 0
        System.out.println(p.upload("789"));  // 0(复用)
        System.out.println(p.watch(1, 0, 5)); // "456"
        System.out.println(p.watch(1, 0, 1)); // "45"
        p.like(1);
        p.dislike(1);
        p.dislike(1);
        System.out.println(java.util.Arrays.toString(p.getLikesAndDislikes(1))); // [1,2]
        System.out.println(p.getViews(1));    // 2

        VideoSharingPlatform p2 = new VideoSharingPlatform();
        p2.remove(0);
        System.out.println(p2.watch(0, 0, 1));            // "-1"
        p2.like(0);
        p2.dislike(0);
        System.out.println(java.util.Arrays.toString(p2.getLikesAndDislikes(0))); // [-1]
        System.out.println(p2.getViews(0));               // -1
    }
}

class VideoSharingPlatform {
    private static class Video {
        String content;
        int views, likes, dislikes;
        Video(String content) {
            this.content = content;
        }
    }

    private Map<Integer, Video> videos = new HashMap<>();
    private PriorityQueue<Integer> reusable = new PriorityQueue<>();
    private int nextId = 0;

    public int upload(String video) {
        int id;
        if (!reusable.isEmpty()) {
            id = reusable.poll();        // 复用被删除的最小 id
        } else {
            id = nextId++;
        }
        videos.put(id, new Video(video));
        return id;
    }

    public void remove(int videoId) {
        if (videos.remove(videoId) != null) {
            reusable.offer(videoId);
        }
    }

    public String watch(int videoId, int startMinute, int endMinute) {
        Video v = videos.get(videoId);
        if (v == null) {
            return "-1";
        }
        v.views++;
        int end = Math.min(endMinute, v.content.length() - 1);
        return v.content.substring(startMinute, end + 1);
    }

    public void like(int videoId) {
        Video v = videos.get(videoId);
        if (v != null) {
            v.likes++;
        }
    }

    public void dislike(int videoId) {
        Video v = videos.get(videoId);
        if (v != null) {
            v.dislikes++;
        }
    }

    public int[] getLikesAndDislikes(int videoId) {
        Video v = videos.get(videoId);
        if (v == null) {
            return new int[]{-1};
        }
        return new int[]{v.likes, v.dislikes};
    }

    public int getViews(int videoId) {
        Video v = videos.get(videoId);
        return v == null ? -1 : v.views;
    }
}

复杂度分析

  • 解法:哈希表 + 最小堆复用 id(主提交版本)
  • 时间复杂度:upload O(log n),其余 O(1)
  • 空间复杂度:O(在存视频数)

2255. Count Prefixes of a Given String(统计是给定字符串前缀的字符串数目)

题目描述

给你一个字符串数组 words 和一个字符串 s,其中 words[i] 和 s 只包含小写英文字母 。返回 words 中是字符串 s 前缀 的字符串数目。相同字符串出现多次应计数多次。

示例 1:

复制代码
输入:words = ["a","b","c","ab","bc","abc"], s = "abc"
输出:3
解释:"a"、"ab"、"abc" 是 "abc" 的前缀。

示例 2:

复制代码
输入:words = ["a","a"], s = "aa"
输出:2
解释:两个 "a" 都计一次。

约束:

  • 1 <= words.length <= 1000

  • 1 <= words[i].length, s.length <= 10

  • words[i] 和 s 只含小写英文字母

解题思路

核心思想 :直接调用 String.startsWith。每个 word 只要 s.startsWith(word) 就计数 +1。

步骤:

  1. 遍历 words。
  2. s.startsWith(w) 为真则 ans++。

复杂度:O(sum |word|) 时间、O(1) 空间。

代码实现

java 复制代码
/**
 * 2255. Count Prefixes of a Given String 统计是给定字符串前缀的字符串数目
 *
 * 给你一个字符串数组 words 和一个字符串 s,其中 words[i] 和 s 只包含小写英文字母。
 * 返回 words 中是字符串 s 前缀的字符串数目。相同字符串出现多次应计数多次。
 *
 * 算法思路:遍历每个 word,若 s.startsWith(word) 则计数 +1。直接利用 String.startsWith。
 *
 * 时间复杂度:O(sum(|word|)),空间复杂度:O(1)
 */
public class Solution {

    public int countPrefixes(String[] words, String s) {
        int ans = 0;
        for (String w : words) {
            if (s.startsWith(w)) {
                ans++;
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        // 示例 1:预期 3
        System.out.println(s.countPrefixes(new String[]{"a", "b", "c", "ab", "bc", "abc"}, "abc"));
        // 示例 2:预期 2
        System.out.println(s.countPrefixes(new String[]{"a", "a"}, "aa"));
        // 自测:空串前缀(题目保证非空,这里验证行为):预期 2
        System.out.println(s.countPrefixes(new String[]{"a", "aa", "ab", "abcd"}, "abc"));
    }
}

复杂度分析

  • 解法:遍历 + startsWith(主提交版本)
  • 时间复杂度:O(∑|word|)
  • 空间复杂度:O(1)

2256. Minimum Average Difference(最小平均差)

题目描述

给你一个下标从 0 开始长度为 n 的整数数组 nums。下标 i 处的平均差 指的是 nums 中前 i + 1 个元素平均值和后 n - i - 1 个元素平均值的绝对差 。两个平均值都需要向下取整 到最近的整数。返回产生最小平均差 的下标;如果有多个结果相等,返回最小 的下标。注意 :0 个元素的平均值视为 0。

示例 1:

复制代码
输入:nums = [2,5,3,9,5,3]
输出:3
解释:下标 3 处前后平均分别为 19/4=4 与 8/2=4,差 0 最小。

示例 2:

复制代码
输入:nums = [0]
输出:0
解释:唯一下标,差 |0 - 0| = 0。

约束:

  • 1 <= nums.length <= 10⁵

  • 0 <= nums[i] <= 10⁵

解题思路

核心思想 :前缀和一趟扫描。先求总和 total,再从左往右维护前缀和 pre;下标 i 处左侧平均 = pre / (i+1),右侧平均 =(i == n-1 时为 0,否则 (total - pre) / (n-i-1)),整除即向下取整。平均差 = 两者绝对差,维护最小差值及其下标。

步骤:

  1. 求 total(long 防溢出)。
  2. 遍历:累积 pre,计算 diff = |pre/(i+1) - (total-pre)/(n-i-1)|。
  3. diff < best 时更新 best 与 idx(严格小于保证取最小下标)。

复杂度:O(n) 时间、O(1) 空间。

代码实现

java 复制代码
/**
 * 2256. Minimum Average Difference 最小平均差
 *
 * 给你一个下标从 0 开始长度为 n 的整数数组 nums。下标 i 处的平均差指的是 nums 中前 i + 1 个元素
 * 平均值和后 n - i - 1 个元素平均值的绝对差。两个平均值都需要向下取整到最近的整数。
 * 返回产生最小平均差的下标,若有多个取最小的下标。注意:0 个元素的平均值视为 0。
 *
 * 算法思路:先求总和 total。从左到右用前缀和 pre 维护前 i+1 个元素的和,则左侧平均值
 * left = pre / (i+1)(整除),右侧平均值 right = (i == n-1) ? 0 : (total - pre) / (n-i-1)。
 * 平均差 = |left - right|,维护最小值和对应下标。
 *
 * 时间复杂度:O(n),空间复杂度:O(1)
 */
public class Solution {

    public int minimumAverageDifference(int[] nums) {
        int n = nums.length;
        long total = 0;
        for (int v : nums) {
            total += v;
        }
        long pre = 0;
        long best = Long.MAX_VALUE;
        int idx = 0;
        for (int i = 0; i < n; i++) {
            pre += nums[i];
            long left = pre / (i + 1);
            long right = (i == n - 1) ? 0 : (total - pre) / (n - i - 1);
            long diff = Math.abs(left - right);
            if (diff < best) {
                best = diff;
                idx = i;
            }
        }
        return idx;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        // 示例 1:预期 3
        System.out.println(s.minimumAverageDifference(new int[]{2, 5, 3, 9, 5, 3}));
        // 示例 2:预期 0
        System.out.println(s.minimumAverageDifference(new int[]{0}));
        // 自测:预期 0(左右平均差为 |1-2|=1 vs |1-1|=0... 实际算 [1,2,3,4,5])
        System.out.println(s.minimumAverageDifference(new int[]{1, 2, 3, 4, 5}));
    }
}

复杂度分析

  • 解法:前缀和扫描(主提交版本)
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

2257. Count Unguarded Cells in the Grid(统计网格图中没有被保卫的格子数)

题目描述

给你两个整数 m 和 n 表示一个下标从 0 开始的 m x n 网格图。同时给二维数组 guards(guards[i] = [rowi, coli] 表示第 i 个警卫的位置)和 walls(walls[j] = [rowj, colj] 表示第 j 座墙的位置)。一个警卫能看到 4 个坐标轴方向(东、南、西、北)的所有 格子,除非被墙或另一个警卫挡住 视线。如果一个格子能被至少 一个警卫看到,则说它被保卫 了。返回空格子中有多少格子没被保卫。

示例 1:

复制代码
输入:m = 4, n = 6, guards = [[0,0],[1,1],[2,3]], walls = [[0,1],[2,2],[1,4]]
输出:7
解释:网格中共 7 个空格未被保卫。

示例 2:

复制代码
输入:m = 3, n = 3, guards = [[1,1]], walls = [[0,1],[1,0],[2,1],[1,2]]
输出:4
解释:警卫四面都被墙挡住,其余空格全未保卫。

约束:

  • 1 <= m, n <= 10⁵(m * n <= 10⁵)

  • 0 <= guards.length, walls.length <= m * n

  • 1 <= guards.length + walls.length <= m * n

  • 警卫与墙位置互不重复

解题思路

核心思想 :网格标记 + 方向扩散。用 int 矩阵标记:墙 = 2、警卫 = 1、被保卫 = 3、空格 = 0。对每个警卫向 4 个方向直线扩散,遇到墙或另一个警卫停止,途中的空格全部标记为被保卫;最后统计仍为 0 的格子数。

步骤:

  1. 先标记墙为 2、警卫为 1。
  2. 对每个警卫,沿 4 个方向 while 循环标记(g[r][c] 非墙非警卫时置 3)。
  3. 统计值为 0 的格子。

复杂度:每个格子最多被扫常数次,O(m × n) 时间、O(m × n) 空间。

代码实现

java 复制代码
/**
 * 2257. Count Unguarded Cells in the Grid 统计网格图中没有被保卫的格子数
 *
 * 给你两个整数 m 和 n 表示一个下标从 0 开始的 m x n 网格图。同时给你 guards[i] = [rowi, coli]
 * 表示第 i 个警卫所在位置,walls[j] = [rowj, colj] 表示第 j 座墙所在位置。一个警卫能看到 4 个
 * 坐标轴方向的所有格子,除非视线被墙或另一个警卫挡住。如果一个格子能被至少一个警卫看到,则被保卫。
 * 返回空格子中有多少格子没被保卫。
 *
 * 算法思路:标记网格:墙 = 2,警卫 = 1。然后对每个警卫向 4 个方向扩散,遇到墙或警卫停止,
 * 途中标记为"被保卫"(3)。最后统计值为 0(空格且未被保卫)的单元格数量。
 *
 * 时间复杂度:O(m * n),空间复杂度:O(m * n)
 */
public class Solution {

    public int countUnguarded(int m, int n, int[][] guards, int[][] walls) {
        int[][] g = new int[m][n]; // 0 空, 1 警卫, 2 墙, 3 被保卫
        for (int[] w : walls) {
            g[w[0]][w[1]] = 2;
        }
        for (int[] gu : guards) {
            g[gu[0]][gu[1]] = 1;
        }
        // 四方向
        int[][] dirs = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
        for (int[] gu : guards) {
            for (int[] d : dirs) {
                int r = gu[0] + d[0], c = gu[1] + d[1];
                // 沿方向扩散直到出界、墙或另一个警卫
                while (r >= 0 && r < m && c >= 0 && c < n && g[r][c] != 2 && g[r][c] != 1) {
                    g[r][c] = 3;
                    r += d[0];
                    c += d[1];
                }
            }
        }
        int ans = 0;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (g[i][j] == 0) {
                    ans++;
                }
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        // 示例 1:预期 7
        System.out.println(s.countUnguarded(4, 6,
                new int[][]{{0, 0}, {1, 1}, {2, 3}},
                new int[][]{{0, 1}, {2, 2}, {1, 4}}));
        // 示例 2:预期 4
        System.out.println(s.countUnguarded(3, 3,
                new int[][]{{1, 1}},
                new int[][]{{0, 1}, {1, 0}, {2, 1}, {1, 2}}));
    }
}

复杂度分析

  • 解法:网格标记 + 四向扩散(主提交版本)
  • 时间复杂度:O(m × n)
  • 空间复杂度:O(m × n)

2258. Escape the Spreading Fire(逃离火灾)

题目描述

给你一个下标从 0 开始大小为 m x n 的二维整数数组 grid,表示一个网格图,每个格子为 0(草地)、1(着火的格子)或 2(墙,你跟火都不能通过)。一开始你在最左上角的格子 (0, 0),要到达最右下角的安全屋 (m - 1, n - 1)。每一分钟,你可以移动到相邻 的草地格子;每次你移动之后 ,着火的格子会扩散到所有不是墙的相邻 格子。返回在初始位置可以停留的最多 分钟数,且停留完后仍能安全到达安全屋;如果无法实现返回 -1;如果不管停留多久总是 能到达,返回 10⁹。注意:到达安全屋后火马上到安全屋,视为安全到达。

示例 1:

复制代码
输入:grid = [[0,2,0,0,0,0,0],[0,0,0,2,2,1,0],[0,2,0,0,1,2,0],[0,0,2,2,2,0,2],[0,0,0,0,0,0,0]]
输出:3
解释:停留 3 分钟后仍可安全到达安全屋,超过 3 分钟不行。

示例 2:

复制代码
输入:grid = [[0,0,0,0],[0,1,2,0],[0,2,0,0]]
输出:-1
解释:火会蔓延到所有可达格子,无法安全到达。

示例 3:

复制代码
输入:grid = [[0,0,0],[2,2,0],[1,2,0]]
输出:1000000000
解释:火被墙围住,永远无法到达安全屋路径。

约束:

  • m == grid.length

  • n == grid[i].length

  • 1 <= m, n <= 300

  • grid[i][j] 为 0、1 或 2

  • grid[0][0] == grid[m-1][n-1] == 0

解题思路

核心思想:多源 BFS 求着火时间 + 二分等待时间。

步骤:

  1. 多源 BFS(所有 1 同时出发)求每个格子的着火时间 fire[i][j],不可达记为 INF。
  2. BFS 求人从 (0,0) 到每个格子的最短时间 human[i][j]。
  3. 若人原本到不了终点返回 -1;若起点永远不着火(fire[0][0] == INF)返回 10⁹。
  4. 二分等待分钟数 wait(上界 fire[0][0] - 1):人在时刻 wait 出发,到达中间格子 (r,c) 的实际时刻 = human[r][c] + wait,必须严格小于 fire[r][c];终点允许 <=(同时到达算安全)。用 BFS 验证 wait 是否可行,二分最大可行值。

复杂度:两次 BFS + 二分内 BFS,总 O(m×n × log(maxFire))。

代码实现

java 复制代码
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Queue;

/**
 * 2258. Escape the Spreading Fire 逃离火灾
 *
 * 网格图 grid,0 草地、1 着火的格子、2 墙。一开始在 (0,0),要到达安全屋 (m-1,n-1)。每分钟
 * 可以先移动一步,然后火扩散到相邻的草地(不能穿墙)。返回在初始位置可以停留的最多分钟数,
 * 且停留后仍能安全到达安全屋;无法到达返回 -1;无论停留多久总能到达返回 10⁹。
 * 注意:到达安全屋后火马上到,也算安全到达。
 *
 * 算法思路:
 * 1. 多源 BFS 求每个格子的着火时间 fire[i][j](无法到达的格子记为 INF)。
 * 2. BFS 求人从 (0,0) 到达每个格子的最早时间 human[i][j](不带等待)。
 * 3. 二分"停留分钟数 wait":人在时刻 wait 从起点出发(实际到达时间 human+wait),要满足
 *    对每个中间格子 t < fire[t],且到终点时 t <= fire[终点](安全屋允许同时到达)。
 *    用一条 BFS 验证 wait 是否可行。对 wait 二分上界取 fire[0][0] 或一个充分大的值。
 *
 * 时间复杂度:O(m*n),空间复杂度:O(m*n)
 */
public class Solution {

    private static final int INF = Integer.MAX_VALUE / 2;

    public int maximumMinutes(int[][] grid) {
        int m = grid.length, n = grid[0].length;
        int[][] fire = new int[m][n];
        for (int[] row : fire) {
            Arrays.fill(row, INF);
        }
        Queue<int[]> q = new ArrayDeque<>();
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (grid[i][j] == 1) {
                    fire[i][j] = 0;
                    q.offer(new int[]{i, j});
                }
            }
        }
        bfsFire(fire, q, m, n, grid);

        // 人的可达性(不带等待,纯按最短时间)
        int[][] human = new int[m][n];
        for (int[] row : human) {
            Arrays.fill(row, INF);
        }
        human[0][0] = 0;
        q.clear();
        q.offer(new int[]{0, 0});
        bfsHuman(human, q, m, n, grid);

        // 若人根本到不了终点 / 起点,返回 -1 或 INF 处理
        if (human[m - 1][n - 1] == INF) {
            return -1;
        }
        // 若火根本烧不到终点 / 起点,人可以无限等(只要起点不被烧)
        if (fire[0][0] == INF) {
            return 1000000000;
        }
        int lo = -1, hi = fire[0][0]; // hi 取 fire[0][0]:若停留 fire[0][0] 分钟起点已被烧
        while (lo < hi) {
            int mid = (lo + hi + 1) >>> 1;
            if (canReach(grid, human, fire, m, n, mid)) {
                lo = mid;
            } else {
                hi = mid - 1;
            }
        }
        return lo;
    }

    /** 多源 BFS 扩散火 */
    private void bfsFire(int[][] fire, Queue<int[]> q, int m, int n, int[][] grid) {
        int[][] dirs = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
        while (!q.isEmpty()) {
            int[] cur = q.poll();
            for (int[] d : dirs) {
                int r = cur[0] + d[0], c = cur[1] + d[1];
                if (r < 0 || r >= m || c < 0 || c >= n || grid[r][c] == 2 || fire[r][c] != INF) {
                    continue;
                }
                fire[r][c] = fire[cur[0]][cur[1]] + 1;
                q.offer(new int[]{r, c});
            }
        }
    }

    /** BFS 求人的最早到达时间 */
    private void bfsHuman(int[][] human, Queue<int[]> q, int m, int n, int[][] grid) {
        int[][] dirs = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
        while (!q.isEmpty()) {
            int[] cur = q.poll();
            for (int[] d : dirs) {
                int r = cur[0] + d[0], c = cur[1] + d[1];
                if (r < 0 || r >= m || c < 0 || c >= n || grid[r][c] == 2 || human[r][c] != INF) {
                    continue;
                }
                human[r][c] = human[cur[0]][cur[1]] + 1;
                q.offer(new int[]{r, c});
            }
        }
    }

    /** 验证停留 wait 分钟后从起点出发能否安全到达终点 */
    private boolean canReach(int[][] grid, int[][] human, int[][] fire, int m, int n, int wait) {
        // 起点在 wait 分钟后已被烧,则不可能
        if (wait >= fire[0][0]) {
            return false;
        }
        boolean[][] vis = new boolean[m][n];
        Queue<int[]> q = new ArrayDeque<>();
        q.offer(new int[]{0, 0});
        vis[0][0] = true;
        int[][] dirs = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
        while (!q.isEmpty()) {
            int[] cur = q.poll();
            if (cur[0] == m - 1 && cur[1] == n - 1) {
                return true;
            }
            for (int[] d : dirs) {
                int r = cur[0] + d[0], c = cur[1] + d[1];
                if (r < 0 || r >= m || c < 0 || c >= n || grid[r][c] == 2 || vis[r][c]) {
                    continue;
                }
                int t = human[cur[0]][cur[1]] + 1 + wait; // 实际到达时刻
                if (r == m - 1 && c == n - 1) {
                    // 终点允许与火同时到达
                    if (t <= fire[r][c]) {
                        vis[r][c] = true;
                        q.offer(new int[]{r, c});
                    }
                } else {
                    // 中间格必须在火到达之前
                    if (t < fire[r][c]) {
                        vis[r][c] = true;
                        q.offer(new int[]{r, c});
                    }
                }
            }
        }
        return false;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        // 示例 1:预期 3
        System.out.println(s.maximumMinutes(new int[][]{
                {0, 2, 0, 0, 0, 0, 0},
                {0, 0, 0, 2, 2, 1, 0},
                {0, 2, 0, 0, 1, 2, 0},
                {0, 0, 2, 2, 2, 0, 2},
                {0, 0, 0, 0, 0, 0, 0}}));
        // 示例 2:预期 -1
        System.out.println(s.maximumMinutes(new int[][]{
                {0, 0, 0, 0},
                {0, 1, 2, 0},
                {0, 2, 0, 0}}));
        // 示例 3:预期 1000000000
        System.out.println(s.maximumMinutes(new int[][]{
                {0, 0, 0},
                {2, 2, 0},
                {1, 2, 0}}));
    }
}

复杂度分析

  • 解法:多源 BFS 着火时间 + 二分等待(主提交版本)
  • 时间复杂度:O(m × n × log(maxFire))
  • 空间复杂度:O(m × n)

2259. Remove Digit From Number to Maximize Result(移除指定数字得到的最大结果)

题目描述

给你一个表示某个正整数的字符串 number 和一个字符 digit。从 number 中恰好 移除一个 等于 digit 的字符后,返回按十进制 表示最大 的结果字符串。生成的测试用例保证 digit 在 number 中出现至少一次。

示例 1:

复制代码
输入:number = "123", digit = '3'
输出:"12"
解释:数字串中只有一个 '3',移除后为 "12"。

示例 2:

复制代码
输入:number = "1231", digit = '1'
输出:"231"
解释:移除第一个 '1' 得 "231",移除第二个得 "123",取较大者。

示例 3:

复制代码
输入:number = "551", digit = '5'
输出:"51"
解释:移除任一 '5' 结果都是 "51"。

约束:

  • 2 <= number.length <= 100

  • number 由数字组成

  • digit 是 '0' 到 '9' 的数字

  • digit 在 number 中至少出现一次

解题思路

核心思想 :枚举试删 + 字符串比较。数字串长度相同,字典序比较即数值比较。对每个等于 digit 的位置试删一次,保留字典序最大的结果。

步骤:

  1. 遍历 number,对每个等于 digit 的字符构造 substring(0,i) + substring(i+1)。
  2. 用 compareTo 与当前最优比较,更新。

复杂度:O(n²)(n ≤ 100),空间 O(n)。

进阶写法的贪心 :也可只删『digit 且后一位更大』的第一个位置,否则删最后一次出现的该字符,O(n) 实现;本题枚举更直观。

代码实现

java 复制代码
/**
 * 2259. Remove Digit From Number to Maximize Result 移除指定数字得到的最大结果
 *
 * 给你一个表示某个正整数的字符串 number 和一个字符 digit。从 number 中恰好移除一个等于 digit
 * 的字符后,返回按十进制表示最大的结果字符串。测试用例保证 digit 在 number 中出现至少一次。
 *
 * 算法思路:贪心 + 字符串比较。从左到右扫描,若当前字符等于 digit 且是最后一次出现(或
 * 它后面紧邻的字符更大------实际上对于相同长度的数字串,字典序比较即数值比较),更通用的做法:
 * 对每个等于 digit 的位置试删一次,保留字典序(即数值)最大的结果。因 length 最多 100,
 * O(n²) 完全可行,简单不易错。
 *
 * 时间复杂度:O(n²),空间复杂度:O(n)
 */
public class Solution {

    public String removeDigit(String number, char digit) {
        String best = "";
        for (int i = 0; i < number.length(); i++) {
            if (number.charAt(i) == digit) {
                String cand = number.substring(0, i) + number.substring(i + 1);
                // 字符串比较等价于数值比较(长度相同)
                if (cand.compareTo(best) > 0) {
                    best = cand;
                }
            }
        }
        return best;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        // 示例 1:预期 "12"
        System.out.println(s.removeDigit("123", '3'));
        // 示例 2:预期 "231"
        System.out.println(s.removeDigit("1231", '1'));
        // 示例 3:预期 "51"
        System.out.println(s.removeDigit("551", '5'));
        // 自测:预期 "52"(删大的那一个)
        System.out.println(s.removeDigit("552", '5'));
    }
}

复杂度分析

  • 解法:枚举试删 + 字符串比较(主提交版本)
  • 时间复杂度:O(n²)(n ≤ 100)
  • 空间复杂度:O(n)

2260. Minimum Consecutive Cards to Pick Up(必须拿起的最小连续卡牌数)

题目描述

给你一个整数数组 cards,其中 cards[i] 表示第 i 张卡牌的值 。如果两张卡牌的值相同,则认为这一对卡牌匹配 。返回必须拿起的最小连续卡牌数,以使在拿起的卡牌中有一对匹配的卡牌;如果无法得到一对匹配的卡牌返回 -1。

示例 1:

复制代码
输入:cards = [3,4,2,3,4,7]
输出:4
解释:拿起 [3,4,2,3] 包含一对 3;[4,2,3,4] 也是最优方案。

示例 2:

复制代码
输入:cards = [1,0,5,3]
输出:-1
解释:没有任何值出现两次。

约束:

  • 1 <= cards.length <= 10⁵

  • 0 <= cards[i] <= 10⁶

解题思路

核心思想 :哈希表记录最近下标。用 Map<值, 最近出现下标> 扫描数组,遇到已出现的值,连续区间长度 = i - last + 1,更新全局最小值,然后刷新该值最近下标。若从未遇到重复,返回 -1。

步骤:

  1. Map<Integer,Integer> last 记录每个值最近一次出现的位置。
  2. 遍历 cards:若 last 中存在 cards[i],ans = min(ans, i - prev + 1);更新 last。
  3. 返回 ans 或 -1。

复杂度:O(n) 时间、O(n) 空间。

代码实现

java 复制代码
import java.util.HashMap;
import java.util.Map;

/**
 * 2260. Minimum Consecutive Cards to Pick Up 必须拿起的最小连续卡牌数
 *
 * 给你一个整数数组 cards,cards[i] 表示第 i 张卡牌的值。如果两张卡牌的值相同,则认为这一对卡牌
 * 匹配。返回必须拿起的最小连续卡牌数,使拿起的卡牌中有一对匹配;如果无法得到,返回 -1。
 *
 * 算法思路:滑动窗口 + 哈希表。用 Map<值, 最近出现下标> 记录每个值最近一次出现的位置;
 * 遍历数组,若当前值之前出现过,则窗口大小 = i - last + 1,更新答案最小值;然后更新该值的
 * 最近下标为 i。
 *
 * 时间复杂度:O(n),空间复杂度:O(n)
 */
public class Solution {

    public int minimumCardPickup(int[] cards) {
        Map<Integer, Integer> last = new HashMap<>();
        int ans = Integer.MAX_VALUE;
        for (int i = 0; i < cards.length; i++) {
            Integer prev = last.get(cards[i]);
            if (prev != null) {
                ans = Math.min(ans, i - prev + 1);
            }
            last.put(cards[i], i);
        }
        return ans == Integer.MAX_VALUE ? -1 : ans;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        // 示例 1:预期 4
        System.out.println(s.minimumCardPickup(new int[]{3, 4, 2, 3, 4, 7}));
        // 示例 2:预期 -1
        System.out.println(s.minimumCardPickup(new int[]{1, 0, 5, 3}));
        // 自测:预期 2
        System.out.println(s.minimumCardPickup(new int[]{1, 1}));
    }
}

复杂度分析

  • 解法:哈希表记录最近下标(主提交版本)
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

相关推荐
文丑颜不良啊16 分钟前
LeetCode2321~2340题目解析
java·leetcode
驭渊的小故事1 小时前
Spring AOP 核心概念与通知类型详解
java·后端·spring
vipxieliang1 小时前
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