LeetCode826~839题目解析

3 阅读10分钟

LeetCode 826-839. 题解合集


826. Most Profit Assigning Work(安排工作以达到最大收益)

题目描述

你有 n 个工作和 m 个工人。给定三个数组:difficulty、profit 和 worker,其中:

  1. difficulty[i] 表示第 i 个工作的难度,profit[i] 表示第 i 个工作的收益。
  2. worker[i] 是第 i 个工人的能力,即该工人只能完成难度小于等于 worker[i] 的工作。
    每个工人最多只能安排一个工作,但是一个工作可以完成多次。
    如果 3 个工人都尝试完成一份报酬为 1的同样工作,那么总收益为1 的同样工作,那么总收益为 3。如果一个工人不能完成
    任何工作,他的收益为 $0。
    返回在把工人分配到工作岗位后,我们所能获得的最大利润。

示例 1

  • 输入:difficulty = [2,4,6,8,10], profit = [10,20,30,40,50], worker = [4,5,6,7]
  • 输出:100
  • 解释:工人被分配的工作难度是 [4,4,6,6],分别获得 [20,20,30,30] 的收益。

示例 2

  • 输入:difficulty = [85,47,57], profit = [24,66,99], worker = [40,25,25]
  • 输出:0
    提示:
  • n == difficulty.length
  • n == profit.length
  • m == worker.length
  • 1 <= n, m <= 10^4
  • 1 <= difficulty[i], profit[i], worker[i] <= 10^5

解题思路

解法一(排序 + 双指针,主提交):将工作按难度升序排列,将工人按能力升序排列。遍历工人,
用一个指针推进所有难度不超过当前工人能力的工作,并维护这些工作收益的最大值 mx,则该工人
的最佳收益就是 mx,累加即可。
解法二(数组前缀最大值,对照实现):记 M 为最大难度,数组 f[d] 记录难度恰为 d 的
最大收益,再做前缀 f[i] = max(f[i], f[i-1]),使 f[i] 表示难度不超过 i 的最佳收益。
每个工人直接查 f[min(w, M)] 累加,省去对工人排序。

代码实现

import java.util.Arrays;
class Solution {

    // 解法一:排序 + 双指针(推荐提交)
    public int maxProfitAssignment(int[] difficulty, int[] profit, int[] worker) {
        int n = profit.length;
        int[][] jobs = new int[n][2];
        for (int i = 0; i < n; ++i) {
            jobs[i][0] = difficulty[i];
            jobs[i][1] = profit[i];
        }
        Arrays.sort(jobs, (a, b) -> a[0] - b[0]);
        Arrays.sort(worker);
        int ans = 0, mx = 0, i = 0;
        for (int w : worker) {
            while (i < n && jobs[i][0] <= w) {
                mx = Math.max(mx, jobs[i][1]);
                ++i;
            }
            ans += mx;
        }
        return ans;
    }

    // 解法二:数组前缀最大值(对照实现)
    public int maxProfitAssignment2(int[] difficulty, int[] profit, int[] worker) {
        int m = 0;
        for (int d : difficulty) {
            m = Math.max(m, d);
        }
        int[] f = new int[m + 1];
        int n = profit.length;
        for (int i = 0; i < n; ++i) {
            int d = difficulty[i];
            f[d] = Math.max(f[d], profit[i]);
        }
        for (int i = 1; i <= m; ++i) {
            f[i] = Math.max(f[i], f[i - 1]);
        }
        int ans = 0;
        for (int w : worker) {
            ans += f[Math.min(w, m)];
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;

        ++total; if (sol.maxProfitAssignment(new int[]{2,4,6,8,10}, new int[]{10,20,30,40,50}, new int[]{4,5,6,7}) == 100) ++pass; else System.out.println("FAIL 826-1");
        ++total; if (sol.maxProfitAssignment(new int[]{85,47,57}, new int[]{24,66,99}, new int[]{40,25,25}) == 0) ++pass; else System.out.println("FAIL 826-2");
        ++total; if (sol.maxProfitAssignment(new int[]{2,3,4,5}, new int[]{50,10,40,30}, new int[]{2,3,4,5}) == 200) ++pass; else System.out.println("FAIL 826-3");
        ++total; if (sol.maxProfitAssignment(new int[]{5,50,92,21,24,70,17,63,30,53}, new int[]{68,100,3,99,56,43,26,93,4,100}, new int[]{88,85,22,25,54,42,42,45,32,2}) == 894) ++pass; else System.out.println("FAIL 826-4");

        ++total; if (sol.maxProfitAssignment2(new int[]{2,4,6,8,10}, new int[]{10,20,30,40,50}, new int[]{4,5,6,7}) == 100) ++pass; else System.out.println("FAIL 826-5");
        ++total; if (sol.maxProfitAssignment2(new int[]{85,47,57}, new int[]{24,66,99}, new int[]{40,25,25}) == 0) ++pass; else System.out.println("FAIL 826-6");
        ++total; if (sol.maxProfitAssignment2(new int[]{5,50,92,21,24,70,17,63,30,53}, new int[]{68,100,3,99,56,43,26,93,4,100}, new int[]{88,85,22,25,54,42,42,45,32,2}) == 894) ++pass; else System.out.println("FAIL 826-7");

        if (pass == total) {
            System.out.println("== 0826 PASS ==");
        } else {
            System.out.println("== 0826 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(n log n + m log m)O(n)
解法二O(n + M)O(M)

827. Making A Large Island(最大人工岛)

题目描述

给你一个大小为 n x n 二进制矩阵 grid。最多只能将一格 0 变成 1。
返回执行此操作后,grid 中最大的岛屿面积是多少?
岛屿由一组上、下、左、右四个方向相连的 1 形成。

示例 1

  • 输入:grid = [[1,0],[0,1]]
  • 输出:3
  • 解释:将一格 0 变成 1,最终连通两个小岛得到面积为 3 的岛屿。

示例 2

  • 输入:grid = [[1,1],[1,0]]
  • 输出:4

示例 3

  • 输入:grid = [[1,1],[1,1]]
  • 输出:4
    提示:
  • n == grid.length
  • n == grid[i].length
  • 1 <= n <= 500
  • grid[i][j] 为 0 或 1

解题思路

解法一(DFS + 编号,主提交):第一遍遍历,给每座岛屿一个唯一的编号 root,并用数组
p[i][j] 记录每个格子所属的岛屿编号,cnt[root] 记录每座岛的面积。第二遍遍历每个 0,
收集其四邻的集合去重,将不同岛屿面积相加再加一,即为把该格填 1 后的面积,取最大值。
若矩阵全为 1,答案为最大岛屿面积。
解法二(并查集,对照实现):把所有为 1 且相邻的格子合并到同一集合,统计每个集合的大小。
再枚举每个 0,将四邻所属集合(去重)的大小相加再加一,取最大值。

代码实现

import java.util.HashSet;
import java.util.Set;
class Solution {

    private int n;
    private int root;
    private int[] cnt;
    private int[][] p;
    private int[][] grid;
    private final int[] dirs = {-1, 0, 1, 0, -1};

    // 解法一:DFS 编号 + 枚举 0(推荐提交)
    public int largestIsland(int[][] grid) {
        n = grid.length;
        p = new int[n][n];
        this.grid = grid;
        cnt = new int[n * n + 1];
        int ans = 0;
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < n; ++j) {
                if (grid[i][j] == 1 && p[i][j] == 0) {
                    ++root;
                    ans = Math.max(ans, dfs(i, j));
                }
            }
        }
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < n; ++j) {
                if (grid[i][j] == 0) {
                    Set<Integer> s = new HashSet<>();
                    for (int k = 0; k < 4; ++k) {
                        int x = i + dirs[k];
                        int y = j + dirs[k + 1];
                        if (x >= 0 && x < n && y >= 0 && y < n) {
                            s.add(p[x][y]);
                        }
                    }
                    int t = 1;
                    for (int x : s) {
                        t += cnt[x];
                    }
                    ans = Math.max(ans, t);
                }
            }
        }
        return ans;
    }

    private int dfs(int i, int j) {
        p[i][j] = root;
        ++cnt[root];
        for (int k = 0; k < 4; ++k) {
            int x = i + dirs[k];
            int y = j + dirs[k + 1];
            if (x >= 0 && x < n && y >= 0 && y < n && grid[x][y] == 1 && p[x][y] == 0) {
                dfs(x, y);
            }
        }
        return cnt[root];
    }

    // ===== 解法二:并查集(对照实现)=====
    private int[] parent;
    private int[] size;

    class UnionFind {
        UnionFind(int total) {
            parent = new int[total];
            size = new int[total];
            for (int i = 0; i < total; ++i) {
                parent[i] = i;
            }
        }
        int find(int x) {
            if (parent[x] != x) {
                parent[x] = find(parent[x]);
            }
            return parent[x];
        }
        void union(int a, int b) {
            int ra = find(a), rb = find(b);
            if (ra == rb) {
                return;
            }
            if (size[ra] < size[rb]) {
                int tmp = ra;
                ra = rb;
                rb = tmp;
            }
            parent[rb] = ra;
            size[ra] += size[rb];
        }
        void add(int x) {
            size[x] = 1;
        }
        int sizeOf(int root) {
            return size[root];
        }
    }

    public int largestIsland2(int[][] grid) {
        n = grid.length;
        UnionFind uf = new UnionFind(n * n);
        boolean[] has = new boolean[n * n];
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < n; ++j) {
                if (grid[i][j] == 1) {
                    int id = i * n + j;
                    uf.add(id);
                    has[id] = true;
                    if (i > 0 && grid[i - 1][j] == 1) {
                        uf.union(id, (i - 1) * n + j);
                    }
                    if (j > 0 && grid[i][j - 1] == 1) {
                        uf.union(id, i * n + j - 1);
                    }
                }
            }
        }
        int ans = 0;
        for (int i = 0; i < n * n; ++i) {
            if (has[i]) {
                ans = Math.max(ans, uf.sizeOf(uf.find(i)));
            }
        }
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < n; ++j) {
                if (grid[i][j] == 0) {
                    Set<Integer> s = new HashSet<>();
                    for (int k = 0; k < 4; ++k) {
                        int x = i + dirs[k];
                        int y = j + dirs[k + 1];
                        if (x >= 0 && x < n && y >= 0 && y < n && grid[x][y] == 1) {
                            s.add(uf.find(x * n + y));
                        }
                    }
                    int t = 1;
                    for (int r : s) {
                        t += uf.sizeOf(r);
                    }
                    ans = Math.max(ans, t);
                }
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;

        ++total; if (sol.largestIsland(new int[][]{{1,0},{0,1}}) == 3) ++pass; else System.out.println("FAIL 827-1");
        ++total; if (sol.largestIsland(new int[][]{{1,1},{1,0}}) == 4) ++pass; else System.out.println("FAIL 827-2");
        ++total; if (sol.largestIsland(new int[][]{{1,1},{1,1}}) == 4) ++pass; else System.out.println("FAIL 827-3");
        ++total; if (sol.largestIsland(new int[][]{{0,0},{0,0}}) == 1) ++pass; else System.out.println("FAIL 827-4");
        ++total; if (sol.largestIsland(new int[][]{{1,0,1},{0,0,0},{1,0,1}}) == 3) ++pass; else System.out.println("FAIL 827-5");

        ++total; if (sol.largestIsland2(new int[][]{{1,0},{0,1}}) == 3) ++pass; else System.out.println("FAIL 827-6");
        ++total; if (sol.largestIsland2(new int[][]{{1,1},{1,0}}) == 4) ++pass; else System.out.println("FAIL 827-7");
        ++total; if (sol.largestIsland2(new int[][]{{0,0},{0,0}}) == 1) ++pass; else System.out.println("FAIL 827-8");
        ++total; if (sol.largestIsland2(new int[][]{{1,0,1},{0,0,0},{1,0,1}}) == 3) ++pass; else System.out.println("FAIL 827-9");

        if (pass == total) {
            System.out.println("== 0827 PASS ==");
        } else {
            System.out.println("== 0827 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(n^2)O(n^2)
解法二O(n^2 * alpha(n^2))O(n^2)

828. Count Unique Characters of All Substrings of a Given String

题目描述

我们定义了一个函数 countUniqueChars(s) 来统计字符串 s 中的唯一字符,并返回唯一字符的
个数。例如:s = "LEETCODE",则其中 "L"、"T"、"C"、"O"、"D" 都是唯一字符,
所以 countUniqueChars(s) = 5。
本题将会给你一个字符串 s,我们需要返回 countUniqueChars(t) 的总和,其中 t 是 s 的
子字符串。输入用例保证返回值为 32 位整数。
注意,某些子字符串可能是重复的,但你统计时也必须算上这些重复的子字符串(也就是说,你必须
统计 s 的所有子字符串中的唯一字符)。

示例 1

  • 输入:s = "ABC"
  • 输出:10
  • 解释:所有可能的子串为:"A","B","C","AB","BC" 和 "ABC"。每一个子串都由独特字符构成,
    所以其长度总和为:1 + 1 + 1 + 2 + 2 + 3 = 10。

示例 2

  • 输入:s = "ABA"
  • 输出:8

示例 3

  • 输入:s = "LEETCODE"
  • 输出:92
    提示:
  • 1 <= s.length <= 10^5
  • s 只包含大写英文字符

解题思路

解法一(贡献法/位置表,主提交):一个字符 c 对答案有贡献,当且仅当存在某个子串恰好包含
它一次。对每种字母记录所有出现下标,两端补哨兵 -1 与 len(s)。若某次出现下标为 p,
左侧相邻出现为 l、右侧相邻出现为 r,则包含 p 且不包含其他同字符的子串左端点可取
(l, p]、右端点可取 [p, r),个数为 (p - l) * (r - p),累加所有字符所有出现即可。
解法二(在线贡献,对照实现):扫描时对每个位置 i 作为子串右端点,维护每个字符最近两次
出现的下标。字符 c 若上次出现在 last1[c]、再上次出现在 last2[c],则以 i 结尾的
子串中,c 作为唯一字符的子串个数为 last1[c] - last2[c](左端点范围),逐位累加得到
相同答案。

代码实现

import java.util.ArrayList;
import java.util.List;
class Solution {

    // 解法一:贡献法(推荐提交)
    public int uniqueLetterString(String s) {
        List<Integer>[] d = new List[26];
        for (int i = 0; i < 26; ++i) {
            d[i] = new ArrayList<>();
            d[i].add(-1);
        }
        int n = s.length();
        for (int i = 0; i < n; ++i) {
            d[s.charAt(i) - 'A'].add(i);
        }
        int ans = 0;
        for (List<Integer> v : d) {
            v.add(n);
            for (int i = 1; i < v.size() - 1; ++i) {
                ans += (v.get(i) - v.get(i - 1)) * (v.get(i + 1) - v.get(i));
            }
        }
        return ans;
    }

    // 解法二:在线贡献(对照实现)
    // 对每个右端点 i,贡献 = 所有字符在该右端点下"作为唯一字符"的子串个数。
    // 字符 c 对当前右端点的贡献 = last1[c] - last2[c](左端点可取 (last2, last1])。
    // 扫描到新位置 i 时,只有字符 c 的贡献发生变化,把旧贡献换掉即可。
    public int uniqueLetterString2(String s) {
        int n = s.length();
        int[] last1 = new int[26]; // 最近一次出现下标(默认 -1 表示未出现)
        int[] last2 = new int[26]; // 倒数第二次出现下标
        for (int i = 0; i < 26; ++i) {
            last1[i] = -1;
            last2[i] = -1;
        }
        int total = 0; // 当前维护的"每个右端点累积贡献"中的字符贡献集合
        int ans = 0;
        for (int i = 0; i < n; ++i) {
            int c = s.charAt(i) - 'A';
            // 移除字符 c 的旧贡献(若曾出现)
            if (last1[c] != -1) {
                total -= last1[c] - last2[c];
            }
            last2[c] = last1[c];
            last1[c] = i;
            // 加入字符 c 的新贡献
            total += last1[c] - last2[c];
            // 以 i 为右端点的所有子串贡献累加
            ans += total;
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;

        ++total; if (sol.uniqueLetterString("ABC") == 10) ++pass; else System.out.println("FAIL 828-1");
        ++total; if (sol.uniqueLetterString("ABA") == 8) ++pass; else System.out.println("FAIL 828-2");
        ++total; if (sol.uniqueLetterString("LEETCODE") == 92) ++pass; else System.out.println("FAIL 828-3");
        ++total; if (sol.uniqueLetterString("A") == 1) ++pass; else System.out.println("FAIL 828-4");
        ++total; if (sol.uniqueLetterString("AA") == 2) ++pass; else System.out.println("FAIL 828-5");

        ++total; if (sol.uniqueLetterString2("ABC") == 10) ++pass; else System.out.println("FAIL 828-6");
        ++total; if (sol.uniqueLetterString2("ABA") == 8) ++pass; else System.out.println("FAIL 828-7");
        ++total; if (sol.uniqueLetterString2("LEETCODE") == 92) ++pass; else System.out.println("FAIL 828-8");
        ++total; if (sol.uniqueLetterString2("AA") == 2) ++pass; else System.out.println("FAIL 828-9");

        if (pass == total) {
            System.out.println("== 0828 PASS ==");
        } else {
            System.out.println("== 0828 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

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

829. Consecutive Numbers Sum(连续整数求和)

题目描述

给定一个正整数 n,返回连续正整数满足所有数字之和为 n 的组数。

示例 1

  • 输入:n = 5
  • 输出:2
  • 解释:5 = 2 + 3,共有两组连续整数 ([5],[2,3]) 求和后为 5。

示例 2

  • 输入:n = 9
  • 输出:3
  • 解释:9 = 4 + 5 = 2 + 3 + 4

示例 3

  • 输入:n = 15
  • 输出:4
  • 解释:15 = 8 + 7 = 4 + 5 + 6 = 1 + 2 + 3 + 4 + 5
    提示:
  • 1 <= n <= 10^9

解题思路

连续正整数构成公差为 1 的等差数列。设首项为 a,项数为 k,则
n = (a + a + k - 1) * k / 2,即 2n = k * (2a + k - 1)。
因此 k 必须整除 2n,且 2n/k - k + 1 为偶数;又 a >= 1 推出
k * (k + 1) <= 2n。
解法一(枚举长度 k,主提交):从 k = 1 递增到满足 k * (k + 1) <= 2n,对每个 k 判断
能否整除且 (2n/k + 1 - k) % 2 == 0,计数即可。
解法二(因数对称枚举,对照实现):利用 2n = k * (2a + k - 1),对 k 枚举到 sqrt(2n),
每次同时检查因数对 k 与 2n/k 是否构成合法长度,结果与解法一一致但枚举范围更小。

代码实现

class Solution {

    // 解法一:枚举长度 k(推荐提交)
    public int consecutiveNumbersSum(int n) {
        long m = (long) n << 1;
        int ans = 0;
        for (long k = 1; k * (k + 1) <= m; ++k) {
            if (m % k == 0 && (m / k + 1 - k) % 2 == 0) {
                ++ans;
            }
        }
        return ans;
    }

    // 解法二:因数对称枚举(对照实现)
    // 利用 2n = k * (2a + k - 1),k 与 2n/k 配对。对每个因数对,只需分别验证
    // k 与 2n/k 作为长度时是否满足条件(同样要满足 k(k+1) <= 2n 的长度上界)。
    public int consecutiveNumbersSum2(int n) {
        long m = (long) n << 1;
        int ans = 0;
        for (long k = 1; k * k <= m; ++k) {
            if (m % k != 0) {
                continue;
            }
            if (k * (k + 1) <= m && (m / k + 1 - k) % 2 == 0) {
                ++ans;
            }
            long other = m / k;
            if (other != k && other * (other + 1) <= m && (m / other + 1 - other) % 2 == 0) {
                ++ans;
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;

        ++total; if (sol.consecutiveNumbersSum(5) == 2) ++pass; else System.out.println("FAIL 829-1");
        ++total; if (sol.consecutiveNumbersSum(9) == 3) ++pass; else System.out.println("FAIL 829-2");
        ++total; if (sol.consecutiveNumbersSum(15) == 4) ++pass; else System.out.println("FAIL 829-3");
        ++total; if (sol.consecutiveNumbersSum(1) == 1) ++pass; else System.out.println("FAIL 829-4");
        ++total; if (sol.consecutiveNumbersSum(2) == 1) ++pass; else System.out.println("FAIL 829-5");
        ++total; if (sol.consecutiveNumbersSum(1000000000) == 10) ++pass; else System.out.println("FAIL 829-6");

        ++total; if (sol.consecutiveNumbersSum2(5) == 2) ++pass; else System.out.println("FAIL 829-7");
        ++total; if (sol.consecutiveNumbersSum2(9) == 3) ++pass; else System.out.println("FAIL 829-8");
        ++total; if (sol.consecutiveNumbersSum2(15) == 4) ++pass; else System.out.println("FAIL 829-9");
        ++total; if (sol.consecutiveNumbersSum2(1000000000) == 10) ++pass; else System.out.println("FAIL 829-10");

        if (pass == total) {
            System.out.println("== 0829 PASS ==");
        } else {
            System.out.println("== 0829 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(sqrt(n))O(1)
解法二O(sqrt(n))O(1)

830. Positions of Large Groups(较大分组的位置)

题目描述

在一个由小写字母构成的字符串 s 中,包含由一些连续的相同字符所构成的分组。
例如,在字符串 s = "abbxxxxzyy" 中,就含有 "a"、"bb"、"xxxx"、"z" 和 "yy"
这样的一些分组。分组可以用区间 [start, end] 表示。我们称所有包含大于或等于三个连续字符
的分组为较大分组。找到每一个较大分组的区间,按起始位置下标递增顺序排序后返回结果。

示例 1

  • 输入:s = "abbxxxxzzy"
  • 输出:[[3,6]]

示例 2

  • 输入:s = "abc"
  • 输出:[]

示例 3

  • 输入:s = "abcdddeeeeaabbbcd"
  • 输出:[[3,5],[6,9],[12,14]]

示例 4

  • 输入:s = "aba"
  • 输出:[]
    提示:
  • 1 <= s.length <= 1000
  • s 仅含小写英文字母

解题思路

解法一(双指针,主提交):指针 i 指向分组起点,指针 j 向右扩展直到字符变化,若
j - i >= 3 则将 [i, j-1] 加入答案,再令 i = j 处理下一段。
解法二(逐位扫描,对照实现):用 start 记录当前组的起点,从 1 开始扫描:当 s[i] 与
s[i-1] 不同时结算上一组(长度达到 3 则记录),并重置起点;扫描结束后再结算最后一组。

代码实现

import java.util.ArrayList;
import java.util.List;
class Solution {

    // 解法一:双指针(推荐提交)
    public List<List<Integer>> largeGroupPositions(String s) {
        int n = s.length();
        List<List<Integer>> ans = new ArrayList<>();
        int i = 0;
        while (i < n) {
            int j = i;
            while (j < n && s.charAt(j) == s.charAt(i)) {
                ++j;
            }
            if (j - i >= 3) {
                List<Integer> seg = new ArrayList<>(2);
                seg.add(i);
                seg.add(j - 1);
                ans.add(seg);
            }
            i = j;
        }
        return ans;
    }

    // 解法二:逐位扫描(对照实现)
    public List<List<Integer>> largeGroupPositions2(String s) {
        int n = s.length();
        List<List<Integer>> ans = new ArrayList<>();
        int start = 0;
        for (int i = 1; i < n; ++i) {
            if (s.charAt(i) != s.charAt(i - 1)) {
                if (i - start >= 3) {
                    List<Integer> seg = new ArrayList<>(2);
                    seg.add(start);
                    seg.add(i - 1);
                    ans.add(seg);
                }
                start = i;
            }
        }
        if (n - start >= 3) {
            List<Integer> seg = new ArrayList<>(2);
            seg.add(start);
            seg.add(n - 1);
            ans.add(seg);
        }
        return ans;
    }

    private static boolean eq(List<List<Integer>> a, int[][] b) {
        if (a.size() != b.length) {
            return false;
        }
        for (int i = 0; i < b.length; ++i) {
            List<Integer> seg = a.get(i);
            if (seg.size() != 2 || seg.get(0) != b[i][0] || seg.get(1) != b[i][1]) {
                return false;
            }
        }
        return true;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;

        ++total; if (eq(sol.largeGroupPositions("abbxxxxzzy"), new int[][]{{3,6}})) ++pass; else System.out.println("FAIL 830-1");
        ++total; if (eq(sol.largeGroupPositions("abc"), new int[][]{})) ++pass; else System.out.println("FAIL 830-2");
        ++total; if (eq(sol.largeGroupPositions("abcdddeeeeaabbbcd"), new int[][]{{3,5},{6,9},{12,14}})) ++pass; else System.out.println("FAIL 830-3");
        ++total; if (eq(sol.largeGroupPositions("aba"), new int[][]{})) ++pass; else System.out.println("FAIL 830-4");
        ++total; if (eq(sol.largeGroupPositions("aaa"), new int[][]{{0,2}})) ++pass; else System.out.println("FAIL 830-5");

        ++total; if (eq(sol.largeGroupPositions2("abbxxxxzzy"), new int[][]{{3,6}})) ++pass; else System.out.println("FAIL 830-6");
        ++total; if (eq(sol.largeGroupPositions2("abcdddeeeeaabbbcd"), new int[][]{{3,5},{6,9},{12,14}})) ++pass; else System.out.println("FAIL 830-7");
        ++total; if (eq(sol.largeGroupPositions2("aaa"), new int[][]{{0,2}})) ++pass; else System.out.println("FAIL 830-8");

        if (pass == total) {
            System.out.println("== 0830 PASS ==");
        } else {
            System.out.println("== 0830 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

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

831. Masking Personal Information(隐藏个人信息)

题目描述

给你一条个人信息字符串 s,可能表示一个邮箱地址,也可能表示一串电话号码。返回按规则隐藏
个人信息后的结果:
电子邮件地址:

  1. 一个名字,由大小写英文字母组成,后面跟着一个 '@' 字符,后面跟着一个域名,由大小写
    英文字母和一个位于中间的 '.' 字符组成。
  2. 名字和域名部分的大写英文字母应当转换成小写英文字母。
  3. 名字中间的字母(即,除第一个和最后一个字母外)必须用 5 个 "*****" 替换。
    电话号码:
  4. 电话号码可以由 10-13 位数字组成;后 10 位构成本地号码;前面剩下的 0-3 位构成国家代码。
  5. 利用 {'+', '-', '(', ')', ' '} 这些分隔字符按某种形式对上述数字进行分隔。
  6. 移除所有分隔字符;若国家代码为 0/1/2/3 位,分别格式化为 "***-***-XXXX" /
    "+*-***-***-XXXX" / "+**-***-***-XXXX" / "+***-***-***-XXXX","XXXX" 是
    最后 4 位本地号码。

示例 1

示例 2

  • 输入:s = "AB@qq.com"
  • 输出:"a*****b@qq.com"

示例 3

  • 输入:s = "1(234)567-890"
  • 输出:"--7890"
    提示:
  • s 是一个长度在 [1, 40] 之间的字符串
  • s 是由若干大小写英文字母、数字、'@'、'+'、'-'、'('、')'、' ' 组成的合法个人信息

解题思路

解法一(分类模拟,主提交):先判断字符串中是否包含 '@' 来决定走邮箱还是电话分支。
邮箱:找到 '@' 位置,名字部分取首尾字符小写、中间用 ***** 填充,域名整体转小写拼接。
电话:过滤出所有数字,后 10 位为本地号码;若总长大于 10,则前面多出来的位构成国家代码,
按国家代码位数拼接对应格式。
解法二(正则/字符流统一处理,对照实现):思路与解法一相同,但用 StringBuilder 显式逐字符
过滤数字,并先构造本地号码骨架再根据国家代码位数在前面插入 + 与对应数量的 *,逻辑
更直接便于维护。

代码实现

class Solution {

    // 解法一:分类模拟(推荐提交)
    public String maskPII(String s) {
        if (s.indexOf('@') >= 0) {
            int at = s.indexOf('@');
            String name = s.substring(0, at).toLowerCase();
            String domain = s.substring(at + 1).toLowerCase();
            return name.charAt(0) + "*****" + name.charAt(name.length() - 1) + "@" + domain;
        }
        StringBuilder digits = new StringBuilder();
        for (char c : s.toCharArray()) {
            if (Character.isDigit(c)) {
                digits.append(c);
            }
        }
        String num = digits.toString();
        String local = "***-***-" + num.substring(num.length() - 4);
        int country = num.length() - 10;
        if (country == 0) {
            return local;
        }
        StringBuilder sb = new StringBuilder("+");
        for (int i = 0; i < country; ++i) {
            sb.append('*');
        }
        sb.append('-').append(local);
        return sb.toString();
    }

    // 解法二:字符流过滤 + 骨架拼接(对照实现)
    public String maskPII2(String s) {
        Result r = getPhone(s);
        if (r != null) {
            return r.val;
        }
        int at = s.indexOf('@');
        String name = s.substring(0, at).toLowerCase();
        String domain = s.substring(at + 1).toLowerCase();
        return name.charAt(0) + "*****" + name.charAt(name.length() - 1) + "@" + domain;
    }

    private Result getPhone(String s) {
        StringBuilder digits = new StringBuilder();
        boolean hasDigit = false;
        for (char c : s.toCharArray()) {
            if (Character.isDigit(c)) {
                digits.append(c);
                hasDigit = true;
            }
        }
        if (!hasDigit) {
            return null;
        }
        String num = digits.toString();
        int country = num.length() - 10;
        String local = "***-***-" + num.substring(num.length() - 4);
        if (country <= 0) {
            return new Result(local);
        }
        StringBuilder sb = new StringBuilder("+");
        for (int i = 0; i < country; ++i) {
            sb.append('*');
        }
        sb.append('-').append(local);
        return new Result(sb.toString());
    }

    private static class Result {
        String val;
        Result(String v) {
            val = v;
        }
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;

        ++total; if (sol.maskPII("LeetCode@LeetCode.com").equals("l*****e@leetcode.com")) ++pass; else System.out.println("FAIL 831-1");
        ++total; if (sol.maskPII("AB@qq.com").equals("a*****b@qq.com")) ++pass; else System.out.println("FAIL 831-2");
        ++total; if (sol.maskPII("1(234)567-890").equals("***-***-7890")) ++pass; else System.out.println("FAIL 831-3");
        ++total; if (sol.maskPII("86-(10)12345678").equals("+**-***-***-5678")) ++pass; else System.out.println("FAIL 831-4");
        ++total; if (sol.maskPII("+111 (222) 333-4444").equals("+***-***-***-4444")) ++pass; else System.out.println("FAIL 831-5");

        ++total; if (sol.maskPII2("LeetCode@LeetCode.com").equals("l*****e@leetcode.com")) ++pass; else System.out.println("FAIL 831-6");
        ++total; if (sol.maskPII2("86-(10)12345678").equals("+**-***-***-5678")) ++pass; else System.out.println("FAIL 831-7");
        ++total; if (sol.maskPII2("+111 (222) 333-4444").equals("+***-***-***-4444")) ++pass; else System.out.println("FAIL 831-8");

        if (pass == total) {
            System.out.println("== 0831 PASS ==");
        } else {
            System.out.println("== 0831 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(n)O(n)
解法二O(n)O(n)

832. Flipping an Image(翻转图像)

题目描述

给定一个 n x n 的二进制矩阵 image,先水平翻转图像,然后反转图像并返回结果。
水平翻转图片就是将图片的每一行都进行翻转,即逆序。例如,水平翻转 [1,1,0] 的结果是
[0,1,1]。反转图片的意思是图片中的 0 全部被 1 替换,1 全部被 0 替换。例如,
反转 [0,1,1] 的结果是 [1,0,0]。

示例 1

  • 输入:image = [[1,1,0],[1,0,1],[0,0,0]]
  • 输出:[[1,0,0],[0,1,0],[1,1,1]]

示例 2

  • 输入:image = [[1,1,0,0],[1,0,0,1],[0,1,1,1],[1,0,1,0]]
  • 输出:[[1,1,0,0],[0,1,1,0],[0,0,0,1],[1,0,1,0]]
    提示:
  • n == image.length
  • n == image[i].length
  • 1 <= n <= 20
  • image[i][j] == 0 或 1

解题思路

解法一(双指针原地变换,主提交):水平翻转后再反转,等价于对每一行从两端向中间:交换
左右元素并各自取反。若行长为奇数,中间元素只需取反。一趟完成,空间 O(1)。
解法二(构造新矩阵,对照实现):按规则显式模拟:先对每行做水平翻转得到新行,再遍历新行
把每个元素取反填入答案矩阵,逻辑直观、便于对照验证。

代码实现

class Solution {

    // 解法一:双指针原地变换(推荐提交)
    public int[][] flipAndInvertImage(int[][] image) {
        int n = image.length;
        for (int[] row : image) {
            int i = 0, j = n - 1;
            while (i < j) {
                int tmp = row[i];
                row[i] = row[j] ^ 1;
                row[j] = tmp ^ 1;
                ++i;
                --j;
            }
            if (i == j) {
                row[i] ^= 1;
            }
        }
        return image;
    }

    // 解法二:显式模拟(对照实现)
    public int[][] flipAndInvertImage2(int[][] image) {
        int n = image.length;
        int[][] ans = new int[n][n];
        for (int r = 0; r < n; ++r) {
            for (int c = 0; c < n; ++c) {
                ans[r][c] = image[r][n - 1 - c] ^ 1;
            }
        }
        return ans;
    }

    private static boolean eq(int[][] a, int[][] b) {
        if (a.length != b.length) {
            return false;
        }
        for (int i = 0; i < a.length; ++i) {
            if (!java.util.Arrays.equals(a[i], b[i])) {
                return false;
            }
        }
        return true;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;

        ++total; if (eq(sol.flipAndInvertImage(new int[][]{{1,1,0},{1,0,1},{0,0,0}}), new int[][]{{1,0,0},{0,1,0},{1,1,1}})) ++pass; else System.out.println("FAIL 832-1");
        ++total; if (eq(sol.flipAndInvertImage(new int[][]{{1,1,0,0},{1,0,0,1},{0,1,1,1},{1,0,1,0}}), new int[][]{{1,1,0,0},{0,1,1,0},{0,0,0,1},{1,0,1,0}})) ++pass; else System.out.println("FAIL 832-2");
        ++total; if (eq(sol.flipAndInvertImage(new int[][]{{1}}), new int[][]{{0}})) ++pass; else System.out.println("FAIL 832-3");
        ++total; if (eq(sol.flipAndInvertImage(new int[][]{{0,1},{1,0}}), new int[][]{{0,1},{1,0}})) ++pass; else System.out.println("FAIL 832-4");

        ++total; if (eq(sol.flipAndInvertImage2(new int[][]{{1,1,0},{1,0,1},{0,0,0}}), new int[][]{{1,0,0},{0,1,0},{1,1,1}})) ++pass; else System.out.println("FAIL 832-5");
        ++total; if (eq(sol.flipAndInvertImage2(new int[][]{{1,1,0,0},{1,0,0,1},{0,1,1,1},{1,0,1,0}}), new int[][]{{1,1,0,0},{0,1,1,0},{0,0,0,1},{1,0,1,0}})) ++pass; else System.out.println("FAIL 832-6");
        ++total; if (eq(sol.flipAndInvertImage2(new int[][]{{0,1},{1,0}}), new int[][]{{0,1},{1,0}})) ++pass; else System.out.println("FAIL 832-7");

        if (pass == total) {
            System.out.println("== 0832 PASS ==");
        } else {
            System.out.println("== 0832 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(n^2)O(1)
解法二O(n^2)O(n^2)

833. Find And Replace in String(字符串中的查找与替换)

题目描述

你会得到一个字符串 s(索引从 0 开始),你必须对它执行 k 个替换操作。替换操作以三个
长度均为 k 的并行数组给出:indices、sources、targets。
要完成第 i 个替换操作:

  1. 检查子字符串 sources[i] 是否出现在原字符串 s 的索引 indices[i] 处。
  2. 如果没有出现,什么也不做。
  3. 如果出现,则用 targets[i] 替换该子字符串。
    所有替换操作必须同时发生,这意味着替换操作不应该影响彼此的索引。测试用例保证元素间不会
    重叠。
    在对 s 执行所有替换操作后返回结果字符串。

示例 1

  • 输入:s = "abcd", indices = [0,2], sources = ["a","cd"], targets = ["eee","ffff"]
  • 输出:"eeebffff"

示例 2

  • 输入:s = "abcd", indices = [0,2], sources = ["ab","ec"], targets = ["eee","ffff"]
  • 输出:"eeecd"
    提示:
  • 1 <= s.length <= 1000
  • k == indices.length == sources.length == targets.length
  • 1 <= k <= 100
  • 0 <= indices[i] < s.length
  • 1 <= sources[i].length, targets[i].length <= 50

解题思路

解法一(记录有效替换 + 顺序拼接,主提交):先逐个判断 s.substring(idx, idx+len) 是否等于
sources[i],命中则把 (idx, len, target) 记录到列表;由于互不重叠,按 idx 升序排序后
用指针依次跳过被替换的区间、拼接保留部分与替换串。
解法二(按索引映射重建,对照实现):用数组 map 记录每个位置是否作为某次替换的起点及
对应下标。遍历时若当前位置是替换起点则直接追加其 target 并跳到区间末尾,否则原样保留字符,
同样得到结果串。

代码实现

import java.util.ArrayList;
import java.util.List;
class Solution {

    // 解法一:收集有效替换后拼接(推荐提交)
    public String findReplaceString(String s, int[] indices, String[] sources, String[] targets) {
        int k = indices.length;
        List<int[]> ops = new ArrayList<>();
        for (int i = 0; i < k; ++i) {
            int idx = indices[i];
            String src = sources[i];
            if (idx + src.length() <= s.length() && s.substring(idx, idx + src.length()).equals(src)) {
                ops.add(new int[]{idx, src.length(), i});
            }
        }
        ops.sort((a, b) -> a[0] - b[0]);
        StringBuilder sb = new StringBuilder();
        int pos = 0;
        for (int[] op : ops) {
            int idx = op[0];
            sb.append(s, pos, idx);
            sb.append(targets[op[2]]);
            pos = idx + op[1];
        }
        sb.append(s, pos, s.length());
        return sb.toString();
    }

    // 解法二:映射重建(对照实现)
    public String findReplaceString2(String s, int[] indices, String[] sources, String[] targets) {
        int n = s.length();
        int[] match = new int[n];
        java.util.Arrays.fill(match, -1);
        int k = indices.length;
        for (int i = 0; i < k; ++i) {
            int idx = indices[i];
            String src = sources[i];
            if (idx + src.length() <= n && s.substring(idx, idx + src.length()).equals(src)) {
                match[idx] = i;
            }
        }
        StringBuilder sb = new StringBuilder();
        int i = 0;
        while (i < n) {
            if (match[i] >= 0) {
                sb.append(targets[match[i]]);
                i += sources[match[i]].length();
            } else {
                sb.append(s.charAt(i));
                ++i;
            }
        }
        return sb.toString();
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;

        ++total; if (sol.findReplaceString("abcd", new int[]{0,2}, new String[]{"a","cd"}, new String[]{"eee","ffff"}).equals("eeebffff")) ++pass; else System.out.println("FAIL 833-1");
        ++total; if (sol.findReplaceString("abcd", new int[]{0,2}, new String[]{"ab","ec"}, new String[]{"eee","ffff"}).equals("eeecd")) ++pass; else System.out.println("FAIL 833-2");
        ++total; if (sol.findReplaceString("vmokgggqzp", new int[]{3,5,1}, new String[]{"kg","ggq","mo"}, new String[]{"s","so","bfr"}).equals("vbfrssozp")) ++pass; else System.out.println("FAIL 833-3");
        ++total; if (sol.findReplaceString("abc", new int[]{0}, new String[]{"a"}, new String[]{"zzz"}).equals("zzzbc")) ++pass; else System.out.println("FAIL 833-4");

        ++total; if (sol.findReplaceString2("abcd", new int[]{0,2}, new String[]{"a","cd"}, new String[]{"eee","ffff"}).equals("eeebffff")) ++pass; else System.out.println("FAIL 833-5");
        ++total; if (sol.findReplaceString2("abcd", new int[]{0,2}, new String[]{"ab","ec"}, new String[]{"eee","ffff"}).equals("eeecd")) ++pass; else System.out.println("FAIL 833-6");
        ++total; if (sol.findReplaceString2("vmokgggqzp", new int[]{3,5,1}, new String[]{"kg","ggq","mo"}, new String[]{"s","so","bfr"}).equals("vbfrssozp")) ++pass; else System.out.println("FAIL 833-7");

        if (pass == total) {
            System.out.println("== 0833 PASS ==");
        } else {
            System.out.println("== 0833 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(k log k + n)O(k)
解法二O(k + n)O(n)

834. Sum of Distances in Tree(树中距离之和)

题目描述

给定一个无向、连通的树。树中有 n 个标记为 0...n-1 的节点以及 n-1 条边。
edges[i] = [ai, bi] 表示树中的节点 ai 和 bi 之间有一条边。
返回长度为 n 的数组 answer,其中 answer[i] 是树中第 i 个节点与所有其他节点之间的
距离之和。

示例 1

  • 输入:n = 6, edges = [[0,1],[0,2],[2,3],[2,4],[2,5]]
  • 输出:[8,12,6,10,10,10]

示例 2

  • 输入:n = 1, edges = []
  • 输出:[0]

示例 3

  • 输入:n = 2, edges = [[1,0]]
  • 输出:[1,1]
    提示:
  • 1 <= n <= 3 * 10^4
  • edges.length == n - 1
  • 给定的输入保证为有效的树

解题思路

解法一(树形 DP / 换根,主提交):先以 0 为根做一次 DFS,算出每棵子树的大小 sz[u] 以及
0 号节点到所有节点的距离和 ans[0]。再换根:若把根从 u 换到子节点 v,那么原来在 v
子树内的 sz[v] 个节点距离都减少 1,其余 n - sz[v] 个节点距离都增加 1,即
ans[v] = ans[u] + n - 2 * sz[v]。两次 DFS(第二次可用迭代栈防爆栈)后得到全部答案。
解法二(双 DFS 朴素换根,对照实现):先任取一点做 BFS/DFS 求到各点距离和,再对每个节点
依次执行同样的换根递推。与解法一本质相同,代码按「先加边、再两遍遍历」的顺序组织,便于验证。

代码实现

import java.util.ArrayList;
import java.util.List;
class Solution {

    private List<Integer>[] g;
    private int[] sz;
    private int[] ans;
    private int n;

    // 解法一:树形 DP(换根,推荐提交)
    public int[] sumOfDistancesInTree(int n, int[][] edges) {
        this.n = n;
        g = new List[n];
        for (int i = 0; i < n; ++i) {
            g[i] = new ArrayList<>();
        }
        for (int[] e : edges) {
            g[e[0]].add(e[1]);
            g[e[1]].add(e[0]);
        }
        sz = new int[n];
        ans = new int[n];
        dfs1(0, -1, 0);
        dfs2(0, -1);
        return ans;
    }

    // 返回以 u 为根的子树大小,同时累计深度总和
    private int dfs1(int u, int fa, int depth) {
        ans[0] += depth;
        int sum = 1;
        for (int v : g[u]) {
            if (v != fa) {
                sum += dfs1(v, u, depth + 1);
            }
        }
        sz[u] = sum;
        return sum;
    }

    private void dfs2(int u, int fa) {
        for (int v : g[u]) {
            if (v != fa) {
                ans[v] = ans[u] + n - 2 * sz[v];
                dfs2(v, u);
            }
        }
    }

    // 解法二:显式栈换根(对照实现,避免递归深度问题)
    public int[] sumOfDistancesInTree2(int n, int[][] edges) {
        List<Integer>[] adj = new List[n];
        for (int i = 0; i < n; ++i) {
            adj[i] = new ArrayList<>();
        }
        for (int[] e : edges) {
            adj[e[0]].add(e[1]);
            adj[e[1]].add(e[0]);
        }
        int[] size = new int[n];
        // 栈式前序遍历得到节点顺序(父节点总在子节点之前)
        int[] stack = new int[n];
        int[] parent = new int[n];
        java.util.Arrays.fill(parent, -1);
        int top = 0;
        stack[top++] = 0;
        int[] order = new int[n];
        int cnt = 0;
        while (top > 0) {
            int u = stack[--top];
            order[cnt++] = u;
            for (int v : adj[u]) {
                if (v != parent[u]) {
                    parent[v] = u;
                    stack[top++] = v;
                }
            }
        }
        // 逆序处理:先叶子后根,累加子树大小
        java.util.Arrays.fill(size, 1);
        for (int i = cnt - 1; i > 0; --i) {
            int u = order[i];
            size[parent[u]] += size[u];
        }
        // 根节点距离和(BFS 求深度后累加)
        int rootSum = depthSum(n, adj);
        int[] res = new int[n];
        res[0] = rootSum;
        // 按前序顺序递推换根:res[u] = res[p] + n - 2 * size[u]
        for (int i = 1; i < cnt; ++i) {
            int u = order[i];
            int p = parent[u];
            res[u] = res[p] + n - 2 * size[u];
        }
        return res;
    }

    private int depthSum(int n, List<Integer>[] adj) {
        int[] depth = new int[n];
        java.util.Arrays.fill(depth, -1);
        depth[0] = 0;
        java.util.ArrayDeque<Integer> q = new java.util.ArrayDeque<>();
        q.offer(0);
        int sum = 0;
        while (!q.isEmpty()) {
            int u = q.poll();
            sum += depth[u];
            for (int v : adj[u]) {
                if (depth[v] == -1) {
                    depth[v] = depth[u] + 1;
                    q.offer(v);
                }
            }
        }
        return sum;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;

        ++total; if (java.util.Arrays.equals(sol.sumOfDistancesInTree(6, new int[][]{{0,1},{0,2},{2,3},{2,4},{2,5}}), new int[]{8,12,6,10,10,10})) ++pass; else System.out.println("FAIL 834-1");
        ++total; if (java.util.Arrays.equals(sol.sumOfDistancesInTree(1, new int[][]{}), new int[]{0})) ++pass; else System.out.println("FAIL 834-2");
        ++total; if (java.util.Arrays.equals(sol.sumOfDistancesInTree(2, new int[][]{{1,0}}), new int[]{1,1})) ++pass; else System.out.println("FAIL 834-3");
        ++total; if (java.util.Arrays.equals(sol.sumOfDistancesInTree(3, new int[][]{{0,1},{1,2}}), new int[]{3,2,3})) ++pass; else System.out.println("FAIL 834-4");

        ++total; if (java.util.Arrays.equals(sol.sumOfDistancesInTree2(6, new int[][]{{0,1},{0,2},{2,3},{2,4},{2,5}}), new int[]{8,12,6,10,10,10})) ++pass; else System.out.println("FAIL 834-5");
        ++total; if (java.util.Arrays.equals(sol.sumOfDistancesInTree2(1, new int[][]{}), new int[]{0})) ++pass; else System.out.println("FAIL 834-6");
        ++total; if (java.util.Arrays.equals(sol.sumOfDistancesInTree2(3, new int[][]{{0,1},{1,2}}), new int[]{3,2,3})) ++pass; else System.out.println("FAIL 834-7");

        if (pass == total) {
            System.out.println("== 0834 PASS ==");
        } else {
            System.out.println("== 0834 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(n)O(n)
解法二O(n)O(n)

835. Image Overlap(图像重叠)

题目描述

给你两个图像 img1 和 img2,两个图像的大小都是 n x n,用大小相同的二进制正方形矩阵
表示。转换其中一个图像,将所有的 1 向左、右、上或下滑动任何数量的单位,然后把它放在
另一个图像的上面。该转换的重叠是指两个图像都具有 1 的位置的数目。
请注意,转换不包括向任何方向旋转。越过矩阵边界的 1 都将被清除。
最大可能的重叠数量是多少?

示例 1

  • 输入:img1 = [[1,1,0],[0,1,0],[0,1,0]], img2 = [[0,0,0],[0,1,1],[0,0,1]]
  • 输出:3

示例 2

  • 输入:img1 = [[1]], img2 = [[1]]
  • 输出:1

示例 3

  • 输入:img1 = [[0]], img2 = [[0]]
  • 输出:0
    提示:
  • n == img1.length == img1[i].length
  • n == img2.length == img2[i].length
  • 1 <= n <= 30
  • img1[i][j] 为 0 或 1
  • img2[i][j] 为 0 或 1

解题思路

解法一(偏移量众数,主提交):把 img1 中每个 1 的位置记为 (r1, c1),img2 中每个 1
记为 (r2, c2)。若将 img1 平移 (dr, dc) 后其某个 1 与 img2 的某个 1 重合,则
(r2 - r1, c2 - c1) = (dr, dc)。因此统计所有「img1 的 1 → img2 的 1」配对偏移量的众数,
出现次数最多的偏移量对应的配对数量就是最大重叠数。
解法二(枚举平移量暴力验证,对照实现):枚举所有允许的平移量 (dr, dc) 范围
[-(n-1), n-1],对每个平移量直接扫描重叠区域统计两个矩阵同为 1 的格子数,取最大值。
正确但复杂度为 O(n^4),仅作为对照。

代码实现

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
class Solution {

    // 解法一:偏移量众数(推荐提交)
    public int largestOverlap(int[][] img1, int[][] img2) {
        int n = img1.length;
        List<int[]> ones1 = new ArrayList<>();
        List<int[]> ones2 = new ArrayList<>();
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < n; ++j) {
                if (img1[i][j] == 1) {
                    ones1.add(new int[]{i, j});
                }
                if (img2[i][j] == 1) {
                    ones2.add(new int[]{i, j});
                }
            }
        }
        Map<String, Integer> cnt = new HashMap<>();
        int best = 0;
        for (int[] a : ones1) {
            for (int[] b : ones2) {
                String key = (b[0] - a[0]) + "," + (b[1] - a[1]);
                int v = cnt.getOrDefault(key, 0) + 1;
                cnt.put(key, v);
                best = Math.max(best, v);
            }
        }
        return best;
    }

    // 解法二:枚举平移量(对照实现)
    public int largestOverlap2(int[][] img1, int[][] img2) {
        int n = img1.length;
        int best = 0;
        for (int dr = -(n - 1); dr <= n - 1; ++dr) {
            for (int dc = -(n - 1); dc <= n - 1; ++dc) {
                best = Math.max(best, overlapCount(img1, img2, dr, dc));
            }
        }
        return best;
    }

    // 将 img1 平移 (dr, dc) 后与 img2 的重叠 1 数
    private int overlapCount(int[][] img1, int[][] img2, int dr, int dc) {
        int n = img1.length;
        int cnt = 0;
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < n; ++j) {
                int r = i + dr, c = j + dc;
                if (r >= 0 && r < n && c >= 0 && c < n && img1[i][j] == 1 && img2[r][c] == 1) {
                    ++cnt;
                }
            }
        }
        return cnt;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;

        ++total; if (sol.largestOverlap(new int[][]{{1,1,0},{0,1,0},{0,1,0}}, new int[][]{{0,0,0},{0,1,1},{0,0,1}}) == 3) ++pass; else System.out.println("FAIL 835-1");
        ++total; if (sol.largestOverlap(new int[][]{{1}}, new int[][]{{1}}) == 1) ++pass; else System.out.println("FAIL 835-2");
        ++total; if (sol.largestOverlap(new int[][]{{0}}, new int[][]{{0}}) == 0) ++pass; else System.out.println("FAIL 835-3");
        ++total; if (sol.largestOverlap(new int[][]{{1,0},{0,0}}, new int[][]{{0,1},{0,0}}) == 1) ++pass; else System.out.println("FAIL 835-4");

        ++total; if (sol.largestOverlap2(new int[][]{{1,1,0},{0,1,0},{0,1,0}}, new int[][]{{0,0,0},{0,1,1},{0,0,1}}) == 3) ++pass; else System.out.println("FAIL 835-5");
        ++total; if (sol.largestOverlap2(new int[][]{{1}}, new int[][]{{1}}) == 1) ++pass; else System.out.println("FAIL 835-6");
        ++total; if (sol.largestOverlap2(new int[][]{{1,0},{0,0}}, new int[][]{{0,1},{0,0}}) == 1) ++pass; else System.out.println("FAIL 835-7");

        if (pass == total) {
            System.out.println("== 0835 PASS ==");
        } else {
            System.out.println("== 0835 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(n^4)(配对数最多 n^4,n<=30 可接受)O(n^2)
解法二O(n^4)O(1)

836. Rectangle Overlap(矩形重叠)

题目描述

矩形以列表 [x1, y1, x2, y2] 的形式表示,其中 (x1, y1) 为左下角的坐标,(x2, y2) 是
右上角的坐标。矩形的上下边平行于 x 轴,左右边平行于 y 轴。
如果相交的面积为正,则称两矩形重叠。需要明确的是,只在角或边接触的两个矩形不构成重叠。
给出两个矩形 rec1 和 rec2。如果它们重叠,返回 true;否则,返回 false。

示例 1

  • 输入:rec1 = [0,0,2,2], rec2 = [1,1,3,3]
  • 输出:true

示例 2

  • 输入:rec1 = [0,0,1,1], rec2 = [1,0,2,1]
  • 输出:false

示例 3

  • 输入:rec1 = [0,0,1,1], rec2 = [2,2,3,3]
  • 输出:false 提示:
  • rec1.length == 4
  • rec2.length == 4
  • -10^9 <= rec1[i], rec2[i] <= 10^9
  • rec1 和 rec2 表示一个面积不为零的有效矩形

解题思路

解法一(排除法,主提交):两个矩形不重叠当且仅当上下左右四方向任一错开: rec2 在 rec1 上方(y3 >= y2)、下方(y4 <= y1)、右方(x3 >= x2)、 左方(x4 <= x1)。以上任一成立返回 false,否则返回 true。 解法二(交集区间求法,对照实现):重叠区域在 x 方向区间为 [max(x1,x3), min(x2,x4)],y 方向为 [max(y1,y3), min(y2,y4)],两个区间宽度都 > 0 才表示正面积重叠。

代码实现

class Solution {

    // 解法一:排除不重叠情况(推荐提交)
    public boolean isRectangleOverlap(int[] rec1, int[] rec2) {
        return !(rec2[0] >= rec1[2] || rec2[2] <= rec1[0]
                || rec2[1] >= rec1[3] || rec2[3] <= rec1[1]);
    }

    // 解法二:交集区间求法(对照实现)
    public boolean isRectangleOverlap2(int[] rec1, int[] rec2) {
        long x1 = Math.max(rec1[0], rec2[0]);
        long x2 = Math.min(rec1[2], rec2[2]);
        long y1 = Math.max(rec1[1], rec2[1]);
        long y2 = Math.min(rec1[3], rec2[3]);
        return x1 < x2 && y1 < y2;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;

        ++total; if (sol.isRectangleOverlap(new int[]{0,0,2,2}, new int[]{1,1,3,3})) ++pass; else System.out.println("FAIL 836-1");
        ++total; if (!sol.isRectangleOverlap(new int[]{0,0,1,1}, new int[]{1,0,2,1})) ++pass; else System.out.println("FAIL 836-2");
        ++total; if (!sol.isRectangleOverlap(new int[]{0,0,1,1}, new int[]{2,2,3,3})) ++pass; else System.out.println("FAIL 836-3");
        ++total; if (sol.isRectangleOverlap(new int[]{0,0,1,1}, new int[]{0,0,1,1})) ++pass; else System.out.println("FAIL 836-4");
        ++total; if (!sol.isRectangleOverlap(new int[]{0,0,1,1}, new int[]{1,1,2,2})) ++pass; else System.out.println("FAIL 836-5");

        ++total; if (sol.isRectangleOverlap2(new int[]{0,0,2,2}, new int[]{1,1,3,3})) ++pass; else System.out.println("FAIL 836-6");
        ++total; if (!sol.isRectangleOverlap2(new int[]{0,0,1,1}, new int[]{1,0,2,1})) ++pass; else System.out.println("FAIL 836-7");
        ++total; if (!sol.isRectangleOverlap2(new int[]{0,0,1,1}, new int[]{1,1,2,2})) ++pass; else System.out.println("FAIL 836-8");

        if (pass == total) {
            System.out.println("== 0836 PASS ==");
        } else {
            System.out.println("== 0836 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(1)O(1)
解法二O(1)O(1)

837. New 21 Game(新 21 点)

题目描述

爱丽丝参与一个大致基于纸牌游戏 "21点" 规则的游戏。爱丽丝以 0 分开始,并在她的得分少于 k 分时抽取数字。抽取时,她从 [1, maxPts] 的范围中随机获得一个整数作为分数进行累计, 其中 maxPts 是一个整数。每次抽取都是独立的,其结果具有相同的概率。 当爱丽丝获得 k 分或更多分时,她就停止抽取数字。 爱丽丝的分数不超过 n 的概率是多少?与实际答案误差不超过 10^-5 的答案将被视为正确答案。

示例 1

  • 输入:n = 10, k = 1, maxPts = 10
  • 输出:1.00000

示例 2

  • 输入:n = 6, k = 1, maxPts = 10
  • 输出:0.60000

示例 3

  • 输入:n = 21, k = 17, maxPts = 10
  • 输出:0.73278 提示:
  • 0 <= k <= n <= 10^4
  • 1 <= maxPts <= 10^4

解题思路

解法一(倒推 + 滑动窗口,主提交):记 dp[i] 为当前分数为 i 时,最终停止时分数不超过 n 的概率。当 i >= k 时停止,dp[i] = i <= n ? 1 : 0;当 i < k 时, dp[i] = (dp[i+1] + ... + dp[i+maxPts]) / maxPts,其中下标超过 n 的 dp 值为 0。 从 k-1 倒推到 0,用长度 maxPts 的滑动窗口维护分子和,单次 O(1) 转移,总复杂度 O(k)。 解法二(正推 + 前缀和,对照实现):正向计算到达每个分数 i 的概率 dp[i](仍在抽牌过程 中的状态),转移为「前 maxPts 个可转移状态的均值」。用前缀和数组把区间求和压到 O(1), 最后对 k..min(n, k-1+maxPts) 的停止态概率求和即为答案。

代码实现

class Solution {

    // 解法一:倒推 + 滑动窗口(推荐提交)
    public double new21Game(int n, int k, int maxPts) {
        if (k == 0) {
            return 1.0;
        }
        double[] dp = new double[n + 1];
        // 基础值:i >= k 且 i <= n 时停止且成功,概率为 1
        for (int i = k; i <= n; ++i) {
            dp[i] = 1.0;
        }
        // 初始窗口:dp[k] + ... + dp[k + maxPts - 1],超过 n 的下标值为 0,因此只累加
        // [k, min(n, k-1+maxPts)] 区间内的 1
        double window = 0.0;
        int hi = Math.min(n, k - 1 + maxPts);
        for (int i = k; i <= hi; ++i) {
            window += 1.0;
        }
        for (int i = k - 1; i >= 0; --i) {
            dp[i] = window / maxPts;
            if (i == 0) {
                break;
            }
            // 窗口右移:移入 dp[i],移出 dp[i + maxPts](若未超出 n)
            window += dp[i];
            if (i + maxPts <= n) {
                window -= dp[i + maxPts];
            }
        }
        return dp[0];
    }

    // 解法二:正推 + 前缀和(对照实现)
    public double new21Game2(int n, int k, int maxPts) {
        if (k == 0) {
            return 1.0;
        }
        int m = k - 1 + maxPts;
        if (n >= m) {
            return 1.0;
        }
        double[] dp = new double[m + 1];
        double[] pre = new double[m + 2]; // pre[i+1] = dp[0] + ... + dp[i]
        dp[0] = 1.0;
        pre[1] = 1.0;
        for (int i = 1; i <= m; ++i) {
            int l = Math.max(0, i - maxPts);
            int r = Math.min(i - 1, k - 1);
            if (l <= r) {
                dp[i] = (pre[r + 1] - pre[l]) / maxPts;
            }
            pre[i + 1] = pre[i] + dp[i];
        }
        double ans = 0.0;
        for (int i = k; i <= Math.min(n, m); ++i) {
            ans += dp[i];
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;

        ++total; if (Math.abs(sol.new21Game(10, 1, 10) - 1.00000) < 1e-6) ++pass; else System.out.println("FAIL 837-1");
        ++total; if (Math.abs(sol.new21Game(6, 1, 10) - 0.60000) < 1e-6) ++pass; else System.out.println("FAIL 837-2");
        ++total; if (Math.abs(sol.new21Game(21, 17, 10) - 0.73278) < 1e-5) ++pass; else System.out.println("FAIL 837-3");
        ++total; if (Math.abs(sol.new21Game(0, 0, 1) - 1.0) < 1e-9) ++pass; else System.out.println("FAIL 837-4");
        ++total; if (Math.abs(sol.new21Game(12, 1, 10) - 1.0) < 1e-6) ++pass; else System.out.println("FAIL 837-5");

        ++total; if (Math.abs(sol.new21Game2(10, 1, 10) - 1.00000) < 1e-6) ++pass; else System.out.println("FAIL 837-6");
        ++total; if (Math.abs(sol.new21Game2(6, 1, 10) - 0.60000) < 1e-6) ++pass; else System.out.println("FAIL 837-7");
        ++total; if (Math.abs(sol.new21Game2(21, 17, 10) - 0.73278) < 1e-5) ++pass; else System.out.println("FAIL 837-8");
        ++total; if (Math.abs(sol.new21Game2(12, 1, 10) - 1.0) < 1e-6) ++pass; else System.out.println("FAIL 837-9");

        if (pass == total) {
            System.out.println("== 0837 PASS ==");
        } else {
            System.out.println("== 0837 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(k)O(n)
解法二O(k + maxPts)O(k + maxPts)

838. Push Dominoes(推多米诺)

题目描述

n 张多米诺骨牌排成一行,将每张多米诺骨牌垂直竖立。在开始时,同时把一些多米诺骨牌向左或 向右推。每过一秒,倒向左边的多米诺骨牌会推动其左侧相邻的多米诺骨牌。同样地,倒向右边的 多米诺骨牌也会推动竖立在其右侧的相邻多米诺骨牌。如果一张垂直竖立的多米诺骨牌的两侧同时有 多米诺骨牌倒下时,由于受力平衡,该骨牌仍然保持不变。就这个问题而言,我们会认为一张正在倒下 的多米诺骨牌不会对其它正在倒下或已经倒下的多米诺骨牌施加额外的力。 给你一个字符串 dominoes 表示这一行多米诺骨牌的初始状态,其中 dominoes[i] = 'L' 表示 被推向左侧,'R' 表示被推向右侧,'.' 表示没有被推动。 返回表示最终状态的字符串。

示例 1

  • 输入:dominoes = "RR.L"
  • 输出:"RR.L"

示例 2

  • 输入:dominoes = ".L.R...LR..L.."
  • 输出:"LL.RR.LLRRLL.." 提示:
  • n == dominoes.length
  • 1 <= n <= 10^5
  • dominoes[i] 为 'L'、'R' 或 '.'

解题思路

把所有初始受到推力的骨牌(L 或 R)视作源点,它们同时向外扩散各自的力,按时间分层 BFS: 解法一(多源 BFS,主提交):time[i] 记录骨牌第一次受力的时刻,force[i] 记录它在同一 时刻收到的方向集合。初始把每个 L/R 入队。弹出 i 时,若 force[i] 只有一个方向则向 相邻骨牌 j 传力:time[j] == -1 时记录时刻并入队;time[j] == time[i]+1 时把方向 追加进 force[j](两方向对撞)。最终长度 1 的倒下,长度 2 的保持 '.'。 解法二(区间扫描,对照实现):用 force[i] 数组记录每个位置的受力:R 向右传播时每个 位置得到右力 +1,L 向左传播时左力 +1。每个位置比较左力与右力:左力大于右力倒向 L, 小于则倒向 R,相等保持竖直。用差分/扫描一次性结算所有受力,不需要队列。

代码实现

class Solution {

    // 解法一:多源 BFS(推荐提交)
    public String pushDominoes(String dominoes) {
        int n = dominoes.length();
        int[] time = new int[n];
        java.util.Arrays.fill(time, -1);
        char[] force = new char[n];
        java.util.ArrayDeque<Integer> q = new java.util.ArrayDeque<>();
        for (int i = 0; i < n; ++i) {
            char c = dominoes.charAt(i);
            if (c != '.') {
                q.offer(i);
                time[i] = 0;
                force[i] = c;
            }
        }
        char[] ans = dominoes.toCharArray();
        while (!q.isEmpty()) {
            int i = q.poll();
            if (force[i] == 0) {
                // 同时受到两方力,平衡保持竖直
                ans[i] = '.';
                continue;
            }
            ans[i] = force[i];
            int j = force[i] == 'L' ? i - 1 : i + 1;
            if (j < 0 || j >= n) {
                continue;
            }
            if (time[j] == -1) {
                time[j] = time[i] + 1;
                force[j] = force[i];
                q.offer(j);
            } else if (time[j] == time[i] + 1 && force[j] != force[i]) {
                // 同时 (同一时刻) 收到相反方向的力 -> 平衡
                force[j] = 0;
            }
        }
        return new String(ans);
    }

    // 解法二:区间扫描(对照实现)
    public String pushDominoes2(String dominoes) {
        int n = dominoes.length();
        char[] ans = dominoes.toCharArray();
        // 添加左右哨兵,统一处理边界
        char[] ext = new char[n + 2];
        ext[0] = 'L';
        ext[n + 1] = 'R';
        for (int i = 0; i < n; ++i) {
            ext[i + 1] = dominoes.charAt(i);
        }
        // 以两个相邻的非 '.' 字符为界,逐段填充
        int i = 0;
        while (i < n + 1) {
            int j = i + 1;
            while (j <= n + 1 && ext[j] == '.') {
                ++j;
            }
            if (j > n + 1) {
                break;
            }
            char left = ext[i], right = ext[j];
            if (left == 'L' && right == 'L') {
                // 中间全倒向左
                for (int k = i + 1; k < j; ++k) {
                    ans[k - 1] = 'L';
                }
            } else if (left == 'R' && right == 'R') {
                // 中间全倒向右
                for (int k = i + 1; k < j; ++k) {
                    ans[k - 1] = 'R';
                }
            } else if (left == 'R' && right == 'L') {
                // 相向:两边向中间倒,中间一格平衡
                // 区间 [i+1, j-1] 内的 '.' 归左右力平分:
                // 前半倒 R,后半倒 L,奇数长度时正中间保持 '.'
                int len = j - i - 1;
                int half = len / 2;
                for (int k = i + 1; k <= i + half; ++k) {
                    ans[k - 1] = 'R';
                }
                for (int k = j - half; k <= j - 1; ++k) {
                    ans[k - 1] = 'L';
                }
            }
            i = j;
        }
        return new String(ans);
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;

        ++total; if (sol.pushDominoes("RR.L").equals("RR.L")) ++pass; else System.out.println("FAIL 838-1");
        ++total; if (sol.pushDominoes(".L.R...LR..L..").equals("LL.RR.LLRRLL..")) ++pass; else System.out.println("FAIL 838-2");
        ++total; if (sol.pushDominoes("L").equals("L")) ++pass; else System.out.println("FAIL 838-3");
        ++total; if (sol.pushDominoes("R.").equals("RR")) ++pass; else System.out.println("FAIL 838-4");
        ++total; if (sol.pushDominoes(".LR").equals("LLR")) ++pass; else System.out.println("FAIL 838-5");

        ++total; if (sol.pushDominoes2("RR.L").equals("RR.L")) ++pass; else System.out.println("FAIL 838-6");
        ++total; if (sol.pushDominoes2(".L.R...LR..L..").equals("LL.RR.LLRRLL..")) ++pass; else System.out.println("FAIL 838-7");
        ++total; if (sol.pushDominoes2("R.").equals("RR")) ++pass; else System.out.println("FAIL 838-8");
        ++total; if (sol.pushDominoes2(".L.R.").equals("LL.RR")) ++pass; else System.out.println("FAIL 838-9");

        if (pass == total) {
            System.out.println("== 0838 PASS ==");
        } else {
            System.out.println("== 0838 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(n)O(n)
解法二O(n)O(n)

839. Similar String Groups(相似字符串组)

题目描述

如果交换字符串 X 中的两个不同位置的字母,使得它和字符串 Y 相等,那么称 X 和 Y 两个 字符串相似。如果这两个字符串本身是相等的,那它们也是相似的。 例如,"tars" 和 "rats" 是相似的(交换 0 与 2 的位置);"rats" 和 "arts" 也是相似 的,但是 "star" 不与 "tars"、"rats" 或 "arts" 相似。 总之,它们通过相似性形成了两个关联组:{"tars", "rats", "arts"} 和 {"star"}。形式上,对 每个组而言,要确定一个单词在组中,只需要这个词和该组中至少一个单词相似。 给你一个字符串列表 strs。列表中的每个字符串都是 strs 中其它所有字符串的一个字母异位词。 请问 strs 中有多少个相似字符串组?

示例 1

  • 输入:strs = ["tars","rats","arts","star"]
  • 输出:2

示例 2

  • 输入:strs = ["omv","ovm"]
  • 输出:1 提示:
  • 1 <= strs.length <= 300
  • 1 <= strs[i].length <= 300
  • strs[i] 只包含小写字母
  • strs 中的所有单词都具有相同的长度,且是彼此的字母异位词

解题思路

解法一(并查集,主提交):字符串互为异位词,故两串相似当且仅当对应位置不同的字符数不超过 2。 两两比较并合并:若相似且合并成功则连通分量数减一,最后剩下的分量数即答案。 解法二(DFS 连通块,对照实现):对每个未访问的下标做 DFS,凡是与当前串相似且未访问的都 加入同一连通块,每启动一次 DFS 就得到一个组,组数即启动次数。

代码实现

class Solution {

    private int[] parent;
    private int[] size;

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

    private boolean union(int a, int b) {
        int ra = find(a), rb = find(b);
        if (ra == rb) {
            return false;
        }
        if (size[ra] < size[rb]) {
            int tmp = ra;
            ra = rb;
            rb = tmp;
        }
        parent[rb] = ra;
        size[ra] += size[rb];
        return true;
    }

    private boolean similar(String a, String b) {
        int diff = 0;
        for (int i = 0; i < a.length(); ++i) {
            if (a.charAt(i) != b.charAt(i)) {
                if (++diff > 2) {
                    return false;
                }
            }
        }
        return true;
    }

    // 解法一:并查集(推荐提交)
    public int numSimilarGroups(String[] strs) {
        int n = strs.length;
        parent = new int[n];
        size = new int[n];
        for (int i = 0; i < n; ++i) {
            parent[i] = i;
            size[i] = 1;
        }
        int groups = n;
        for (int i = 0; i < n; ++i) {
            for (int j = i + 1; j < n; ++j) {
                if (similar(strs[i], strs[j]) && union(i, j)) {
                    --groups;
                }
            }
        }
        return groups;
    }

    // 解法二:DFS 连通块(对照实现)
    public int numSimilarGroups2(String[] strs) {
        int n = strs.length;
        boolean[] vis = new boolean[n];
        int groups = 0;
        for (int i = 0; i < n; ++i) {
            if (!vis[i]) {
                ++groups;
                dfs(i, strs, vis);
            }
        }
        return groups;
    }

    private void dfs(int i, String[] strs, boolean[] vis) {
        vis[i] = true;
        for (int j = 0; j < strs.length; ++j) {
            if (!vis[j] && similar(strs[i], strs[j])) {
                dfs(j, strs, vis);
            }
        }
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;

        ++total; if (sol.numSimilarGroups(new String[]{"tars","rats","arts","star"}) == 2) ++pass; else System.out.println("FAIL 839-1");
        ++total; if (sol.numSimilarGroups(new String[]{"omv","ovm"}) == 1) ++pass; else System.out.println("FAIL 839-2");
        ++total; if (sol.numSimilarGroups(new String[]{"a"}) == 1) ++pass; else System.out.println("FAIL 839-3");
        ++total; if (sol.numSimilarGroups(new String[]{"kccomwcgcs","socgcmcwkc","sgckwcmcoc","coswcmcgkc","cowkccmsgc","cosgmccwkc","sgmkwcccoc","coswmccgkc","kowcccsmcg","kcgmcwccso"}) == 7) ++pass; else System.out.println("FAIL 839-4");

        ++total; if (sol.numSimilarGroups2(new String[]{"tars","rats","arts","star"}) == 2) ++pass; else System.out.println("FAIL 839-5");
        ++total; if (sol.numSimilarGroups2(new String[]{"omv","ovm"}) == 1) ++pass; else System.out.println("FAIL 839-6");
        ++total; if (sol.numSimilarGroups2(new String[]{"a"}) == 1) ++pass; else System.out.println("FAIL 839-7");
        ++total; if (sol.numSimilarGroups2(new String[]{"kccomwcgcs","socgcmcwkc","sgckwcmcoc","coswcmcgkc","cowkccmsgc","cosgmccwkc","sgmkwcccoc","coswmccgkc","kowcccsmcg","kcgmcwccso"}) == 7) ++pass; else System.out.println("FAIL 839-8");

        if (pass == total) {
            System.out.println("== 0839 PASS ==");
        } else {
            System.out.println("== 0839 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(n^2 * m)O(n)
解法二O(n^2 * m)O(n)