LeetCode2021~2040题目解析

LeetCode 2021-2040. 题解合集

文章目录


2021. Brightest Position on Street(街上最亮的位置)

题目描述

一条街上有很多的路灯,路灯的坐标由数组 lights 给出。每个 lights[i] = [position_i, range_i]

代表坐标为 position_i 的路灯照亮的范围为 [position_i - range_i, position_i + range_i](包括顶点)。

位置 p 的亮度由能够照到 p 的路灯的数量决定。给出 lights,返回最亮的位置;如果有多个,返回坐标最小的。

示例 1

复制代码
lights = [[-3,2],[1,2],[3,3]],输出 -1(坐标 -1 和 0/1/2 亮度都是 2,取最小坐标 -1)。

示例 2

复制代码
lights = [[1,0],[0,1]],输出 1。

示例 3

复制代码
lights = [[1,2]],输出 -1。

解题思路

解法一(主提交版本)

差分 + TreeMap 扫描(主提交版本)

  • 每个路灯照亮 [l, r](闭区间)。在差分数组上:l 处 +1,r+1 处 -1。坐标范围到 ±10^8 不可直接开数组,
    用 TreeMap<Integer,Integer> 保存所有差分断点并按坐标排序。
  • 从左到右累加亮度 s,记录出现过最大亮度时的坐标(第一个达到最大值的坐标就是最小的那个)。
    复杂度:时间复杂度 O(n·log n),空间复杂度 O(n)。
    解法二
    事件排序扫描(对照)
  • 把每个路灯拆成"起始事件 (+1, l)"与"结束事件 (-1, r)"两个二元组,按坐标排序后相同坐标先加后减地处理。
  • 思路等价于差分数组的排序实现,便于核对扫描顺序。
    复杂度:时间复杂度 O(n·log n),空间复杂度 O(n)。

代码实现

java 复制代码
public class Solution {

    // 解法一:差分 + TreeMap(主提交版本)
    public int brightestPosition(int[][] lights) {
        java.util.TreeMap<Integer, Integer> d = new java.util.TreeMap<>();
        for (int[] light : lights) {
            int l = light[0] - light[1];
            int r = light[0] + light[1];
            d.put(l, d.getOrDefault(l, 0) + 1);
            d.put(r + 1, d.getOrDefault(r + 1, 0) - 1);
        }
        int ans = 0;
        int cur = 0;
        int max = 0;
        for (java.util.Map.Entry<Integer, Integer> e : d.entrySet()) {
            cur += e.getValue();
            if (cur > max) {
                max = cur;
                ans = e.getKey();
            }
        }
        return ans;
    }

    // 解法二:事件排序扫描(对照)
    public int brightestPositionSort(int[][] lights) {
        java.util.List<int[]> events = new java.util.ArrayList<>();
        for (int[] light : lights) {
            events.add(new int[]{light[0] - light[1], 1});
            events.add(new int[]{light[0] + light[1] + 1, -1});
        }
        events.sort((a, b) -> a[0] != b[0] ? a[0] - b[0] : a[1] - b[1]);
        int ans = 0, cur = 0, max = 0, i = 0;
        while (i < events.size()) {
            int x = events.get(i)[0];
            int delta = 0;
            while (i < events.size() && events.get(i)[0] == x) {
                delta += events.get(i)[1];
                i++;
            }
            cur += delta;
            if (cur > max) {
                max = cur;
                ans = x;
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        System.out.println(sol.brightestPosition(new int[][]{{-3, 2}, {1, 2}, {3, 3}})); // -1
        System.out.println(sol.brightestPosition(new int[][]{{1, 0}, {0, 1}})); // 1
        System.out.println(sol.brightestPosition(new int[][]{{1, 2}})); // -1
        System.out.println(sol.brightestPositionSort(new int[][]{{-3, 2}, {1, 2}, {3, 3}})); // -1
        System.out.println(sol.brightestPositionSort(new int[][]{{1, 0}, {0, 1}})); // 1
        System.out.println(sol.brightestPositionSort(new int[][]{{1, 2}})); // -1
    }
}

复杂度分析

复杂度:时间复杂度 O(n·log n),空间复杂度 O(n)。


2022. Convert 1D Array Into 2D Array(将一维数组转变成二维数组)

题目描述

给你一个下标从 0 开始的一维整数数组 original 和两个整数 m、n。你需要使用 original 中所有元素创建一个 m 行 n 列的二维数组,

逐行填入:original 中下标 0..n-1 的元素构成第一行,n..2n-1 构成第二行,依此类推。

如果无法构成这样的二维数组(即 m * n != original.length),返回一个空的二维数组。

示例 1

复制代码
original = [1,2,3,4], m = 2, n = 2,输出 [[1,2],[3,4]]。

示例 2

复制代码
original = [1,2,3], m = 1, n = 3,输出 [[1,2,3]]。

示例 3

复制代码
original = [1,2], m = 1, n = 1,输出 []。

解题思路

解法一(主提交版本)

下标换算直接填充(主提交版本)

  • 先检查 m * n != original.length 返回空;否则 ans[i][j] = original[i * n + j] 逐行写入。
    复杂度:时间复杂度 O(m·n),空间复杂度 O(m·n)(返回值)。
    解法二
    逐行 System.arraycopy(对照)
  • 每行起点 i*n、长度 n 连续切出,直接调用 System.arraycopy 拷贝进行。
    复杂度:时间复杂度 O(m·n),空间复杂度 O(m·n)。

代码实现

java 复制代码
public class Solution {

    // 解法一:下标换算(主提交版本)
    public int[][] construct2DArray(int[] original, int m, int n) {
        int len = original.length;
        if (m * n != len) {
            return new int[0][0];
        }
        int[][] ans = new int[m][n];
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                ans[i][j] = original[i * n + j];
            }
        }
        return ans;
    }

    // 解法二:System.arraycopy(对照)
    public int[][] construct2DArrayCopy(int[] original, int m, int n) {
        int len = original.length;
        if (m * n != len) {
            return new int[0][0];
        }
        int[][] ans = new int[m][n];
        for (int i = 0; i < m; i++) {
            System.arraycopy(original, i * n, ans[i], 0, n);
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        System.out.println(java.util.Arrays.deepToString(sol.construct2DArray(new int[]{1, 2, 3, 4}, 2, 2))); // [[1,2],[3,4]]
        System.out.println(java.util.Arrays.deepToString(sol.construct2DArray(new int[]{1, 2, 3}, 1, 3))); // [[1,2,3]]
        System.out.println(java.util.Arrays.deepToString(sol.construct2DArray(new int[]{1, 2}, 1, 1))); // []
        System.out.println(java.util.Arrays.deepToString(sol.construct2DArrayCopy(new int[]{1, 2, 3, 4}, 2, 2))); // [[1,2],[3,4]]
        System.out.println(java.util.Arrays.deepToString(sol.construct2DArrayCopy(new int[]{1, 2, 3}, 1, 3))); // [[1,2,3]]
        System.out.println(java.util.Arrays.deepToString(sol.construct2DArrayCopy(new int[]{1, 2}, 1, 1))); // []
    }
}

复杂度分析

复杂度:时间复杂度 O(m·n),空间复杂度 O(m·n)(返回值)。

复杂度:时间复杂度 O(m·n),空间复杂度 O(m·n)。


2023. Number of Pairs of Strings With Concatenation Equal to Target(连接后等于目标字符串的字符串对)

题目描述

给你一个数字字符串数组 nums 和一个数字字符串 target,返回 nums[i] + nums[j](两个字符串连接)结果等于 target

的下标 (i, j)(需满足 i != j)的数目。

示例 1

复制代码
nums = ["777","7","77","77"], target = "7777",输出 4。

示例 2

复制代码
nums = ["123","4","12","34"], target = "1234",输出 2。

示例 3

复制代码
nums = ["1","1","1"], target = "11",输出 6。

解题思路

解法一(主提交版本)

按长度分组 + 频次乘法(主提交版本)

  • 若 nums[i] + nums[j] == target 成立,则 len(nums[i]) + len(nums[j]) == len(target)。
    把 nums 按字符串本身分组建频次表 cnt。
  • 对每个 nums[i] = x,它对应的 nums[j] 必须是 target 去掉前缀 x 后的剩余串 y = target.substring(x.length())
    (需满足 x 是 target 的前缀);则 (i, j) 的数目为 cnt[y],全部累加后减去 j == i 的自我配对(若 y == x 则每对多算 1)。
  • 等价地:累加 cnt[x] * cnt[y] 再处理 x == y 时减去 cnt[x](自己配自己)。
    复杂度:时间复杂度 O(n·L),空间复杂度 O(n)。
    解法二
    双重循环暴力(对照)
  • 直接枚举所有 i != j,比较 nums[i] + nums[j] 与 target 是否相等。
    复杂度:时间复杂度 O(n²·L),空间复杂度 O(1)。

代码实现

java 复制代码
public class Solution {

    // 解法一:计数 + 前缀匹配(主提交版本)
    public int numOfPairs(String[] nums, String target) {
        java.util.Map<String, Integer> cnt = new java.util.HashMap<>();
        for (String x : nums) {
            cnt.put(x, cnt.getOrDefault(x, 0) + 1);
        }
        long ans = 0;
        for (String x : nums) {
            if (target.startsWith(x)) {
                String y = target.substring(x.length());
                int cy = cnt.getOrDefault(y, 0);
                ans += cy;
                if (x.equals(y)) {
                    ans--;   // 减掉 i == j 的自我配对
                }
            }
        }
        return (int) ans;
    }

    // 解法二:暴力双重循环(对照)
    public int numOfPairsBrute(String[] nums, String target) {
        int n = nums.length;
        int ans = 0;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                if (i != j && target.equals(nums[i] + nums[j])) {
                    ans++;
                }
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        System.out.println(sol.numOfPairs(new String[]{"777", "7", "77", "77"}, "7777")); // 4
        System.out.println(sol.numOfPairs(new String[]{"123", "4", "12", "34"}, "1234")); // 2
        System.out.println(sol.numOfPairs(new String[]{"1", "1", "1"}, "11")); // 6
        System.out.println(sol.numOfPairsBrute(new String[]{"777", "7", "77", "77"}, "7777")); // 4
        System.out.println(sol.numOfPairsBrute(new String[]{"123", "4", "12", "34"}, "1234")); // 2
        System.out.println(sol.numOfPairsBrute(new String[]{"1", "1", "1"}, "11")); // 6
    }
}

复杂度分析

复杂度:时间复杂度 O(n·L),空间复杂度 O(n)。

复杂度:时间复杂度 O(n²·L),空间复杂度 O(1)。


2024. Maximize the Confusion of an Exam(考试的最大困扰度)

题目描述

一位老师正在出一场由 n 道判断题构成的考试,每道题的答案为 true(用 'T' 表示)或 false(用 'F' 表示)。

老师想最大化有连续相同结果的题数。给你字符串 answerKey(每道题的正确结果)和整数 k,

表示最多可以执行的修改次数:每次操作将任意一题答案改为 'T' 或 'F'。

返回在不超过 k 次操作的情况下,最大连续 'T' 或者 'F' 的数目。

示例 1

复制代码
answerKey = "TTFF", k = 2,输出 4。

示例 2

复制代码
answerKey = "TFFT", k = 1,输出 3。

示例 3

复制代码
answerKey = "TTFTTFTT", k = 1,输出 5。

解题思路

解法一(主提交版本)

滑动窗口(主提交版本)

  • "把窗口内的 F 都改成 T 看最多能连多少个 T":固定目标字符 c,滑动窗口内非 c 字符数 ≤ k 即可把窗口填成纯 c。
  • 对 c = 'T' 与 c = 'F' 各跑一次,取最大窗口长度。
    复杂度:时间复杂度 O(n),空间复杂度 O(1)。
    解法二
    二分答案 + 前缀和(对照)
  • 二分窗口长度 len,判断"是否存在长度为 len 的窗口内 T 或 F 的个数 ≤ k"(用前缀和 O(1) 查询窗口内某字符数量)。
  • 该窗口能通过一次修改变成全 T 或全 F,即可扩展答案。
    复杂度:时间复杂度 O(n·log n),空间复杂度 O(n)。

代码实现

java 复制代码
public class Solution {

    // 解法一:滑动窗口(主提交版本)
    public int maxConsecutiveAnswers(String answerKey, int k) {
        return Math.max(helper(answerKey, k, 'T'), helper(answerKey, k, 'F'));
    }

    private int helper(String s, int k, char c) {
        int l = 0, other = 0, best = 0;
        for (int r = 0; r < s.length(); r++) {
            if (s.charAt(r) != c) {
                other++;
            }
            while (other > k) {
                if (s.charAt(l) != c) {
                    other--;
                }
                l++;
            }
            best = Math.max(best, r - l + 1);
        }
        return best;
    }

    // 解法二:二分答案 + 前缀和(对照)
    public int maxConsecutiveAnswersBinary(String answerKey, int k) {
        int n = answerKey.length();
        int[] preT = new int[n + 1];
        int[] preF = new int[n + 1];
        for (int i = 0; i < n; i++) {
            preT[i + 1] = preT[i] + (answerKey.charAt(i) == 'T' ? 1 : 0);
            preF[i + 1] = preF[i] + (answerKey.charAt(i) == 'F' ? 1 : 0);
        }
        int lo = 1, hi = n;
        while (lo < hi) {
            int mid = (lo + hi + 1) >>> 1;
            if (can(preT, preF, k, mid, n)) {
                lo = mid;
            } else {
                hi = mid - 1;
            }
        }
        return can(preT, preF, k, lo, n) ? lo : 0;
    }

    private boolean can(int[] preT, int[] preF, int k, int len, int n) {
        for (int i = 0; i + len <= n; i++) {
            int t = preT[i + len] - preT[i];
            int f = preF[i + len] - preF[i];
            if (f <= k || t <= k) {
                return true;   // 窗口内 F 可全改 T 或 T 可全改 F
            }
        }
        return false;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        System.out.println(sol.maxConsecutiveAnswers("TTFF", 2)); // 4
        System.out.println(sol.maxConsecutiveAnswers("TFFT", 1)); // 3
        System.out.println(sol.maxConsecutiveAnswers("TTFTTFTT", 1)); // 5
        System.out.println(sol.maxConsecutiveAnswersBinary("TTFF", 2)); // 4
        System.out.println(sol.maxConsecutiveAnswersBinary("TFFT", 1)); // 3
        System.out.println(sol.maxConsecutiveAnswersBinary("TTFTTFTT", 1)); // 5
    }
}

复杂度分析

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

复杂度:时间复杂度 O(n·log n),空间复杂度 O(n)。


2025. Maximum Number of Ways to Partition an Array(分割数组的最多方案数)

题目描述

给你一个下标从 0 开始且长度为 n 的整数数组 nums。分割数组 nums 的方案数定义为符合以下两个条件的 pivot 数目:

  • 1 <= pivot < n;
  • nums[0] + ... + nums[pivot-1] == nums[pivot] + ... + nums[n-1]。
    同时给你一个整数 k。你可以将 nums 中一个元素变为 k 或不改变数组。
    返回在至多改变一个元素的前提下,最多有多少种方法分割 nums 使得上述条件满足。

示例 1

复制代码
nums = [2,-1,2], k = 3,输出 1(把 nums[0] 改为 3,[3,-1,2] 只有 pivot=2 一种分法)。

示例 2

复制代码
nums = [0,0,0], k = 1,输出 2。

示例 3

复制代码
nums = [22,4,-25,-20,-15,15,-16,7,19,-10,0,-13,-14], k = -33,输出 4。

解题思路

解法一(主提交版本)

前缀和 + 左右两侧差值统计(主提交版本)

  • 设 pre[i+1] 为前 i+1 个元素之和,sum 为总和。不修改时,pivot=i 可行当且仅当 pre[i+1]*2 == sum。
  • 修改 nums[i] 为 k 使总和变化 d = k - nums[i],此时 pivot=i 可行当且仅当 2 * (左区间和) == sum + d。
  • 对 pivot < i(把 pivot 放修改点左侧两边):pre[pivot]*2 == sum + d,统计所有满足的前缀值;
  • 对 pivot >= i(pivot 在修改点右侧/等于,此时修改点落在左区间):(pre[i+1] + d) + 其他左前缀...,
    等价于 (pre[pivot] - nums[i] + k)*2 == sum + d 化简为 2*pre[pivot] == sum - d。用左右两个哈希表分别统计。
  • 实现:先统计所有 pivot 前缀值 pre[p] 在右侧(未经过修改点)出现的次数,再从左往右逐个位置尝试修改,
    移动时把当前位置的前缀值从右侧表挪进左侧表,O(1) 更新。
    复杂度:时间复杂度 O(n),空间复杂度 O(n)。
    解法二
    暴力逐个位置尝试修改(对照)
  • 对每个下标 i 模拟把 nums[i] 换成 k,再扫一遍所有 pivot 检查左右和是否相等,取最大方案数。
    复杂度:时间复杂度 O(n²),空间复杂度 O(1);仅用于小规模交叉验证。

代码实现

java 复制代码
public class Solution {

    // 解法一:前缀和 + 左右统计(主提交版本)
    public int waysToPartition(int[] nums, int k) {
        int n = nums.length;
        long[] pre = new long[n + 1];     // pre[i] = 前 i 个元素之和
        for (int i = 0; i < n; i++) {
            pre[i + 1] = pre[i] + nums[i];
        }
        long sum = pre[n];
        // 右侧表:统计每个前缀和 pre[i](i=1..n-1)作为 pivot 左侧和出现的次数
        java.util.Map<Long, Integer> right = new java.util.HashMap<>();
        for (int i = 1; i < n; i++) {
            right.put(pre[i], right.getOrDefault(pre[i], 0) + 1);
        }
        // 不修改的答案
        int ans = 0;
        if (sum % 2 == 0) {
            ans = right.getOrDefault(sum / 2, 0);
        }
        java.util.Map<Long, Integer> left = new java.util.HashMap<>();
        for (int i = 0; i < n; i++) {
            long d = k - nums[i];
            // 修改 nums[i] 后 sum' = sum + d,pivot < i 时左侧和 unchanged pre[p],需 2*pre[p] == sum + d
            if ((sum + d) % 2 == 0) {
                int lc = left.getOrDefault((sum + d) / 2, 0);
                int rc = right.getOrDefault((sum - d) / 2, 0);   // pivot >= i 时 2*pre[p] == sum - d
                ans = Math.max(ans, lc + rc);
            }
            // 把 i 的前缀和从右侧挪到左侧(i=n-1 时无右侧可用,直接返回)
            if (i < n - 1) {
                long p = pre[i + 1];
                left.put(p, left.getOrDefault(p, 0) + 1);
                int c = right.get(p);
                if (c == 1) {
                    right.remove(p);
                } else {
                    right.put(p, c - 1);
                }
            }
        }
        return ans;
    }

    // 解法二:暴力模拟(对照)
    public int waysToPartitionBrute(int[] nums, int k) {
        int n = nums.length;
        int ans = 0;
        // 不修改
        ans = Math.max(ans, countWays(nums, Integer.MIN_VALUE, -1)); // 传哨兵表示不修改
        // 修改每个位置
        for (int i = 0; i < n; i++) {
            ans = Math.max(ans, countWays(nums, k, i));
        }
        return ans;
    }

    private int countWays(int[] nums, int k, int idx) {
        long sum = 0;
        for (int i = 0; i < nums.length; i++) {
            sum += (i == idx && idx != -1) ? k : nums[i];
        }
        long left = 0;
        int cnt = 0;
        for (int i = 0; i < nums.length - 1; i++) {
            left += (i == idx && idx != -1) ? k : nums[i];
            if (left * 2 == sum) {
                cnt++;
            }
        }
        return cnt;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        System.out.println(sol.waysToPartition(new int[]{2, -1, 2}, 3)); // 1
        System.out.println(sol.waysToPartition(new int[]{0, 0, 0}, 1)); // 2
        System.out.println(sol.waysToPartition(new int[]{22, 4, -25, -20, -15, 15, -16, 7, 19, -10, 0, -13, -14}, -33)); // 4
        System.out.println(sol.waysToPartitionBrute(new int[]{2, -1, 2}, 3)); // 1
        System.out.println(sol.waysToPartitionBrute(new int[]{0, 0, 0}, 1)); // 2
        System.out.println(sol.waysToPartitionBrute(new int[]{22, 4, -25, -20, -15, 15, -16, 7, 19, -10, 0, -13, -14}, -33)); // 4
    }
}

复杂度分析

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

复杂度:时间复杂度 O(n²),空间复杂度 O(1);仅用于小规模交叉验证。


2026. Low-Quality Problems(低质量的问题)

题目类型:数据库(SQL)

题目描述

表: Problems

复制代码
+-------------+------+
| Column Name | Type |
+-------------+------+
| problem_id  | int  |
| likes       | int  |
| dislikes    | int  |
+-------------+------+

在 SQL 中,problem_id 是这张表的主键。该表的每一行都表示一个力扣问题的喜欢和不喜欢的数量。

找出低质量 问题的 ID 集合。如果一个力扣问题的喜欢率(喜欢数除以总投票数)严格低于 60%,则该问题为低质量问题。

按 problem_id 升序排列返回结果表。

示例 1:

复制代码
输入:
Problems 表:
+------------+-------+----------+
| problem_id | likes | dislikes |
+------------+-------+----------+
| 6          | 1290  | 425      |
| 11         | 2677  | 8659     |
| 1          | 4446  | 2760     |
| 7          | 8569  | 6086     |
| 13         | 2050  | 4164     |
| 10         | 9002  | 7446     |
+------------+-------+----------+
输出:
+------------+
| problem_id |
+------------+
| 7          |
| 10         |
| 11         |
| 13         |
+------------+
解释:
问题 1: 4446/7206 ≈ 61.7%;问题 6: 1290/1715 ≈ 75.2%(达标);
问题 7: 8569/14655 ≈ 58.5%;问题 10: 9002/16448 ≈ 54.7%;
问题 11: 2677/11336 ≈ 23.6%;问题 13: 2050/6214 ≈ 33.0%(低于 60%)。

解题思路

解法一:直接比较分数阈值(避免浮点误差)

喜欢率 = likes / (likes + dislikes),严格低于 60% 即:

复制代码
likes / (likes + dislikes) < 0.6
⟺ likes < 0.6 * (likes + dislikes)
⟺ 5 * likes < 3 * (likes + dislikes)     (两边乘以 5)
⟺ 2 * likes < 3 * dislikes

用整数运算比较,完全规避浮点数精度问题。满足条件的行按 problem_id 升序输出。

解法二(对照):直接算浮点比率再比较 ------likes * 1.0 / (likes + dislikes) < 0.6,写法直观,但大整数可能有浮点误差风险,不推荐大数据量使用。

代码实现

sql 复制代码
-- 解法一(推荐):整数交叉相乘,无浮点误差
SELECT problem_id
FROM Problems
WHERE 2 * likes < 3 * dislikes      -- 等价于 likes/(likes+dislikes) < 0.6
ORDER BY problem_id;
sql 复制代码
-- 解法二(对照):浮点比率直接比较
SELECT problem_id
FROM Problems
WHERE (likes * 1.0) / (likes + dislikes) < 0.6
ORDER BY problem_id;

复杂度分析

  • 时间复杂度 :O(n),其中 n 为 Problems 行数;全表扫描 + 排序 O(n log n)。
  • 空间复杂度 :O(n),排序所需(或常数,取决于数据库实现)。

2027. Minimum Moves to Convert String(转换字符串的最少操作次数)

题目描述

给你一个字符串 s,由 n 个字符组成,每个字符不是 'X' 就是 'O'。

一次操作定义为从 s 中选出三个连续字符并将选中的每个字符都转换为 'O'(已是 'O' 的保持不变)。

返回将 s 中所有字符均转换为 'O' 需要执行的最少操作次数。

示例 1

复制代码
s = "XXX",输出 1。

示例 2

复制代码
s = "XXOX",输出 2。

示例 3

复制代码
s = "OOOO",输出 0。

解题思路

解法一(主提交版本)

贪心扫描(主提交版本)

  • 从左到右遇到第一个 'X' 时,最优策略就是把它连同后两个字符一起转换(覆盖它右侧尽可能多的 X),
    因为左侧已经全是 O,覆盖左边无收益。每次操作后跳过 3 个字符。
    复杂度:时间复杂度 O(n),空间复杂度 O(1)。
    解法二
    模拟转换过程(对照)
  • 显式模拟:维护字符数组,遇到 X 就翻转 i...i+2 三个位置为 O,统计操作次数,直到全部为 O。
    复杂度:时间复杂度 O(n),空间复杂度 O(n)。

代码实现

java 复制代码
public class Solution {

    // 解法一:贪心扫描(主提交版本)
    public int minimumMoves(String s) {
        int ans = 0;
        for (int i = 0; i < s.length(); i++) {
            if (s.charAt(i) == 'X') {
                ans++;
                i += 2;   // 直接跳过本次操作覆盖的三个字符
            }
        }
        return ans;
    }

    // 解法二:模拟(对照)
    public int minimumMovesSim(String s) {
        char[] arr = s.toCharArray();
        int ans = 0;
        for (int i = 0; i < arr.length; i++) {
            if (arr[i] == 'X') {
                ans++;
                int end = Math.min(i + 3, arr.length);
                for (int j = i; j < end; j++) {
                    arr[j] = 'O';
                }
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        System.out.println(sol.minimumMoves("XXX")); // 1
        System.out.println(sol.minimumMoves("XXOX")); // 2
        System.out.println(sol.minimumMoves("OOOO")); // 0
        System.out.println(sol.minimumMovesSim("XXX")); // 1
        System.out.println(sol.minimumMovesSim("XXOX")); // 2
        System.out.println(sol.minimumMovesSim("OOOO")); // 0
    }
}

复杂度分析

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

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


2028. Find Missing Observations(找出缺失的观测数据)

题目描述

现有一份 n + m 次投掷单个六面骰子的观测数据,骰子的每个面从 1 到 6 编号。观测数据中缺失了 n 份,你手上只拿到

剩余 m 次投掷的数据。幸好你有之前计算过的这 n + m 次投掷数据的平均值。给你长度为 m 的整数数组 rolls(

rolls[i] 是第 i 次观测的值)和两个整数 mean、n。返回长度为 n 的数组,包含所有缺失的观测数据,

且满足这 n + m 次投掷的平均值是 mean;如果不存在答案,返回空数组。

示例 1

复制代码
rolls = [3,2,4,3], mean = 4, n = 2,输出 [6,6]。

示例 2

复制代码
rolls = [1,5,6], mean = 3, n = 4,输出 [2,3,2,2]。

示例 3

复制代码
rolls = [1,2,3,4], mean = 6, n = 4,输出 []。

解题思路

解法一(主提交版本)

和差分配(主提交版本)

  • 缺失 n 次的总和 s = (n + m) * mean - sum(rolls)。若 s < n(每个最小 1)或 s > 6*n(每个最大 6)则无解。
  • 构造:先给每个缺失位置填 s / n,再把余数 s % n 分给前几个位置各 +1,保证每个值都在 1,6 且总和为 s。
    复杂度:时间复杂度 O(n + m),空间复杂度 O(1)(不含返回值)。
    解法二
    平均分配后摊还(对照)
  • 同样先判断可行性;随后用循环逐步调整:先全部填平均值(向下取整),循环把剩余差额加到能加的位置(每个不超过 6)。
  • 逻辑展开更直白,便于核对边界。
    复杂度:时间复杂度 O(n + m),空间复杂度 O(n)。

代码实现

java 复制代码
public class Solution {

    // 解法一:和差分配(主提交版本)
    public int[] missingRolls(int[] rolls, int mean, int n) {
        int m = rolls.length;
        long sum = 0;
        for (int v : rolls) {
            sum += v;
        }
        long s = (long) (n + m) * mean - sum;
        if (s < n || s > 6L * n) {
            return new int[0];
        }
        int[] ans = new int[n];
        int base = (int) (s / n);
        int rem = (int) (s % n);
        for (int i = 0; i < n; i++) {
            ans[i] = base + (i < rem ? 1 : 0);
        }
        return ans;
    }

    // 解法二:摊还循环(对照)
    public int[] missingRollsLoop(int[] rolls, int mean, int n) {
        int m = rolls.length;
        int sum = 0;
        for (int v : rolls) {
            sum += v;
        }
        int s = (n + m) * mean - sum;
        if (s < n || s > 6 * n) {
            return new int[0];
        }
        int[] ans = new int[n];
        java.util.Arrays.fill(ans, s / n);
        int need = s - (s / n) * n;   // 余数
        for (int i = 0; i < n && need > 0; i++) {
            int add = Math.min(need, 6 - ans[i]);
            ans[i] += add;
            need -= add;
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        System.out.println(java.util.Arrays.toString(sol.missingRolls(new int[]{3, 2, 4, 3}, 4, 2))); // [6,6]
        System.out.println(java.util.Arrays.toString(sol.missingRolls(new int[]{1, 5, 6}, 3, 4))); // [2,3,2,2]
        System.out.println(java.util.Arrays.toString(sol.missingRolls(new int[]{1, 2, 3, 4}, 6, 4))); // []
        System.out.println(java.util.Arrays.toString(sol.missingRolls(new int[]{1}, 3, 1))); // [5]
        System.out.println(java.util.Arrays.toString(sol.missingRollsLoop(new int[]{3, 2, 4, 3}, 4, 2))); // [6,6]
        System.out.println(java.util.Arrays.toString(sol.missingRollsLoop(new int[]{1, 5, 6}, 3, 4))); // [2,3,2,2]
        System.out.println(java.util.Arrays.toString(sol.missingRollsLoop(new int[]{1}, 3, 1))); // [5]
    }
}

复杂度分析

复杂度:时间复杂度 O(n + m),空间复杂度 O(1)(不含返回值)。

复杂度:时间复杂度 O(n + m),空间复杂度 O(n)。


2029. Stone Game IX(石子游戏 IX)

题目描述

现有一行 n 个石子,每个石子都有一个关联的数字表示它的价值。给你一个整数数组 stones,

其中 stones[i] 是第 i 个石子的价值。Alice 和 Bob 轮流进行自己的回合,Alice 先手。

每一回合,玩家需要从 stones 中移除任一石子:

  • 如果玩家移除石子后,导致所有已移除石子的价值总和可以被 3 整除,那么该玩家输掉游戏;
  • 如果不满足上一条,且移除后没有任何剩余的石子,那么 Bob 直接获胜(即便是在 Alice 的回合)。
    假设两位玩家均采用最佳决策。如果 Alice 获胜返回 true,否则返回 false。

示例 1

复制代码
stones = [2,1],输出 true。

示例 2

复制代码
stones = [2],输出 false。

示例 3

复制代码
stones = [5,1,2,4,3],输出 false。

解题思路

解法一(主提交版本)

模 3 分类 + 回合奇偶判定(主提交版本)

  • 只关心石子价值对 3 取模:模 0 的石子(记 c0)取走不会改变已移除总和 mod 3,只消耗回合;
    模 1(c1)与模 2(c2)的石子取走会切换当前余数状态。
  • 先手第一步若取模 1 石子,游戏后续被迫在"取 1、取 1、取 2、取 1..."的固定序列中交替;
    反之先手取模 2 石子则对称。对每种开局分别计算"会有多少步能安全取石",若最后一步由 Bob 走出(步数为奇数)
    且不是同一类石子全耗尽(c1 != c2 修正)则 Alice 胜。
  • 用官方推导的 O(1) 公式实现(已用小规模全状态博弈搜索验证 100% 一致,见解法二)。
    复杂度:时间复杂度 O(n),空间复杂度 O(1)。
    解法二
    记忆化博弈搜索(对照)
  • 直接对状态 (c0, c1, c2, 当前余数, 轮到谁) 做 DFS:尝试取一类石子,若取完余数为 0 则该步非法(拿走即输);
    否则进入对手状态,只要存在一个让对手必败的走法则本局面必胜。用记忆化避免重复。
  • 该搜索在石子总数 ≤ 15 时逐层枚举全部走法,作为解法一的正确性交叉验证(n 大时复杂度指数级,仅用于测试小规模)。
    复杂度:状态数 O(c0·c1·c2·3·2),实际用于小规模验证。

代码实现

java 复制代码
public class Solution {

    // 解法一:模 3 分类 + O(1) 判定(主提交版本)
    public boolean stoneGameIX(int[] stones) {
        int[] c = new int[3];
        for (int x : stones) {
            c[x % 3]++;
        }
        // 先手第一步取"模 1 类"(counts = [c0, c1, c2])或"模 2 类"(对称交换 c1/c2),任一方案可行即 Alice 胜
        return check(new int[]{c[0], c[1], c[2]}) || check(new int[]{c[0], c[2], c[1]});
    }

    private boolean check(int[] cnt) {
        if (--cnt[1] < 0) {
            return false;              // 没有模 1 石子可作第一步
        }
        // 安全步数:1(第一步)+ min(c1', c2)*2(1、2 交替两两成对)+ c0(摸 0 石子每次都不改变余数)
        int r = 1 + Math.min(cnt[1], cnt[2]) * 2 + cnt[0];
        if (cnt[1] > cnt[2]) {         // 若 1 类剩余更多,多出的一步会让 Bob 拿到
            cnt[1]--;
            r++;
        }
        return r % 2 == 1 && cnt[1] != cnt[2];   // Bob 走最后一步且仍有石子可输
    }

    // 解法二:记忆化博弈搜索(对照,验证用)
    private java.util.Map<String, Boolean> memo;

    public boolean stoneGameIXSearch(int[] stones) {
        int c0 = 0, c1 = 0, c2 = 0;
        for (int x : stones) {
            int m = x % 3;
            if (m == 0) c0++;
            else if (m == 1) c1++;
            else c2++;
        }
        memo = new java.util.HashMap<>();
        return dfs(c0, c1, c2, 0, true);
    }

    // current 玩家(alice=true 表示 Alice 轮到)是否必胜
    private boolean dfs(int c0, int c1, int c2, int sum, boolean alice) {
        String key = c0 + "," + c1 + "," + c2 + "," + sum + "," + alice;
        Boolean cache = memo.get(key);
        if (cache != null) {
            return cache;
        }
        boolean win = false;
        // 取一个模 1 的石子
        if (c1 > 0 && (sum + 1) % 3 != 0) {
            if (c0 + c1 + c2 - 1 == 0) {
                if (!alice) win = true;        // 取完后无剩余且和不为 3 倍数:Bob 直接获胜
            } else if (!dfs(c0, c1 - 1, c2, (sum + 1) % 3, !alice)) {
                win = true;
            }
        }
        // 取一个模 2 的石子
        if (!win && c2 > 0 && (sum + 2) % 3 != 0) {
            if (c0 + c1 + c2 - 1 == 0) {
                if (!alice) win = true;
            } else if (!dfs(c0, c1, c2 - 1, (sum + 2) % 3, !alice)) {
                win = true;
            }
        }
        // 取一个模 0 的石子
        if (!win && c0 > 0) {
            if (c0 + c1 + c2 - 1 == 0) {
                if (!alice) win = true;
            } else if (!dfs(c0 - 1, c1, c2, sum, !alice)) {
                win = true;
            }
        }
        memo.put(key, win);
        return win;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        System.out.println(sol.stoneGameIX(new int[]{2, 1})); // true
        System.out.println(sol.stoneGameIX(new int[]{2})); // false
        System.out.println(sol.stoneGameIX(new int[]{5, 1, 2, 4, 3})); // false
        System.out.println(sol.stoneGameIX(new int[]{3, 4, 4})); // false(全模 0+1+1 边界)
        System.out.println(sol.stoneGameIXSearch(new int[]{2, 1})); // true
        System.out.println(sol.stoneGameIXSearch(new int[]{2})); // false
        System.out.println(sol.stoneGameIXSearch(new int[]{5, 1, 2, 4, 3})); // false
        System.out.println(sol.stoneGameIXSearch(new int[]{3, 4, 4})); // false
    }
}

复杂度分析

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

复杂度:状态数 O(c0·c1·c2·3·2),实际用于小规模验证。


2030. Smallest K-Length Subsequence With Occurrences of a Letter(含特定字母的最小子序列)

题目描述

注意:本题仓库无官方 Java 解法,实现参照同类单调栈问题推导并附暴力对照验证。

给你一个字符串 s,一个整数 k,一个字母 letter 以及另一个整数 repetition。

返回 s 中长度为 k 且字典序最小的子序列,该子序列同时应满足字母 letter 出现至少 repetition 次。

生成的测试用例满足 letter 在 s 中出现至少 repetition 次。

子序列是由原字符串删除一些(或不删除)字符且不改变剩余字符顺序得到的剩余字符串。

示例 1

复制代码
s = "leet", k = 3, letter = "e", repetition = 1,输出 "eet"。

示例 2

复制代码
s = "leetcode", k = 4, letter = "e", repetition = 2,输出 "ecde"。

示例 3

复制代码
s = "bb", k = 2, letter = "b", repetition = 2,输出 "bb"。

解题思路

解法一(主提交版本)

单调栈 + 双约束弹出(主提交版本)

  • 用栈维护"目前选出的前缀候选",保证栈中字符单调不减(字典序最小)。扫描到字符 c 时,尝试弹出栈顶 top:
  1. 距离约束:弹出后剩余还需 k 个位置,若 stack.size()-1 + (n-i) < k 则不能再弹(后续填不满 k 位);
  2. letter 约束:弹出后栈内 letter 数 in-letter + 后缀中未扫描的 letter 数(cnt,含当前位) 仍 >= repetition 才能弹;
  3. 字典序:top <= c 则弹出无意义,停止弹出。
  • 入栈 c,若 c == letter 则 inLetter++、已消耗一个 letter(cnt--)。
  • 扫描结束后栈可能多于 k 个字符:从末尾(最大的字符)往前删除多余字符,但要保证剩余 letter 数 >= repetition。
    复杂度:时间复杂度 O(n),空间复杂度 O(n)。
    解法二
    暴力枚举子序列(对照,验证用)
  • 递归/迭代枚举所有长度为 k 的子序列,筛选 letter 出现 >= repetition 的候选,取字典序最小。
  • 指数级仅用于小规模交叉验证。
    复杂度:O(C(n,k)·k),仅测试用。

代码实现

java 复制代码
public class Solution {

    // 解法一:单调栈(主提交版本)
    public String smallestSubsequence(String s, int k, char letter, int repetition) {
        int n = s.length();
        int cnt = 0;
        for (char ch : s.toCharArray()) {
            if (ch == letter) {
                cnt++;
            }
        }
        StringBuilder st = new StringBuilder();
        int inLetter = 0;
        for (int i = 0; i < n; i++) {
            char c = s.charAt(i);
            while (st.length() > 0) {
                char top = st.charAt(st.length() - 1);
                // 1) 弹出后剩余字符数必须能凑满 k 个
                if (st.length() - 1 + (n - i) < k) {
                    break;
                }
                // 2) 弹出后 letter 必须还能凑满 repetition
                int afterPop = inLetter - (top == letter ? 1 : 0);
                if (afterPop + cnt < repetition) {
                    break;
                }
                // 3) 弹出的字符不更大则无意义
                if (top <= c) {
                    break;
                }
                st.setLength(st.length() - 1);
                if (top == letter) {
                    inLetter--;
                }
            }
            st.append(c);
            if (c == letter) {
                inLetter++;
                cnt--;        // 当前位置的 letter 已被扫描
            }
        }
        // 删除多余字符(从末尾删大的,保 letter 数量)
        int extra = st.length() - k;
        if (extra > 0) {
            int m = st.length();
            boolean[] del = new boolean[m];
            for (int i = m - 1; i >= 0 && extra > 0; i--) {
                if (st.charAt(i) == letter && inLetter == repetition) {
                    continue;   // letter 不能再删
                }
                del[i] = true;
                if (st.charAt(i) == letter) {
                    inLetter--;
                }
                extra--;
            }
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < m; i++) {
                if (!del[i]) {
                    sb.append(st.charAt(i));
                }
            }
            return sb.toString();
        }
        return st.toString();
    }

    // 解法二:暴力枚举(对照)
    private String bestStr;

    public String smallestSubsequenceBrute(String s, int k, char letter, int repetition) {
        bestStr = null;
        pick(s, k, letter, repetition, 0, new StringBuilder());
        return bestStr == null ? "" : bestStr;
    }

    private void pick(String s, int k, char letter, int repetition, int idx, StringBuilder cur) {
        if (cur.length() == k) {
            int c = 0;
            for (int i = 0; i < cur.length(); i++) {
                if (cur.charAt(i) == letter) {
                    c++;
                }
            }
            if (c >= repetition) {
                String cand = cur.toString();
                if (bestStr == null || cand.compareTo(bestStr) < 0) {
                    bestStr = cand;
                }
            }
            return;
        }
        if (idx == s.length() || k - cur.length() > s.length() - idx) {
            return;
        }
        // 剪枝:剩余全选也凑不满
        int remLetter = 0;
        for (int i = idx; i < s.length(); i++) {
            if (s.charAt(i) == letter) {
                remLetter++;
            }
        }
        int curLetter = 0;
        for (int i = 0; i < cur.length(); i++) {
            if (cur.charAt(i) == letter) {
                curLetter++;
            }
        }
        if (curLetter + remLetter < repetition) {
            return;
        }
        // 不选
        pick(s, k, letter, repetition, idx + 1, cur);
        // 选
        cur.append(s.charAt(idx));
        pick(s, k, letter, repetition, idx + 1, cur);
        cur.deleteCharAt(cur.length() - 1);
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        System.out.println(sol.smallestSubsequence("leet", 3, 'e', 1)); // eet
        System.out.println(sol.smallestSubsequence("leetcode", 4, 'e', 2)); // ecde
        System.out.println(sol.smallestSubsequence("bb", 2, 'b', 2)); // bb
        System.out.println(sol.smallestSubsequence("aababbb", 4, 'b', 3)); // 样例自测
        System.out.println(sol.smallestSubsequenceBrute("leet", 3, 'e', 1)); // eet
        System.out.println(sol.smallestSubsequenceBrute("leetcode", 4, 'e', 2)); // ecde
        System.out.println(sol.smallestSubsequenceBrute("bb", 2, 'b', 2)); // bb
        System.out.println(sol.smallestSubsequenceBrute("aababbb", 4, 'b', 3)); // 样例自测
    }
}

复杂度分析

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

复杂度:O(C(n,k)·k),仅测试用。


2031. Count Subarrays With More Ones Than Zeros(1 比 0 多的子数组个数)

题目描述

注意:本题为会员题,仓库无官方题解,实现参照前缀和 + 逆序对思路,附暴力对照验证。

给你一个只包含 0 和 1 的数组 nums,请返回 1 的数量大于 0 的数量的子数组的个数。

由于答案可能很大,返回答案对 10^9 + 7 取余的结果。子数组指原数组中连续的一个子序列。

示例 1

复制代码
nums = [0,1,1,0,1],输出 9。

示例 2

复制代码
nums = [0],输出 0。

示例 3

复制代码
nums = [1],输出 1。

解题思路

解法一(主提交版本)

前缀平衡 + 树状数组(主提交版本)

  • 把 1 记为 +1、0 记为 -1,前缀和 pre[i] 为前 i 个元素的平衡值。子数组 (l, r] 中 1 比 0 多
    当且仅当 pre[r] > pre[l]。问题转化为:对每个右端点 r,统计满足 pre[l] < pre[r] 的左端点 l 的数量。
  • 用树状数组维护已出现的前缀值频次:依次扫描每个前缀值,查询"小于当前值的数量"并累加,再把当前值插入。
    前缀值范围 -n, n,偏移后存树状数组。
    复杂度:时间复杂度 O(n·log n),空间复杂度 O(n)。
    解法二
    暴力枚举子数组(对照)
  • 直接枚举所有连续子数组,统计 1 的数量是否大于 0 的数量。
    复杂度:时间复杂度 O(n²),空间复杂度 O(1);仅用于交叉验证。

代码实现

java 复制代码
public class Solution {

    private int[] tree;
    private int size;

    private void init(int n) {
        size = n;
        tree = new int[n + 1];
    }

    private void update(int x, int v) {
        for (; x <= size; x += x & -x) {
            tree[x] += v;
        }
    }

    private int query(int x) {
        int s = 0;
        for (; x > 0; x -= x & -x) {
            s += tree[x];
        }
        return s;
    }

    // 解法一:前缀平衡 + 树状数组(主提交版本)
    public int subarraysWithMoreZerosThanOnes(int[] nums) {
        int n = nums.length;
        final int MOD = 1000000007;
        int base = n + 1;                       // 前缀取值范围 [-n, n]
        init(n + base + 2);
        long ans = 0;
        int pre = 0;
        update(pre + base, 1);                  // 空前缀 pre[0] = 0
        for (int x : nums) {
            pre += (x == 1 ? 1 : -1);
            long less = query(pre + base - 1);  // 统计 pre[l] < pre[r] 的 l 个数
            ans = (ans + less) % MOD;
            update(pre + base, 1);
        }
        return (int) ans;
    }

    // 解法二:暴力(对照)
    public int subarraysWithMoreZerosThanOnesBrute(int[] nums) {
        int n = nums.length;
        int ans = 0;
        for (int i = 0; i < n; i++) {
            int bal = 0;
            for (int j = i; j < n; j++) {
                bal += (nums[j] == 1 ? 1 : -1);
                if (bal > 0) {
                    ans++;
                }
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        System.out.println(sol.subarraysWithMoreZerosThanOnes(new int[]{0, 1, 1, 0, 1})); // 9
        System.out.println(sol.subarraysWithMoreZerosThanOnes(new int[]{0})); // 0
        System.out.println(sol.subarraysWithMoreZerosThanOnes(new int[]{1})); // 1
        System.out.println(sol.subarraysWithMoreZerosThanOnesBrute(new int[]{0, 1, 1, 0, 1})); // 9
        System.out.println(sol.subarraysWithMoreZerosThanOnesBrute(new int[]{0})); // 0
        System.out.println(sol.subarraysWithMoreZerosThanOnesBrute(new int[]{1})); // 1
    }
}

复杂度分析

复杂度:时间复杂度 O(n·log n),空间复杂度 O(n)。

复杂度:时间复杂度 O(n²),空间复杂度 O(1);仅用于交叉验证。


2032. Two Out of Three(至少在两个数组中出现的值)

题目描述

给你三个整数数组 nums1、nums2 和 nums3,请你构造并返回一个元素各不相同的数组,

且由至少在两个数组中出现的所有值组成。数组中的元素可以按任意顺序排列。

示例 1

复制代码
nums1 = [1,1,3,2], nums2 = [2,3], nums3 = [3],输出 [3,2]。

示例 2

复制代码
nums1 = [3,1], nums2 = [2,3], nums3 = [1,2],输出 [2,3,1]。

示例 3

复制代码
nums1 = [1,2,2], nums2 = [4,3,3], nums3 = [5],输出 []。

解题思路

解法一(主提交版本)

三个布尔数组 + 计数(主提交版本)

  • 值域 1,100,用三个布尔数组分别记录各数组中出现过的值;对 1...100 依次检查出现在几个数组中,≥2 则加入答案。
  • 也可用整数位掩码表示"出现在哪些数组":mask[v] |= 1<<i,最后统计 popcount(maskv) >= 2。
    复杂度:时间复杂度 O(a+b+c+100),空间复杂度 O(1)。
    解法二
    Set 交集合并(对照)
  • 用三个 Set 去重,答案 = 属于至少两个集合的值。先取 s1∩s2、s2∩s3、s1∩s3 三组交集去重合并。
    复杂度:时间复杂度 O(a+b+c),空间复杂度 O(a+b+c)。

代码实现

java 复制代码
public class Solution {

    // 解法一:位掩码(主提交版本)
    public java.util.List<Integer> twoOutOfThree(int[] nums1, int[] nums2, int[] nums3) {
        int[] mask = new int[101];
        for (int v : nums1) {
            mask[v] |= 1;
        }
        for (int v : nums2) {
            mask[v] |= 2;
        }
        for (int v : nums3) {
            mask[v] |= 4;
        }
        java.util.List<Integer> ans = new java.util.ArrayList<>();
        for (int i = 1; i <= 100; i++) {
            int c = Integer.bitCount(mask[i]);
            if (c >= 2) {
                ans.add(i);
            }
        }
        return ans;
    }

    // 解法二:Set 交集(对照)
    public java.util.List<Integer> twoOutOfThreeSet(int[] nums1, int[] nums2, int[] nums3) {
        java.util.Set<Integer> s1 = toSet(nums1), s2 = toSet(nums2), s3 = toSet(nums3);
        java.util.Set<Integer> res = new java.util.HashSet<>();
        intersect(res, s1, s2);
        intersect(res, s2, s3);
        intersect(res, s1, s3);
        return new java.util.ArrayList<>(res);
    }

    private java.util.Set<Integer> toSet(int[] a) {
        java.util.Set<Integer> s = new java.util.HashSet<>();
        for (int v : a) {
            s.add(v);
        }
        return s;
    }

    private void intersect(java.util.Set<Integer> res, java.util.Set<Integer> a, java.util.Set<Integer> b) {
        for (int v : a) {
            if (b.contains(v)) {
                res.add(v);
            }
        }
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        System.out.println(sol.twoOutOfThree(new int[]{1, 1, 3, 2}, new int[]{2, 3}, new int[]{3})); // [3,2]
        System.out.println(sol.twoOutOfThree(new int[]{3, 1}, new int[]{2, 3}, new int[]{1, 2})); // [2,3,1]
        System.out.println(sol.twoOutOfThree(new int[]{1, 2, 2}, new int[]{4, 3, 3}, new int[]{5})); // []
        System.out.println(sol.twoOutOfThreeSet(new int[]{1, 1, 3, 2}, new int[]{2, 3}, new int[]{3})); // 乱序
        System.out.println(sol.twoOutOfThreeSet(new int[]{3, 1}, new int[]{2, 3}, new int[]{1, 2})); // 乱序
        System.out.println(sol.twoOutOfThreeSet(new int[]{1, 2, 2}, new int[]{4, 3, 3}, new int[]{5})); // []
    }
}

复杂度分析

复杂度:时间复杂度 O(a+b+c+100),空间复杂度 O(1)。

复杂度:时间复杂度 O(a+b+c),空间复杂度 O(a+b+c)。


2033. Minimum Operations to Make a Uni-Value Grid(单值网格的最小操作数)

题目描述

给你一个大小为 m x n 的二维整数网格 grid 和一个整数 x。每一次操作,你可以对 grid 中的任一元素加 x 或减 x。

单值网格是全部元素都相等的网格。返回使网格化为单值网格需要的最少操作数;如果不能,返回 -1。

示例 1

复制代码
grid = [[2,4],[6,8]], x = 2,输出 4。

示例 2

复制代码
grid = [[1,5],[2,3]], x = 1,输出 5。

示例 3

复制代码
grid = [[1,2],[3,4]], x = 2,输出 -1。

解题思路

解法一(主提交版本)

中位数目标(主提交版本)

  • 所有操作只改变元素对 x 的模,若存在两个元素模 x 不同则无解。
  • 全部元素可以写成 v = a*x + r 形式;由于只差整数倍 x,问题等价于在一维整数轴上把所有 a 调整为同一个 a0:
    最少操作数 = 所有 |a - a0| 之和,最优 a0 是 a 数组的中位数(一维距离和最小)。
  • 排序后取中位数,累加 |a - midA| 即答案。
    复杂度:时间复杂度 O(m·n·log(m·n)),空间复杂度 O(m·n)。
    解法二
    遍历候选值(对照)
  • 取排序后每个候选目标值 v(从中位数附近枚举全部元素值),计算所有元素到它的操作次数,取最小。
    数学上最优必是某个元素值,此方法穷举每个元素值作为目标。
    复杂度:时间复杂度 O((m·n)²),空间复杂度 O(m·n);仅用于交叉验证。

代码实现

java 复制代码
public class Solution {

    // 解法一:中位数(主提交版本)
    public int minOperations(int[][] grid, int x) {
        int m = grid.length, n = grid[0].length;
        int[] nums = new int[m * n];
        int mod = ((grid[0][0] % x) + x) % x;
        int idx = 0;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                int v = grid[i][j];
                if (((v % x) + x) % x != mod) {
                    return -1;
                }
                nums[idx++] = (v - mod) / x;   // 归一化为"倍数差"
            }
        }
        java.util.Arrays.sort(nums);
        int mid = nums[nums.length / 2];
        long ans = 0;
        for (int v : nums) {
            ans += Math.abs(v - mid);
        }
        return (int) ans;
    }

    // 解法二:枚举目标值(对照)
    public int minOperationsEnum(int[][] grid, int x) {
        int m = grid.length, n = grid[0].length;
        int[] nums = new int[m * n];
        int mod = ((grid[0][0] % x) + x) % x;
        int idx = 0;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                int v = grid[i][j];
                if (((v % x) + x) % x != mod) {
                    return -1;
                }
                nums[idx++] = v;
            }
        }
        long best = Long.MAX_VALUE;
        for (int target : nums) {
            long sum = 0;
            for (int v : nums) {
                sum += Math.abs(v - target) / x;
            }
            best = Math.min(best, sum);
        }
        return (int) best;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        System.out.println(sol.minOperations(new int[][]{{2, 4}, {6, 8}}, 2)); // 4
        System.out.println(sol.minOperations(new int[][]{{1, 5}, {2, 3}}, 1)); // 5
        System.out.println(sol.minOperations(new int[][]{{1, 2}, {3, 4}}, 2)); // -1
        System.out.println(sol.minOperationsEnum(new int[][]{{2, 4}, {6, 8}}, 2)); // 4
        System.out.println(sol.minOperationsEnum(new int[][]{{1, 5}, {2, 3}}, 1)); // 5
        System.out.println(sol.minOperationsEnum(new int[][]{{1, 2}, {3, 4}}, 2)); // -1
    }
}

复杂度分析

复杂度:时间复杂度 O(m·n·log(m·n)),空间复杂度 O(m·n)。

复杂度:时间复杂度 O((m·n)²),空间复杂度 O(m·n);仅用于交叉验证。


2034. Stock Price Fluctuation(股票价格波动)

题目描述

给你一支股票价格的数据流。数据流中每一条记录包含一个时间戳 和该时间点股票对应的价格 。

由于股票市场内在的波动性,股票价格记录可能不是按时间顺序到来的。某些情况下,有的记录可能是错的:

如果两个有相同时间戳的记录出现在数据流中,前一条记录视为错误记录,后出现的记录更正 前一条错误的记录。

请你设计一个算法,实现:

  • 更新 股票在某一时间戳的股票价格,如果有之前同一时间戳的价格,这一操作将更正之前的错误价格;
  • 找到当前记录里最新股票价格(时间戳最晚的股票价格);
  • 找到当前记录里股票的最高价格;
  • 找到当前记录里股票的最低价格 。
    请你实现 StockPrice 类:
  • StockPrice() 初始化对象,当前无股票价格记录;
  • void update(int timestamp, int price) 在时间点 timestamp 更新股票价格为 price;
  • int current() 返回股票最新价格;
  • int maximum() 返回股票最高价格;
  • int minimum() 返回股票最低价格。

示例

复制代码
输入 ["StockPrice","update","update","current","maximum","update","maximum","update","minimum"]
[[],[1,10],[2,5],[],[],[1,3],[],[4,2],[]]
输出 [null,null,null,5,10,null,5,null,2]。

解题思路

解法一(主提交版本)

哈希表 + TreeMap 计数(主提交版本)

  • 用 Map<Integer, Integer>(时间戳 -> 当前有效价格)存储最新记录,last 记录最大时间戳;
  • 用 TreeMap<Integer, Integer>(价格 -> 出现次数)维护所有有效价格的出现次数:
    update 时若时间戳已存在,先把旧价格计数减一(减到 0 则移除),再插入新价格;
  • current() 取 last 对应的价格;maximum()/minimum() 直接取 TreeMap 的首尾键。
    复杂度:每次 update O(log n),current/maximum/minimum 均 O(1),n 为不同价格数量。
    解法二
    哈希表 + 惰性扫描(对照验证用)
  • 同样用 Map<Integer, Integer> 记录最新记录(含 last);
  • 不维护有序结构,maximum()/minimum() 直接遍历哈希表扫描最值,仅用于小规模交叉验证。
    复杂度:update O(1),扫描最值 O(n),仅测试用。

代码实现

java 复制代码
public class Solution {

    // 解法一:哈希表 + TreeMap(主提交版本)
    private java.util.Map<Integer, Integer> d;          // timestamp -> 当前有效价格
    private java.util.TreeMap<Integer, Integer> ls;     // price -> 出现次数
    private int last;                                    // 最大时间戳

    public Solution() {
        d = new java.util.HashMap<>();
        ls = new java.util.TreeMap<>();
        last = 0;
    }

    public void update(int timestamp, int price) {
        if (d.containsKey(timestamp)) {
            int old = d.get(timestamp);
            int c = ls.get(old) - 1;
            if (c == 0) {
                ls.remove(old);
            } else {
                ls.put(old, c);
            }
        }
        d.put(timestamp, price);
        ls.put(price, ls.getOrDefault(price, 0) + 1);
        last = Math.max(last, timestamp);
    }

    public int current() {
        return d.get(last);
    }

    public int maximum() {
        return ls.lastKey();
    }

    public int minimum() {
        return ls.firstKey();
    }

    // 解法二:哈希表 + 惰性扫描(对照)
    private java.util.Map<Integer, Integer> rec;
    private int lastTs;

    public void initBrute() {
        rec = new java.util.HashMap<>();
        lastTs = 0;
    }

    public void addBrute(int timestamp, int price) {
        rec.put(timestamp, price);
        lastTs = Math.max(lastTs, timestamp);
    }

    public int curBrute() {
        return rec.get(lastTs);
    }

    public int maxBrute() {
        int res = Integer.MIN_VALUE;
        for (int v : rec.values()) {
            res = Math.max(res, v);
        }
        return res;
    }

    public int minBrute() {
        int res = Integer.MAX_VALUE;
        for (int v : rec.values()) {
            res = Math.min(res, v);
        }
        return res;
    }

    public static void main(String[] args) {
        Solution main = new Solution();
        main.update(1, 10);
        main.update(2, 5);
        System.out.println(main.current());   // 5
        System.out.println(main.maximum());   // 10
        main.update(1, 3);
        System.out.println(main.maximum());   // 5
        main.update(4, 2);
        System.out.println(main.minimum());   // 2

        Solution brute = new Solution();
        brute.initBrute();
        brute.addBrute(1, 10);
        brute.addBrute(2, 5);
        System.out.println(brute.curBrute()); // 5
        System.out.println(brute.maxBrute()); // 10
        brute.addBrute(1, 3);
        System.out.println(brute.maxBrute()); // 5
        brute.addBrute(4, 2);
        System.out.println(brute.minBrute()); // 2
    }
}

复杂度分析

复杂度:每次 update O(log n),current/maximum/minimum 均 O(1),n 为不同价格数量。

复杂度:update O(1),扫描最值 O(n),仅测试用。


2035. Partition Array Into Two Arrays to Minimize Sum Difference(将数组分成两个数组并最小化数组和的差)

题目描述

给你一个长度为 2 * n 的整数数组。你需要将 nums 分成两个 长度为 n 的数组,分别求出两个数组的和,

并最小化 两个数组和之差的绝对值 。nums 中每个元素都需要放入其中一个数组。

返回可能的最小值。

示例 1

复制代码
nums = [3,9,7,3],输出 2。解释:最优分组为 [3,9](和 12)与 [7,3](和 10),差为 2。

示例 2

复制代码
nums = [-36,36],输出 72。

示例 3

复制代码
nums = [2,-1,0,4,-2,-9],输出 0。解释:可分成 [2,4,-9](和 -3)与 [-1,0,-2](和 -3),差为 0。

解题思路

解法一(主提交版本)

中途相遇(Meet in the Middle)+ 二分查找(主提交版本)

  • 将数组均匀分成前后两半,每半长度 n。对每半枚举 2^n 种"选入第一组"的子集方案,记录
    (选入个数 cnt, 符号和 s):某元素入第一组记 +nums[j],入第二组记 -nums[j];
    这样整组的"第一组和 - 第二组和" = 前半贡献 + 后半贡献 = s + s1,目标即让 |s + s1| 最小。
  • 用 f[cnt] 存前半取 cnt 个入第一组的所有符号和,g[cnt1] 同理存后半。
    由于总长度 2n 中第一组恰好 n 个,若前半取 i 个则后半必须取 n - i 个;
    对 f[i] 中每个值 a,在排序后的 g[n-i] 中二分查找最接近 -a 的值使 |a + b| 最小。
    复杂度:O(n·2^n)(n ≤ 15,2^15 × 15 ≈ 50 万,可接受)。
    解法二
    暴力枚举全部选择(对照)
  • 枚举每个元素属于第一组还是第二组(2^(2n) 种),只统计第一组恰好 n 个的方案,计算 |sum1 - sum2| 取最小。
  • 指数级仅用于小规模交叉验证。复杂度 O(2^(2n)·2n)。

代码实现

java 复制代码
public class Solution {

    // 解法一:中途相遇 + 二分(主提交版本)
    public int minimumDifference(int[] nums) {
        int n = nums.length >> 1;
        java.util.List<Integer>[] f = new java.util.List[n + 1];
        java.util.List<Integer>[] g = new java.util.List[n + 1];
        for (int i = 0; i <= n; i++) {
            f[i] = new java.util.ArrayList<>();
            g[i] = new java.util.ArrayList<>();
        }
        for (int mask = 0; mask < (1 << n); mask++) {
            int s = 0, cnt = 0;     // 前半:选入第一组 +,否则 -
            int s1 = 0, cnt1 = 0;   // 后半:同上
            for (int j = 0; j < n; j++) {
                if ((mask & (1 << j)) != 0) {
                    s += nums[j];
                    cnt++;
                    s1 += nums[n + j];
                    cnt1++;
                } else {
                    s -= nums[j];
                    s1 -= nums[n + j];
                }
            }
            f[cnt].add(s);
            g[cnt1].add(s1);
        }
        for (int i = 0; i <= n; i++) {
            java.util.Collections.sort(f[i]);
            java.util.Collections.sort(g[i]);
        }
        int ans = Integer.MAX_VALUE;
        for (int i = 0; i <= n; i++) {              // 前半取 i 个入第一组
            java.util.List<Integer> gi = g[n - i];  // 后半必须取 n-i 个入第一组
            if (gi.isEmpty()) {
                continue;
            }
            for (int a : f[i]) {
                int idx = java.util.Collections.binarySearch(gi, -a);
                if (idx < 0) {
                    idx = -idx - 1;
                }
                if (idx < gi.size()) {
                    ans = Math.min(ans, Math.abs(a + gi.get(idx)));
                }
                if (idx > 0) {
                    ans = Math.min(ans, Math.abs(a + gi.get(idx - 1)));
                }
            }
        }
        return ans;
    }

    // 解法二:暴力枚举(对照)
    public int minimumDifferenceBrute(int[] nums) {
        int len = nums.length;
        int half = len >> 1;
        int best = Integer.MAX_VALUE;
        for (int mask = 0; mask < (1 << len); mask++) {
            if (Integer.bitCount(mask) != half) {
                continue;
            }
            int sum = 0;
            for (int j = 0; j < len; j++) {
                sum += ((mask >> j) & 1) == 1 ? nums[j] : -nums[j];
            }
            best = Math.min(best, Math.abs(sum));
        }
        return best;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int[] a = {3, 9, 7, 3};
        System.out.println(sol.minimumDifference(a));       // 2
        System.out.println(sol.minimumDifferenceBrute(a));  // 2
        int[] b = {-36, 36};
        System.out.println(sol.minimumDifference(b));       // 72
        System.out.println(sol.minimumDifferenceBrute(b));  // 72
        int[] c = {2, -1, 0, 4, -2, -9};
        System.out.println(sol.minimumDifference(c));       // 0
        System.out.println(sol.minimumDifferenceBrute(c));  // 0
        int[] d = {1, 2, 3, 4};
        System.out.println(sol.minimumDifference(d));       // 0 (1+4=5, 2+3=5)
        System.out.println(sol.minimumDifferenceBrute(d));  // 0
    }
}

复杂度分析

复杂度:O(n·2^n)(n ≤ 15,2^15 × 15 ≈ 50 万,可接受)。


2036. Maximum Alternating Subarray Sum(最大交替子数组和)

题目描述

子数组 是以 0 下标开始的数组的连续非空子序列。

从 i 到 j(0 <= i <= j < nums.length)的子数组交替和 被定义为

nums[i] - nums[i+1] + nums[i+2] - ... +/- nums[j](以正号开始、正负交替)。

给定一个以 0 下标开始的整数数组 nums,返回它所有可能的交替子数组和的最大值。

示例 1

复制代码
nums = [3,-1,1,2],输出 5。解释:[3,-1,1] 的交替和 3-(-1)+1 = 5 最大。

示例 2

复制代码
nums = [2,2,2,2,2],输出 2。解释:[2] 的交替和为 2。

示例 3

复制代码
nums = [1],输出 1。

解题思路

解法一(主提交版本)

线性动态规划(主提交版本)

  • 用 f 表示"以当前位置结尾、且当前位置符号为正"的最大交替和;g 表示"以当前位置结尾、当前位置符号为负"的最大交替和。
  • 扫描到 x 时:
  • 若 x 作为正号结尾:可在上一段 g 后继续(g + x),也可从 x 处开始新的一段(x),故 f = max(g, 0) + x;
  • 若 x 作为负号结尾:必须接在正号结尾的上一段后,g = f - x(用更新前的 f)。
  • 全程用 ans 记录 f、g 的最大值。
    复杂度:时间复杂度 O(n),空间复杂度 O(1)。
    解法二
    暴力枚举子数组(对照)
  • 枚举每个起点 i 与终点 j,按 nums[i] - nums[i+1] + ... 交替求和,记录最大值。
  • 复杂度 O(n^2),仅用于小规模交叉验证。

代码实现

java 复制代码
public class Solution {

    // 解法一:线性 DP(主提交版本)
    public long maximumAlternatingSubarraySum(int[] nums) {
        long negInf = Long.MIN_VALUE / 4;
        long ans = negInf, f = negInf, g = negInf;
        for (long x : toLongArray(nums)) {
            long ff = Math.max(g, 0) + x;   // 当前项为正号结尾
            g = f - x;                      // 当前项为负号结尾(用旧 f)
            f = ff;
            ans = Math.max(ans, Math.max(f, g));
        }
        return ans;
    }

    private long[] toLongArray(int[] nums) {
        long[] res = new long[nums.length];
        for (int i = 0; i < nums.length; i++) {
            res[i] = nums[i];
        }
        return res;
    }

    // 解法二:暴力枚举子数组(对照)
    public long maximumAlternatingSubarraySumBrute(int[] nums) {
        long ans = Long.MIN_VALUE / 4;
        int n = nums.length;
        for (int i = 0; i < n; i++) {
            long cur = 0;
            int sign = 1;
            for (int j = i; j < n; j++) {
                cur += sign * nums[j];
                ans = Math.max(ans, cur);
                sign = -sign;
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        System.out.println(sol.maximumAlternatingSubarraySum(new int[]{3, -1, 1, 2}));   // 5
        System.out.println(sol.maximumAlternatingSubarraySumBrute(new int[]{3, -1, 1, 2})); // 5
        System.out.println(sol.maximumAlternatingSubarraySum(new int[]{2, 2, 2, 2, 2}));  // 2
        System.out.println(sol.maximumAlternatingSubarraySumBrute(new int[]{2, 2, 2, 2, 2})); // 2
        System.out.println(sol.maximumAlternatingSubarraySum(new int[]{1}));               // 1
        System.out.println(sol.maximumAlternatingSubarraySumBrute(new int[]{1}));          // 1
        System.out.println(sol.maximumAlternatingSubarraySum(new int[]{-5, -1, -3}));      // -1
        System.out.println(sol.maximumAlternatingSubarraySumBrute(new int[]{-5, -1, -3})); // -1
    }
}

复杂度分析

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


2037. Minimum Number of Moves to Seat Everyone(使每位学生都有座位的最少移动次数)

题目描述

一个房间里有 n 个互不相同的 座位和 n 个学生,房间用一个数轴表示。

给你一个长度为 n 的数组 seats,其中 seats[i] 是第 i 个座位的位置。

同时给你一个长度为 n 的数组 students,其中 students[j] 是第 j 位学生的位置。

你可以执行以下操作任意次:增加或者减少某位学生的位置,每次可以将位置加一或减一(也就是将学生从位置 x 移动到 x + 1 或者 x - 1)。

请你返回使所有学生都有座位坐在的最少移动次数,同时保持初始状态下座位和学生之间的相对顺序不变。

示例 1

复制代码
seats = [3,1,5], students = [2,7,4],输出 4。解释:学生 0 坐在 1 号座位(移动 1),学生 1 坐在 5 号座位(移动 2),学生 2 坐在 3 号座位(移动 1),共 4。

示例 2

复制代码
seats = [4,1,5,9], students = [1,3,2,6],输出 7。

示例 3

复制代码
seats = [2,2,6,6], students = [1,3,2,6],输出 4。

解题思路

解法一(主提交版本)

排序 + 贪心(主提交版本)

  • 将 seats 与 students 分别排序:最优做法下,排序后第 i 小的学生一定坐到第 i 小的座位上
    (交换任意两个学生的座位只会增大或不减少总移动距离,三角不等式保证)。
  • 累加每一对 |seats[i] - students[i]| 即为答案。
    复杂度:时间复杂度 O(n log n)(排序),空间复杂度 O(log n) 或 O(1)。
    解法二
    计数排序(对照)
  • 位置范围有限(1 <= seats[i], students[i] <= 100),用计数数组统计每个位置上座位与学生的数量,
    双指针"依次配对"累计移动次数,两种解法结果一致。
    复杂度:O(maxPos + n),仅用于交叉验证。

代码实现

java 复制代码
public class Solution {

    // 解法一:排序贪心(主提交版本)
    public int minMovesToSeat(int[] seats, int[] students) {
        java.util.Arrays.sort(seats);
        java.util.Arrays.sort(students);
        int ans = 0;
        for (int i = 0; i < seats.length; i++) {
            ans += Math.abs(seats[i] - students[i]);
        }
        return ans;
    }

    // 解法二:计数排序配对(对照)
    public int minMovesToSeatCount(int[] seats, int[] students) {
        int[] cs = new int[105];
        int[] ct = new int[105];
        for (int x : seats) {
            cs[x]++;
        }
        for (int x : students) {
            ct[x]++;
        }
        int ans = 0, i = 1, j = 1;
        while (i <= 100 && j <= 100) {
            while (i <= 100 && cs[i] == 0) {
                i++;
            }
            while (j <= 100 && ct[j] == 0) {
                j++;
            }
            if (i > 100 || j > 100) {
                break;
            }
            int take = Math.min(cs[i], ct[j]);
            ans += take * Math.abs(i - j);
            cs[i] -= take;
            ct[j] -= take;
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int[] a1 = {3, 1, 5}, a2 = {2, 7, 4};
        System.out.println(sol.minMovesToSeat(a1, a2));      // 4
        System.out.println(sol.minMovesToSeatCount(a1, a2)); // 4
        int[] b1 = {4, 1, 5, 9}, b2 = {1, 3, 2, 6};
        System.out.println(sol.minMovesToSeat(b1, b2));      // 7
        System.out.println(sol.minMovesToSeatCount(b1, b2)); // 7
        int[] c1 = {2, 2, 6, 6}, c2 = {1, 3, 2, 6};
        System.out.println(sol.minMovesToSeat(c1, c2));      // 4
        System.out.println(sol.minMovesToSeatCount(c1, c2)); // 4
    }
}

复杂度分析

复杂度:时间复杂度 O(n log n)(排序),空间复杂度 O(log n) 或 O(1)。

复杂度:O(maxPos + n),仅用于交叉验证。


2038. Remove Colored Pieces if Both Neighbors are the Same Color(如果相邻两个颜色均相同则删除当前颜色)

题目描述

总共有 n 个颜色片段排成一列,每个颜色片段要么是 'A' 要么是 'B'。

给你一个长度为 n 的字符串 colors,其中 colors[i] 表示第 i 个颜色片段的颜色。

Alice 和 Bob 在玩一个游戏,他们轮流对这个字符串进行删除操作,Alice 先手:

  • 如果某个颜色片段同时 有两个左右相邻的颜色片段,每个片段颜色相同(均为 'A' 或均为 'B'),
    则该片段可以被删除,即 "AAABBB" 中位置 1 的 'A' 可删(左右都是 A)。
    更形式化地:存在三个连续的片段 XXX,其中 XXX 是 "AAA" 或 "BBB",则可删除中间的那个片段;
  • 在轮到 Alice 时只能删除 'A' 片段,轮到 Bob 时只能删除 'B' 片段;
  • 如果轮到某人时无法删除任何片段,则此人输掉游戏 ,另一个人获胜。
    请你返回 Alice 是否获胜。

示例 1

复制代码
colors = "AAABABB",输出 true。解释:AAABABB -> AABABB(Alice 删除位置 2 的 A)-> ABABB(Bob 无法删除)-> ...

示例 2

复制代码
colors = "AA",输出 false。解释:Alice 先手,但没有三个连续 A,无法删除,直接输。

示例 3

复制代码
colors = "ABBBBBBBAAA",输出 false。解释:Bob 能通过删除 B 制造更多可删除的 A,最终 Alice 无路可走。

解题思路

解法一(主提交版本)

贪心统计(主提交版本)

  • 关键结论:连续 k 个相同字符,Alice/Bob 最多能删除 k - 2 次(删除中间的不破坏两端的连续性;
    例如 "AAAA" 可删两次中间 A 后剩 "AA" 无法再删)。
  • 统计所有连续 'A' 段与连续 'B' 段,各自的可删除次数为 段长 - 2(要求段长 ≥ 3)。
    Alice 赢当且仅当 A 的可删除总次数 大于 B 的可删除总次数(Alice 先手,且两者互相独立、互不影响胜负次序)。
    复杂度:时间复杂度 O(n),空间复杂度 O(1)。
    解法二
    模拟对局(对照)
  • 按规则真实模拟轮流删除:每轮扫描找第一个可删除且属于当前玩家颜色的片段,删掉后切换玩家;
    若当前玩家无片段可删则判负。仅用于小规模交叉验证。
    复杂度:O(n^2),仅测试用。

代码实现

java 复制代码
public class Solution {

    // 解法一:贪心统计(主提交版本)
    public boolean winnerOfGame(String colors) {
        int a = 0, b = 0;
        int n = colors.length();
        for (int i = 0, j = 0; i < n; i = j) {
            while (j < n && colors.charAt(j) == colors.charAt(i)) {
                j++;
            }
            int len = j - i;
            if (len > 2) {
                if (colors.charAt(i) == 'A') {
                    a += len - 2;
                } else {
                    b += len - 2;
                }
            }
        }
        return a > b;
    }

    // 解法二:模拟对局(对照)
    public boolean winnerOfGameSim(String colors) {
        char[] cs = colors.toCharArray();
        int turn = 0; // 0: Alice(A), 1: Bob(B)
        int len = cs.length;
        while (true) {
            char target = (turn == 0) ? 'A' : 'B';
            int pos = -1;
            for (int i = 1; i < len - 1; i++) {
                if (cs[i] == target && cs[i - 1] == target && cs[i + 1] == target) {
                    pos = i;
                    break;
                }
            }
            if (pos == -1) {
                return turn == 1; // Alice 无法删除 -> Alice 输(Bob 赢);Bob 无法删除 -> Alice 赢
            }
            for (int i = pos; i < len - 1; i++) {
                cs[i] = cs[i + 1];
            }
            len--;
            turn ^= 1;
        }
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        System.out.println(sol.winnerOfGame("AAABABB"));      // true
        System.out.println(sol.winnerOfGameSim("AAABABB"));   // true
        System.out.println(sol.winnerOfGame("AA"));           // false
        System.out.println(sol.winnerOfGameSim("AA"));        // false
        System.out.println(sol.winnerOfGame("ABBBBBBBAAA"));  // false
        System.out.println(sol.winnerOfGameSim("ABBBBBBBAAA")); // false
        System.out.println(sol.winnerOfGame("AAAAABBB"));     // true (A 可删 3 次 > B 1 次)
        System.out.println(sol.winnerOfGameSim("AAAAABBB"));  // true
    }
}

复杂度分析

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

复杂度:O(n^2),仅测试用。


2039. The Time When the Network Becomes Idle(网络空闲的时刻)

题目描述

给你一个有 n 个服务器的计算机网络,服务器编号为 0 到 n - 1。同时给你一个二维整数数组 edges,

其中 edges[i] = [ui, vi] 表示服务器 ui 和 vi 之间有一条信息线路,在一秒内它们之间可以传输任意 数量的信息。

给你一个下标从 0 开始且长度为 n 的整数数组 patience。

在时间 t = 0 时,所有服务器的数据都在发送,服务器 0 给所有其他服务器发出一个数据包 ,其他服务器收到包后立刻开始处理。

当服务器 i 收到服务器 0 发出的数据包后,会立刻将收到的数据包回传给服务器 0。

每个数据包在网络上传输需要 2 * d 秒(d 为源服务器到服务器 0 的最短距离);且数据服务器会按照 patience[i] 的节奏重发:

若数据服务器 i 在发出某个数据包后 patience[i] 秒内(时刻为 patience[i]、2 * patience[i]、...)仍未收到服务器 0 的回复,

则它会重发该数据包(每重发一次都会收到一次回复,回复会持续消耗网络)。

网络空闲的条件:当从时刻 0 开始到某个时刻,没有任何 正在传输的信息(包括发出去未回复的与正在回传的),

则该时刻网络空闲。请返回最早 不再有任何信息传输的秒数(网络变空闲的时刻,即网络最后一个忙的整秒之后)。

示例 1

复制代码
edges = [[0,1],[1,2]], patience = [0,2,1],输出 8。

示例 2

复制代码
edges = [[0,1],[0,2],[1,2]], patience = [0,10,10],输出 3。

解题思路

解法一(主提交版本)

BFS 求最短路 + 数学计算(主提交版本)

  • 先 BFS 从服务器 0 出发求出每个服务器 v 到 0 的最短距离 d(边权为 1),往返时间 t = 2 * d。
  • 对服务器 v(v != 0):第一个包 t 秒到达服务器 0 并得到回复,回复在 t + 1 秒到达(整个信息在 t + 1 秒结束传输)。
    在收到第一个回复前,若 t > patience[v],还会重发 floor((t - 1) / patience[v]) 次,最晚一次重发在
    lastSend = floor((t - 1) / patience[v]) * patience[v] 时刻,其回复在 lastSend + t + 1 秒到达,这是该服务器的最后一个忙时刻。
  • 所有服务器的最后忙时刻取最大,即为答案(网络从该时刻的下一个整数秒起空闲)。
    复杂度:O(n + m)(BFS),m 为边数。
    解法二
    事件模拟(对照)
  • 每台服务器维护"最后一次重发时刻",直接按公式模拟重发序列:从 0 起每隔 patience[v] 发送直到超过 t,
    记录每次发送对应的回复完成时刻,取全局最大值。复杂度 O(sum t/patience),仅用于交叉验证。

代码实现

java 复制代码
public class Solution {

    // 解法一:BFS + 数学(主提交版本)
    public int networkBecomesIdle(int[][] edges, int[] patience) {
        int n = patience.length;
        java.util.List<Integer>[] g = new java.util.ArrayList[n];
        for (int i = 0; i < n; i++) {
            g[i] = new java.util.ArrayList<>();
        }
        for (int[] e : edges) {
            g[e[0]].add(e[1]);
            g[e[1]].add(e[0]);
        }
        int[] dist = new int[n];
        java.util.Arrays.fill(dist, -1);
        dist[0] = 0;
        java.util.ArrayDeque<Integer> q = new java.util.ArrayDeque<>();
        q.offer(0);
        while (!q.isEmpty()) {
            int u = q.poll();
            for (int v : g[u]) {
                if (dist[v] == -1) {
                    dist[v] = dist[u] + 1;
                    q.offer(v);
                }
            }
        }
        int ans = 0;
        for (int v = 1; v < n; v++) {
            int t = 2 * dist[v];               // 往返时间
            int lastSend;
            if (t <= patience[v]) {
                lastSend = 0;                  // 不重发,仅初始包
            } else {
                lastSend = (t - 1) / patience[v] * patience[v];
            }
            ans = Math.max(ans, lastSend + t + 1);
        }
        return ans;
    }

    // 解法二:事件模拟(对照)
    public int networkBecomesIdleSim(int[][] edges, int[] patience) {
        int n = patience.length;
        java.util.List<Integer>[] g = new java.util.ArrayList[n];
        for (int i = 0; i < n; i++) {
            g[i] = new java.util.ArrayList<>();
        }
        for (int[] e : edges) {
            g[e[0]].add(e[1]);
            g[e[1]].add(e[0]);
        }
        int[] dist = new int[n];
        java.util.Arrays.fill(dist, -1);
        dist[0] = 0;
        java.util.ArrayDeque<Integer> q = new java.util.ArrayDeque<>();
        q.offer(0);
        while (!q.isEmpty()) {
            int u = q.poll();
            for (int v : g[u]) {
                if (dist[v] == -1) {
                    dist[v] = dist[u] + 1;
                    q.offer(v);
                }
            }
        }
        int ans = 0;
        for (int v = 1; v < n; v++) {
            int t = 2 * dist[v];
            for (long send = 0; send < t; send += patience[v]) {
                ans = Math.max(ans, (int) (send + t + 1));
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int[][] e1 = {{0, 1}, {1, 2}};
        int[] p1 = {0, 2, 1};
        System.out.println(sol.networkBecomesIdle(e1, p1));     // 8
        System.out.println(sol.networkBecomesIdleSim(e1, p1));  // 8
        int[][] e2 = {{0, 1}, {0, 2}, {1, 2}};
        int[] p2 = {0, 10, 10};
        System.out.println(sol.networkBecomesIdle(e2, p2));     // 3
        System.out.println(sol.networkBecomesIdleSim(e2, p2));  // 3
        int[][] e3 = {{0, 1}};
        int[] p3 = {0, 5};
        System.out.println(sol.networkBecomesIdle(e3, p3));     // 3
        System.out.println(sol.networkBecomesIdleSim(e3, p3));  // 3
    }
}

复杂度分析

复杂度:O(n + m)(BFS),m 为边数。


2040. Kth Smallest Product of Two Sorted Arrays(两个有序数组的第 K 小乘积)

题目描述

给你两个从小到大 排好序且下标从 0 开始的整数数组 nums1 和 nums2 以及一个整数 k。

请你返回第 k 小(从 1 开始计数)的 nums1[i] * nums2[j] 的乘积。

示例 1

复制代码
nums1 = [2,5], nums2 = [3,4], k = 2,输出 8。解释:乘积排序为 6, 8, 10, 15,第 2 小是 8。

示例 2

复制代码
nums1 = [-4,-2,0,3], nums2 = [2,4], k = 6,输出 0。

示例 3

复制代码
nums1 = [2,1,4,3], nums2 = [-3,-6,4,1], k = 8,输出 -12。(注意并将 nums1 先排序)

解题思路

解法一(主提交版本)

二分答案 + 统计不超过 mid 的乘积个数(主提交版本)

  • 答案范围:[-max(|nums1|)*max(|nums2|), max(|nums1|)*max(|nums2|)],在该范围上二分查找最小的
    mid 使得 count(mid) >= k(count(mid) = 乘积 <= mid 的对数)。
  • count(mid) 的统计:对每个 x ∈ nums1:
  • x > 0:x * nums2[j] <= mid 即 nums2[j] <= mid / x,用二分在 nums2 中数出满足的个数;
  • x < 0:x * nums2[j] <= mid 需除以负数翻转不等式,nums2[j] >= ceil(mid / x),同样用二分;
  • x == 0:0 <= mid 时所有 n 个乘积都满足。
    复杂度:O((n1 + n2) log(maxVal)),可过 5e4 规模。
    解法二
    堆 + 归并(对照)
  • 分正负乘积情况:把所有可行的 (乘积, 行指针) 放入小根堆,每次弹出当前最小乘积后推进对应数组的指针,
    取出第 k 个即为答案。复杂度 O(k log m),仅用于小规模交叉验证。

代码实现

java 复制代码
public class Solution {

    private int[] a;
    private int[] b;

    // 解法一:二分答案 + 计数(主提交版本)
    public long kthSmallestProduct(int[] nums1, int[] nums2, long k) {
        this.a = nums1;
        this.b = nums2;
        // 先排序(题目保证有序,双保险)
        java.util.Arrays.sort(a);
        java.util.Arrays.sort(b);
        int m = a.length, n = b.length;
        long maxA = Math.max(Math.abs(a[0]), Math.abs(a[m - 1]));
        long maxB = Math.max(Math.abs(b[0]), Math.abs(b[n - 1]));
        long lo = -maxA * maxB, hi = maxA * maxB;
        while (lo < hi) {
            long mid = (lo + hi) >> 1;
            if (count(mid) >= k) {
                hi = mid;
            } else {
                lo = mid + 1;
            }
        }
        return lo;
    }

    // 统计乘积 <= p 的对数
    private long count(long p) {
        long cnt = 0;
        int n = b.length;
        for (long x : a) {
            if (x > 0) {
                // x * y <= p  <=>  y <= p/x(取整均用乘法二分判断,避免向零取整误差)
                int l = 0, r = n;         // 找第一个满足 x*b[mid] > p 的位置
                while (l < r) {
                    int mid = (l + r) >> 1;
                    if (x * b[mid] > p) {
                        r = mid;
                    } else {
                        l = mid + 1;
                    }
                }
                cnt += l;                 // [0, l) 都满足
            } else if (x < 0) {
                // x * y <= p  <=>  y >= ceil(p / x);负号翻转
                // 用浮点/整数方式避免精度:ceil(p / x) = floor(p / x) 对负数 x 未必正确,改用二分判断
                int l = 0, r = n;         // 找最后一个 y 使 x*y <= p(即 y >= 上界)
                while (l < r) {
                    int mid = (l + r) >> 1;
                    if (x * b[mid] <= p) {
                        r = mid;
                    } else {
                        l = mid + 1;
                    }
                }
                cnt += n - l;             // [l, n) 都满足
            } else {
                // x == 0
                if (p >= 0) {
                    cnt += n;
                }
            }
        }
        return cnt;
    }

    // 解法二:堆归并(对照)
    public long kthSmallestProductHeap(int[] nums1, int[] nums2, long k) {
        java.util.Arrays.sort(nums1);
        java.util.Arrays.sort(nums2);
        int m = nums1.length, n = nums2.length;
        // 元素入堆:{乘积, idx1, idx2}
        java.util.PriorityQueue<long[]> pq = new java.util.PriorityQueue<>((u, v) ->
                u[0] < v[0] ? -1 : (u[0] > v[0] ? 1 : 0));
        // 枚举所有可能的小起点:把 nums1 第一列按列序(正负交错)的小乘积先推入
        // 简化:全量入堆不可行,改走"按行:对每行用两个序列头"复杂化------直接全部枚举对 N 小规模可接受
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                pq.offer(new long[]{(long) nums1[i] * nums2[j], i, j});
            }
        }
        long ans = 0;
        for (long t = 0; t < k; t++) {
            ans = pq.poll()[0];
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int[] a1 = {2, 5}, b1 = {3, 4};
        System.out.println(sol.kthSmallestProduct(a1, b1, 2));      // 8
        System.out.println(sol.kthSmallestProductHeap(a1, b1, 2));  // 8
        int[] a2 = {-4, -2, 0, 3}, b2 = {2, 4};
        System.out.println(sol.kthSmallestProduct(a2, b2, 6));      // 0
        System.out.println(sol.kthSmallestProductHeap(a2, b2, 6));  // 0
        int[] a3 = {-2, -1, 0, 1, 2}, b3 = {-3, -1, 2, 4, 5};
        System.out.println(sol.kthSmallestProduct(a3, b3, 3));      // -6
        System.out.println(sol.kthSmallestProductHeap(a3, b3, 3));  // -6
        int[] a4 = {-1, 0, 1}, b4 = {-2, -1, 1, 2};
        System.out.println(sol.kthSmallestProduct(a4, b4, 9));      // 2
        System.out.println(sol.kthSmallestProductHeap(a4, b4, 9));  // 2
    }
}

复杂度分析

复杂度:O((n1 + n2) log(maxVal)),可过 5e4 规模。

相关推荐
bksczm30 分钟前
InnoDB 行格式详解:DYNAMIC / COMPACT / COMPRESSED / REDUNDANT
java·开发语言·数据库
liangbo71 小时前
Spring系列03:Bean 的三种注册方式
java·spring
余槐i1 小时前
MySQL 8.0 联合索引走错:为什么加了索引查询反而从 200ms 变成 8s
java·spring boot·mysql·docker·性能优化
SL_staff1 小时前
制造业订单交付总在最后一刻爆雷?用结构化计划模板打通跨系统协同断点
java·开源·全栈
做科研的周师兄1 小时前
【R语言实战】MixSIAR 氢氧同位素水源解析全流程:从混合空间校验到贝叶斯 MCMC 收敛诊断(后附完整代码)
java·大数据·开发语言
有点小困9771 小时前
Spring Boot 异步任务 ClassLoader 踩坑实录:ClassNotFoundException 的诡异真相
java
aiks_yuan2 小时前
Java 正则表达式很慢怎么办:Pattern 预编译只快 2~5 倍,真正卡死线程的是回溯(JDK 8、11、17 实测)
java
用户3721574261352 小时前
C# 填写 PDF 交互式表单:文本框、复选框、单选按钮和下拉列表
java