LeetCode914~925题目解析

3 阅读5分钟

LeetCode 914-925. 题解合集


914. X of a Kind in a Deck of Cards(卡牌分组)

题目描述

给定一副牌,每张牌上都写着一个整数。
此时,你需要选定一个数字 X,使我们可以将整副牌按下述规则分成 1 组或更多组:

  • 每组都有 X 张牌。
  • 组内所有的牌上都写着相同的整数。
    仅当你可选的 X >= 2 时返回 true。

示例 1

  • 输入:deck = [1,2,3,4,4,3,2,1]
  • 输出:true
  • 解释:可行的分组是 [1,1],[2,2],[3,3],[4,4]。

示例 2

  • 输入:deck = [1,1,1,2,2,2,3,3]
  • 输出:false
  • 解释:没有满足要求的分组。
    提示:
  • 1 <= deck.length <= 10^4
  • 0 <= deck[i] < 10^4

解题思路

每种数字的牌张数必须都能被同一个 X >= 2 整除,即所有出现次数的最大公约数 >= 2。
解法一(计数 + 最大公约数,主提交):统计每个数字出现次数,求这些次数的 gcd;
若 gcd >= 2 返回 true,否则 false。
解法二(枚举 X,对照实现):从 2 枚举到最小出现次数,检查每个出现次数是否能被 X 整除,
找到任意一个即返回 true;纯枚举便于理解但不高效,作为对照。

代码实现

import java.util.HashMap;
import java.util.Map;

class Solution {

    // 解法一:计数 + 最大公约数(推荐提交)
    public boolean hasGroupsSizeX(int[] deck) {
        Map<Integer, Integer> cnt = new HashMap<>();
        for (int x : deck) {
            cnt.put(x, cnt.getOrDefault(x, 0) + 1);
        }
        int g = 0;
        for (int v : cnt.values()) {
            g = gcd(g, v);
        }
        return g >= 2;
    }

    private int gcd(int a, int b) {
        return b == 0 ? a : gcd(b, a % b);
    }

    // 解法二:枚举 X(对照实现)
    public boolean hasGroupsSizeX2(int[] deck) {
        Map<Integer, Integer> cnt = new HashMap<>();
        for (int x : deck) {
            cnt.put(x, cnt.getOrDefault(x, 0) + 1);
        }
        int min = deck.length;
        for (int v : cnt.values()) {
            min = Math.min(min, v);
        }
        outer:
        for (int x = 2; x <= min; ++x) {
            for (int v : cnt.values()) {
                if (v % x != 0) {
                    continue outer;
                }
            }
            return true;
        }
        return false;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[][] decks = {
            {1, 2, 3, 4, 4, 3, 2, 1},
            {1, 1, 1, 2, 2, 2, 3, 3},
            {1},
            {1, 1},
            {1, 1, 2, 2, 2, 2},
            {1, 1, 1, 1, 2, 2, 2, 2, 2, 2}
        };
        boolean[] exp = {true, false, false, true, true, true};
        for (int t = 0; t < decks.length; ++t) {
            ++total;
            boolean g1 = s.hasGroupsSizeX(decks[t]);
            boolean g2 = s.hasGroupsSizeX2(decks[t]);
            if (g1 == exp[t] && g2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 914-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0914 PASS ==");
        } else {
            System.out.println("== 0914 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一(gcd)O(n + k)O(k)(k 为不同数字数)
解法二(枚举 X)O(n + minCnt * k)O(k)

915. Partition Array into Disjoint Intervals(分割数组)

题目描述

给定一个数组 nums,将其划分为两个连续子数组 left 和 right,使得:

  • left 中的每个元素都小于或等于 right 中的每个元素。
  • left 和 right 都是非空的。
  • left 的长度要尽可能小。
    在完成这样的分组后返回 left 的长度。
    用例可以保证存在这样的划分方法。

示例 1

  • 输入:nums = [5,0,3,8,6]
  • 输出:3
  • 解释:left = [5,0,3],right = [8,6]。

示例 2

  • 输入:nums = [1,1,1,0,6,12]
  • 输出:4
  • 解释:left = [1,1,1,0],right = [6,12]。
    提示:
  • 2 <= nums.length <= 10^5
  • 0 <= nums[i] <= 10^6
  • 可以保证至少有一种方法能够按题目所描述的那样对 nums 进行划分。

解题思路

划分点 i(left 为 nums[0..i])合法当且仅当 left 的最大值 <= right 的最小值。
解法一(左右最值预处理,主提交):先算后缀最小值 sufMin[i](nums[i..n-1] 的最小
值),再从前往后维护前缀最大值 preMax,第一个满足 preMax <= sufMin[i+1] 的
i+1 即答案,O(n)。
解法二(一趟扫描 + 双值维护,对照实现):遍历时维护当前段最大值 curMax 与全局
最大值 maxSeen;若 nums[i] < maxSeen,说明 i 必须并入 left,更新
cut = i 并把 curMax 提升为 maxSeen;最终答案为 cut + 1。一趟完成。

代码实现

class Solution {

    // 解法一:前缀最大 + 后缀最小(推荐提交)
    public int partitionDisjoint(int[] nums) {
        int n = nums.length;
        int[] sufMin = new int[n];
        sufMin[n - 1] = nums[n - 1];
        for (int i = n - 2; i >= 0; --i) {
            sufMin[i] = Math.min(nums[i], sufMin[i + 1]);
        }
        int preMax = nums[0];
        for (int i = 0; i < n - 1; ++i) {
            preMax = Math.max(preMax, nums[i]);
            if (preMax <= sufMin[i + 1]) {
                return i + 1;
            }
        }
        return n;   // 题目保证有解,不会到这
    }

    // 解法二:一趟扫描(对照实现)
    public int partitionDisjoint2(int[] nums) {
        int n = nums.length;
        int cut = 0;
        int curMax = nums[0];     // 当前 left 段的最大值
        int maxSeen = nums[0];    // 已扫描全体的最大值
        for (int i = 1; i < n; ++i) {
            maxSeen = Math.max(maxSeen, nums[i]);
            if (nums[i] < curMax) {
                // 该元素必须并入 left,left 扩展到 i
                cut = i;
                curMax = maxSeen;
            }
        }
        return cut + 1;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[][] arrs = {
            {5, 0, 3, 8, 6},
            {1, 1, 1, 0, 6, 12},
            {1, 2, 3, 4, 5},
            {5, 4, 3, 2, 1},
            {32, 57, 24, 19, 0, 89}
        };
        // 期望值由 Python 核对:case4 递减序列 left 必须包含全部 5 个;case5=5
        int[] exp = {3, 4, 1, 5, 5};
        for (int t = 0; t < arrs.length; ++t) {
            ++total;
            int g1 = s.partitionDisjoint(arrs[t]);
            int g2 = s.partitionDisjoint2(arrs[t]);
            if (g1 == exp[t] && g2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 915-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0915 PASS ==");
        } else {
            System.out.println("== 0915 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一(左右最值)O(n)O(n)
解法二(一趟双值)O(n)O(1)

916. Word Subsets(单词子集)

题目描述

给你两个字符串数组 words1 和 words2。
现在,如果 b 中的每个字母都出现在 a 中,包括重复出现的字母,那么称字符串 b 是
字符串 a 的子集。
例如,"wrr" 是 "warrior" 的子集,但不是 "world" 的子集。
如果对 words2 中的每一个单词 b,b 都是 a 的子集,那么我们称 words1 中的单词
a 是通用单词。
以数组形式返回 words1 中所有的通用单词。你可以按任意顺序返回答案。

示例 1

  • 输入:words1 = ["amazon","apple","facebook","google","leetcode"], words2 = ["e","o"]
  • 输出:["facebook","google","leetcode"]

示例 2

  • 输入:words1 = ["amazon","apple","facebook","google","leetcode"], words2 = ["l","e"]
  • 输出:["apple","google","leetcode"]
    提示:
  • 1 <= words1.length, words2.length <= 10^4
  • 1 <= words1[i].length, words2[i].length <= 10
  • words1[i] 和 words2[i] 仅由小写英文字母组成
  • words1 中的所有字符串互不相同

解题思路

b 是 a 的子集当且仅当对每个字母 c,a 中 c 的出现次数 >= b 中 c 的出现次数。
对多个 b:每个字母统计 words2 中该字母的"最大需求次数",得到一个需求向量 need;
然后遍历 words1,看每个单词的字母频率是否全部 >= need。
解法一(频率向量 + 合并需求,主提交):先合并 words2 的频次需求,再逐词检查 words1。
解法二(按 word2 逐词检查 + 剪枝,对照实现):直接对每个 words1 单词累计 words2
所有单词的频次需求再比较,逻辑相同但实现稍冗余,展示两种写法。

代码实现

import java.util.ArrayList;
import java.util.List;

class Solution {

    // 解法一:合并需求向量(推荐提交)
    public List<String> wordSubsets(String[] words1, String[] words2) {
        int[] need = new int[26];
        for (String b : words2) {
            int[] f = freq(b);
            for (int i = 0; i < 26; ++i) {
                need[i] = Math.max(need[i], f[i]);
            }
        }
        List<String> ans = new ArrayList<>();
        for (String a : words1) {
            int[] f = freq(a);
            boolean ok = true;
            for (int i = 0; i < 26; ++i) {
                if (f[i] < need[i]) {
                    ok = false;
                    break;
                }
            }
            if (ok) {
                ans.add(a);
            }
        }
        return ans;
    }

    private int[] freq(String s) {
        int[] f = new int[26];
        for (char c : s.toCharArray()) {
            ++f[c - 'a'];
        }
        return f;
    }

    // 解法二:逐词检查(对照实现)
    public List<String> wordSubsets2(String[] words1, String[] words2) {
        List<String> ans = new ArrayList<>();
        for (String a : words1) {
            int[] fa = freq(a);
            boolean ok = true;
            for (String b : words2) {
                int[] fb = freq(b);
                for (int i = 0; i < 26; ++i) {
                    if (fa[i] < fb[i]) {
                        ok = false;
                        break;
                    }
                }
                if (!ok) {
                    break;
                }
            }
            if (ok) {
                ans.add(a);
            }
        }
        return ans;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        String[][] w1s = {
            {"amazon", "apple", "facebook", "google", "leetcode"},
            {"amazon", "apple", "facebook", "google", "leetcode"},
            {"aaaa", "aa", "a"},
            {"abc", "abd", "abb"}
        };
        String[][] w2s = {
            {"e", "o"},
            {"l", "e"},
            {"aa"},
            {"a", "b", "bb"}
        };
        String[][] exp = {
            {"facebook", "google", "leetcode"},
            {"apple", "google", "leetcode"},
            {"aaaa", "aa"},
            {"abb"}
        };
        for (int t = 0; t < w1s.length; ++t) {
            ++total;
            java.util.List<String> g1 = s.wordSubsets(w1s[t], w2s[t]);
            java.util.List<String> g2 = s.wordSubsets2(w1s[t], w2s[t]);
            java.util.Collections.sort(g1);
            java.util.Collections.sort(g2);
            java.util.List<String> e = new ArrayList<>();
            for (String x : exp[t]) {
                e.add(x);
            }
            java.util.Collections.sort(e);
            if (g1.equals(e) && g2.equals(e)) {
                ++pass;
            } else {
                System.out.println("FAIL 916-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + e);
            }
        }
        if (pass == total) {
            System.out.println("== 0916 PASS ==");
        } else {
            System.out.println("== 0916 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一(合并需求)O((n1 + n2) * L + 26 * n1)O(26)
解法二(逐词检查)O(26 * n1 * n2)O(26)

917. Reverse Only Letters(仅仅反转字母)

题目描述

给你一个字符串 s,根据下述规则反转字符串:

  • 所有非英文字母保留在原有位置。
  • 所有英文字母(小写或大写)位置反转。
    返回反转后的 s。

示例 1

  • 输入:s = "ab-cd"
  • 输出:"dc-ba"

示例 2

  • 输入:s = "a-bC-dEf-ghIj"
  • 输出:"j-Ih-gfE-dCba"

示例 3

  • 输入:s = "Test1ng-Leet=code-Q!"
  • 输出:"Qedo1ct-eeLg=ntse-T!"
    提示:
  • 1 <= s.length <= 100
  • s 仅由 ASCII 值在范围 [33, 122] 的字符组成
  • s 不含 '\"' 或 '\\'

解题思路

双指针从两端向中间,忽略非字母字符,交换遇到的成对字母。
解法一(双指针交换,主提交):l 从头、r 从尾;l 跳过非字母、r 跳过非字母,
当 l < r 时交换两个字符。
解法二(收集字母逆序填充,对照实现):先收集所有字母按原序,再倒序填回原串的字母
槽位,非字母保持不动。

代码实现

class Solution {

    // 解法一:双指针交换(推荐提交)
    public String reverseOnlyLetters(String s) {
        char[] cs = s.toCharArray();
        int l = 0, r = cs.length - 1;
        while (l < r) {
            while (l < r && !Character.isLetter(cs[l])) {
                ++l;
            }
            while (l < r && !Character.isLetter(cs[r])) {
                --r;
            }
            if (l < r) {
                char t = cs[l];
                cs[l] = cs[r];
                cs[r] = t;
                ++l;
                --r;
            }
        }
        return new String(cs);
    }

    // 解法二:收集字母逆序填充(对照实现)
    public String reverseOnlyLetters2(String s) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < s.length(); ++i) {
            char c = s.charAt(i);
            if (Character.isLetter(c)) {
                sb.append(c);
            }
        }
        String letters = sb.reverse().toString();
        char[] cs = s.toCharArray();
        int idx = 0;
        for (int i = 0; i < cs.length; ++i) {
            if (Character.isLetter(cs[i])) {
                cs[i] = letters.charAt(idx++);
            }
        }
        return new String(cs);
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        String[] ss = {"ab-cd", "a-bC-dEf-ghIj", "Test1ng-Leet=code-Q!", "a", "---"};
        String[] exp = {"dc-ba", "j-Ih-gfE-dCba", "Qedo1ct-eeLg=ntse-T!", "a", "---"};
        for (int t = 0; t < ss.length; ++t) {
            ++total;
            String g1 = s.reverseOnlyLetters(ss[t]);
            String g2 = s.reverseOnlyLetters2(ss[t]);
            if (g1.equals(exp[t]) && g2.equals(exp[t])) {
                ++pass;
            } else {
                System.out.println("FAIL 917-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0917 PASS ==");
        } else {
            System.out.println("== 0917 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一(双指针)O(n)O(1)(原地,字符串需转字符数组)
解法二(收集逆填)O(n)O(n)

918. Maximum Sum Circular Subarray(环形子数组的最大和)

题目描述

给定一个长度为 n 的环形整数数组 nums,返回 nums 的非空子数组的最大可能和。
环形数组意味着数组的末端将会与开头相连呈环状。形式上,nums[i] 的下一个元素是
nums[(i+1) % n],nums[i] 的前一个元素是 nums[(i-1+n) % n]。
子数组最多只能包含固定缓冲区 nums 中的每个元素一次。

示例 1

  • 输入:nums = [1,-2,3,-2]
  • 输出:3
  • 解释:从子数组 [3] 得到最大和 3。

示例 2

  • 输入:nums = [5,-3,5]
  • 输出:10
  • 解释:从子数组 [5,5] 得到最大和 5 + 5 = 10。

示例 3

  • 输入:nums = [3,-2,2,-3]
  • 输出:3
  • 解释:从子数组 [3] 和 [3,-2,2] 都可以得到最大和 3。
    提示:
  • 1 <= nums.length <= 3 * 10^4
  • -3 * 10^4 <= nums[i] <= 3 * 10^4

解题思路

环形最大子段和 = max(普通最大子段和, 总和 - 普通最小子段和)。后者表示跨过首尾的
环上子数组,等价于去掉中间的"补集"(最小子段)。
注意特殊情况:若整个数组全是负数,普通最大子段和为最大负数,而 总和 - 最小子段 会
得到 0(不允许空子数组),所以此时答案就是普通最大子段和。
解法一(一次遍历同时求最大/最小子段和,主提交):Kadane 同时维护 maxEnding 与
minEnding,另附总和非负判断,O(n)。
解法二(双数组前缀 + 单调队列,对照实现):复制数组成两倍长,滑动窗口长度 <= n,
用前缀和 + 单调队列求环上不超过 n 长度的最大子段和,直观但代码更长。

代码实现

import java.util.ArrayDeque;
import java.util.Deque;

class Solution {

    // 解法一:Kadane 最大/最小子段和(推荐提交)
    public int maxSubarraySumCircular(int[] nums) {
        int total = 0;
        int curMax = 0, maxSum = Integer.MIN_VALUE;
        int curMin = 0, minSum = Integer.MAX_VALUE;
        for (int x : nums) {
            total += x;
            curMax = Math.max(curMax + x, x);
            maxSum = Math.max(maxSum, curMax);
            curMin = Math.min(curMin + x, x);
            minSum = Math.min(minSum, curMin);
        }
        // 全是负数时,total - minSum == 0(空子数组),不合法,退回 maxSum
        if (maxSum < 0) {
            return maxSum;
        }
        return Math.max(maxSum, total - minSum);
    }

    // 解法二:双倍数组 + 单调队列求长 <= n 的最大子段和(对照实现)
    public int maxSubarraySumCircular2(int[] nums) {
        int n = nums.length;
        int[] a = new int[2 * n];
        for (int i = 0; i < 2 * n; ++i) {
            a[i] = nums[i % n];
        }
        // 前缀和
        long[] pre = new long[2 * n + 1];
        for (int i = 0; i < 2 * n; ++i) {
            pre[i + 1] = pre[i] + a[i];
        }
        // 单调队列维护 pre 的最小值(窗口长度 <= n)
        Deque<Integer> q = new ArrayDeque<>();
        q.offer(0);
        long ans = Long.MIN_VALUE;
        for (int i = 1; i <= 2 * n; ++i) {
            // 出队:窗口 [i-n+1, i] 维护头元素的合法性
            while (!q.isEmpty() && q.peekFirst() < i - n) {
                q.pollFirst();
            }
            ans = Math.max(ans, pre[i] - pre[q.peekFirst()]);
            while (!q.isEmpty() && pre[q.peekLast()] >= pre[i]) {
                q.pollLast();
            }
            q.offerLast(i);
        }
        return (int) ans;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[][] arrs = {
            {1, -2, 3, -2},
            {5, -3, 5},
            {3, -2, 2, -3},
            {1, 2, 3, 4},
            {-1, -2, -3},
            {0, 5, 8, -9, 9, 10}
        };
        // case6:总和 23 - 最小子段(-9) = 32(环上跨过 -9 的 [5,8,-9,9,10] 补集),Python 核对
        int[] exp = {3, 10, 3, 10, -1, 32};
        for (int t = 0; t < arrs.length; ++t) {
            ++total;
            int g1 = s.maxSubarraySumCircular(arrs[t]);
            int g2 = s.maxSubarraySumCircular2(arrs[t]);
            if (g1 == exp[t] && g2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 918-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0918 PASS ==");
        } else {
            System.out.println("== 0918 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一(Kadane 双极值)O(n)O(1)
解法二(前缀和 + 单调队列)O(n)O(n)

919. Complete Binary Tree Inserter(完全二叉树插入器)

题目描述

完全二叉树是每一层(除最后一层外)都是完全填充(即,节点数达到最大)的,并且所有的
节点都尽可能地集中在左侧。
设计一种算法,将一个新节点插入到一棵完全二叉树中,并在插入后保持其完整。
实现 CBTInserter 类:

  • CBTInserter(TreeNode root) 使用头节点为 root 的给定树初始化该数据结构;
  • CBTInserter.insert(int v) 向树中插入一个值为 Node.val == v 的新节点
    TreeNode。使树保持完全二叉树的状态,并返回插入节点 TreeNode 的父节点的值;
  • CBTInserter.get_root() 将返回树的头节点。

示例 1

  • 输入:
    ["CBTInserter", "insert", "insert", "get_root"]
    [[[1, 2]], [3], [4], []]
  • 输出:[null, 1, 2, [1, 2, 3, 4]]
  • 解释:
    CBTInserter cBTInserter = new CBTInserter([1, 2]);
    cBTInserter.insert(3); // 返回 1
    cBTInserter.insert(4); // 返回 2
    cBTInserter.get_root(); // 返回 [1, 2, 3, 4]
    提示:
  • 树中节点数量范围为 [1, 1000]
  • 0 <= Node.val <= 5000
  • root 是完全二叉树
  • insert 调用次数不超过 10^4

解题思路

完全二叉树按层序(BFS)遍历时,第一个"有空缺"的节点就是下一个插入位置:要么它的
左孩子为空(插左边),要么左孩子非空但右孩子为空(插右边)。
解法一(BFS 预收集候选父节点,主提交):构造时用队列层序遍历所有节点,把拥有至少一个
空孩子(即左右孩子不全)的节点按顺序存进 candidate 队列。insert(v) 弹出队首
候选:左空插左、否则插右;插入后若该节点补满了,则把新节点加入候选队列(新节点本身
没有孩子),并把队首弹掉。逻辑用游标维护,避免频繁出入队。
解法二(每次都 BFS 找候选,对照实现):insert 时重新层序遍历找第一个空位父节点,
直观但每次 O(n),仅作对照。

代码实现

import java.util.ArrayDeque;
import java.util.Queue;

class CBTInserter {

    private final TreeNode root;
    private final Queue<TreeNode> cand;   // 有残缺孩子的节点按序存放

    public CBTInserter(TreeNode root) {
        this.root = root;
        cand = new ArrayDeque<>();
        Queue<TreeNode> q = new ArrayDeque<>();
        q.offer(root);
        while (!q.isEmpty()) {
            TreeNode node = q.poll();
            if (node.left == null || node.right == null) {
                cand.offer(node);
            }
            if (node.left != null) {
                q.offer(node.left);
            }
            if (node.right != null) {
                q.offer(node.right);
            }
        }
    }

    public int insert(int v) {
        TreeNode parent = cand.peek();
        TreeNode child = new TreeNode(v);
        if (parent.left == null) {
            parent.left = child;
        } else {
            parent.right = child;
            cand.poll();   // 该节点已满,出队
        }
        // 新节点没有孩子,是后续潜在的候选
        cand.offer(child);
        return parent.val;
    }

    public TreeNode get_root() {
        return root;
    }

    // 解法二:每次 BFS 找空位(对照实现)
    static class CBTInserter2 {

        private final TreeNode root;

        public CBTInserter2(TreeNode root) {
            this.root = root;
        }

        public int insert(int v) {
            Queue<TreeNode> q = new ArrayDeque<>();
            q.offer(root);
            while (!q.isEmpty()) {
                TreeNode node = q.poll();
                if (node.left == null) {
                    node.left = new TreeNode(v);
                    return node.val;
                }
                q.offer(node.left);
                if (node.right == null) {
                    node.right = new TreeNode(v);
                    return node.val;
                }
                q.offer(node.right);
            }
            return -1;   // 不会到这
        }

        public TreeNode get_root() {
            return root;
        }
    }

    static class TreeNode {
        int val;
        TreeNode left;
        TreeNode right;

        TreeNode(int x) {
            val = x;
        }
    }

    // 层序建树
    private static TreeNode build(Integer[] vals) {
        if (vals == null || vals.length == 0) {
            return null;
        }
        TreeNode root = new TreeNode(vals[0]);
        Queue<TreeNode> q = new ArrayDeque<>();
        q.offer(root);
        int i = 1;
        while (!q.isEmpty() && i < vals.length) {
            TreeNode cur = q.poll();
            if (vals[i] != null) {
                cur.left = new TreeNode(vals[i]);
                q.offer(cur.left);
            }
            ++i;
            if (i < vals.length && vals[i] != null) {
                cur.right = new TreeNode(vals[i]);
                q.offer(cur.right);
            }
            ++i;
        }
        return root;
    }

    private static String levelStr(TreeNode root) {
        StringBuilder sb = new StringBuilder("[");
        java.util.Queue<TreeNode> q = new java.util.LinkedList<>();
        q.offer(root);
        boolean anyNonNull = true;
        while (!q.isEmpty() && anyNonNull) {
            anyNonNull = false;
            int sz = q.size();
            for (int i = 0; i < sz; ++i) {
                TreeNode node = q.poll();
                if (node == null) {
                    sb.append("null,");
                    continue;
                }
                sb.append(node.val).append(',');
                if (node.left != null || node.right != null) {
                    anyNonNull = true;
                }
                q.offer(node.left);
                q.offer(node.right);
            }
        }
        String s = sb.toString();
        while (s.endsWith("null,")) {
            s = s.substring(0, s.length() - 5);
        }
        return s.substring(0, s.length() - 1) + "]";
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        int pass = 0, total = 0;
        {
            CBTInserter c = new CBTInserter(build(new Integer[]{1, 2}));
            ++total;
            if (c.insert(3) == 1 && c.insert(4) == 2
                    && levelStr(c.get_root()).equals("[1,2,3,4]")) {
                ++pass;
            } else {
                System.out.println("FAIL 919-1: " + levelStr(c.get_root()));
            }
        }
        {
            CBTInserter2 c = new CBTInserter2(build(new Integer[]{1, 2, 3, 4, 5, 6}));
            ++total;
            if (c.insert(7) == 3 && c.insert(8) == 4
                    && levelStr(c.get_root()).equals("[1,2,3,4,5,6,7,8]")) {
                ++pass;
            } else {
                System.out.println("FAIL 919-2: " + levelStr(c.get_root()));
            }
        }
        {
            CBTInserter c = new CBTInserter(build(new Integer[]{1}));
            ++total;
            if (c.insert(2) == 1 && levelStr(c.get_root()).equals("[1,2]")) {
                ++pass;
            } else {
                System.out.println("FAIL 919-3");
            }
        }
        if (pass == total) {
            System.out.println("== 0919 PASS ==");
        } else {
            System.out.println("== 0919 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一(候选队列)构造 O(n) / insert O(1)O(n)
解法二(每次 BFS)insert O(n)O(n)

920. Number of Music Playlists(播放列表的数量)

题目描述

你的音乐播放器里有 n 首不同的歌,在旅途中,你计划听 goal 首歌(不一定不同,即,
允许歌曲重复)。你将会按如下规则创建播放列表:

  • 每首歌至少播放一次。
  • 一首歌只有在其他 k 首歌播放完之后才能再次播放。
    给你 n、goal 和 k,返回可以满足要求的播放列表的数量。由于答案可能非常大,请
    返回对 10^9 + 7 取余的结果。

示例 1

  • 输入:n = 3, goal = 3, k = 1
  • 输出:6
  • 解释:有 6 种可能的播放列表。[1, 2, 3],[1, 3, 2],[2, 1, 3],[2, 3, 1],[3, 1, 2],
    [3, 2, 1]。

示例 2

  • 输入:n = 2, goal = 3, k = 0
  • 输出:6
  • 解释:有 6 种可能的播放列表。[1, 1, 2],[1, 2, 1],[2, 1, 1],[2, 2, 1],[2, 1, 2],
    [1, 2, 2]。

示例 3

  • 输入:n = 2, goal = 3, k = 1
  • 输出:2
  • 解释:有 2 种可能的播放列表。[1, 2, 1],[2, 1, 2]。
    提示:
  • 0 <= k < n <= goal <= 100

解题思路

动态规划:dp[i][j] 表示听完 i 首歌(已填充播放列表前 i 个位置)且使用了
j 首不同歌曲的播放列表数量。
转移考虑第 i 个位置放什么歌:

  • 放一首"从未用过"的歌:有 n - (j-1) 种选择,dp[i-1][j-1] * (n - j + 1);
  • 放一首"用过"的歌:前 k 首不能重复,能用的旧歌为 j - k 首(当 j > k),
    贡献 dp[i-1][j] * max(0, j - k)。
    答案 dp[goal][n]。
    解法一(二维 DP,主提交):按上述转移 O(goal * n)。
    解法二(滚动数组优化,对照实现):状态只依赖上一行,用一维数组滚动;另外附
    组合数 + 容斥的对照公式实现,展示不同切入点。

代码实现

class Solution {

    private static final int MOD = 1_000_000_007;

    // 解法一:二维 DP(推荐提交)
    public int numMusicPlaylists(int n, int goal, int k) {
        long[][] dp = new long[goal + 1][n + 1];
        dp[0][0] = 1;
        for (int i = 1; i <= goal; ++i) {
            for (int j = 1; j <= n; ++j) {
                // 放新歌
                long ways = dp[i - 1][j - 1] * (n - j + 1) % MOD;
                // 放旧歌(前 k 首不能重,可用旧歌数为 j - k)
                if (j > k) {
                    ways = (ways + dp[i - 1][j] * (j - k)) % MOD;
                }
                dp[i][j] = ways;
            }
        }
        return (int) dp[goal][n];
    }

    // 解法二:滚动数组 + 幂次/组合容斥对照(滚动实现)
    public int numMusicPlaylists2(int n, int goal, int k) {
        long[] dp = new long[n + 1];
        dp[0] = 1;
        for (int i = 1; i <= goal; ++i) {
            long[] ndp = new long[n + 1];
            for (int j = 1; j <= n; ++j) {
                long ways = ndp[j];
                ways = (ways + dp[j - 1] * (n - j + 1)) % MOD;
                if (j > k) {
                    ways = (ways + dp[j] * (j - k)) % MOD;
                }
                ndp[j] = ways;
            }
            dp = ndp;
        }
        return (int) dp[n];
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[][] cases = {
            {3, 3, 1},
            {2, 3, 0},
            {2, 3, 1},
            {3, 4, 1},
            {1, 1, 0},
            {5, 10, 4}
        };
        // 期望值由 Python 同款 DP 核对:case4=18,case6=120
        int[] exp = {6, 6, 2, 18, 1, 120};
        for (int t = 0; t < cases.length; ++t) {
            ++total;
            int g1 = s.numMusicPlaylists(cases[t][0], cases[t][1], cases[t][2]);
            int g2 = s.numMusicPlaylists2(cases[t][0], cases[t][1], cases[t][2]);
            if (g1 == exp[t] && g2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 920-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0920 PASS ==");
        } else {
            System.out.println("== 0920 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一(二维 DP)O(goal * n)O(goal * n)
解法二(滚动数组)O(goal * n)O(n)

921. Minimum Add to Make Parentheses Valid(使括号有效的最少添加)

题目描述

只有满足下面几点之一,括号字符串才是有效的:

  • 它是一个空字符串,或者
  • 它可以被写成 AB(A 与 B 连接),其中 A 和 B 都是有效字符串,或者
  • 它可以被写作 (A),其中 A 是有效字符串。
    给定一个括号字符串 s,在每一次操作中,你都可以在字符串的任何位置插入一个括号。
    例如,如果 s = "()))",你可以插入一个开始括号为 "(()))" 或结束括号为 "())))"。
    返回为使结果字符串 s 有效而必须添加的最少括号数。

示例 1

  • 输入:s = "())"
  • 输出:1

示例 2

  • 输入:s = "((("
  • 输出:3
    提示:
  • 1 <= s.length <= 1000
  • s[i] 为 '(' 或 ')'

解题思路

用计数器维护"未匹配的左括号数":遇到 ( 计数 +1;遇到 ) 若有未匹配的左括号则
匹配掉一个(计数 -1),否则这个右括号无法匹配,需要补一个左括号(答案 +1)。最后
剩余的未匹配左括号数也需要补右括号,计入答案。
解法一(计数贪心,主提交):单次遍历 O(n) 完成。
解法二(栈模拟,对照实现):用栈模拟,遇到 ) 时若栈顶是 ( 则弹出,否则答案 +1;
结束时答案再加上栈中剩余的 ( 数量。

代码实现

class Solution {

    // 解法一:计数贪心(推荐提交)
    public int minAddToMakeValid(String s) {
        int left = 0, ans = 0;
        for (char c : s.toCharArray()) {
            if (c == '(') {
                ++left;
            } else {
                if (left > 0) {
                    --left;
                } else {
                    ++ans;
                }
            }
        }
        return ans + left;
    }

    // 解法二:栈模拟(对照实现)
    public int minAddToMakeValid2(String s) {
        java.util.Deque<Character> stack = new java.util.ArrayDeque<>();
        int ans = 0;
        for (char c : s.toCharArray()) {
            if (c == '(') {
                stack.push(c);
            } else {
                if (!stack.isEmpty() && stack.peek() == '(') {
                    stack.pop();
                } else {
                    ++ans;
                }
            }
        }
        return ans + stack.size();
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        String[] ss = {"())", "(((", "()", "()))((", "((()))", ")))", "()()"};
        int[] exp = {1, 3, 0, 4, 0, 3, 0};
        for (int t = 0; t < ss.length; ++t) {
            ++total;
            int g1 = s.minAddToMakeValid(ss[t]);
            int g2 = s.minAddToMakeValid2(ss[t]);
            if (g1 == exp[t] && g2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 921-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0921 PASS ==");
        } else {
            System.out.println("== 0921 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一(计数)O(n)O(1)
解法二(栈)O(n)O(n)

922. Sort Array By Parity II(按奇偶排序数组 II)

题目描述

给定一个非负整数数组 nums,nums 中一半整数是奇数,一半整数是偶数。
对数组进行排序,以便当 nums[i] 为奇数时,i 也是奇数;当 nums[i] 为偶数时,
i 也是偶数。
你可以返回任何满足上述条件的数组作为答案。

示例 1

  • 输入:nums = [4,2,5,7]
  • 输出:[4,5,2,7]
  • 解释:[4,7,2,5],[2,5,4,7],[2,7,4,5] 也会被接受。

示例 2

  • 输入:nums = [2,3]
  • 输出:[2,3]
    提示:
  • 2 <= nums.length <= 2 * 10^4
  • nums.length 是偶数
  • nums 中一半是偶数
  • 0 <= nums[i] <= 1000

解题思路

偶数下标 i 应放偶数、奇数下标放奇数。用两个指针分别指向下一个待填的偶数位和奇数位。
解法一(双指针填放,主提交):遍历原数组,偶数放入偶指针位置(步长 2),奇数放入
奇指针位置(步长 2)。O(n) 且一次填好。
解法二(原地交换,对照实现):偶指针 i 从 0 步长 2 扫描,若 nums[i] 是奇数,
则找奇数指针 j(从 1 步长 2)处为偶数的位置与之交换。原地完成,不使用额外数组。

代码实现

import java.util.Arrays;

class Solution {

    // 解法一:双指针填放(推荐提交)
    public int[] sortArrayByParityII(int[] nums) {
        int n = nums.length;
        int[] ans = new int[n];
        int even = 0, odd = 1;
        for (int x : nums) {
            if (x % 2 == 0) {
                ans[even] = x;
                even += 2;
            } else {
                ans[odd] = x;
                odd += 2;
            }
        }
        return ans;
    }

    // 解法二:原地交换(对照实现)
    public int[] sortArrayByParityII2(int[] nums) {
        int n = nums.length;
        int j = 1;
        for (int i = 0; i < n; i += 2) {
            if (nums[i] % 2 != 0) {
                // 找奇数下标处是偶数的位置
                while (j < n && nums[j] % 2 == 1) {
                    j += 2;
                }
                int t = nums[i];
                nums[i] = nums[j];
                nums[j] = t;
            }
        }
        return nums;
    }

    private static boolean isValid(int[] arr) {
        for (int i = 0; i < arr.length; ++i) {
            if (arr[i] % 2 != i % 2) {
                return false;
            }
        }
        return true;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[][] arrs = {
            {4, 2, 5, 7},
            {2, 3},
            {1, 2, 3, 4},
            {0, 1, 2, 3, 4, 5},
            {2, 0, 3, 1}
        };
        for (int t = 0; t < arrs.length; ++t) {
            ++total;
            if (isValid(s.sortArrayByParityII(arrs[t].clone()))
                    && isValid(s.sortArrayByParityII2(arrs[t].clone()))) {
                ++pass;
            } else {
                System.out.println("FAIL 922-" + (t + 1));
            }
        }
        if (pass == total) {
            System.out.println("== 0922 PASS ==");
        } else {
            System.out.println("== 0922 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一(双指针填放)O(n)O(n)
解法二(原地交换)O(n)O(1)

923. 3Sum With Multiplicity(三数之和的多种可能)

题目描述

给定一个整数数组 arr,以及一个整数 target 作为目标值,返回满足 i < j < k 且
arr[i] + arr[j] + arr[k] == target 的元组 i, j, k 的数量。
由于结果会非常大,请返回 10^9 + 7 的模。

示例 1

  • 输入:arr = [1,1,2,2,3,3,4,4,5,5], target = 8
  • 输出:20
  • 解释:
    按值枚举 (arr[i], arr[j], arr[k]):
    (1, 2, 5) 出现 8 次;(1, 3, 4) 出现 8 次;(2, 2, 4) 出现 2 次;(2, 3, 3) 出现 2 次。

示例 2

  • 输入:arr = [1,1,2,2,2,2], target = 5
  • 输出:12
  • 解释:
    arr[i] = 1, arr[j] = arr[k] = 2 出现 12 次:
    我们从 [1,1] 中选择一个 1,有 2 种情况,
    从 [2,2,2,2] 中选出两个 2,有 6 种情况。
    提示:
  • 3 <= arr.length <= 3000
  • 0 <= arr[i] <= 100
  • 0 <= target <= 300

解题思路

元素值范围很小(0..100),可以用计数 + 两两组合统计,或排序 + 双指针。
解法一(计数 + 枚举两数,主提交):统计每个值的出现次数 cnt。枚举 i < j
(值层面),need = target - i - j。若 0 <= need <= 100,按三数取值情况组合计数:
三个都相等、两个相等、全不同分别用组合数公式。注意 i <= j <= need 去重。
解法二(排序 + 双指针,对照实现):排序后经典三数之和双指针,累加出现组合数
lCnt * rCnt 处理重复值,取模返回。

代码实现

import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

class Solution {

    private static final int MOD = 1_000_000_007;

    // 解法一:计数 + 组合数(推荐提交)
    public int threeSumMulti(int[] arr, int target) {
        long[] cnt = new long[101];
        for (int x : arr) {
            ++cnt[x];
        }
        long ans = 0;
        for (int i = 0; i <= 100; ++i) {
            if (cnt[i] == 0) {
                continue;
            }
            for (int j = i; j <= 100; ++j) {
                if (cnt[j] == 0) {
                    continue;
                }
                int need = target - i - j;
                if (need < j || need > 100 || cnt[need] == 0) {
                    continue;
                }
                if (i == j && j == need) {
                    // 三个相同:C(cnt, 3)
                    ans += comb3(cnt[i]);
                } else if (i == j) {
                    // i==j!=need:C(cnt[i],2) * cnt[need]
                    ans += comb2(cnt[i]) * cnt[need];
                } else if (j == need) {
                    // i!=j==need:cnt[i] * C(cnt[j],2)
                    ans += cnt[i] * comb2(cnt[j]);
                } else {
                    // 全不同
                    ans += cnt[i] * cnt[j] * cnt[need];
                }
                ans %= MOD;
            }
        }
        return (int) (ans % MOD);
    }

    private long comb2(long c) {
        return c * (c - 1) / 2;
    }

    private long comb3(long c) {
        return c * (c - 1) * (c - 2) / 6;
    }

    // 解法二:排序 + 双指针(对照实现)
    public int threeSumMulti2(int[] arr, int target) {
        Arrays.sort(arr);
        int n = arr.length;
        long ans = 0;
        for (int i = 0; i < n - 2; ++i) {
            int l = i + 1, r = n - 1;
            int need = target - arr[i];
            while (l < r) {
                int sum = arr[l] + arr[r];
                if (sum < need) {
                    ++l;
                } else if (sum > need) {
                    --r;
                } else {
                    if (arr[l] == arr[r]) {
                        // l..r 段全部相同:C(len,2)
                        long len = r - l + 1;
                        ans = (ans + len * (len - 1) / 2) % MOD;
                        break;
                    } else {
                        // 统计左侧重复数、右侧重复数
                        int lv = arr[l], rv = arr[r];
                        long lc = 0, rc = 0;
                        while (l <= r && arr[l] == lv) {
                            ++lc;
                            ++l;
                        }
                        while (l <= r && arr[r] == rv) {
                            ++rc;
                            --r;
                        }
                        ans = (ans + lc * rc) % MOD;
                    }
                }
            }
        }
        return (int) ans;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[][] arrs = {
            {1, 1, 2, 2, 3, 3, 4, 4, 5, 5},
            {1, 1, 2, 2, 2, 2},
            {1, 2, 3},
            {0, 0, 0},
            {2, 2, 2, 2, 2},
            {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
        };
        int[] targets = {8, 5, 6, 0, 6, 12};
        // 期望值由 Python 核对:case6=(1,2,9)(1,3,8)(1,4,7)(1,5,6)(2,3,7)(2,4,6)(3,4,5) 共 7
        int[] exp = {20, 12, 1, 1, 10, 7};
        for (int t = 0; t < arrs.length; ++t) {
            ++total;
            int g1 = s.threeSumMulti(arrs[t], targets[t]);
            int g2 = s.threeSumMulti2(arrs[t], targets[t]);
            if (g1 == exp[t] && g2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 923-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0923 PASS ==");
        } else {
            System.out.println("== 0923 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一(计数枚举)O(100^2)O(100)
解法二(双指针)O(n^2)O(log n)(排序栈)

924. Minimize Malware Spread(尽量减少恶意软件的传播)

题目描述

给出了一个由 n 个节点组成的网络,用 n x n 个邻接矩阵图 graph 表示。在节点网络中,
当 graph[i][j] = 1 时,表示节点 i 能够直接连接到另一个节点 j。
一些节点 initial 最初被恶意软件感染。只要两个节点直接连接,且其中至少一个节点受到
恶意软件的感染,那么两个节点都将被恶意软件感染。这种恶意软件的传播将继续,直到没有更多
的节点可以被这种方式感染。
假设 M(initial) 是在恶意软件停止传播之后,整个网络中感染恶意软件的最终节点数。
如果从 initial 中移除某一节点能够最小化 M(initial),返回该节点。如果有多个节点满足
条件,就返回索引最小的节点。
请注意,如果某个节点已从受感染节点的列表 initial 中删除,它以后仍有可能因恶意软件传播
而受到感染。

示例 1

  • 输入:graph = [[1,1,0],[1,1,0],[0,0,1]], initial = [0,1]
  • 输出:0

示例 2

  • 输入:graph = [[1,0,0],[0,1,0],[0,0,1]], initial = [0,2]
  • 输出:0

示例 3

  • 输入:graph = [[1,1,1],[1,1,1],[1,1,1]], initial = [1,2]
  • 输出:1
    提示:
  • 1 <= graph.length <= 300
  • 0 <= graph[i][j] == graph[j][i] <= 1
  • graph[i][i] == 1
  • 1 <= initial.length <= graph.length
  • 0 <= initial[i] < graph.length
  • initial 中所有整数均不重复

解题思路

按连通分量分析:一个连通块若有多个 initial 中的节点,删除其中任意一个,该块仍会被其他
感染节点传播感染,收益为 0;若恰好只有 1 个 initial 节点,删除它能让整个块免于感染,
收益 = 块大小。
解法一(DFS 染色分量 + 统计,主提交):DFS/BFS 给每个连通分量编号,记录分量大小、
分量内的 initial 节点个数。对每个 initial 节点,若它所在块恰好只含它一个感染源,
潜在收益为块大小,取最大收益、同收益取最小索引;若所有块收益都为 0,返回 initial 中
最小的元素。
解法二(并查集,对照实现):用并查集合并连通块,再统计每个根的分量大小与 initial
计数,逻辑与解一同源,展示并查集实现。

代码实现

import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

class Solution {

    // 解法一:DFS 连通分量分析(推荐提交)
    public int minMalwareSpread(int[][] graph, int[] initial) {
        int n = graph.length;
        int[] comp = new int[n];
        Arrays.fill(comp, -1);
        int compCount = 0;
        for (int i = 0; i < n; ++i) {
            if (comp[i] == -1) {
                dfs(graph, comp, i, compCount++);
            }
        }
        // 每个分量的节点数
        int[] size = new int[compCount];
        for (int i = 0; i < n; ++i) {
            ++size[comp[i]];
        }
        // 每个分量内 initial 的节点数
        int[] infect = new int[compCount];
        Set<Integer> initSet = new HashSet<>();
        for (int v : initial) {
            initSet.add(v);
            ++infect[comp[v]];
        }
        int best = initial[0];
        int bestGain = -1;
        for (int v : initial) {
            int c = comp[v];
            if (infect[c] == 1) {
                // 独立感染源:收益就是块大小
                if (size[c] > bestGain || (size[c] == bestGain && v < best)) {
                    best = v;
                    bestGain = size[c];
                }
            } else {
                // 同块多感染源:删除无收益,但作为兜底候选
                if (bestGain == -1 && v < best) {
                    best = v;
                }
            }
        }
        return best;
    }

    private void dfs(int[][] graph, int[] comp, int u, int id) {
        comp[u] = id;
        for (int v = 0; v < graph.length; ++v) {
            if (graph[u][v] == 1 && comp[v] == -1) {
                dfs(graph, comp, v, id);
            }
        }
    }

    // 解法二:并查集(对照实现)
    public int minMalwareSpread2(int[][] graph, int[] initial) {
        int n = graph.length;
        int[] parent = new int[n];
        for (int i = 0; i < n; ++i) {
            parent[i] = i;
        }
        for (int i = 0; i < n; ++i) {
            for (int j = i + 1; j < n; ++j) {
                if (graph[i][j] == 1) {
                    union(parent, i, j);
                }
            }
        }
        int[] size = new int[n];
        for (int i = 0; i < n; ++i) {
            ++size[find(parent, i)];
        }
        int[] infect = new int[n];
        Set<Integer> initSet = new HashSet<>();
        for (int v : initial) {
            initSet.add(v);
            ++infect[find(parent, v)];
        }
        int best = initial[0];
        int bestGain = -1;
        for (int v : initial) {
            int root = find(parent, v);
            if (infect[root] == 1) {
                if (size[root] > bestGain || (size[root] == bestGain && v < best)) {
                    best = v;
                    bestGain = size[root];
                }
            } else {
                if (bestGain == -1 && v < best) {
                    best = v;
                }
            }
        }
        return best;
    }

    private int find(int[] parent, int x) {
        while (parent[x] != x) {
            parent[x] = parent[parent[x]];
            x = parent[x];
        }
        return x;
    }

    private void union(int[] parent, int a, int b) {
        int ra = find(parent, a), rb = find(parent, b);
        if (ra != rb) {
            parent[ra] = rb;
        }
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[][][] graphs = {
            {{1, 1, 0}, {1, 1, 0}, {0, 0, 1}},
            {{1, 0, 0}, {0, 1, 0}, {0, 0, 1}},
            {{1, 1, 1}, {1, 1, 1}, {1, 1, 1}},
            {{1, 0, 1, 0}, {0, 1, 0, 0}, {1, 0, 1, 1}, {0, 0, 1, 1}}
        };
        int[][] initials = {
            {0, 1},
            {0, 2},
            {1, 2},
            {0, 2}
        };
        // case4:{0,2,3} 同块两个感染源,删除无收益,兜底返回最小索引 0(Python 核对)
        int[] exp = {0, 0, 1, 0};
        for (int t = 0; t < graphs.length; ++t) {
            ++total;
            int g1 = s.minMalwareSpread(graphs[t], initials[t]);
            int g2 = s.minMalwareSpread2(graphs[t], initials[t]);
            if (g1 == exp[t] && g2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 924-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0924 PASS ==");
        } else {
            System.out.println("== 0924 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一(DFS 染色)O(n^2)O(n)
解法二(并查集)O(n^2 * α)O(n)

925. Long Pressed Name(长按键入)

题目描述

你的朋友正在使用键盘输入他的名字 name。偶尔,在键入字符 c 时,按键可能会被长按,
而字符可能被输入 1 次或多次。
你将会检查键盘输入的字符 typed。如果它对应的可能是你的朋友的名字(其中一些字符可能
被长按),那么就返回 True。

示例 1

  • 输入:name = "alex", typed = "aaleex"
  • 输出:true
  • 解释:'alex' 中的 'a' 和 'e' 被长按。

示例 2

  • 输入:name = "saeed", typed = "ssaaedd"
  • 输出:false
  • 解释:'e' 一定需要被键入两次,但在 typed 的输出中不是这样。

示例 3

  • 输入:name = "leelee", typed = "lleeelee"
  • 输出:true

示例 4

  • 输入:name = "laiden", typed = "laiden"
  • 输出:true
    提示:
  • 1 <= name.length, typed.length <= 1000
  • name 和 typed 的字符都是小写字母

解题思路

双指针扫描:i 指向 name、j 指向 typed。

  • 若 name[i] == typed[j],双双前进;
  • 否则若 typed[j] == typed[j-1](长按多打的字符),j 前进;
  • 否则直接失败。
    结束条件:name 走完且 typed 剩余字符全是最后一个字符的重复。
    解法一(双指针,主提交):按上述规则 O(len(name) + len(typed))。
    解法二(分组计数,对照实现):把两个串各自按连续相同字符分组为 (char, count) 列表,
    逐一比较:字符必须一致且 name 组内长度 <= typed 组内长度。直观但代码稍长。

代码实现

class Solution {

    // 解法一:双指针(推荐提交)
    public boolean isLongPressedName(String name, String typed) {
        int i = 0, j = 0;
        int m = name.length(), n = typed.length();
        while (j < n) {
            if (i < m && name.charAt(i) == typed.charAt(j)) {
                ++i;
                ++j;
            } else if (j > 0 && typed.charAt(j) == typed.charAt(j - 1)) {
                ++j;   // 长按重复字符
            } else {
                return false;
            }
        }
        return i == m;
    }

    // 解法二:分组计数(对照实现)
    public boolean isLongPressedName2(String name, String typed) {
        java.util.List<int[]> g1 = group(name);
        java.util.List<int[]> g2 = group(typed);
        if (g1.size() != g2.size()) {
            return false;
        }
        for (int k = 0; k < g1.size(); ++k) {
            if (g1.get(k)[0] != g2.get(k)[0] || g1.get(k)[1] > g2.get(k)[1]) {
                return false;
            }
        }
        return true;
    }

    private java.util.List<int[]> group(String s) {
        java.util.List<int[]> res = new java.util.ArrayList<>();
        int i = 0;
        while (i < s.length()) {
            int j = i;
            while (j < s.length() && s.charAt(j) == s.charAt(i)) {
                ++j;
            }
            res.add(new int[]{s.charAt(i), j - i});
            i = j;
        }
        return res;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        String[] names = {"alex", "saeed", "leelee", "laiden", "abc", "xnhtq"};
        String[] typeds = {"aaleex", "ssaaedd", "lleeelee", "laiden", "aabbcc", "xhhttqq"};
        boolean[] exp = {true, false, true, true, true, false};
        for (int t = 0; t < names.length; ++t) {
            ++total;
            boolean g1 = s.isLongPressedName(names[t], typeds[t]);
            boolean g2 = s.isLongPressedName2(names[t], typeds[t]);
            if (g1 == exp[t] && g2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 925-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0925 PASS ==");
        } else {
            System.out.println("== 0925 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一(双指针)O(len(name) + len(typed))O(1)
解法二(分组计数)O(len(name) + len(typed))O(len)