LeetCode 2161-2180. 题解合集
文章目录
- [LeetCode 2161-2180. 题解合集](#LeetCode 2161-2180. 题解合集)
-
- [2161. Partition Array According to Given Pivot(根据给定数字划分数组)](#2161. Partition Array According to Given Pivot(根据给定数字划分数组))
- [2162. Minimum Cost to Set Cooking Time(设置时间的最少代价)](#2162. Minimum Cost to Set Cooking Time(设置时间的最少代价))
- [2163. Minimum Difference in Sums After Removal of Elements(删除元素后和的最小差值)](#2163. Minimum Difference in Sums After Removal of Elements(删除元素后和的最小差值))
- [2164. Sort Even and Odd Indices Independently(对奇偶下标分别排序)](#2164. Sort Even and Odd Indices Independently(对奇偶下标分别排序))
- [2165. Smallest Value of the Rearranged Number(重排数字的最小值)](#2165. Smallest Value of the Rearranged Number(重排数字的最小值))
- [2166. Design Bitset(设计位集)](#2166. Design Bitset(设计位集))
- [2167. Minimum Time to Remove All Cars Containing Illegal Goods(移除所有载有违禁货物车厢所需的最少时间)](#2167. Minimum Time to Remove All Cars Containing Illegal Goods(移除所有载有违禁货物车厢所需的最少时间))
- [2168. Unique Substrings With Equal Digit Frequency(每个数字的频率都相同的不同子字符串的数量)](#2168. Unique Substrings With Equal Digit Frequency(每个数字的频率都相同的不同子字符串的数量))
- [2169. Count Operations to Obtain Zero(得到 0 的操作数)](#2169. Count Operations to Obtain Zero(得到 0 的操作数))
- [2170. Minimum Operations to Make the Array Alternating(使数组变成交替数组的最少操作数)](#2170. Minimum Operations to Make the Array Alternating(使数组变成交替数组的最少操作数))
- [2171. Removing Minimum Number of Magic Beans(拿出最少数目的魔法豆)](#2171. Removing Minimum Number of Magic Beans(拿出最少数目的魔法豆))
- [2172. Maximum AND Sum of Array(数组的最大与和)](#2172. Maximum AND Sum of Array(数组的最大与和))
- [2173. Longest Winning Streak(最多连胜的次数)](#2173. Longest Winning Streak(最多连胜的次数))
- [2174. Remove All Ones With Row and Column Flips II(通过翻转行或列来去除所有的 1 II)](#2174. Remove All Ones With Row and Column Flips II(通过翻转行或列来去除所有的 1 II))
- [2175. The Change in Global Rankings(世界排名的变化)](#2175. The Change in Global Rankings(世界排名的变化))
- [2176. Count Equal and Divisible Pairs in an Array(统计数组中相等且可以被整除的数对)](#2176. Count Equal and Divisible Pairs in an Array(统计数组中相等且可以被整除的数对))
- [2177. Find Three Consecutive Integers That Sum to a Given Number(找到和为给定整数的三个连续整数)](#2177. Find Three Consecutive Integers That Sum to a Given Number(找到和为给定整数的三个连续整数))
- [2178. Maximum Split of Positive Even Integers(拆分成最多数目的正偶数之和)](#2178. Maximum Split of Positive Even Integers(拆分成最多数目的正偶数之和))
- [2179. Count Good Triplets in an Array(统计数组中好三元组数目)](#2179. Count Good Triplets in an Array(统计数组中好三元组数目))
- [2180. Count Integers With Even Digit Sum(统计各位数字之和为偶数的整数个数)](#2180. Count Integers With Even Digit Sum(统计各位数字之和为偶数的整数个数))
2161. Partition Array According to Given Pivot(根据给定数字划分数组)
题目描述
给你一个下标从 0 开始的整数数组 nums 和一个整数 pivot。请你将 nums 重新排列,使得:
- 所有小于
pivot的元素都出现在所有大于pivot的元素之前; - 所有等于
pivot的元素都出现在小于和大于pivot的元素中间; - 小于
pivot的元素之间和大于pivot的元素之间的相对顺序不发生改变。
返回重新排列nums数组后的结果数组。
示例 1
nums = [9,12,5,10,14,3,10], pivot = 10 → [9,5,3,10,10,12,14]
示例 2
nums = [-3,4,3,2], pivot = 2 → [-3,2,4,3]
约束:
- 1 <=
nums.length<= 10^5,pivot等于nums中的一个元素
解题思路
解法一(主提交版本)
三遍收集(主提交版本)
- 依次收集 < pivot、== pivot、> pivot 的元素,拼接返回。
复杂度:O(n) 时间,O(n) 空间。
解法二
单遍三指针写入(对照) - 先统计小于与等于 pivot 的数量,确定三段起始位置,再单遍按段写入;逻辑清晰且稳定。
复杂度:O(n) 时间,O(n) 空间。用于交叉验证。
代码实现
java
import java.util.ArrayList;
import java.util.List;
public class Solution {
// 解法一:三遍收集(主提交版本)
public int[] pivotArray(int[] nums, int pivot) {
List<Integer> less = new ArrayList<>();
List<Integer> eq = new ArrayList<>();
List<Integer> greater = new ArrayList<>();
for (int v : nums) {
if (v < pivot) less.add(v);
else if (v == pivot) eq.add(v);
else greater.add(v);
}
int[] ans = new int[nums.length];
int idx = 0;
for (int v : less) ans[idx++] = v;
for (int v : eq) ans[idx++] = v;
for (int v : greater) ans[idx++] = v;
return ans;
}
// 解法二:计数定位单遍写入(对照)
public int[] pivotArrayCounter(int[] nums, int pivot) {
int n = nums.length;
int lessCnt = 0, eqCnt = 0;
for (int v : nums) {
if (v < pivot) lessCnt++;
else if (v == pivot) eqCnt++;
}
int[] ans = new int[n];
int iLess = 0, iEq = lessCnt, iGt = lessCnt + eqCnt;
for (int v : nums) {
if (v < pivot) ans[iLess++] = v;
else if (v == pivot) ans[iEq++] = v;
else ans[iGt++] = v;
}
return ans;
}
private static void print(int[] a) {
StringBuilder sb = new StringBuilder("[");
for (int i = 0; i < a.length; i++) {
if (i > 0) sb.append(",");
sb.append(a[i]);
}
sb.append("]");
System.out.println(sb.toString());
}
public static void main(String[] args) {
Solution sol = new Solution();
print(sol.pivotArray(new int[]{9, 12, 5, 10, 14, 3, 10}, 10)); // [9,5,3,10,10,12,14]
print(sol.pivotArrayCounter(new int[]{9, 12, 5, 10, 14, 3, 10}, 10));
print(sol.pivotArray(new int[]{-3, 4, 3, 2}, 2)); // [-3,2,4,3]
print(sol.pivotArrayCounter(new int[]{-3, 4, 3, 2}, 2));
}
}
复杂度分析
复杂度:O(n) 时间,O(n) 空间。
复杂度:O(n) 时间,O(n) 空间。用于交叉验证。
2162. Minimum Cost to Set Cooking Time(设置时间的最少代价)
题目描述
常见的微波炉可以设置加热时间,至少 1 秒,至多 99 分 99 秒。你可以最多输入 4 个数字来设置加热时间,
位数不足 4 位会自动加前缀 0 补足 4 位。微波炉将设置好的四位数中,前两位当作分钟数,后两位当作秒数,
它们所表示的总时间就是加热时间(秒数最多可设置到 99 秒,1 分钟 = 60 秒)。
给你 startAt(初始手指所在数字)、moveCost(移动到其他数字的代价)、pushCost(输入当前数字的代价)
和 targetSeconds。你需要设置 targetSeconds 秒的加热时间,要求这些方法中总代价最小。
示例 1
startAt=1, moveCost=2, pushCost=1, targetSeconds=600 → 6
示例 2
startAt=0, moveCost=1, pushCost=2, targetSeconds=76 → 6
约束:
- 0 <=
startAt<= 9,1 <=moveCost,pushCost<= 10^5,1 <=targetSeconds<= 6039
解题思路
解法一(主提交版本)
枚举 mm:ss 两位表示(主提交版本)
- 表示 targetSeconds 的方式有限:设总秒数 t,合法格式 (minute, second) 满足 minute*60+second == t,
且 0 <= minute <= 99,0 <= second <= 99。注意秒可为 >= 60 的溢出形式(如 9 分 60 秒=600 秒,
前两位 9、后两位 60 是合法输入)。 - 对每种 (mm, ss),把 (mm*100 + ss) 转成去掉前导零的数字串逐位模拟输入代价:
初始手指在 startAt,每位若与当前手指数字不同则 +moveCost,每次 +pushCost。 - 取所有表示法代价最小值。
复杂度:O(1)(表示方法至多若干种,串长 ≤ 4)。
解法二
枚举全部 4 位带前导零(对照) - 直接把 mm、ss 各两位(含前导零)拼接的 4 位数逐位算代价;对含前导零情况(实际自动补零也如此),
与解法一统一为「字符串去前导零」处理后再到 4 位。两种写法一致,此作交叉验证。
复杂度:O(1)。
代码实现
java
public class Solution {
// 解法一:枚举 mm:ss 合法表示(主提交版本)
public int minCostSetTime(int startAt, int moveCost, int pushCost, int targetSeconds) {
int best = Integer.MAX_VALUE;
// 合法的 (minute, second) 组合:直接 mm:ss
for (int m = 0; m <= 99; m++) {
for (int s = 0; s <= 99; s++) {
if (m * 60 + s == targetSeconds) {
int val = m * 100 + s; // 四位(含前导零)的数值
best = Math.min(best, cost(toDigitsNoLeadingZero(val), startAt, moveCost, pushCost));
}
}
}
return best;
}
// 解法二:枚举余数格式(对照,直接针对「分0秒/分秒溢出」结构)
public int minCostSetTimeAlt(int startAt, int moveCost, int pushCost, int targetSeconds) {
int best = Integer.MAX_VALUE;
// 情况 1:正常 mm:ss
int m1 = targetSeconds / 60, s1 = targetSeconds % 60;
if (m1 <= 99 && s1 <= 99) {
int v = m1 * 100 + s1;
best = Math.min(best, cost(toDigitsNoLeadingZero(v), startAt, moveCost, pushCost));
}
// 情况 2:秒溢出(s >= 60),如 9 分 60 秒 = 600 秒:min = (t/60) - 1, sec = t % 60 + 60
if (targetSeconds > 60) {
int m2 = targetSeconds / 60 - 1, s2 = targetSeconds % 60 + 60;
if (m2 >= 0 && m2 <= 99 && s2 <= 99) {
int v = m2 * 100 + s2;
best = Math.min(best, cost(toDigitsNoLeadingZero(v), startAt, moveCost, pushCost));
}
}
return best;
}
// 去掉前导零后的数字序列,依次计算输入代价(手指初始在 startAt)
private int cost(int[] digits, int startAt, int moveCost, int pushCost) {
int pos = startAt;
int total = 0;
for (int d : digits) {
if (d != pos) {
total += moveCost;
pos = d;
}
total += pushCost;
}
return total;
}
private int[] toDigitsNoLeadingZero(int v) {
// 1000..9999,或 0..999:但输入 v 总在 [0, 9999];取各位并跳过前导 0
if (v == 0) return new int[]{0};
String s = String.valueOf(v);
int[] res = new int[s.length()];
for (int i = 0; i < s.length(); i++) res[i] = s.charAt(i) - '0';
return res;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.minCostSetTime(1, 2, 1, 600) + " " + sol.minCostSetTimeAlt(1, 2, 1, 600)); // 6 6
System.out.println(sol.minCostSetTime(0, 1, 2, 76) + " " + sol.minCostSetTimeAlt(0, 1, 2, 76)); // 6 6
System.out.println(sol.minCostSetTime(1, 1, 1, 1) + " " + sol.minCostSetTimeAlt(1, 1, 1, 1)); // 1(输入 1,1 秒)
System.out.println(sol.minCostSetTime(0, 2, 3, 6039) + " " + sol.minCostSetTimeAlt(0, 2, 3, 6039)); // 99分99秒=6039
}
}
复杂度分析
复杂度:O(1)(表示方法至多若干种,串长 ≤ 4)。
复杂度:O(1)。
2163. Minimum Difference in Sums After Removal of Elements(删除元素后和的最小差值)
题目描述
给你一个下标从 0 开始的整数数组 nums,它包含 3*n 个元素。你可以从 nums 中删除恰好 n 个元素,
剩下的 2*n 个元素将会被分成两个相同大小的部分:前面 n 个元素的和记为 sumfirst,后面 n 个元素的和
记为 sumsecond。两部分和的差值记为 sumfirst - sumsecond。请你返回删除 n 个元素之后,
剩下两部分和差值的最小值。
示例 1
nums = [3,1,2] → -1
示例 2
nums = [7,9,5,8,1,3] → 1
约束:
nums.length== 3*n,1 <=n<= 10^5,1 <=nums[i]<= 10^5
解题思路
解法一(主提交版本)
前缀最小 n 和 + 后缀最大 n 和(主提交版本)
- 删除 n 个元素后,前半部分取自原数组某个前缀区间、后半部分取自原数组后缀区间,
分界点在原排列中的位置决定。问题可重述为:在位置 i(i 从 n 到 2n)处,
前半段(左 n 个保留元素)取左侧区间的 n 个最小元素,后半段(右 n 个保留元素)
取右侧区间的 n 个最大元素;依次求 min(prefixMinSumi - suffixMaxSumi)。 - 实现:leftMini = 前 i 个元素中选 n 个最小元素的和(用大顶堆维护滑动);
偏右 rightMaxi = 从 i 到末尾选 n 个最大元素的和(用小顶堆维护)。 - 原数组相对顺序中前段保留元素必须在后段保留元素左侧------由于删除后两部分是「前 n 个、后 n 个」,
保留元素的相对顺序必然保持,因此该枚举覆盖所有方案。
复杂度:O(n log n) 时间,O(n) 空间。
解法二
双向堆同步(对照) - 与解法一同构的另一种写法:前缀扫一遍维护前 n 小之和,后缀扫一遍维护后 n 大之和。
复杂度:O(n log n) 时间,O(n) 空间。用于交叉验证。
代码实现
java
import java.util.Arrays;
import java.util.PriorityQueue;
public class Solution {
// 解法一:前缀最小 n 和 + 后缀最大 n 和(主提交版本)
public long minimumDifference(int[] nums) {
int n = nums.length / 3;
int m = nums.length;
long[] leftMin = new long[m + 1]; // leftMin[i]: 前 i 个元素中选 n 个最小元素的和
PriorityQueue<Integer> maxHeap = new PriorityQueue<>((a, b) -> b - a);
long sum = 0;
for (int i = 0; i < n; i++) {
maxHeap.offer(nums[i]);
sum += nums[i];
}
leftMin[n] = sum;
for (int i = n; i < m; i++) {
maxHeap.offer(nums[i]);
sum += nums[i];
sum -= maxHeap.poll();
leftMin[i + 1] = sum;
}
long[] rightMax = new long[m + 1]; // rightMax[i]: 从 i 到末尾选 n 个最大元素的和
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
sum = 0;
for (int i = m - 1; i >= m - n; i--) {
minHeap.offer(nums[i]);
sum += nums[i];
}
rightMax[m - n] = sum;
for (int i = m - n - 1; i >= 0; i--) {
minHeap.offer(nums[i]);
sum += nums[i];
sum -= minHeap.poll();
rightMax[i] = sum;
}
long best = Long.MAX_VALUE;
// 分界点 i:左段保留 n 个来自 [0,i),右段保留 n 个来自 [i, m)
for (int i = n; i <= 2 * n; i++) {
best = Math.min(best, leftMin[i] - rightMax[i]);
}
return best;
}
// 解法二:双向堆同步(对照,与解法一重复但独立实现)
public long minimumDifferenceAlt(int[] nums) {
int n = nums.length / 3;
int m = nums.length;
long[] leftMin = minSumEachPos(nums, n, true);
long[] rightMax = maxSumEachPos(nums, n, false);
long best = Long.MAX_VALUE;
for (int i = n; i <= 2 * n; i++) {
best = Math.min(best, leftMin[i] - rightMax[i]);
}
return best;
}
// 返回 leftMin[i]:前 i 个元素中选 n 个最小之和(mode=true);rightMax[i]:i 起选 n 个最大之和
private long[] minSumEachPos(int[] nums, int n, boolean forward) {
int m = nums.length;
long[] res = new long[m + 1];
PriorityQueue<Integer> heap = new PriorityQueue<>((a, b) -> b - a);
long sum = 0;
if (forward) {
for (int i = 0; i < n; i++) {
heap.offer(nums[i]);
sum += nums[i];
}
res[n] = sum;
for (int i = n; i < m; i++) {
heap.offer(nums[i]);
sum += nums[i] - heap.poll();
res[i + 1] = sum;
}
} else {
for (int i = m - 1; i >= m - n; i--) {
heap.offer(nums[i]);
sum += nums[i];
}
res[m - n] = sum;
for (int i = m - n - 1; i >= 0; i--) {
heap.offer(nums[i]);
sum += nums[i] - heap.poll();
res[i] = sum;
}
}
return res;
}
private long[] maxSumEachPos(int[] nums, int n, boolean forward) {
int m = nums.length;
long[] res = new long[m + 1];
PriorityQueue<Integer> heap = new PriorityQueue<>();
long sum = 0;
if (forward) {
for (int i = 0; i < n; i++) {
heap.offer(nums[i]);
sum += nums[i];
}
res[n] = sum;
for (int i = n; i < m; i++) {
heap.offer(nums[i]);
sum += nums[i] - heap.poll();
res[i + 1] = sum;
}
} else {
for (int i = m - 1; i >= m - n; i--) {
heap.offer(nums[i]);
sum += nums[i];
}
res[m - n] = sum;
for (int i = m - n - 1; i >= 0; i--) {
heap.offer(nums[i]);
sum += nums[i] - heap.poll();
res[i] = sum;
}
}
return res;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.minimumDifference(new int[]{3, 1, 2})
+ " " + sol.minimumDifferenceAlt(new int[]{3, 1, 2})); // -1 -1
System.out.println(sol.minimumDifference(new int[]{7, 9, 5, 8, 1, 3})
+ " " + sol.minimumDifferenceAlt(new int[]{7, 9, 5, 8, 1, 3})); // 1 1
System.out.println(sol.minimumDifference(new int[]{1, 2, 3, 4, 5, 6})
+ " " + sol.minimumDifferenceAlt(new int[]{1, 2, 3, 4, 5, 6})); // 删 2 个:前段 1+2=3 后段 5+6=11 → -8? 更优留 1,2,5,6 → (1+2)-(5+6)=-8
}
}
复杂度分析
复杂度:O(n log n) 时间,O(n) 空间。
复杂度:O(n log n) 时间,O(n) 空间。用于交叉验证。
2164. Sort Even and Odd Indices Independently(对奇偶下标分别排序)
题目描述
给你一个下标从 0 开始的整数数组 nums。根据下述规则重排 nums 中的值:
- 按非递增顺序排列
nums奇数下标上的所有值; - 按非递减顺序排列
nums偶数下标上的所有值。
返回重排nums的值之后形成的数组。
示例 1
nums = [4,1,2,3] → [2,3,4,1]
示例 2
nums = [2,1] → [2,1]
约束:
- 1 <=
nums.length<= 100,1 <=nums[i]<= 100
解题思路
解法一(主提交版本)
分离奇偶下标排序(主提交版本)
- 收集奇数下标的值与偶数下标的值分别排序(奇数非递增、偶数非递减),再按原下标回填。
复杂度:O(n log n) 时间,O(n) 空间。
解法二
计数排序(对照) - 值域只有 1...100,用计数数组实现 O(n + C)。用于交叉验证。
复杂度:O(n + C) 时间,O© 空间。
代码实现
java
import java.util.Arrays;
public class Solution {
// 解法一:分离排序(主提交版本)
public int[] sortEvenOdd(int[] nums) {
int n = nums.length;
int evenCnt = (n + 1) / 2, oddCnt = n / 2;
int[] even = new int[evenCnt];
int[] odd = new int[oddCnt];
int ei = 0, oi = 0;
for (int i = 0; i < n; i++) {
if (i % 2 == 0) even[ei++] = nums[i];
else odd[oi++] = nums[i];
}
Arrays.sort(even); // 偶数下标非递减
Arrays.sort(odd); // 奇数下标先升序再反转实现非递增
for (int i = 0; i < oddCnt / 2; i++) {
int t = odd[i]; odd[i] = odd[oddCnt - 1 - i]; odd[oddCnt - 1 - i] = t;
}
int[] ans = new int[n];
for (int i = 0; i < n; i++) {
ans[i] = (i % 2 == 0) ? even[i / 2] : odd[i / 2];
}
return ans;
}
// 解法二:计数实现(对照)
public int[] sortEvenOddCount(int[] nums) {
int n = nums.length;
int[] cntEven = new int[101];
int[] cntOdd = new int[101];
for (int i = 0; i < n; i++) {
if (i % 2 == 0) cntEven[nums[i]]++;
else cntOdd[nums[i]]++;
}
int[] ans = new int[n];
int ei = 0, oi = 0;
for (int v = 1; v <= 100; v++) {
while (cntEven[v]-- > 0) {
ans[2 * ei] = v;
ei++;
}
}
for (int v = 100; v >= 1; v--) {
while (cntOdd[v]-- > 0) {
ans[2 * oi + 1] = v;
oi++;
}
}
return ans;
}
private static void print(int[] a) {
StringBuilder sb = new StringBuilder("[");
for (int i = 0; i < a.length; i++) {
if (i > 0) sb.append(",");
sb.append(a[i]);
}
sb.append("]");
System.out.println(sb.toString());
}
public static void main(String[] args) {
Solution sol = new Solution();
print(sol.sortEvenOdd(new int[]{4, 1, 2, 3})); // [2,3,4,1]
print(sol.sortEvenOddCount(new int[]{4, 1, 2, 3}));
print(sol.sortEvenOdd(new int[]{2, 1})); // [2,1]
print(sol.sortEvenOddCount(new int[]{2, 1}));
}
}
复杂度分析
复杂度:O(n log n) 时间,O(n) 空间。
复杂度:O(n + C) 时间,O© 空间。
2165. Smallest Value of the Rearranged Number(重排数字的最小值)
题目描述
给你一个整数 num。重排 num 中的各位数字,使其值最小化且不含任何前导零。返回不含前导零且值最小的
重排数字。注意,重排各位数字后,num 的符号不会改变。
示例 1
num = 310 → 103
示例 2
num = -7605 → -7650
约束:
-10^15 <= num <= 10^15
解题思路
解法一(主提交版本)
正负数分别处理(主提交版本)
- 负数:数字按降序排列可使数值最小(位权越高放越大数字),直接降序拼接加负号。
- 正数:数字升序排列,但首个非零数字(最小的非零)放到首位,其余 0 紧随其后,
再按升序接剩余数字。
复杂度:O(d log d) 时间(d ≤ 16),O(d) 空间。
解法二
计数数组实现(对照) - 用 cnt10 计数,贪心构造:负数从 9 到 0 取;正数先放最小非零数再放 0 再多非零升序。
复杂度:O(d) 时间,O(1) 空间。用于交叉验证。
代码实现
java
import java.util.Arrays;
public class Solution {
// 解法一:排序处理(主提交版本)
public long smallestNumber(long num) {
if (num == 0) return 0;
boolean neg = num < 0;
long abs = Math.abs(num);
String ds = String.valueOf(abs);
char[] chars = ds.toCharArray();
Arrays.sort(chars);
if (neg) {
// 降序:从后往前取
StringBuilder sb = new StringBuilder("-");
for (int i = chars.length - 1; i >= 0; i--) sb.append(chars[i]);
return Long.parseLong(sb.toString());
}
// 正数:升序,把第一个非零移到首位
int firstNonZero = 0;
while (firstNonZero < chars.length && chars[firstNonZero] == '0') firstNonZero++;
StringBuilder sb = new StringBuilder();
sb.append(chars[firstNonZero]);
for (int i = 0; i < chars.length; i++) {
if (i != firstNonZero) sb.append(chars[i]);
}
return Long.parseLong(sb.toString());
}
// 解法二:计数构造(对照)
public long smallestNumberCnt(long num) {
if (num == 0) return 0;
boolean neg = num < 0;
long abs = Math.abs(num);
int[] cnt = new int[10];
while (abs > 0) {
cnt[(int) (abs % 10)]++;
abs /= 10;
}
StringBuilder sb = new StringBuilder();
if (neg) {
sb.append('-');
for (int d = 9; d >= 0; d--) {
while (cnt[d]-- > 0) sb.append((char) ('0' + d));
}
} else {
// 第一个非零数字
int first = 1;
while (first < 10 && cnt[first] == 0) first++;
cnt[first]--;
sb.append((char) ('0' + first));
for (int d = 0; d < 10; d++) {
while (cnt[d]-- > 0) sb.append((char) ('0' + d));
}
}
return Long.parseLong(sb.toString());
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.smallestNumber(310) + " " + sol.smallestNumberCnt(310)); // 103 103
System.out.println(sol.smallestNumber(-7605) + " " + sol.smallestNumberCnt(-7605)); // -7650 -7650
System.out.println(sol.smallestNumber(0) + " " + sol.smallestNumberCnt(0)); // 0 0
System.out.println(sol.smallestNumber(100000) + " " + sol.smallestNumberCnt(100000)); // 100000(无前导零)
System.out.println(sol.smallestNumber(120) + " " + sol.smallestNumberCnt(120)); // 102 102
}
}
复杂度分析
复杂度:O(d log d) 时间(d ≤ 16),O(d) 空间。
复杂度:O(d) 时间,O(1) 空间。用于交叉验证。
2166. Design Bitset(设计位集)
题目描述
位集 Bitset 是一种能以紧凑形式存储位的数据结构。请你实现 Bitset 类:
- Bitset(
intsize) 用size个位初始化Bitset,所有位都是 0。 voidfix(intidx) 将下标idx的位更新为 1(已是 1 则不变)。voidunfix(intidx) 将下标idx的位更新为 0(已是 0 则不变)。voidflip()翻转每一位上的值。booleanall()检查每一位是否都是 1。booleanone()检查是否至少一位是 1。intcount()返回值为 1 的位的总数。StringtoString()返回Bitset的当前组成情况(第i个字符与第i位一致)。
示例
size=5 依次 fix(3) fix(1) flip all() unfix(0) flip one() unfix(0) count() toString()
- 输出 :`null`,`null`,`null`,`null`,`false`,`null`,`null`,`true`,`null`,2,"01010"
约束: - 1 <=
size<= 10^5,总调用次数 <= 10^5,toString至多调用 5 次
解题思路
解法一(主提交版本)
懒标记翻转位集(主提交版本)
- 用 int\[\] bits 存位、int ones 存 1 的个数、boolean flipped 表示整体翻转的懒标记。
- 实际位值 = bitsi ^ (flipped ? 1 : 0)。
- fix/unfix/count/all/one 按懒标记换算,flip 只需翻转 flipped 并把 ones = size - ones。
- toString 时按懒标记逐位拼。
复杂度:O(1) fix/unfix/flip/count/all/one,O(n) toString;空间 O(n)。
解法二
位运算 long 数组(对照) - 用 long 数组存储位,操作按 block 定位;同样使用翻转掩码概念。由于有懒翻转标记,
实现与解法一同构;这里提供用 bit 运算的等价写法交叉验证。
复杂度:O(1) 均摊,O(n) toString/toArray;空间 O(n/64)。
代码实现
java
public class Solution {
// 解法一实现(内嵌 Bitset 类,便于本地验证)
public static class Bitset {
private int[] bits;
private int size;
private int ones;
private boolean flipped;
public Bitset(int size) {
this.size = size;
bits = new int[size];
}
public void fix(int idx) {
int v = bits[idx] ^ (flipped ? 1 : 0);
if (v == 0) {
bits[idx] ^= 1;
ones++;
}
}
public void unfix(int idx) {
int v = bits[idx] ^ (flipped ? 1 : 0);
if (v == 1) {
bits[idx] ^= 1;
ones--;
}
}
public void flip() {
flipped = !flipped;
ones = size - ones;
}
public boolean all() {
return ones == size;
}
public boolean one() {
return ones > 0;
}
public int count() {
return ones;
}
public String toString() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < size; i++) {
int v = bits[i] ^ (flipped ? 1 : 0);
sb.append(v == 1 ? '1' : '0');
}
return sb.toString();
}
}
// 解法二:long 数组位集(对照)
public static class BitsetLong {
private long[] blocks;
private int size;
private int ones;
private boolean flipped;
public BitsetLong(int size) {
this.size = size;
blocks = new long[(size + 63) / 64];
}
public void fix(int idx) {
int b = idx >> 6, off = idx & 63;
long cur = blocks[b];
boolean isOne = ((cur ^ (flipped ? -1L : 0L)) >> off & 1L) == 1L;
if (!isOne) {
blocks[b] ^= 1L << off;
ones++;
}
}
public void unfix(int idx) {
int b = idx >> 6, off = idx & 63;
boolean isOne = ((blocks[b] ^ (flipped ? -1L : 0L)) >> off & 1L) == 1L;
if (isOne) {
blocks[b] ^= 1L << off;
ones--;
}
}
public void flip() {
flipped = !flipped;
ones = size - ones;
}
public boolean all() { return ones == size; }
public boolean one() { return ones > 0; }
public int count() { return ones; }
public String toString() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < size; i++) {
int b = i >> 6, off = i & 63;
long v = (blocks[b] >> off & 1L) ^ (flipped ? 1L : 0L);
sb.append(v == 1 ? '1' : '0');
}
return sb.toString();
}
}
public static void main(String[] args) {
// 示例流程验证
Bitset bs = new Bitset(5);
bs.fix(3);
bs.fix(1);
bs.flip();
System.out.println(bs.all()); // false
bs.unfix(0);
bs.flip();
System.out.println(bs.one()); // true
bs.unfix(0);
System.out.println(bs.count()); // 2
System.out.println(bs.toString()); // 01010
BitsetLong b2 = new BitsetLong(5);
b2.fix(3); b2.fix(1); b2.flip();
System.out.println(b2.all());
b2.unfix(0); b2.flip();
System.out.println(b2.one());
b2.unfix(0);
System.out.println(b2.count());
System.out.println(b2.toString());
}
}
复杂度分析
复杂度:O(1) fix/unfix/flip/count/all/one,O(n) toString;空间 O(n)。
复杂度:O(1) 均摊,O(n) toString/toArray;空间 O(n/64)。
2167. Minimum Time to Remove All Cars Containing Illegal Goods(移除所有载有违禁货物车厢所需的最少时间)
题目描述
给你一个下标从 0 开始的二进制字符串 s,表示一个列车车厢序列。s[i]='1' 表示含违禁货物。
你需要清理掉所有载有违禁货物的车厢,可以执行三种操作(不限次数):
- 从列车左端移除一节车厢,用去 1 单位时间;
- 从列车右端移除一节车厢,用去 1 单位时间;
- 从列车任意位置移除一节车厢,用去 2 单位时间。
返回移除所有载有违禁货物车厢所需要的最少单位时间数。
示例 1
s = "1100101" → 5
示例 2
s = "0010" → 2
约束:
- 1 <=
s.length<= 2*10^5,s[i]为 '0' 或 '1'
解题思路
解法一(主提交版本)
前缀最小值枚举中间段(主提交版本)
- 最优方案形如:左端连续移除 [0, l),中间对 [l, r) 内的 1 用「任意位置」代价 2 各移除,
右端连续移除 [r, n)。总代价 = l + 2 * cnt1(l, r) + (n - r)。 - 用前缀 1 计数 pref:代价 = l + 2*(prefr-prefl) + (n-r)
= (l - 2prefl) + (2 prefr + n - r)。
对每个 r 维护 l <= r 的最小 (l - 2*prefl),O(n) 求解。
复杂度:O(n) 时间,O(n) 空间(前缀数组)。
解法二
滚动状态 DP(对照) - 从左往右维护 best:处理完前缀的最小代价(允许左端与中间操作,不含右端)。
遇 '1':best = min(best + 2, 位置+1)(前者为中间移除,后者为全部改走左端);
遇 '0':best 不变。枚举分界 i,答案 = min(besti + 右侧从 i+1 起的最右 1 的右端代价)。
右侧右端代价可预处理 sufi:从 i 起到末尾全部走右端移除的代价(= n - 最左 1 的位置,若无 1 则 0)。
复杂度:O(n) 时间,O(n) 空间。
代码实现
java
public class Solution {
// 解法一:前缀最小值公式(主提交版本)
public int minimumTime(String s) {
int n = s.length();
int[] pref = new int[n + 1];
for (int i = 0; i < n; i++) {
pref[i + 1] = pref[i] + (s.charAt(i) == '1' ? 1 : 0);
}
int bestL = 0; // min over l<=r of (l - 2*pref[l]),初始 l=0 → 0
int ans = Integer.MAX_VALUE;
for (int r = 0; r <= n; r++) {
// 允许 l == r(中间段为空)
bestL = Math.min(bestL, r - 2 * pref[r]);
int cost = bestL + 2 * pref[r] + (n - r);
ans = Math.min(ans, cost);
}
return ans;
}
// 解法二:滚动 DP + 后缀右端代价(对照)
public int minimumTimeDp(String s) {
int n = s.length();
// suf[i]:从 i 起到末尾全部右端移除的代价 = n - (i 起最左 1 的位置)
int[] suf = new int[n + 1];
int firstOneFromRight = -1;
for (int i = n - 1; i >= 0; i--) {
if (s.charAt(i) == '1') firstOneFromRight = i;
suf[i] = firstOneFromRight == -1 ? 0 : n - firstOneFromRight;
}
int best = 0; // 前缀处理(左端+中间)最小代价
int ans = Integer.MAX_VALUE;
for (int i = 0; i < n; i++) {
if (s.charAt(i) == '1') {
best = Math.min(best + 2, i + 1); // 中间移除 or 全部改左端到 i
}
ans = Math.min(ans, best + suf[i + 1]); // 右侧 i+1 起走右端
}
ans = Math.min(ans, best + suf[0] < 0 ? Integer.MAX_VALUE : best + suf[0]); // 边界兜底
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.minimumTime("1100101") + " " + sol.minimumTimeDp("1100101")); // 5 5
System.out.println(sol.minimumTime("0010") + " " + sol.minimumTimeDp("0010")); // 2 2
System.out.println(sol.minimumTime("0") + " " + sol.minimumTimeDp("0")); // 0 0
System.out.println(sol.minimumTime("1") + " " + sol.minimumTimeDp("1")); // 1 1
System.out.println(sol.minimumTime("01010") + " " + sol.minimumTimeDp("01010")); // 4 4
System.out.println(sol.minimumTime("111") + " " + sol.minimumTimeDp("111")); // 3(左端移除到 pos2 = 3;或中间 2+1? 总 3)
}
}
复杂度分析
复杂度:O(n) 时间,O(n) 空间(前缀数组)。
复杂度:O(n) 时间,O(n) 空间。
2168. Unique Substrings With Equal Digit Frequency(每个数字的频率都相同的不同子字符串的数量)
题目描述
给你一个由数字组成的字符串 s,返回 s 中不同子字符串的数量,其中每个数字出现的频率都相同。
即子串中所有出现过的数字(0~9 中的若干种),其出现次数都相等。
示例 1
s = "1212" → 5("1","2","12","21","1212","12" 出现两次只计一次)
示例 2
s = "12321" → 9
约束:
- 1 <=
s.length<= 1000,s只包含阿拉伯数字
解题思路
解法一(主提交版本)
枚举子串 + 频率校验 + 集合去重(主提交版本)
- 枚举所有起止点,滑动扩展右端点时维护 cnt10 与 distinct 计数:
每次加入字符后,检查「所有出现过的数字频率是否相等」:维护 maxFreq、零频个数,
条件 maxFreq * distinct == 子串长度 且 所有非零频均为 maxFreq。 - 用 HashSet 存储子串哈希去重(用双哈希或字符串 hashCode 碰撞风险小,长度 ≤ 1000);
更稳妥:存 2 个 hash(模大质数)或直接存 String(内存可控:子串数 O(n²) ≤ 5*10^5,String 可接受)。 - 这里用字符串去重保证零碰撞(n ≤ 1000,子串数 ≤ 5*10^5,OK)。
复杂度:O(n² * 10) 时间(频率维护常数),O(n²) 空间(集合)。
解法二
前缀计数 + 二元组窗口(对照) - 预计算每种数字的前缀出现次数,O(1) 查询任意子串的频率,再统一校验;逻辑与解法一同构,
用于交叉验证(同样用 Set 去重)。
复杂度:O(n² * 10) 时间,O(n²) 空间。
代码实现
java
import java.util.HashSet;
import java.util.Set;
public class Solution {
// 解法一:滑动窗口频率校验(主提交版本)
public int equalDigitFrequency(String s) {
int n = s.length();
Set<String> set = new HashSet<>();
for (int i = 0; i < n; i++) {
int[] cnt = new int[10];
int distinct = 0;
int maxFreq = 0;
for (int j = i; j < n; j++) {
int d = s.charAt(j) - '0';
if (cnt[d] == 0) distinct++;
cnt[d]++;
if (cnt[d] > maxFreq) maxFreq = cnt[d];
if (maxFreq * distinct == (j - i + 1)) {
set.add(s.substring(i, j + 1));
}
}
}
return set.size();
}
// 解法二:前缀计数校验(对照)
public int equalDigitFrequencyPref(String s) {
int n = s.length();
int[][] pref = new int[n + 1][10];
for (int i = 0; i < n; i++) {
for (int d = 0; d < 10; d++) pref[i + 1][d] = pref[i][d];
pref[i + 1][s.charAt(i) - '0']++;
}
Set<String> set = new HashSet<>();
for (int i = 0; i < n; i++) {
for (int j = i; j < n; j++) {
int[] cnt = new int[10];
int distinct = 0, maxFreq = 0;
for (int d = 0; d < 10; d++) {
cnt[d] = pref[j + 1][d] - pref[i][d];
if (cnt[d] > 0) {
distinct++;
maxFreq = Math.max(maxFreq, cnt[d]);
}
}
if (maxFreq * distinct == (j - i + 1)) {
set.add(s.substring(i, j + 1));
}
}
}
return set.size();
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.equalDigitFrequency("1212")
+ " " + sol.equalDigitFrequencyPref("1212")); // 5 5
System.out.println(sol.equalDigitFrequency("12321")
+ " " + sol.equalDigitFrequencyPref("12321")); // 9 9
System.out.println(sol.equalDigitFrequency("11")
+ " " + sol.equalDigitFrequencyPref("11")); // 1("11" 两个 1 频率相等)
System.out.println(sol.equalDigitFrequency("1")
+ " " + sol.equalDigitFrequencyPref("1")); // 1
}
}
复杂度分析
复杂度:O(n² * 10) 时间(频率维护常数),O(n²) 空间(集合)。
复杂度:O(n² * 10) 时间,O(n²) 空间。
2169. Count Operations to Obtain Zero(得到 0 的操作数)
题目描述
给你两个非负整数 num1 和 num2。每一步操作中,如果 num1 >= num2,必须用 num1 减 num2;否则,
必须用 num2 减 num1。返回使 num1 = 0 或 num2 = 0 的操作数。
示例 1
num1 = 2, num2 = 3 → 3
示例 2
num1 = 10, num2 = 10 → 1
约束:
0 <= num1, num2 <= 10^5
解题思路
解法一(主提交版本)
模拟相减(主提交版本,简单直观)
- 循环按规则大减小,计数,直到有一方为 0。
复杂度:最坏 O(max/min) 时间(如 1 和 10^5),O(1) 空间。
解法二
辗转相除优化(对照) - 连续相减等价于除法:当 num1 是较大者时,一次操作可直接减 num1/num2 次取余,
步数累加 floor(num1/num2)=q,num1 = num1 % num2;交换继续,本质是欧几里得算法的计数版本。
复杂度:O(log max) 时间,O(1) 空间。用于交叉验证。
代码实现
java
public class Solution {
// 解法一:模拟相减(主提交版本)
public int countOperations(int num1, int num2) {
int ops = 0;
while (num1 != 0 && num2 != 0) {
if (num1 >= num2) num1 -= num2;
else num2 -= num1;
ops++;
}
return ops;
}
// 解法二:辗转相除计数(对照)
public int countOperationsDiv(int num1, int num2) {
int ops = 0;
while (num1 != 0 && num2 != 0) {
if (num1 >= num2) {
ops += num1 / num2;
num1 %= num2;
} else {
ops += num2 / num1;
num2 %= num1;
}
}
return ops;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.countOperations(2, 3) + " " + sol.countOperationsDiv(2, 3)); // 3 3
System.out.println(sol.countOperations(10, 10) + " " + sol.countOperationsDiv(10, 10)); // 1 1
System.out.println(sol.countOperations(0, 5) + " " + sol.countOperationsDiv(0, 5)); // 0 0
System.out.println(sol.countOperations(100000, 1) + " " + sol.countOperationsDiv(100000, 1)); // 100000 100000
}
}
复杂度分析
复杂度:最坏 O(max/min) 时间(如 1 和 10^5),O(1) 空间。
复杂度:O(log max) 时间,O(1) 空间。用于交叉验证。
2170. Minimum Operations to Make the Array Alternating(使数组变成交替数组的最少操作数)
题目描述
给你一个下标从 0 开始的数组 nums,该数组由 n 个正整数组成。如果满足下述条件,则数组 nums 是一个交替数组:
nums[i-2]==nums[i](2 <=i<=n-1);nums[i-1]!=nums[i](1 <=i<=n-1)。
在一步操作中,你可以选择下标i并将nums[i]更改为任一正整数。返回使数组变成交替数组的最少操作数。
示例 1
nums = [3,1,3,2,4,3] → 3
示例 2
nums = [1,2,2,2,2] → 2
约束:
- 1 <=
nums.length<= 10^5,1 <=nums[i]<= 10^5
解题思路
解法一(主提交版本)
奇偶位众数贪心(主提交版本)
- 若交替成立,则偶数位值全相同、奇数位值全相同,且二者不同。
- 统计偶数位各值出现次数与奇数位各值出现次数。保留最多的值即可使操作数最少:
若偶位众数与奇位众数不同,ans = n - cntEvenMax - cntOddMax;
若相同,则需二选一降级为次多值,取两种情况最小值。
复杂度:O(n) 时间,O(n) 空间。
解法二
频率数组(对照) - 值域 ≤ 10^5,用数组计数代替 Map;同样取两个最大频率组合。用于交叉验证。
复杂度:O(n) 时间,O© 空间。
代码实现
java
import java.util.HashMap;
import java.util.Map;
public class Solution {
// 解法一:奇偶位众数贪心(主提交版本)
public int minimumOperations(int[] nums) {
int n = nums.length;
Map<Integer, Integer> even = new HashMap<>();
Map<Integer, Integer> odd = new HashMap<>();
for (int i = 0; i < n; i++) {
if (i % 2 == 0) even.merge(nums[i], 1, Integer::sum);
else odd.merge(nums[i], 1, Integer::sum);
}
int[] eTop = top2(even);
int[] oTop = top2(odd);
if (eTop[1] != oTop[1]) return n - eTop[0] - oTop[0];
return n - Math.max(eTop[0] + oTop[2], eTop[2] + oTop[0]);
}
// 返回 {maxCnt, maxVal, secondMaxCnt};secondMaxCnt 传回次大计数(值可以同 maxVal 无关)
private int[] top2(Map<Integer, Integer> map) {
int maxCnt = 0, maxVal = -1;
int secondCnt = 0;
for (Map.Entry<Integer, Integer> e : map.entrySet()) {
int c = e.getValue();
if (c > maxCnt) {
secondCnt = maxCnt;
maxCnt = c;
maxVal = e.getKey();
} else if (c > secondCnt) {
secondCnt = c;
}
}
return new int[]{maxCnt, maxVal, secondCnt};
}
// 解法二:频率数组(对照)
public int minimumOperationsArr(int[] nums) {
int n = nums.length;
int maxV = 0;
for (int v : nums) maxV = Math.max(maxV, v);
int[] ce = new int[maxV + 1];
int[] co = new int[maxV + 1];
for (int i = 0; i < n; i++) {
if (i % 2 == 0) ce[nums[i]]++;
else co[nums[i]]++;
}
int[][] et = freTop2(ce);
int[][] ot = freTop2(co);
if (et[0][0] != ot[0][0]) return n - et[0][1] - ot[0][1];
return n - Math.max(et[0][1] + ot[1][1], et[1][1] + ot[0][1]);
}
// 返回 [[val, cnt] 按 cnt 降序的前两名]
private int[][] freTop2(int[] cnt) {
int[][] top = {{-1, 0}, {-1, 0}};
for (int v = 1; v < cnt.length; v++) {
if (cnt[v] > top[0][1]) {
top[1][0] = top[0][0]; top[1][1] = top[0][1];
top[0][0] = v; top[0][1] = cnt[v];
} else if (cnt[v] > top[1][1]) {
top[1][0] = v; top[1][1] = cnt[v];
}
}
return top;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.minimumOperations(new int[]{3, 1, 3, 2, 4, 3})
+ " " + sol.minimumOperationsArr(new int[]{3, 1, 3, 2, 4, 3})); // 3 3
System.out.println(sol.minimumOperations(new int[]{1, 2, 2, 2, 2})
+ " " + sol.minimumOperationsArr(new int[]{1, 2, 2, 2, 2})); // 2 2
System.out.println(sol.minimumOperations(new int[]{1, 1, 1, 1})
+ " " + sol.minimumOperationsArr(new int[]{1, 1, 1, 1})); // 2(全 1:改一半)
System.out.println(sol.minimumOperations(new int[]{1})
+ " " + sol.minimumOperationsArr(new int[]{1})); // 0
}
}
复杂度分析
复杂度:O(n) 时间,O(n) 空间。
复杂度:O(n) 时间,O© 空间。
2171. Removing Minimum Number of Magic Beans(拿出最少数目的魔法豆)
题目描述
给定一个正整数数组 beans,其中每个整数表示一个袋子里装的魔法豆的数目。
请你从每个袋子中拿出一些豆子(也可以不拿出),使得剩下的非空袋子中(即至少还有一颗魔法豆的袋子)
魔法豆的数目相等。一旦把魔法豆从袋子中取出,你不能再将它放到任何袋子中。
返回你需要拿出魔法豆的最少数目。
示例 1
beans = [4,1,6,5] → 4
示例 2
beans = [2,10,3,2] → 7
约束:
- 1 <= beans.length <= 10^5,1 <= beansi <= 10^5
解题思路
解法一(主提交版本)
排序 + 前缀和枚举目标值(主提交版本)
- 若所有非空袋子最终同为目标值 v,则对每袋 beansi:若 beansi >= v,拿出 beansi-v;
若 beansi < v,整袋拿空(拿出 beansi)。总拿出量 = Σ beans - v * (beansi >= v 的袋数)。 - 排序后,枚举每袋的 beansi 作为目标 v(最优 v 必为某袋原值:若 v 介于两袋值之间,取大值更优),
用后缀和快速计算总拿出量,取最小。
复杂度:O(n log n) 时间,O(n) 空间。
解法二
枚举前缀/后缀累计(对照) - 排序后,设拿空前面 k 袋、后面每袋保留到 beansk:代价 = prefixk + (totalSuf - beansk*(n-k)),
遍历 k 取最小。与解法一等价,实现用前缀和数组展开,用于交叉验证。
复杂度:O(n log n) 时间,O(n) 空间。
代码实现
java
import java.util.Arrays;
public class Solution {
// 解法一:排序 + 后缀代价(主提交版本)
public long minimumRemoval(int[] beans) {
Arrays.sort(beans);
int n = beans.length;
long total = 0;
for (int b : beans) total += b;
long best = Long.MAX_VALUE;
// 以 beans[i] 为目标值:所有 > 它的留到该值,<= 它的全部拿空
// 等价代价 = total - beans[i] * (n - i)(后缀中 >= beans[i] 的袋数)
for (int i = 0; i < n; i++) {
long keep = (long) beans[i] * (n - i);
long cost = total - keep;
best = Math.min(best, cost);
}
return best;
}
// 解法二:前缀和展开(对照)
public long minimumRemovalPrefix(int[] beans) {
Arrays.sort(beans);
int n = beans.length;
long[] pref = new long[n + 1];
for (int i = 0; i < n; i++) pref[i + 1] = pref[i] + beans[i];
long best = Long.MAX_VALUE;
for (int i = 0; i < n; i++) {
// 前 i 袋拿空,[i, n) 保留到 beans[i]
long cost = pref[i] + (pref[n] - pref[i]) - (long) beans[i] * (n - i);
best = Math.min(best, cost);
}
return best;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.minimumRemoval(new int[]{4, 1, 6, 5})
+ " " + sol.minimumRemovalPrefix(new int[]{4, 1, 6, 5})); // 4 4
System.out.println(sol.minimumRemoval(new int[]{2, 10, 3, 2})
+ " " + sol.minimumRemovalPrefix(new int[]{2, 10, 3, 2})); // 7 7
System.out.println(sol.minimumRemoval(new int[]{1})
+ " " + sol.minimumRemovalPrefix(new int[]{1})); // 0
System.out.println(sol.minimumRemoval(new int[]{1, 1, 100})
+ " " + sol.minimumRemovalPrefix(new int[]{1, 1, 100})); // 2(拿空两个 1)
}
}
复杂度分析
复杂度:O(n log n) 时间,O(n) 空间。
2172. Maximum AND Sum of Array(数组的最大与和)
题目描述
给你一个长度为 n 的整数数组 nums 和一个整数 numSlots,满足 2 * numSlots >= n。总共有 numSlots 个篮子,
编号为 1 到 numSlots。你需要把所有 n 个整数分到这些篮子中,且每个篮子至多有 2 个整数。
一种分配方案的与和定义为每个数与它所在篮子编号的按位与运算结果之和。
请你返回将 nums 中所有数放入 numSlots 个篮子中的最大与和。
示例 1
nums = [1,2,3,4,5,6], numSlots = 3 → 9
示例 2
nums = [1,3,10,4,7,1], numSlots = 9 → 24
约束:
- 1 <=
numSlots<= 9,1 <=n<= 2*numSlots,1 <=nums[i]<= 15
解题思路
解法一(主提交版本)
三进制状压 DP(主提交版本)
- 每个篮子最多放 2 个数,可用三进制状态:第 k 位(0/1/2)表示篮子 k 当前放了几个数。
- dpmask:已处理完 mask 中已分配数字后的最大与和(mask 为三进制,范围 3^9 = 19683)。
- 转移:枚举下一个数字 numsi(i = popcount-of-mask 顺序分配),对每个篮子 k(位 < 2)尝试放入,
新状态收益 (numsi & (k+1))。 - 也可用二进制双槽位技巧:把每个篮子拆成两个槽位(2numSlots 个槽),用 2 numSlots bit 掩码,
但同一篮子的两个槽要共享篮子号------标准解法用三进制更直接。此处实现三进制。
复杂度:O(3^numSlots * numSlots * n) ≈ 2*10^4 * 9 * 18,可行;空间 O(3^numSlots)。
解法二
二进制槽位 + 奇偶配对(对照) - 将 numSlots 个篮子拆成 2*numSlots 个「槽」;槽 2k 与 2k+1 属于篮子 k。
- dpmask:mask 是 2numSlots 位,表示哪些槽已被占用;依次为每个数选择空槽
(槽位收益 = numsi & (槽所属篮子号))。由于每个篮子两个槽、且一个数只占一个槽,
该模型与原问题等价。
复杂度:O(2^(2numSlots) * (2*numSlots) * n) ≈ 2^18 * 18 * 18,可行;用于交叉验证。
代码实现
java
import java.util.Arrays;
public class Solution {
// 解法一:三进制状压 DP(主提交版本)
public int maximumANDSum(int[] nums, int numSlots) {
int n = nums.length;
int total = 1;
for (int i = 0; i < numSlots; i++) total *= 3; // 3^numSlots
int[] dp = new int[total];
Arrays.fill(dp, -1);
dp[0] = 0;
int ans = 0;
for (int mask = 0; mask < total; mask++) {
if (dp[mask] < 0) continue;
int cnt = cntDigits(mask, numSlots); // 已放入数字个数
if (cnt == n) {
ans = Math.max(ans, dp[mask]);
continue;
}
int x = nums[cnt]; // 顺序处理(任意顺序不影响最优值)
int tmp = mask;
for (int k = 0; k < numSlots; k++) {
int digit = tmp % 3;
if (digit < 2) {
int nxt = mask + pow3(k); // 该篮子数 +1
int gain = x & (k + 1);
dp[nxt] = Math.max(dp[nxt], dp[mask] + gain);
}
tmp /= 3;
}
}
return ans;
}
private int cntDigits(int mask, int numSlots) {
int c = 0;
for (int i = 0; i < numSlots; i++) {
c += mask % 3;
mask /= 3;
}
return c;
}
private int pow3(int e) {
int r = 1;
for (int i = 0; i < e; i++) r *= 3;
return r;
}
// 解法二:二进制槽位 DP(对照)
public int maximumANDSumSlots(int[] nums, int numSlots) {
int m = 2 * numSlots;
int size = 1 << m;
int[] dp = new int[size];
Arrays.fill(dp, -1);
dp[0] = 0;
int ans = 0;
for (int mask = 0; mask < size; mask++) {
if (dp[mask] < 0) continue;
int used = Integer.bitCount(mask);
if (used == nums.length) {
ans = Math.max(ans, dp[mask]);
continue;
}
int x = nums[used];
for (int s = 0; s < m; s++) {
if ((mask >> s & 1) == 0) {
int nxt = mask | (1 << s);
int basket = s / 2 + 1; // 槽 s 属于篮子
int gain = x & basket;
dp[nxt] = Math.max(dp[nxt], dp[mask] + gain);
}
}
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.maximumANDSum(new int[]{1, 2, 3, 4, 5, 6}, 3)
+ " " + sol.maximumANDSumSlots(new int[]{1, 2, 3, 4, 5, 6}, 3)); // 9 9
System.out.println(sol.maximumANDSum(new int[]{1, 3, 10, 4, 7, 1}, 9)
+ " " + sol.maximumANDSumSlots(new int[]{1, 3, 10, 4, 7, 1}, 9)); // 24 24
System.out.println(sol.maximumANDSum(new int[]{1}, 1)
+ " " + sol.maximumANDSumSlots(new int[]{1}, 1)); // 1(1&1=1)
System.out.println(sol.maximumANDSum(new int[]{2}, 1)
+ " " + sol.maximumANDSumSlots(new int[]{2}, 1)); // 0(2&1=0)
}
}
复杂度分析
复杂度:O(3^numSlots * numSlots * n) ≈ 210^4 * 9 * 18,可行;空间 O(3^numSlots)。
复杂度:O(2^(2numSlots) * (2*numSlots) * n) ≈ 2^18 * 18 * 18,可行;用于交叉验证。
2173. Longest Winning Streak(最多连胜的次数)
题目类型:数据库(SQL)
题目描述
表: Matches
+-------------+------+
| Column Name | Type |
+-------------+------+
| player_id | int |
| match_day | date |
| result | enum |
+-------------+------+
(player_id, match_day) 是该表的主键(具有唯一值的列的组合)。
每一行包括了:参赛选手 id、比赛时间、比赛结果。
比赛结果(result)的枚举类型为 ('Win', 'Draw', 'Lose')。
选手的连胜数是指连续获胜的次数,且没有被平局或输球中断。
编写解决方案来计算每个参赛选手最多的连胜数。结果可以以任意顺序返回。
示例 1:
输入:
Matches 表:
+-----------+------------+--------+
| player_id | match_day | result |
+-----------+------------+--------+
| 1 | 2022-01-17 | Win |
| 1 | 2022-01-18 | Win |
| 1 | 2022-01-25 | Win |
| 1 | 2022-01-31 | Draw |
| 1 | 2022-02-08 | Win |
| 2 | 2022-02-06 | Lose |
| 2 | 2022-02-08 | Lose |
| 3 | 2022-03-30 | Win |
+-----------+------------+--------+
输出:
+-----------+----------------+
| player_id | longest_streak |
+-----------+----------------+
| 1 | 3 |
| 2 | 0 |
| 3 | 1 |
+-----------+----------------+
解释:
- 玩家 1 从 2022-01-17 到 2022-01-25 连续赢了三场(最多连胜 3);2 月 8 日又赢 1 场但被平局截断。
- 玩家 2 两场全输,连胜为 0。
- 玩家 3 赢了一场,连胜为 1。
解题思路
核心技巧:连胜分组 = 两个 ROW_NUMBER 的差
连续获胜段可以用「差分分组」识别:对每个选手按比赛日排序:
rn= 该选手所有比赛的行号;wr= 该选手result='Win'的行号(只给 Win 行递增编号)。
在连续 Win 段内,rn 与 wr 同步 +1,rn - wr 恒定;一旦出现 Draw/Lose,rn 继续 +1 而 wr 不再变,差立即跳变。因此每个恒定差值组就是一段连续 Win。
解法一(窗口函数 + 组内计数):
- CTE
numbered计算rn - wr作为grp; - 只保留
result = 'Win'的行,按(player_id, grp)分组计数,得到每段的连胜长度; - 对每个选手取最大值;没有获胜记录的选手用
LEFT JOIN补0。
解法二(对照):变量模拟逐一递推(略,同思路的过程式写法)。
代码实现
sql
-- 解法一(推荐):ROW_NUMBER 差分分组 + 组内计数
WITH numbered AS (
SELECT player_id, match_day, result,
ROW_NUMBER() OVER (PARTITION BY player_id ORDER BY match_day) AS rn,
ROW_NUMBER() OVER (PARTITION BY player_id, result ORDER BY match_day) AS wr
FROM Matches
),
streaks AS (
SELECT player_id, COUNT(*) AS len
FROM numbered
WHERE result = 'Win'
GROUP BY player_id, (rn - wr) -- 每一段连续 Win 的差分常量
)
SELECT p.player_id, COALESCE(MAX(s.len), 0) AS longest_streak
FROM (SELECT DISTINCT player_id FROM Matches) p
LEFT JOIN streaks s ON p.player_id = s.player_id
GROUP BY p.player_id;
sql
-- 解法二(对照):分组键显式命名为 grp 的写法(逻辑相同)
WITH numbered AS (
SELECT player_id, match_day, result,
ROW_NUMBER() OVER (PARTITION BY player_id ORDER BY match_day)
- ROW_NUMBER() OVER (PARTITION BY player_id, result ORDER BY match_day) AS grp
FROM Matches
)
SELECT player_id, COALESCE(MAX(cnt), 0) AS longest_streak
FROM (SELECT DISTINCT player_id FROM Matches) p
LEFT JOIN (
SELECT player_id, COUNT(*) AS cnt
FROM numbered
WHERE result = 'Win'
GROUP BY player_id, grp
) s ON p.player_id = s.player_id
GROUP BY p.player_id;
复杂度分析
- 时间复杂度 :
O(n log n),其中n为Matches行数;按(player_id, result)排序为主导。 - 空间复杂度 :
O(n),窗口排序与 CTE 中间结果与行数成正比。
2174. Remove All Ones With Row and Column Flips II(通过翻转行或列来去除所有的 1 II)
题目描述
给定下标从 0 开始的 m x n 二进制矩阵 grid。在一次操作中,可以选择满足 grid[i][j] == 1 的任意 i 和 j,
并将第 i 行和第 j 列中的所有单元格的值更改为零。返回从 grid 中删除所有 1 所需的最小操作数。
示例 1
grid = [[1,1,1],[1,1,1],[0,1,0]] → 2
示例 2
grid = [[0,1,0],[1,0,1],[0,1,0]] → 2
示例 3
grid = [[0,0],[0,0]] → 0
约束:
- 1 <=
m,n<= 15,1 <=m*n<= 15(格子总数很小),grid[i][j]为 0 或 1
解题思路
解法一(主提交版本)
状态压缩 + 最短路(主提交版本)
- 每个 1 对应一个可执行操作:(i, j)。执行一次操作将行 i 与列 j 清零。
- 用位掩码表示整个矩阵(m*n <= 15 位)。BFS:从初始掩码出发,每次对当前掩码中仍为 1 的格子
(可只选第一个 1,贪心证明仍完备)执行「清除其行和列」的操作,得到新掩码,步数 +1,
直到掩码为 0。BFS 层数即最少操作数。 - 优化:每步只枚举「第一个 1」的操作(其行或列清零的任一组合),因为最终所有 1 都会被处理,
先处理哪个 1 不影响所需操作集合的下界------证明:每个操作必须先覆盖某个仍未清 1 的格子,
按格子出现顺序选择即可覆盖全部方案。- BFS 最坏状态数 2^15 = 32768。
复杂度:O(2^(mn) * (m+n)),m n <= 15,可行;空间 O(2^(m*n))。
解法二
递归枚举行集(对照) - 观察:若选定要操作的行集合 R,操作后矩阵中未被行覆盖的 1 必须全部由列操作清除。
枚举 R(2^m 种),对每种 R 统计:剩余 1 所在列的并集 C,操作数 = |R| + |C|,
取最小。同样验证。
复杂度:O(2^m * m*n),m <= 15 可行;用于交叉验证。
代码实现
java
import java.util.ArrayList;
import java.util.List;
public class Solution {
// 解法一:BFS 状态压缩(主提交版本)
public int removeOnes(int[][] grid) {
int m = grid.length, n = grid[0].length;
int total = m * n;
int start = 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == 1) start |= 1 << (i * n + j);
}
}
if (start == 0) return 0;
int[] dist = new int[1 << total];
java.util.Arrays.fill(dist, -1);
java.util.ArrayDeque<Integer> q = new java.util.ArrayDeque<>();
dist[start] = 0;
q.offer(start);
while (!q.isEmpty()) {
int mask = q.poll();
int d = dist[mask];
// 枚举所有仍为 1 的格子 (i, j):操作清除第 i 行与第 j 列
int tmp = mask;
while (tmp != 0) {
int low = Integer.numberOfTrailingZeros(tmp);
tmp &= (tmp - 1);
int r = low / n, c = low % n;
int nxt = clearRowCol(mask, r, c, n, total);
if (dist[nxt] == -1) {
dist[nxt] = d + 1;
if (nxt == 0) return d + 1;
q.offer(nxt);
}
}
}
return -1;
}
// 清除第 r 行与第 c 列所有位(bit 严格 < total,避免移位溢出)
private int clearRowCol(int mask, int r, int c, int n, int total) {
for (int j = 0; j < n; j++) mask &= ~(1 << (r * n + j));
for (int i = 0; ; i++) {
int bit = i * n + c;
if (bit >= total) break;
mask &= ~(1 << bit);
}
return mask;
}
// 行清零辅助(供对照解法使用)
private int clearRow(int mask, int r, int n) {
for (int j = 0; j < n; j++) mask &= ~(1 << (r * n + j));
return mask;
}
// 解法二:枚举行集 + 列集(对照,BFS 的等价验证方式)
public int removeOnesEnumRows(int[][] grid) {
int m = grid.length, n = grid[0].length;
int best = m + n;
for (int rowMask = 0; rowMask < (1 << m); rowMask++) {
int colMask = 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == 1 && (rowMask >> i & 1) == 0) {
colMask |= 1 << j; // 该 1 必须靠列清除
}
}
}
best = Math.min(best, Integer.bitCount(rowMask) + Integer.bitCount(colMask));
}
return best;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.removeOnes(new int[][]{{1, 1, 1}, {1, 1, 1}, {0, 1, 0}})
+ " " + sol.removeOnesEnumRows(new int[][]{{1, 1, 1}, {1, 1, 1}, {0, 1, 0}})); // 2 2
System.out.println(sol.removeOnes(new int[][]{{0, 1, 0}, {1, 0, 1}, {0, 1, 0}})
+ " " + sol.removeOnesEnumRows(new int[][]{{0, 1, 0}, {1, 0, 1}, {0, 1, 0}})); // 2 2
System.out.println(sol.removeOnes(new int[][]{{0, 0}, {0, 0}})
+ " " + sol.removeOnesEnumRows(new int[][]{{0, 0}, {0, 0}})); // 0 0
}
}
复杂度分析
复杂度:O(2^(mn) * (m+n)),m n <= 15,可行;空间 O(2^(mn))。
复杂度:O(2^m * mn),m <= 15 可行;用于交叉验证。
2175. The Change in Global Rankings(世界排名的变化)
题目类型:数据库(SQL)
题目描述
表: TeamPoints
+-------------+---------+
| Column Name | Type |
+-------------+---------+
| team_id | int |
| name | varchar |
| points | int |
+-------------+---------+
team_id 包含唯一值。这张表的每一行均包含了一支国家队的 ID,它所代表的国家,以及它在全球排名中的得分。没有两支队伍代表同一个国家。
表: PointsChange
+---------------+------+
| Column Name | Type |
+---------------+------+
| team_id | int |
| points_change | int |
+---------------+------+
team_id 包含唯一值。这张表的每一行均包含了一支国家队的 ID 以及它在世界排名中的得分的变化。分数的变化分以下情况:0 代表分数没有改变、正数代表分数增加、负数代表分数降低。TeamPoints 表中出现的每一个 team_id 均会在这张表中出现。
国家队的全球排名是按降序 排列所有队伍的得分后所得出的排名。如果两支队伍得分相同,我们将按其名称的字典顺序排列以打破平衡。
每支国家队的分数应根据其相应的 points_change 进行更新。编写解决方案来计算在分数更新后,每个队伍的全球排名的变化。以任意顺序返回结果。
示例 1:
输入:
TeamPoints 表:
+---------+-------------+--------+
| team_id | name | points |
+---------+-------------+--------+
| 3 | Algeria | 1431 |
| 1 | Senegal | 2132 |
| 2 | New Zealand | 1402 |
| 4 | Croatia | 1817 |
+---------+-------------+--------+
PointsChange 表:
+---------+---------------+
| team_id | points_change |
+---------+---------------+
| 3 | 399 |
| 2 | 0 |
| 4 | 13 |
| 1 | -22 |
+---------+---------------+
输出:
+---------+-------------+-----------+
| team_id | name | rank_diff |
+---------+-------------+-----------+
| 1 | Senegal | 0 |
| 4 | Croatia | -1 |
| 3 | Algeria | 1 |
| 2 | New Zealand | 0 |
+---------+-------------+-----------+
解释:
更新前排名(按 points 降序,同分按 name 字典序):
1. Senegal(2132) 2. Croatia(1817) 3. Algeria(1431) 4. New Zealand(1402)
更新后得分:
Senegal 2110、Croatia 1830、Algeria 1830、New Zealand 1402
更新后排名(Croatia 与 Algeria 同分 1830,按 name 字典序 Croatia < Algeria 但此处 Algeria 在 Croatia 之前?------
按示例输出:Algeria 升到第 2、Croatia 掉到第 3(rank_diff 分别为 +1 / -1),即按 name 字典序 Algeria < Croatia 时 Algeria 在前)。
解题思路
解法一:两次 RANK + 相减
- 先把两表
JOIN,计算每个队的更新后得分:points + points_change; - 计算更新前的排名
rank_before:按points DESC, name ASC; - 计算更新后的排名
rank_after:按(points + points_change) DESC, name ASC; rank_diff = rank_before - rank_after。
注意排名规则:同分用 RANK()(并列同名次)还是 ROW_NUMBER()(严格连续)取决于题意;「两支队伍得分相同按字典序打破平衡」说明排名是严格排序 ,因此用 ROW_NUMBER() 更符合"字典序打破平衡"的连续排名语义(示例中 rank_diff 按严格名次差计算)。
解法二(对照):窗口函数分别求名次后外层相减------与解法一同构造,只是把排名计算放进派生表再相减。
代码实现
sql
-- 解法一(推荐):JOIN + 两个窗口排名 + 相减
WITH joined AS (
SELECT t.team_id, t.name, t.points,
COALESCE(pc.points_change, 0) AS delta
FROM TeamPoints t
LEFT JOIN PointsChange pc ON t.team_id = pc.team_id
),
ranked AS (
SELECT team_id, name,
ROW_NUMBER() OVER (ORDER BY points DESC, name ASC) AS rank_before,
ROW_NUMBER() OVER (ORDER BY points + delta DESC, name ASC) AS rank_after
FROM joined
)
SELECT team_id, name, rank_before - rank_after AS rank_diff
FROM ranked;
sql
-- 解法二(对照):显式两步派生表(逻辑相同)
SELECT j.team_id, j.name,
rb.rk - ra.rk AS rank_diff
FROM (SELECT team_id, name, points, COALESCE(points_change, 0) AS delta
FROM TeamPoints t LEFT JOIN PointsChange pc USING (team_id)) j
JOIN (SELECT team_id,
ROW_NUMBER() OVER (ORDER BY points DESC, name ASC) AS rk
FROM TeamPoints) rb ON j.team_id = rb.team_id
JOIN (SELECT team_id,
ROW_NUMBER() OVER (ORDER BY (points + delta) DESC, name ASC) AS rk
FROM (SELECT t.team_id, t.name, t.points, COALESCE(pc.points_change, 0) AS delta
FROM TeamPoints t LEFT JOIN PointsChange pc USING (team_id)) x) ra ON j.team_id = ra.team_id;
复杂度分析
- 时间复杂度 :
O(n log n),其中n为队伍数;窗口排序为瓶颈。 - 空间复杂度 :
O(n),CTE / 派生表中间结果与队伍数成正比。
2176. Count Equal and Divisible Pairs in an Array(统计数组中相等且可以被整除的数对)
题目描述
给你一个下标从 0 开始长度为 n 的整数数组 nums 和一个整数 k,请你返回满足 0 <= i < j < n、
nums[i] == nums[j] 且 (i * j) 能被 k 整除的数对 (i, j) 的数目。
示例 1
nums = [3,1,2,2,2,1,3], k = 2 → 4
示例 2
nums = [1,2,3,4], k = 1 → 0
约束:
- 1 <=
nums.length<= 100,1 <=nums[i],k<= 100
解题思路
解法一(主提交版本)
双重循环暴力(主提交版本)
- 枚举所有 i < j,检查值相等且 (i * j) % k == 0。
复杂度:O(n²) 时间,O(1) 空间(n <= 100 可行)。
解法二
同值分组优化(对照) - 按值分组,只对同组下标两两检查乘积整除性;平均更快,用于交叉验证。
复杂度:O(n²/k) 平均,O(n) 空间。
代码实现
java
public class Solution {
// 解法一:双重循环暴力(主提交版本)
public int countPairs(int[] nums, int k) {
int n = nums.length;
int ans = 0;
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
if (nums[i] == nums[j] && (i * j) % k == 0) ans++;
}
}
return ans;
}
// 解法二:同值分组(对照)
public int countPairsGroup(int[] nums, int k) {
java.util.Map<Integer, java.util.List<Integer>> pos = new java.util.HashMap<>();
for (int i = 0; i < nums.length; i++) {
pos.computeIfAbsent(nums[i], x -> new java.util.ArrayList<>()).add(i);
}
int ans = 0;
for (java.util.List<Integer> list : pos.values()) {
int m = list.size();
for (int a = 0; a < m; a++) {
for (int b = a + 1; b < m; b++) {
if ((list.get(a) * list.get(b)) % k == 0) ans++;
}
}
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.countPairs(new int[]{3, 1, 2, 2, 2, 1, 3}, 2)
+ " " + sol.countPairsGroup(new int[]{3, 1, 2, 2, 2, 1, 3}, 2)); // 4 4
System.out.println(sol.countPairs(new int[]{1, 2, 3, 4}, 1)
+ " " + sol.countPairsGroup(new int[]{1, 2, 3, 4}, 1)); // 0 0
System.out.println(sol.countPairs(new int[]{5, 5, 5}, 1)
+ " " + sol.countPairsGroup(new int[]{5, 5, 5}, 1)); // 3(i*j 均整除 1)
}
}
复杂度分析
复杂度:O(n²) 时间,O(1) 空间(n <= 100 可行)。
复杂度:O(n²/k) 平均,O(n) 空间。
2177. Find Three Consecutive Integers That Sum to a Given Number(找到和为给定整数的三个连续整数)
题目描述
给你一个整数 num,请你返回三个连续的整数,它们的和为 num。如果 num 无法被表示成三个连续整数的和,
返回一个空数组。
示例 1
num = 33 → [10,11,12]
示例 2
num = 4 → []
约束:
0 <= num <= 10^15
解题思路
解法一(主提交版本)
数学推导(主提交版本)
- 三个连续整数为 x、x+1、x+2,和为 3x + 3 = num → x = num/3 - 1。
- 若 num 能被 3 整除(num % 3 == 0),则 x = num/3 - 1,返回 x, x+1, x+2;否则空数组。
复杂度:O(1) 时间,O(1) 空间。
解法二
枚举起点(对照) - 枚举 x 从 0 到 num/3,检查 x + (x+1) + (x+2) == num;num 很大时无法枚举,
仅作为小值交叉验证思路的说明,实际提交用解法一。
复杂度:O(1)(用公式)+ 提供 O(num) 降级参考。
代码实现
java
import java.util.ArrayList;
import java.util.List;
public class Solution {
// 解法一:数学推导(主提交版本)
public long[] sumOfThree(long num) {
if (num % 3 != 0) return new long[0];
long x = num / 3 - 1;
return new long[]{x, x + 1, x + 2};
}
// 解法二:双解法等价公式(对照写法)
public long[] sumOfThreeAlt(long num) {
long center = num / 3;
if (center * 3 == num) {
return new long[]{center - 1, center, center + 1};
}
return new long[0];
}
private static void print(long[] a) {
if (a.length == 0) {
System.out.println("[]");
return;
}
StringBuilder sb = new StringBuilder("[");
for (int i = 0; i < a.length; i++) {
if (i > 0) sb.append(",");
sb.append(a[i]);
}
sb.append("]");
System.out.println(sb.toString());
}
public static void main(String[] args) {
Solution sol = new Solution();
print(sol.sumOfThree(33)); // [10,11,12]
print(sol.sumOfThreeAlt(33));
print(sol.sumOfThree(4)); // []
print(sol.sumOfThreeAlt(4));
print(sol.sumOfThree(0)); // [-1,0,1]
print(sol.sumOfThreeAlt(0));
}
}
复杂度分析
复杂度:O(1) 时间,O(1) 空间。
复杂度:O(1)(用公式)+ 提供 O(num) 降级参考。
2178. Maximum Split of Positive Even Integers(拆分成最多数目的正偶数之和)
题目描述
给你一个整数 finalSum。请你将它拆分成若干个互不相同的正偶数之和,且拆分出来的正偶数数目最多。
返回一个整数数组,表示将整数拆分成最多数目的正偶数数组;如果没有办法拆分(finalSum 为奇数),
返回空数组。可以按任意顺序返回。
示例 1
finalSum = 12 → [2,4,6]
示例 2
finalSum = 7 → []
示例 3
finalSum = 28 → [6,8,2,12] 等(4 个)
约束:
1 <= finalSum <= 10^10
解题思路
解法一(主提交版本)
贪心取最小互异偶数(主提交版本)
- finalSum 为奇数直接返回空。
- 从最小的正偶数 2 开始依次取 2、4、6、8...(互不相同)累加,直到剩余值小于等于下一个偶数:
此时把剩余值并入最后一个偶数(保证互异且和为 finalSum)。 - 项数为满足 2+4+...+2k <= finalSum 的最大 k(约 √(finalSum) 项)。
复杂度:O(√finalSum) 时间,O(√finalSum) 空间。
解法二
公式 + 调整(对照) - 由 2+4+...+2k = k(k+1),先求最大 k 使 k(k+1) <= finalSum;余数 r = finalSum - k(k+1)
(r 必为偶数,因为 k(k+1) 为偶数、偶-偶=偶),把 r 加到最后一个元素上即得解。
复杂度:O(√finalSum)(求 k 用二分 O(log)),O(k) 空间。用于交叉验证。
代码实现
java
import java.util.ArrayList;
import java.util.List;
public class Solution {
// 解法一:贪心取最小互异偶数(主提交版本)
public List<Long> maximumEvenSplit(long finalSum) {
List<Long> ans = new ArrayList<>();
if ((finalSum & 1) == 1) return ans; // 奇数无法拆分
long cur = 2;
long remaining = finalSum;
while (remaining >= cur) {
ans.add(cur);
remaining -= cur;
cur += 2;
}
// 剩余 remaining < cur 且为偶数:并入最后一项
if (remaining > 0) {
ans.set(ans.size() - 1, ans.get(ans.size() - 1) + remaining);
}
return ans;
}
// 解法二:k(k+1) 公式 + 调整(对照)
public List<Long> maximumEvenSplitFormula(long finalSum) {
List<Long> ans = new ArrayList<>();
if ((finalSum & 1) == 1) return ans;
// 求最大 k 使 k(k+1) <= finalSum
long lo = 1, hi = 200000; // k² 上限:finalSum/2 级别
while (lo <= hi) {
long mid = (lo + hi) >>> 1;
if (mid * (mid + 1) <= finalSum) lo = mid + 1;
else hi = mid - 1;
}
long k = hi;
for (long i = 1; i <= k; i++) ans.add(2 * i);
if (ans.isEmpty()) return ans;
long r = finalSum - k * (k + 1);
if (r > 0) ans.set(ans.size() - 1, ans.get(ans.size() - 1) + r);
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.maximumEvenSplit(12)); // [2,4,6]
System.out.println(sol.maximumEvenSplitFormula(12));
System.out.println(sol.maximumEvenSplit(7)); // []
System.out.println(sol.maximumEvenSplitFormula(7));
System.out.println(sol.maximumEvenSplit(28)); // [2,4,6,16] 或 [2,4,6,8,8]?应为 [2,4,6,8,8] 不行,互异 → [2,4,6,16]?不,2+4+6+16=28,4 项
System.out.println(sol.maximumEvenSplitFormula(28)); // 实际贪心:2+4+6=12 余16 ≥8 → 加 8 → 8+? 详见输出
}
}
复杂度分析
复杂度:O(√finalSum) 时间,O(√finalSum) 空间。
复杂度:O(√finalSum)(求 k 用二分 O(log)),O(k) 空间。用于交叉验证。
2179. Count Good Triplets in an Array(统计数组中好三元组数目)
题目描述
给你两个下标从 0 开始且长度为 n 的整数数组 nums1 和 nums2,两者都是 0,1,...,`n`-1 的排列。
好三元组指的是 3 个互不相同的值,且它们在数组 nums1 和 nums2 中出现顺序保持一致:
若 pos1[v]、pos2[v] 分别为值 v 在两个数组中的位置,则好三元组 (x,y,z) 满足
pos1[x] < pos1[y] < pos1[z] 且 pos2[x] < pos2[y] < pos2[z]。返回好三元组的总数目。
示例 1
nums1 = [2,0,1,3], nums2 = [0,1,2,3] → 1
示例 2
nums1 = [4,0,1,3,2], nums2 = [4,1,0,2,3] → 4
约束:
- 3 <=
n<= 10^5,nums1/nums2是 0, `n`-1 的排列
解题思路
解法一(主提交版本)
映射 + 树状数组(主提交版本,O(n log n))
- 记 pos2v 为值 v 在 nums2 中的位置。按 nums1 的顺序遍历值 v(即依次处理 pos1 升序),
对每个 v:它作为「中间值」时,好三元组数 = (它左侧已处理且在 nums2 中排在它之前的个数)
× (它右侧未处理且...),等价于:
先处理前缀:leftLessv = 已处理且 pos2 < pos2v 的个数(树状数组实时维护 pos2 出现情况)。
右侧更大数 = (n - 1 - pos2v) - (已处理总数中 pos2 > pos2v),
其中左侧总数 = 已处理数量 i,leftGreater_v_in_prefix = i - leftLessv。
则 rightGreater = (n - 1 - pos2v) - (i - leftLessv)。
贡献 = leftLessv * rightGreater,累加。
复杂度:O(n log n) 时间,O(n) 空间。
解法二
归并/分治(对照思路说明) - 三维偏序的经典分治计数;此处以树状数组为主解,提供暴力 O(n²) 对照(仅演示逻辑)。
复杂度:O(n²) 时间(暴力对照),仅供小数据交叉验证。
代码实现
java
public class Solution {
// 解法一:pos2 映射 + 树状数组(主提交版本)
public long goodTriplets(int[] nums1, int[] nums2) {
int n = nums1.length;
int[] pos2 = new int[n];
for (int i = 0; i < n; i++) pos2[nums2[i]] = i;
BIT bit = new BIT(n);
long ans = 0;
for (int i = 0; i < n; i++) {
int v = nums1[i];
int p = pos2[v]; // v 在 nums2 中的位置
int leftLess = bit.query(p + 1); // 前缀中 pos2 < p(已处理 && 在 nums2 中更左)
int leftTotal = i; // 已处理的个数
int leftGreater = leftTotal - leftLess; // 已处理且 pos2 > p
int rightTotal = n - 1 - p; // nums2 中 p 右侧总个数
int rightGreater = rightTotal - leftGreater; // 未处理且 pos2 > p
ans += (long) leftLess * rightGreater;
bit.add(p + 1, 1);
}
return ans;
}
// 树状数组(1-indexed)
static class BIT {
private int[] tree;
private int n;
BIT(int n) {
this.n = n;
tree = new int[n + 1];
}
void add(int i, int v) {
while (i <= n) {
tree[i] += v;
i += i & -i;
}
}
int query(int i) {
int s = 0;
while (i > 0) {
s += tree[i];
i -= i & -i;
}
return s;
}
}
// 解法二:暴力对照(O(n²),仅小数据)
public long goodTripletsBrute(int[] nums1, int[] nums2) {
int n = nums1.length;
int[] pos1 = new int[n];
int[] pos2 = new int[n];
for (int i = 0; i < n; i++) pos1[nums1[i]] = i;
for (int i = 0; i < n; i++) pos2[nums2[i]] = i;
long ans = 0;
for (int x = 0; x < n; x++) {
for (int y = 0; y < n; y++) {
if (y == x) continue;
if (pos1[x] >= pos1[y]) continue;
for (int z = 0; z < n; z++) {
if (z == x || z == y) continue;
if (pos1[y] < pos1[z] && pos2[x] < pos2[y] && pos2[y] < pos2[z]) ans++;
}
}
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.goodTriplets(new int[]{2, 0, 1, 3}, new int[]{0, 1, 2, 3})
+ " " + sol.goodTripletsBrute(new int[]{2, 0, 1, 3}, new int[]{0, 1, 2, 3})); // 1 1
System.out.println(sol.goodTriplets(new int[]{4, 0, 1, 3, 2}, new int[]{4, 1, 0, 2, 3})
+ " " + sol.goodTripletsBrute(new int[]{4, 0, 1, 3, 2}, new int[]{4, 1, 0, 2, 3})); // 4 4
System.out.println(sol.goodTriplets(new int[]{0, 1, 2}, new int[]{0, 1, 2})
+ " " + sol.goodTripletsBrute(new int[]{0, 1, 2}, new int[]{0, 1, 2})); // 1 1
}
}
复杂度分析
复杂度:O(n log n) 时间,O(n) 空间。
复杂度:O(n²) 时间(暴力对照),仅供小数据交叉验证。
2180. Count Integers With Even Digit Sum(统计各位数字之和为偶数的整数个数)
题目描述
给你一个正整数 num ,请你统计并返回小于或等于 num 且各位数字之和为偶数的正整数的数目。
正整数的各位数字之和是其所有位上的对应数字相加的结果。
示例 1
- 输入 :
num= 4 - 输出: 2
- 解释: 只有 2 和 4 满足小于等于 4 且各位数字之和为偶数。
示例 2
- 输入 :
num= 30 - 输出: 14
- 解释 : 只有 14 个整数满足小于等于 30 且各位数字之和为偶数,分别是:
2、4、6、8、11、13、15、17、19、20、22、24、26 和 28。
提示: 1 <= num <= 1000
解题思路
解法一:简单计数(主提交版本)
从 1 枚举到 num,逐个计算各位数字之和并判断奇偶,统计偶数个数。
时间复杂度 O(num * digits) ≈ O(num log num),空间复杂度 O(1)。
解法二:数学推导
把计数区间扩为 0, num 再剔除 0。由于个位 0~9 中恰好 5 个
偶数字(0,2,4,6,8)、5 个奇数字,且更高位的数字之和在整十进位处保持不变,
因此每个完整十段 10k, 10k+9 中恰好有 5 个各位数字之和为偶数的数。
完整段贡献 (num / 10) * 5(其中包含 0,需减 1 剔除),
零头段 num - num%10, num 逐个计算各位数字之和即可。
时间复杂度 O(10) ≈ O(1),空间复杂度 O(1)。
代码实现
java
public class Solution {
public int countEven(int num) {
int ans = 0;
for (int i = 1; i <= num; i++) {
int s = sumDigits(i);
if ((s & 1) == 0) {
ans++;
}
}
return ans;
}
public int countEvenMath(int num) {
// [0, num] 内完整十段段数 * 5(注意 0 的各位数字之和为偶数,需要剔除)
int ans = (num / 10) * 5 - 1;
int r = num % 10;
for (int i = num - r; i <= num; i++) {
int s = sumDigits(i);
if ((s & 1) == 0) {
ans++;
}
}
return ans;
}
private int sumDigits(int x) {
int s = 0;
while (x > 0) {
s += x % 10;
x /= 10;
}
return s;
}
public static void main(String[] args) {
Solution sol = new Solution();
// 示例 1
check(sol.countEven(4) == 2, "示例1 countEven(4) => 2, 实际: " + sol.countEven(4));
check(sol.countEvenMath(4) == 2, "示例1 countEvenMath(4) => 2, 实际: " + sol.countEvenMath(4));
// 示例 2
check(sol.countEven(30) == 14, "示例2 countEven(30) => 14, 实际: " + sol.countEven(30));
check(sol.countEvenMath(30) == 14, "示例2 countEvenMath(30) => 14, 实际: " + sol.countEvenMath(30));
// 边界
for (int n = 1; n <= 1000; n++) {
int a = sol.countEven(n);
int b = sol.countEvenMath(n);
if (a != b) {
System.out.println("不一致: n=" + n + " countEven=" + a + " countEvenMath=" + b);
}
}
// 边界值
check(sol.countEven(1) == 0, "边界 countEven(1) => 0, 实际: " + sol.countEven(1));
check(sol.countEven(10) == 4, "边界 countEven(10) => 4, 实际: " + sol.countEven(10));
check(sol.countEven(1000) == 499, "边界 countEven(1000) => 499, 实际: " + sol.countEven(1000));
System.out.println("全部 PASS");
}
private static void check(boolean ok, String msg) {
if (!ok) {
throw new AssertionError(msg);
}
System.out.println("PASS " + msg);
}
}
复杂度分析
时间复杂度 O(num * digits) ≈ O(num log num),空间复杂度 O(1)。
时间复杂度 O(10) ≈ O(1),空间复杂度 O(1)。