LeetCode 2461-2480. 题解合集
文章目录
- [LeetCode 2461-2480. 题解合集](#LeetCode 2461-2480. 题解合集)
-
- [2461. Maximum Sum of Distinct Subarrays With Length K(长度为 K 子数组中的最大和)](#2461. Maximum Sum of Distinct Subarrays With Length K(长度为 K 子数组中的最大和))
- [2462. Total Cost to Hire K Workers(雇佣 K 位工人的总代价)](#2462. Total Cost to Hire K Workers(雇佣 K 位工人的总代价))
- [2463. Minimum Total Distance Traveled(最小移动总距离)](#2463. Minimum Total Distance Traveled(最小移动总距离))
- [2464. Minimum Subarrays in a Valid Split(有效分割中的最少子数组数目)](#2464. Minimum Subarrays in a Valid Split(有效分割中的最少子数组数目))
- [2465. Number of Distinct Averages(不同的平均值数目)](#2465. Number of Distinct Averages(不同的平均值数目))
- [2466. Count Ways To Build Good Strings(统计构造好字符串的方案数)](#2466. Count Ways To Build Good Strings(统计构造好字符串的方案数))
- [2467. Most Profitable Path in a Tree(树上最大得分和路径)](#2467. Most Profitable Path in a Tree(树上最大得分和路径))
- [2468. Split Message Based on Limit(根据限制分割消息)](#2468. Split Message Based on Limit(根据限制分割消息))
- [2469. Convert the Temperature(温度转换)](#2469. Convert the Temperature(温度转换))
- [2470. Number of Subarrays With LCM Equal to K(最小公倍数等于 K 的子数组数目)](#2470. Number of Subarrays With LCM Equal to K(最小公倍数等于 K 的子数组数目))
- [2471. Minimum Number of Operations to Sort a Binary Tree by Level(逐层排序二叉树所需的最少操作数目)](#2471. Minimum Number of Operations to Sort a Binary Tree by Level(逐层排序二叉树所需的最少操作数目))
- [2472. Maximum Number of Non-overlapping Palindrome Substrings(不重叠回文子字符串的最大数目)](#2472. Maximum Number of Non-overlapping Palindrome Substrings(不重叠回文子字符串的最大数目))
- [2473. Minimum Cost to Buy Apples(购买苹果的最低成本)](#2473. Minimum Cost to Buy Apples(购买苹果的最低成本))
- [2474. Customers With Strictly Increasing Purchases(购买量严格增加的客户)](#2474. Customers With Strictly Increasing Purchases(购买量严格增加的客户))
- [2475. Number of Unequal Triplets in Array(数组中不等三元组的数目)](#2475. Number of Unequal Triplets in Array(数组中不等三元组的数目))
- [2476. Closest Nodes Queries in a Binary Search Tree(二叉搜索树最近节点查询)](#2476. Closest Nodes Queries in a Binary Search Tree(二叉搜索树最近节点查询))
- [2477. Minimum Fuel Cost to Report to the Capital(到达首都的最少油耗)](#2477. Minimum Fuel Cost to Report to the Capital(到达首都的最少油耗))
- [2478. Number of Beautiful Partitions(完美分割的方案数)](#2478. Number of Beautiful Partitions(完美分割的方案数))
- [2479. Maximum XOR of Two Non-Overlapping Subtrees(两个不重叠子树的最大异或值)](#2479. Maximum XOR of Two Non-Overlapping Subtrees(两个不重叠子树的最大异或值))
- [2480. Form a Chemical Bond(形成化学键)](#2480. Form a Chemical Bond(形成化学键))
2461. Maximum Sum of Distinct Subarrays With Length K(长度为 K 子数组中的最大和)
题目描述
给你一个整数数组 nums 和一个整数 k。从 nums 中找出满足:长度 k 且所有元素各不相同 的
子数组,返回其最大和;若不存在返回 0。
示例 1
`nums = [1,5,4,2,9,9,9], k = 3` → `15`。
示例 2
`nums = [4,4,4], k = 3` → `0`。
提示:
1 <= k <= nums.length <= 10^5,1 <= nums[i] <= 10^5
解题思路
解法一:滑动窗口 + 计数(主提交版本)
- 维护窗口 l, r 长为当前扩展、计数表记录窗口内元素出现次数与重复数 dup。
- 右指针 r 扩展 numsr,若窗口内元素互异(dup==0)且窗口大小 == k,则更新答案并删除窗口左端。
- 标准写法:r 从 0 到 n-1,加入 numsr,若计数>1 dup++;若 r-l+1 > k 则收缩 l;
若 dup0 且长度k 更新答案。
解法二:双重循环暴力(对照) - 枚举窗口起点,逐个检查含重复则停;O(n*k) 小规模对拍。
复杂度: - 时间复杂度:O(n)。
- 空间复杂度:O(n)。
代码实现
java
import java.util.HashMap;
import java.util.Map;
public class Solution {
// 解法一:滑动窗口(主提交版本)
public long maximumSubarraySum(int[] nums, int k) {
Map<Integer, Integer> cnt = new HashMap<>();
long best = 0, sum = 0;
int l = 0, dup = 0;
for (int r = 0; r < nums.length; r++) {
int v = nums[r];
int c = cnt.getOrDefault(v, 0);
if (c == 1) {
dup++;
}
cnt.put(v, c + 1);
sum += v;
while (r - l + 1 > k) {
int out = nums[l++];
int oc = cnt.get(out);
if (oc == 2) {
dup--;
}
cnt.put(out, oc - 1);
sum -= out;
}
if (dup == 0 && r - l + 1 == k) {
best = Math.max(best, sum);
}
}
return best;
}
// 解法二:暴力(对照)
public long maximumSubarraySumBrute(int[] nums, int k) {
long best = 0;
int n = nums.length;
for (int i = 0; i + k <= n; i++) {
Map<Integer, Integer> seen = new HashMap<>();
long sum = 0;
boolean ok = true;
for (int j = i; j < i + k; j++) {
int v = nums[j];
if (seen.containsKey(v)) {
ok = false;
break;
}
seen.put(v, 1);
sum += v;
}
if (ok) {
best = Math.max(best, sum);
}
}
return best;
}
public static void main(String[] args) {
Solution sol = new Solution();
check(sol.maximumSubarraySum(new int[] {1, 5, 4, 2, 9, 9, 9}, 3) == 15, "2461 ex1");
check(sol.maximumSubarraySumBrute(new int[] {1, 5, 4, 2, 9, 9, 9}, 3) == 15, "2461 ex1b");
check(sol.maximumSubarraySum(new int[] {4, 4, 4}, 3) == 0, "2461 ex2");
check(sol.maximumSubarraySumBrute(new int[] {4, 4, 4}, 3) == 0, "2461 ex2b");
// 随机对拍
java.util.Random rnd = new java.util.Random(223);
for (int t = 0; t < 500; t++) {
int n = 1 + rnd.nextInt(30);
int[] a = new int[n];
for (int i = 0; i < n; i++) {
a[i] = 1 + rnd.nextInt(10);
}
int k = 1 + rnd.nextInt(n);
long x = sol.maximumSubarraySum(a, k), y = sol.maximumSubarraySumBrute(a, k);
if (x != y) {
throw new AssertionError("2461 mismatch t=" + t + " x=" + x + " y=" + y);
}
}
System.out.println("2461 全部 PASS");
}
private static void check(boolean ok, String msg) {
if (!ok) {
throw new AssertionError(msg);
}
System.out.println("PASS " + msg);
}
}
复杂度分析
- 时间复杂度:O(n)。
- 空间复杂度:O(n)。
2462. Total Cost to Hire K Workers(雇佣 K 位工人的总代价)
题目描述
给你一个下标从 0 开始的整数数组 costs(costs[i] 是雇佣第 i 位工人的代价)以及两个整数 k 和
candidates。共进行 k 轮,每轮恰好雇佣一位工人:从最前面 candidates 人和最后面 candidates 人中
选代价最小者(同价取下标更小);若剩余人数不足 candidates,则从剩余所有人中选。返回雇佣恰好 k 位
工人的总代价。
示例 1
`costs=[17,12,10,2,7,2,11,20,8], k=3, candidates=4` → `11`。
示例 2
`costs=[1,2,4,1], k=3, candidates=3` → `4`。
提示:
1 <= costs.length <= 10^5,1 <= costs[i] <= 10^5,1 <= k, candidates <= costs.length
解题思路
解法一:双堆模拟(主提交版本)
- 维护两个最小堆:左堆(前 l 个待选)与右堆(后 r 个待选),每轮比较堆顶(值、下标)取较小者,
从对应端补充新候选人。当左右区间相遇时合并处理,避免重复。 - 为标准做法:用两个堆分别装「前端 candidates」与「后端 candidates」,双指针维护边界,
若 l > r 说明两堆已重叠,直接把两边都放进一个堆即可(防止重复计)。
解法二:朴素模拟(对照,小规模) - 每轮扫描一次剩余数组选最小,O(k*n)。
复杂度: - 时间复杂度:O((n+k) log n)。
- 空间复杂度:O(n)。
代码实现
java
import java.util.Arrays;
import java.util.PriorityQueue;
public class Solution {
// 解法一:双堆 + 双指针(主提交版本)
public long totalCost(int[] costs, int k, int candidates) {
int n = costs.length;
// 堆元素:代价;其次按下标(编码:代价 * 1e6 + idx)
PriorityQueue<long[]> left = new PriorityQueue<>((a, b) ->
a[0] != b[0] ? Long.compare(a[0], b[0]) : Long.compare(a[1], b[1]));
PriorityQueue<long[]> right = new PriorityQueue<>((a, b) ->
a[0] != b[0] ? Long.compare(a[0], b[0]) : Long.compare(a[1], b[1]));
int l = 0, r = n - 1;
// 初始化:左端 candidates 人、右端 candidates 人
for (int i = 0; i < candidates && l <= r; i++) {
left.offer(new long[] {costs[l], l});
l++;
}
for (int i = 0; i < candidates && l <= r; i++) {
right.offer(new long[] {costs[r], r});
r--;
}
long ans = 0;
for (int round = 0; round < k; round++) {
long[] p;
if (left.isEmpty()) {
p = right.poll();
} else if (right.isEmpty()) {
p = left.poll();
} else {
long[] a = left.peek(), b = right.peek();
if (a[0] < b[0] || (a[0] == b[0] && a[1] < b[1])) {
p = left.poll();
} else {
p = right.poll();
}
}
ans += p[0];
// 从弹出端补充新候选人
if (p[1] < l) {
if (l <= r) {
left.offer(new long[] {costs[l], l});
l++;
}
} else {
if (l <= r) {
right.offer(new long[] {costs[r], r});
r--;
}
}
}
return ans;
}
// 解法二:朴素模拟(对照,小规模)
public long totalCostBrute(int[] costs, int k, int candidates) {
int n = costs.length;
boolean[] hired = new boolean[n];
int head = 0, tail = n - 1;
long ans = 0;
for (int round = 0; round < k; round++) {
int bestIdx = -1;
// 收集候选范围
int l = head, r = tail;
// 扫描前面 candidates 个未雇佣
int cnt = 0;
for (int i = head; i <= tail && cnt < candidates; i++) {
if (!hired[i]) {
cnt++;
if (bestIdx == -1 || costs[i] < costs[bestIdx]
|| (costs[i] == costs[bestIdx] && i < bestIdx)) {
bestIdx = i;
}
}
}
// 扫描最后 candidates 个未雇佣
cnt = 0;
for (int i = tail; i >= head && cnt < candidates; i--) {
if (!hired[i]) {
cnt++;
if (bestIdx == -1 || costs[i] < costs[bestIdx]
|| (costs[i] == costs[bestIdx] && i < bestIdx)) {
bestIdx = i;
}
}
}
hired[bestIdx] = true;
ans += costs[bestIdx];
while (head <= tail && hired[head]) {
head++;
}
while (tail >= head && hired[tail]) {
tail--;
}
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
check(sol.totalCost(new int[] {17, 12, 10, 2, 7, 2, 11, 20, 8}, 3, 4) == 11, "2462 ex1");
check(sol.totalCostBrute(new int[] {17, 12, 10, 2, 7, 2, 11, 20, 8}, 3, 4) == 11, "2462 ex1b");
check(sol.totalCost(new int[] {1, 2, 4, 1}, 3, 3) == 4, "2462 ex2");
check(sol.totalCostBrute(new int[] {1, 2, 4, 1}, 3, 3) == 4, "2462 ex2b");
// 随机对拍
java.util.Random rnd = new java.util.Random(227);
for (int t = 0; t < 300; t++) {
int n = 1 + rnd.nextInt(30);
int[] a = new int[n];
for (int i = 0; i < n; i++) {
a[i] = 1 + rnd.nextInt(100);
}
int k = 1 + rnd.nextInt(n);
int c = 1 + rnd.nextInt(n);
long x = sol.totalCost(a.clone(), k, c), y = sol.totalCostBrute(a.clone(), k, c);
if (x != y) {
throw new AssertionError("2462 mismatch t=" + t + " x=" + x + " y=" + y
+ " a=" + Arrays.toString(a) + " k=" + k + " c=" + c);
}
}
System.out.println("2462 全部 PASS");
}
private static void check(boolean ok, String msg) {
if (!ok) {
throw new AssertionError(msg);
}
System.out.println("PASS " + msg);
}
}
复杂度分析
- 时间复杂度:O((n+k) log n)。
- 空间复杂度:O(n)。
2463. Minimum Total Distance Traveled(最小移动总距离)
题目描述
X 轴上有一些机器人和工厂。robot 是机器人位置数组,factory = [position, limit] 表示工厂位置与
最多可修理的机器人数量。机器人沿设定方向移动,经过未满工厂则被修理停止。任何时刻可设置部分机器人
方向。目标是最小化所有机器人总移动距离(所有机器人都可被维修)。
示例 1
`robot=[0,4,6], factory=[[2,2],[6,2]]` → `4`。
示例 2
`robot=[1,-1], factory=[[-2,1],[2,1]]` → `2`。
提示:
1 <= robot.length, factory.length <= 100,位置可负可正,0 <= limit <= robot.length
解题思路
解法一:排序 + 三维 DP(主提交版本)
- 最优解中机器人按位置顺序被工厂按顺序修理(排序后不相交),因此把 robot 与 factory 都按位置排序。
dp[i][j] = 前 i 个工厂修理前 j 个机器人、且第 j 个机器人恰由第 i 个工厂修理(或更前)的最小距离。
简化经典写法:dp[i][j]前 i 工厂、前 j 机器人的最小总距离。
转移:第 i 工厂(position p, limit L)修的最后一批:修 0...min(L, j) 个,
dp[i][j] = min(dp[i-1][j - t] + cost(i, j-t+1..j)),cost = t 个机器人到 p 的距离和。
O(F * R^2 * L) 可优化:枚举 j 与 t(t ≤ L)。总 O(FR L) 可过(100100100)。- 实现:对 t 从 1...min(L,j) 枚举:
dp[i][j] = min(dp[i-1][j], min_t(dp[i-1][j-t] + sumDist))。
解法二:记忆化 DFS(对照) dfs(i, j):处理前 i 个工厂、前 j 个机器人的最小费用,同上转移。
复杂度:- 时间复杂度:O(F * R * L) ≈ 1e6。
- 空间复杂度:O(F * R)。
代码实现
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class Solution {
// 解法一:排序 + DP(主提交版本)
public long minimumTotalDistance(List<Integer> robot, int[][] factory) {
List<Integer> rb = new ArrayList<>(robot);
Collections.sort(rb);
// 工厂按位置排序
int[][] fac = factory.clone();
Arrays.sort(fac, (a, b) -> Integer.compare(a[0], b[0]));
int R = rb.size(), F = fac.length;
long INF = Long.MAX_VALUE / 4;
long[][] dp = new long[F + 1][R + 1];
for (int j = 1; j <= R; j++) {
dp[0][j] = INF;
}
for (int i = 1; i <= F; i++) {
int pos = fac[i - 1][0], lim = fac[i - 1][1];
for (int j = 0; j <= R; j++) {
dp[i][j] = dp[i - 1][j]; // 第 i 工厂一个不修
// 第 i 工厂修最后 t 个机器人(t from 1..min(lim, j))
long acc = 0;
for (int t = 1; t <= lim && t <= j; t++) {
acc += Math.abs(rb.get(j - t) - (long) pos);
if (dp[i - 1][j - t] != INF) {
dp[i][j] = Math.min(dp[i][j], dp[i - 1][j - t] + acc);
}
}
}
}
return dp[F][R];
}
// 解法二:记忆化 DFS(对照)
public long minimumTotalDistanceDfs(List<Integer> robot, int[][] factory) {
List<Integer> rb = new ArrayList<>(robot);
Collections.sort(rb);
int[][] fac = factory.clone();
Arrays.sort(fac, (a, b) -> Integer.compare(a[0], b[0]));
int R = rb.size(), F = fac.length;
memo = new Long[F + 1][R + 1];
this.rb = rb;
this.fac = fac;
return dfs(0, 0);
}
private Long[][] memo;
private List<Integer> rb;
private int[][] fac;
private long dfs(int i, int j) {
if (j == rb.size()) {
return 0;
}
if (i == fac.length) {
return Long.MAX_VALUE / 4;
}
if (memo[i][j] != null) {
return memo[i][j];
}
// 不修任何机器人
long best = dfs(i + 1, j);
// 修 t 个
int pos = fac[i][0];
long acc = 0;
for (int t = 1; t <= fac[i][1] && j + t <= rb.size(); t++) {
acc += Math.abs(rb.get(j + t - 1) - (long) pos);
best = Math.min(best, acc + dfs(i + 1, j + t));
}
memo[i][j] = best;
return best;
}
public static void main(String[] args) {
Solution sol = new Solution();
check(sol.minimumTotalDistance(Arrays.asList(0, 4, 6), new int[][] {{2, 2}, {6, 2}}) == 4, "2463 ex1");
check(sol.minimumTotalDistanceDfs(Arrays.asList(0, 4, 6), new int[][] {{2, 2}, {6, 2}}) == 4, "2463 ex1d");
check(sol.minimumTotalDistance(Arrays.asList(1, -1), new int[][] {{-2, 1}, {2, 1}}) == 2, "2463 ex2");
check(sol.minimumTotalDistanceDfs(Arrays.asList(1, -1), new int[][] {{-2, 1}, {2, 1}}) == 2, "2463 ex2d");
// 随机对拍
java.util.Random rnd = new java.util.Random(229);
for (int t = 0; t < 80; t++) {
int rn = 1 + rnd.nextInt(7);
List<Integer> rbt = new ArrayList<>();
for (int i = 0; i < rn; i++) {
rbt.add(-5 + rnd.nextInt(11));
}
int fn = 1 + rnd.nextInt(4);
int[][] f = new int[fn][2];
int remain = rn;
for (int i = 0; i < fn; i++) {
f[i][0] = -5 + rnd.nextInt(11);
int mx = remain;
f[i][1] = (i == fn - 1) ? remain : rnd.nextInt(mx + 1);
remain -= f[i][1];
if (remain < 0) {
f[i][1] += remain;
remain = 0;
}
}
// 确保总容量足够
if (remain > 0) {
f[fn - 1][1] += remain;
}
long x = sol.minimumTotalDistance(rbt, f), y = sol.minimumTotalDistanceDfs(rbt, f);
if (x != y) {
throw new AssertionError("2463 mismatch t=" + t + " x=" + x + " y=" + y);
}
}
System.out.println("2463 全部 PASS");
}
private static void check(boolean ok, String msg) {
if (!ok) {
throw new AssertionError(msg);
}
System.out.println("PASS " + msg);
}
}
复杂度分析
- 时间复杂度:O(F * R * L) ≈ 1e6。
- 空间复杂度:O(F * R)。
2464. Minimum Subarrays in a Valid Split(有效分割中的最少子数组数目)
题目描述
给定整数数组 nums。将 nums 拆分为一个或多个互不覆盖的子数组,若每个子数组第一个和最后一个元素
的最大公约数大于 1,则分割有效(每个元素只属于一个子数组)。返回有效分割中最少子数组数目;
若无法有效分割返回 -1。
示例 1
`nums = [2,6,3,4,3]` → `2`([2,6]|[3,4,3])。
示例 2
`nums = [3,5]` → `2`([3]|[5])。
示例 3
`nums = [1,2,1]` → `-1`(元素 1 无法单独成段------但注意示例 2 中 [3] 与 [5] 的长度 1
子数组:首尾同一元素 gcd=自身>1 即可。而 1 的 gcd=1 不合法,且 1 与任何数 gcd 都是 1,无解)。
提示:
1 <= nums.length <= 1000,1 <= nums[i] <= 10^5
解题思路
解法一:区间 DP(主提交版本)
dp[i]前 i 个元素的最少子数组数;dp[i] = min(dp[j] + 1),其中gcd(nums[j-1], nums[i-1]) > 1
且 j...i-1 这段可以是一个子数组?------ 不,这里子数组只需首尾 gcd>1,中间无约束。
所以转移:for j in 1..i: if gcd(nums[j-1], nums[i-1]) > 1: dp[i] = min(dp[i], dp[j-1]+1)。
预处理所有对 gcd 加速到 O(1)。
解法二:DFS 记忆化(对照)f(i)从 i 开始的最少段数,递归枚举下一段的结束点。
复杂度:- 时间复杂度:O(n^2 log A)。
- 空间复杂度:O(n)。
代码实现
java
import java.util.Arrays;
public class Solution {
// 解法一:区间 DP(主提交版本)
public int validSubarraySplit(int[] nums) {
int n = nums.length;
int[] dp = new int[n + 1];
Arrays.fill(dp, Integer.MAX_VALUE);
dp[0] = 0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= i; j++) {
if (gcd(nums[j - 1], nums[i - 1]) > 1 && dp[j - 1] != Integer.MAX_VALUE) {
dp[i] = Math.min(dp[i], dp[j - 1] + 1);
}
}
}
return dp[n] == Integer.MAX_VALUE ? -1 : dp[n];
}
// 解法二:记忆化 DFS(对照)
public int validSubarraySplitDfs(int[] nums) {
this.nums = nums;
memo = new int[nums.length];
Arrays.fill(memo, -1);
int r = f(0);
return r >= Integer.MAX_VALUE / 2 ? -1 : r;
}
private int[] nums;
private int[] memo;
private int f(int i) {
if (i == nums.length) {
return 0;
}
if (memo[i] != -1) {
return memo[i];
}
int best = Integer.MAX_VALUE / 2;
for (int j = i; j < nums.length; j++) {
if (gcd(nums[i], nums[j]) > 1) {
best = Math.min(best, 1 + f(j + 1));
}
}
memo[i] = best;
return best;
}
private static int gcd(int a, int b) {
while (b != 0) {
int t = a % b;
a = b;
b = t;
}
return a;
}
public static void main(String[] args) {
Solution sol = new Solution();
check(sol.validSubarraySplit(new int[] {2, 6, 3, 4, 3}) == 2, "2464 ex1");
check(sol.validSubarraySplitDfs(new int[] {2, 6, 3, 4, 3}) == 2, "2464 ex1d");
check(sol.validSubarraySplit(new int[] {3, 5}) == 2, "2464 ex2");
check(sol.validSubarraySplitDfs(new int[] {3, 5}) == 2, "2464 ex2d");
check(sol.validSubarraySplit(new int[] {1, 2, 1}) == -1, "2464 ex3");
check(sol.validSubarraySplitDfs(new int[] {1, 2, 1}) == -1, "2464 ex3d");
// 随机对拍
java.util.Random rnd = new java.util.Random(233);
for (int t = 0; t < 300; t++) {
int n = 1 + rnd.nextInt(20);
int[] a = new int[n];
for (int i = 0; i < n; i++) {
a[i] = 1 + rnd.nextInt(40);
}
int x = sol.validSubarraySplit(a), y = sol.validSubarraySplitDfs(a);
if (x != y) {
throw new AssertionError("2464 mismatch t=" + t + " x=" + x + " y=" + y);
}
}
System.out.println("2464 全部 PASS");
}
private static void check(boolean ok, String msg) {
if (!ok) {
throw new AssertionError(msg);
}
System.out.println("PASS " + msg);
}
}
复杂度分析
- 时间复杂度:O(n^2 log A)。
- 空间复杂度:O(n)。
2465. Number of Distinct Averages(不同的平均值数目)
题目描述
给你一个下标从 0 开始、长度为偶数 的整数数组 nums。重复以下步骤直到 nums 为空:
找到最小值并删除;找到最大值并删除;计算被删两数的平均值 (a + b) / 2。
返回过程中能得到的不同平均值的数目。
示例 1
`nums = [4,1,4,0,3,5]` → `2`(2.5、2.5、3.5)。
示例 2
`nums = [1,100]` → `1`。
提示:
2 <= nums.length <= 100,长度为偶数,0 <= nums[i] <= 100
解题思路
解法一:排序 + 双指针 + Set(主提交版本)
- 排序后最小在左、最大在右,两两配对求和存入 Set(用 2*sum 或和值去重,避免浮点),
返回 Set 大小。平均值去重等价于「和」去重(除以 2 单调)。
解法二:TreeMap 删除(对照) - 用有序 Map 维护 min/max 逐个删除,思路不同。
复杂度: - 时间复杂度:O(n log n)。
- 空间复杂度:O(n)。
代码实现
java
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
public class Solution {
// 解法一:排序双指针(主提交版本)
public int distinctAverages(int[] nums) {
Arrays.sort(nums);
Set<Integer> sums = new HashSet<>();
int n = nums.length;
for (int i = 0; i < n / 2; i++) {
sums.add(nums[i] + nums[n - 1 - i]);
}
return sums.size();
}
// 解法二:TreeMap(对照)
public int distinctAverages2(int[] nums) {
java.util.TreeMap<Integer, Integer> map = new java.util.TreeMap<>();
for (int x : nums) {
map.put(x, map.getOrDefault(x, 0) + 1);
}
Set<Integer> sums = new HashSet<>();
while (!map.isEmpty()) {
int mn = map.firstKey(), mx = map.lastKey();
sums.add(mn + mx);
dec(map, mn);
dec(map, mx);
}
return sums.size();
}
private void dec(java.util.TreeMap<Integer, Integer> map, int k) {
int c = map.get(k) - 1;
if (c == 0) {
map.remove(k);
} else {
map.put(k, c);
}
}
public static void main(String[] args) {
Solution sol = new Solution();
check(sol.distinctAverages(new int[] {4, 1, 4, 0, 3, 5}) == 2, "2465 ex1");
check(sol.distinctAverages2(new int[] {4, 1, 4, 0, 3, 5}) == 2, "2465 ex12");
check(sol.distinctAverages(new int[] {1, 100}) == 1, "2465 ex2");
check(sol.distinctAverages2(new int[] {1, 100}) == 1, "2465 ex22");
// 随机对拍
java.util.Random rnd = new java.util.Random(239);
for (int t = 0; t < 500; t++) {
int n = 2 + 2 * rnd.nextInt(15);
int[] a = new int[n];
for (int i = 0; i < n; i++) {
a[i] = rnd.nextInt(100);
}
int x = sol.distinctAverages(a), y = sol.distinctAverages2(a);
if (x != y) {
throw new AssertionError("2465 mismatch t=" + t + " x=" + x + " y=" + y);
}
}
System.out.println("2465 全部 PASS");
}
private static void check(boolean ok, String msg) {
if (!ok) {
throw new AssertionError(msg);
}
System.out.println("PASS " + msg);
}
}
复杂度分析
- 时间复杂度:O(n log n)。
- 空间复杂度:O(n)。
2466. Count Ways To Build Good Strings(统计构造好字符串的方案数)
题目描述
给你整数 zero、one、low、high。从空字符串开始,每步执行:在末尾添加 zero 个 '0',
或在末尾添加 one 个 '1'。若得到长度在 `low`, `high` 之间(含边界)的字符串,称为好 字符串。
返回不同好字符串数目,对 10^9+7 取余。
示例 1
`low=3, high=3, zero=1, one=1` → `8`。
示例 2
`low=2, high=3, zero=1, one=2` → `5`。
提示:
1 <= low <= high <= 10^5,1 <= zero, one <= low
解题思路
解法一:线性 DP(主提交版本)
dp[len]= 构造出长度为 len 的字符串的方案数。dp[len] += dp[len-zero](加 0 串)、 dp[len-one](加 1 串),dp[0]=1。- 答案 = sum(dplow...high) % MOD。
解法二:记忆化 DFS(对照) f(len)表示从长度 len 继续构造的方案数,递归 + memo。
复杂度:- 时间复杂度:O(high)。
- 空间复杂度:O(high)。
代码实现
java
public class Solution {
private static final int MOD = 1000000007;
// 解法一:线性 DP(主提交版本)
public int countGoodStrings(int low, int high, int zero, int one) {
long[] dp = new long[high + 1];
dp[0] = 1;
long ans = 0;
for (int len = 1; len <= high; len++) {
if (len >= zero) {
dp[len] = (dp[len] + dp[len - zero]) % MOD;
}
if (len >= one) {
dp[len] = (dp[len] + dp[len - one]) % MOD;
}
if (len >= low) {
ans = (ans + dp[len]) % MOD;
}
}
return (int) ans;
}
// 解法二:记忆化 DFS(对照)
public int countGoodStringsDfs(int low, int high, int zero, int one) {
memo = new long[high + 1];
java.util.Arrays.fill(memo, -1);
this.low = low;
return (int) f(0, high, zero, one);
}
private int low;
private long[] memo;
private long f(int len, int high, int zero, int one) {
if (len > high) {
return 0;
}
if (memo[len] != -1) {
return memo[len];
}
long res = (len >= low) ? 1 : 0;
res = (res + f(len + zero, high, zero, one)) % MOD;
res = (res + f(len + one, high, zero, one)) % MOD;
memo[len] = res;
return res;
}
public static void main(String[] args) {
Solution sol = new Solution();
check(sol.countGoodStrings(3, 3, 1, 1) == 8, "2466 ex1");
check(sol.countGoodStringsDfs(3, 3, 1, 1) == 8, "2466 ex1d");
check(sol.countGoodStrings(2, 3, 1, 2) == 5, "2466 ex2");
check(sol.countGoodStringsDfs(2, 3, 1, 2) == 5, "2466 ex2d");
// 随机对拍
java.util.Random rnd = new java.util.Random(241);
for (int t = 0; t < 300; t++) {
int h = 1 + rnd.nextInt(60);
int l = 1 + rnd.nextInt(h);
int z = 1 + rnd.nextInt(l);
int o = 1 + rnd.nextInt(l);
int x = sol.countGoodStrings(l, h, z, o), y = sol.countGoodStringsDfs(l, h, z, o);
if (x != y) {
throw new AssertionError("2466 mismatch t=" + t + " x=" + x + " y=" + y
+ " l=" + l + " h=" + h);
}
}
System.out.println("2466 全部 PASS");
}
private static void check(boolean ok, String msg) {
if (!ok) {
throw new AssertionError(msg);
}
System.out.println("PASS " + msg);
}
}
复杂度分析
- 时间复杂度:O(high)。
- 空间复杂度:O(high)。
2467. Most Profitable Path in a Tree(树上最大得分和路径)
题目描述
一个 n 个节点的无向树(节点 0...n-1),根为 0。每条边在 edges 中给出。每个节点处有一扇门,
amount[i](偶数)表示开门获得的分数(负数为扣分)。游戏规则:
Alice一开始在节点 0,Bob在节点bob。- 每一秒
Alice和Bob分别移动到相邻节点。Alice朝某个叶子移动,Bob朝节点 0 移动。 - 经过的节点都要开门计分;若门已被别人打开则不再计分;若两人同时到达同一节点则评分(各得一半)。
Alice到达叶子后停止;Bob到达 0 后停止;两者互不影响。
返回Alice朝最优叶子移动的最大净得分。
示例 1
`edges=[[0,1],[1,2],[1,3],[3,4]], bob=3, amount=[-2,4,2,-4,6]` → `6`。
示例 2
`edges=[[0,1]], bob=1, amount=[-7280,2350]` → `-7280`。
提示:
2 <= n <= 10^5,amount[i]是范围 -10\^4, 10\^4 的偶数,1 <= bob < n
解题思路
解法一:Bob 到达时间 + Alice DFS(主提交版本)
- 第一步:从 Bob 出发 DFS/BFS,记录 Bob 到达每个节点的时刻
bobTime[v](根 0 也记录)。 - 第二步:Alice 从根 DFS 到叶子,维护已花费时间
t与累计得分: - 若
t < bobTime[v]:Alice 先到,全额得分 amountv; - 若
t == bobTime[v]:同时到达,各得 amountv/2; - 若
t > bobTime[v]:Bob 已开过门,得 0。 - 叶子(除根外度数为 1 的节点)处更新答案最大值。
解法二:另起一遍 BFS 暴力模拟 Alice 每条路径(对照,小规模) - 枚举 Alice 到每个叶子的路径逐步模拟计分,与解法一对拍。
复杂度: - 时间复杂度:O(n)。
- 空间复杂度:O(n)。
代码实现
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class Solution {
private List<Integer>[] g;
private int[] bobTime;
private int[] amount;
private int best = Integer.MIN_VALUE;
// 解法一:Bob 路径时间 + Alice DFS(主提交版本)
public int mostProfitablePath(int[][] edges, int bob, int[] amount) {
int n = amount.length;
g = new List[n];
for (int i = 0; i < n; i++) {
g[i] = new ArrayList<>();
}
for (int[] e : edges) {
g[e[0]].add(e[1]);
g[e[1]].add(e[0]);
}
// 只给 Bob 到根 0 的路径上的节点时间;其他节点 Bob 不会到达(视为无穷)
bobTime = new int[n];
Arrays.fill(bobTime, Integer.MAX_VALUE);
List<Integer> path = new ArrayList<>();
findPath(bob, -1, path); // 路径从 bob 到 0
for (int i = 0; i < path.size(); i++) {
bobTime[path.get(i)] = i;
}
this.amount = amount;
best = Integer.MIN_VALUE;
dfsAlice(0, -1, 0, 0);
return best;
}
private boolean findPath(int u, int fa, List<Integer> path) {
path.add(u);
if (u == 0) {
return true;
}
for (int v : g[u]) {
if (v != fa) {
if (findPath(v, u, path)) {
return true;
}
}
}
path.remove(path.size() - 1);
return false;
}
private void dfsAlice(int u, int fa, int t, int score) {
int add;
if (t < bobTime[u]) {
add = amount[u];
} else if (t == bobTime[u]) {
add = amount[u] / 2;
} else {
add = 0;
}
score += add;
boolean leaf = true;
for (int v : g[u]) {
if (v != fa) {
leaf = false;
dfsAlice(v, u, t + 1, score);
}
}
if (leaf) {
best = Math.max(best, score);
}
}
// 解法二:枚举叶子路径模拟(对照,小规模)
public int mostProfitablePathBrute(int[][] edges, int bob, int[] amount) {
int n = amount.length;
List<Integer>[] gg = new List[n];
for (int i = 0; i < n; i++) {
gg[i] = new ArrayList<>();
}
for (int[] e : edges) {
gg[e[0]].add(e[1]);
gg[e[1]].add(e[0]);
}
// Bob 到尾部的根路径时间
int[] bt = new int[n];
Arrays.fill(bt, Integer.MAX_VALUE);
List<Integer> bpath = new ArrayList<>();
collectBobPath(gg, bob, -1, bpath);
for (int i = 0; i < bpath.size(); i++) {
bt[bpath.get(i)] = i;
}
int res = Integer.MIN_VALUE;
// 找所有叶子路径
List<List<Integer>> paths = new ArrayList<>();
collectPath(gg, 0, -1, new ArrayList<>(), paths);
for (List<Integer> path : paths) {
int score = 0;
for (int i = 0; i < path.size(); i++) {
int v = path.get(i);
if (i < bt[v]) {
score += amount[v];
} else if (i == bt[v]) {
score += amount[v] / 2;
}
}
res = Math.max(res, score);
}
return res;
}
// 收集 bob -> 0 的路径(返回是否找到)
private boolean collectBobPath(List<Integer>[] gg, int u, int fa, List<Integer> path) {
path.add(u);
if (u == 0) {
return true;
}
for (int v : gg[u]) {
if (v != fa) {
if (collectBobPath(gg, v, u, path)) {
return true;
}
}
}
path.remove(path.size() - 1);
return false;
}
private void collectPath(List<Integer>[] gg, int u, int fa, List<Integer> cur, List<List<Integer>> paths) {
cur.add(u);
boolean leaf = true;
for (int v : gg[u]) {
if (v != fa) {
leaf = false;
List<Integer> copy = new ArrayList<>(cur);
collectPath(gg, v, u, copy, paths);
}
}
if (leaf) {
paths.add(new ArrayList<>(cur));
}
}
public static void main(String[] args) {
Solution sol = new Solution();
check(sol.mostProfitablePath(new int[][] {{0, 1}, {1, 2}, {1, 3}, {3, 4}}, 3,
new int[] {-2, 4, 2, -4, 6}) == 6, "2467 ex1");
check(sol.mostProfitablePathBrute(new int[][] {{0, 1}, {1, 2}, {1, 3}, {3, 4}}, 3,
new int[] {-2, 4, 2, -4, 6}) == 6, "2467 ex1b");
check(sol.mostProfitablePath(new int[][] {{0, 1}}, 1, new int[] {-7280, 2350}) == -7280, "2467 ex2");
check(sol.mostProfitablePathBrute(new int[][] {{0, 1}}, 1, new int[] {-7280, 2350}) == -7280, "2467 ex2b");
// 随机小规模对拍
java.util.Random rnd = new java.util.Random(251);
for (int t = 0; t < 80; t++) {
int n = 2 + rnd.nextInt(8);
List<int[]> es = new ArrayList<>();
for (int u = 1; u < n; u++) {
es.add(new int[] {u, rnd.nextInt(u)});
}
int[][] edgeArr = es.toArray(new int[0][]);
int bob = 1 + rnd.nextInt(n - 1);
int[] amt = new int[n];
for (int i = 0; i < n; i++) {
amt[i] = (rnd.nextInt(21) - 10) * 2;
}
int x = sol.mostProfitablePath(edgeArr, bob, amt);
int y = sol.mostProfitablePathBrute(edgeArr, bob, amt);
if (x != y) {
throw new AssertionError("2467 mismatch t=" + t + " x=" + x + " y=" + y);
}
}
System.out.println("2467 全部 PASS");
}
private static void check(boolean ok, String msg) {
if (!ok) {
throw new AssertionError(msg);
}
System.out.println("PASS " + msg);
}
}
复杂度分析
- 时间复杂度:O(n)。
- 空间复杂度:O(n)。
2468. Split Message Based on Limit(根据限制分割消息)
题目描述
给你一个字符串 message 和一个正整数 limit。将 message 分割为一个或多个部分,每个部分结尾是
"<a/b>"(b 是总段数,a 是当前段编号,从 1 到 b)。除最后一部分长度 ≤ limit 外,其他每部分
(含结尾)长度都应等于 limit。结果数组越短越好。按顺序拼接各部分去掉结尾后应还原 message。
返回分割数组;若无法分割返回空数组。
示例 1
`message="this is really a very awesome message", limit=9` → 14 段。
示例 2
`message="short message", limit=15` → `["short mess<1/2>","age<2/2>"]`。
提示:
1 <= message.length <= 10^4,message只含小写字母与空格,1 <= limit <= 10^4
解题思路
解法一:枚举总段数 b(主提交版本)
- 对每个可能的总段数 b(从 1 到 message.length),计算「尾部标记」固定长度占用:
标记<a/b>的长度与 a 的位数有关;为简便,先求每个段尾标记长度为
tailLen(a) = 1 + len(a) + 1 + len(b) + 1 = 3 + len(a) + len(b)。 - 若每个段体(不含标记)长度一致为 bodyLen,则需
b * (tailLen 和)? 不对:
每段长度 = bodyLen + tailLen(a),最后一段 ≤ limit,其余 = limit。
第 a 段 (a<b) 长度 = bodyLen(段体)+ tailLen(a) == limit → bodyLen 不统一!
官方做法:第 a 段的 body 字符数 = limit - tailLen(a),最后一段 = 剩余。 - 判断:
canSplit(b):前 b-1 段每段可装limit - tailLen(a)个字符(必须 ≥ 0),
最后一段装全部剩余(剩余 ≤ limit - tailLen(b))。 - 从小到大枚举 b,首个可行即最优(段数最少)。
解法二:贪心逐段构造(对照) - 固定 b 后按上述公式逐段切分并拼回验证,与枚举思路一致,写法不同。
复杂度: - 时间复杂度:O(n * log n)(枚举 b ≤ n,每段 O(len) 拼接,最坏 O(n^2) 但 n=1e4 可行)。
- 空间复杂度:O(n)。
代码实现
java
import java.util.ArrayList;
import java.util.List;
public class Solution {
// 解法一:枚举总段数(主提交版本)
public String[] splitMessage(String message, int limit) {
int n = message.length();
for (int b = 1; b <= n; b++) {
if (canSplit(message, limit, b)) {
return build(message, limit, b);
}
}
return new String[0];
}
private int lenOf(int x) {
int c = 0;
while (x > 0) {
c++;
x /= 10;
}
return c;
}
private boolean canSplit(String msg, int limit, int b) {
int n = msg.length();
int lb = lenOf(b);
int used = 0;
for (int a = 1; a <= b; a++) {
int la = lenOf(a);
int tail = 3 + la + lb; // "<a/b>"
if (tail >= limit) {
return false; // 标记本身超限
}
if (a == b) {
// 最后一段:剩余字符 ≤ limit - tail
int rem = n - used;
if (rem > limit - tail) {
return false;
}
} else {
int body = limit - tail;
used += body;
if (used > n) {
return false; // 已超出总长(后面还有段但字符没了)
}
}
}
// 最后一段必须正好装完剩余
return used + 0 >= 0 && used <= n; // used 最终等于 n 检查在循环内完成
}
private String[] build(String msg, int limit, int b) {
int n = msg.length();
int lb = lenOf(b);
String[] res = new String[b];
int used = 0;
for (int a = 1; a <= b; a++) {
int la = lenOf(a);
int tail = 3 + la + lb;
String mark = "<" + a + "/" + b + ">";
if (a == b) {
String body = msg.substring(used);
res[a - 1] = body + mark;
} else {
int body = limit - tail;
res[a - 1] = msg.substring(used, used + body) + mark;
used += body;
}
}
return res;
}
// 解法二:校验函数(对照)
public boolean verify(String[] parts, String message) {
StringBuilder sb = new StringBuilder();
int b = parts.length;
for (int i = 0; i < parts.length; i++) {
String mark = "<" + (i + 1) + "/" + b + ">";
if (!parts[i].endsWith(mark)) {
return false;
}
sb.append(parts[i], 0, parts[i].length() - mark.length());
}
return sb.toString().equals(message);
}
public static void main(String[] args) {
Solution sol = new Solution();
String[] r1 = sol.splitMessage("this is really a very awesome message", 9);
check(r1.length == 14, "2468 ex1 len " + r1.length);
check(sol.verify(r1, "this is really a very awesome message"), "2468 ex1 verify");
String[] r2 = sol.splitMessage("short message", 15);
check(r2.length == 2 && r2[0].equals("short mess<1/2>") && r2[1].equals("age<2/2>"), "2468 ex2");
check(sol.verify(r2, "short message"), "2468 ex2 verify");
// 无法分割的情形:limit 太小
check(sol.splitMessage("abc", 1).length == 0, "2468 limit=1 无法分割");
// 随机验证
java.util.Random rnd = new java.util.Random(257);
for (int t = 0; t < 200; t++) {
int n = 1 + rnd.nextInt(40);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; i++) {
sb.append((char) ('a' + rnd.nextInt(3)));
}
String msg = sb.toString();
int limit = 1 + rnd.nextInt(15);
String[] parts = sol.splitMessage(msg, limit);
if (parts.length > 0) {
if (!sol.verify(parts, msg)) {
throw new AssertionError("2468 verify fail msg=" + msg + " limit=" + limit);
}
// 检查每段长度约束
for (int i = 0; i < parts.length; i++) {
if (i < parts.length - 1 && parts[i].length() != limit) {
throw new AssertionError("2468 len fail " + parts[i].length() + " limit=" + limit);
}
if (i == parts.length - 1 && parts[i].length() > limit) {
throw new AssertionError("2468 last len fail");
}
}
}
}
System.out.println("2468 全部 PASS");
}
private static void check(boolean ok, String msg) {
if (!ok) {
throw new AssertionError(msg);
}
System.out.println("PASS " + msg);
}
}
复杂度分析
- 时间复杂度:O(n * log n)(枚举 b ≤ n,每段 O(len) 拼接,最坏 O(n^2) 但 n=1e4 可行)。
- 空间复杂度:O(n)。
2469. Convert the Temperature(温度转换)
题目描述
给你一个四舍五入到两位小数的非负浮点数 celsius 来表示温度,以摄氏度(Celsius)为单位。
你需要将摄氏度转换为开氏度(Kelvin)和华氏度(Fahrenheit),
并以数组 ans = [kelvin, fahrenheit] 的形式返回结果。
注意:
- 开氏度 = 摄氏度 + 273.15
- 华氏度 = 摄氏度 * 1.80 + 32.00
示例 1
`celsius = 36.50`(摄氏)→ `[309.65000, 97.70000]`
示例 2
`celsius = 122.11`(摄氏)→ `[395.26000, 251.79800]`
提示:
0 <= celsius <= 1000
解题思路
解法一:公式直算(主提交版本)
- 直接按题目给出的两个换算公式计算
kelvin = celsius + 273.15、fahrenheit = celsius * 1.80 + 32.00。
解法二:拆步计算(对照) - 先算开氏,再用
celsius * 9 / 5 + 32(即 ×1.8 的等价写法)算华氏,结果一致。
复杂度: - 时间复杂度:O(1)
- 空间复杂度:O(1)
代码实现
java
public class Solution {
// 解法一:公式直算(主提交版本)
public double[] convertTemperature(double celsius) {
return new double[]{celsius + 273.15, celsius * 1.80 + 32.00};
}
// 解法二:拆步计算(对照,×9/5 + 32 等价写法)
public double[] convertTemperatureStep(double celsius) {
double kelvin = celsius + 273.15;
double fahrenheit = celsius * 9.0 / 5.0 + 32.0;
return new double[]{kelvin, fahrenheit};
}
public static void main(String[] args) {
Solution sol = new Solution();
double c1 = 36.50;
double[] r1 = sol.convertTemperature(c1);
double[] r1b = sol.convertTemperatureStep(c1);
assertNearly(r1, new double[]{309.65, 97.70});
assertNearly(r1b, new double[]{309.65, 97.70});
double c2 = 122.11;
double[] r2 = sol.convertTemperature(c2);
double[] r2b = sol.convertTemperatureStep(c2);
assertNearly(r2, new double[]{395.26, 251.798});
assertNearly(r2b, new double[]{395.26, 251.798});
double c3 = 0.0;
double[] r3 = sol.convertTemperature(c3);
assertNearly(r3, new double[]{273.15, 32.00});
double c4 = 1000.0;
double[] r4 = sol.convertTemperature(c4);
assertNearly(r4, new double[]{1273.15, 1832.00});
System.out.println("2469 全部 PASS");
}
private static void assertNearly(double[] actual, double[] expected) {
for (int i = 0; i < expected.length; i++) {
assert Math.abs(actual[i] - expected[i]) < 1e-5 : "期望 " + expected[i] + " 实际 " + actual[i];
}
}
}
复杂度分析
- 时间复杂度:O(1)
- 空间复杂度:O(1)
2470. Number of Subarrays With LCM Equal to K(最小公倍数等于 K 的子数组数目)
题目描述
给你一个整数数组 nums 和一个整数 k ,请你统计并返回 nums 的子数组中满足
元素最小公倍数为 k 的子数组数目。
子数组是数组中一个连续非空的元素序列。数组的最小公倍数是可被所有数组元素整除的最小正整数。
示例 1
`nums = [3,6,2,7,1], k = 6` → 4
示例 2
`nums = [3], k = 2` → 0
提示:
1 <= nums.length <= 10001 <= nums[i], k <= 1000
解题思路
解法一:右端点固定,滚动维护 LCM(主提交版本)
- 枚举子数组右端点
r,维护以r结尾的区间 LCM 集合。 - 由于 LCM 是单调不降的(随着区间左移不断并入新元素,LCM 只增不减),
不同 LCM 值数量很少(每个值只会被更大值替换,值域受k限制)。 - 对每个右端点,维护上一步的
<lcm, 区间个数>列表,并入nums[r]后合并相同 LCM。 - 统计 LCM == k 的区间个数,累加进答案。
解法二:暴力枚举全部子数组(对照) - 双重循环枚举
(l, r),边扩展边累乘 LCM,等于k则计数,超过k可提前剪枝退出。
复杂度: - 时间复杂度:解法一 O(n * τ)(τ 为每点 LCM 去重后的值个数,很小),解法二 O(n^2)
- 空间复杂度:O(τ)
代码实现
java
public class Solution {
private static int gcd(int a, int b) {
return b == 0 ? a : gcd(b, a % b);
}
private static long lcm(long a, long b) {
return a / gcd((int) a, (int) b) * b;
}
// 解法一:滚动 LCM 集合(主提交版本)
public int subarrayLCM(int[] nums, int k) {
int n = nums.length;
long[][] cur; // {lcm, count}
int curLen = 0;
cur = new long[n][2];
int ans = 0;
for (int r = 0; r < n; r++) {
long[][] nxt = new long[n + 1][2];
int nxtLen = 0;
long v = nums[r];
// 单个元素区间
nxt[nxtLen][0] = v;
nxt[nxtLen][1] = 1;
nxtLen++;
// 并入上一个右端点结束的所有区间
for (int i = 0; i < curLen; i++) {
long nlcm = lcm(cur[i][0], v);
if (nlcm > k) { // 超过 k 的不会再变回 k(LCM 单调不减)
continue;
}
boolean merged = false;
for (int j = 0; j < nxtLen; j++) {
if (nxt[j][0] == nlcm) {
nxt[j][1] += cur[i][1];
merged = true;
break;
}
}
if (!merged) {
nxt[nxtLen][0] = nlcm;
nxt[nxtLen][1] = cur[i][1];
nxtLen++;
}
}
for (int j = 0; j < nxtLen; j++) {
if (nxt[j][0] == k) {
ans += nxt[j][1];
}
}
cur = nxt;
curLen = nxtLen;
}
return ans;
}
// 解法二:暴力枚举全部子数组(对照)
public int subarrayLCMBrute(int[] nums, int k) {
int n = nums.length;
int ans = 0;
for (int l = 0; l < n; l++) {
long cur = 1;
for (int r = l; r < n; r++) {
cur = lcm(cur, nums[r]);
if (cur > k) {
break;
}
if (cur == k) {
ans++;
}
}
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
int[] n1 = {3, 6, 2, 7, 1};
assert sol.subarrayLCM(n1, 6) == 4 : "2470 例1 主";
assert sol.subarrayLCMBrute(n1, 6) == 4 : "2470 例1 暴力";
int[] n2 = {3};
assert sol.subarrayLCM(n2, 2) == 0 : "2470 例2 主";
assert sol.subarrayLCMBrute(n2, 2) == 0 : "2470 例2 暴力";
int[] n3 = {2, 3};
assert sol.subarrayLCM(n3, 6) == 1 : "2470 例3 主";
assert sol.subarrayLCMBrute(n3, 6) == 1 : "2470 例3 暴力";
// 随机对拍
java.util.Random rnd = new java.util.Random(42);
for (int t = 0; t < 500; t++) {
int len = 1 + rnd.nextInt(30);
int[] arr = new int[len];
int k = 1 + rnd.nextInt(60);
for (int i = 0; i < len; i++) {
arr[i] = 1 + rnd.nextInt(60);
}
int a = sol.subarrayLCM(arr, k);
int b = sol.subarrayLCMBrute(arr, k);
if (a != b) {
System.out.println("对拍失败 nums=" + java.util.Arrays.toString(arr) + " k=" + k + " 主=" + a + " 暴力=" + b);
System.exit(1);
}
}
System.out.println("2470 全部 PASS");
}
}
复杂度分析
- 时间复杂度:解法一 O(n * τ)(τ 为每点 LCM 去重后的值个数,很小),解法二 O(n^2)
- 空间复杂度:O(τ)
2471. Minimum Number of Operations to Sort a Binary Tree by Level(逐层排序二叉树所需的最少操作数目)
题目描述
给你一个值互不相同的二叉树的根节点 root。
在一步操作中,你可以选择同一层上任意两个节点,交换这两个节点的值。
返回每一层按严格递增顺序排序所需的最少操作数目。
节点的层数是该节点和根节点之间的路径的边数。
示例 1
`root = [1,4,3,7,6,8,5,null,null,null,null,9,null,10]` → 3
示例 2
`root = [1,3,2,7,6,5,4]` → 3
示例 3
`root = [1,2,3,4,5,6]` → 0
提示:
- 树中节点的数目在范围
[1, 10^5] 1 <= Node.val <= 10^5- 树中的所有值互不相同
解题思路
解法一:BFS 分层 + 置换环计数(主提交版本)
- BFS 按层收集每层节点值数组
vals。 - 对每一层:将
vals拷贝排序得到目标数组sorted,记录每个值应当落在的位置pos。 - 遍历每个元素,沿着「当前位置 → 目标位置」的置换形成若干环,每个长度为 L 的环
需要 L - 1 次交换才能就位,累加环长 - 1。 - 各层最少交换次数相加即为答案。
解法二:逐层冒泡式计数(对照) - 同样 BFS 分层,但每层用「贪心交换」:扫描 i,若
vals[i] != sorted[i],
找到值sorted[i]所在位置 j 并交换,每交换一次计数 +1。
复杂度: - 时间复杂度:O(n log n)(每层排序,总元素为 n)
- 空间复杂度:O(n)
代码实现
java
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Deque;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class Solution {
// 解法一:BFS + 置换环(主提交版本)
public int minimumOperations(TreeNode root) {
int ans = 0;
if (root == null) {
return 0;
}
Deque<TreeNode> q = new ArrayDeque<>();
q.offer(root);
while (!q.isEmpty()) {
int size = q.size();
int[] vals = new int[size];
for (int i = 0; i < size; i++) {
TreeNode node = q.poll();
vals[i] = node.val;
if (node.left != null) {
q.offer(node.left);
}
if (node.right != null) {
q.offer(node.right);
}
}
ans += minSwaps(vals);
}
return ans;
}
private int minSwaps(int[] vals) {
int n = vals.length;
int[] sorted = vals.clone();
Arrays.sort(sorted);
Map<Integer, Integer> pos = new HashMap<>();
for (int i = 0; i < n; i++) {
pos.put(sorted[i], i);
}
boolean[] vis = new boolean[n];
int swaps = 0;
for (int i = 0; i < n; i++) {
if (!vis[i]) {
int j = i;
int len = 0;
while (!vis[j]) {
vis[j] = true;
j = pos.get(vals[j]);
len++;
}
if (len > 0) {
swaps += len - 1;
}
}
}
return swaps;
}
// 解法二:BFS + 贪心交换(对照)
public int minimumOperationsGreedy(TreeNode root) {
int ans = 0;
if (root == null) {
return 0;
}
Deque<TreeNode> q = new ArrayDeque<>();
q.offer(root);
while (!q.isEmpty()) {
int size = q.size();
int[] vals = new int[size];
for (int i = 0; i < size; i++) {
TreeNode node = q.poll();
vals[i] = node.val;
if (node.left != null) {
q.offer(node.left);
}
if (node.right != null) {
q.offer(node.right);
}
}
int[] sorted = vals.clone();
Arrays.sort(sorted);
Map<Integer, Integer> pos = new HashMap<>();
for (int i = 0; i < size; i++) {
pos.put(vals[i], i);
}
for (int i = 0; i < size; i++) {
if (vals[i] != sorted[i]) {
int j = pos.get(sorted[i]);
// 交换 vals[i] 与 vals[j]
int tmp = vals[i];
vals[i] = vals[j];
vals[j] = tmp;
pos.put(vals[i], i);
pos.put(vals[j], j);
ans++;
}
}
}
return ans;
}
// ---- 树构建辅助(层序数组 → 二叉树) ----
static class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int val) {
this.val = val;
}
}
private static TreeNode buildTree(List<Integer> arr) {
if (arr == null || arr.isEmpty() || arr.get(0) == null) {
return null;
}
TreeNode root = new TreeNode(arr.get(0));
Deque<TreeNode> q = new ArrayDeque<>();
q.offer(root);
int idx = 1;
while (!q.isEmpty() && idx < arr.size()) {
TreeNode node = q.poll();
if (idx < arr.size() && arr.get(idx) != null) {
node.left = new TreeNode(arr.get(idx));
q.offer(node.left);
}
idx++;
if (idx < arr.size() && arr.get(idx) != null) {
node.right = new TreeNode(arr.get(idx));
q.offer(node.right);
}
idx++;
}
return root;
}
public static void main(String[] args) {
Solution sol = new Solution();
TreeNode r1 = buildTree(Arrays.asList(1, 4, 3, 7, 6, 8, 5, null, null, null, null, 9, null, 10));
assert sol.minimumOperations(r1) == 3 : "2471 例1 环";
assert sol.minimumOperationsGreedy(r1) == 3 : "2471 例1 贪心";
TreeNode r2 = buildTree(Arrays.asList(1, 3, 2, 7, 6, 5, 4));
assert sol.minimumOperations(r2) == 3 : "2471 例2 环";
assert sol.minimumOperationsGreedy(r2) == 3 : "2471 例2 贪心";
TreeNode r3 = buildTree(Arrays.asList(1, 2, 3, 4, 5, 6));
assert sol.minimumOperations(r3) == 0 : "2471 例3 环";
assert sol.minimumOperationsGreedy(r3) == 0 : "2471 例3 贪心";
TreeNode r4 = new TreeNode(1);
assert sol.minimumOperations(r4) == 0 : "2471 单节点 环";
assert sol.minimumOperationsGreedy(r4) == 0 : "2471 单节点 贪心";
System.out.println("2471 全部 PASS");
}
}
复杂度分析
- 时间复杂度:O(n log n)(每层排序,总元素为 n)
- 空间复杂度:O(n)
2472. Maximum Number of Non-overlapping Palindrome Substrings(不重叠回文子字符串的最大数目)
题目描述
给你一个字符串 s 和一个正整数 k。
从字符串 s 中选出一组满足下述条件且不重叠的子字符串:
- 每个子字符串的长度至少为
k - 每个子字符串是一个回文串
返回最优方案中能选择的子字符串的最大数目。
示例 1
`s = "abaccdbbd", k = 3` → 2(可选 "aba" 和 "dbbd")
示例 2
`s = "adbcda", k = 2` → 0
提示:
1 <= k <= s.length <= 2000s仅由小写英文字母组成
解题思路
解法一:区间 DP 判回文 + 线性 DP 选区间(主提交版本)
- 先做区间 DP:
pal[i][j]表示子串s[i..j]是否为回文。
pal[i][j] = (s[i] == s[j]) && (j - i < 2 || pal[i + 1][j - 1])。 - 再做一维 DP:
dp[i]表示前缀s[0..i-1]最多能选出多少个合法回文子串,
为了让已选子串尽量靠左给后面留空间(贪心思想),转移: - 不选以 i-1 结尾的回文:
dp[i + 1] = max(dp[i + 1], dp[i]) - 若
s[j..i]是回文且长度 ≥ k:dp[i + 1] = max(dp[i + 1], dp[j] + 1)
解法二:中心扩展建表 + 线性 DP(对照) - 用中心扩展法(奇数中心 1 个点、偶数中心 2 个点向两侧扩散)标记所有回文区间
pal[l][r]。 - DP 转移与解法一相同,只是回文表的构建方式不同(中心扩展 vs 区间递推)。
复杂度: - 时间复杂度:O(n^2)
- 空间复杂度:O(n^2)
代码实现
java
public class Solution {
// 解法一:区间 DP + 线性 DP(主提交版本)
public int maxPalindromes(String s, int k) {
int n = s.length();
boolean[][] pal = new boolean[n][n];
for (int len = 1; len <= n; len++) {
for (int i = 0; i + len - 1 < n; i++) {
int j = i + len - 1;
if (s.charAt(i) == s.charAt(j) && (len <= 2 || pal[i + 1][j - 1])) {
pal[i][j] = true;
}
}
}
// dp[i] = 前缀 s[0..i-1] 的最大选择数
int[] dp = new int[n + 1];
for (int i = 0; i < n; i++) {
dp[i + 1] = Math.max(dp[i + 1], dp[i]);
for (int j = i; j >= 0; j--) {
int len = i - j + 1;
if (len >= k && pal[j][i]) {
dp[i + 1] = Math.max(dp[i + 1], dp[j] + 1);
}
}
}
return dp[n];
}
// 解法二:中心扩展建表 + 线性 DP(对照)
public int maxPalindromesGreedy(String s, int k) {
int n = s.length();
boolean[][] pal = new boolean[n][n];
// 奇数中心
for (int c = 0; c < n; c++) {
int l = c, r = c;
while (l >= 0 && r < n && s.charAt(l) == s.charAt(r)) {
pal[l][r] = true;
l--;
r++;
}
}
// 偶数中心
for (int c = 0; c + 1 < n; c++) {
int l = c, r = c + 1;
while (l >= 0 && r < n && s.charAt(l) == s.charAt(r)) {
pal[l][r] = true;
l--;
r++;
}
}
int[] dp = new int[n + 1];
for (int i = 0; i < n; i++) {
dp[i + 1] = Math.max(dp[i + 1], dp[i]);
for (int j = i; j >= 0; j--) {
if (i - j + 1 >= k && pal[j][i]) {
dp[i + 1] = Math.max(dp[i + 1], dp[j] + 1);
}
}
}
return dp[n];
}
private boolean isPal(String s, int l, int r) {
while (l < r) {
if (s.charAt(l) != s.charAt(r)) {
return false;
}
l++;
r--;
}
return true;
}
public static void main(String[] args) {
Solution sol = new Solution();
assert sol.maxPalindromes("abaccdbbd", 3) == 2 : "2472 例1 主";
assert sol.maxPalindromesGreedy("abaccdbbd", 3) == 2 : "2472 例1 贪心";
assert sol.maxPalindromes("adbcda", 2) == 0 : "2472 例2 主";
assert sol.maxPalindromesGreedy("adbcda", 2) == 0 : "2472 例2 贪心";
assert sol.maxPalindromes("aaaa", 2) == 2 : "2472 特例主"; // "aa" + "aa"
assert sol.maxPalindromesGreedy("aaaa", 2) == 2 : "2472 特例贪心";
assert sol.maxPalindromes("a", 1) == 1 : "2472 单字符主";
assert sol.maxPalindromesGreedy("a", 1) == 1 : "2472 单字符贪心";
// 随机对拍(n 小规模)
java.util.Random rnd = new java.util.Random(7);
for (int t = 0; t < 300; t++) {
int n = 1 + rnd.nextInt(25);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; i++) {
sb.append((char) ('a' + rnd.nextInt(4)));
}
String s = sb.toString();
int k = 1 + rnd.nextInt(n);
int a = sol.maxPalindromes(s, k);
int b = sol.maxPalindromesGreedy(s, k);
if (a != b) {
System.out.println("对拍失败 s=" + s + " k=" + k + " 主=" + a + " 贪心=" + b);
System.exit(1);
}
}
System.out.println("2472 全部 PASS");
}
}
复杂度分析
- 时间复杂度:O(n^2)
- 空间复杂度:O(n^2)
2473. Minimum Cost to Buy Apples(购买苹果的最低成本)
题目描述
给你一个正整数 n,表示从 1 到 n 的 n 个城市。还给你一个二维数组 roads,
其中 roads[i] = [ai, bi, costi] 表示在城市 ai 和 bi 之间有一条双向道路,其旅行成本等于 costi。
你可以在任何城市买到苹果,但是有些城市买苹果的费用不同。给定数组 appleCost,其中 appleCost[i]
是从城市 i 购买一个苹果的成本。
你从某个城市开始,穿越各种道路,最终从任何一个城市买一个苹果。买了苹果之后,你必须回到开始的
城市,但现在所有道路的成本将乘以一个给定的因子 k。
返回大小为 n 的数组 answer,其中 answer[i] 是从城市 i 开始购买一个苹果的最小总成本。
示例 1
`n = 4, roads = [[1,2,4],[2,3,2],[2,4,5],[3,4,1],[1,3,4]], appleCost = [56,42,102,301], k = 2` → `[54,42,48,51]`
示例 2
`n = 3, roads = [[1,2,5],[2,3,1],[3,1,2]], appleCost = [2,3,1], k = 3` → `[2,3,1]`
提示:
2 <= n <= 10001 <= roads.length <= 10001 <= costi <= 10^5,1 <= appleCost[i] <= 10^51 <= k <= 100,无重复边
解题思路
解法一:以每个城市为源点跑 Dijkstra(主提交版本)
- 对起点 i,单趟最短距离数组
dist[i]表示从 i 到各城市的最短单程费用。 - 总费用 = 单程去程 distt + 苹果价 appleCostt + 返程 distt * k。
即在城市 t 买苹果:answer[i] = min_t (dist[t] * (1 + k) + appleCost[t])。 - 对每个 i 跑一遍 Dijkstra(n ≤ 1000,边 ≤ 1000,复杂度可接受)。
解法二:反向构图 + 多源扩展(对照) - 从「买苹果」的角度看,等价于对每个卖点 t,向外扩展:单程费用 d 满足
d * (1 + k) + appleCost[t]。但返程 k 倍使简单多源失效;这里改用朴素 Bellman 式
逐步松弛验证 = 逐城市 Dijkstra 的一致性(写法不同:直接对每个源点用优先队列)。
复杂度: - 时间复杂度:O(n * (m log n)),n, m ≤ 1000
- 空间复杂度:O(n + m)
代码实现
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.PriorityQueue;
public class Solution {
// 解法一:逐源点 Dijkstra(主提交版本)
public long[] minCost(int n, int[][] roads, int[] appleCost, int k) {
List<int[]>[] g = buildGraph(n, roads);
long[] ans = new long[n];
for (int s = 0; s < n; s++) {
long[] dist = dijkstra(g, s, n);
long best = Long.MAX_VALUE;
for (int t = 0; t < n; t++) {
if (dist[t] == Long.MAX_VALUE) {
continue;
}
best = Math.min(best, dist[t] * (1L + k) + appleCost[t]);
}
ans[s] = best;
}
return ans;
}
private List<int[]>[] buildGraph(int n, int[][] roads) {
List<int[]>[] g = new List[n];
for (int i = 0; i < n; i++) {
g[i] = new ArrayList<>();
}
for (int[] r : roads) {
int a = r[0] - 1, b = r[1] - 1; // 转 0 基
g[a].add(new int[]{b, r[2]});
g[b].add(new int[]{a, r[2]});
}
return g;
}
private long[] dijkstra(List<int[]>[] g, int s, int n) {
long[] dist = new long[n];
Arrays.fill(dist, Long.MAX_VALUE);
dist[s] = 0;
PriorityQueue<long[]> pq = new PriorityQueue<>((x, y) -> Long.compare(x[1], y[1])); // {node, cost}
pq.offer(new long[]{s, 0});
while (!pq.isEmpty()) {
long[] cur = pq.poll();
int u = (int) cur[0];
long d = cur[1];
if (d > dist[u]) {
continue;
}
for (int[] e : g[u]) {
int v = e[0];
long nd = d + e[1];
if (nd < dist[v]) {
dist[v] = nd;
pq.offer(new long[]{v, nd});
}
}
}
return dist;
}
// 解法二:Floyd 预处理全源最短路(对照)
public long[] minCostFloyd(int n, int[][] roads, int[] appleCost, int k) {
long INF = Long.MAX_VALUE / 4;
long[][] dist = new long[n][n];
for (int i = 0; i < n; i++) {
Arrays.fill(dist[i], INF);
dist[i][i] = 0;
}
for (int[] r : roads) {
int a = r[0] - 1, b = r[1] - 1;
dist[a][b] = Math.min(dist[a][b], r[2]);
dist[b][a] = Math.min(dist[b][a], r[2]);
}
for (int mid = 0; mid < n; mid++) {
for (int i = 0; i < n; i++) {
if (dist[i][mid] == INF) {
continue;
}
for (int j = 0; j < n; j++) {
if (dist[mid][j] == INF) {
continue;
}
dist[i][j] = Math.min(dist[i][j], dist[i][mid] + dist[mid][j]);
}
}
}
long[] ans = new long[n];
for (int i = 0; i < n; i++) {
long best = Long.MAX_VALUE;
for (int t = 0; t < n; t++) {
if (dist[i][t] == INF) {
continue;
}
best = Math.min(best, dist[i][t] * (1L + k) + appleCost[t]);
}
ans[i] = best;
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
long[] r1 = sol.minCost(4, new int[][]{{1, 2, 4}, {2, 3, 2}, {2, 4, 5}, {3, 4, 1}, {1, 3, 4}},
new int[]{56, 42, 102, 301}, 2);
assert Arrays.equals(r1, new long[]{54, 42, 48, 51}) : "2473 例1 主: " + Arrays.toString(r1);
long[] r1b = sol.minCostFloyd(4, new int[][]{{1, 2, 4}, {2, 3, 2}, {2, 4, 5}, {3, 4, 1}, {1, 3, 4}},
new int[]{56, 42, 102, 301}, 2);
assert Arrays.equals(r1b, new long[]{54, 42, 48, 51}) : "2473 例1 Floyd: " + Arrays.toString(r1b);
long[] r2 = sol.minCost(3, new int[][]{{1, 2, 5}, {2, 3, 1}, {3, 1, 2}}, new int[]{2, 3, 1}, 3);
assert Arrays.equals(r2, new long[]{2, 3, 1}) : "2473 例2 主: " + Arrays.toString(r2);
long[] r2b = sol.minCostFloyd(3, new int[][]{{1, 2, 5}, {2, 3, 1}, {3, 1, 2}}, new int[]{2, 3, 1}, 3);
assert Arrays.equals(r2b, new long[]{2, 3, 1}) : "2473 例2 Floyd: " + Arrays.toString(r2b);
// 随机对拍(小规模)
java.util.Random rnd = new java.util.Random(11);
for (int t = 0; t < 200; t++) {
int n = 2 + rnd.nextInt(8);
int m = rnd.nextInt(n * (n - 1) / 2 + 1);
int[][] roads = new int[m][3];
java.util.Set<Integer> seen = new java.util.HashSet<>();
for (int i = 0; i < m; i++) {
int a, b;
do {
a = rnd.nextInt(n) + 1;
b = rnd.nextInt(n) + 1;
} while (a == b || !seen.add(a * 100 + b));
roads[i][0] = a;
roads[i][1] = b;
roads[i][2] = 1 + rnd.nextInt(20);
}
int[] apple = new int[n];
for (int i = 0; i < n; i++) {
apple[i] = 1 + rnd.nextInt(100);
}
int k = 1 + rnd.nextInt(10);
long[] a1 = sol.minCost(n, roads, apple, k);
long[] a2 = sol.minCostFloyd(n, roads, apple, k);
if (!Arrays.equals(a1, a2)) {
System.out.println("对拍失败 n=" + n + " roads=" + Arrays.deepToString(roads)
+ " apple=" + Arrays.toString(apple) + " k=" + k
+ " 主=" + Arrays.toString(a1) + " Floyd=" + Arrays.toString(a2));
System.exit(1);
}
}
System.out.println("2473 全部 PASS");
}
}
复杂度分析
- 时间复杂度:O(n * (m log n)),n, m ≤ 1000
- 空间复杂度:O(n + m)
2474. Customers With Strictly Increasing Purchases(购买量严格增加的客户)
题目类型:数据库(SQL)
题目描述
表: Orders
+--------------+------+
| Column Name | Type |
+--------------+------+
| order_id | int |
| customer_id | int |
| order_date | date |
| price | int |
+--------------+------+
order_id 是该表的主键。每行包含订单的 id、订购该订单的客户 id、订单日期和价格。
编写一个 SQL 查询,报告总购买量每年严格增加 的客户 id。
- 客户在一年内的总购买量 是该年订单价格的总和。如果某一年客户没有下任何订单,我们认为总购买量为
0。 - 对于每个客户,要考虑的第一年是第一次下单的年份。
- 对于每个客户,要考虑的最后一年是最后一次下单的年份。
以任意顺序返回结果表。
示例 1:
输入:
Orders 表:
+----------+-------------+------------+-------+
| order_id | customer_id | order_date | price |
+----------+-------------+------------+-------+
| 1 | 1 | 2019-07-01 | 1100 |
| 2 | 1 | 2019-11-01 | 1200 |
| 3 | 1 | 2020-05-26 | 3000 |
| 4 | 1 | 2021-08-31 | 3100 |
| 5 | 1 | 2022-12-07 | 4700 |
| 6 | 2 | 2015-01-01 | 700 |
| 7 | 2 | 2017-11-07 | 1000 |
| 8 | 3 | 2017-01-01 | 900 |
| 9 | 3 | 2018-11-07 | 900 |
+----------+-------------+------------+-------+
输出:
+-------------+
| customer_id |
+-------------+
| 1 |
+-------------+
解释:
客户 1: 第一年是 2019 年,最后一年是 2022 年
- 2019: 1100 + 1200 = 2300
- 2020: 3000
- 2021: 3100
- 2022: 4700
总购买量每年严格增加,所以包含客户 1。
客户 2: 第一年是 2015 年,最后一年是 2017 年
- 2015: 700
- 2016: 0(当年无订单)
- 2017: 1000
2016 年为 0,并非严格增加,所以不包含客户 2。
客户 3: 第一年是 2017 年,最后一年是 2018 年
- 2017: 900
- 2018: 900
两年相等,并非严格增加,所以不包含客户 3。
解题思路
解法一:年度汇总补零 + LAG 同比严格递增判断
- 第一步:按
customer_id, 年份汇总出每个客户每年实际购买量yearly(SUM(price))。 - 第二步:某年没有订单要按
0计。为连续对比,需要把「首次下单年 → 末次下单年」之间的每一年都补齐(0)。可用递归/数字表生成客户年份序列,或直接使用LAG与前一年的实际值比较------补零更稳妥。 - 具体做法:
yearly:SELECT customer_id, YEAR(order_date) y, SUM(price) amount FROM Orders GROUP BY customer_id, YEAR(order_date)。- 对每个客户,用递归 CTE
seq生成从MIN(y)到MAX(y)的连续年份,再LEFT JOIN yearly得到amount(无订单年COALESCE(..., 0))。 - 用
LAG(amount) OVER (PARTITION BY customer_id ORDER BY y)取上一年购买量; - 保留所有年份都是
amount > prev的客户:GROUP BY customer_id HAVING SUM(CASE WHEN amount > prev OR prev IS NULL THEN 0 ELSE 1 END) = 0。
解法二(对照):JOIN 自比较相邻年
- 同样先补出连续年份序列,再用自连接
a JOIN b ON a.customer_id = b.customer_id AND a.y = b.y - 1比较相邻两年; - 若存在任何一对相邻年
a.amount >= b.amount则淘汰该客户;等价于NOT EXISTS反证。
代码实现
sql
-- 解法一(推荐):年度汇总 + 递归补零 + LAG 严格递增判断
WITH RECURSIVE yearly AS (
SELECT customer_id, YEAR(order_date) AS y,
SUM(price) AS amount
FROM Orders
GROUP BY customer_id, YEAR(order_date)
),
seq AS (
SELECT customer_id, MIN(y) AS y, MAX(y) AS maxy
FROM yearly
GROUP BY customer_id
UNION ALL
SELECT customer_id, y + 1, maxy
FROM seq
WHERE y < maxy
),
full_year AS (
SELECT s.customer_id, s.y,
COALESCE(y.amount, 0) AS amount
FROM seq s
LEFT JOIN yearly y
ON y.customer_id = s.customer_id AND y.y = s.y
),
with_prev AS (
SELECT customer_id, y, amount,
LAG(amount) OVER (PARTITION BY customer_id ORDER BY y) AS prev
FROM full_year
)
SELECT DISTINCT customer_id
FROM with_prev
GROUP BY customer_id
HAVING SUM(CASE WHEN prev IS NOT NULL AND amount <= prev THEN 1 ELSE 0 END) = 0;
sql
-- 解法二(对照):连续年份自连接比较相邻年
WITH RECURSIVE yearly AS (
SELECT customer_id, YEAR(order_date) AS y,
SUM(price) AS amount
FROM Orders
GROUP BY customer_id, YEAR(order_date)
),
seq AS (
SELECT customer_id, MIN(y) AS y, MAX(y) AS maxy
FROM yearly
GROUP BY customer_id
UNION ALL
SELECT customer_id, y + 1, maxy
FROM seq
WHERE y < maxy
),
full_year AS (
SELECT s.customer_id, s.y,
COALESCE(y.amount, 0) AS amount
FROM seq s
LEFT JOIN yearly y
ON y.customer_id = s.customer_id AND y.y = s.y
)
SELECT DISTINCT a.customer_id
FROM full_year a
WHERE a.y > (SELECT MIN(y) FROM full_year WHERE customer_id = a.customer_id)
AND NOT EXISTS (
SELECT 1
FROM full_year b
WHERE b.customer_id = a.customer_id
AND b.y = a.y - 1
AND b.amount >= a.amount
);
复杂度分析
- 时间复杂度 :
O(n + y),n为Orders行数、y为所有客户【末次年 - 首次年】跨度之和;递归补零遍历每个客户每年的记录。 - 空间复杂度 :
O(y),递归 CTE 与中间年份表与生成的年记录数成正比。
2475. Number of Unequal Triplets in Array(数组中不等三元组的数目)
题目描述
给你一个下标从 0 开始的正整数数组 nums。找出并统计满足下述条件的三元组 (i, j, k) 的数目:
0 <= i < j < k < nums.lengthnums[i]、nums[j]和nums[k]两两不同。
示例 1
`nums = [4,4,2,4,3]` → 3
示例 2
`nums = [1,1,1,1,1]` → 0
提示:
3 <= nums.length <= 1001 <= nums[i] <= 1000
解题思路
解法一:频率计数 + 组合计数(主提交版本)
- 用哈希表统计每个值出现的频次
cnt。 - 枚举三个不同值 a、b、c,其三元组个数为
cnt[a] * cnt[b] * cnt[c],累加即可。 - 三个值只需保证不相同(数组下标天然满足 i < j < k:每个下标只属于一个值,任意
从三个不交值集合中各取一个下标都自动满足 i<j<k 的相对顺序)。
解法二:三重暴力枚举(对照) - 直接枚举所有 (i, j, k) 组合并判断三者两两不同。
复杂度: - 时间复杂度:O(n + v^3)(v 为不同值个数),解法二 O(n^3)
- 空间复杂度:O(v)
代码实现
java
public class Solution {
// 解法一:频次 + 组合计数(主提交版本)
public int unequalTriplets(int[] nums) {
java.util.Map<Integer, Integer> cnt = new java.util.HashMap<>();
for (int v : nums) {
cnt.put(v, cnt.getOrDefault(v, 0) + 1);
}
Integer[] keys = cnt.keySet().toArray(new Integer[0]);
int m = keys.length;
int ans = 0;
for (int i = 0; i < m; i++) {
for (int j = i + 1; j < m; j++) {
for (int t = j + 1; t < m; t++) {
ans += cnt.get(keys[i]) * cnt.get(keys[j]) * cnt.get(keys[t]);
}
}
}
return ans;
}
// 解法二:暴力三重循环(对照)
public int unequalTripletsBrute(int[] nums) {
int n = nums.length;
int ans = 0;
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
for (int k = j + 1; k < n; k++) {
if (nums[i] != nums[j] && nums[i] != nums[k] && nums[j] != nums[k]) {
ans++;
}
}
}
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
assert sol.unequalTriplets(new int[]{4, 4, 2, 4, 3}) == 3 : "2475 例1 主";
assert sol.unequalTripletsBrute(new int[]{4, 4, 2, 4, 3}) == 3 : "2475 例1 暴力";
assert sol.unequalTriplets(new int[]{1, 1, 1, 1, 1}) == 0 : "2475 例2 主";
assert sol.unequalTripletsBrute(new int[]{1, 1, 1, 1, 1}) == 0 : "2475 例2 暴力";
assert sol.unequalTriplets(new int[]{1, 2, 3}) == 1 : "2475 例3 主";
assert sol.unequalTripletsBrute(new int[]{1, 2, 3}) == 1 : "2475 例3 暴力";
java.util.Random rnd = new java.util.Random(5);
for (int t = 0; t < 300; t++) {
int n = 3 + rnd.nextInt(20);
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = 1 + rnd.nextInt(10);
}
int a = sol.unequalTriplets(arr);
int b = sol.unequalTripletsBrute(arr);
if (a != b) {
System.out.println("对拍失败 " + java.util.Arrays.toString(arr) + " 主=" + a + " 暴力=" + b);
System.exit(1);
}
}
System.out.println("2475 全部 PASS");
}
}
复杂度分析
- 时间复杂度:O(n + v^3)(v 为不同值个数),解法二 O(n^3)
- 空间复杂度:O(v)
2476. Closest Nodes Queries in a Binary Search Tree(二叉搜索树最近节点查询)
题目描述
给你一个二叉搜索树的根节点 root,和一个由正整数组成、长度为 n 的数组 queries。
找出一个长度为 n 的二维答案数组 answer,其中 answer[i] = [mini, maxi]:
mini是树中小于等于queries[i]的最大值;如果不存在这样的值,则使用 -1 代替。maxi是树中大于等于queries[i]的最小值;如果不存在这样的值,则使用 -1 代替。
示例 1
`root = [6,2,13,1,4,9,15,null,null,null,null,null,null,14], queries = [2,5,16]` → `[[2,2],[4,6],[15,-1]]`
示例 2
`root = [4,null,9], queries = [3]` → `[[-1,4]]`
提示:
- 树中节点的数目在范围
[2, 10^5] 1 <= Node.val <= 10^6n == queries.length,1 <= n <= 10^5,1 <= queries[i] <= 10^6
解题思路
解法一:中序收集 + 二分查找(主提交版本)
- 二叉搜索树中序遍历得到严格递增的节点值数组
vals。 - 对每个查询
q: mini:二分找最后一个<= q的元素(lowerBound(q+1) - 1)。maxi:二分找第一个>= q的元素(lowerBound(q))。- 找不到返回 -1。
解法二:迭代中序(对照) - 用显式栈做中序遍历,写法不同,结果一致。
复杂度: - 时间复杂度:O(n + q log n)
- 空间复杂度:O(n)
代码实现
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class Solution {
public static class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int val) {
this.val = val;
}
}
// 解法一:递归中序 + 二分(主提交版本)
public List<List<Integer>> closestNodes(TreeNode root, List<Integer> queries) {
List<Integer> vals = new ArrayList<>();
inorder(root, vals);
int[] arr = new int[vals.size()];
for (int i = 0; i < vals.size(); i++) {
arr[i] = vals.get(i);
}
List<List<Integer>> ans = new ArrayList<>();
for (int q : queries) {
int min = -1, max = -1;
// mini:最后一个 <= q
int lo = 0, hi = arr.length - 1;
while (lo <= hi) {
int mid = (lo + hi) >>> 1;
if (arr[mid] <= q) {
min = arr[mid];
lo = mid + 1;
} else {
hi = mid - 1;
}
}
// maxi:第一个 >= q
lo = 0;
hi = arr.length - 1;
while (lo <= hi) {
int mid = (lo + hi) >>> 1;
if (arr[mid] >= q) {
max = arr[mid];
hi = mid - 1;
} else {
lo = mid + 1;
}
}
ans.add(Arrays.asList(min, max));
}
return ans;
}
private void inorder(TreeNode node, List<Integer> vals) {
if (node == null) {
return;
}
inorder(node.left, vals);
vals.add(node.val);
inorder(node.right, vals);
}
// 解法二:迭代中序(对照)
public List<List<Integer>> closestNodesIter(TreeNode root, List<Integer> queries) {
List<Integer> vals = new ArrayList<>();
java.util.Deque<TreeNode> st = new java.util.ArrayDeque<>();
TreeNode cur = root;
while (cur != null || !st.isEmpty()) {
while (cur != null) {
st.push(cur);
cur = cur.left;
}
cur = st.pop();
vals.add(cur.val);
cur = cur.right;
}
int[] arr = new int[vals.size()];
for (int i = 0; i < vals.size(); i++) {
arr[i] = vals.get(i);
}
List<List<Integer>> ans = new ArrayList<>();
for (int q : queries) {
int min = -1, max = -1;
// 用 Arrays.binarySearch 语义:返回插入点
int pos = Arrays.binarySearch(arr, q);
if (pos >= 0) {
min = arr[pos];
max = arr[pos];
} else {
int ins = -pos - 1;
if (ins - 1 >= 0) {
min = arr[ins - 1];
}
if (ins < arr.length) {
max = arr[ins];
}
}
ans.add(Arrays.asList(min, max));
}
return ans;
}
private static TreeNode build(List<Integer> arr) {
if (arr == null || arr.isEmpty() || arr.get(0) == null) {
return null;
}
TreeNode root = new TreeNode(arr.get(0));
java.util.Deque<TreeNode> q = new java.util.ArrayDeque<>();
q.offer(root);
int i = 1;
while (!q.isEmpty() && i < arr.size()) {
TreeNode node = q.poll();
if (arr.get(i) != null) {
node.left = new TreeNode(arr.get(i));
q.offer(node.left);
}
i++;
if (i < arr.size() && arr.get(i) != null) {
node.right = new TreeNode(arr.get(i));
q.offer(node.right);
}
i++;
}
return root;
}
public static void main(String[] args) {
Solution sol = new Solution();
TreeNode r1 = build(Arrays.asList(6, 2, 13, 1, 4, 9, 15, null, null, null, null, null, null, 14));
List<List<Integer>> a1 = sol.closestNodes(r1, Arrays.asList(2, 5, 16));
List<List<Integer>> a1b = sol.closestNodesIter(r1, Arrays.asList(2, 5, 16));
assert a1.equals(Arrays.asList(Arrays.asList(2, 2), Arrays.asList(4, 6), Arrays.asList(15, -1))) : "2476 例1 主: " + a1;
assert a1b.equals(Arrays.asList(Arrays.asList(2, 2), Arrays.asList(4, 6), Arrays.asList(15, -1))) : "2476 例1 迭代: " + a1b;
TreeNode r2 = build(Arrays.asList(4, null, 9));
List<List<Integer>> a2 = sol.closestNodes(r2, Collections.singletonList(3));
List<List<Integer>> a2b = sol.closestNodesIter(r2, Collections.singletonList(3));
assert a2.equals(Arrays.asList(Arrays.asList(-1, 4))) : "2476 例2 主: " + a2;
assert a2b.equals(Arrays.asList(Arrays.asList(-1, 4))) : "2476 例2 迭代: " + a2b;
System.out.println("2476 全部 PASS");
}
}
复杂度分析
- 时间复杂度:O(n + q log n)
- 空间复杂度:O(n)
2477. Minimum Fuel Cost to Report to the Capital(到达首都的最少油耗)
题目描述
给你一棵 n 个节点的树(无向、连通、无环图),每个节点表示一个城市,编号从 0 到 n-1,
恰好有 n-1 条路。0 是首都。roads[i] = [ai, bi] 表示城市 ai 和 bi 之间有一条双向路。
每个城市里有一个代表,他们都要去首都参加一个会议。每座城市里有一辆车,
给你一个整数 seats 表示每辆车里面座位的数目。
相邻城市之间一辆车的油耗是一升汽油。返回到达首都最少需要多少升汽油。
示例 1
`roads = [[0,1],[0,2],[0,3]], seats = 5` → 3
示例 2
`roads = [[3,1],[3,2],[1,0],[0,4],[0,5],[4,6]], seats = 2` → 7
示例 3
`roads = [], seats = 1` → 0
提示:
1 <= n <= 10^5,roads.length == n - 11 <= seats <= 10^5
解题思路
解法一:后序 DFS 统计子树人数(主提交版本)
- 以 0 为根,每辆车从某个子树方向驶向首都时,车上应尽量坐满。
- 对每个子节点 u(相对父节点 p),子树 u 内有
cnt[u]个人需要经过边(p,u),
至少需要ceil(cnt[u] / seats)辆车,即该边贡献ceil(cnt[u] / seats)升油。 - 后序遍历累加子树人数并计算答案。
解法二:按层 BFS 反向计算(对照) - 从叶子向根逐层累加人数,每层边贡献
ceil(cnt / seats),结果一致。
复杂度: - 时间复杂度:O(n)
- 空间复杂度:O(n)
代码实现
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class Solution {
private long ans = 0;
// 解法一:后序 DFS(主提交版本)
public long minimumFuelCost(int[][] roads, int seats) {
int n = roads.length + 1;
List<Integer>[] g = new List[n];
for (int i = 0; i < n; i++) {
g[i] = new ArrayList<>();
}
for (int[] r : roads) {
g[r[0]].add(r[1]);
g[r[1]].add(r[0]);
}
ans = 0;
dfs(0, -1, g, seats);
return ans;
}
private long dfs(int u, int parent, List<Integer>[] g, int seats) {
long cnt = 1; // 本节点代表
for (int v : g[u]) {
if (v == parent) {
continue;
}
long sub = dfs(v, u, g, seats);
ans += (sub + seats - 1) / seats; // 子节点 v 这棵子树过这条边需要的车次数
cnt += sub;
}
return cnt;
}
// 解法二:按「度 1 剥离」的 BFS/拓扑式(对照)
public long minimumFuelCostTopo(int[][] roads, int seats) {
int n = roads.length + 1;
if (n == 1) {
return 0;
}
List<Integer>[] g = new List[n];
int[] deg = new int[n];
for (int i = 0; i < n; i++) {
g[i] = new ArrayList<>();
}
for (int[] r : roads) {
g[r[0]].add(r[1]);
g[r[1]].add(r[0]);
deg[r[0]]++;
deg[r[1]]++;
}
long[] cnt = new long[n];
Arrays.fill(cnt, 1);
java.util.Deque<Integer> q = new java.util.ArrayDeque<>();
for (int i = 1; i < n; i++) {
if (deg[i] == 1) {
q.offer(i);
}
}
long ans2 = 0;
boolean[] vis = new boolean[n];
vis[0] = true;
while (!q.isEmpty()) {
int u = q.poll();
if (vis[u]) {
continue;
}
vis[u] = true;
ans2 += (cnt[u] + seats - 1) / seats;
for (int v : g[u]) {
if (vis[v]) {
continue;
}
cnt[v] += cnt[u];
if (--deg[v] == 1 && v != 0) {
q.offer(v);
}
}
}
return ans2;
}
public static void main(String[] args) {
Solution sol = new Solution();
assert sol.minimumFuelCost(new int[][]{{0, 1}, {0, 2}, {0, 3}}, 5) == 3 : "2477 例1 主";
assert sol.minimumFuelCostTopo(new int[][]{{0, 1}, {0, 2}, {0, 3}}, 5) == 3 : "2477 例1 拓扑";
assert sol.minimumFuelCost(new int[][]{{3, 1}, {3, 2}, {1, 0}, {0, 4}, {0, 5}, {4, 6}}, 2) == 7 : "2477 例2 主";
assert sol.minimumFuelCostTopo(new int[][]{{3, 1}, {3, 2}, {1, 0}, {0, 4}, {0, 5}, {4, 6}}, 2) == 7 : "2477 例2 拓扑";
assert sol.minimumFuelCost(new int[][]{}, 1) == 0 : "2477 例3 主";
assert sol.minimumFuelCostTopo(new int[][]{}, 1) == 0 : "2477 例3 拓扑";
// 随机对拍
java.util.Random rnd = new java.util.Random(3);
for (int t = 0; t < 200; t++) {
int n = 1 + rnd.nextInt(20);
List<Integer> parent = new ArrayList<>();
int[][] roads = new int[Math.max(0, n - 1)][2];
for (int i = 1; i < n; i++) {
int p = rnd.nextInt(i);
parent.add(p);
roads[i - 1][0] = p;
roads[i - 1][1] = i;
}
int seats = 1 + rnd.nextInt(8);
long a = sol.minimumFuelCost(roads, seats);
long b = sol.minimumFuelCostTopo(roads, seats);
if (a != b) {
System.out.println("对拍失败 n=" + n + " roads=" + Arrays.deepToString(roads) + " seats=" + seats + " 主=" + a + " 拓扑=" + b);
System.exit(1);
}
}
System.out.println("2477 全部 PASS");
}
}
复杂度分析
- 时间复杂度:O(n)
- 空间复杂度:O(n)
2478. Number of Beautiful Partitions(完美分割的方案数)
题目描述
给你一个字符串 s,每个字符是数字 '1' 到 '9',再给你两个整数 k 和 minLength。
如果对 s 的分割满足以下条件,那么它被称为一个完美分割:
s被分成k段互不相交的子字符串- 每个子字符串长度都至少为
minLength - 每个子字符串的第一个字符都是一个质数数字,最后一个字符都是一个非质数数字。
质数数字为 '2'、'3'、'5' 和 '7',剩下的都是非质数数字。
返回s的完美分割数目,对 10^9 + 7 取余。
示例 1
`s = "23542185131", k = 3, minLength = 2` → 3
示例 2
`s = "23542185131", k = 3, minLength = 3` → 1
示例 3
`s = "3312958", k = 3, minLength = 1` → 1
提示:
1 <= k, minLength <= s.length <= 1000s每个字符都为数字 '1' 到 '9' 之一
解题思路
解法一:分割点 DP + 前缀和优化(主提交版本)
- 合法分段的边界:段首字符必须是质数数字,段尾字符必须是非质数数字。
- 定义
dp[i][j]:前 i 个字符分成 j 段的方案数(i 为前缀长度)。 - 转移:最后一段为
s[t..i-1],需s[t]为质数、s[i-1]非质数、长度 ≥ minLength:
dp[i][j] = sum(dp[t][j-1]),其中t <= i - minLength且s[t]为质数。 - 用前缀和数组
pre[t] = sum(dp[t'][j-1])(只累加 st' 为质数的位置)将内层循环均摊 O(1)。 - 注意:若第 i-1 位不是非质数,则无法作为段尾,dp 直接为 0,且此前缀和不更新。
解法二:二重循环朴素 DP(对照) - 不优化前缀和,直接枚举 t 累加,结果一致。
复杂度: - 时间复杂度:O(n * k),n = s.length() ≤ 1000
- 空间复杂度:O(n * k)
代码实现
java
public class Solution {
private static final int MOD = 1_000_000_007;
private boolean isPrime(char c) {
return c == '2' || c == '3' || c == '5' || c == '7';
}
// 解法一:前缀和优化 DP(主提交版本)
public int beautifulPartitions(String s, int k, int minLength) {
int n = s.length();
// 首尾合法性快速剪枝:首字符必须质数、尾字符必须非质数
if (!isPrime(s.charAt(0)) || isPrime(s.charAt(n - 1))) {
return 0;
}
int[][] dp = new int[n + 1][k + 1];
dp[0][0] = 1;
for (int j = 1; j <= k; j++) {
// pre[x] = sum_{t=0..x-1 且 s[t] 为质数} dp[t][j-1]
int[] pre = new int[n + 1];
for (int i = 1; i <= n; i++) {
pre[i] = pre[i - 1];
if (isPrime(s.charAt(i - 1)) && dp[i - 1][j - 1] > 0) {
pre[i] = (pre[i] + dp[i - 1][j - 1]) % MOD;
}
}
for (int i = 1; i <= n; i++) {
// 当前位置作为段尾:s[i-1] 必须非质数
if (isPrime(s.charAt(i - 1))) {
dp[i][j] = 0;
continue;
}
// 段头 t:t <= i - minLength 且 s[t] 质数,即 dp[i][j] = pre[i - minLength + 1]
int lim = i - minLength;
if (lim >= 0) {
dp[i][j] = pre[lim + 1];
} else {
dp[i][j] = 0;
}
}
}
return dp[n][k];
}
// 解法二:朴素三重循环(对照,小规模验证用)
public int beautifulPartitionsBrute(String s, int k, int minLength) {
int n = s.length();
if (!isPrime(s.charAt(0)) || isPrime(s.charAt(n - 1))) {
return 0;
}
int[][] dp = new int[n + 1][k + 1];
dp[0][0] = 1;
for (int i = 1; i <= n; i++) {
if (isPrime(s.charAt(i - 1))) {
continue; // 段尾必须非质数
}
for (int j = 1; j <= k; j++) {
for (int t = 0; t <= i - minLength; t++) {
if (t == 0 || isPrime(s.charAt(t))) {
dp[i][j] = (dp[i][j] + dp[t][j - 1]) % MOD;
}
}
}
}
return dp[n][k];
}
public static void main(String[] args) {
Solution sol = new Solution();
assert sol.beautifulPartitions("23542185131", 3, 2) == 3 : "2478 例1 主";
assert sol.beautifulPartitionsBrute("23542185131", 3, 2) == 3 : "2478 例1 暴力";
assert sol.beautifulPartitions("23542185131", 3, 3) == 1 : "2478 例2 主";
assert sol.beautifulPartitionsBrute("23542185131", 3, 3) == 1 : "2478 例2 暴力";
assert sol.beautifulPartitions("3312958", 3, 1) == 1 : "2478 例3 主";
assert sol.beautifulPartitionsBrute("3312958", 3, 1) == 1 : "2478 例3 暴力";
// 随机对拍(多组小规模)
java.util.Random rnd = new java.util.Random(17);
for (int t = 0; t < 200; t++) {
int n = 3 + rnd.nextInt(25);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; i++) {
sb.append((char) ('1' + rnd.nextInt(9)));
}
String s = sb.toString();
int k = 1 + rnd.nextInt(Math.min(5, n));
int minL = 1 + rnd.nextInt(Math.min(6, n));
int a = sol.beautifulPartitions(s, k, minL);
int b = sol.beautifulPartitionsBrute(s, k, minL);
if (a != b) {
System.out.println("对拍失败 s=" + s + " k=" + k + " minL=" + minL + " 主=" + a + " 暴力=" + b);
System.exit(1);
}
}
System.out.println("2478 全部 PASS");
}
}
复杂度分析
- 时间复杂度:O(n * k),n = s.length() ≤ 1000
- 空间复杂度:O(n * k)
2479. Maximum XOR of Two Non-Overlapping Subtrees(两个不重叠子树的最大异或值)
题目描述
有一个无向树,有 n 个节点,节点标记为从 0 到 n-1。根是节点 0。
edges[i] = [ai, bi] 表示节点 ai 和 bi 之间有一条边。
每个节点有一个相关联的值:values[i] 是第 i 个节点的值。
选择任意两个不重叠的子树,你的分数是这些子树中值的和的逐位异或。
返回能达到的最大分数;如果不可能找到两个不重叠的子树,则返回 0。
节点的子树是由该节点及其所有子节点组成的树。如果两个子树不共享任何公共节点,则它们是不重叠的。
示例 1
`n = 6, edges = [[0,1],[0,2],[1,3],[1,4],[2,5]], values = [2,8,3,6,2,5]` → 24(16 XOR 8)
示例 2
`n = 3, edges = [[0,1],[1,2]], values = [4,6,1]` → 0(链上子树互相包含,无两棵不重叠子树)
提示:
2 <= n <= 5 * 10^4values[i] <= 10^9,子树和可达 5e13
解题思路
解法一:DFS 子树和 + 增量 Trie(主提交版本)
- 先 DFS 求出每棵子树的值和
sum[u]。 - 关键观察:两棵子树不重叠 ⇔ 它们对应的 DFS 序区间完全互不相交
(一棵的区间整体在另一棵的左边或右边,绝不嵌套包含)。 - 于是按 DFS 顺序维护一个增量 Trie:
- 访问节点 u 时,Trie 中恰好包含所有「已完全处理完」(DFS 序在 u 左侧) 的子树根 v,
这些 v 与 u 必然不重叠;用sum[u]查询最大异或。 - 处理完 u 的整棵子树后再把
sum[u]插入 Trie,供后续(右侧)节点查询。 - 通过这一单向增量过程,任意合法对 (u, v) 都会在遍历到两者中较晚者时被统计到。
解法二:暴力枚举节点对(对照,仅小规模对拍) - 先算子树和,再枚举全部节点对 (u, v),用祖先后代关系判断是否互相包含(重叠),取最大异或。
复杂度: - 时间复杂度:O(n log V)(V 为值和上界,位宽取 46)
- 空间复杂度:O(n log V)
代码实现
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Deque;
import java.util.List;
public class Solution {
private static final int BITS = 46; // 2^46 > 5e13,覆盖子树和上界
private List<Integer>[] g;
private long[] sum;
private Trie trie;
private long best;
// 解法一:DFS + 增量 Trie(主提交版本)
public long maxXor(int n, int[][] edges, int[] values) {
g = new List[n];
for (int i = 0; i < n; i++) {
g[i] = new ArrayList<>();
}
for (int[] e : edges) {
g[e[0]].add(e[1]);
g[e[1]].add(e[0]);
}
sum = new long[n];
dfsSum(0, -1, values);
trie = new Trie();
best = 0;
solve(0, -1);
return best;
}
// 增量 DFS:查询时 Trie 中只含「已完成的左侧子树根」
private void solve(int u, int parent) {
// 此刻 Trie 中所有节点都与 u 不重叠(区间整体在 u 左侧)
if (u != 0) {
best = Math.max(best, trie.query(sum[u]));
}
for (int v : g[u]) {
if (v != parent) {
solve(v, u);
}
}
if (u != 0) {
trie.insert(sum[u]); // u 子树整体处理完毕,可供给右侧节点
}
}
private void dfsSum(int u, int parent, int[] values) {
long s = values[u];
for (int v : g[u]) {
if (v != parent) {
dfsSum(v, u, values);
s += sum[v];
}
}
sum[u] = s;
}
private static class Trie {
Trie zero, one;
void insert(long x) {
Trie node = this;
for (int i = BITS - 1; i >= 0; i--) {
int b = (int) ((x >> i) & 1);
if (b == 0) {
if (node.zero == null) {
node.zero = new Trie();
}
node = node.zero;
} else {
if (node.one == null) {
node.one = new Trie();
}
node = node.one;
}
}
}
long query(long x) {
Trie node = this;
if (node.zero == null && node.one == null) {
return 0;
}
long bestVal = 0;
for (int i = BITS - 1; i >= 0; i--) {
int b = (int) ((x >> i) & 1);
int want = 1 - b;
Trie nxt = want == 0 ? node.zero : node.one;
if (nxt != null) {
bestVal |= (1L << i);
node = nxt;
} else {
node = want == 0 ? node.one : node.zero;
}
}
return bestVal;
}
}
// 解法二:暴力枚举节点对(对照,仅小规模对拍)
public long maxXorBrute(int n, int[][] edges, int[] values) {
List<Integer>[] gg = new List[n];
for (int i = 0; i < n; i++) {
gg[i] = new ArrayList<>();
}
for (int[] e : edges) {
gg[e[0]].add(e[1]);
gg[e[1]].add(e[0]);
}
// 父节点 + 拓扑序
int[] parent = new int[n];
Arrays.fill(parent, -2);
parent[0] = -1;
Deque<Integer> st = new java.util.ArrayDeque<>();
st.push(0);
int[] order = new int[n];
int oi = 0;
while (!st.isEmpty()) {
int u = st.pop();
order[oi++] = u;
for (int v : gg[u]) {
if (parent[v] == -2) {
parent[v] = u;
st.push(v);
}
}
}
long[] sum2 = new long[n];
for (int i = n - 1; i >= 0; i--) {
int u = order[i];
sum2[u] = values[u];
for (int v : gg[u]) {
if (parent[v] == u) {
sum2[u] += sum2[v];
}
}
}
long bst = 0;
for (int u = 0; u < n; u++) {
for (int v = u + 1; v < n; v++) {
if (isAncestor(parent, u, v) || isAncestor(parent, v, u)) {
continue; // 一棵包含另一棵 → 重叠
}
bst = Math.max(bst, sum2[u] ^ sum2[v]);
}
}
return bst;
}
private boolean isAncestor(int[] parent, int anc, int x) {
while (x != -1) {
if (x == anc) {
return true;
}
x = parent[x];
}
return false;
}
public static void main(String[] args) {
Solution sol = new Solution();
long r1 = sol.maxXor(6, new int[][]{{0, 1}, {0, 2}, {1, 3}, {1, 4}, {2, 5}}, new int[]{2, 8, 3, 6, 2, 5});
long r1b = sol.maxXorBrute(6, new int[][]{{0, 1}, {0, 2}, {1, 3}, {1, 4}, {2, 5}}, new int[]{2, 8, 3, 6, 2, 5});
assert r1 == 24 : "2479 例1 主: " + r1;
assert r1b == 24 : "2479 例1 暴力: " + r1b;
long r2 = sol.maxXor(3, new int[][]{{0, 1}, {1, 2}}, new int[]{4, 6, 1});
long r2b = sol.maxXorBrute(3, new int[][]{{0, 1}, {1, 2}}, new int[]{4, 6, 1});
assert r2 == 0 : "2479 例2 主: " + r2;
assert r2b == 0 : "2479 例2 暴力: " + r2b;
// 随机对拍(小规模随机树)
java.util.Random rnd = new java.util.Random(29);
for (int t = 0; t < 150; t++) {
int n = 3 + rnd.nextInt(10);
int[][] edges = new int[n - 1][2];
for (int i = 1; i < n; i++) {
edges[i - 1][0] = rnd.nextInt(i);
edges[i - 1][1] = i;
}
int[] vs = new int[n];
for (int i = 0; i < n; i++) {
vs[i] = 1 + rnd.nextInt(200);
}
long a = sol.maxXor(n, edges, vs);
long b = sol.maxXorBrute(n, edges, vs);
if (a != b) {
System.out.println("对拍失败 n=" + n + " edges=" + Arrays.deepToString(edges)
+ " vals=" + Arrays.toString(vs) + " 主=" + a + " 暴力=" + b);
System.exit(1);
}
}
System.out.println("2479 全部 PASS");
}
}
复杂度分析
- 时间复杂度:O(n log V)(V 为值和上界,位宽取 46)
- 空间复杂度:O(n log V)
2480. Form a Chemical Bond(形成化学键)
题目类型:数据库(SQL)
题目描述
表: Elements
+-------------+---------+
| Column Name | Type |
+-------------+---------+
| symbol | varchar |
| type | enum |
| electrons | int |
+-------------+---------+
symbol 是该表的主键(具有唯一值的列)。该表的每一行包含一个元素的信息。
type 是 ENUM 类型,它的值是 ('Metal', 'Nonmetal', 'Noble') 之一:
- 如果
type是Noble,electrons是0。 - 如果
type是Metal,electrons是这种元素的一个原子所能给出的电子数。 - 如果
type是Nonmetal,electrons是这种元素的一个原子所需要的电子数。
如果一个元素是 'Metal',另外一个元素是 'Nonmetal',那么它们可以形成键。
编写一个解决方案找出所有可以形成键的元素对。
以任意顺序返回结果表。
示例 1:
输入:
Elements 表:
+--------+----------+-----------+
| symbol | type | electrons |
+--------+----------+-----------+
| He | Noble | 0 |
| Na | Metal | 1 |
| Ca | Metal | 2 |
| La | Metal | 3 |
| Cl | Nonmetal | 1 |
| O | Nonmetal | 2 |
| N | Nonmetal | 3 |
+--------+----------+-----------+
输出:
+-------+----------+
| metal | nonmetal |
+-------+----------+
| La | Cl |
| Ca | Cl |
| Na | Cl |
| La | O |
| Ca | O |
| Na | O |
| La | N |
| Ca | N |
| Na | N |
+-------+----------+
解释:
Metal 元素包括 La, Ca, and Na。
Nonmetal 元素包括 Cl, O, and N。
每个 Metal 元素与输出表中的 Nonmeal 元素配对。
解题思路
解法一:笛卡尔积 + 类型过滤(推荐)
- 从
Elements中筛出type = 'Metal'的元素记为m,筛出type = 'Nonmetal'的元素记为nmt。 - 两表做笛卡尔积(
CROSS JOIN),结果列别名为metal、nonmetal。由于Metal与Nonmetal是互斥的枚举值,任意金属与任意非金属都能形成键,无需再校验电子数。 - 输出顺序任意,直接
SELECT m.symbol AS metal, nmt.symbol AS nonmetal即可。
解法二(对照):自连接 + 不等条件
- 同一张表自连接:
FROM Elements e1 JOIN Elements e2 ON e1.type = 'Metal' AND e2.type = 'Nonmetal',效果与解法一一致,写法上强调「配对」语义。
代码实现
sql
-- 解法一(推荐):分类过滤 + 笛卡尔积
SELECT m.symbol AS metal, nm.symbol AS nonmetal
FROM Elements m
CROSS JOIN Elements nm
WHERE m.type = 'Metal'
AND nm.type = 'Nonmetal';
sql
-- 解法二(对照):单表自连接配对
SELECT e1.symbol AS metal, e2.symbol AS nonmetal
FROM Elements e1
JOIN Elements e2
ON e1.type = 'Metal' AND e2.type = 'Nonmetal';
复杂度分析
- 时间复杂度 :
O(M * N),M为金属元素个数、N为非金属元素个数;输出行数即是二者乘积(如示例 3 × 3 = 9 行)。 - 空间复杂度 :
O(M * N),结果集大小与配对数量成正比。