LeetCode 2901-2920. 题解合集
文章目录
- [LeetCode 2901-2920. 题解合集](#LeetCode 2901-2920. 题解合集)
-
- [2901. Longest Unequal Adjacent Groups Subsequence II(最长相邻不相等子序列 II)](#2901. Longest Unequal Adjacent Groups Subsequence II(最长相邻不相等子序列 II))
- [2902. Count of Sub-Multisets With Bounded Sum(和带限制的子多重集合的数目)](#2902. Count of Sub-Multisets With Bounded Sum(和带限制的子多重集合的数目))
- [2903. Find Indices With Index and Value Difference I(找出满足差值条件的下标 I)](#2903. Find Indices With Index and Value Difference I(找出满足差值条件的下标 I))
- [2904. Shortest and Lexicographically Smallest Beautiful String(最短且字典序最小的美丽子字符串)](#2904. Shortest and Lexicographically Smallest Beautiful String(最短且字典序最小的美丽子字符串))
- [2905. Find Indices With Index and Value Difference II(找出满足差值条件的下标 II)](#2905. Find Indices With Index and Value Difference II(找出满足差值条件的下标 II))
- [2906. Construct Product Matrix(构造乘积矩阵)](#2906. Construct Product Matrix(构造乘积矩阵))
- [2907. Maximum Profitable Triplets With Increasing Prices I(价格递增的最大利润三元组 I)](#2907. Maximum Profitable Triplets With Increasing Prices I(价格递增的最大利润三元组 I))
- [2908. Minimum Sum of Mountain Triplets I(元素和最小的山形三元组 I)](#2908. Minimum Sum of Mountain Triplets I(元素和最小的山形三元组 I))
- [2909. Minimum Sum of Mountain Triplets II(元素和最小的山形三元组 II)](#2909. Minimum Sum of Mountain Triplets II(元素和最小的山形三元组 II))
- [2910. Minimum Number of Groups to Create a Valid Assignment(合法分组的最少组数)](#2910. Minimum Number of Groups to Create a Valid Assignment(合法分组的最少组数))
- [2911. Minimum Changes to Make K Semi-palindromes(得到 K 个半回文串的最少修改次数)](#2911. Minimum Changes to Make K Semi-palindromes(得到 K 个半回文串的最少修改次数))
- [2912. Number of Ways to Reach Destination in the Grid(在网格上移动到目的地的方法数)](#2912. Number of Ways to Reach Destination in the Grid(在网格上移动到目的地的方法数))
- [2913. Subarrays Distinct Element Sum of Squares I(子数组不同元素数目的平方和 I)](#2913. Subarrays Distinct Element Sum of Squares I(子数组不同元素数目的平方和 I))
- [2914. Minimum Number of Changes to Make Binary String Beautiful(使二进制字符串变美丽的最少修改次数)](#2914. Minimum Number of Changes to Make Binary String Beautiful(使二进制字符串变美丽的最少修改次数))
- [2915. Length of the Longest Subsequence That Sums to Target(和为目标值的最长子序列的长度)](#2915. Length of the Longest Subsequence That Sums to Target(和为目标值的最长子序列的长度))
- [2916. Subarrays Distinct Element Sum of Squares II(子数组不同元素数目的平方和 II)](#2916. Subarrays Distinct Element Sum of Squares II(子数组不同元素数目的平方和 II))
- [2917. Find the K-or of an Array(找出数组中的 K-or 值)](#2917. Find the K-or of an Array(找出数组中的 K-or 值))
- [2918. Minimum Equal Sum of Two Arrays After Replacing Zeros(数组的最小相等和)](#2918. Minimum Equal Sum of Two Arrays After Replacing Zeros(数组的最小相等和))
- [2919. Minimum Increment Operations to Make Array Beautiful(使数组变美的最小增量运算数)](#2919. Minimum Increment Operations to Make Array Beautiful(使数组变美的最小增量运算数))
- [2920. Maximum Points After Collecting Coins From All Nodes(收集所有金币可获得的最大积分)](#2920. Maximum Points After Collecting Coins From All Nodes(收集所有金币可获得的最大积分))
2901. Longest Unequal Adjacent Groups Subsequence II(最长相邻不相等子序列 II)
题目描述
给定一个字符串数组 words 和一个数组 groups,两个数组长度都是 n。
两个长度相等字符串的汉明距离定义为对应位置字符不同的数目。
需要从下标 0, 1, ..., `n`-1 中选出一个最长子序列 `i0`, `i1`, ..., `ik`-1,满足:
- 相邻下标的
groups值不同; - 相邻
words字符串长度相等,且汉明距离为 1。
返回下标子序列依次对应words数组中的字符串连接形成的字符串数组。若有多个答案,返回任意一个。
示例 1
words = ["bab","dab","cab"], groups = [1,2,2] -> ["bab","cab"]
示例 2
words = ["a","b","c","d"], groups = [1,2,3,4] -> ["a","b","c","d"]
提示:
1 <= n == words.length == groups.length <= 10001 <= words[i].length <= 101 <= groups[i] <= nwords中的字符串互不相同
解题思路
解题思路:动态规划 + 前驱回溯
定义 dpi 为以 i 结尾的最长合法子序列长度,previ 为前驱下标(-1 表示无前驱)。
对每个 i,枚举 j < i,若 groupsj != groupsi 且两字符串等长、汉明距离为 1,
则 dpi = max(dpj + 1)。最后找到最长 dp 的下标,沿 prev 回溯重建答案。
时间复杂度 O(n^2 * L),L <= 10 为字符串长度,空间 O(n)。
复杂度:
- 时间复杂度:O(n^2 * L)
- 空间复杂度:O(n)
代码实现
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
class Solution {
public List<String> getWordsInLongestSubsequence(String[] words, int[] groups) {
int n = words.length;
int[] dp = new int[n];
int[] prev = new int[n];
Arrays.fill(dp, 1);
Arrays.fill(prev, -1);
int best = 1, bestIdx = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < i; j++) {
if (groups[j] != groups[i] && words[j].length() == words[i].length()
&& ham(words[i], words[j]) == 1 && dp[j] + 1 > dp[i]) {
dp[i] = dp[j] + 1;
prev[i] = j;
}
}
if (dp[i] > best) {
best = dp[i];
bestIdx = i;
}
}
List<String> tmp = new ArrayList<>();
for (int cur = bestIdx; cur != -1; cur = prev[cur]) {
tmp.add(words[cur]);
}
List<String> ans = new ArrayList<>();
for (int i = tmp.size() - 1; i >= 0; i--) {
ans.add(tmp.get(i));
}
return ans;
}
// 计算两个等长字符串的汉明距离
private int ham(String a, String b) {
int d = 0;
for (int i = 0; i < a.length(); i++) {
if (a.charAt(i) != b.charAt(i)) {
d++;
}
}
return d;
}
// ========== 本地 main 自测 ==========
private static boolean canLink(String a, String b, int ga, int gb) {
if (ga == gb || a.length() != b.length()) {
return false;
}
int d = 0;
for (int i = 0; i < a.length(); i++) {
if (a.charAt(i) != b.charAt(i)) {
d++;
}
}
return d == 1;
}
// 暴力对照:DFS 枚举所有子序列求最长,仅用于小数据对拍
private static List<String> brute(String[] words, int[] groups) {
int n = words.length;
List<String> best = new ArrayList<>();
int[] idxs = new int[n];
dfs(words, groups, 0, idxs, 0, best);
return best;
}
private static void dfs(String[] words, int[] groups, int pos, int[] idxs, int len, List<String> best) {
if (len > best.size()) {
best.clear();
for (int i = 0; i < len; i++) {
best.add(words[idxs[i]]);
}
}
if (pos == words.length) {
return;
}
// 尝试把 pos 加入子序列末尾
if (len == 0 || canLink(words[idxs[len - 1]], words[pos],
groups[idxs[len - 1]], groups[pos])) {
idxs[len] = pos;
dfs(words, groups, pos + 1, idxs, len + 1, best);
}
dfs(words, groups, pos + 1, idxs, len, best);
}
private static boolean valid(String[] words, int[] groups, List<String> ans) {
// 校验 ans 是否是由 words 的下标子序列构成的合法答案
int n = words.length;
outer:
for (int mask = 0; mask < (1 << n); mask++) {
List<Integer> li = new ArrayList<>();
for (int i = 0; i < n; i++) {
if ((mask >> i & 1) != 0) {
li.add(i);
}
}
if (li.size() != ans.size()) {
continue;
}
// 校验 content
for (int t = 0; t < li.size(); t++) {
if (!words[li.get(t)].equals(ans.get(t))) {
continue outer;
}
}
boolean ok = true;
for (int t = 1; t < li.size(); t++) {
if (!canLink(words[li.get(t - 1)], words[li.get(t)],
groups[li.get(t - 1)], groups[li.get(t)])) {
ok = false;
break;
}
}
if (ok) {
return true;
}
}
return false;
}
public static void main(String[] args) {
Solution s = new Solution();
// 官方案例
check(s, new String[]{"bab", "dab", "cab"}, new int[]{1, 2, 2}, 2);
check(s, new String[]{"a", "b", "c", "d"}, new int[]{1, 2, 3, 4}, 4);
// 随机对拍
java.util.Random rnd = new java.util.Random(42);
for (int t = 0; t < 300; t++) {
int n = 1 + rnd.nextInt(8);
String[] words = new String[n];
int[] groups = new int[n];
java.util.Set<String> used = new java.util.HashSet<>();
for (int i = 0; i < n; i++) {
groups[i] = 1 + rnd.nextInt(2);
int len = 2 + rnd.nextInt(2); // 长度为 2~3,保证不重复单词空间充足
String w;
do {
StringBuilder sb = new StringBuilder();
for (int c = 0; c < len; c++) {
sb.append((char) ('a' + rnd.nextInt(3)));
}
w = sb.toString();
} while (used.contains(w));
used.add(w);
words[i] = w;
}
List<String> exp = brute(words, groups);
List<String> got = s.getWordsInLongestSubsequence(words, groups);
if (got.size() != exp.size() || !valid(words, groups, got)) {
System.out.println("FAIL t=" + t + " words=" + Arrays.toString(words)
+ " groups=" + Arrays.toString(groups)
+ " got=" + got + " expSize=" + exp.size());
return;
}
}
System.out.println("ALL PASS");
}
private static void check(Solution s, String[] words, int[] groups, int expLen) {
List<String> got = s.getWordsInLongestSubsequence(words, groups);
boolean ok = got.size() == expLen && valid(words, groups, got);
System.out.println((ok ? "PASS" : "FAIL") + " words=" + Arrays.toString(words)
+ " groups=" + Arrays.toString(groups) + " got=" + got + " expLen=" + expLen);
}
}
复杂度分析
时间复杂度 O(n^2 * L),L <= 10 为字符串长度,空间 O(n)。
- 时间复杂度:O(n^2 * L)
- 空间复杂度:O(n)
2902. Count of Sub-Multisets With Bounded Sum(和带限制的子多重集合的数目)
题目描述
给定一个下标从 0 开始的非负整数数组 nums 和两个整数 l、r。
返回 nums 中子多重集合的和在闭区间 `l`, `r` 之间的子多重集合的数目,答案对 1e9+7 取模。
子多重集合指从数组中选出一些元素构成的无序集合,每个元素 x 出现次数可以是 0...occ[x]。
空集合的和是 0。
示例 1
nums = [1,2,2,3], l = 6, r = 6 -> 1({1,2,3})
示例 2
nums = [2,1,4,2,7], l = 1, r = 5 -> 7
示例 3
nums = [1,2,1,3,5,2], l = 3, r = 5 -> 9
提示:
1 <= nums.length <= 2 * 10^40 <= nums[i] <= 2 * 10^4nums的和不超过 2 * 10^40 <= l <= r <= 2 * 10^4
解题思路
解题思路:多重背包 + 前缀和/滑动窗口优化
统计每个数值 v 的出现次数 cntv。对每个 v > 0 做多重背包:
dps 为当前考虑过的值组合出和为 s 的方案数,每个值可选 0...cntv 个。
朴素转移 O(S*cnt) 会超时,利用「同余分组 + 滑动窗口」把每个值的转移降到 O(S):
对每个余数 r(0 <= r < v),在序列 r, r+v, r+2v, ... 上维护一个覆盖 c+1 个 dp 值的滑窗和。
值为 0 的元素可选任意个但不影响和,最终答案整体乘上 (cnt0+1)。
关键观察:不同正值种类数 k 满足 1+2+...+k <= S <= 2e4,故 k <= ~200,
总复杂度 O(S * k) <= 4e6,轻松通过。
复杂度:
- 时间复杂度:O(S * k),S 为 nums 总和(<= 2e4),k 为不同正值的种类数(<= ~200)
- 空间复杂度:O(S)
代码实现
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
class Solution {
private static final int MOD = 1_000_000_007;
public int countSubMultisets(List<Integer> nums, int l, int r) {
Map<Integer, Integer> cntMap = new HashMap<>();
int sum = 0;
for (int x : nums) {
cntMap.put(x, cntMap.getOrDefault(x, 0) + 1);
sum += x;
}
int size = Math.min(sum, r);
long[] dp = new long[size + 1];
dp[0] = 1; // 空集
int zeroCnt = cntMap.getOrDefault(0, 0);
// 对每个正值做多重背包(滑动窗口优化)
for (Map.Entry<Integer, Integer> e : cntMap.entrySet()) {
int v = e.getKey();
int c = e.getValue();
if (v == 0) {
continue;
}
long[] ndp = new long[size + 1];
for (int rem = 0; rem < v; rem++) {
long win = 0;
int cntWin = 0; // 窗口内 dp 值个数
for (int s = rem; s <= size; s += v) {
win = (win + dp[s]) % MOD;
cntWin++;
if (cntWin > c + 1) {
win = (win - dp[s - (c + 1) * v] + MOD) % MOD;
}
ndp[s] = win;
}
}
dp = ndp;
}
long ans = 0;
for (int s = l; s <= r && s <= size; s++) {
ans = (ans + dp[s]) % MOD;
}
// 0 可以选 0..zeroCnt 次
ans = ans * (zeroCnt + 1) % MOD;
return (int) ans;
}
// ========== 本地 main 自测 ==========
// 暴力对照:按值分组 DFS 枚举每个值选 0..occ 次,统计和落在 [l,r] 的方案数
private static int brute(List<Integer> nums, int l, int r) {
Map<Integer, Integer> cntMap = new HashMap<>();
for (int x : nums) {
cntMap.put(x, cntMap.getOrDefault(x, 0) + 1);
}
List<Integer> vals = new ArrayList<>(cntMap.keySet());
int[] cnts = new int[vals.size()];
for (int i = 0; i < vals.size(); i++) {
cnts[i] = cntMap.get(vals.get(i));
}
int[] res = new int[1];
dfs(vals, cnts, 0, 0, l, r, res);
return res[0];
}
private static void dfs(List<Integer> vals, int[] cnts, int idx, int cur, int l, int r, int[] res) {
if (idx == vals.size()) {
if (cur >= l && cur <= r) {
res[0]++;
}
return;
}
int v = vals.get(idx);
for (int k = 0; k <= cnts[idx]; k++) {
if (cur + k * v > r) {
break;
}
dfs(vals, cnts, idx + 1, cur + k * v, l, r, res);
}
}
public static void main(String[] args) {
Solution s = new Solution();
// 官方案例
check(s, Arrays.asList(1, 2, 2, 3), 6, 6, 1);
check(s, Arrays.asList(2, 1, 4, 2, 7), 1, 5, 7);
check(s, Arrays.asList(1, 2, 1, 3, 5, 2), 3, 5, 9);
// 空集属于和 0
check(s, Arrays.asList(3, 5), 0, 0, 1);
check(s, Arrays.asList(0, 0), 0, 0, 3);
// 随机对拍
Random rnd = new Random(7);
for (int t = 0; t < 300; t++) {
int n = 1 + rnd.nextInt(8);
List<Integer> nums = new ArrayList<>();
int tot = 0;
for (int i = 0; i < n; i++) {
int x = rnd.nextInt(6);
nums.add(x);
tot += x;
}
int r = rnd.nextInt(tot + 1);
int l = rnd.nextInt(r + 1);
int exp = brute(nums, l, r);
int got = (int) s.countSubMultisets(nums, l, r);
if (exp != got) {
System.out.println("FAIL t=" + t + " nums=" + nums + " l=" + l + " r=" + r
+ " got=" + got + " exp=" + exp);
return;
}
}
System.out.println("ALL PASS");
}
private static void check(Solution s, List<Integer> nums, int l, int r, int exp) {
int got = s.countSubMultisets(nums, l, r);
int exp2 = brute(nums, l, r);
System.out.println((got == exp && got == exp2 ? "PASS" : "FAIL")
+ " nums=" + nums + " l=" + l + " r=" + r + " got=" + got
+ " exp=" + exp + " brute=" + exp2);
}
}
复杂度分析
- 时间复杂度:O(S * k),S 为 nums 总和(<= 2e4),k 为不同正值的种类数(<= ~200)
- 空间复杂度:O(S)
2903. Find Indices With Index and Value Difference I(找出满足差值条件的下标 I)
题目描述
给定一个下标从 0 开始、长度为 n 的整数数组 nums,以及整数 indexDifference 和
valueDifference。找出 2 个下标 i 和 j,满足:
abs(i - j) >= indexDifferenceabs(nums[i] - nums[j]) >= valueDifference
返回 `i`, `j`;若不存在返回 -1, -1。存在多组答案返回任意一组。注意i和j可能相等。
示例 1
nums = [5,1,4,1], indexDifference = 2, valueDifference = 4 -> [0,3]
示例 2
nums = [2,1], indexDifference = 0, valueDifference = 0 -> [0,0]
示例 3
nums = [1,2,3], indexDifference = 2, valueDifference = 4 -> [-1,-1]
提示:
1 <= n == nums.length <= 1000 <= nums[i] <= 500 <= indexDifference <= 1000 <= valueDifference <= 50
解题思路
解题思路:滑动窗口同时维护最大/最小值下标
固定 j 从后往前扫(j 从 n-1 递减到 0),要求 abs(i - j) >= indexDifference。
当扫描到 j 时,所有满足 abs(i - j) >= indexDifference 的 i 都位于 0, j - indexDifference
区间内。因此维护该区间内 nums 最大值和最小值的下标(相等时取任一即可),
若 max - numsj >= valueDifference 或 numsj - min >= valueDifference 即找到答案。
时间复杂度 O(n),比暴力 O(n^2) 更优。
复杂度:
- 时间复杂度:O(n)
- 空间复杂度:O(1)
代码实现
java
import java.util.Arrays;
import java.util.Random;
class Solution {
public int[] findIndices(int[] nums, int indexDifference, int valueDifference) {
int n = nums.length;
// 前缀维护 [0, j-indexDifference] 区间的最大/最小下标
int maxIdx = 0, minIdx = 0;
for (int j = 0; j < n; j++) {
int left = j - indexDifference;
if (left >= 0) {
// 更新区间右端点 left 处的候选
if (nums[left] > nums[maxIdx]) {
maxIdx = left;
}
if (nums[left] < nums[minIdx]) {
minIdx = left;
}
if (nums[j] - nums[minIdx] >= valueDifference
|| nums[maxIdx] - nums[j] >= valueDifference) {
// 优先返回差值满足的下标对
if (nums[j] - nums[minIdx] >= valueDifference) {
return new int[]{minIdx, j};
}
return new int[]{maxIdx, j};
}
}
}
return new int[]{-1, -1};
}
// ========== 本地 main 自测 ==========
// 暴力对照
private static int[] brute(int[] nums, int indexDifference, int valueDifference) {
int n = nums.length;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (Math.abs(i - j) >= indexDifference
&& Math.abs(nums[i] - nums[j]) >= valueDifference) {
return new int[]{i, j};
}
}
}
return new int[]{-1, -1};
}
public static void main(String[] args) {
Solution s = new Solution();
// 官方案例
System.out.println("PASS " + Arrays.toString(s.findIndices(new int[]{5, 1, 4, 1}, 2, 4)));
System.out.println("PASS " + Arrays.toString(s.findIndices(new int[]{2, 1}, 0, 0)));
System.out.println("PASS " + Arrays.toString(s.findIndices(new int[]{1, 2, 3}, 2, 4)));
// 随机对拍
Random rnd = new Random(5);
for (int t = 0; t < 500; t++) {
int n = 1 + rnd.nextInt(10);
int[] nums = new int[n];
for (int i = 0; i < n; i++) {
nums[i] = rnd.nextInt(10);
}
int id = rnd.nextInt(n);
int vd = rnd.nextInt(11);
int[] exp = brute(nums, id, vd);
int[] got = s.findIndices(nums, id, vd);
boolean expOk = exp[0] != -1
&& Math.abs(exp[0] - exp[1]) >= id
&& Math.abs(nums[exp[0]] - nums[exp[1]]) >= vd;
boolean gotOk = got[0] == -1 ? !expOk
: (Math.abs(got[0] - got[1]) >= id
&& Math.abs(nums[got[0]] - nums[got[1]]) >= vd);
if (!gotOk) {
System.out.println("FAIL t=" + t + " nums=" + Arrays.toString(nums)
+ " id=" + id + " vd=" + vd + " got=" + Arrays.toString(got)
+ " exp=" + Arrays.toString(exp));
return;
}
}
System.out.println("ALL PASS");
}
}
复杂度分析
时间复杂度 O(n),比暴力 O(n^2) 更优。
- 时间复杂度:O(n)
- 空间复杂度:O(1)
2904. Shortest and Lexicographically Smallest Beautiful String(最短且字典序最小的美丽子字符串)
题目描述
给一个二进制字符串 s 和一个正整数 k。若 s 的某个子字符串中 '1' 的个数恰好等于 k,
则称其为美丽子字符串。令 len 等于最短美丽子字符串的长度。返回长度等于 len 且字典序
最小的美丽子字符串;若不存在返回空字符串。
示例 1
s = "100011001", k = 3 -> "11001"(最短美丽子串长度为 5)
示例 2
s = "1011", k = 2 -> "11"
示例 3
s = "000", k = 1 -> ""(不存在美丽子字符串)
提示:
1 <= s.length <= 1001 <= k <= s.length
解题思路
解题思路:枚举 + 比较
n <= 100,可以暴力枚举所有子串:对每个起点 i 向右扩展并统计 '1' 个数,
一旦等于 k 就得到一个候选子串。在满足"最短"这一优先级的候选集合中,
维护字典序最小的那个。注意:短子串长度相同的情况下用 compareTo 比较字典序。
复杂度:
- 时间复杂度:O(n^2 * L)(每个候选与当前最优比较,L <= n),实际上子串截取与比较亦可
平摊到 O(n^2),n <= 100 完全可接受 - 空间复杂度:O(n)(存储候选子串)
代码实现
java
import java.util.Random;
class Solution {
public String shortestBeautifulSubstring(String s, int k) {
int n = s.length();
String best = null;
for (int i = 0; i < n; i++) {
int ones = 0;
for (int j = i; j < n; j++) {
if (s.charAt(j) == '1') {
ones++;
}
if (ones > k) {
break;
}
if (ones == k) {
String cand = s.substring(i, j + 1);
if (best == null || cand.length() < best.length()
|| (cand.length() == best.length() && cand.compareTo(best) < 0)) {
best = cand;
}
}
}
}
return best == null ? "" : best;
}
// ========== 本地 main 自测 ==========
// 暴力对照:枚举所有子串,按(长度, 字典序)规则维护最优
private static String brute(String s, int k) {
int n = s.length();
String best = null;
for (int i = 0; i < n; i++) {
for (int j = i; j < n; j++) {
String sub = s.substring(i, j + 1);
int ones = 0;
for (int c = 0; c < sub.length(); c++) {
if (sub.charAt(c) == '1') {
ones++;
}
}
if (ones == k) {
if (best == null || sub.length() < best.length()
|| (sub.length() == best.length() && sub.compareTo(best) < 0)) {
best = sub;
}
}
}
}
return best == null ? "" : best;
}
public static void main(String[] args) {
Solution s = new Solution();
// 官方案例
check(s, "100011001", 3, "11001");
check(s, "1011", 2, "11");
check(s, "000", 1, "");
// 随机对拍
Random rnd = new Random(11);
char[] bits = {'0', '1'};
for (int t = 0; t < 300; t++) {
int n = 1 + rnd.nextInt(12);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; i++) {
sb.append(bits[rnd.nextInt(2)]);
}
String str = sb.toString();
int k = 1 + rnd.nextInt(n);
String exp = brute(str, k);
String got = s.shortestBeautifulSubstring(str, k);
if (!exp.equals(got)) {
System.out.println("FAIL t=" + t + " s=" + str + " k=" + k
+ " got=" + got + " exp=" + exp);
return;
}
}
System.out.println("ALL PASS");
}
private static void check(Solution s, String str, int k, String exp) {
String got = s.shortestBeautifulSubstring(str, k);
System.out.println((exp.equals(got) ? "PASS" : "FAIL") + " s=" + str
+ " k=" + k + " got=" + got + " exp=" + exp);
}
}
复杂度分析
- 时间复杂度:O(n^2 * L)(每个候选与当前最优比较,L <= n),实际上子串截取与比较亦可
- 空间复杂度:O(n)(存储候选子串)
2905. Find Indices With Index and Value Difference II(找出满足差值条件的下标 II)
题目描述
给定一个下标从 0 开始、长度为 n 的整数数组 nums,以及整数 indexDifference 和
valueDifference。找出 2 个下标 i 和 j,满足:
abs(i - j) >= indexDifferenceabs(nums[i] - nums[j]) >= valueDifference
返回 `i`, `j`;若不存在返回 -1, -1。存在多组答案返回任意一组。注意i和j可能相等。
(本题为 2903 的升级版,数据规模更大)
示例 1
nums = [5,1,4,1], indexDifference = 2, valueDifference = 4 -> [0,3]
示例 2
nums = [2,1], indexDifference = 0, valueDifference = 0 -> [0,0]
示例 3
nums = [1,2,3], indexDifference = 2, valueDifference = 4 -> [-1,-1]
提示:
1 <= n == nums.length <= 10^51 <= nums[i] <= 10^90 <= indexDifference <= 10^50 <= valueDifference <= 10^9
解题思路
解题思路:滑动窗口同时维护最大/最小值下标
与 2903 相同的贪心:固定 j 从后往前扫,所有满足 abs(i - j) >= indexDifference 的 i
都位于 0, j - indexDifference。维护该区间 nums 的最大值和最小值所在下标,
检查 numsj 与区间极值的差是否 >= valueDifference。
O(n) 时间即可判定,满足 10^5 数据规模。
复杂度:
- 时间复杂度:O(n)
- 空间复杂度:O(1)
代码实现
java
import java.util.Arrays;
import java.util.Random;
class Solution {
public int[] findIndices(int[] nums, int indexDifference, int valueDifference) {
int n = nums.length;
int maxIdx = 0, minIdx = 0;
for (int j = 0; j < n; j++) {
int left = j - indexDifference;
if (left >= 0) {
if (nums[left] > nums[maxIdx]) {
maxIdx = left;
}
if (nums[left] < nums[minIdx]) {
minIdx = left;
}
if (nums[j] - nums[minIdx] >= valueDifference) {
return new int[]{minIdx, j};
}
if (nums[maxIdx] - nums[j] >= valueDifference) {
return new int[]{maxIdx, j};
}
}
}
return new int[]{-1, -1};
}
// ========== 本地 main 自测 ==========
// 暴力对照
private static int[] brute(int[] nums, int indexDifference, int valueDifference) {
int n = nums.length;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (Math.abs(i - j) >= indexDifference
&& Math.abs(nums[i] - nums[j]) >= valueDifference) {
return new int[]{i, j};
}
}
}
return new int[]{-1, -1};
}
public static void main(String[] args) {
Solution s = new Solution();
// 官方案例
System.out.println("PASS " + Arrays.toString(s.findIndices(new int[]{5, 1, 4, 1}, 2, 4)));
System.out.println("PASS " + Arrays.toString(s.findIndices(new int[]{2, 1}, 0, 0)));
System.out.println("PASS " + Arrays.toString(s.findIndices(new int[]{1, 2, 3}, 2, 4)));
// 随机对拍(使用大值域验证 10^9 场景)
Random rnd = new Random(99);
for (int t = 0; t < 500; t++) {
int n = 1 + rnd.nextInt(12);
int[] nums = new int[n];
for (int i = 0; i < n; i++) {
nums[i] = rnd.nextInt(1_000_000_000);
}
int id = rnd.nextInt(n + 1);
int vd = rnd.nextInt(1_000_000_000);
int[] exp = brute(nums, id, vd);
int[] got = s.findIndices(nums, id, vd);
boolean expOk = exp[0] != -1
&& Math.abs(exp[0] - exp[1]) >= id
&& Math.abs(nums[exp[0]] - nums[exp[1]]) >= vd;
boolean gotOk = got[0] == -1 ? !expOk
: (Math.abs(got[0] - got[1]) >= id
&& Math.abs(nums[got[0]] - nums[got[1]]) >= vd);
if (!gotOk) {
System.out.println("FAIL t=" + t + " n=" + n
+ " id=" + id + " vd=" + vd + " got=" + Arrays.toString(got)
+ " exp=" + Arrays.toString(exp));
return;
}
}
System.out.println("ALL PASS");
}
}
复杂度分析
- 时间复杂度:O(n)
- 空间复杂度:O(1)
2906. Construct Product Matrix(构造乘积矩阵)
题目描述
给定一个下标从 0 开始、大小为 n * m 的二维整数矩阵 grid,构造乘积矩阵 p:
p[i][j] 等于除 grid[i][j] 外所有元素的乘积,对 12345 取余。
示例 1
grid = [[1,2],[3,4]] -> [[24,12],[8,6]]
示例 2
grid = [[12345],[2],[1]] -> [[2],[0],[0]]
提示:
1 <= n == grid.length <= 10^51 <= m == grid[i].length <= 10^52 <= n * m <= 10^51 <= grid[i][j] <= 10^9
解题思路
解题思路:展平 + 前缀/后缀积
将二维矩阵按行展平成长度为 N = nm 的一维数组 a。
对每个位置 i 的答案 = (a0 ...ai-1) % MOD * (ai+1 ...*aN-1) % MOD。
用前缀积 prei = a0...i-1 的模积、后缀积 sufi = ai+1...N-1 的模积,
一次遍历即可。注意题目要求模 12345(非质数),不能直接用逆元。
复杂度:
- 时间复杂度:O(n*m)
- 空间复杂度:O(n*m)
代码实现
java
import java.util.Arrays;
import java.util.Random;
class Solution {
private static final int MOD = 12345;
public int[][] constructProductMatrix(int[][] grid) {
int n = grid.length, m = grid[0].length;
int N = n * m;
long[] a = new long[N];
int idx = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
a[idx++] = grid[i][j] % MOD;
}
}
long[] suf = new long[N + 1];
suf[N] = 1;
for (int i = N - 1; i >= 0; i--) {
suf[i] = suf[i + 1] * a[i] % MOD;
}
int[][] ans = new int[n][m];
long pre = 1;
idx = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
ans[i][j] = (int) (pre * suf[idx + 1] % MOD);
pre = pre * a[idx] % MOD;
idx++;
}
}
return ans;
}
// ========== 本地 main 自测 ==========
// 暴力对照:直接按定义乘出除自身外所有元素(小数据)
private static int[][] brute(int[][] grid) {
int n = grid.length, m = grid[0].length;
int[][] ans = new int[n][m];
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
long prod = 1;
for (int x = 0; x < n; x++) {
for (int y = 0; y < m; y++) {
if (x == i && y == j) {
continue;
}
prod = prod * grid[x][y] % MOD;
}
}
ans[i][j] = (int) prod;
}
}
return ans;
}
public static void main(String[] args) {
Solution s = new Solution();
// 官方案例
check(s, new int[][]{{1, 2}, {3, 4}}, new int[][]{{24, 12}, {8, 6}});
check(s, new int[][]{{12345}, {2}, {1}}, new int[][]{{2}, {0}, {0}});
// 随机对拍
Random rnd = new Random(3);
for (int t = 0; t < 200; t++) {
int n = 1 + rnd.nextInt(4);
int m = 1 + rnd.nextInt(4);
if (n * m < 2) {
m = 2;
}
int[][] g = new int[n][m];
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
g[i][j] = 1 + rnd.nextInt(20000); // 覆盖 12345 余数场景
}
}
int[][] exp = brute(g);
int[][] got = s.constructProductMatrix(g);
boolean ok = Arrays.deepEquals(exp, got);
if (!ok) {
System.out.println("FAIL t=" + t + " grid=" + Arrays.deepToString(g)
+ " got=" + Arrays.deepToString(got) + " exp=" + Arrays.deepToString(exp));
return;
}
}
System.out.println("ALL PASS");
}
private static void check(Solution s, int[][] grid, int[][] exp) {
int[][] got = s.constructProductMatrix(grid);
System.out.println((Arrays.deepEquals(got, exp) ? "PASS" : "FAIL")
+ " grid=" + Arrays.deepToString(grid) + " got=" + Arrays.deepToString(got));
}
}
复杂度分析
- 时间复杂度:O(n*m)
- 空间复杂度:O(n*m)
2907. Maximum Profitable Triplets With Increasing Prices I(价格递增的最大利润三元组 I)
题目描述
给定两个长度为 n 的下标从 0 开始的数组 prices 和 profits。商店里有 n 件商品,
第 i 件商品价格为 prices[i]、利润为 profits[i]。必须按条件选择三件商品:
prices[i]<prices[j]<prices[k],其中i<j<k。
利润 =profits[i]+profits[j]+profits[k],返回最大利润;无法选择则返回 -1。
示例 1
prices = [10,2,3,4], profits = [100,2,7,10] -> 19
示例 2
prices = [1,2,3,4,5], profits = [1,5,3,4,6] -> 15
示例 3
prices = [4,3,2,1], profits = [33,20,19,87] -> -1
提示:
3 <= prices.length == profits.length <= 20001 <= prices[i] <= 10^61 <= profits[i] <= 10^6
解题思路
解题思路:枚举中间点 + 前缀最大利润
枚举三元组的中间下标 j,在其左侧找 pricesi < pricesj 且 profitsi 最大的 i,
右侧找 pricesk > pricesj 且 profitsk 最大的 k。
用 leftMaxj 记录 j 左侧满足价格小于 pricesj 的最大利润,rightMaxj 记录右侧
价格大于 pricesj 的最大利润。O(n^2) 预处理两侧最值后 O(n) 汇总。
(n <= 2000,O(n^2) 足够;若 n 更大可用值域树状数组/线段树优化到 O(n log V))
复杂度:
- 时间复杂度:O(n^2)
- 空间复杂度:O(n)
代码实现
java
import java.util.Arrays;
import java.util.Random;
class Solution {
public int maxProfit(int[] prices, int[] profits) {
int n = prices.length;
int[] leftMax = new int[n];
int[] rightMax = new int[n];
Arrays.fill(leftMax, -1);
Arrays.fill(rightMax, -1);
for (int j = 0; j < n; j++) {
int best = -1;
for (int i = 0; i < j; i++) {
if (prices[i] < prices[j] && profits[i] > best) {
best = profits[i];
}
}
leftMax[j] = best;
}
for (int j = 0; j < n; j++) {
int best = -1;
for (int k = j + 1; k < n; k++) {
if (prices[k] > prices[j] && profits[k] > best) {
best = profits[k];
}
}
rightMax[j] = best;
}
int ans = -1;
for (int j = 0; j < n; j++) {
if (leftMax[j] != -1 && rightMax[j] != -1) {
ans = Math.max(ans, leftMax[j] + profits[j] + rightMax[j]);
}
}
return ans;
}
// ========== 本地 main 自测 ==========
// 暴力对照
private static int brute(int[] prices, int[] profits) {
int n = prices.length;
int best = -1;
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
for (int k = j + 1; k < n; k++) {
if (prices[i] < prices[j] && prices[j] < prices[k]) {
best = Math.max(best, profits[i] + profits[j] + profits[k]);
}
}
}
}
return best;
}
public static void main(String[] args) {
Solution s = new Solution();
// 官方案例
check(s, new int[]{10, 2, 3, 4}, new int[]{100, 2, 7, 10}, 19);
check(s, new int[]{1, 2, 3, 4, 5}, new int[]{1, 5, 3, 4, 6}, 15);
check(s, new int[]{4, 3, 2, 1}, new int[]{33, 20, 19, 87}, -1);
// 随机对拍
Random rnd = new Random(13);
for (int t = 0; t < 300; t++) {
int n = 3 + rnd.nextInt(9);
int[] prices = new int[n];
int[] profits = new int[n];
for (int i = 0; i < n; i++) {
prices[i] = 1 + rnd.nextInt(20);
profits[i] = 1 + rnd.nextInt(20);
}
int exp = brute(prices, profits);
int got = s.maxProfit(prices, profits);
if (exp != got) {
System.out.println("FAIL t=" + t + " prices=" + Arrays.toString(prices)
+ " profits=" + Arrays.toString(profits) + " got=" + got + " exp=" + exp);
return;
}
}
System.out.println("ALL PASS");
}
private static void check(Solution s, int[] prices, int[] profits, int exp) {
int got = s.maxProfit(prices, profits);
System.out.println((got == exp ? "PASS" : "FAIL") + " prices=" + Arrays.toString(prices)
+ " profits=" + Arrays.toString(profits) + " got=" + got + " exp=" + exp);
}
}
复杂度分析
- 时间复杂度:O(n^2)
- 空间复杂度:O(n)
2908. Minimum Sum of Mountain Triplets I(元素和最小的山形三元组 I)
题目描述
给定下标从 0 开始的整数数组 nums。下标三元组 (i, j, k) 是山形三元组当且仅当:
i < j < knums[i]<nums[j]且nums[k]<nums[j]
返回nums中元素和最小的山形三元组的元素和;不存在则返回 -1。
示例 1
nums = [8,6,1,5,3] -> 9(三元组 (2,3,4))
示例 2
nums = [5,4,8,7,10,2] -> 13(三元组 (1,3,5))
示例 3
nums = [6,5,4,3,4,5] -> -1
提示:
3 <= nums.length <= 501 <= nums[i] <= 50
解题思路
解题思路:枚举中间点
枚举三元组中间下标 j,求两侧满足 numsi < numsj 的最小 numsi 与
numsk < numsj 的最小 numsk,三者相加更新答案。
数据规模小,O(n^2) 预处理两侧最小即可(O(n^3) 暴力也能过,这里给出更优写法)。
复杂度:
- 时间复杂度:O(n^2)
- 空间复杂度:O(n)
代码实现
java
import java.util.Arrays;
import java.util.Random;
class Solution {
public int minimumSum(int[] nums) {
int n = nums.length;
int[] leftMin = new int[n];
int[] rightMin = new int[n];
Arrays.fill(leftMin, Integer.MAX_VALUE);
Arrays.fill(rightMin, Integer.MAX_VALUE);
for (int j = 1; j < n; j++) {
int best = Integer.MAX_VALUE;
for (int i = 0; i < j; i++) {
if (nums[i] < nums[j] && nums[i] < best) {
best = nums[i];
}
}
leftMin[j] = best;
}
for (int j = 0; j < n; j++) {
int best = Integer.MAX_VALUE;
for (int k = j + 1; k < n; k++) {
if (nums[k] < nums[j] && nums[k] < best) {
best = nums[k];
}
}
rightMin[j] = best;
}
int ans = Integer.MAX_VALUE;
for (int j = 0; j < n; j++) {
if (leftMin[j] != Integer.MAX_VALUE && rightMin[j] != Integer.MAX_VALUE) {
ans = Math.min(ans, leftMin[j] + nums[j] + rightMin[j]);
}
}
return ans == Integer.MAX_VALUE ? -1 : ans;
}
// ========== 本地 main 自测 ==========
// 暴力对照
private static int brute(int[] nums) {
int n = nums.length;
int best = Integer.MAX_VALUE;
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[k] < nums[j]) {
best = Math.min(best, nums[i] + nums[j] + nums[k]);
}
}
}
}
return best == Integer.MAX_VALUE ? -1 : best;
}
public static void main(String[] args) {
Solution s = new Solution();
// 官方案例
check(s, new int[]{8, 6, 1, 5, 3}, 9);
check(s, new int[]{5, 4, 8, 7, 10, 2}, 13);
check(s, new int[]{6, 5, 4, 3, 4, 5}, -1);
// 随机对拍
Random rnd = new Random(8);
for (int t = 0; t < 500; t++) {
int n = 3 + rnd.nextInt(8);
int[] a = new int[n];
for (int i = 0; i < n; i++) {
a[i] = 1 + rnd.nextInt(20);
}
int exp = brute(a);
int got = s.minimumSum(a);
if (exp != got) {
System.out.println("FAIL t=" + t + " nums=" + Arrays.toString(a)
+ " got=" + got + " exp=" + exp);
return;
}
}
System.out.println("ALL PASS");
}
private static void check(Solution s, int[] nums, int exp) {
int got = s.minimumSum(nums);
System.out.println((got == exp ? "PASS" : "FAIL") + " nums=" + Arrays.toString(nums)
+ " got=" + got + " exp=" + exp);
}
}
复杂度分析
- 时间复杂度:O(n^2)
- 空间复杂度:O(n)
2909. Minimum Sum of Mountain Triplets II(元素和最小的山形三元组 II)
题目描述
给定下标从 0 开始的整数数组 nums。下标三元组 (i, j, k) 是山形三元组当且仅当:
i < j < knums[i]<nums[j]且nums[k]<nums[j]
返回nums中元素和最小的山形三元组的元素和;不存在则返回 -1。
示例 1
nums = [8,6,1,5,3] -> 9(三元组 (2,3,4))
示例 2
nums = [5,4,8,7,10,2] -> 13(三元组 (1,3,5))
示例 3
nums = [6,5,4,3,4,5] -> -1
提示:
3 <= nums.length <= 10^51 <= nums[i] <= 10^8
解题思路
解题思路:前缀最小 + 后缀最小
与 2908 思路相同,但 n 可达 10^5,需要 O(n):
- leftMinj 为 0, j-1 内小于 numsj 的最小值(需区分"是否小于",用严格小于判断);
更稳妥做法:prefixMinj = nums0...j-1 的全局最小,若其 < numsj 则左侧可取;
但全局最小可能不 < numsj,因此正确写法是:
leftBestj = min{ numsi | i < j 且 numsi < numsj },维护方式:
由于只需判断"存在更小的",用 TreeSet/有序结构,或者直接用双向扫描: - 先求 preMinj(前 j 个的最小值),再求 sufMinj(j+1...n-1 的最小值)。
若 preMinj < numsj 且 sufMinj < numsj,则答案候选为 preMinj + numsj + sufMinj?
不一定:preMin 可能来自左侧,sufMin 来自右侧,都满足 < numsj,三者就是合法三元组吗?
preMinj 是左侧最小值(下标 < j),sufMinj 是右侧最小值(下标 > j),
且两者均 < numsj,故 (preMinIdx, j, sufMinIdx) 是一个合法山形三元组,
且其和就是该 j 下能达到的最小和(因为固定 j 后两侧都取最小即可)。
所以做法正确:ans = min(preMinj + numsj + sufMinj)。
复杂度: - 时间复杂度:O(n)
- 空间复杂度:O(n)
代码实现
java
import java.util.Arrays;
import java.util.Random;
class Solution {
public int minimumSum(int[] nums) {
int n = nums.length;
long[] preMin = new long[n];
long[] sufMin = new long[n];
long best = Long.MAX_VALUE;
for (int i = 0; i < n; i++) {
preMin[i] = best;
best = Math.min(best, nums[i]);
}
best = Long.MAX_VALUE;
for (int i = n - 1; i >= 0; i--) {
sufMin[i] = best;
best = Math.min(best, nums[i]);
}
long ans = Long.MAX_VALUE;
for (int j = 1; j < n - 1; j++) {
if (preMin[j] < nums[j] && sufMin[j] < nums[j]) {
ans = Math.min(ans, preMin[j] + nums[j] + sufMin[j]);
}
}
return ans == Long.MAX_VALUE ? -1 : (int) ans;
}
// ========== 本地 main 自测 ==========
// 暴力对照(用 long 防溢出)
private static int brute(int[] nums) {
int n = nums.length;
long best = Long.MAX_VALUE;
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[k] < nums[j]) {
best = Math.min(best, (long) nums[i] + nums[j] + nums[k]);
}
}
}
}
return best == Long.MAX_VALUE ? -1 : (int) best;
}
public static void main(String[] args) {
Solution s = new Solution();
// 官方案例
check(s, new int[]{8, 6, 1, 5, 3}, 9);
check(s, new int[]{5, 4, 8, 7, 10, 2}, 13);
check(s, new int[]{6, 5, 4, 3, 4, 5}, -1);
// 随机对拍(值域贴合题目约束 10^8)
Random rnd = new Random(23);
for (int t = 0; t < 500; t++) {
int n = 3 + rnd.nextInt(9);
int[] a = new int[n];
for (int i = 0; i < n; i++) {
a[i] = 1 + rnd.nextInt(100_000_000);
}
int exp = brute(a);
int got = s.minimumSum(a);
if (exp != got) {
System.out.println("FAIL t=" + t + " nums=" + Arrays.toString(a)
+ " got=" + got + " exp=" + exp);
return;
}
}
System.out.println("ALL PASS");
}
private static void check(Solution s, int[] nums, int exp) {
int got = s.minimumSum(nums);
System.out.println((got == exp ? "PASS" : "FAIL") + " nums=" + Arrays.toString(nums)
+ " got=" + got + " exp=" + exp);
}
}
复杂度分析
- 时间复杂度:O(n)
- 空间复杂度:O(n)
2910. Minimum Number of Groups to Create a Valid Assignment(合法分组的最少组数)
题目描述
给定一组带编号的 balls,将它们分类到盒子里。规则:
- 同一个盒子里的球必须具有相同的编号(相同编号的球可以分到不同盒子);
- 最大的盒子最多只比最小的盒子多一个球。
返回所需的最少盒子数。
示例 1
balls = [3,2,3,2,3] -> 2(盒子 [3,3,3] 与 [2,2])
示例 2
balls = [10,10,10,3,1,1] -> 4([10]、[10,10]、[3]、[1,1])
提示:
1 <= balls.length <= 10^51 <= balls[i] <= 10^9
解题思路
解题思路:枚举盒子最小/最大容量
设每个盒子容量为 size 或 size+1(最大只比最小多 1)。若最小的盒子容量为 s,
则所有盒子容量只能是 s 或 s+1。
对每个编号出现次数 c,必须能把 c 分解为 a 个 s 和 b 个 (s+1):
c = as + b (s+1),等价于 x 个容量 s 的盒子后剩 c - xs 用 s+1 补。
枚举 s 从 1 到最小出现次数 minC(对每个 s 检查所有 c 是否可分解),可分解时
盒子数 = sum(c / (s+1) 上取整)?(不是:最优是尽量用 s+1 的盒子)
单个 c 用容量 s 与 s+1 的最少盒子数:优先用 s+1(更大的盒子),
若 c 无法恰好表示为 a s + b*(s+1),则该 s 不可行。
可行时最少盒子数 = 使 (a+b) 最小,尽量多用 s+1,等价于:
对 c 用 max(s+1) 个装会剩 r = c mod (s+1),需要把 r 个"散球"分配给某些盒子
(每个盒子容量降到 s 就多出 1 个空位),需要 r <= a(a = 盒子数)。最小盒子数
= ceil(c / (s+1)),且要求 ceil(c/(s+1)) * s <= c?推导:设盒子数 t,全部容量 s+1
时总容量 t*(s+1),需要删掉 t*(s+1) - c 个容量单位,每个盒子最多删 1(降到 s),
因此需要 t*(s+1) - c <= t,即 c >= ts。令 t = ceil(c/(s+1)) 时要求 c >= t s。
如果不行就 t+1 个盒子。综上:对固定 s,c 的最少盒子数 = 最小的 t 满足 ts <= c <= t (s+1)
(t 越大越松)。枚举 s 后取可分解全部 c 的那个 s 的总盒子数最小者。
复杂度:
- 时间复杂度:O(minC * cnt),minC <= 10^5,cnt 为编号种类数;由于 sum©=n,
实际枚举每个 s 只检查种类数,总 O(cnt * sqrt(n)) 级别可接受 - 空间复杂度:O(cnt)
代码实现
java
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
class Solution {
public int minGroupsForValidAssignment(int[] balls) {
Map<Integer, Integer> freq = new HashMap<>();
int minC = Integer.MAX_VALUE;
for (int b : balls) {
int c = freq.getOrDefault(b, 0) + 1;
freq.put(b, c);
}
for (int c : freq.values()) {
minC = Math.min(minC, c);
}
int ans = Integer.MAX_VALUE;
// 枚举最小盒子容量 s(每个盒子容量为 s 或 s+1)
for (int s = 1; s <= minC; s++) {
if (s > minC) {
break;
}
int total = 0;
boolean ok = true;
for (int c : freq.values()) {
// 找最小盒子数 t:t*s <= c <= t*(s+1)
// 等价于 t >= ceil(c/(s+1)) 且 t <= floor(c/s)
int tMin = (c + s) / (s + 1); // ceil(c / (s+1))
int tMax = c / s;
if (tMin > tMax) {
ok = false;
break;
}
total += tMin;
}
if (ok) {
ans = Math.min(ans, total);
}
}
return ans;
}
// ========== 本地 main 自测 ==========
// 暴力对照:枚举 s(最小容量)与分配做检查,与主解同思路但独立实现
// 更独立的暴力:D 整除检查------对每个 s,检查每个 c 是否可表示为 a*s + b*(s+1)
private static int brute(int[] balls) {
Map<Integer, Integer> freq = new HashMap<>();
int minC = Integer.MAX_VALUE;
for (int b : balls) {
int c = freq.getOrDefault(b, 0) + 1;
freq.put(b, c);
}
for (int c : freq.values()) {
minC = Math.min(minC, c);
}
int ans = Integer.MAX_VALUE;
for (int s = 1; s <= minC; s++) {
int total = 0;
boolean ok = true;
for (int c : freq.values()) {
int best = Integer.MAX_VALUE;
// 枚举用几个容量 s 的盒子(其余用 s+1)
for (int a = 0; a * s <= c; a++) {
int rem = c - a * s;
if (rem % (s + 1) == 0) {
best = Math.min(best, a + rem / (s + 1));
}
}
if (best == Integer.MAX_VALUE) {
ok = false;
break;
}
total += best;
}
if (ok) {
ans = Math.min(ans, total);
}
}
return ans;
}
public static void main(String[] args) {
Solution s = new Solution();
// 官方案例
check(s, new int[]{3, 2, 3, 2, 3}, 2);
check(s, new int[]{10, 10, 10, 3, 1, 1}, 4);
// 随机对拍
Random rnd = new Random(41);
for (int t = 0; t < 500; t++) {
int n = 1 + rnd.nextInt(12);
int[] a = new int[n];
for (int i = 0; i < n; i++) {
a[i] = 1 + rnd.nextInt(6);
}
int exp = brute(a);
int got = s.minGroupsForValidAssignment(a);
if (exp != got) {
System.out.println("FAIL t=" + t + " balls=" + Arrays.toString(a)
+ " got=" + got + " exp=" + exp);
return;
}
}
System.out.println("ALL PASS");
}
private static void check(Solution s, int[] balls, int exp) {
int got = s.minGroupsForValidAssignment(balls);
System.out.println((got == exp ? "PASS" : "FAIL") + " balls=" + Arrays.toString(balls)
+ " got=" + got + " exp=" + exp);
}
}
复杂度分析
- 时间复杂度:O(minC * cnt),minC <= 10^5,cnt 为编号种类数;由于 sum©=n,
- 空间复杂度:O(cnt)
2911. Minimum Changes to Make K Semi-palindromes(得到 K 个半回文串的最少修改次数)
题目描述
给定字符串 s 和整数 k,将 s 划分为 k 个非空子串,使把每个子串变为半回文串所需的
字符修改次数之和最小,返回最少修改次数。
半回文串:长度为 L 的字符串,若存在正因数 d(1 <= d < L,即 d 是 L 的因数且严格小于 L),
将字符串按位置模 d 分组(第 1 组为位置 1, 1+d, 1+2d, ...;第 2 组为位置 2, 2+d, ...),
每组都为回文串,则该串是半回文串。长度为 1 的字符串没有合法因数,不能是半回文串。
示例 1
s = "abcac", k = 2 -> 1("ab" 改 1 个字符为 "aa";"cac" 已是半回文)
示例 2
s = "abcdef", k = 2 -> 2("abc" 与 "def" 各改 1 个字符)
示例 3
s = "aabbaa", k = 3 -> 0("aa"、"bb"、"aa" 都已是半回文)
提示:
2 <= s.length <= 200- 1 <=
k<=s.length/ 2 s仅由小写英文字母组成
解题思路
解题思路:预处理代价 + 区间 DP
第一步:costij = 使 si...j 成为半回文串的最少修改次数。
对长度 len = j-i+1,枚举合法因数 d(len % d == 0 且 d < len),
对每个余数 r in [0, d),收集下标 i+r, i+r+d, i+r+2d, ... 的字符成组,
组内需回文,每组修改次数 = 组内不满足"首尾对称相等"的位置对数;
costij = min 所有 d 的修改次数。len == 1 时无合法因数,记为无穷。
第二步:DP。dppc = 把 s0...p-1(前 p 个字符)切成 c 段的最小总修改次数。
dppc = min_{last < p} dplastc-1 + costlastp-1
答案 dpnk。
复杂度:
- 时间复杂度:O(n^3 * dmax)(预处理)+ O(k * n^2)(DP),n <= 200 完全可承受
- 空间复杂度:O(n^2)
代码实现
java
import java.util.Arrays;
class Solution {
private static final int INF = Integer.MAX_VALUE / 4;
public int minimumChanges(String s, int k) {
int n = s.length();
// 预处理 cost[i][j]
int[][] cost = new int[n][n];
for (int i = 0; i < n; i++) {
for (int j = i; j < n; j++) {
cost[i][j] = calcCost(s, i, j);
}
}
// dp[p][c]
int[][] dp = new int[n + 1][k + 1];
for (int[] row : dp) {
Arrays.fill(row, INF);
}
dp[0][0] = 0;
for (int p = 1; p <= n; p++) {
for (int c = 1; c <= k && c <= p; c++) {
int best = INF;
for (int last = c - 1; last < p; last++) {
// 最后一段为 s[last..p-1],长度至少 1
if (dp[last][c - 1] < INF && cost[last][p - 1] < INF) {
best = Math.min(best, dp[last][c - 1] + cost[last][p - 1]);
}
}
dp[p][c] = best;
}
}
return dp[n][k];
}
// 使 s[l..r] 成为半回文串的最少修改次数(收集分组版本)
private int calcCost(String s, int l, int r) {
int len = r - l + 1;
int best = INF;
// 枚举因数 d(d < len 且 len % d == 0)
for (int d = 1; d < len; d++) {
if (len % d != 0) {
continue;
}
int changes = 0;
for (int rem = 0; rem < d; rem++) {
// 组内元素:下标 l+rem, l+rem+d, ...(共 g = len / d 个)
int g = len / d;
for (int t = 0; t < g / 2; t++) {
int p1 = l + rem + t * d;
int p2 = l + rem + (g - 1 - t) * d;
if (s.charAt(p1) != s.charAt(p2)) {
changes++;
}
}
}
best = Math.min(best, changes);
}
return best;
}
// ========== 本地 main 自测 ==========
// 独立暴力:cost 用"位置公式"实现,dp 结构相同(交叉验证 cost 计算)
private static int calcCostBrute(String s, int l, int r) {
int len = r - l + 1;
int best = Integer.MAX_VALUE / 2;
for (int d = 1; d < len; d++) {
if (len % d != 0) {
continue;
}
int g = len / d;
int[] cnt = new int[26];
int changes = 0;
// 每个余数一组:组内第 t 个字符在原串下标 = l + rem + t*d
for (int rem = 0; rem < d; rem++) {
// 该组长度 g,回文需第 t 与第 g-1-t 个字符相等
for (int t = 0; t < g / 2; t++) {
int p1 = l + rem + t * d;
int p2 = l + rem + (g - 1 - t) * d;
if (s.charAt(p1) != s.charAt(p2)) {
changes++;
}
}
}
best = Math.min(best, changes);
}
return best;
}
// 独立暴力 dp:与主解同结构,但 cost 用 calcCostBrute
private static int brute(String s, int k) {
int n = s.length();
int[][] cost = new int[n][n];
for (int i = 0; i < n; i++) {
for (int j = i; j < n; j++) {
cost[i][j] = calcCostBrute(s, i, j);
}
}
int[][] dp = new int[n + 1][k + 1];
for (int[] row : dp) {
Arrays.fill(row, Integer.MAX_VALUE / 4);
}
dp[0][0] = 0;
for (int p = 1; p <= n; p++) {
for (int c = 1; c <= k && c <= p; c++) {
int best = Integer.MAX_VALUE / 4;
for (int last = c - 1; last < p; last++) {
if (dp[last][c - 1] < Integer.MAX_VALUE / 4
&& cost[last][p - 1] < Integer.MAX_VALUE / 4) {
best = Math.min(best, dp[last][c - 1] + cost[last][p - 1]);
}
}
dp[p][c] = best;
}
}
return dp[n][k];
}
public static void main(String[] args) {
Solution s = new Solution();
// 官方案例
check(s, "abcac", 2, 1);
check(s, "abcdef", 2, 2);
check(s, "aabbaa", 3, 0);
// 随机对拍(n 小,k 任意)
java.util.Random rnd = new java.util.Random(17);
char[] cs = {'a', 'b', 'c', 'd'};
for (int t = 0; t < 300; t++) {
int n = 2 + rnd.nextInt(10);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; i++) {
sb.append(cs[rnd.nextInt(4)]);
}
String str = sb.toString();
int k = 1 + rnd.nextInt(n / 2);
int exp = brute(str, k);
int got = s.minimumChanges(str, k);
if (exp != got) {
System.out.println("FAIL t=" + t + " s=" + str + " k=" + k
+ " got=" + got + " exp=" + exp);
return;
}
}
System.out.println("ALL PASS");
}
private static void check(Solution s, String str, int k, int exp) {
int got = s.minimumChanges(str, k);
int exp2 = brute(str, k);
System.out.println((got == exp && got == exp2 ? "PASS" : "FAIL") + " s=" + str
+ " k=" + k + " got=" + got + " exp=" + exp + " brute=" + exp2);
}
}
复杂度分析
- 时间复杂度:O(n^3 * dmax)(预处理)+ O(k * n^2)(DP),n <= 200 完全可承受
- 空间复杂度:O(n^2)
2912. Number of Ways to Reach Destination in the Grid(在网格上移动到目的地的方法数)
题目描述
给定 n*m 的网格(下标从 1 开始),整数 k,source = `sx`, `sy`,dest = `dx`, `dy`。
每次移动:从 `x1`,`y1` 可移动到 `x2`,`y2`,只要 x1``x2 或 y1``y2,且不能停在原地。
求从 source 恰好移动 k 次到 dest 的方法数,对 1e9+7 取模。
示例 1
n=3, m=2, k=2, source=[1,1], dest=[2,2] -> 2
示例 2
n=3, m=4, k=3, source=[1,2], dest=[2,3] -> 9
提示:
2 <= n, m <= 10^91 <= k <= 10^5source.length == dest.length == 2
解题思路
解题思路:拆解成行/列两个独立维度的递推
观察移动规则:一次移动要么在同一行换列(x 不变),要么在同一列换行(y 不变)。
把"行号 x 是否等于 dx"与"列号 y 是否等于 dy"分别看成两个布尔状态,
每一步 (x,y) 的状态变化只有两种:
- 保持 x 不变:行的匹配状态不变;
- 保持 y 不变:列的匹配状态不变。
由于一次移动中行和列只有一个可能改变,无法直接拆成两个独立马尔可夫链
(一次移动必改其一),但可以建立 4 状态(行匹配?,列匹配?)转移矩阵加速:
状态 0:(x!=dx, y!=dy) 1:(xdx, y!=dy) 2:(x!=dx, ydy) 3:(xdx, ydy)
从任意 (x,y): - 若 x != dx,则可选行方向换到 dx(1 种),或换到其他 n-2 行(n-2 种);
- 若 x == dx,换行只会离开 dx(n-1 种)。
列同理(m-1 或 1 / m-2)。
任意状态经过一次移动后:行状态与列状态中恰有一个翻转、另一个保持原匹配关系。
即从 (rowState, colState) 一步转移到 (rowState', colState'),
其中 rowState' != rowState 或 colState' != colState 恰有一个成立: - 保持行、改列:改列保持"y == dy?"被翻转的概率。
转移计数:
保持 x(行状态不变):
列状态 ydy -> y!=dy:1 种(y 换到 dy)或 0;y!=dy -> ydy:1 种;
y!=dy -> y!=dy:m-2 种;ydy -> ydy:m-1 种。
保持 y(列状态不变):
行状态 xdx -> x!=dx:n-1 种;x!=dx -> xdx:1 种;
x!=dx -> x!=dx:n-2 种。
于是 4 状态矩阵可写,用矩阵快速幂 O(4^3 log k) 求出 k 步后状态 3 的方案数。
复杂度: - 时间复杂度:O(4^3 log k)
- 空间复杂度:O(4^2)
代码实现
java
import java.util.Arrays;
import java.util.Random;
class Solution {
private static final int MOD = 1_000_000_007;
private static final long[][] UNIT = {{1, 0, 0, 0}, {0, 1, 0, 0}, {0, 0, 1, 0}, {0, 0, 0, 1}};
// 状态:idx = (rowMatch ? 1 : 0) * 2 + (colMatch ? 1 : 0)
// rowMatch = (x==dx),colMatch = (y==dy)
public int numberOfWays(int n, int m, int k, int[] source, int[] dest) {
long N = n, M = m;
long[][] trans = new long[4][4];
for (int rowM = 0; rowM < 2; rowM++) {
for (int colM = 0; colM < 2; colM++) {
int from = rowM * 2 + colM;
// ---- 保持 x 不变(改列):rowMatch 不变,y 变 ----
// y 从 !colM -> colM 有 1 种(移向 dy);y!=dy -> y!=dy 有 M-2 种;
// y == dy -> y!=dy 有 M-1 种(全离开 dy),不可能停在 dy
if (colM == 0) {
add(trans, from, rowM * 2 + 1, 1); // -> (rowM, 1)
add(trans, from, rowM * 2 + 0, M - 2); // -> (rowM, 0)
} else {
add(trans, from, rowM * 2 + 0, M - 1); // -> (rowM, 0)
}
// ---- 保持 y 不变(改行):colMatch 不变,x 变 ----
// x 从 !rowM -> rowM 有 1 种(移向 dx);x!=dx -> x!=dx 有 N-2 种;
// x == dx -> x!=dx 有 N-1 种(全离开 dx)
if (rowM == 0) {
add(trans, from, 1 * 2 + colM, 1); // -> (1, colM)
add(trans, from, 0 * 2 + colM, N - 2); // -> (0, colM)
} else {
add(trans, from, 0 * 2 + colM, N - 1); // -> (0, colM)
}
}
}
long[][] mat = pow(trans, k);
int s = (source[0] == dest[0] ? 1 : 0) * 2 + (source[1] == dest[1] ? 1 : 0);
int e = 3; // (dx, dy)
return (int) mat[s][e];
}
private void add(long[][] t, int from, int to, long cnt) {
t[from][to] = (t[from][to] + cnt) % MOD;
}
private long[][] mul(long[][] a, long[][] b) {
long[][] c = new long[4][4];
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
long s = 0;
for (int t = 0; t < 4; t++) {
s = (s + a[i][t] * b[t][j]) % MOD;
}
c[i][j] = s;
}
}
return c;
}
private long[][] pow(long[][] a, int k) {
long[][] r = UNIT;
while (k > 0) {
if ((k & 1) == 1) {
r = mul(r, a);
}
a = mul(a, a);
k >>= 1;
}
return r;
}
// ========== 本地 main 自测 ==========
// 暴力对照:网格 BFS/DP(小数据)
private static int brute(int n, int m, int k, int[] src, int[] dst) {
int[][][] dp = new int[k + 1][n][m];
dp[0][src[0] - 1][src[1] - 1] = 1;
for (int step = 0; step < k; step++) {
for (int x = 0; x < n; x++) {
for (int y = 0; y < m; y++) {
if (dp[step][x][y] == 0) {
continue;
}
int cur = dp[step][x][y];
for (int xx = 0; xx < n; xx++) {
if (xx == x) {
continue;
}
dp[step + 1][xx][y] = (dp[step + 1][xx][y] + cur) % MOD;
}
for (int yy = 0; yy < m; yy++) {
if (yy == y) {
continue;
}
dp[step + 1][x][yy] = (dp[step + 1][x][yy] + cur) % MOD;
}
}
}
}
return dp[k][dst[0] - 1][dst[1] - 1];
}
public static void main(String[] args) {
Solution s = new Solution();
// 官方案例
check(s, 3, 2, 2, new int[]{1, 1}, new int[]{2, 2}, 2);
check(s, 3, 4, 3, new int[]{1, 2}, new int[]{2, 3}, 9);
// 随机对拍
Random rnd = new Random(5);
for (int t = 0; t < 200; t++) {
int n = 2 + rnd.nextInt(4);
int m = 2 + rnd.nextInt(4);
int k = 1 + rnd.nextInt(6);
int[] src = {1 + rnd.nextInt(n), 1 + rnd.nextInt(m)};
int[] dst = {1 + rnd.nextInt(n), 1 + rnd.nextInt(m)};
int exp = brute(n, m, k, src, dst);
int got = s.numberOfWays(n, m, k, src, dst);
if (exp != got) {
System.out.println("FAIL t=" + t + " n=" + n + " m=" + m + " k=" + k
+ " src=" + Arrays.toString(src) + " dst=" + Arrays.toString(dst)
+ " got=" + got + " exp=" + exp);
return;
}
}
System.out.println("ALL PASS");
}
private static void check(Solution s, int n, int m, int k, int[] src, int[] dst, int exp) {
int got = s.numberOfWays(n, m, k, src, dst);
int exp2 = brute(n, m, k, src, dst);
System.out.println((got == exp && got == exp2 ? "PASS" : "FAIL") + " n=" + n + " m=" + m
+ " k=" + k + " src=" + Arrays.toString(src) + " dst=" + Arrays.toString(dst)
+ " got=" + got + " exp=" + exp + " brute=" + exp2);
}
}
复杂度分析
- 时间复杂度:O(4^3 log k)
- 空间复杂度:O(4^2)
2913. Subarrays Distinct Element Sum of Squares I(子数组不同元素数目的平方和 I)
题目描述
给定下标从 0 开始的整数数组 nums。对每个非空子数组 nums`i`...`j`,定义其不同计数为
子数组中不同值的数目。返回所有子数组不同计数的平方和。
示例 1
nums = [1,2,1] -> 15
示例 2
nums = [2,2] -> 3
提示:
1 <= nums.length <= 1001 <= nums[i] <= 100
解题思路
解题思路:枚举起点 + 增量维护
对每个起点 i 向右扩展终点 j,用布尔数组/集合维护当前子数组的不同元素个数 c,
每加入一个新元素(首次出现)时 c 增 1,把 c^2 累加进答案。
由于 n <= 100,O(n^2) 足够;这也是 2916(II 题)的暴力对照思路。
复杂度:
- 时间复杂度:O(n^2)
- 空间复杂度:O(maxV)
代码实现
java
import java.util.Arrays;
import java.util.List;
import java.util.Random;
class Solution {
public int sumCounts(List<Integer> nums) {
int n = nums.size();
int ans = 0;
boolean[] seen = new boolean[101];
for (int i = 0; i < n; i++) {
Arrays.fill(seen, false);
int c = 0;
for (int j = i; j < n; j++) {
int v = nums.get(j);
if (!seen[v]) {
seen[v] = true;
c++;
}
ans += c * c;
}
}
return ans;
}
// ========== 本地 main 自测 ==========
// 独立暴力对照:用 Set 统计每个子数组的不同数
private static int brute(List<Integer> nums) {
int n = nums.size();
int ans = 0;
for (int i = 0; i < n; i++) {
java.util.Set<Integer> set = new java.util.HashSet<>();
for (int j = i; j < n; j++) {
set.add(nums.get(j));
ans += set.size() * set.size();
}
}
return ans;
}
public static void main(String[] args) {
Solution s = new Solution();
// 官方案例
check(s, Arrays.asList(1, 2, 1), 15);
check(s, Arrays.asList(2, 2), 3);
// 随机对拍
Random rnd = new Random(6);
for (int t = 0; t < 300; t++) {
int n = 1 + rnd.nextInt(10);
List<Integer> a = new java.util.ArrayList<>();
for (int i = 0; i < n; i++) {
a.add(1 + rnd.nextInt(6));
}
int exp = brute(a);
int got = s.sumCounts(a);
if (exp != got) {
System.out.println("FAIL t=" + t + " nums=" + a + " got=" + got + " exp=" + exp);
return;
}
}
System.out.println("ALL PASS");
}
private static void check(Solution s, List<Integer> nums, int exp) {
int got = s.sumCounts(nums);
int exp2 = brute(nums);
System.out.println((got == exp && got == exp2 ? "PASS" : "FAIL") + " nums=" + nums
+ " got=" + got + " exp=" + exp + " brute=" + exp2);
}
}
复杂度分析
- 时间复杂度:O(n^2)
- 空间复杂度:O(maxV)
2914. Minimum Number of Changes to Make Binary String Beautiful(使二进制字符串变美丽的最少修改次数)
题目描述
给定长度为偶数、下标从 0 开始的二进制字符串 s。
如果 s 能分割成若干子字符串,且每个子字符串长度都是偶数、只包含 1 或只包含 0,
则称 s 是美丽的。可以把任一字符改成 0 或 1,返回使 s 美丽的最少修改次数。
示例 1
s = "1001" -> 2(改为 "1100" 或 "0011")
示例 2
s = "10" -> 1(改为 "11" 或 "00")
示例 3
s = "0000" -> 0
提示:
2 <= s.length <= 10^5s的长度为偶数
解题思路
解题思路:两两配对
贪心:把 s 每两个字符分为一组。若一组内两个字符相同,则不改动即可成为
长度 2 的合法块;若不同,则必须改其中一个(1 次)使其相同。
可以证明不存在更优的跨组重组方案:任何合法分割的每组长度为偶数且内部字符全同,
把每个长度 > 2 的块再切成 2 字符小块仍然合法,因此只需考虑 2 字符分组。
所以答案 = 相邻(i, i+1)不同的对数。
复杂度:
- 时间复杂度:O(n)
- 空间复杂度:O(1)
代码实现
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
class Solution {
public int minChanges(String s) {
int n = s.length();
int ans = 0;
for (int i = 0; i < n; i += 2) {
if (s.charAt(i) != s.charAt(i + 1)) {
ans++;
}
}
return ans;
}
// ========== 本地 main 自测 ==========
// 独立暴力:枚举所有合法分割进行最小修改(小数据 BFS/DP)
// dp[i] = 使 s[0..i-1] 美丽的最小修改次数,i 为偶数
private static int brute(String s) {
int n = s.length();
int[] dp = new int[n + 1];
Arrays.fill(dp, Integer.MAX_VALUE / 2);
dp[0] = 0;
for (int i = 2; i <= n; i += 2) {
// 最后一块 s[j..i-1],长度 i-j 为偶数
for (int j = 0; j < i; j += 2) {
int len = i - j;
// 将 s[j..i-1] 全改为 '0' 或全改为 '1' 的代价
int c0 = 0, c1 = 0;
for (int t = j; t < i; t++) {
if (s.charAt(t) == '0') {
c1++;
} else {
c0++;
}
}
int cost = Math.min(c0, c1);
dp[i] = Math.min(dp[i], dp[j] + cost);
}
}
return dp[n];
}
public static void main(String[] args) {
Solution s = new Solution();
// 官方案例
check(s, "1001", 2);
check(s, "10", 1);
check(s, "0000", 0);
// 随机对拍
java.util.Random rnd = new java.util.Random(9);
for (int t = 0; t < 300; t++) {
int n = 2 * (1 + rnd.nextInt(6));
StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; i++) {
sb.append(rnd.nextBoolean() ? '1' : '0');
}
String str = sb.toString();
int exp = brute(str);
int got = s.minChanges(str);
if (exp != got) {
System.out.println("FAIL t=" + t + " s=" + str + " got=" + got + " exp=" + exp);
return;
}
}
System.out.println("ALL PASS");
}
private static void check(Solution s, String str, int exp) {
int got = s.minChanges(str);
int exp2 = brute(str);
System.out.println((got == exp && got == exp2 ? "PASS" : "FAIL") + " s=" + str
+ " got=" + got + " exp=" + exp + " brute=" + exp2);
}
}
复杂度分析
- 时间复杂度:O(n)
- 空间复杂度:O(1)
2915. Length of the Longest Subsequence That Sums to Target(和为目标值的最长子序列的长度)
题目描述
给定下标从 0 开始的整数数组 nums 和整数 target。返回和为 target 的 nums 子序列中
长度的最大值;若不存在返回 -1。子序列保持原顺序(背包问题中顺序不影响选择集合)。
示例 1
nums = [1,2,3,4,5], target = 9 -> 3([1,3,5] 或 [2,3,4])
示例 2
nums = [4,1,3,2,1,5], target = 7 -> 4([1,3,2,1])
示例 3
nums = [1,1,5,4,5], target = 3 -> -1
提示:
1 <= nums.length <= 10001 <= nums[i] <= 10001 <= target <= 1000
解题思路
解题思路:0-1 背包求最长
dps = 凑出和 s 的子序列最大长度,初始 dp0 = 0,其余为 -1。
对每个 numsi,倒序更新 s:若 dps - v != -1,
则 dps = max(dps, dps - v + 1)。
答案 dptarget。
复杂度:
- 时间复杂度:O(n * target)
- 空间复杂度:O(target)
代码实现
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
class Solution {
public int lengthOfLongestSubsequence(List<Integer> nums, int target) {
int[] dp = new int[target + 1];
Arrays.fill(dp, -1);
dp[0] = 0;
for (int v : nums) {
if (v > target) {
continue;
}
for (int s = target; s >= v; s--) {
if (dp[s - v] != -1) {
dp[s] = Math.max(dp[s], dp[s - v] + 1);
}
}
}
return dp[target];
}
// ========== 本地 main 自测 ==========
// 暴力/记忆化对照:枚举所有子序列(小数据)
private static int brute(List<Integer> nums, int target) {
int n = nums.size();
int best = -1;
for (int mask = 0; mask < (1 << n); mask++) {
int sum = 0, cnt = 0;
for (int i = 0; i < n; i++) {
if ((mask >> i & 1) != 0) {
sum += nums.get(i);
cnt++;
}
}
if (sum == target) {
best = Math.max(best, cnt);
}
}
return best;
}
public static void main(String[] args) {
Solution s = new Solution();
// 官方案例
check(s, Arrays.asList(1, 2, 3, 4, 5), 9, 3);
check(s, Arrays.asList(4, 1, 3, 2, 1, 5), 7, 4);
check(s, Arrays.asList(1, 1, 5, 4, 5), 3, -1);
// 随机对拍
Random rnd = new Random(31);
for (int t = 0; t < 300; t++) {
int n = 1 + rnd.nextInt(10);
List<Integer> a = new ArrayList<>();
for (int i = 0; i < n; i++) {
a.add(1 + rnd.nextInt(8));
}
int target = 1 + rnd.nextInt(15);
int exp = brute(a, target);
int got = s.lengthOfLongestSubsequence(a, target);
if (exp != got) {
System.out.println("FAIL t=" + t + " nums=" + a + " target=" + target
+ " got=" + got + " exp=" + exp);
return;
}
}
System.out.println("ALL PASS");
}
private static void check(Solution s, List<Integer> nums, int target, int exp) {
int got = s.lengthOfLongestSubsequence(nums, target);
int exp2 = brute(nums, target);
System.out.println((got == exp && got == exp2 ? "PASS" : "FAIL") + " nums=" + nums
+ " target=" + target + " got=" + got + " exp=" + exp + " brute=" + exp2);
}
}
复杂度分析
- 时间复杂度:O(n * target)
- 空间复杂度:O(target)
2916. Subarrays Distinct Element Sum of Squares II(子数组不同元素数目的平方和 II)
题目描述
给定下标从 0 开始的整数数组 nums。对每个非空子数组 nums`i`...`j`,定义其不同计数为
子数组中不同值的数目。返回所有子数组不同计数的平方和,对 1e9+7 取模。
示例 1
nums = [1,2,1] -> 15
示例 2
nums = [2,2] -> 3
提示:
1 <= nums.length <= 10^51 <= nums[i] <= 10^5
解题思路
解题思路:线段树 + 增量维护
遍历右端点 i。对每个起点 j,记 diffj = 子数组 numsj...i-1 的不同计数。
当新元素 v = numsi 加入时,只有起点 j 满足其左边界在 lastv+1...i 的子数组
会"新增一个不同元素",即 diffj += 1(j in last\[v+1, i]),其余不变。
用线段树维护 diff 数组:节点保存 sumD = sum diffj 与 sumSq = sum diffj^2,
支持区间加 1:sumSq += 2*sumD + len;sumD += len。
加分后以 i 结尾的所有子数组的平方和 = 当前全局 sumSq(尚未更新的起点 diff 为 0)。
累加即可。复杂度 O(n log n)。
复杂度:
- 时间复杂度:O(n log n)
- 空间复杂度:O(n)
代码实现
java
import java.util.Arrays;
import java.util.Random;
class Solution {
private static final int MOD = 1_000_000_007;
public int sumCounts(int[] nums) {
int n = nums.length;
SegTree seg = new SegTree(n);
int[] last = new int[100_001];
Arrays.fill(last, -1);
long ans = 0;
for (int i = 0; i < n; i++) {
int v = nums[i];
int l = last[v] + 1;
int r = i;
if (l <= r) {
seg.add(l, r, 1);
}
last[v] = i;
ans = (ans + seg.sumSq()) % MOD;
}
return (int) ans;
}
// 线段树:区间加 1,维护 sumD 与 sumSq,查询全局 sumSq
static class SegTree {
int n;
long[] sumD, sumSq, lazy;
SegTree(int n) {
this.n = n;
sumD = new long[4 * n];
sumSq = new long[4 * n];
lazy = new long[4 * n];
}
void add(int l, int r, int v) {
add(1, 0, n - 1, l, r, v);
}
private void add(int node, int nl, int nr, int l, int r, int v) {
if (l <= nl && nr <= r) {
apply(node, nl, nr, v);
return;
}
push(node, nl, nr);
int mid = (nl + nr) >>> 1;
if (l <= mid) {
add(node * 2, nl, mid, l, r, v);
}
if (r > mid) {
add(node * 2 + 1, mid + 1, nr, l, r, v);
}
sumD[node] = sumD[node * 2] + sumD[node * 2 + 1];
sumSq[node] = sumSq[node * 2] + sumSq[node * 2 + 1];
}
private void apply(int node, int nl, int nr, long v) {
sumSq[node] = (sumSq[node] + 2 * v * sumD[node] % MOD + v * v % MOD * (nr - nl + 1)) % MOD;
sumD[node] = (sumD[node] + v * (nr - nl + 1)) % MOD;
lazy[node] = (lazy[node] + v) % MOD;
}
private void push(int node, int nl, int nr) {
if (lazy[node] != 0) {
int mid = (nl + nr) >>> 1;
apply(node * 2, nl, mid, lazy[node]);
apply(node * 2 + 1, mid + 1, nr, lazy[node]);
lazy[node] = 0;
}
}
long sumSq() {
return sumSq[1];
}
}
// ========== 本地 main 自测 ==========
// 独立暴力对照
private static int brute(int[] nums) {
int n = nums.length;
long ans = 0;
for (int i = 0; i < n; i++) {
java.util.Set<Integer> set = new java.util.HashSet<>();
for (int j = i; j < n; j++) {
set.add(nums[j]);
ans = (ans + (long) set.size() * set.size()) % MOD;
}
}
return (int) ans;
}
public static void main(String[] args) {
Solution s = new Solution();
// 官方案例
check(s, new int[]{1, 2, 1}, 15);
check(s, new int[]{2, 2}, 3);
check(s, new int[]{1, 2, 3}, 20); // [1],[2],[3],[1,2],[2,3],[1,2,3] -> 1+1+1+4+4+9=20
// 随机对拍
Random rnd = new Random(11);
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] = 1 + rnd.nextInt(5);
}
int exp = brute(a);
int got = s.sumCounts(a);
if (exp != got) {
System.out.println("FAIL t=" + t + " nums=" + Arrays.toString(a)
+ " got=" + got + " exp=" + exp);
return;
}
}
System.out.println("ALL PASS");
}
private static void check(Solution s, int[] nums, int exp) {
int got = s.sumCounts(nums);
int exp2 = brute(nums);
System.out.println((got == exp && got == exp2 ? "PASS" : "FAIL") + " nums="
+ Arrays.toString(nums) + " got=" + got + " exp=" + exp + " brute=" + exp2);
}
}
复杂度分析
- 时间复杂度:O(n log n)
- 空间复杂度:O(n)
2917. Find the K-or of an Array(找出数组中的 K-or 值)
题目描述
给定整数数组 nums 和整数 k。K-or 操作定义:若第 i 位在 nums 中至少 k 个元素的该位上
值为 1,则 K-or 结果的第 i 位为 1。返回 nums 的 K-or 值。
示例 1
nums = [7,12,9,8,9,15], k = 4 -> 9(二进制 1001)
示例 2
nums = [2,12,1,11,4,5], k = 6 -> 0(无任何位出现 6 次)
提示:
1 <= nums.length <= 500 <= nums[i] < 2^311 <= k <= nums.length
解题思路
解题思路:逐位统计
对 0...30 每一位统计 nums 中该位为 1 的元素个数 cnt,
若 cnt >= k 则把 1<<bit 加入答案。
复杂度:
- 时间复杂度:O(n * 31)
- 空间复杂度:O(1)
代码实现
java
import java.util.Arrays;
import java.util.Random;
class Solution {
public int findKOr(int[] nums, int k) {
int ans = 0;
for (int b = 0; b < 31; b++) {
int cnt = 0;
for (int v : nums) {
if (((v >> b) & 1) != 0) {
cnt++;
}
}
if (cnt >= k) {
ans |= 1 << b;
}
}
return ans;
}
// ========== 本地 main 自测 ==========
// 独立暴力:按定义逐位验证
private static int brute(int[] nums, int k) {
int ans = 0;
for (int b = 0; b < 31; b++) {
int cnt = 0;
for (int v : nums) {
if (v < 0) {
continue;
}
if (((v >>> b) & 1) != 0) {
cnt++;
}
}
if (cnt >= k) {
ans |= 1 << b;
}
}
return ans;
}
public static void main(String[] args) {
Solution s = new Solution();
// 官方案例
check(s, new int[]{7, 12, 9, 8, 9, 15}, 4, 9);
check(s, new int[]{2, 12, 1, 11, 4, 5}, 6, 0);
check(s, new int[]{10, 8, 8, 9, 3, 7}, 2, 11); // 位3(4个)、位1(3个)、位0(3个) -> 8+2+1
// 随机对拍
Random rnd = new Random(15);
for (int t = 0; t < 500; t++) {
int n = 1 + rnd.nextInt(12);
int[] a = new int[n];
for (int i = 0; i < n; i++) {
a[i] = rnd.nextInt(1024);
}
int k = 1 + rnd.nextInt(n);
int exp = brute(a, k);
int got = s.findKOr(a, k);
if (exp != got) {
System.out.println("FAIL t=" + t + " nums=" + Arrays.toString(a)
+ " k=" + k + " got=" + got + " exp=" + exp);
return;
}
}
System.out.println("ALL PASS");
}
private static void check(Solution s, int[] nums, int k, int exp) {
int got = s.findKOr(nums, k);
int exp2 = brute(nums, k);
System.out.println((got == exp && got == exp2 ? "PASS" : "FAIL") + " nums="
+ Arrays.toString(nums) + " k=" + k + " got=" + got + " exp=" + exp
+ " brute=" + exp2);
}
}
复杂度分析
- 时间复杂度:O(n * 31)
- 空间复杂度:O(1)
2918. Minimum Equal Sum of Two Arrays After Replacing Zeros(数组的最小相等和)
题目描述
给定两个由正整数和 0 组成的数组 nums1 和 nums2。必须把两个数组中的所有 0 替换为
严格正整数,且满足两数组元素和相等。返回最小的相等和;若无法相等返回 -1。
示例 1
nums1 = [3,2,0,1,0], nums2 = [6,5,0] -> 12
示例 2
nums1 = [2,0,2,0], nums2 = [1,4] -> -1
提示:
1 <= nums1.length, nums2.length <= 10^50 <= nums1[i], nums2[i] <= 10^6
解题思路
解题思路:贪心
设 sum1 为 nums1 非 0 和、z1 为 nums1 中 0 的个数,sum2、z2 同理。
每个 0 至少替换为 1,所以数组 1 的和最小为 min1 = sum1 + z1,最大无上限;
数组 2 同理。两数组可达的相等和 = 两区间 [min1, +inf) 与 [min2, +inf) 的交集,
最小者 = max(min1, min2)。若某数组没有 0,其和固定,则要求另一数组可达该固定值:
即 max(min1, min2) 必须等于固定和(当且仅当固定和 >= 另一数组的 min)。
复杂度:
- 时间复杂度:O(n + m)
- 空间复杂度:O(1)
代码实现
java
import java.util.Arrays;
import java.util.Random;
class Solution {
public long minSum(int[] nums1, int[] nums2) {
long sum1 = 0, sum2 = 0;
int z1 = 0, z2 = 0;
for (int v : nums1) {
sum1 += v;
if (v == 0) {
z1++;
}
}
for (int v : nums2) {
sum2 += v;
if (v == 0) {
z2++;
}
}
long min1 = sum1 + z1;
long min2 = sum2 + z2;
if (z1 == 0 && z2 == 0) {
return sum1 == sum2 ? sum1 : -1;
}
if (z1 == 0) {
// 数组 1 固定为 sum1;数组 2 需能到 sum1
return sum1 >= min2 ? sum1 : -1;
}
if (z2 == 0) {
return sum2 >= min1 ? sum2 : -1;
}
return Math.max(min1, min2);
}
// ========== 本地 main 自测 ==========
// 独立暴力:枚举 0 替换(小数据)
private static long brute(int[] n1, int[] n2) {
long best = -1;
// 枚举 nums1 的 0 替换为 1..30,nums2 同理(小数据穷举和值)
java.util.Set<Long> s1 = new java.util.HashSet<>();
java.util.Set<Long> s2 = new java.util.HashSet<>();
gen(n1, 0, 0, s1);
gen(n2, 0, 0, s2);
for (long a : s1) {
if (s2.contains(a)) {
if (best == -1 || a < best) {
best = a;
}
}
}
return best;
}
private static void gen(int[] arr, int idx, long sum, java.util.Set<Long> out) {
if (idx == arr.length) {
out.add(sum);
return;
}
if (arr[idx] != 0) {
gen(arr, idx + 1, sum + arr[idx], out);
} else {
for (int v = 1; v <= 20; v++) {
gen(arr, idx + 1, sum + v, out);
}
}
}
public static void main(String[] args) {
Solution s = new Solution();
// 官方案例
check(s, new int[]{3, 2, 0, 1, 0}, new int[]{6, 5, 0}, 12);
check(s, new int[]{2, 0, 2, 0}, new int[]{1, 4}, -1);
// 随机对拍(小数据,0 较少)
Random rnd = new Random(27);
for (int t = 0; t < 300; t++) {
int n = 1 + rnd.nextInt(5);
int m = 1 + rnd.nextInt(5);
int[] a = new int[n], b = new int[m];
for (int i = 0; i < n; i++) {
a[i] = rnd.nextInt(3) == 0 ? 0 : 1 + rnd.nextInt(6);
}
for (int i = 0; i < m; i++) {
b[i] = rnd.nextInt(3) == 0 ? 0 : 1 + rnd.nextInt(6);
}
long exp = brute(a, b);
long got = s.minSum(a, b);
if (exp != got) {
System.out.println("FAIL t=" + t + " n1=" + Arrays.toString(a)
+ " n2=" + Arrays.toString(b) + " got=" + got + " exp=" + exp);
return;
}
}
System.out.println("ALL PASS");
}
private static void check(Solution s, int[] n1, int[] n2, long exp) {
long got = s.minSum(n1, n2);
long exp2 = brute(n1, n2);
System.out.println((got == exp && got == exp2 ? "PASS" : "FAIL") + " n1="
+ Arrays.toString(n1) + " n2=" + Arrays.toString(n2) + " got=" + got
+ " exp=" + exp + " brute=" + exp2);
}
}
复杂度分析
- 时间复杂度:O(n + m)
- 空间复杂度:O(1)
2919. Minimum Increment Operations to Make Array Beautiful(使数组变美的最小增量运算数)
题目描述
给定下标从 0 开始、长度为 n 的整数数组 nums 和整数 k。可以任意次执行:选择下标 i,
将 nums[i] 加 1。若数组中任何长度 >= 3 的子数组的最大元素都 >= k,则数组美丽。
返回使数组美丽的最小递增运算数。
示例 1
nums = [2,3,0,0,2], k = 4 -> 3
示例 2
nums = [0,1,3,3], k = 5 -> 2
示例 3
nums = [1,1,2], k = 1 -> 0
提示:
3 <= n == nums.length <= 10^50 <= nums[i] <= 10^90 <= k <= 10^9
解题思路
解题思路:滚动 DP + 最近升位置距离
美丽数组 \u21d4 任意连续 3 个位置至少有一个元素 >= k(更长的子数组必然包含长度 3 的子段)。
对每个位置决策:把它"养"到 >= k(代价 max(0, k - numsi))或不养(代价 0)。
用状态 d 表示最近一个被养的位置与当前位置的距离(1/2/3):
- 养:nxt1 = min_d curd + cost_i;
- 不养:距离加 1,仅当 d+1 <= 3 合法(距离到 4 意味着连续 3 个都没养)。
处理完所有位置后,距离 1/2/3 都合法(最右窗口已被最后元素覆盖),答案 = min(cur1...3)。
时间复杂度 O(n),空间 O(1)。
复杂度: - 时间复杂度:O(n)
- 空间复杂度:O(1)
代码实现
java
import java.util.Arrays;
import java.util.Random;
class Solution {
public long minIncrementOperations(int[] nums, int k) {
int n = nums.length;
long inf = Long.MAX_VALUE / 4;
long[] cur = new long[4];
Arrays.fill(cur, inf);
// 处理位置 0
cur[1] = Math.max(0, (long) k - nums[0]); // 养
cur[2] = 0; // 不养(距离变为 2)
for (int i = 1; i < n; i++) {
long c = Math.max(0, (long) k - nums[i]);
long[] nxt = new long[4];
Arrays.fill(nxt, inf);
for (int d = 1; d <= 3; d++) {
if (cur[d] >= inf) {
continue;
}
// 养 -> 距离变为 1
if (cur[d] + c < nxt[1]) {
nxt[1] = cur[d] + c;
}
// 不养 -> 距离 +1(<= 3)
if (d + 1 <= 3 && cur[d] < nxt[d + 1]) {
nxt[d + 1] = cur[d];
}
}
cur = nxt;
}
long ans = inf;
for (int d = 1; d <= 3; d++) {
ans = Math.min(ans, cur[d]);
}
return ans;
}
// ========== 本地 main 自测 ==========
// 独立暴力:2^n 枚举每个位置养/不养,逐窗口验证
private static long brute(int[] nums, int k) {
int n = nums.length;
long best = Long.MAX_VALUE;
for (int mask = 0; mask < (1 << n); mask++) {
long cost = 0;
for (int i = 0; i < n; i++) {
if ((mask >> i & 1) == 1) {
cost += Math.max(0, (long) k - nums[i]);
}
}
// 验证所有长度 >= 3 的窗口
boolean ok = true;
for (int l = 0; l < n && ok; l++) {
for (int r = l + 2; r < n && ok; r++) {
boolean has = false;
for (int t = l; t <= r; t++) {
long v = nums[t];
if ((mask >> t & 1) == 1) {
v = k;
}
if (v >= k) {
has = true;
break;
}
}
if (!has) {
ok = false;
}
}
}
if (ok) {
best = Math.min(best, cost);
}
}
return best;
}
public static void main(String[] args) {
Solution s = new Solution();
// 官方案例
check(s, new int[]{2, 3, 0, 0, 2}, 4, 3);
check(s, new int[]{0, 1, 3, 3}, 5, 2);
check(s, new int[]{1, 1, 2}, 1, 0);
// 随机对拍
Random rnd = new Random(19);
for (int t = 0; t < 300; t++) {
int n = 3 + rnd.nextInt(8);
int[] a = new int[n];
for (int i = 0; i < n; i++) {
a[i] = rnd.nextInt(10);
}
int k = rnd.nextInt(10);
long exp = brute(a, k);
long got = s.minIncrementOperations(a, k);
if (exp != got) {
System.out.println("FAIL t=" + t + " nums=" + Arrays.toString(a)
+ " k=" + k + " got=" + got + " exp=" + exp);
return;
}
}
System.out.println("ALL PASS");
}
private static void check(Solution s, int[] nums, int k, long exp) {
long got = s.minIncrementOperations(nums, k);
long exp2 = brute(nums, k);
System.out.println((got == exp && got == exp2 ? "PASS" : "FAIL") + " nums="
+ Arrays.toString(nums) + " k=" + k + " got=" + got + " exp=" + exp
+ " brute=" + exp2);
}
}
复杂度分析
时间复杂度 O(n),空间 O(1)。
- 时间复杂度:O(n)
- 空间复杂度:O(1)
2920. Maximum Points After Collecting Coins From All Nodes(收集所有金币可获得的最大积分)
题目描述
有 n 个节点的无向树,以 0 为根。edges 为树边,coins[i] 为节点 i 的金币数,k 为整数。
必须从根开始收集所有金币(先祖先后子孙)。节点 i 的金币收集方式二选一:
- 收集全部,得到
coins[i]-k点积分(可为负); - 收集 floor(
coins[i]/ 2) 点积分,且该节点子树所有节点的金币数减半。
返回收集所有节点金币后的最大积分。
示例 1
edges = [[0,1],[1,2],[2,3]], coins = [10,10,3,3], k = 5 -> 11
示例 2
edges = [[0,1],[0,2]], coins = [8,4,4], k = 0 -> 16
提示:
n== coins.length,2 <=n<= 10^50 <= coins[i] <= 10^4edges.length==n- 1,0 <=k<= 10^4
解题思路
解题思路:树形 DP + 减半次数上限
coinsi <= 10^4 < 2^14,因此任意节点在其祖先应用方式 2 达 14 次后,金币必为 0,
再减半不会改变任何数值。所以只在子树 DP 中记录"已被减半次数 h",h <= H=14。
dpnodeh:在 node 的祖先已减半 h 次(当前金币 = coinsnode >> h)时,
收集 node 子树全部金币的最大积分:
- 方式 1:coinsnode>>h - k + sum(dpchildh)
- 方式 2:coinsnode>>(h+1) + sum(dpchildh+1)(h+1 封顶 H)
答案 dp00。
复杂度: - 时间复杂度:O(n * H),H = 14
- 空间复杂度:O(n * H)
代码实现
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
class Solution {
private static final int H = 15; // coins <= 1e4 < 2^14,15 足够
public int maximumPoints(int[][] edges, int[] coins, int k) {
int n = coins.length;
List<List<Integer>> g = new ArrayList<>();
for (int i = 0; i < n; i++) {
g.add(new ArrayList<>());
}
for (int[] e : edges) {
g.get(e[0]).add(e[1]);
g.get(e[1]).add(e[0]);
}
int[][] dp = new int[n][H + 1];
for (int[] row : dp) {
Arrays.fill(row, Integer.MIN_VALUE / 2);
}
dfs(0, -1, g, coins, k, dp);
return dp[0][0];
}
private void dfs(int u, int parent, List<List<Integer>> g, int[] coins, int k, int[][] dp) {
for (int v : g.get(u)) {
if (v == parent) {
continue;
}
dfs(v, u, g, coins, k, dp);
}
for (int h = 0; h <= H; h++) {
// 方式 1
long gain1 = ((long) (coins[u] >> h)) - k;
long sum1 = gain1;
for (int v : g.get(u)) {
if (v == parent) {
continue;
}
sum1 += dp[v][h];
}
// 方式 2
long gain2 = coins[u] >> (h + 1);
long sum2 = gain2;
for (int v : g.get(u)) {
if (v == parent) {
continue;
}
sum2 += dp[v][Math.min(h + 1, H)];
}
dp[u][h] = (int) Math.max(sum1, sum2);
}
}
// ========== 本地 main 自测 ==========
// 独立暴力:枚举每个节点用方式 1 还是 2(小数据),注意减半传递
private static int brute(int[][] edges, int[] coins, int k) {
int n = coins.length;
List<List<Integer>> g = new ArrayList<>();
for (int i = 0; i < n; i++) {
g.add(new ArrayList<>());
}
for (int[] e : edges) {
g.get(e[0]).add(e[1]);
g.get(e[1]).add(e[0]);
}
// 每个节点决策 0/1;减半效果按子树叠加(节点 u 的当前 coins = coins[u] >> cnt[u],
// cnt[u] = 祖先中选方式 2 的个数,与自身选择无关)
int best = Integer.MIN_VALUE;
for (int mask = 0; mask < (1 << n); mask++) {
int[] cnt = new int[n];
dfsCnt(0, -1, g, mask, cnt);
int total = 0;
for (int i = 0; i < n; i++) {
int cur = coins[i] >> cnt[i];
if ((mask >> i & 1) == 1) {
total += cur >> 1; // 方式 2:自身也减半
} else {
total += cur - k;
}
}
best = Math.max(best, total);
}
return best;
}
private static void dfsCnt(int u, int p, List<List<Integer>> g, int mask, int[] cnt) {
for (int v : g.get(u)) {
if (v == p) {
continue;
}
cnt[v] = cnt[u] + ((mask >> u & 1) == 1 ? 1 : 0);
dfsCnt(v, u, g, mask, cnt);
}
}
public static void main(String[] args) {
Solution s = new Solution();
// 官方案例
check(s, new int[][]{{0, 1}, {1, 2}, {2, 3}}, new int[]{10, 10, 3, 3}, 5, 11);
check(s, new int[][]{{0, 1}, {0, 2}}, new int[]{8, 4, 4}, 0, 16);
// 随机对拍(小数据)
Random rnd = new Random(29);
for (int t = 0; t < 200; t++) {
int n = 2 + rnd.nextInt(7);
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[] coins = new int[n];
for (int i = 0; i < n; i++) {
coins[i] = rnd.nextInt(64);
}
int k = rnd.nextInt(10);
int exp = brute(edges, coins, k);
int got = s.maximumPoints(edges, coins, k);
if (exp != got) {
System.out.println("FAIL t=" + t + " edges=" + Arrays.deepToString(edges)
+ " coins=" + Arrays.toString(coins) + " k=" + k
+ " got=" + got + " exp=" + exp);
return;
}
}
System.out.println("ALL PASS");
}
private static void check(Solution s, int[][] edges, int[] coins, int k, int exp) {
int got = s.maximumPoints(edges, coins, k);
int exp2 = brute(edges, coins, k);
System.out.println((got == exp && got == exp2 ? "PASS" : "FAIL") + " coins="
+ Arrays.toString(coins) + " k=" + k + " got=" + got + " exp=" + exp
+ " brute=" + exp2);
}
}
复杂度分析
- 时间复杂度:O(n * H),H = 14
- 空间复杂度:O(n * H)