LeetCode2901~2920题目解析

LeetCode 2901-2920. 题解合集

文章目录


2901. Longest Unequal Adjacent Groups Subsequence II(最长相邻不相等子序列 II)

题目描述

给定一个字符串数组 words 和一个数组 groups,两个数组长度都是 n。

两个长度相等字符串的汉明距离定义为对应位置字符不同的数目。

需要从下标 0, 1, ..., `n`-1 中选出一个最长子序列 `i0`, `i1`, ..., `ik`-1,满足:

  1. 相邻下标的 groups 值不同;
  2. 相邻 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 <= 1000
  • 1 <= words[i].length <= 10
  • 1 <= groups[i] <= n
  • words 中的字符串互不相同

解题思路

解题思路:动态规划 + 前驱回溯

定义 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^4
  • 0 <= nums[i] <= 2 * 10^4
  • nums 的和不超过 2 * 10^4
  • 0 <= 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) >= indexDifference
  • abs(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 <= 100
  • 0 <= nums[i] <= 50
  • 0 <= indexDifference <= 100
  • 0 <= 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 <= 100
  • 1 <= 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) >= indexDifference
  • abs(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^5
  • 1 <= nums[i] <= 10^9
  • 0 <= indexDifference <= 10^5
  • 0 <= 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^5
  • 1 <= m == grid[i].length <= 10^5
  • 2 <= n * m <= 10^5
  • 1 <= 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 <= 2000
  • 1 <= prices[i] <= 10^6
  • 1 <= 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 < k
  • nums[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 <= 50
  • 1 <= 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 < k
  • nums[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^5
  • 1 <= 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. 同一个盒子里的球必须具有相同的编号(相同编号的球可以分到不同盒子);
  2. 最大的盒子最多只比最小的盒子多一个球。
    返回所需的最少盒子数。

示例 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^5
  • 1 <= 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^9
  • 1 <= k <= 10^5
  • source.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 <= 100
  • 1 <= 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^5
  • s 的长度为偶数

解题思路

解题思路:两两配对

贪心:把 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 <= 1000
  • 1 <= nums[i] <= 1000
  • 1 <= 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^5
  • 1 <= 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 <= 50
  • 0 <= nums[i] < 2^31
  • 1 <= 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^5
  • 0 <= 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^5
  • 0 <= nums[i] <= 10^9
  • 0 <= 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 的金币收集方式二选一:

  1. 收集全部,得到 coins[i] - k 点积分(可为负);
  2. 收集 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^5
  • 0 <= coins[i] <= 10^4
  • edges.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)
相关推荐
码云社区15 分钟前
能碳管理系统开发要关注什么?能源数据、统计口径与AI分析的衔接
java·人工智能·ai·能源·能源管理系统·能碳管理系统·能碳管理系统源码
Flynn Liang35 分钟前
Agentic 工作流:子代理、Supervisor 与人工介入
java·langchain·多智能体·supervisor·agentic·langchain4j
xys_67839 分钟前
JavaSE通关手册:运算符
java·java-ee
jyOverQ2 小时前
从零学 JVM(七):垃圾收集器详解——Serial、ParNew、Parallel、CMS 与 G1
java·jvm
liangbo72 小时前
Spring系列11:切点表达式
java·spring
3095566662 小时前
股票接口返回 404 与空数组的排查笔记:证书状态与级别参数
java·笔记·python·ios·iphone·股票数据api·股票数据接口
nice_lcj5202 小时前
MySQL 存储引擎入门:InnoDB 和 MyISAM 到底怎么选
java·数据库·mysql·面试·innodb·存储引擎·myisam
文丑颜不良啊2 小时前
LeetCode2861~2880题目解析
java·leetcode
装不满的克莱因瓶3 小时前
设计模式之策略模式核心概念和应用场景
java·后端·设计模式·架构·系统架构·策略模式