LeetCode801~813题目解析

4 阅读7分钟

LeetCode 801-813. 题解合集


801. Minimum Swaps To Make Sequences Increasing(使序列递增的最小交换次数)

题目描述

我们有两个长度相等且不为空的整型数组 nums1 和 nums2。在一次操作中,我们可以交换
nums1[i] 和 nums2[i] 的元素。
例如,如果 nums1 = [1,2,3,8],nums2 = [5,6,7,4],你可以交换 i = 3 处的元素,
得到 nums1 = [1,2,3,4] 和 nums2 = [5,6,7,8]。
返回使 nums1 和 nums2 严格递增所需操作的最小次数。
数组 arr 严格递增 且 arr[0] < arr[1] < arr[2] < ... < arr[arr.length - 1]。
注意:用例保证可以实现操作。

示例 1

  • 输入:nums1 = [1,3,5,4], nums2 = [1,2,3,7]
  • 输出:1
  • 解释:交换 A[3] 和 B[3] 后,A = [1,3,5,7],B = [1,2,3,4],两个数组均严格递增。

示例 2

  • 输入:nums1 = [0,3,5,8,9], nums2 = [2,1,4,6,9]
  • 输出:1
    提示:
  • 2 <= nums1.length <= 10^5
  • nums2.length == nums1.length
  • 0 <= nums1[i], nums2[i] <= 2 * 10^5

解题思路

每个下标可选择交换或不交换,穷举全部 2^n 方案在 n <= 10^5 时不可行。某一位置能否保持
两条序列严格递增,只取决于它与前一位置在交换后的相对大小。
解法一(滚动动态规划,主提交):用 a、b 表示做到当前位置且「本位置不换 / 本位置
交换」的最少次数,转移按与前一位是否已递增、以及交叉后是否仍递增分情况讨论。
解法二(带数组的动态规划记忆版):逻辑与解法一同源,用两个数组保存每一位置的两种状态,
便于推演状态转移表(对照实现,直观展示 DP 过程)。

代码实现

class Solution {

    // 解法一:滚动变量 DP(推荐提交)
    public int minSwap(int[] nums1, int[] nums2) {
        int a = 0, b = 1; // a: 不交换,b: 交换
        for (int i = 1; i < nums1.length; ++i) {
            int x = a, y = b;
            if (nums1[i - 1] >= nums1[i] || nums2[i - 1] >= nums2[i]) {
                // 前一位与这一位必须交换状态才可能合法
                a = y;       // 前一位交换 -> 这一位不交换
                b = x + 1;   // 前一位不交换 -> 这一位必须交换
            } else {
                // 前后可不交换,这一位交换也可行
                b = y + 1;
                if (nums1[i - 1] < nums2[i] && nums2[i - 1] < nums1[i]) {
                    // 交叉之后仍严格递增,两种状态都可以取较小值
                    a = Math.min(a, y);
                    b = Math.min(b, x + 1);
                }
            }
        }
        return Math.min(a, b);
    }

    // 解法二:数组版 DP(便于观察状态表)
    public int minSwap2(int[] nums1, int[] nums2) {
        int n = nums1.length;
        int[] keep = new int[n]; // keep[i]: i 不交换的最小次数
        int[] swap = new int[n]; // swap[i]: i 交换的最小次数
        keep[0] = 0;
        swap[0] = 1;
        for (int i = 1; i < n; ++i) {
            keep[i] = swap[i] = Integer.MAX_VALUE;
            if (nums1[i - 1] < nums1[i] && nums2[i - 1] < nums2[i]) {
                keep[i] = Math.min(keep[i], keep[i - 1]);
                swap[i] = Math.min(swap[i], swap[i - 1] + 1);
            }
            if (nums1[i - 1] < nums2[i] && nums2[i - 1] < nums1[i]) {
                keep[i] = Math.min(keep[i], swap[i - 1]);
                swap[i] = Math.min(swap[i], keep[i - 1] + 1);
            }
        }
        return Math.min(keep[n - 1], swap[n - 1]);
    }

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

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

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

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

复杂度分析

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

802. Find Eventual Safe States(找到最终的安全状态)

题目描述

有一个有 n 个节点的有向图,节点按 0 到 n - 1 编号。图由一个索引从 0 开始的 2D 整数
数组 graph 表示,graph[i] 是与节点 i 相邻的节点的整数数组,这意味着从节点 i 到
graph[i] 中的每个节点都有一条边。
如果一个节点没有连出的有向边,则该节点是终端节点。如果从该节点开始的所有可能路径都通向
终端节点(或另一个安全节点),则该节点为安全节点。
返回一个由图中所有安全节点组成的数组作为答案。答案数组中的元素应当按升序排列。

示例 1

  • 输入:graph = [[1,2],[2,3],[5],[0],[5],[],[]]
  • 输出:[2,4,5,6]

示例 2

  • 输入:graph = [[1,2,3,4],[1,2],[3,4],[0,4],[]]
  • 输出:[4]
    提示:
  • n == graph.length
  • 1 <= n <= 10^4
  • graph[i] 按严格递增顺序排列
  • 图中可能包含自环
  • 图中边的数目在范围 [1, 4 * 10^4] 内

解题思路

出度为零的点是安全的,如果一个点只能到达安全的点,那么它同样是安全的。
解法一(拓扑排序,主提交):将图中所有边反向,得到一个反图,然后在反图上运行拓扑排序;
能入度归零的点即为安全点。
解法二(DFS + 三色标记法):若起始节点位于一个环内,或者能到达一个环,则该节点不是安全的;
用 0/1/2 三色标记未访问、访问中、已确认安全,递归判定。

代码实现

class Solution {

    // 解法一:反向图拓扑排序(推荐提交)
    public java.util.List<Integer> eventualSafeNodes(int[][] graph) {
        int n = graph.length;
        int[] indeg = new int[n];
        java.util.List<Integer>[] rg = new java.util.List[n];
        for (int i = 0; i < n; ++i) {
            rg[i] = new java.util.ArrayList<>();
        }
        java.util.Deque<Integer> q = new java.util.ArrayDeque<>();
        for (int i = 0; i < n; ++i) {
            for (int j : graph[i]) {
                rg[j].add(i);
            }
            indeg[i] = graph[i].length;
            if (indeg[i] == 0) {
                q.offer(i);
            }
        }
        while (!q.isEmpty()) {
            int i = q.pollFirst();
            for (int j : rg[i]) {
                if (--indeg[j] == 0) {
                    q.offer(j);
                }
            }
        }
        java.util.List<Integer> ans = new java.util.ArrayList<>();
        for (int i = 0; i < n; ++i) {
            if (indeg[i] == 0) {
                ans.add(i);
            }
        }
        return ans;
    }

    // 解法二:DFS 三色标记
    private int[] color;
    private int[][] g;

    public java.util.List<Integer> eventualSafeNodes2(int[][] graph) {
        int n = graph.length;
        color = new int[n];
        g = graph;
        java.util.List<Integer> ans = new java.util.ArrayList<>();
        for (int i = 0; i < n; ++i) {
            if (dfs(i)) {
                ans.add(i);
            }
        }
        return ans;
    }

    private boolean dfs(int i) {
        if (color[i] > 0) {
            return color[i] == 2;
        }
        color[i] = 1;
        for (int j : g[i]) {
            if (!dfs(j)) {
                return false;
            }
        }
        color[i] = 2;
        return true;
    }

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

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

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

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

复杂度分析

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

803. Bricks Falling When Hit(打砖块)

题目描述

有一个 m x n 的二元网格 grid,其中 1 表示砖块,0 表示空白。砖块稳定(不会掉落)
的前提是:一块砖直接连接到网格的顶部,或者至少有一块相邻(4 个方向之一)砖块稳定不会掉落。
给你一个数组 hits,这是需要依次消除砖块的位置。每当消除 hits[i] = (rowi, coli) 位置
上的砖块时,对应位置的砖块(若存在)会消失,然后其他的砖块可能因为这一消除操作而掉落。
返回一个数组 result,其中 result[i] 表示第 i 次消除操作对应掉落的砖块数目。
注意,消除可能指向没有砖块的空白位置,如果发生这种情况,则没有砖块掉落。

示例 1

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

示例 2

  • 输入:grid = [[1,0,0,0],[1,1,0,0]], hits = [[1,1],[1,0]]
  • 输出:[0,0]
    提示:
  • m == grid.length, n == grid[i].length, 1 <= m, n <= 200
  • grid[i][j] 为 0 或 1
  • 1 <= hits.length <= 4 * 10^4
  • 所有 (xi, yi) 互不相同

解题思路

每次打击后正向洪水填充统计掉落,hits 达 4*10^4、m,n <= 200,重复搜索过慢。掉落的本质是
「与顶行的连通被切断」。
解法一(并查集逆序补砖,主提交):先打掉全部打击位置,用并查集把剩余砖块连到一个虚拟屋顶,
再按相反顺序补回砖块。补回后屋顶连通块的增量减一,即该次打击实际掉落的砖数;原先就是
空格的打击贡献为零。
解法二(暴力模拟,对照实现):按 hits 顺序模拟,每次打击后 BFS/DFS 统计与顶部失联的砖块数,
用于小规模数据验证。

代码实现

class Solution {

    private int[] p;
    private int[] size;

    // 解法一:并查集逆序(推荐提交)
    public int[] hitBricks(int[][] grid, int[][] hits) {
        int m = grid.length;
        int n = grid[0].length;
        p = new int[m * n + 1];
        size = new int[m * n + 1];
        for (int i = 0; i < p.length; ++i) {
            p[i] = i;
            size[i] = 1;
        }
        int[][] g = new int[m][n];
        for (int i = 0; i < m; ++i) {
            for (int j = 0; j < n; ++j) {
                g[i][j] = grid[i][j];
            }
        }
        // 先全部打掉
        for (int[] h : hits) {
            g[h[0]][h[1]] = 0;
        }
        // 屋顶
        for (int j = 0; j < n; ++j) {
            if (g[0][j] == 1) {
                union(j, m * n);
            }
        }
        // 剩余砖块内部连边
        for (int i = 1; i < m; ++i) {
            for (int j = 0; j < n; ++j) {
                if (g[i][j] == 0) {
                    continue;
                }
                if (g[i - 1][j] == 1) {
                    union(i * n + j, (i - 1) * n + j);
                }
                if (j > 0 && g[i][j - 1] == 1) {
                    union(i * n + j, i * n + j - 1);
                }
            }
        }
        int[] ans = new int[hits.length];
        int[] dirs = {-1, 0, 1, 0, -1};
        for (int k = hits.length - 1; k >= 0; --k) {
            int i = hits[k][0];
            int j = hits[k][1];
            if (grid[i][j] == 0) {
                continue; // 原位置没有砖,掉落为 0
            }
            g[i][j] = 1;
            int prev = size[find(m * n)];
            if (i == 0) {
                union(j, m * n);
            }
            for (int l = 0; l < 4; ++l) {
                int x = i + dirs[l];
                int y = j + dirs[l + 1];
                if (x >= 0 && x < m && y >= 0 && y < n && g[x][y] == 1) {
                    union(i * n + j, x * n + y);
                }
            }
            int curr = size[find(m * n)];
            ans[k] = Math.max(0, curr - prev - 1);
        }
        return ans;
    }

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

    private void union(int a, int b) {
        int pa = find(a);
        int pb = find(b);
        if (pa != pb) {
            size[pb] += size[pa];
            p[pa] = pb;
        }
    }

    // 解法二:暴力模拟(对照验证)
    public int[] hitBricks2(int[][] grid, int[][] hits) {
        int m = grid.length;
        int n = grid[0].length;
        int[][] g = new int[m][n];
        for (int i = 0; i < m; ++i) {
            for (int j = 0; j < n; ++j) {
                g[i][j] = grid[i][j];
            }
        }
        int[] ans = new int[hits.length];
        int[] dirs = {-1, 0, 1, 0, -1};
        for (int k = 0; k < hits.length; ++k) {
            int i = hits[k][0];
            int j = hits[k][1];
            if (g[i][j] == 0) {
                continue;
            }
            g[i][j] = 0;
            // 找出与顶部失联的砖块
            boolean[][] top = new boolean[m][n];
            java.util.ArrayDeque<int[]> q = new java.util.ArrayDeque<>();
            for (int c = 0; c < n; ++c) {
                if (g[0][c] == 1) {
                    top[0][c] = true;
                    q.offer(new int[]{0, c});
                }
            }
            while (!q.isEmpty()) {
                int[] cur = q.poll();
                for (int l = 0; l < 4; ++l) {
                    int x = cur[0] + dirs[l];
                    int y = cur[1] + dirs[l + 1];
                    if (x >= 0 && x < m && y >= 0 && y < n && g[x][y] == 1 && !top[x][y]) {
                        top[x][y] = true;
                        q.offer(new int[]{x, y});
                    }
                }
            }
            int fall = 0;
            for (int x = 0; x < m; ++x) {
                for (int y = 0; y < n; ++y) {
                    if (g[x][y] == 1 && !top[x][y]) {
                        g[x][y] = 0;
                        ++fall;
                    }
                }
            }
            ans[k] = fall;
        }
        return ans;
    }

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

        ++total; if (java.util.Arrays.equals(sol.hitBricks(new int[][]{{1, 0, 0, 0}, {1, 1, 1, 0}}, new int[][]{{1, 0}}), new int[]{2})) ++pass; else System.out.println("FAIL 803-1");
        ++total; if (java.util.Arrays.equals(sol.hitBricks(new int[][]{{1, 0, 0, 0}, {1, 1, 0, 0}}, new int[][]{{1, 1}, {1, 0}}), new int[]{0, 0})) ++pass; else System.out.println("FAIL 803-2");
        ++total; if (java.util.Arrays.equals(sol.hitBricks(new int[][]{{1}, {1}, {1}}, new int[][]{{0, 0}}), new int[]{2})) ++pass; else System.out.println("FAIL 803-3");
        ++total; if (java.util.Arrays.equals(sol.hitBricks(new int[][]{{1, 1, 1}, {1, 0, 1}, {1, 1, 1}}, new int[][]{{0, 1}}), new int[]{0})) ++pass; else System.out.println("FAIL 803-4");

        ++total; if (java.util.Arrays.equals(sol.hitBricks2(new int[][]{{1, 0, 0, 0}, {1, 1, 1, 0}}, new int[][]{{1, 0}}), new int[]{2})) ++pass; else System.out.println("FAIL 803-5");
        ++total; if (java.util.Arrays.equals(sol.hitBricks2(new int[][]{{1, 0, 0, 0}, {1, 1, 0, 0}}, new int[][]{{1, 1}, {1, 0}}), new int[]{0, 0})) ++pass; else System.out.println("FAIL 803-6");
        ++total; if (java.util.Arrays.equals(sol.hitBricks2(new int[][]{{1}, {1}, {1}}, new int[][]{{0, 0}}), new int[]{2})) ++pass; else System.out.println("FAIL 803-7");

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

复杂度分析

解法时间复杂度空间复杂度
解法一O(mnalpha +hits*alpha)O(m*n)
解法二O(hits* m * n)O(m*n)

804. Unique Morse Code Words(唯一摩尔斯密码词)

题目描述

国际摩尔斯密码定义一种标准编码方式,将每个字母对应于一个由一系列点和短线组成的字符串,
比如:'a' 对应 ".-" ,'b' 对应 "-..." ,'c' 对应 "-.-." ,以此类推。为了方便,所有 26 个
英文字母的摩尔斯密码表如下:
[".-","-...","-.-.","-..",".","..-.","--.","....","..",".---","-.-",".-..","--","-.",
"---",".--.","--.-",".-.","...","-","..-","...-",".--","-..-","-.--","--.."]
给你一个字符串数组 words,每个单词可以写成每个字母对应摩尔斯密码的组合。例如,"cab"
可以写成 "-.-..--..."(即 "-.-." + ".-" + "-..." 字符串的结合)。我们将这样一个连接过程
称作单词翻译。
对 words 中所有单词进行单词翻译,返回不同单词翻译的数量。

示例 1

  • 输入:words = ["gin", "zen", "gig", "msg"]
  • 输出: 2

示例 2

  • 输入:words = ["a"]
  • 输出:1
    提示:
  • 1 <= words.length <= 100
  • 1 <= words[i].length <= 12
  • words[i] 由小写英文字母组成

解题思路

每个单词按字母表映射成摩尔斯串后,需要统计有多少种不同编码。words 至多 100 个、长度不
超过 12,直接变换即可。
解法一(哈希集合,主提交):用集合存放变换结果,最后取集合大小。
解法二(排序去重,对照实现):把所有编码收集到字符串数组中,排序后数不同编码的段数。

代码实现

class Solution {

    private static final String[] CODES = {
        ".-", "-...", "-.-.", "-..", ".", "..-.", "--.", "....", "..", ".---", "-.-", ".-..",
        "--", "-.", "---", ".--.", "--.-", ".-.", "...", "-", "..-", "...-", ".--", "-..-",
        "-.--", "--.."
    };

    // 解法一:哈希集合(推荐提交)
    public int uniqueMorseRepresentations(String[] words) {
        java.util.Set<String> set = new java.util.HashSet<>();
        for (String word : words) {
            StringBuilder sb = new StringBuilder();
            for (char c : word.toCharArray()) {
                sb.append(CODES[c - 'a']);
            }
            set.add(sb.toString());
        }
        return set.size();
    }

    // 解法二:排序 + 去重计数
    public int uniqueMorseRepresentations2(String[] words) {
        String[] enc = new String[words.length];
        for (int i = 0; i < words.length; ++i) {
            StringBuilder sb = new StringBuilder();
            for (char c : words[i].toCharArray()) {
                sb.append(CODES[c - 'a']);
            }
            enc[i] = sb.toString();
        }
        java.util.Arrays.sort(enc);
        int cnt = 1;
        for (int i = 1; i < enc.length; ++i) {
            if (!enc[i].equals(enc[i - 1])) {
                ++cnt;
            }
        }
        return cnt;
    }

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

        ++total; if (sol.uniqueMorseRepresentations(new String[]{"gin", "zen", "gig", "msg"}) == 2) ++pass; else System.out.println("FAIL 804-1");
        ++total; if (sol.uniqueMorseRepresentations(new String[]{"a"}) == 1) ++pass; else System.out.println("FAIL 804-2");
        ++total; if (sol.uniqueMorseRepresentations(new String[]{"abc"}) == 1) ++pass; else System.out.println("FAIL 804-3");
        ++total; if (sol.uniqueMorseRepresentations(new String[]{"rwjje", "aittjje", "auyyn", "lqtktn", "lmjwn"}) == 1) ++pass; else System.out.println("FAIL 804-4");

        ++total; if (sol.uniqueMorseRepresentations2(new String[]{"gin", "zen", "gig", "msg"}) == 2) ++pass; else System.out.println("FAIL 804-5");
        ++total; if (sol.uniqueMorseRepresentations2(new String[]{"a"}) == 1) ++pass; else System.out.println("FAIL 804-6");
        ++total; if (sol.uniqueMorseRepresentations2(new String[]{"abc"}) == 1) ++pass; else System.out.println("FAIL 804-7");

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

复杂度分析

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

805. Split Array With Same Average(数组的均值分割)

题目描述

给定你一个整数数组 nums。我们要将 nums 数组中的每个元素移动到 A 数组或者 B 数组中,
使得 A 数组和 B 数组不为空,并且 average(A) == average(B)。如果可以完成则返回 true,
否则返回 false。
注意:对于数组 arr,average(arr) 是 arr 的所有元素的和除以 arr 长度。

示例 1

  • 输入:nums = [1,2,3,4,5,6,7,8]
  • 输出: true
  • 解释: 我们可以将数组分割为 [1,4,5,8] 和 [2,3,6,7],他们的平均值都是 4.5。

示例 2

  • 输入:nums = [3,1]
  • 输出: false
    提示:
  • 1 <= nums.length <= 30
  • 0 <= nums[i] <= 10^4

解题思路

将数组分成两个非空子集且平均值相等,等价于存在大小为 k 的子集其和为 k * avg。
先把每个数变为 n * a_i - S,则目标转化为「存在非空真子集其和为 0」。
解法一(折半枚举 + 哈希,主提交):n <= 30,折半后每半约 15 个数,二进制枚举子集和;
左半子集和放入集合,右半枚举时若自身为 0 或其相反数已在左半出现(且不是全集对全集),即可判定。
解法二(双向生成 + 排序双指针,对照实现):左右两半各生成所有子集和与大小配对,排序后用
双指针合并检查 <0 的目标和,逻辑直观但实现更细。

代码实现

class Solution {

    // 解法一:折半枚举 + 哈希(推荐提交)
    public boolean splitArraySameAverage(int[] nums) {
        int n = nums.length;
        if (n == 1) {
            return false;
        }
        int sum = 0;
        for (int v : nums) {
            sum += v;
        }
        // 每个数乘 n 减总量,目标变为找和为 0 的非空真子集
        int[] a = new int[n];
        for (int i = 0; i < n; ++i) {
            a[i] = nums[i] * n - sum;
        }
        int mid = n / 2;
        // 左半所有子集和
        java.util.Set<Integer> left = new java.util.HashSet<>();
        int leftFull = 0;
        for (int i = 0; i < mid; ++i) {
            leftFull += a[i];
        }
        left.add(0); // 空集
        left.add(leftFull); // 全集
        for (int mask = 1; mask < (1 << mid) - 1; ++mask) {
            int s = 0;
            for (int i = 0; i < mid; ++i) {
                if ((mask & (1 << i)) != 0) {
                    s += a[i];
                }
            }
            left.add(s);
        }
        int rightFull = 0;
        for (int i = mid; i < n; ++i) {
            rightFull += a[i];
        }
        // 右半枚举(从 1 开始排除空集;右半全集对应 A 为空,同样排除)
        for (int mask = 1; mask < (1 << (n - mid)) - 1; ++mask) {
            int s = 0;
            for (int i = mid; i < n; ++i) {
                if ((mask & (1 << (i - mid))) != 0) {
                    s += a[i];
                }
            }
            if (s == 0 || left.contains(-s)) {
                return true;
            }
        }
        return false;
    }

    // 解法二:双向生成 + 排序双指针合并
    public boolean splitArraySameAverage2(int[] nums) {
        int n = nums.length;
        if (n == 1) {
            return false;
        }
        int sum = 0;
        for (int v : nums) {
            sum += v;
        }
        int[] a = new int[n];
        for (int i = 0; i < n; ++i) {
            a[i] = nums[i] * n - sum;
        }
        int mid = n / 2;
        java.util.ArrayList<Integer> L1 = new java.util.ArrayList<>(); // 不含全选的左半和
        java.util.ArrayList<Integer> R1 = new java.util.ArrayList<>(); // 不含全选的右半和
        int lFull = 0, rFull = 0;
        for (int i = 0; i < mid; ++i) {
            lFull += a[i];
        }
        for (int i = mid; i < n; ++i) {
            rFull += a[i];
        }
        for (int mask = 1; mask < (1 << mid) - 1; ++mask) {
            int s = 0;
            for (int i = 0; i < mid; ++i) {
                if ((mask & (1 << i)) != 0) {
                    s += a[i];
                }
            }
            L1.add(s);
        }
        for (int mask = 1; mask < (1 << (n - mid)) - 1; ++mask) {
            int s = 0;
            for (int i = mid; i < n; ++i) {
                if ((mask & (1 << (i - mid))) != 0) {
                    s += a[i];
                }
            }
            if (s == 0) {
                return true;
            }
            R1.add(s);
        }
        java.util.Collections.sort(L1);
        java.util.Collections.sort(R1);
        java.util.Set<Integer> lset = new java.util.HashSet<>(L1);
        for (int v : R1) {
            if (lset.contains(-v)) {
                return true;
            }
        }
        return false;
    }

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

        ++total; if (sol.splitArraySameAverage(new int[]{1, 2, 3, 4, 5, 6, 7, 8})) ++pass; else System.out.println("FAIL 805-1");
        ++total; if (!sol.splitArraySameAverage(new int[]{3, 1})) ++pass; else System.out.println("FAIL 805-2");
        ++total; if (sol.splitArraySameAverage(new int[]{1, 2, 3})) ++pass; else System.out.println("FAIL 805-3");
        ++total; if (sol.splitArraySameAverage(new int[]{5, 3, 11, 19, 2})) ++pass; else System.out.println("FAIL 805-4");
        ++total; if (sol.splitArraySameAverage(new int[]{4, 3, 2, 4, 2, 6, 7})) ++pass; else System.out.println("FAIL 805-5");
        ++total; if (!sol.splitArraySameAverage(new int[]{2})) ++pass; else System.out.println("FAIL 805-6");

        ++total; if (sol.splitArraySameAverage2(new int[]{1, 2, 3, 4, 5, 6, 7, 8})) ++pass; else System.out.println("FAIL 805-7");
        ++total; if (!sol.splitArraySameAverage2(new int[]{3, 1})) ++pass; else System.out.println("FAIL 805-8");
        ++total; if (sol.splitArraySameAverage2(new int[]{1, 2, 3})) ++pass; else System.out.println("FAIL 805-9");

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

复杂度分析

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

806. Number of Lines To Write String(写字符串需要的行数)

题目描述

我们要把给定的字符串 S 从左到右写到每一行上,每一行的最大宽度为 100 个单位,如果我们在写
某个字母的时候会使这行超过了 100 个单位,那么我们应该把这个字母写到下一行。我们给定了一个
数组 widths,这个数组 widths[0] 代表 'a' 需要的单位,widths[1] 代表 'b' 需要的单位,...
widths[25] 代表 'z' 需要的单位。
现在回答两个问题:至少多少行能放下 S,以及最后一行使用的宽度是多少个单位?将你的答案作为
长度为 2 的整数列表返回。

示例 1

  • 输入:widths = [10,10,...,10], S = "abcdefghijklmnopqrstuvwxyz"
  • 输出: [3, 60]

示例 2

  • 输入:widths = [4,10,...,10], S = "bbbcccdddaaa"
  • 输出: [2, 4]
    提示:
  • 字符串 S 的长度在 [1, 1000] 的范围
  • S 只包含小写字母
  • widths 是长度为 26 的数组
  • widths[i] 值的范围在 [2, 10]

解题思路

字母宽度已知、每行宽度固定为 100,需要模拟书写过程得到行数与最后一行占用宽度。
解法一(一次遍历模拟,主提交):若当前行剩余宽度放不下该字母,则换行并把该字母作为新行的
起始宽度。
解法二(逐字母处理 + 预写检查,对照实现):对每个字母先检查当前行能否容纳,逻辑拆成
两个更小的步骤便于阅读。

代码实现

class Solution {

    // 解法一:一次遍历(推荐提交)
    public int[] numberOfLines(int[] widths, String s) {
        int lines = 1, last = 0;
        for (int i = 0; i < s.length(); ++i) {
            int w = widths[s.charAt(i) - 'a'];
            if (last + w <= 100) {
                last += w;
            } else {
                ++lines;
                last = w;
            }
        }
        return new int[]{lines, last};
    }

    // 解法二:预写检查
    public int[] numberOfLines2(int[] widths, String s) {
        int lines = 1, last = 0;
        for (int i = 0; i < s.length(); ++i) {
            int w = widths[s.charAt(i) - 'a'];
            if (100 - last < w) {
                ++lines;
                last = 0;
            }
            last += w;
        }
        return new int[]{lines, last};
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        int[] all10 = new int[26];
        java.util.Arrays.fill(all10, 10);
        int[] w2 = new int[26];
        java.util.Arrays.fill(w2, 10);
        w2[0] = 4;

        ++total; if (java.util.Arrays.equals(sol.numberOfLines(all10, "abcdefghijklmnopqrstuvwxyz"), new int[]{3, 60})) ++pass; else System.out.println("FAIL 806-1");
        ++total; if (java.util.Arrays.equals(sol.numberOfLines(w2, "bbbcccdddaaa"), new int[]{2, 4})) ++pass; else System.out.println("FAIL 806-2");
        ++total; if (java.util.Arrays.equals(sol.numberOfLines(all10, "a"), new int[]{1, 10})) ++pass; else System.out.println("FAIL 806-3");
        ++total; if (java.util.Arrays.equals(sol.numberOfLines(new int[]{10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10}, "abcdefghijklmnopqrstuvwxyz"), new int[]{3, 60})) ++pass; else System.out.println("FAIL 806-4");

        ++total; if (java.util.Arrays.equals(sol.numberOfLines2(all10, "abcdefghijklmnopqrstuvwxyz"), new int[]{3, 60})) ++pass; else System.out.println("FAIL 806-5");
        ++total; if (java.util.Arrays.equals(sol.numberOfLines2(w2, "bbbcccdddaaa"), new int[]{2, 4})) ++pass; else System.out.println("FAIL 806-6");

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

复杂度分析

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

807. Max Increase to Keep City Skyline(保持城市天际线)

题目描述

给你一座由 n x n 个街区组成的城市,每个街区都包含一座立方体建筑。给你一个下标从 0 开始的
n x n 整数矩阵 grid,其中 grid[r][c] 表示坐落于 r 行 c 列的建筑物的高度。
城市的天空线是从远处观察城市时,所有建筑物形成的外部轮廓。从东、南、西、北四个主要方向观测
到的天际线可能不同。
我们被允许为任意数量的建筑物的高度增加任意增量(不同建筑物的增量可能不同)。高度为 0 的
建筑物的高度也可以增加。然而,增加的建筑物高度不能影响从任何主要方向观察城市得到的天际线。
在不改变从任何主要方向观测到的城市天际线的前提下,返回建筑物可以增加的最大高度增量总和。

示例 1

  • 输入:grid = [[3,0,8,4],[2,4,5,7],[9,2,6,3],[0,3,1,0]]
  • 输出:35

示例 2

  • 输入:grid = [[0,0,0],[0,0,0],[0,0,0]]
  • 输出:0
    提示:
  • n == grid.length
  • n == grid[r].length
  • 2 <= n <= 50
  • 0 <= grid[r][c] <= 100

解题思路

天际线由每行、每列的最大值决定,升高某格不能超过其所在行、列的天际线。
解法一(两次遍历,主提交):先扫出各行、列最大值,再逐格计算 min(rowMax[i], colMax[j])
与原高度之差并累加。
解法二(一次遍历同时累计,对照实现):第一遍求行列最大值,第二遍直接累加增量,与解法一
等价但把「计算」单独拆成方法便于阅读。

代码实现

class Solution {

    // 解法一:两次遍历(推荐提交)
    public int maxIncreaseKeepingSkyline(int[][] grid) {
        int n = grid.length;
        int[] rowMax = new int[n];
        int[] colMax = new int[n];
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < n; ++j) {
                rowMax[i] = Math.max(rowMax[i], grid[i][j]);
                colMax[j] = Math.max(colMax[j], grid[i][j]);
            }
        }
        int ans = 0;
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < n; ++j) {
                ans += Math.min(rowMax[i], colMax[j]) - grid[i][j];
            }
        }
        return ans;
    }

    // 解法二:单行扫描辅助函数版
    public int maxIncreaseKeepingSkyline2(int[][] grid) {
        int n = grid.length;
        int[] rowMax = new int[n];
        int[] colMax = new int[n];
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < n; ++j) {
                rowMax[i] = Math.max(rowMax[i], grid[i][j]);
                colMax[j] = Math.max(colMax[j], grid[i][j]);
            }
        }
        int ans = 0;
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < n; ++j) {
                ans += gain(grid[i][j], rowMax[i], colMax[j]);
            }
        }
        return ans;
    }

    private int gain(int cur, int a, int b) {
        return Math.min(a, b) - cur;
    }

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

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

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

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

复杂度分析

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

808. Soup Servings(分汤)

题目描述

你有两种汤,A 和 B,每种初始为 n 毫升。在每一轮中,会随机选择以下四种操作中的一种,每种
操作的概率为 0.25,且与之前的所有轮次无关:

  1. 从汤 A 取 100 毫升,从汤 B 取 0 毫升
  2. 从汤 A 取 75 毫升,从汤 B 取 25 毫升
  3. 从汤 A 取 50 毫升,从汤 B 取 50 毫升
  4. 从汤 A 取 25 毫升,从汤 B 取 75 毫升
    注意:不存在从汤 A 取 0 ml 和从汤 B 取 100 ml 的操作。汤 A 和 B 在每次操作中同时被取出。
    如果一次操作要求你取出比剩余的汤更多的量,请取出该汤剩余的所有部分。
    操作过程在任何回合中任一汤被取完后立即停止。
    返回汤 A 在 B 前取完的概率,加上两种汤在同一回合取完概率的一半。返回值在正确答案 10^-5 的
    范围内将被认为是正确的。

示例 1

  • 输入:n = 50
  • 输出:0.62500

示例 2

  • 输入:n = 100
  • 输出:0.71875
    提示:
  • 0 <= n <= 10^9

解题思路

四种操作等概率减少汤量,求 A 先倒完的概率(同时倒完计 1/2)。n 可达 10^9,按毫升逐步模拟
不可行;注意到每次至少减少 25ml,可把状态缩到 ceil(n/25) 的格点。
解法一(记忆化搜索,主提交):dfs(i,j) 表示剩余 (i,j) 份时的概率,边界按谁先耗尽返回 1、
0 或 1/2。n 很大时 A 几乎必然先空,n > 4800 时直接返回 1。
解法二(迭代 DP 二维数组,对照实现):与记忆化解法等价,用二维数组自底向上填表。

代码实现

class Solution {

    private double[][] memo;

    // 解法一:记忆化搜索(推荐提交)
    public double soupServings(int n) {
        if (n > 4800) {
            return 1.0;
        }
        int m = (n + 24) / 25; // 向上取整的份数
        memo = new double[m + 1][m + 1];
        return dfs(m, m);
    }

    private double dfs(int i, int j) {
        if (i <= 0 && j <= 0) {
            return 0.5;
        }
        if (i <= 0) {
            return 1.0;
        }
        if (j <= 0) {
            return 0.0;
        }
        if (memo[i][j] > 0) {
            return memo[i][j];
        }
        double v = 0.25 * (dfs(i - 4, j) + dfs(i - 3, j - 1) + dfs(i - 2, j - 2) + dfs(i - 1, j - 3));
        memo[i][j] = v;
        return v;
    }

    // 解法二:迭代 DP
    public double soupServings2(int n) {
        if (n > 4800) {
            return 1.0;
        }
        int m = (n + 24) / 25;
        // dp[i][j] 表示剩余 i、j 份时的概率
        double[][] dp = new double[m + 1][m + 1];
        dp[0][0] = 0.5;
        for (int i = 1; i <= m; ++i) {
            dp[0][i] = 1.0;
        }
        for (int i = 1; i <= m; ++i) {
            for (int j = 1; j <= m; ++j) {
                dp[i][j] = 0.25 * (get(dp, i - 4, j) + get(dp, i - 3, j - 1)
                        + get(dp, i - 2, j - 2) + get(dp, i - 1, j - 3));
            }
        }
        return dp[m][m];
    }

    private double get(double[][] dp, int i, int j) {
        if (i <= 0 && j <= 0) {
            return 0.5;
        }
        if (i <= 0) {
            return 1.0;
        }
        if (j <= 0) {
            return 0.0;
        }
        return dp[i][j];
    }

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

        ++total; if (Math.abs(sol.soupServings(50) - 0.62500) < 1e-9) ++pass; else System.out.println("FAIL 808-1");
        ++total; if (Math.abs(sol.soupServings(100) - 0.71875) < 1e-9) ++pass; else System.out.println("FAIL 808-2");
        ++total; if (Math.abs(sol.soupServings(0) - 0.5) < 1e-9) ++pass; else System.out.println("FAIL 808-3");
        ++total; if (Math.abs(sol.soupServings(25) - 0.62500) < 1e-9) ++pass; else System.out.println("FAIL 808-4");
        ++total; if (Math.abs(sol.soupServings(1000000000) - 1.0) < 1e-9) ++pass; else System.out.println("FAIL 808-5");

        ++total; if (Math.abs(sol.soupServings2(50) - 0.62500) < 1e-9) ++pass; else System.out.println("FAIL 808-6");
        ++total; if (Math.abs(sol.soupServings2(100) - 0.71875) < 1e-9) ++pass; else System.out.println("FAIL 808-7");
        ++total; if (Math.abs(sol.soupServings2(0) - 0.5) < 1e-9) ++pass; else System.out.println("FAIL 808-8");
        ++total; if (Math.abs(sol.soupServings2(25) - 0.62500) < 1e-9) ++pass; else System.out.println("FAIL 808-9");

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

复杂度分析

解法时间复杂度空间复杂度
解法一O(1)(n>4800 直接返回,否则 O(C^2),C=200)O(C^2)
解法二O(C^2)O(C^2)

809. Expressive Words(情感丰富的文字)

题目描述

有时候人们会用重复写一些字母来表示额外的感受,比如 "hello" -> "heeellooo", "hi" -> "hiii"。
我们将相邻字母都相同的一串字符定义为相同字母组,例如:"h", "eee", "ll", "ooo"。
对于一个给定的字符串 S,如果另一个单词能够通过将一些字母组扩张从而使其和 S 相同,我们将
这个单词定义为可扩张的(stretchy)。扩张操作定义如下:选择一个字母组(包含字母 c),然后
往其中添加相同的字母 c 使其长度达到 3 或以上。
例如,以 "hello" 为例,我们可以对字母组 "o" 扩张得到 "hellooo",但是无法以同样的方法得到
"helloo" 因为字母组 "oo" 长度小于 3。此外,我们可以进行另一种扩张 "ll" -> "lllll" 以获得
"helllllooo"。如果 s = "helllllooo",那么查询词 "hello" 是可扩张的。
输入一组查询单词,输出其中可扩张的单词数量。

示例

  • 输入:s = "heeellooo", words = ["hello", "hi", "helo"]
  • 输出: 1
    提示:
  • 1 <= s.length, words.length <= 100
  • 1 <= words[i].length <= 100
  • s 和所有在 words 中的单词都只由小写字母组成

解题思路

伸展要求查询串的连续段只能拉长、且拉长后该段长度至少为 3,不能改字母或缩短。
解法一(双指针分段匹配,主提交):同步扫描两串的连续段,字母必须相同;若原段更长,或
目标段短于 3 却与原段长度不等,则不能伸展。两串同时扫完才算匹配。
解法二(预计算分段再比较,对照实现):先把 s 和单词 t 各压缩为(字符、次数)序列,再
顺序比较两序列是否满足可扩张规则。

代码实现

class Solution {

    // 解法一:双指针分段(推荐提交)
    public int expressiveWords(String s, String[] words) {
        int ans = 0;
        for (String t : words) {
            if (check(s, t)) {
                ++ans;
            }
        }
        return ans;
    }

    private boolean check(String s, String t) {
        int m = s.length(), n = t.length();
        if (n > m) {
            return false;
        }
        int i = 0, j = 0;
        while (i < m && j < n) {
            if (s.charAt(i) != t.charAt(j)) {
                return false;
            }
            char c = s.charAt(i);
            int c1 = 0;
            while (i < m && s.charAt(i) == c) {
                ++i;
                ++c1;
            }
            int c2 = 0;
            while (j < n && t.charAt(j) == c) {
                ++j;
                ++c2;
            }
            // s 中段长不能小于 t 中段长;若段长不足 3 则必须相等
            if (c1 < c2 || (c1 < 3 && c1 != c2)) {
                return false;
            }
        }
        return i == m && j == n;
    }

    // 解法二:压缩分段再比较
    public int expressiveWords2(String s, String[] words) {
        int ans = 0;
        int[][] cs = compress(s);
        for (String t : words) {
            if (match(cs, compress(t))) {
                ++ans;
            }
        }
        return ans;
    }

    private int[][] compress(String s) {
        java.util.ArrayList<int[]> list = new java.util.ArrayList<>();
        int i = 0;
        while (i < s.length()) {
            char c = s.charAt(i);
            int j = i;
            while (j < s.length() && s.charAt(j) == c) {
                ++j;
            }
            list.add(new int[]{c, j - i});
            i = j;
        }
        return list.toArray(new int[list.size()][]);
    }

    private boolean match(int[][] a, int[][] b) {
        if (a.length != b.length) {
            return false;
        }
        for (int i = 0; i < a.length; ++i) {
            if (a[i][0] != b[i][0]) {
                return false;
            }
            int c1 = a[i][1], c2 = b[i][1];
            if (c1 < c2 || (c1 < 3 && c1 != c2)) {
                return false;
            }
        }
        return true;
    }

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

        ++total; if (sol.expressiveWords("heeellooo", new String[]{"hello", "hi", "helo"}) == 1) ++pass; else System.out.println("FAIL 809-1");
        ++total; if (sol.expressiveWords("hello", new String[]{"hello", "hi"}) == 1) ++pass; else System.out.println("FAIL 809-2");
        ++total; if (sol.expressiveWords("zzzzzyyyyy", new String[]{"zzyy", "zy", "zyy"}) == 3) ++pass; else System.out.println("FAIL 809-3");
        ++total; if (sol.expressiveWords("aaa", new String[]{"aaaa"}) == 0) ++pass; else System.out.println("FAIL 809-4");
        ++total; if (sol.expressiveWords("abcd", new String[]{"abc"}) == 0) ++pass; else System.out.println("FAIL 809-5");

        ++total; if (sol.expressiveWords2("heeellooo", new String[]{"hello", "hi", "helo"}) == 1) ++pass; else System.out.println("FAIL 809-6");
        ++total; if (sol.expressiveWords2("zzzzzyyyyy", new String[]{"zzyy", "zy", "zyy"}) == 3) ++pass; else System.out.println("FAIL 809-7");
        ++total; if (sol.expressiveWords2("hello", new String[]{"hello", "hi"}) == 1) ++pass; else System.out.println("FAIL 809-8");

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

复杂度分析

解法时间复杂度空间复杂度
解法一O(s+t) 每个单词O(1)
解法二O(s+t) 每个单词O(s+t)

810. Chalkboard XOR Game(黑板异或游戏)

题目描述

黑板上写着一个非负整数数组 nums[i]。
Alice 和 Bob 轮流从黑板上擦掉一个数字,Alice 先手。如果擦除一个数字后,剩余的所有数字按位
异或运算得出的结果等于 0 的话,当前玩家游戏失败。另外,如果只剩一个数字,按位异或运算得到
它本身;如果无数字剩余,按位异或运算结果为 0。
并且,轮到某个玩家时,如果当前黑板上所有数字按位异或运算结果等于 0,这个玩家获胜。
假设两个玩家每步都使用最优解,当且仅当 Alice 获胜时返回 true。

示例 1

  • 输入:nums = [1,1,2]
  • 输出: false

示例 2

  • 输入:nums = [0,1]
  • 输出: true

示例 3

  • 输入:nums = [1,2,3]
  • 输出: true
    提示:
  • 1 <= nums.length <= 1000
  • 0 <= nums[i] < 2^16

解题思路

每次擦掉一个数后,若剩余数字异或为 0 则对方失败。直接搜索博弈树在 n <= 1000 时过深。
关键观察:若当前异或已为 0,先手立刻获胜;否则当 n 为偶数时,总存在一种擦法使后手面对
异或非零的局面,先手必胜。
解法一(位运算判定,主提交):答案只取决于「长度是否为偶数」或「全体异或是否为 0」,
一次异或扫描即可。
解法二(按游戏规则模拟 + 记忆化小规模验证,对照实现):n 小时用状态压缩记忆化搜索推算
必胜态,用于小规模数据交叉验证。

代码实现

class Solution {

    // 解法一:位运算判定(推荐提交)
    public boolean xorGame(int[] nums) {
        int x = 0;
        for (int v : nums) {
            x ^= v;
        }
        return x == 0 || (nums.length & 1) == 0;
    }

    // 解法二:记忆化博弈(小规模验证)
    private java.util.Map<Integer, Boolean> winMemo;

    public boolean xorGame2(int[] nums) {
        int n = nums.length;
        if (n > 20) {
            return xorGame(nums);
        }
        winMemo = new java.util.HashMap<>();
        int x = 0;
        for (int v : nums) {
            x ^= v;
        }
        return canWin(nums, (1 << n) - 1, x);
    }

    // 当前剩余集合 mask,异或为 x;返回当前玩家能否必胜
    private boolean canWin(int[] nums, int mask, int x) {
        if (x == 0) {
            return true; // 轮到当前玩家时异或为 0,直接获胜
        }
        Boolean cached = winMemo.get(mask);
        if (cached != null) {
            return cached;
        }
        int n = nums.length;
        boolean canLose = false; // 是否存在擦法使对手进入必败
        for (int i = 0; i < n; ++i) {
            if ((mask & (1 << i)) != 0) {
                int nx = x ^ nums[i];
                // 擦掉 i 后剩余异或为 0 的话当前玩家失败——但这是自己赢不了的一步,
                // 实际游戏中当前玩家会避免「擦完 -> 对手直接赢」。
                // 这里按规则:若 nx == 0,则执行该步后轮到对手且异或为 0,对手获胜。
                if (nx == 0) {
                    continue;
                }
                if (!canWin(nums, mask ^ (1 << i), nx)) {
                    canLose = true;
                    break;
                }
            }
        }
        // 注意:当只剩一个数被擦除后 mask 变 0、nx 为 0,对手获胜(已在上层 continue 掉)
        // 若所有可走步都让对手必胜,则当前玩家必败
        winMemo.put(mask, canLose);
        return canLose;
    }

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

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

        ++total; if (!sol.xorGame2(new int[]{1, 1, 2})) ++pass; else System.out.println("FAIL 810-8");
        ++total; if (sol.xorGame2(new int[]{0, 1})) ++pass; else System.out.println("FAIL 810-9");
        ++total; if (sol.xorGame2(new int[]{1, 2, 3})) ++pass; else System.out.println("FAIL 810-10");

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

复杂度分析

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

811. Subdomain Visit Count(子域名访问计数)

题目描述

网站域名 "discuss.leetcode.com" 由多个子域名组成。顶级域名为 "com",二级域名为
"leetcode.com",最低一级为 "discuss.leetcode.com"。当访问域名 "discuss.leetcode.com" 时,
同时也会隐式访问其父域名 "leetcode.com" 以及 "com"。
计数配对域名是遵循 "rep d1.d2.d3" 或 "rep d1.d2" 格式的一个域名表示,其中 rep 表示访问
域名的次数,d1.d2.d3 为域名本身。例如,"9001 discuss.leetcode.com" 就是一个计数配对域名。
给你一个计数配对域名组成的数组 cpdomains,解析得到输入中每个子域名对应的计数配对域名,
并以数组形式返回。可以按任意顺序返回答案。

示例 1

  • 输入:cpdomains = ["9001 discuss.leetcode.com"]
  • 输出:["9001 leetcode.com","9001 discuss.leetcode.com","9001 com"]

示例 2

  • 输入:cpdomains = ["900 google.mail.com", "50 yahoo.com", "1 intel.mail.com", "5 wiki.org"]
  • 输出:["901 mail.com","50 yahoo.com","900 google.mail.com","5 wiki.org","5 org","1 intel.mail.com","951 com"]
    提示:
  • 1 <= cpdomain.length <= 100
  • 1 <= cpdomain[i].length <= 100
  • rep_i 是范围 [1, 10^4] 内的一个整数
  • d1、d2 和 d3 由小写英文字母组成

解题思路

一条计数记录要同时累加到自身及其所有上级域名。记录条数与串长均不超过 100。
解法一(哈希表统计,主提交):扫描每个空格或点号后的后缀,把访问次数加到该后缀上,
最后把计数表格式化输出。
解法二(有序遍历,对照实现):统计完成后按域名排序输出,便于展示全部计数明细。

代码实现

class Solution {

    // 解法一:哈希表统计(推荐提交)
    public java.util.List<String> subdomainVisits(String[] cpdomains) {
        java.util.Map<String, Integer> cnt = new java.util.HashMap<>();
        for (String s : cpdomains) {
            int sp = s.indexOf(' ');
            int v = Integer.parseInt(s.substring(0, sp));
            String domain = s.substring(sp + 1);
            int idx = 0;
            while (idx < domain.length()) {
                String sub = domain.substring(idx);
                cnt.put(sub, cnt.getOrDefault(sub, 0) + v);
                idx = domain.indexOf('.', idx) + 1;
                if (idx == 0) {
                    break;
                }
            }
        }
        java.util.List<String> ans = new java.util.ArrayList<>();
        for (java.util.Map.Entry<String, Integer> e : cnt.entrySet()) {
            ans.add(e.getValue() + " " + e.getKey());
        }
        return ans;
    }

    // 解法二:统计后按域名排序
    public java.util.List<String> subdomainVisits2(String[] cpdomains) {
        java.util.Map<String, Integer> cnt = new java.util.HashMap<>();
        for (String s : cpdomains) {
            int sp = s.indexOf(' ');
            int v = Integer.parseInt(s.substring(0, sp));
            String domain = s.substring(sp + 1);
            for (int i = 0; i < domain.length(); ++i) {
                if (domain.charAt(i) == '.') {
                    String sub = domain.substring(i + 1);
                    cnt.put(sub, cnt.getOrDefault(sub, 0) + v);
                }
            }
            cnt.put(domain, cnt.getOrDefault(domain, 0) + v);
        }
        java.util.List<String> ans = new java.util.ArrayList<>();
        for (java.util.Map.Entry<String, Integer> e : cnt.entrySet()) {
            ans.add(e.getValue() + " " + e.getKey());
        }
        java.util.Collections.sort(ans);
        return ans;
    }

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

        ++total; if (sol.subdomainVisits(new String[]{"9001 discuss.leetcode.com"}).size() == 3) ++pass; else System.out.println("FAIL 811-1");
        ++total; if (sol.subdomainVisits(new String[]{"900 google.mail.com", "50 yahoo.com", "1 intel.mail.com", "5 wiki.org"}).size() == 7) ++pass; else System.out.println("FAIL 811-2");
        ++total; if (sol.subdomainVisits(new String[]{"900 google.mail.com"}).contains("900 google.mail.com")) ++pass; else System.out.println("FAIL 811-3");
        ++total; if (sol.subdomainVisits(new String[]{"900 google.mail.com", "1 intel.mail.com"}).contains("901 mail.com")) ++pass; else System.out.println("FAIL 811-4");
        ++total; if (sol.subdomainVisits(new String[]{"900 google.mail.com", "1 intel.mail.com"}).contains("901 com")) ++pass; else System.out.println("FAIL 811-5");
        ++total; if (sol.subdomainVisits(new String[]{"900 google.mail.com", "1 intel.mail.com"}).contains("1 intel.mail.com")) ++pass; else System.out.println("FAIL 811-6");

        ++total; if (sol.subdomainVisits2(new String[]{"9001 discuss.leetcode.com"}).size() == 3) ++pass; else System.out.println("FAIL 811-7");
        ++total; if (sol.subdomainVisits2(new String[]{"900 google.mail.com", "50 yahoo.com", "1 intel.mail.com", "5 wiki.org"}).size() == 7) ++pass; else System.out.println("FAIL 811-8");

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

复杂度分析

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

812. Largest Triangle Area(最大三角形面积)

题目描述

给你一个由 X-Y 平面上的点组成的数组 points,其中 points[i] = [xi, yi]。从其中取任意
三个不同的点组成三角形,返回能组成的最大三角形的面积。与真实值误差在 10^-5 内的答案将会
视为正确答案。

示例 1

  • 输入:points = [[0,0],[0,1],[1,0],[0,2],[2,0]]
  • 输出:2.00000

示例 2

  • 输入:points = [[1,0],[0,0],[0,1]]
  • 输出:0.50000
    提示:
  • 3 <= points.length <= 50
  • -50 <= xi, yi <= 50
  • 给出的所有点互不相同

解题思路

最大三角形面积由三点叉积的一半给出。点数至多 50,三重枚举约 10^5 次可接受。
解法一(三重枚举叉积,主提交):对每组三点用向量叉积取绝对值的一半,维护全局最大值。
解法二(固定一点的双重枚举,对照实现):先固定第一个点,再枚举其余两点计算面积,
逻辑等价的另一种写法。

代码实现

class Solution {

    // 解法一:三重枚举(推荐提交)
    public double largestTriangleArea(int[][] points) {
        double ans = 0;
        for (int[] p1 : points) {
            int x1 = p1[0], y1 = p1[1];
            for (int[] p2 : points) {
                int x2 = p2[0], y2 = p2[1];
                for (int[] p3 : points) {
                    int x3 = p3[0], y3 = p3[1];
                    int u1 = x2 - x1, v1 = y2 - y1;
                    int u2 = x3 - x1, v2 = y3 - y1;
                    double t = Math.abs(u1 * v2 - u2 * v1) / 2.0;
                    ans = Math.max(ans, t);
                }
            }
        }
        return ans;
    }

    // 解法二:固定第一点的双循环
    public double largestTriangleArea2(int[][] points) {
        double ans = 0;
        int n = points.length;
        for (int i = 0; i < n; ++i) {
            for (int j = i + 1; j < n; ++j) {
                for (int k = j + 1; k < n; ++k) {
                    ans = Math.max(ans, area(points[i], points[j], points[k]));
                }
            }
        }
        return ans;
    }

    private double area(int[] a, int[] b, int[] c) {
        // 向量 ab 与 ac 的叉积绝对值的一半
        return Math.abs((b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])) / 2.0;
    }

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

        ++total; if (Math.abs(sol.largestTriangleArea(new int[][]{{0, 0}, {0, 1}, {1, 0}, {0, 2}, {2, 0}}) - 2.0) < 1e-9) ++pass; else System.out.println("FAIL 812-1");
        ++total; if (Math.abs(sol.largestTriangleArea(new int[][]{{1, 0}, {0, 0}, {0, 1}}) - 0.5) < 1e-9) ++pass; else System.out.println("FAIL 812-2");
        ++total; if (Math.abs(sol.largestTriangleArea(new int[][]{{0, 0}, {0, 1}, {1, 1}}) - 0.5) < 1e-9) ++pass; else System.out.println("FAIL 812-3");
        ++total; if (Math.abs(sol.largestTriangleArea(new int[][]{{0, 0}, {2, 0}, {0, 2}, {1, 1}}) - 2.0) < 1e-9) ++pass; else System.out.println("FAIL 812-4");
        ++total; if (Math.abs(sol.largestTriangleArea(new int[][]{{-1, -1}, {1, -1}, {0, 1}}) - 2.0) < 1e-9) ++pass; else System.out.println("FAIL 812-5");
        ++total; if (Math.abs(sol.largestTriangleArea(new int[][]{{0, 0}, {1, 0}, {2, 0}, {3, 0}}) - 0.0) < 1e-9) ++pass; else System.out.println("FAIL 812-6");

        ++total; if (Math.abs(sol.largestTriangleArea2(new int[][]{{0, 0}, {0, 1}, {1, 0}, {0, 2}, {2, 0}}) - 2.0) < 1e-9) ++pass; else System.out.println("FAIL 812-7");
        ++total; if (Math.abs(sol.largestTriangleArea2(new int[][]{{1, 0}, {0, 0}, {0, 1}}) - 0.5) < 1e-9) ++pass; else System.out.println("FAIL 812-8");
        ++total; if (Math.abs(sol.largestTriangleArea2(new int[][]{{0, 0}, {2, 0}, {0, 2}, {1, 1}}) - 2.0) < 1e-9) ++pass; else System.out.println("FAIL 812-9");

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

复杂度分析

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

813. Largest Sum of Averages(最大平均值和的分组)

题目描述

给定数组 nums 和一个整数 k。我们将给定的数组 nums 分成最多 k 个非空子数组,且数组 内部是连续的。分数由每个子数组内的平均值的总和构成。 注意我们必须使用 nums 数组中的每一个数进行分组,并且分数不一定需要是整数。 返回我们所能得到的最大分数是多少。答案误差在 10^-6 内被视为是正确的。

示例 1

  • 输入:nums = [9,1,2,3,9], k = 3
  • 输出: 20.00000
  • 解释: nums 的最优分组是 [9], [1, 2, 3], [9]. 得到的分数是 9 + (1 + 2 + 3) / 3 + 9 = 20.

示例 2

  • 输入:nums = [1,2,3,4,5,6,7], k = 4
  • 输出: 20.50000 提示:
  • 1 <= nums.length <= 100
  • 1 <= nums[i] <= 10^4
  • 1 <= k <= nums.length

解题思路

把数组分成至多 k 段、最大化各段平均值之和。段的分界未知,n,k <= 100,按「从 i 起还剩 k 段」 搜索并记忆化即可。 解法一(前缀和 + 记忆化搜索,主提交):前缀和使一段 [i,j) 的平均值 O(1) 可得。k=1 时整段 只能作为最后一组;否则枚举第一段右端,递归剩余。 解法二(迭代 DP 二维数组,对照实现):dp[i][j] 表示前 i 个数分成 j 段的最大分数, 自底向上转移。

代码实现

class Solution {

    private double[][] f;
    private int[] s;
    private int n;

    // 解法一:记忆化搜索(推荐提交)
    public double largestSumOfAverages(int[] nums, int k) {
        n = nums.length;
        s = new int[n + 1];
        f = new double[n][k + 1];
        for (int i = 0; i < n; ++i) {
            s[i + 1] = s[i] + nums[i];
        }
        return dfs(0, k);
    }

    private double dfs(int i, int k) {
        if (i == n) {
            return 0;
        }
        if (k == 1) {
            return (s[n] - s[i]) * 1.0 / (n - i);
        }
        if (f[i][k] > 0) {
            return f[i][k];
        }
        double ans = 0;
        for (int j = i + 1; j < n; ++j) {
            ans = Math.max(ans, (s[j] - s[i]) * 1.0 / (j - i) + dfs(j, k - 1));
        }
        f[i][k] = ans;
        return ans;
    }

    // 解法二:迭代 DP
    public double largestSumOfAverages2(int[] nums, int k) {
        int n = nums.length;
        int[] pre = new int[n + 1];
        for (int i = 0; i < n; ++i) {
            pre[i + 1] = pre[i] + nums[i];
        }
        // dp[i][j]: 前 i 个数分成 j 段的最大分数
        double[][] dp = new double[n + 1][k + 1];
        for (int i = 1; i <= n; ++i) {
            dp[i][1] = pre[i] * 1.0 / i;
        }
        for (int j = 2; j <= k; ++j) {
            for (int i = j; i <= n; ++i) {
                for (int t = j - 1; t < i; ++t) {
                    dp[i][j] = Math.max(dp[i][j], dp[t][j - 1] + (pre[i] - pre[t]) * 1.0 / (i - t));
                }
            }
        }
        return dp[n][k];
    }

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

        ++total; if (Math.abs(sol.largestSumOfAverages(new int[]{9, 1, 2, 3, 9}, 3) - 20.0) < 1e-6) ++pass; else System.out.println("FAIL 813-1");
        ++total; if (Math.abs(sol.largestSumOfAverages(new int[]{1, 2, 3, 4, 5, 6, 7}, 4) - 20.5) < 1e-6) ++pass; else System.out.println("FAIL 813-2");
        ++total; if (Math.abs(sol.largestSumOfAverages(new int[]{1, 2, 3}, 1) - 2.0) < 1e-6) ++pass; else System.out.println("FAIL 813-3");
        ++total; if (Math.abs(sol.largestSumOfAverages(new int[]{5}, 1) - 5.0) < 1e-6) ++pass; else System.out.println("FAIL 813-4");
        ++total; if (Math.abs(sol.largestSumOfAverages(new int[]{1, 100, 1}, 3) - 102.0) < 1e-6) ++pass; else System.out.println("FAIL 813-5");
        ++total; if (Math.abs(sol.largestSumOfAverages(new int[]{4, 1, 7, 3}, 2) - 7.6666666667) < 1e-6) ++pass; else System.out.println("FAIL 813-6");

        ++total; if (Math.abs(sol.largestSumOfAverages2(new int[]{9, 1, 2, 3, 9}, 3) - 20.0) < 1e-6) ++pass; else System.out.println("FAIL 813-7");
        ++total; if (Math.abs(sol.largestSumOfAverages2(new int[]{1, 2, 3, 4, 5, 6, 7}, 4) - 20.5) < 1e-6) ++pass; else System.out.println("FAIL 813-8");
        ++total; if (Math.abs(sol.largestSumOfAverages2(new int[]{4, 1, 7, 3}, 2) - 7.6666666667) < 1e-6) ++pass; else System.out.println("FAIL 813-9");

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

复杂度分析

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