LeetCode 2021-2040. 题解合集
文章目录
- [LeetCode 2021-2040. 题解合集](#LeetCode 2021-2040. 题解合集)
-
- [2021. Brightest Position on Street(街上最亮的位置)](#2021. Brightest Position on Street(街上最亮的位置))
- [2022. Convert 1D Array Into 2D Array(将一维数组转变成二维数组)](#2022. Convert 1D Array Into 2D Array(将一维数组转变成二维数组))
- [2023. Number of Pairs of Strings With Concatenation Equal to Target(连接后等于目标字符串的字符串对)](#2023. Number of Pairs of Strings With Concatenation Equal to Target(连接后等于目标字符串的字符串对))
- [2024. Maximize the Confusion of an Exam(考试的最大困扰度)](#2024. Maximize the Confusion of an Exam(考试的最大困扰度))
- [2025. Maximum Number of Ways to Partition an Array(分割数组的最多方案数)](#2025. Maximum Number of Ways to Partition an Array(分割数组的最多方案数))
- [2026. Low-Quality Problems(低质量的问题)](#2026. Low-Quality Problems(低质量的问题))
- [2027. Minimum Moves to Convert String(转换字符串的最少操作次数)](#2027. Minimum Moves to Convert String(转换字符串的最少操作次数))
- [2028. Find Missing Observations(找出缺失的观测数据)](#2028. Find Missing Observations(找出缺失的观测数据))
- [2029. Stone Game IX(石子游戏 IX)](#2029. Stone Game IX(石子游戏 IX))
- [2030. Smallest K-Length Subsequence With Occurrences of a Letter(含特定字母的最小子序列)](#2030. Smallest K-Length Subsequence With Occurrences of a Letter(含特定字母的最小子序列))
- [2031. Count Subarrays With More Ones Than Zeros(1 比 0 多的子数组个数)](#2031. Count Subarrays With More Ones Than Zeros(1 比 0 多的子数组个数))
- [2032. Two Out of Three(至少在两个数组中出现的值)](#2032. Two Out of Three(至少在两个数组中出现的值))
- [2033. Minimum Operations to Make a Uni-Value Grid(单值网格的最小操作数)](#2033. Minimum Operations to Make a Uni-Value Grid(单值网格的最小操作数))
- [2034. Stock Price Fluctuation(股票价格波动)](#2034. Stock Price Fluctuation(股票价格波动))
- [2035. Partition Array Into Two Arrays to Minimize Sum Difference(将数组分成两个数组并最小化数组和的差)](#2035. Partition Array Into Two Arrays to Minimize Sum Difference(将数组分成两个数组并最小化数组和的差))
- [2036. Maximum Alternating Subarray Sum(最大交替子数组和)](#2036. Maximum Alternating Subarray Sum(最大交替子数组和))
- [2037. Minimum Number of Moves to Seat Everyone(使每位学生都有座位的最少移动次数)](#2037. Minimum Number of Moves to Seat Everyone(使每位学生都有座位的最少移动次数))
- [2038. Remove Colored Pieces if Both Neighbors are the Same Color(如果相邻两个颜色均相同则删除当前颜色)](#2038. Remove Colored Pieces if Both Neighbors are the Same Color(如果相邻两个颜色均相同则删除当前颜色))
- [2039. The Time When the Network Becomes Idle(网络空闲的时刻)](#2039. The Time When the Network Becomes Idle(网络空闲的时刻))
- [2040. Kth Smallest Product of Two Sorted Arrays(两个有序数组的第 K 小乘积)](#2040. Kth Smallest Product of Two Sorted Arrays(两个有序数组的第 K 小乘积))
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:
- 距离约束:弹出后剩余还需
k个位置,若stack.size()-1 + (n-i) < k则不能再弹(后续填不满 k 位); - letter 约束:弹出后栈内 letter 数
in-letter + 后缀中未扫描的 letter 数(cnt,含当前位)仍>= repetition才能弹; - 字典序:
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 规模。