LeetCode 2861-2880. 题解合集
文章目录
- [LeetCode 2861-2880. 题解合集](#LeetCode 2861-2880. 题解合集)
-
- [2861. Maximum Number of Alloys(最大合金数)](#2861. Maximum Number of Alloys(最大合金数))
- [2862. Maximum Element-Sum of a Complete Subset of Indices(完全子集的最大元素和)](#2862. Maximum Element-Sum of a Complete Subset of Indices(完全子集的最大元素和))
- [2863. Maximum Length of Semi-Decreasing Subarrays(最长半递减子数组的长度)](#2863. Maximum Length of Semi-Decreasing Subarrays(最长半递减子数组的长度))
- [2864. Maximum Odd Binary Number(最大二进制奇数)](#2864. Maximum Odd Binary Number(最大二进制奇数))
- [2865. Beautiful Towers I(美丽塔 I)](#2865. Beautiful Towers I(美丽塔 I))
- [2866. Beautiful Towers II(美丽塔 II)](#2866. Beautiful Towers II(美丽塔 II))
- [2867. Count Valid Paths in a Tree(统计树中的合法路径数目)](#2867. Count Valid Paths in a Tree(统计树中的合法路径数目))
- [2868. The Wording Game(单词游戏)](#2868. The Wording Game(单词游戏))
- [2869. Minimum Operations to Collect Elements(收集元素的最少操作次数)](#2869. Minimum Operations to Collect Elements(收集元素的最少操作次数))
- [2870. Minimum Number of Operations to Make Array Empty(使数组为空的最少操作次数)](#2870. Minimum Number of Operations to Make Array Empty(使数组为空的最少操作次数))
- [2871. Split Array Into Maximum Number of Subarrays(将数组分割成最多数目的子数组)](#2871. Split Array Into Maximum Number of Subarrays(将数组分割成最多数目的子数组))
- [2872. Maximum Number of K-Divisible Components(可以被 K 整除连通块的最大数目)](#2872. Maximum Number of K-Divisible Components(可以被 K 整除连通块的最大数目))
- [2873. Maximum Value of an Ordered Triplet I(有序三元组中的最大值 I)](#2873. Maximum Value of an Ordered Triplet I(有序三元组中的最大值 I))
- [2874. Maximum Value of an Ordered Triplet II(有序三元组中的最大值 II)](#2874. Maximum Value of an Ordered Triplet II(有序三元组中的最大值 II))
- [2875. Minimum Size Subarray in Infinite Array(无限数组的最短子数组)](#2875. Minimum Size Subarray in Infinite Array(无限数组的最短子数组))
- [2876. Count Visited Nodes in a Directed Graph(有向图访问计数)](#2876. Count Visited Nodes in a Directed Graph(有向图访问计数))
- [2877. Create a DataFrame from List(从表中创建 DataFrame)](#2877. Create a DataFrame from List(从表中创建 DataFrame))
- [2878. Get the Size of a DataFrame(获取 DataFrame 的大小)](#2878. Get the Size of a DataFrame(获取 DataFrame 的大小))
- [2879. Display the First Three Rows(显示前三行)](#2879. Display the First Three Rows(显示前三行))
- [2880. Select Data(数据选取)](#2880. Select Data(数据选取))
2861. Maximum Number of Alloys(最大合金数)
题目描述
假设你是一家合金制造公司的老板,你的公司使用多种金属来制造合金。
现在共有 n 种不同类型的金属,并且你可以使用 k 台机器来制造合金。
每台机器都需要特定数量的每种金属来创建合金。
对于第 i 台机器,创建合金需要 composition[i][j] 份 j 类型金属。
最初你拥有 stock[x] 份 x 类型金属,而每购入一份 x 类型金属需要花费 cost[x] 金钱。
给你整数 n、k、budget,下标从 1 开始的二维数组 composition,
两个下标从 1 开始的数组 stock 和 cost,请在预算不超过 budget 的前提下,
最大化公司制造合金的数量。所有合金都需要由同一台机器制造。
返回公司可以制造的最大合金数。
示例 1
- 输入 :
n= 3,k= 2,budget= 15,composition= \[1,1,1,1,1,10],stock= 0,0,0,cost= 1,2,3 - 输出:2
示例 2
- 输入 :
n= 3,k= 2,budget= 15,composition= \[1,1,1,1,1,10],stock= 0,0,100,cost= 1,2,3 - 输出:5
示例 3
- 输入 :
n= 2,k= 3,budget= 10,composition= \[2,1,1,2,1,1],stock= 1,1,cost= 5,5 - 输出 :2
提示: 1 <= n, k <= 1000 <= budget <= 10^81 <= composition[i][j] <= 1000 <= stock[i] <= 10^81 <= cost[i] <= 100
解题思路
解法一:对答案二分
对每种机器独立二分最大合金数 x:制造 x 份的成本为
Σ max(0, compositionij*x - stockj) * costj,
若成本 <= budget 则可行。答案取各机器最大值。
二分上界:设 budget 全部买最便宜的金属,单个合金最大消耗 100 份,
上界 = budget + max(stock)(保险取 1e8 + 1e8 + 100)。
时间复杂度 O(k * n * log(2e8)),空间复杂度 O(1)。
解法二:暴力递增模拟
小规模逐份累加制造直到预算不足(对照)。
代码实现
java
import java.util.ArrayList;
import java.util.List;
public class Solution {
// ---------- 解法一:二分答案(主提交版本) ----------
public int maxNumberOfAlloys(int n, int k, int budget, List<List<Integer>> composition,
List<Integer> stock, List<Integer> cost) {
long[] s = new long[n], c = new long[n];
long maxStock = 0;
for (int i = 0; i < n; i++) { s[i] = stock.get(i); c[i] = cost.get(i); maxStock = Math.max(maxStock, s[i]); }
long hiBound = maxStock + budget + 200; // 安全上界
long best = 0;
for (int m = 0; m < k; m++) {
int[] comp = new int[n];
for (int j = 0; j < n; j++) comp[j] = composition.get(m).get(j);
long lo = 0, hi = hiBound;
while (lo <= hi) {
long mid = (lo + hi) >>> 1;
if (feasible(comp, s, c, budget, mid)) {
best = Math.max(best, mid);
lo = mid + 1;
} else {
hi = mid - 1;
}
}
}
return (int) best;
}
private boolean feasible(int[] comp, long[] s, long[] c, int budget, long x) {
long total = 0;
for (int j = 0; j < s.length; j++) {
long need = comp[j] * x - s[j];
if (need > 0) total += need * c[j];
if (total > budget) return false;
}
return total <= budget;
}
// ---------- 解法二:逐份模拟(对照,小规模) ----------
public int maxNumberOfAlloysBrute(int n, int k, int budget, List<List<Integer>> composition,
List<Integer> stock, List<Integer> cost) {
int best = 0;
for (int m = 0; m < k; m++) {
int[] comp = new int[n];
for (int j = 0; j < n; j++) comp[j] = composition.get(m).get(j);
long[] s = new long[n];
for (int j = 0; j < n; j++) s[j] = stock.get(j);
int b = budget; // 每台机器独立预算
int cnt = 0;
while (true) {
long costNow = 0;
for (int j = 0; j < n; j++) {
long need = comp[j] - s[j];
if (need > 0) costNow += need * cost.get(j);
}
if (costNow > b) break;
b -= (int) costNow;
for (int j = 0; j < n; j++) s[j] = Math.max(0, s[j] - comp[j]);
cnt++;
}
best = Math.max(best, cnt);
}
return best;
}
// ---------- 本地自测 ----------
public static void main(String[] args) {
Solution s = new Solution();
List<List<Integer>> comp1 = new ArrayList<>();
comp1.add(new ArrayList<>(java.util.Arrays.asList(1, 1, 1)));
comp1.add(new ArrayList<>(java.util.Arrays.asList(1, 1, 10)));
check(s.maxNumberOfAlloys(3, 2, 15, comp1, new ArrayList<>(java.util.Arrays.asList(0, 0, 0)), new ArrayList<>(java.util.Arrays.asList(1, 2, 3))) == 2, "2861 例1");
check(s.maxNumberOfAlloys(3, 2, 15, comp1, new ArrayList<>(java.util.Arrays.asList(0, 0, 100)), new ArrayList<>(java.util.Arrays.asList(1, 2, 3))) == 5, "2861 例2");
List<List<Integer>> comp2 = new ArrayList<>();
comp2.add(new ArrayList<>(java.util.Arrays.asList(2, 1)));
comp2.add(new ArrayList<>(java.util.Arrays.asList(1, 2)));
comp2.add(new ArrayList<>(java.util.Arrays.asList(1, 1)));
check(s.maxNumberOfAlloys(2, 3, 10, comp2, new ArrayList<>(java.util.Arrays.asList(1, 1)), new ArrayList<>(java.util.Arrays.asList(5, 5))) == 2, "2861 例3");
java.util.Random rnd = new java.util.Random(79);
boolean allOk = true;
for (int t = 0; t < 200; t++) {
int n = 1 + rnd.nextInt(3);
int k = 1 + rnd.nextInt(3);
int budget = rnd.nextInt(60);
List<List<Integer>> comp = new ArrayList<>();
List<Integer> stock = new ArrayList<>(), cost = new ArrayList<>();
for (int m = 0; m < k; m++) {
List<Integer> row = new ArrayList<>();
for (int j = 0; j < n; j++) row.add(1 + rnd.nextInt(5));
comp.add(row);
}
for (int j = 0; j < n; j++) { stock.add(rnd.nextInt(10)); cost.add(1 + rnd.nextInt(5)); }
int a = s.maxNumberOfAlloys(n, k, budget, comp, stock, cost);
int b = s.maxNumberOfAlloysBrute(n, k, budget, comp, stock, cost);
if (a != b) {
allOk = false;
System.out.println("对拍失败 n=" + n + " k=" + k + " budget=" + budget + " comp=" + comp + " stock=" + stock + " cost=" + cost + " 二分=" + a + " 模拟=" + b);
break;
}
}
check(allOk, "2861 随机对拍");
System.out.println("2861 all tests done");
}
static void check(boolean ok, String name) {
System.out.println((ok ? "PASS " : "FAIL ") + name);
}
}
复杂度分析
时间复杂度 O(k * n * log(2e8)),空间复杂度 O(1)。
2862. Maximum Element-Sum of a Complete Subset of Indices(完全子集的最大元素和)
题目描述
给你一个下标从 1 开始、由 n 个整数组成的数组。你需要从 nums 选择一个完全集,
其中每对元素下标的乘积都是一个完全平方数,例如选择 ai 和 aj,i * j 一定是完全平方数。
返回完全子集所能取到的最大元素和。
示例 1
- 输入 :
nums= 8,7,3,5,7,2,4,9 - 输出:16
示例 2
- 输入 :
nums= 8,10,3,8,1,13,7,9,4 - 输出 :20
提示: 1 <= n == nums.length <= 10^41 <= nums[i] <= 10^9
解题思路
解法一:下标去平方因子分组
i * j 是完全平方数 ⇔ i 与 j 去掉各自平方因子后的「无平方因子的数」(square-free part)相等。
对每个下标 i,求 sqfi = i 去除所有平方因子后的值(如 8 -> 2, 12 -> 3, 18 -> 2)。
用哈希表按 sqf 分组求和,取最大组和。
时间复杂度 O(n sqrt n)(或筛法优化),空间复杂度 O(n)。
解法二:暴力两两校验
小规模枚举所有下标组合验证 i*j 是否为完全平方数(对照)。
代码实现
java
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class Solution {
// ---------- 解法一:平方因子分组(主提交版本) ----------
public long maximumSum(List<Integer> nums) {
int n = nums.size();
Map<Integer, Long> group = new HashMap<>();
for (int idx = 1; idx <= n; idx++) {
int sqf = squareFree(idx);
group.merge(sqf, (long) nums.get(idx - 1), Long::sum);
}
long ans = 0;
for (long v : group.values()) ans = Math.max(ans, v);
return ans;
}
private int squareFree(int x) {
for (int d = 2; d * d <= x; d++) {
while (x % (d * d) == 0) x /= (d * d);
}
return x;
}
// ---------- 解法二:暴力(对照,小规模) ----------
public long maximumSumBrute(List<Integer> nums) {
int n = nums.size();
long ans = 0;
for (int mask = 1; mask < (1 << n); mask++) {
List<Integer> picked = new ArrayList<>();
for (int i = 0; i < n; i++) if (((mask >> i) & 1) != 0) picked.add(i + 1);
boolean ok = true;
for (int a = 0; a < picked.size() && ok; a++) {
for (int b = a + 1; b < picked.size(); b++) {
long prod = (long) picked.get(a) * picked.get(b);
long r = (long) Math.sqrt(prod);
if (r * r != prod) { ok = false; break; }
}
}
if (ok) {
long sum = 0;
for (int i : picked) sum += nums.get(i - 1);
ans = Math.max(ans, sum);
}
}
return ans;
}
// ---------- 本地自测 ----------
public static void main(String[] args) {
Solution s = new Solution();
check(s.maximumSum(new ArrayList<>(java.util.Arrays.asList(8, 7, 3, 5, 7, 2, 4, 9))) == 16, "2862 例1");
check(s.maximumSum(new ArrayList<>(java.util.Arrays.asList(8, 10, 3, 8, 1, 13, 7, 9, 4))) == 20, "2862 例2");
java.util.Random rnd = new java.util.Random(80);
boolean allOk = true;
for (int t = 0; t < 200; t++) {
int n = 1 + rnd.nextInt(8);
List<Integer> a = new ArrayList<>();
for (int i = 0; i < n; i++) a.add(1 + rnd.nextInt(50));
long x = s.maximumSum(a);
long y = s.maximumSumBrute(a);
if (x != y) {
allOk = false;
System.out.println("对拍失败 nums=" + a + " 分组=" + x + " 暴力=" + y);
break;
}
}
check(allOk, "2862 随机对拍");
System.out.println("2862 all tests done");
}
static void check(boolean ok, String name) {
System.out.println((ok ? "PASS " : "FAIL ") + name);
}
}
复杂度分析
时间复杂度 O(n sqrt n)(或筛法优化),空间复杂度 O(n)。
2863. Maximum Length of Semi-Decreasing Subarrays(最长半递减子数组的长度)
题目描述
给定一个整数数组 nums。
返回 nums 的最长半递减子数组的长度,如果没有这样的子数组则返回 0。
- 子数组是数组内的连续非空元素序列。
- 一个非空数组是半递减的,如果它的第一个元素严格大于它的最后一个元素。
示例 1
- 输入 :
nums= 7,6,5,4,3,2,1,6,10,11 - 输出: 8
示例 2
- 输入 :
nums= 57,55,50,60,61,58,63,59,64,60,63 - 输出: 6
示例 3
- 输入 :
nums= 1,2,3,4 - 输出 : 0
提示: 1 <= nums.length <= 10^5-10^9 <= nums[i] <= 10^9
解题思路
解法一:维护前缀最小值下标
一个子数组 i...j 半递减需要 numsj < numsi。
对每个 j,希望起点 i 尽量靠左且 numsi > numsj。
预处理:leftMinj = 满足 numsi <= numsj 且(i 之后)... 其实反了------
正确做法:从右往左扫,维护一个双端队列/单调栈存「比当前 numsj 更小的最左位置」。
更简单:对每个位置 j,最近一个满足 numsi > numsj 的最左 i。
线性做法:用单调栈从右往左记录「递减候选序列」,对每个 j 二分找最小 i。
另一种更简洁:预处理 preLessIdx 或使用排序。
采用:把下标按 nums 值降序排序(值相同下标靠前的优先),逐个把下标加入「可用起点」,
判断时机:对每个 j 作为终点,所有 numsi > numsj 的点都是可用起点,取最小下标即可。
具体:从大到小排序 (value, index),同时从大到小扫描 j:
当值 > numsj 的点全部已加入 TreeSet,取 TreeSet 最小值;若 < j 则长度 = j - min + 1。
时间复杂度 O(n log n),空间复杂度 O(n)。
解法二:暴力双重循环
小规模枚举所有子数组(对照)。
代码实现
java
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.TreeSet;
public class Solution {
// ---------- 解法一:排序 + TreeSet(主提交版本) ----------
public int maxSubarrayLength(int[] nums) {
int n = nums.length;
List<int[]> sorted = new ArrayList<>(); // [value, index]
for (int i = 0; i < n; i++) sorted.add(new int[]{nums[i], i});
Collections.sort(sorted, (a, b) ->
a[0] != b[0] ? Integer.compare(b[0], a[0]) : Integer.compare(a[1], b[1]));
TreeSet<Integer> starts = new TreeSet<>();
int ptr = 0; // 指向 sorted 中值最大且尚未加入的下标
int ans = 0;
for (int j = n - 1; j >= 0; j--) {
// 加入所有值 > nums[j] 的起点(严格大于)
while (ptr < sorted.size() && sorted.get(ptr)[0] > nums[j]) {
starts.add(sorted.get(ptr)[1]);
ptr++;
}
if (starts.isEmpty()) continue;
int minIdx = starts.first();
if (minIdx < j) ans = Math.max(ans, j - minIdx + 1);
}
return ans;
}
// ---------- 解法二:暴力(对照) ----------
public int maxSubarrayLengthBrute(int[] nums) {
int n = nums.length;
int ans = 0;
for (int i = 0; i < n; i++) {
for (int j = i; j < n; j++) {
if (nums[i] > nums[j]) ans = Math.max(ans, j - i + 1);
}
}
return ans;
}
// ---------- 本地自测 ----------
public static void main(String[] args) {
Solution s = new Solution();
check(s.maxSubarrayLength(new int[]{7, 6, 5, 4, 3, 2, 1, 6, 10, 11}) == 8, "2863 例1");
check(s.maxSubarrayLength(new int[]{57, 55, 50, 60, 61, 58, 63, 59, 64, 60, 63}) == 6, "2863 例2");
check(s.maxSubarrayLength(new int[]{1, 2, 3, 4}) == 0, "2863 例3");
java.util.Random rnd = new java.util.Random(81);
boolean allOk = true;
for (int t = 0; t < 300; t++) {
int n = 1 + rnd.nextInt(60);
int[] a = new int[n];
for (int i = 0; i < n; i++) a[i] = rnd.nextInt(101) - 50;
int x = s.maxSubarrayLength(a);
int y = s.maxSubarrayLengthBrute(a);
if (x != y) {
allOk = false;
System.out.println("对拍失败 nums=" + java.util.Arrays.toString(a) + " 主解=" + x + " 暴力=" + y);
break;
}
}
check(allOk, "2863 随机对拍");
System.out.println("2863 all tests done");
}
static void check(boolean ok, String name) {
System.out.println((ok ? "PASS " : "FAIL ") + name);
}
}
复杂度分析
时间复杂度 O(n log n),空间复杂度 O(n)。
2864. Maximum Odd Binary Number(最大二进制奇数)
题目描述
给你一个二进制字符串 s,其中至少包含一个 '1'。
你必须按某种方式重新排列字符串中的位,使得到的二进制数字是
可以由该组合生成的最大二进制奇数。返回该字符串。
注意:返回的结果字符串可以含前导零。
示例 1
- 输入 :
s= "010" - 输出:"001"
示例 2
- 输入 :
s= "0101" - 输出 :"1001"
提示: 1 <= s.length <= 100s仅由 '0' 和 '1' 组成s中至少包含一个 '1'
解题思路
解法一:贪心
二进制奇数要求最低位为 1。剩下的 1 尽量放高位:
结果 = (cnt1 - 1) 个 '1' + cnt0 个 '0' + 最后补一个 '1'。
时间复杂度 O(n),空间复杂度 O(n)。
解法二:全排列找最大
小规模枚举所有排列取字典序最大且为奇数的(对照)。
代码实现
java
public class Solution {
// ---------- 解法一:贪心(主提交版本) ----------
public String maximumOddBinaryNumber(String s) {
int cnt1 = 0, cnt0 = 0;
for (int i = 0; i < s.length(); i++) {
if (s.charAt(i) == '1') cnt1++;
else cnt0++;
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < cnt1 - 1; i++) sb.append('1');
for (int i = 0; i < cnt0; i++) sb.append('0');
sb.append('1');
return sb.toString();
}
// ---------- 解法二:DFS 排列(对照,小规模) ----------
public String maximumOddBinaryNumberBrute(String s) {
char[] cs = s.toCharArray();
java.util.Arrays.sort(cs);
// 从大到小生成排列,取第一个奇数的
String best = "";
return dfs(cs, new boolean[cs.length], new StringBuilder(), best);
}
private String dfs(char[] cs, boolean[] used, StringBuilder cur, String best) {
if (cur.length() == cs.length) {
String str = cur.toString();
if (str.charAt(str.length() - 1) == '1') {
if (best.isEmpty()) return str;
// 字符串长度相同,字典序大的即数值大
return str.compareTo(best) > 0 ? str : best;
}
return best;
}
// 降序选择,保证先构造出最大排列
for (int i = cs.length - 1; i >= 0; i--) {
if (used[i]) continue;
if (i > 0 && cs[i] == cs[i - 1] && !used[i - 1]) continue; // 去重
used[i] = true;
cur.append(cs[i]);
best = dfs(cs, used, cur, best);
cur.deleteCharAt(cur.length() - 1);
used[i] = false;
}
return best;
}
// ---------- 本地自测 ----------
public static void main(String[] args) {
Solution s = new Solution();
check(s.maximumOddBinaryNumber("010").equals("001"), "2864 例1");
check(s.maximumOddBinaryNumber("0101").equals("1001"), "2864 例2");
java.util.Random rnd = new java.util.Random(82);
boolean allOk = true;
for (int t = 0; t < 300; t++) {
int n = 2 + rnd.nextInt(6);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; i++) sb.append(rnd.nextBoolean() ? '1' : '0');
if (sb.indexOf("1") < 0) sb.setCharAt(0, '1');
String sstr = sb.toString();
String a = s.maximumOddBinaryNumber(sstr);
String b = s.maximumOddBinaryNumberBrute(sstr);
if (!a.equals(b)) {
allOk = false;
System.out.println("对拍失败 s=" + sstr + " 贪心=" + a + " DFS=" + b);
break;
}
}
check(allOk, "2864 随机对拍");
System.out.println("2864 all tests done");
}
static void check(boolean ok, String name) {
System.out.println((ok ? "PASS " : "FAIL ") + name);
}
}
复杂度分析
时间复杂度 O(n),空间复杂度 O(n)。
2865. Beautiful Towers I(美丽塔 I)
题目描述
给定一个包含 n 个整数的数组 heights 表示 n 座连续的塔中砖块的数量。
你的任务是移除一些砖块来形成一个山脉状的塔排列。在这种布置中,
塔高度先是非递减,有一个或多个连续塔达到最大峰值,然后非递增排列。
返回满足山脉状塔排列的方案中,高度和的最大值。
示例 1
- 输入 :
maxHeights= 5,3,4,1,1 - 输出:13
示例 2
- 输入 :
maxHeights= 6,5,3,9,2,7 - 输出:22
示例 3
- 输入 :
maxHeights= 3,2,5,5,2,3 - 输出 :18
提示: 1 <= n == heights.length <= 10^31 <= heights[i] <= 10^9
解题思路
解法一:枚举峰值 + 单调栈前缀后缀
对每个位置 i 作为峰值,左半段 non-decreasing ≤ maxHeightsi,
左段最大和 = 单调栈维护,「以 i 结尾的上升段高度和」。
右半段同理。答案 = max(lefti + righti - maxHeightsi)。
由于 n <= 10^3 也可 O(n^2) 直接扩展,但这里统一用 O(n) 单调栈(同 II)。
时间复杂度 O(n),空间复杂度 O(n)。
解法二:暴力扩展
对每个峰值向两侧扩展并取 min(对照)。
代码实现
java
import java.util.ArrayList;
import java.util.List;
public class Solution {
// ---------- 解法一:单调栈 DP(主提交版本) ----------
public long maximumSumOfHeights(List<Integer> maxHeights) {
int n = maxHeights.size();
long[] left = new long[n];
long[] right = new long[n];
// 左:以 i 为峰值的左半段(含 i)和
int[] stack = new int[n];
int top = -1;
long[] lsum = new long[n]; // lsum[i] = 以 i 结尾的非递减段和
for (int i = 0; i < n; i++) {
long h = maxHeights.get(i);
while (top >= 0 && maxHeights.get(stack[top]) >= h) top--;
if (top < 0) {
lsum[i] = h * (i + 1);
} else {
int p = stack[top];
lsum[i] = lsum[p] + h * (i - p);
}
stack[++top] = i;
left[i] = lsum[i];
}
// 右:镜像
top = -1;
long[] rsum = new long[n];
for (int i = n - 1; i >= 0; i--) {
long h = maxHeights.get(i);
while (top >= 0 && maxHeights.get(stack[top]) >= h) top--;
if (top < 0) {
rsum[i] = h * (n - i);
} else {
int p = stack[top];
rsum[i] = rsum[p] + h * (p - i);
}
stack[++top] = i;
right[i] = rsum[i];
}
long ans = 0;
for (int i = 0; i < n; i++) {
ans = Math.max(ans, left[i] + right[i] - maxHeights.get(i));
}
return ans;
}
// ---------- 解法二:暴力扩展(对照) ----------
public long maximumSumOfHeightsBrute(List<Integer> maxHeights) {
int n = maxHeights.size();
long ans = 0;
for (int peak = 0; peak < n; peak++) {
long sum = 0;
long cur = maxHeights.get(peak);
// 向左:逐级取 min(必须保证非递减)
long limit = cur;
for (int i = peak; i >= 0; i--) {
limit = Math.min(limit, maxHeights.get(i));
sum += limit;
}
limit = cur;
for (int i = peak; i < n; i++) {
limit = Math.min(limit, maxHeights.get(i));
sum += limit;
}
sum -= cur; // 峰值算了两次
ans = Math.max(ans, sum);
}
return ans;
}
// ---------- 本地自测 ----------
public static void main(String[] args) {
Solution s = new Solution();
check(s.maximumSumOfHeights(new ArrayList<>(java.util.Arrays.asList(5, 3, 4, 1, 1))) == 13, "2865 例1");
check(s.maximumSumOfHeights(new ArrayList<>(java.util.Arrays.asList(6, 5, 3, 9, 2, 7))) == 22, "2865 例2");
check(s.maximumSumOfHeights(new ArrayList<>(java.util.Arrays.asList(3, 2, 5, 5, 2, 3))) == 18, "2865 例3");
java.util.Random rnd = new java.util.Random(83);
boolean allOk = true;
for (int t = 0; t < 300; t++) {
int n = 1 + rnd.nextInt(50);
List<Integer> a = new ArrayList<>();
for (int i = 0; i < n; i++) a.add(1 + rnd.nextInt(30));
long x = s.maximumSumOfHeights(a);
long y = s.maximumSumOfHeightsBrute(a);
if (x != y) {
allOk = false;
System.out.println("对拍失败 maxHeights=" + a + " 单调栈=" + x + " 暴力=" + y);
break;
}
}
check(allOk, "2865 随机对拍");
System.out.println("2865 all tests done");
}
static void check(boolean ok, String name) {
System.out.println((ok ? "PASS " : "FAIL ") + name);
}
}
复杂度分析
时间复杂度 O(n),空间复杂度 O(n)。
2866. Beautiful Towers II(美丽塔 II)
题目描述
给你一个长度为 n 下标从 0 开始的整数数组 maxHeights。
你的任务是在坐标轴上建 n 座塔。第 i 座塔的下标为 i,高度为 heights[i]。
如果满足以下条件,我们称这些塔是美丽的:
1 <= heights[i] <= maxHeights[i]heights是一个山脉数组。
如果存在下标i满足:对于所有 0 <j<=i有heights[j-1]<=heights[j],
且对于所有i<=k<n-1 有heights[k+1]<=heights[k],那么heights是山脉数组。
返回满足美丽塔要求的方案中,高度和的最大值。
示例 1
- 输入 :
maxHeights= 5,3,4,1,1 - 输出:13
示例 2
- 输入 :
maxHeights= 6,5,3,9,2,7 - 输出:22
示例 3
- 输入 :
maxHeights= 3,2,5,5,2,3 - 输出 :18
提示: 1 <= n == maxHeights.length <= 10^51 <= maxHeights[i] <= 10^9
解题思路
解法一:单调栈前缀/后缀 DP
lefti = 保证 0...i 非递减时,以 i 为峰顶的左半段最大和;
righti = 保证 i...n-1 非递增时,以 i 为峰顶的右半段最大和。
用单调栈存储"前一个更矮的塔"位置 p:
lefti = leftp + maxHeightsi * (i - p)(p 之前高度不超过 hi 已由 p 封顶)。
同理 right。答案 = max(lefti + righti - maxHeightsi)。
时间复杂度 O(n),空间复杂度 O(n)。
解法二:暴力扩展
小规模对每个峰值向两侧扩展取 min(对照)。
代码实现
java
import java.util.ArrayList;
import java.util.List;
public class Solution {
// ---------- 解法一:单调栈 DP(主提交版本) ----------
public long maximumSumOfHeights(List<Integer> maxHeights) {
int n = maxHeights.size();
long[] left = new long[n];
long[] right = new long[n];
int[] st = new int[n];
int top = -1;
for (int i = 0; i < n; i++) {
long h = maxHeights.get(i);
while (top >= 0 && maxHeights.get(st[top]) >= h) top--;
if (top < 0) {
left[i] = h * (i + 1);
} else {
int p = st[top];
left[i] = left[p] + h * (i - p);
}
st[++top] = i;
}
top = -1;
for (int i = n - 1; i >= 0; i--) {
long h = maxHeights.get(i);
while (top >= 0 && maxHeights.get(st[top]) >= h) top--;
if (top < 0) {
right[i] = h * (n - i);
} else {
int p = st[top];
right[i] = right[p] + h * (p - i);
}
st[++top] = i;
}
long ans = 0;
for (int i = 0; i < n; i++) {
ans = Math.max(ans, left[i] + right[i] - maxHeights.get(i));
}
return ans;
}
// ---------- 解法二:暴力扩展(对照) ----------
public long maximumSumOfHeightsBrute(List<Integer> maxHeights) {
int n = maxHeights.size();
long ans = 0;
for (int peak = 0; peak < n; peak++) {
long sum = 0;
long limit = maxHeights.get(peak);
for (int i = peak; i >= 0; i--) {
limit = Math.min(limit, maxHeights.get(i));
sum += limit;
}
limit = maxHeights.get(peak);
for (int i = peak; i < n; i++) {
limit = Math.min(limit, maxHeights.get(i));
sum += limit;
}
sum -= maxHeights.get(peak);
ans = Math.max(ans, sum);
}
return ans;
}
// ---------- 本地自测 ----------
public static void main(String[] args) {
Solution s = new Solution();
check(s.maximumSumOfHeights(new ArrayList<>(java.util.Arrays.asList(5, 3, 4, 1, 1))) == 13, "2866 例1");
check(s.maximumSumOfHeights(new ArrayList<>(java.util.Arrays.asList(6, 5, 3, 9, 2, 7))) == 22, "2866 例2");
check(s.maximumSumOfHeights(new ArrayList<>(java.util.Arrays.asList(3, 2, 5, 5, 2, 3))) == 18, "2866 例3");
java.util.Random rnd = new java.util.Random(84);
boolean allOk = true;
for (int t = 0; t < 300; t++) {
int n = 1 + rnd.nextInt(50);
List<Integer> a = new ArrayList<>();
for (int i = 0; i < n; i++) a.add(1 + rnd.nextInt(30));
long x = s.maximumSumOfHeights(a);
long y = s.maximumSumOfHeightsBrute(a);
if (x != y) {
allOk = false;
System.out.println("对拍失败 maxHeights=" + a + " 单调栈=" + x + " 暴力=" + y);
break;
}
}
check(allOk, "2866 随机对拍");
System.out.println("2866 all tests done");
}
static void check(boolean ok, String name) {
System.out.println((ok ? "PASS " : "FAIL ") + name);
}
}
复杂度分析
时间复杂度 O(n),空间复杂度 O(n)。
2867. Count Valid Paths in a Tree(统计树中的合法路径数目)
题目描述
给你一棵 n 个节点的无向树,节点编号为 1 到 n。
给你整数 n 和长度为 n - 1 的二维整数数组 edges,其中 edges[i] = `ui`, `vi`。
返回树中的合法路径数目。
如果在节点 a 到节点 b 之间恰好有一个节点的编号是质数,那么我们称路径 (a, b) 是合法的。
注意:路径 (a, b) 和路径 (b, a) 视为同一条路径,只计一次。
示例 1
- 输入 :
n= 5,edges= \[1,2,1,3,2,4,2,5] - 输出:4
示例 2
- 输入 :
n= 6,edges= \[1,2,1,3,2,4,3,5,3,6] - 输出 :6
提示: 1 <= n <= 10^5edges.length == n - 11 <= ui, vi <= n- 输入保证
edges形成一棵合法的树。
解题思路
解法一:以质数节点为枢纽计数
把每条合法路径看成「恰好经过一个质数节点」。枚举每个质数节点 p 作为枢纽,
去掉 p 后,p 的每个邻接分量都是「全非质数连通块」。
若各分量大小为 s1, s2, ..., sm:
- 一端在分量内、一端是 p:贡献 s1 + s2 + ... = (总非质数节点数)
- 两个端点分别在两个不同分量:贡献 Σ_{i<j} si*sj。
总和 = a + b,其中 a = Σsi,b = (Σsi * Σsi - Σsi^2) / 2。
预处理每个非质数连通块大小(BFS/DFS 跳过质数节点),
复杂度 O(n)。
解法二:暴力枚举所有点对路径
小规模对每个点对 BFS 路径数质数个数(对照)。
复杂度: - 时间复杂度 :
O(n^2),BFS/DFS 遍历连通块两两计数 - 空间复杂度 :
O(n),连通块大小数组
代码实现
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class Solution {
// ---------- 解法一:质数枢纽计数(主提交版本) ----------
public long countPaths(int n, int[][] edges) {
boolean[] prime = new boolean[n + 1];
Arrays.fill(prime, true);
prime[0] = prime[1] = false;
for (int i = 2; i * i <= n; i++) {
if (prime[i]) {
for (int j = i * i; j <= n; j += i) prime[j] = false;
}
}
List<Integer>[] g = new List[n + 1];
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]);
}
long ans = 0;
for (int p = 2; p <= n; p++) {
if (!prime[p]) continue;
long sum = 0, sumSq = 0;
for (int nei : g[p]) {
if (prime[nei]) continue; // 质数邻接属于其它枢纽,只统计非质数块
long s = dfsBlock(nei, p, g, prime);
sum += s;
sumSq += s * s;
}
// 一端是 p:sum 条;两端都在非质数块:(sum^2 - sumSq)/2
ans += sum + (sum * sum - sumSq) / 2;
}
return ans;
}
// 从 v 出发(跳过父节点 p、跳过质数节点)统计非质数连通块大小
private long dfsBlock(int v, int parent, List<Integer>[] g, boolean[] prime) {
long cnt = 1;
for (int to : g[v]) {
if (to == parent || prime[to]) continue;
cnt += dfsBlock(to, v, g, prime);
}
return cnt;
}
// ---------- 解法二:暴力点对(对照,小规模) ----------
public long countPathsBrute(int n, int[][] edges) {
boolean[] prime = new boolean[n + 1];
Arrays.fill(prime, true);
prime[0] = prime[1] = false;
for (int i = 2; i * i <= n; i++) {
if (prime[i]) for (int j = i * i; j <= n; j += i) prime[j] = false;
}
List<Integer>[] g = new List[n + 1];
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]);
}
long ans = 0;
for (int a = 1; a <= n; a++) {
for (int b = a + 1; b <= n; b++) {
// BFS 找路径并统计质数个数
int[] prev = new int[n + 1];
Arrays.fill(prev, -1);
java.util.ArrayDeque<Integer> q = new java.util.ArrayDeque<>();
q.add(a);
prev[a] = 0;
while (!q.isEmpty()) {
int x = q.poll();
if (x == b) break;
for (int to : g[x]) {
if (prev[to] == -1) { prev[to] = x; q.add(to); }
}
}
int primeCnt = 0;
int cur = b;
while (cur != 0) {
if (prime[cur]) primeCnt++;
cur = prev[cur];
}
if (primeCnt == 1) ans++;
}
}
return ans;
}
// ---------- 本地自测 ----------
public static void main(String[] args) {
Solution s = new Solution();
check(s.countPaths(5, new int[][]{{1,2},{1,3},{2,4},{2,5}}) == 4, "2867 例1");
check(s.countPaths(6, new int[][]{{1,2},{1,3},{2,4},{3,5},{3,6}}) == 6, "2867 例2");
java.util.Random rnd = new java.util.Random(85);
boolean allOk = true;
for (int t = 0; t < 100; t++) {
int n = 2 + rnd.nextInt(9);
List<int[]> es = new ArrayList<>();
for (int v = 2; v <= n; v++) {
int p = 1 + rnd.nextInt(v - 1);
es.add(new int[]{p, v});
}
int[][] edges = es.toArray(new int[0][]);
long a = s.countPaths(n, edges);
long b = s.countPathsBrute(n, edges);
if (a != b) {
allOk = false;
System.out.println("对拍失败 n=" + n + " edges=" + Arrays.deepToString(edges) + " 主解=" + a + " 暴力=" + b);
break;
}
}
check(allOk, "2867 随机对拍");
System.out.println("2867 all tests done");
}
static void check(boolean ok, String name) {
System.out.println((ok ? "PASS " : "FAIL ") + name);
}
}
复杂度分析
- 时间复杂度 :
O(n^2),BFS/DFS 遍历连通块两两计数 - 空间复杂度 :
O(n),连通块大小数组
2868. The Wording Game(单词游戏)
题目描述
Alice 和 Bob 分别拥有一个按字典序排序的字符串数组,分别命名为 a 和 b。
他们正在玩一个单词游戏,遵循以下规则:
- 每一轮,当前玩家应该从他的列表中选择一个单词,并且选择的单词比上一个单词紧邻大;
然后轮到另一名玩家。 - 如果一名玩家在自己的回合中无法选择单词,则输掉比赛。
Alice通过选择在字典序最小的单词开始游戏。
给定a和b,已知两名玩家都按最佳策略玩游戏,如果Alice可以获胜则返回true,否则false。
单词w比z紧邻大:w字典序大于z,且w的首字母等于z的首字母或为z首字母后相邻的字母。
示例 1
- 输入 :
a= "`avokado`","`dabar`",b= "`brazil`" - 输出 :
false
示例 2
- 输入 :
a= "`ananas`","`atlas`","`banana`",b= "`albatros`","`cikla`","`nogomet`" - 输出 :
true
示例 3
- 输入 :
a= "`hrvatska`","`zastava`",b= "`bijeli`","`galeb`" - 输出 :
true
提示: 1 <= a.length, b.length <= 10^5a[i]和b[i]仅包含小写英文字母。a和b按字典序排序。a和b中所有的单词都是不同的。- 所有单词长度之和不超过 10^6。
解题思路
解法一:贪心模拟
Alice 首轮必须选自己字典序最小的词。之后每轮当前玩家在自己未被用过的词中,
选「字典序最小且能紧邻大接上」的词;若选不出则输。
因为数组有序且每个词只用一次,维护两个指针从前往后扫;当前玩家在自己列表中
线性找第一个满足条件的(其后若已用过用下标跳过)。
更严谨:用一个「已用尾标」------由于每轮都尽量选最小的可用词,两个玩家的
已用词必然是各自数组的某个前缀,所以只需维护 aUsed/bUsed 前缀计数。
时间复杂度 O(a.length + b.length),空间复杂度 O(1)。
解法二:回溯搜索
小规模时递归枚举所有走法,验证 Alice 是否有必胜策略(对照)。
代码实现
java
import java.util.HashMap;
import java.util.Map;
public class Solution {
// ---------- 解法一:贪心模拟(主提交版本) ----------
public boolean canAliceWin(String[] a, String[] b) {
// Alice 先手:必须选自己最小的词
String last = a[0];
int aUsed = 1, bUsed = 0;
boolean aliceTurn = false; // 下一轮 Bob
while (true) {
boolean moved = false;
if (aliceTurn) {
for (int i = aUsed; i < a.length; i++) {
if (adjacentLarger(a[i], last)) {
last = a[i];
aUsed = i + 1;
moved = true;
break;
}
}
} else {
for (int i = bUsed; i < b.length; i++) {
if (adjacentLarger(b[i], last)) {
last = b[i];
bUsed = i + 1;
moved = true;
break;
}
}
}
if (!moved) return !aliceTurn; // 当前轮玩家无法出牌 => 对方(下一步的上一轮)赢
aliceTurn = !aliceTurn;
}
}
// w 是否比 z 紧邻大
private boolean adjacentLarger(String w, String z) {
if (w.compareTo(z) <= 0) return false;
char wc = w.charAt(0), zc = z.charAt(0);
return wc == zc || wc == zc + 1;
}
// ---------- 解法二:回溯搜索(对照,小规模) ----------
public boolean canAliceWinBrute(String[] a, String[] b) {
// Alice 第一手强制选自己最小的词
int[] mask = {0, 0};
mask[0] |= 1 << 0;
// 第一个 dfs 是"轮到 Bob 走",返回 Bob 能否赢;Bob 不能赢则 Alice 赢
return !dfsBrute(a, b, a[0], mask, false, new java.util.HashMap<>());
}
// cur 为上一单词,turn=true 表示轮到 Alice
private boolean dfsBrute(String[] a, String[] b, String cur, int[] mask, boolean turn,
java.util.Map<String, Boolean> memo) {
String key = cur + "|" + mask[0] + "|" + mask[1] + "|" + turn;
Boolean cached = memo.get(key);
if (cached != null) return cached;
String[] arr = turn ? a : b;
boolean canMove = false;
for (int i = 0; i < arr.length; i++) {
if (((mask[turn ? 0 : 1] >> i) & 1) != 0) continue;
if (adjacentLarger(arr[i], cur)) {
canMove = true;
mask[turn ? 0 : 1] |= 1 << i;
boolean oppWin = dfsBrute(a, b, arr[i], mask, !turn, memo);
mask[turn ? 0 : 1] &= ~(1 << i);
if (!oppWin) {
memo.put(key, true);
return true;
}
}
}
// 无法出牌 => 当前玩家输
memo.put(key, false);
return false;
}
// ---------- 本地自测 ----------
public static void main(String[] args) {
Solution s = new Solution();
check(!s.canAliceWin(new String[]{"avokado", "dabar"}, new String[]{"brazil"}), "2868 例1");
check(s.canAliceWin(new String[]{"ananas", "atlas", "banana"}, new String[]{"albatros", "cikla", "nogomet"}), "2868 例2");
check(s.canAliceWin(new String[]{"hrvatska", "zastava"}, new String[]{"bijeli", "galeb"}), "2868 例3");
// 用对拍:生成小规模随机按字典序排序的词表
java.util.Random rnd = new java.util.Random(86);
boolean allOk = true;
for (int t = 0; t < 200; t++) {
int na = 1 + rnd.nextInt(4), nb = 1 + rnd.nextInt(4);
java.util.TreeSet<String> aa = new java.util.TreeSet<>();
java.util.TreeSet<String> bb = new java.util.TreeSet<>();
while (aa.size() < na) aa.add(randWord(rnd));
while (bb.size() < nb) bb.add(randWord(rnd));
String[] a = aa.toArray(new String[0]);
String[] b = bb.toArray(new String[0]);
boolean m1 = s.canAliceWin(a, b);
boolean m2 = s.canAliceWinBrute(a, b);
if (m1 != m2) {
allOk = false;
System.out.println("对拍失败 a=" + java.util.Arrays.toString(a) + " b=" + java.util.Arrays.toString(b) + " 贪心=" + m1 + " 搜索=" + m2);
break;
}
}
check(allOk, "2868 随机对拍");
System.out.println("2868 all tests done");
}
static String randWord(java.util.Random rnd) {
int len = 1 + rnd.nextInt(4);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < len; i++) sb.append((char) ('a' + rnd.nextInt(5)));
return sb.toString();
}
static void check(boolean ok, String name) {
System.out.println((ok ? "PASS " : "FAIL ") + name);
}
}
复杂度分析
时间复杂度 O(a.length + b.length),空间复杂度 O(1)。
2869. Minimum Operations to Collect Elements(收集元素的最少操作次数)
题目描述
给你一个正整数数组 nums 和一个整数 k。
一次操作中,你可以将数组的最后一个元素删除,将该元素添加到一个集合中。
返回收集元素 1, 2, ..., k 需要的最少操作次数。
示例 1
- 输入 :
nums= 3,1,5,4,2,k= 2 - 输出:4
示例 2
- 输入 :
nums= 3,1,5,4,2,k= 5 - 输出:5
示例 3
- 输入 :
nums= 3,2,5,3,1,k= 3 - 输出 :4
提示: 1 <= nums.length <= 501 <= nums[i] <= nums.length1 <= k <= nums.length- 输入保证你可以收集到元素 1, 2, ...,
k。
解题思路
解法一:从后向前扫描
从数组末尾开始往左扫,遇到值 <= k 且未收集过的元素就收集,
直到 1...k 全部收集,返回扫描过的元素个数(操作次数)。
时间复杂度 O(n),空间复杂度 O(k)。
解法二:模拟
直接模拟每次删除末尾(对照)。
代码实现
java
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class Solution {
// ---------- 解法一:逆序扫描(主提交版本) ----------
public int minOperations(List<Integer> nums, int k) {
Set<Integer> collected = new HashSet<>();
int n = nums.size();
for (int i = n - 1; i >= 0; i--) {
int v = nums.get(i);
if (v <= k) collected.add(v);
if (collected.size() == k) return n - i;
}
return n; // 理论不会到这
}
// ---------- 解法二:List 模拟(对照) ----------
public int minOperationsBrute(List<Integer> nums, int k) {
List<Integer> a = new ArrayList<>(nums);
Set<Integer> collected = new HashSet<>();
int ops = 0;
while (collected.size() < k) {
int v = a.remove(a.size() - 1);
if (v <= k) collected.add(v);
ops++;
}
return ops;
}
// ---------- 本地自测 ----------
public static void main(String[] args) {
Solution s = new Solution();
check(s.minOperations(new ArrayList<>(java.util.Arrays.asList(3, 1, 5, 4, 2)), 2) == 4, "2869 例1");
check(s.minOperations(new ArrayList<>(java.util.Arrays.asList(3, 1, 5, 4, 2)), 5) == 5, "2869 例2");
check(s.minOperations(new ArrayList<>(java.util.Arrays.asList(3, 2, 5, 3, 1)), 3) == 4, "2869 例3");
java.util.Random rnd = new java.util.Random(87);
boolean allOk = true;
for (int t = 0; t < 500; t++) {
int n = 1 + rnd.nextInt(20);
int k = 1 + rnd.nextInt(Math.min(5, n) + 1);
List<Integer> a = new ArrayList<>();
// 先放入 1..k 保证可收集,再补足到 n
for (int x = 1; x <= k; x++) a.add(x);
while (a.size() < n) a.add(1 + rnd.nextInt(n));
int m1 = s.minOperations(new ArrayList<>(a), k);
int m2 = s.minOperationsBrute(new ArrayList<>(a), k);
if (m1 != m2) {
allOk = false;
System.out.println("对拍失败 nums=" + a + " k=" + k + " 主解=" + m1 + " 模拟=" + m2);
break;
}
}
check(allOk, "2869 随机对拍");
System.out.println("2869 all tests done");
}
static void check(boolean ok, String name) {
System.out.println((ok ? "PASS " : "FAIL ") + name);
}
}
复杂度分析
时间复杂度 O(n),空间复杂度 O(k)。
2870. Minimum Number of Operations to Make Array Empty(使数组为空的最少操作次数)
题目描述
给你一个下标从 0 开始的正整数数组 nums。
你可以对数组执行以下两种操作任意次:
- 从数组中选择两个值相等的元素,并将它们从数组中删除。
- 从数组中选择三个值相等的元素,并将它们从数组中删除。
返回使数组为空的最少操作次数,如果无法达成请返回 -1。
示例 1
- 输入 :
nums= 2,3,3,2,2,4,2,3,4 - 输出:4
示例 2
- 输入 :
nums= 2,1,2,2,3,3 - 输出 :-1
提示: 2 <= nums.length <= 10^51 <= nums[i] <= 10^6
解题思路
解法一:频次贪心
对每个值统计出现次数 c。每次操作删除 2 个或 3 个同值元素。
若 c == 1 无法清空;c >= 2 时最少次数 = (c + 2) / 3 的变形:
优先用 3,余数 1 时借一个 3 拆成 2+2(c=4 -> 两次 2),余数 2 时补一次 2。
公式:ans = (c / 3) + (c % 3 == 0 ? 0 : 1),但 c=1 时 -1,c=4 特判为 2。
更统一:dp 或直接:
if c == 1 -> -1
else ans += (c + 2) / 3 // ceil(c/3) 对 c>=2 均成立?c=2 -> 1, c=3 -> 1, c=4 -> 2, c=5 -> 2, c=6 -> 2, c=7 -> 3
验证:2=1✓ 3=1✓ 4=2✓ 5=2✓ 6=2✓ 7=3✓ 8=3✓(8=3+3+2)
所以 ans = ceil(c/3) = (c+2)/3。
时间复杂度 O(n),空间复杂度 O(n)。
解法二:BFS/DP 小规模验证
对每个 c(小范围)用 DP 求最少操作数(对照)。
代码实现
java
import java.util.Arrays;
public class Solution {
// ---------- 解法一:频次 + 贪心(主提交版本) ----------
public int minOperations(int[] nums) {
Arrays.sort(nums);
int n = nums.length;
int ans = 0;
for (int i = 0; i < n; ) {
int j = i;
while (j < n && nums[j] == nums[i]) j++;
int c = j - i;
if (c == 1) return -1;
ans += (c + 2) / 3;
i = j;
}
return ans;
}
// ---------- 解法二:DP 求最少操作(对照) ----------
public int minOperationsBrute(int[] nums) {
java.util.Map<Integer, Integer> cnt = new java.util.HashMap<>();
for (int v : nums) cnt.merge(v, 1, Integer::sum);
int ans = 0;
for (int c : cnt.values()) {
int dp = dpMin(c);
if (dp < 0) return -1;
ans += dp;
}
return ans;
}
// 对频次 c 求最少操作(2 或 3),记忆化
private int dpMin(int c) {
int[] dp = new int[c + 1];
java.util.Arrays.fill(dp, Integer.MAX_VALUE / 2);
dp[0] = 0;
for (int i = 2; i <= c; i++) {
if (i >= 2) dp[i] = Math.min(dp[i], dp[i - 2] + 1);
if (i >= 3) dp[i] = Math.min(dp[i], dp[i - 3] + 1);
}
return dp[c] >= Integer.MAX_VALUE / 4 ? -1 : dp[c];
}
// ---------- 本地自测 ----------
public static void main(String[] args) {
Solution s = new Solution();
check(s.minOperations(new int[]{2, 3, 3, 2, 2, 4, 2, 3, 4}) == 4, "2870 例1");
check(s.minOperations(new int[]{2, 1, 2, 2, 3, 3}) == -1, "2870 例2");
java.util.Random rnd = new java.util.Random(88);
boolean allOk = true;
for (int t = 0; t < 500; t++) {
int n = 2 + rnd.nextInt(12);
int[] a = new int[n];
for (int i = 0; i < n; i++) a[i] = 1 + rnd.nextInt(6);
int m1 = s.minOperations(a.clone());
int m2 = s.minOperationsBrute(a.clone());
if (m1 != m2) {
allOk = false;
System.out.println("对拍失败 nums=" + Arrays.toString(a) + " 贪心=" + m1 + " DP=" + m2);
break;
}
}
check(allOk, "2870 随机对拍");
System.out.println("2870 all tests done");
}
static void check(boolean ok, String name) {
System.out.println((ok ? "PASS " : "FAIL ") + name);
}
}
复杂度分析
时间复杂度 O(n),空间复杂度 O(n)。
2871. Split Array Into Maximum Number of Subarrays(将数组分割成最多数目的子数组)
题目描述
给你一个只包含非负整数的数组 nums。
定义子数组 nums`l`...`r`(l <= r)的分数为 nums[l] AND ... AND nums[r]。
请你将数组分割成一个或者更多子数组,满足:
- 每个元素都只属于一个子数组。
- 子数组分数之和尽可能小。
在满足以上要求的条件下,返回最多可以得到多少个子数组。
示例 1
- 输入 :
nums= 1,0,2,0,1,2 - 输出:3
示例 2
- 输入 :
nums= 5,7,1,3 - 输出 :1
提示: 1 <= nums.length <= 10^50 <= nums[i] <= 10^6
解题思路
解法一:贪心
记 totalAnd = nums0 & ... & numsn-1。
- 若 totalAnd == 0:可以切成 numZeros 个单位块,使每块 AND 为 0,
答案 = 值为 0 的元素个数(被 0 分割);更精确:每个 0 单独一块,
非 0 元素并入相邻块或不并。官方结论:答案为 0 的个数。
理由:每块分数 >= 0(AND 非负),总分最小为 0;每块为 0 则需要块内含一个 0
或整段 AND 为 0。把每个 0 单独成块(0 的个数 = cnt0),多余的 1 并入相邻块
不增加块数且保持 AND=0(非 0 元素与 0 AND 为 0)。可能 cnt0=0 但全数组 AND=0?
此时 arr 无 0 但 AND 为 0,至少需要 1 块;最多能切多少?只要整块 AND=0
切成两块各 AND=0 需要每个子块 AND=0...... 不一定。官方解法:
若整个数组 AND 不为 0,答案为 1;否则答案为 0 的个数(至少 1)。 - 若 totalAnd != 0:无法分成多块使总和 < totalAnd(每块 AND >= totalAnd ?不一定),
官方结论:答案为 1。
实现:统计 0 的个数 cnt0,全数组 AND 为 totalAnd:
if totalAnd != 0 return 1;
否则 return max(1, cnt0)。
时间复杂度 O(n),空间复杂度 O(1)。
解法二:暴力枚举全部分割
小规模枚举所有分割方式计算最小总分下的最大块数(对照)。
代码实现
java
import java.util.ArrayList;
import java.util.List;
public class Solution {
// ---------- 解法一:贪心(主提交版本) ----------
public int maxSubarrays(int[] nums) {
int totalAnd = -1; // 全 1 掩码
for (int v : nums) totalAnd &= v;
if (totalAnd != 0) return 1;
// 整数组 AND 为 0:贪心从左到右,累积 AND 一旦为 0 就切一块
// 若最后一段累积 AND 不为 0,则并入前一块(不影响其 AND=0)
int ans = 0;
int cur = -1;
for (int v : nums) {
cur &= v;
if (cur == 0) {
ans++;
cur = -1;
}
}
return ans;
}
// ---------- 解法二:暴力分割(对照,小规模) ----------
public int maxSubarraysBrute(int[] nums) {
int n = nums.length;
int minScore = Integer.MAX_VALUE;
// 先求整段 AND 作为最小总和下界参考,正解:总分数最小 = 每块 AND 之和
// 枚举分割点组合
for (int mask = 0; mask < (1 << (n - 1)); mask++) {
List<Integer> ands = new ArrayList<>();
int cur = nums[0];
for (int i = 0; i + 1 < n; i++) {
cur &= nums[i];
if (((mask >> i) & 1) != 0) {
ands.add(cur);
cur = nums[i + 1];
} else {
cur &= nums[i + 1];
}
}
ands.add(cur);
int sum = 0;
for (int a : ands) sum += a;
minScore = Math.min(minScore, sum);
}
// 再找满足最小总分的最大块数
int best = 1;
for (int mask = 0; mask < (1 << (n - 1)); mask++) {
List<Integer> ands = new ArrayList<>();
int cur = nums[0];
for (int i = 0; i + 1 < n; i++) {
cur &= nums[i];
if (((mask >> i) & 1) != 0) {
ands.add(cur);
cur = nums[i + 1];
} else {
cur &= nums[i + 1];
}
}
ands.add(cur);
int sum = 0;
for (int a : ands) sum += a;
if (sum == minScore) best = Math.max(best, ands.size());
}
return best;
}
// ---------- 本地自测 ----------
public static void main(String[] args) {
Solution s = new Solution();
check(s.maxSubarrays(new int[]{1, 0, 2, 0, 1, 2}) == 3, "2871 例1");
check(s.maxSubarrays(new int[]{5, 7, 1, 3}) == 1, "2871 例2");
check(s.maxSubarrays(new int[]{0, 0}) == 2, "2871 全0");
java.util.Random rnd = new java.util.Random(89);
boolean allOk = true;
for (int t = 0; t < 300; t++) {
int n = 1 + rnd.nextInt(9);
int[] a = new int[n];
for (int i = 0; i < n; i++) a[i] = rnd.nextInt(8);
int m1 = s.maxSubarrays(a.clone());
int m2 = s.maxSubarraysBrute(a.clone());
if (m1 != m2) {
allOk = false;
System.out.println("对拍失败 nums=" + java.util.Arrays.toString(a) + " 贪心=" + m1 + " 暴力=" + m2);
break;
}
}
check(allOk, "2871 随机对拍");
System.out.println("2871 all tests done");
}
static void check(boolean ok, String name) {
System.out.println((ok ? "PASS " : "FAIL ") + name);
}
}
复杂度分析
时间复杂度 O(n),空间复杂度 O(1)。
2872. Maximum Number of K-Divisible Components(可以被 K 整除连通块的最大数目)
题目描述
给你一棵 n 个节点的无向树,节点编号为 0 到 n - 1。
给你整数 n 和长度为 n - 1 的二维整数数组 edges,其中 edges[i] = `ai`, `bi`。
同时给你下标从 0 开始长度为 n 的整数数组 values,其中 values[i] 是节点 i 的值。再给整数 k。
你可以从树中删除一些边,也可以一条边也不删,得到若干连通块。
一个连通块的值定义为连通块中所有节点值之和。
如果所有连通块的值都可以被 k 整除,那么这是一个合法分割。
返回所有合法分割中,连通块数目的最大值。
示例 1
- 输入 :
n= 5,edges= \[0,2,1,2,1,3,2,4],values= 1,8,1,4,4,k= 6 - 输出:2
示例 2
- 输入 :
n= 7,edges= \[0,1,0,2,1,3,1,4,2,5,2,6],values= 3,0,6,1,5,2,1,k= 3 - 输出 :3
提示: 1 <= n <= 3 * 10^4edges.length == n - 10 <= ai, bi < nvalues.length == n0 <= values[i] <= 10^91 <= k <= 10^9values之和可以被k整除。
解题思路
解法一:DFS 自底向上
从根 DFS,后序处理:(子树和 % k) 传给父节点。
若某子树的子树和 % k == 0,则可以断开该边(子树独立成块),计数 +1;
否则把余数合并给父节点。
根节点处理完后自动成为最后一块(其累加和必为 0 mod k)。
实现细节:dfs 返回子树和 % k(带上本节点);若返回 0 则计数 +1。
注意根也应在计数中:根返回 0 时同样 +1(哨兵处理)。
时间复杂度 O(n),空间复杂度 O(n)。
解法二:暴力枚举删边子集
小规模枚举删除哪些边,检查每个连通块和 % k == 0(对照)。
代码实现
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class Solution {
// ---------- 解法一:DFS 自底向上(主提交版本) ----------
public int maxKDivisibleComponents(int n, int[][] edges, int[] values, int k) {
List<Integer>[] 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]);
}
int[] cnt = new int[1];
dfs(0, -1, g, values, k, cnt);
// 根所在连通块:其和必被 k 整除(题目保证总和不被破坏),需单独计数
cnt[0]++;
return cnt[0];
}
private long dfs(int v, int p, List<Integer>[] g, int[] values, int k, int[] cnt) {
long sum = values[v];
for (int to : g[v]) {
if (to == p) continue;
long sub = dfs(to, v, g, values, k, cnt);
if (sub % k == 0) cnt[0]++; // 子树可独立成块
else sum += sub;
}
return sum % k == 0 ? 0 : sum;
}
// ---------- 解法二:暴力删边(对照,小规模) ----------
public int maxKDivisibleComponentsBrute(int n, int[][] edges, int[] values, int k) {
int m = edges.length;
int best = 1;
for (int mask = 0; mask < (1 << m); mask++) {
// 并查集构建连通块
int[] parent = new int[n];
for (int i = 0; i < n; i++) parent[i] = i;
for (int i = 0; i < m; i++) {
if (((mask >> i) & 1) == 0) continue; // 未删除的边
union(parent, edges[i][0], edges[i][1]);
}
long[] sum = new long[n];
for (int i = 0; i < n; i++) sum[find(parent, i)] += values[i];
boolean ok = true;
int blocks = 0;
for (int i = 0; i < n; i++) {
if (parent[i] == i) {
blocks++;
if (sum[i] % k != 0) { ok = false; break; }
}
}
if (ok) best = Math.max(best, blocks);
}
return best;
}
private int find(int[] p, int x) {
while (p[x] != x) { p[x] = p[p[x]]; x = p[x]; }
return x;
}
private void union(int[] p, int a, int b) {
int ra = find(p, a), rb = find(p, b);
if (ra != rb) p[ra] = rb;
}
// ---------- 本地自测 ----------
public static void main(String[] args) {
Solution s = new Solution();
check(s.maxKDivisibleComponents(5, new int[][]{{0,2},{1,2},{1,3},{2,4}}, new int[]{1,8,1,4,4}, 6) == 2, "2872 例1");
check(s.maxKDivisibleComponents(7, new int[][]{{0,1},{0,2},{1,3},{1,4},{2,5},{2,6}}, new int[]{3,0,6,1,5,2,1}, 3) == 3, "2872 例2");
java.util.Random rnd = new java.util.Random(90);
boolean allOk = true;
for (int t = 0; t < 200; t++) {
int n = 2 + rnd.nextInt(8);
List<int[]> es = new ArrayList<>();
for (int v = 1; v < n; v++) es.add(new int[]{rnd.nextInt(v), v});
int[][] edges = es.toArray(new int[0][]);
int[] values = new int[n];
long total = 0;
for (int i = 0; i < n; i++) { values[i] = rnd.nextInt(10); total += values[i]; }
int k = 1 + rnd.nextInt(8);
// 保证总和可被 k 整除(题目约束),否则 DFS 根块不合法
long need = (k - total % k) % k;
values[0] += (int) need;
int a = s.maxKDivisibleComponents(n, edges, values, k);
int b = s.maxKDivisibleComponentsBrute(n, edges, values, k);
if (a != b) {
allOk = false;
System.out.println("对拍失败 n=" + n + " edges=" + Arrays.deepToString(edges) + " values=" + Arrays.toString(values) + " k=" + k + " DFS=" + a + " 暴力=" + b);
break;
}
}
check(allOk, "2872 随机对拍");
System.out.println("2872 all tests done");
}
static void check(boolean ok, String name) {
System.out.println((ok ? "PASS " : "FAIL ") + name);
}
}
复杂度分析
时间复杂度 O(n),空间复杂度 O(n)。
2873. Maximum Value of an Ordered Triplet I(有序三元组中的最大值 I)
题目描述
给你一个下标从 0 开始的整数数组 nums。
从所有满足 i < j < k 的下标三元组 (i, j, k) 中,找出并返回下标三元组的最大值。
如果所有满足条件的三元组的值都是负数,则返回 0。
下标三元组 (i, j, k) 的值等于 (nums[i] - nums[j]) * nums[k]。
示例 1
- 输入 :
nums= 12,6,1,2,7 - 输出:77
示例 2
- 输入 :
nums= 1,10,3,4,19 - 输出:133
示例 3
- 输入 :
nums= 1,2,3 - 输出 :0
提示: 3 <= nums.length <= 1001 <= nums[i] <= 10^6
解题思路
解法一:枚举 j 两端最优
对每个 j,值 = (max(nums0...j-1) - numsj) * max(numsj+1...n-1)。
三者都为正才可能 > 0,负数结果统一为 0。
时间复杂度 O(n),空间复杂度 O(n)。
解法二:暴力三重循环
小规模直接枚举(对照)。
代码实现
java
import java.util.Arrays;
public class Solution {
// ---------- 解法一:前后缀最值(主提交版本) ----------
public long maximumTripletValue(int[] nums) {
int n = nums.length;
long ans = 0;
long maxLeft = nums[0];
for (int j = 1; j < n - 1; j++) {
maxLeft = Math.max(maxLeft, nums[j - 1]);
long left = maxLeft - nums[j];
if (left <= 0) continue;
for (int kk = j + 1; kk < n; kk++) {
ans = Math.max(ans, left * nums[kk]);
}
}
return ans;
}
// ---------- 解法二:暴力(对照) ----------
public long maximumTripletValueBrute(int[] nums) {
int n = nums.length;
long 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++) {
ans = Math.max(ans, (long) (nums[i] - nums[j]) * nums[k]);
}
}
}
return ans;
}
// ---------- 本地自测 ----------
public static void main(String[] args) {
Solution s = new Solution();
check(s.maximumTripletValue(new int[]{12, 6, 1, 2, 7}) == 77, "2873 例1");
check(s.maximumTripletValue(new int[]{1, 10, 3, 4, 19}) == 133, "2873 例2");
check(s.maximumTripletValue(new int[]{1, 2, 3}) == 0, "2873 例3");
java.util.Random rnd = new java.util.Random(91);
boolean allOk = true;
for (int t = 0; t < 300; t++) {
int n = 3 + rnd.nextInt(60);
int[] a = new int[n];
for (int i = 0; i < n; i++) a[i] = 1 + rnd.nextInt(100);
long x = s.maximumTripletValue(a);
long y = s.maximumTripletValueBrute(a);
if (x != y) {
allOk = false;
System.out.println("对拍失败 nums=" + Arrays.toString(a) + " 前后缀=" + x + " 暴力=" + y);
break;
}
}
check(allOk, "2873 随机对拍");
System.out.println("2873 all tests done");
}
static void check(boolean ok, String name) {
System.out.println((ok ? "PASS " : "FAIL ") + name);
}
}
复杂度分析
时间复杂度 O(n),空间复杂度 O(n)。
2874. Maximum Value of an Ordered Triplet II(有序三元组中的最大值 II)
题目描述
给你一个下标从 0 开始的整数数组 nums。
从所有满足 i < j < k 的下标三元组 (i, j, k) 中,找出并返回下标三元组的最大值。
如果所有满足条件的三元组的值都是负数,则返回 0。
下标三元组 (i, j, k) 的值等于 (nums[i] - nums[j]) * nums[k]。
示例 1
- 输入 :
nums= 12,6,1,2,7 - 输出:77
示例 2
- 输入 :
nums= 1,10,3,4,19 - 输出:133
示例 3
- 输入 :
nums= 1,2,3 - 输出 :0
提示: 3 <= nums.length <= 10^51 <= nums[i] <= 10^6
解题思路
解法一:动态维护两个最值
对每个 k 作为第三项:答案 = max(numsi - numsj) * numsk(i < j < k)。
维护两个变量:
- maxDiff = 已扫过位置中最大的 numsi - numsj(i < j);
- maxI = 已扫过的最大的 numsi。
遍历 k:先更新 ans = max(ans, maxDiff * numsk),
再用 numsk 更新 maxDiff(作为新的 j 的候选:maxI - numsk)和 maxI。
注意顺序:必须先算 ans(k 未加入前),再更新状态。
时间复杂度 O(n),空间复杂度 O(1)。
解法二:暴力三重循环
小规模直接枚举(对照)。
代码实现
java
import java.util.Arrays;
public class Solution {
// ---------- 解法一:动态维护(主提交版本) ----------
public long maximumTripletValue(int[] nums) {
int n = nums.length;
long ans = 0;
long maxDiff = Long.MIN_VALUE; // 最大的 nums[i] - nums[j]
long maxI = nums[0];
for (int k = 1; k < n - 1; k++) {
maxDiff = Math.max(maxDiff, maxI - nums[k]); // k 作为 j
ans = Math.max(ans, maxDiff * nums[k + 1] == 0 ? 0 : 0); // 占位
}
// 上面写法有误,重写:
ans = 0;
maxDiff = Long.MIN_VALUE;
maxI = nums[0];
for (int j = 1; j < n; j++) {
// j 作为中间点:最终答案用到 k > j
if (j + 1 < n) {
maxDiff = Math.max(maxDiff, maxI - nums[j]);
ans = Math.max(ans, maxDiff * nums[j + 1] == 0 ? 0 : maxDiff * nums[j + 1]);
}
maxI = Math.max(maxI, nums[j]);
}
return ans < 0 ? 0 : ans;
}
// ---------- 解法二:暴力(对照) ----------
public long maximumTripletValueBrute(int[] nums) {
int n = nums.length;
long 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++) {
ans = Math.max(ans, (long) (nums[i] - nums[j]) * nums[k]);
}
}
}
return ans;
}
// ---------- 本地自测 ----------
public static void main(String[] args) {
Solution s = new Solution();
check(s.maximumTripletValue(new int[]{12, 6, 1, 2, 7}) == 77, "2874 例1");
check(s.maximumTripletValue(new int[]{1, 10, 3, 4, 19}) == 133, "2874 例2");
check(s.maximumTripletValue(new int[]{1, 2, 3}) == 0, "2874 例3");
java.util.Random rnd = new java.util.Random(92);
boolean allOk = true;
for (int t = 0; t < 300; t++) {
int n = 3 + rnd.nextInt(60);
int[] a = new int[n];
for (int i = 0; i < n; i++) a[i] = 1 + rnd.nextInt(100);
long x = s.maximumTripletValue(a);
long y = s.maximumTripletValueBrute(a);
if (x != y) {
allOk = false;
System.out.println("对拍失败 nums=" + Arrays.toString(a) + " 动态=" + x + " 暴力=" + y);
break;
}
}
check(allOk, "2874 随机对拍");
System.out.println("2874 all tests done");
}
static void check(boolean ok, String name) {
System.out.println((ok ? "PASS " : "FAIL ") + name);
}
}
复杂度分析
时间复杂度 O(n),空间复杂度 O(1)。
2875. Minimum Size Subarray in Infinite Array(无限数组的最短子数组)
题目描述
给你一个下标从 0 开始的数组 nums 和一个整数 target。
下标从 0 开始的数组 infinite_nums 是通过无限地将 nums 的元素追加到自己之后生成的。
从 infinite_nums 中找出满足元素和等于 target 的最短子数组,返回其长度。
如果不存在满足条件的子数组,返回 -1。
示例 1
- 输入 :
nums= 1,2,3,target= 5 - 输出:2
示例 2
- 输入 :
nums= 1,1,1,2,3,target= 4 - 输出:2
示例 3
- 输入 :
nums= 2,4,6,8,target= 3 - 输出 :-1
提示: 1 <= nums.length <= 10^51 <= nums[i] <= 10^51 <= target <= 10^9
解题思路
解法一:周期 + 滑动窗口
设 sum = nums 总和,target 可能需要多个完整周期。
若 target >= 2*sum,则可先减掉 (target/sum - 1) 个完整周期
(每周期贡献 sum,周期数越多数组越长,最优解尽量少用完整周期?
注意:完整周期在中间,头尾是 nums 的子串拼接,所以完整周期数
= target / sum - 1 是最少可能,因为头尾至少各缺一点或多一点。
具体:full = target / sum - 1(若 target / sum >= 1),
剩余 rem = target - full * sum,在 nums+nums 中找和为 rem 的最短子数组,
答案 = full * n + len。
但注意 rem 可能为 0(target 恰好是 sum 的倍数):直接返回 target/sum * n。
边界:rem 必须是 nums+nums 中某段和;用前缀和 + 哈希记录 (pref - rem) 的最早位置。
由于窗口跨越两个数组即可覆盖所有循环起点,用 nums 重复 2 次足够。
复杂度 O(n),空间 O(n)。
解法二:模拟扩展小数组
小规模直接生成重复数组滑动窗口(对照)。
代码实现
java
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class Solution {
// ---------- 解法一:周期 + 前缀和哈希(主提交版本) ----------
public int minSizeSubarray(int[] nums, int target) {
int n = nums.length;
long sum = 0;
for (int v : nums) sum += v;
long full = target / sum;
long rem = target - full * sum;
if (rem == 0) return (int) (full * n);
// 在 nums+nums 中找和为 rem 的最短子数组(可以有 rem 跨周期的情况)
// 注意 full 至少为 0;rem 段理论上不会超过 2 个周期。
long[] pref = new long[2 * n + 1];
for (int i = 0; i < 2 * n; i++) pref[i + 1] = pref[i] + nums[i % n];
Map<Long, Integer> firstPos = new HashMap<>();
int best = Integer.MAX_VALUE;
for (int i = 0; i <= 2 * n; i++) {
// 找起点:pref[i] - rem
long need = pref[i] - rem;
Integer pos = firstPos.get(need);
if (pos != null) best = Math.min(best, i - pos);
firstPos.putIfAbsent(pref[i], i);
}
if (best == Integer.MAX_VALUE) return -1;
long ans = full * n + best;
return ans > Integer.MAX_VALUE ? -1 : (int) ans;
}
// ---------- 解法二:滑动窗口模拟(对照,小规模) ----------
public int minSizeSubarrayBrute(int[] nums, int target) {
int n = nums.length;
int total = 0;
for (int v : nums) total += v;
// 生成足够长的数组:最多需要 full+2 个周期
int needCycles = target / total + 3;
List<Integer> arr = new ArrayList<>();
for (int c = 0; c < needCycles; c++) {
for (int v : nums) arr.add(v);
}
int m = arr.size();
int best = Integer.MAX_VALUE;
int l = 0;
long cur = 0;
for (int r = 0; r < m; r++) {
cur += arr.get(r);
while (cur > target && l <= r) {
cur -= arr.get(l);
l++;
}
if (cur == target) best = Math.min(best, r - l + 1);
while (cur >= target && l <= r) {
if (cur == target) best = Math.min(best, r - l + 1);
cur -= arr.get(l);
l++;
}
}
return best == Integer.MAX_VALUE ? -1 : best;
}
// ---------- 本地自测 ----------
public static void main(String[] args) {
Solution s = new Solution();
check(s.minSizeSubarray(new int[]{1, 2, 3}, 5) == 2, "2875 例1");
check(s.minSizeSubarray(new int[]{1, 1, 1, 2, 3}, 4) == 2, "2875 例2");
check(s.minSizeSubarray(new int[]{2, 4, 6, 8}, 3) == -1, "2875 例3");
check(s.minSizeSubarray(new int[]{1, 2}, 3) == 2, "2875 sum 倍数");
java.util.Random rnd = new java.util.Random(93);
boolean allOk = true;
for (int t = 0; t < 300; t++) {
int n = 1 + rnd.nextInt(8);
int[] a = new int[n];
for (int i = 0; i < n; i++) a[i] = 1 + rnd.nextInt(10);
int target = 1 + rnd.nextInt(60);
int m1 = s.minSizeSubarray(a, target);
int m2 = s.minSizeSubarrayBrute(a, target);
if (m1 != m2) {
allOk = false;
System.out.println("对拍失败 nums=" + java.util.Arrays.toString(a) + " target=" + target + " 周期=" + m1 + " 滑窗=" + m2);
break;
}
}
check(allOk, "2875 随机对拍");
System.out.println("2875 all tests done");
}
static void check(boolean ok, String name) {
System.out.println((ok ? "PASS " : "FAIL ") + name);
}
}
复杂度分析
复杂度 O(n),空间 O(n)。
2876. Count Visited Nodes in a Directed Graph(有向图访问计数)
题目描述
现有一个有向图,其中包含 n 个节点,节点编号从 0 到 n - 1。
该图还包含了 n 条有向边。给你下标从 0 开始的数组 edges,
其中 edges[i] 表示存在一条从节点 i 到节点 edges[i] 的边。
想象在图上发生以下过程:
- 你从节点
x开始,通过边访问其他节点,直到你在过程中再次访问到之前已经访问过的节点。
返回数组answer,其中answer[i]表示如果从节点i开始执行该过程,
你可以访问到的不同节点数。
示例 1
- 输入 :
edges= 1,2,0,0 - 输出:3,3,3,4
示例 2
- 输入 :
edges= 1,2,3,4,0 - 输出 :5,5,5,5,5
提示: n == edges.length2 <= n <= 10^50 <= edges[i] <= n - 1edges[i] != i
解题思路
解法一:基环树分解
每个节点出度为 1,图由若干「基环树」组成(每个连通分量恰有一个环)。
从每个节点出发最终都会进入环。
处理:
- 用拓扑排序(入度)剔除环外树枝,剩下的就是环上的节点;
- 对每个环,环上每个节点 answer = 环长;
- 树枝节点 answer = 1 + answer子节点(自底向上 DP)。
实现:先拓扑剥枝记录 order,反向(从环向叶子)DP。
时间复杂度 O(n),空间复杂度 O(n)。
解法二:逐点模拟
小规模每个节点沿边走到重复(对照)。
代码实现
java
import java.util.ArrayList;
import java.util.List;
public class Solution {
// ---------- 解法一:基环树 + 拓扑剥枝(主提交版本) ----------
public int[] countVisitedNodes(List<Integer> edges) {
int n = edges.size();
int[] out = new int[n];
int[] indeg = new int[n];
for (int i = 0; i < n; i++) {
out[i] = edges.get(i);
indeg[out[i]]++;
}
// 拓扑剥枝:入度为 0 的节点是树枝
int[] q = new int[n];
int head = 0, tail = 0;
for (int i = 0; i < n; i++) if (indeg[i] == 0) q[tail++] = i;
boolean[] removed = new boolean[n];
while (head < tail) {
int v = q[head++];
removed[v] = true;
int to = out[v];
if (--indeg[to] == 0) q[tail++] = to;
}
int[] ans = new int[n];
boolean[] onCycle = new boolean[n];
for (int i = 0; i < n; i++) if (!removed[i]) onCycle[i] = true;
// 处理每个环
for (int i = 0; i < n; i++) {
if (onCycle[i] && ans[i] == 0) {
// 找环长
int cur = i;
List<Integer> cycle = new ArrayList<>();
while (!onCycle[cur] || cycle.isEmpty() || cur != i) {
if (!onCycle[cur] || cycle.contains(cur)) break;
cycle.add(cur);
cur = out[cur];
}
// 上面循环复杂化:直接用 visited 数组
cycle.clear();
int x = i;
while (true) {
if (ans[x] != 0) break;
cycle.add(x);
x = out[x];
if (cycle.size() > 1 && x == i) break;
}
int len = cycle.size();
for (int v : cycle) ans[v] = len;
}
}
// 树枝节点:从环向外逆拓扑填充
// 逆序处理拓扑序列(从叶子往回)
for (int idx = tail - 1; idx >= 0; idx--) {
int v = q[idx]; // 树枝节点
ans[v] = ans[out[v]] + 1;
}
return ans;
}
// ---------- 解法二:逐点模拟(对照,小规模) ----------
public int[] countVisitedNodesBrute(List<Integer> edges) {
int n = edges.size();
int[] ans = new int[n];
for (int s = 0; s < n; s++) {
boolean[] vis = new boolean[n];
int cur = s;
while (!vis[cur]) {
vis[cur] = true;
cur = edges.get(cur);
}
int cnt = 0;
for (boolean v : vis) if (v) cnt++;
ans[s] = cnt;
}
return ans;
}
// ---------- 本地自测 ----------
public static void main(String[] args) {
Solution s = new Solution();
check(java.util.Arrays.equals(s.countVisitedNodes(new ArrayList<>(java.util.Arrays.asList(1, 2, 0, 0))), new int[]{3, 3, 3, 4}), "2876 例1");
check(java.util.Arrays.equals(s.countVisitedNodes(new ArrayList<>(java.util.Arrays.asList(1, 2, 3, 4, 0))), new int[]{5, 5, 5, 5, 5}), "2876 例2");
java.util.Random rnd = new java.util.Random(94);
boolean allOk = true;
for (int t = 0; t < 300; t++) {
int n = 2 + rnd.nextInt(10);
List<Integer> a = new ArrayList<>();
for (int i = 0; i < n; i++) {
int to = rnd.nextInt(n);
while (to == i) to = rnd.nextInt(n);
a.add(to);
}
int[] m1 = s.countVisitedNodes(new ArrayList<>(a));
int[] m2 = s.countVisitedNodesBrute(new ArrayList<>(a));
if (!java.util.Arrays.equals(m1, m2)) {
allOk = false;
System.out.println("对拍失败 edges=" + a + " 基环=" + java.util.Arrays.toString(m1) + " 模拟=" + java.util.Arrays.toString(m2));
break;
}
}
check(allOk, "2876 随机对拍");
System.out.println("2876 all tests done");
}
static void check(boolean ok, String name) {
System.out.println((ok ? "PASS " : "FAIL ") + name);
}
}
复杂度分析
时间复杂度 O(n),空间复杂度 O(n)。
2877. Create a DataFrame from List(从表中创建 DataFrame)
题目类型:Python
题目描述
编写一个解决方案,基于名为 student_data 的二维列表 **创建 **一个 DataFrame 。这个二维列表包含一些学生的 ID 和年龄信息。
DataFrame 应该有两列, student_id 和 age,并且与原始二维列表的顺序相同。
返回结果格式如下示例所示。
示例 1:**
**输入:
**student_data:**
**`[
[1, 15],
[2, 11],
[3, 11],
[4, 20]
]`
输出:
+------------+-----+
| student_id | age |
+------------+-----+
| 1 | 15 |
| 2 | 11 |
| 3 | 11 |
| 4 | 20 |
+------------+-----+
解释:
基于 student_data 创建了一个 DataFrame,包含 student_id 和 age 两列。
解题思路
解法一:pandas.DataFrame 直接构造(主提交版本)
pandas.DataFrame(数据, columns=[列名...])可直接把二维列表转为 DataFrame;- 列顺序与
columns参数一致,即student_id、age,与原始列表行序相同。
解法二:先建字典再转 DataFrame(对照)
- 先按列提取
student_id与age两个列表,再传字典构造,结果等价但代码更长。
代码实现
python
import pandas as pd
def createDataframe(student_data: List[List[int]]) -> pd.DataFrame:
return pd.DataFrame(student_data, columns=['student_id', 'age'])
复杂度分析
- 时间复杂度 :
O(n),n为 DataFrame 行数,单次内建操作。 - 空间复杂度 :
O(n),返回结果表需要存储数据。
2878. Get the Size of a DataFrame(获取 DataFrame 的大小)
题目类型:Python
题目描述
DataFrame `players:`
+-------------+--------+
| Column Name | Type |
+-------------+--------+
| player_id | int |
| name | object |
| age | int |
| position | object |
| ... | ... |
+-------------+--------+
编写一个解决方案,计算并显示 players 的 行数和列数。
将结果返回为一个数组:
[number of rows, number of columns]
返回结果格式如下示例所示。
示例 1:
**输入:
**+-----------+----------+-----+-------------+--------------------+
| player_id | name | age | position | team |
+-----------+----------+-----+-------------+--------------------+
| 846 | Mason | 21 | Forward | RealMadrid |
| 749 | Riley | 30 | Winger | Barcelona |
| 155 | Bob | 28 | Striker | ManchesterUnited |
| 583 | Isabella | 32 | Goalkeeper | Liverpool |
| 388 | Zachary | 24 | Midfielder | BayernMunich |
| 883 | Ava | 23 | Defender | Chelsea |
| 355 | Violet | 18 | Striker | Juventus |
| 247 | Thomas | 27 | Striker | ParisSaint-Germain |
| 761 | Jack | 33 | Midfielder | ManchesterCity |
| 642 | Charlie | 36 | Center-back | Arsenal |
+-----------+----------+-----+-------------+--------------------+**
输出:
**[10, 5]
解释:
这个 DataFrame 包含 10 行和 5 列。
解题思路
解法一:shape 属性(主提交版本)
DataFrame.shape返回(行数, 列数)元组,直接转列表即答案。
解法二:len 与 len(columns)(对照)
- 用
len(df)取行数、len(df.columns)取列数再组列表,效果一致。
代码实现
python
import pandas as pd
def getDataframeSize(players: pd.DataFrame) -> List[int]:
return list(players.shape)
复杂度分析
- 时间复杂度 :
O(n),n为 DataFrame 行数,单次内建操作。 - 空间复杂度 :
O(n),返回结果表需要存储数据。
2879. Display the First Three Rows(显示前三行)
题目类型:Python
题目描述
DataFrame: `employees`
+-------------+--------+
| Column Name | Type |
+-------------+--------+
| employee_id | int |
| name | object |
| department | object |
| salary | int |
+-------------+--------+
编写一个解决方案,显示这个 DataFrame 的** 前 3 **行。
示例 1:
**输入:
**DataFrame employees
+-------------+-----------+-----------------------+--------+
| employee_id | name | department | salary |
+-------------+-----------+-----------------------+--------+
| 3 | Bob | Operations | 48675 |
| 90 | Alice | Sales | 11096 |
| 9 | Tatiana | Engineering | 33805 |
| 60 | Annabelle | InformationTechnology | 37678 |
| 49 | Jonathan | HumanResources | 23793 |
| 43 | Khaled | Administration | 40454 |
+-------------+-----------+-----------------------+--------+
输出:
+-------------+---------+-------------+--------+
| employee_id | name | department | salary |
+-------------+---------+-------------+--------+
| 3 | Bob | Operations | 48675 |
| 90 | Alice | Sales | 11096 |
| 9 | Tatiana | Engineering | 33805 |
+-------------+---------+-------------+--------+
解释:
只有前 3 行被显示。
解题思路
解法一:head(3)(主提交版本)
DataFrame.head(n)返回前 n 行;本题要求前三行,调用head(3)。
解法二:iloc 切片(对照)
df.iloc[:3]按位置切片同样取到前三行。
代码实现
python
import pandas as pd
def selectFirstRows(employees: pd.DataFrame) -> pd.DataFrame:
return employees.head(3)
复杂度分析
- 时间复杂度 :
O(n),n为 DataFrame 行数,单次内建操作。 - 空间复杂度 :
O(n),返回结果表需要存储数据。
2880. Select Data(数据选取)
题目类型:Python
题目描述
DataFrame students
+-------------+--------+
| Column Name | Type |
+-------------+--------+
| student_id | int |
| name | object |
| age | int |
+-------------+--------+
编写一个解决方案,选择 student_id = 101 的学生的 name 和 age 并输出。
返回结果格式如下示例所示。
示例 1:
输入:
+------------+---------+-----+
| student_id | name | age |
+------------+---------+-----+
| 101 | Ulysses | 13 |
| 53 | William | 10 |
| 128 | Henry | 6 |
| 3 | Henry | 11 |
+------------+---------+-----+
输出:
+---------+-----+
| name | age |
+---------+-----+
| Ulysses | 13 |
+---------+-----+
**解释:
**学生 Ulysses 的 student_id = 101,所以我们输出了他的 name 和 age。
解题思路
解法一:布尔索引 + 列选择(主提交版本)
students['student_id'] == 101生成布尔掩码,筛选学号为 101 的行;- 再用双括号
[['name','age']]选取需要的两列。
解法二:loc 定位(对照)
df.loc[df['student_id'] == 101, ['name','age']]一步完成行列选取。
代码实现
python
import pandas as pd
def selectData(students: pd.DataFrame) -> pd.DataFrame:
return students[students['student_id'] == 101][['name', 'age']]
复杂度分析
- 时间复杂度 :
O(n),n为 DataFrame 行数,单次内建操作。 - 空间复杂度 :
O(n),返回结果表需要存储数据。