LeetCode926~950题目解析

3 阅读10分钟

LeetCode 926-950. 题解合集


926. Flip String to Monotone Increasing(将字符串翻转到单调递增)

题目描述

如果一个二进制字符串,是以一些 0(可能没有 0)后面跟着一些 1(也可能没有 1)
的形式组成的,那么该字符串是单调递增的。
给你一个二进制字符串 s,你可以将任何 0 翻转为 1 或者将 1 翻转为 0。
返回使 s 单调递增的最小翻转次数。

示例 1

  • 输入:s = "00110"
  • 输出:1
  • 解释:翻转最后一位得到 00111。

示例 2

  • 输入:s = "010110"
  • 输出:2
  • 解释:翻转得到 011111,或者是 000111。

示例 3

  • 输入:s = "00011000"
  • 输出:2
  • 解释:翻转得到 00000000。
    提示:
  • 1 <= s.length <= 10^5
  • s[i] 为 '0' 或 '1'

解题思路

单调递增的最终形态必然是 0...01...1:存在一个分界点,分界点左侧全为 0、右侧全为
1。枚举分界点,统计需要翻转的字符数 = 左侧原本是 1 的个数 + 右侧原本是 0 的个数。
解法一(前缀 1 计数 + 枚举分界,主提交):预处理 pre1[i] = s[0..i-1] 中 1 的
个数。枚举分界 i(左侧 [0,i-1] 全 0、右侧 [i,n-1] 全 1):翻转数 =
pre1[i] + ((n - i) - (pre1[n] - pre1[i])),即左侧的 1 全翻 0 + 右侧的 0 全翻 1。
取最小。O(n)。
解法二(DP 状态机,对照实现):dp0[i] 表示 s[0..i] 以 0 结尾(即前段全 0)的
最小翻转,dp1[i] 表示以 1 结尾(可切换为 1 后全为 1)。按当前字符转移,滚动两个
变量即可,O(n)。

代码实现

class Solution {

    // 解法一:前缀 1 计数 + 枚举分界(推荐提交)
    public int minFlipsMonoIncr(String s) {
        int n = s.length();
        int[] pre1 = new int[n + 1];
        for (int i = 0; i < n; ++i) {
            pre1[i + 1] = pre1[i] + (s.charAt(i) == '1' ? 1 : 0);
        }
        int total1 = pre1[n];
        int ans = n;
        for (int i = 0; i <= n; ++i) {   // 左侧 [0,i-1] 全 0,右侧 [i,n-1] 全 1
            int leftOnes = pre1[i];                       // 左侧的 1 需要翻成 0
            int rightZeros = (n - i) - (total1 - pre1[i]); // 右侧的 0 需要翻成 1
            ans = Math.min(ans, leftOnes + rightZeros);
        }
        return ans;
    }

    // 解法二:DP 状态机(对照实现)
    public int minFlipsMonoIncr2(String s) {
        // end0:当前处理前缀全为 0 的最小翻转;end1:当前处理前缀形如 0*1* 的最小翻转
        int end0 = 0, end1 = 0;
        for (char c : s.toCharArray()) {
            int newEnd0 = end0 + (c == '1' ? 1 : 0);
            int newEnd1 = Math.min(end0, end1) + (c == '0' ? 1 : 0);
            end0 = newEnd0;
            end1 = newEnd1;
        }
        return Math.min(end0, end1);
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        String[] ss = {"00110", "010110", "00011000", "111000", "0", "1", "1001010101"};
        int[] exp = {1, 2, 2, 3, 0, 0, 4};
        for (int t = 0; t < ss.length; ++t) {
            ++total;
            int g1 = s.minFlipsMonoIncr(ss[t]);
            int g2 = s.minFlipsMonoIncr2(ss[t]);
            if (g1 == exp[t] && g2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 926-" + (t + 1) + ": " + ss[t] + " got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0926 PASS ==");
        } else {
            System.out.println("== 0926 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一(前缀 1 计数)O(n)O(n)
解法二(DP 状态机)O(n)O(1)

927. Three Equal Parts(三等分)

题目描述

给定一个由 0 和 1 组成的数组 arr,将数组分成 3 个非空的部分,使得所有这些部分表示相同的二进制值。
如果可以做到,请返回任何 [i, j],其中 i+1 < j,这样一来:

  • arr[0], arr[1], ..., arr[i] 为第一部分;
  • arr[i+1], arr[i+2], ..., arr[j-1] 为第二部分;
  • arr[j], arr[j+1], ..., arr[arr.length-1] 为第三部分;
  • 这三个部分所表示的二进制值相等。
    如果无法做到,就返回 [-1, -1]。
    注意,在考虑每个部分所表示的二进制时,应当将其看作一个整体。前导零是允许的,所以 [0,1,1] 和 [1,1] 表示相同的值。

示例 1

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

示例 2

  • 输入:arr = [1,1,0,1,1]
  • 输出:[-1,-1]

示例 3

  • 输入:arr = [1,1,0,0,1]
  • 输出:[0,2]
    提示:
  • 3 <= arr.length <= 3 * 10^4
  • arr[i] 是 0 或 1

解题思路

三个部分表示相同的二进制值。真正决定数值的是每个部分中 1 的个数与各 1 的相对位置。
解法一(计数 + 三指针,主提交):记数组中 1 的总数为 cnt。

  • 若 cnt % 3 != 0,无法三等分,返回 [-1, -1];
  • 若 cnt == 0,全是 0,任意切分(三部分均为 0)都可,返回 [0, n-1];
  • 否则每部分应含 cnt/3 个 1。用 find(x) 定位第 1 个、第 cnt/3+1 个、第 2*cnt/3+1 个 1 的位置 i/j/k,
    这三处分别是三段第一个 1 的位置(前导零不影响数值)。从 i/j/k 同步向右逐位比较,
    只要 arr[i]==arr[j]==arr[k] 就一起右移,直到 k 走到数组末尾;若全程相等则合法,
    返回切分点 [i-1, j],否则返回 [-1, -1]。时间复杂度 O(n),空间 O(1)。
    解法二(后缀零处理对照):先定位三段第一个 1 的位置,然后选定第三段末尾,用第三段整体
    (含后缀零)作为基准,逐位核对第一、二段与之完全一致;此实现显式处理每段等长(长度差为
    后缀零)的约束,逻辑等价但更贴近二进制等值的定义。

代码实现

class Solution {

    // 解法一:计数 + 三指针(推荐提交)
    public int[] threeEqualParts(int[] arr) {
        int n = arr.length;
        int cnt = 0;
        for (int v : arr) {
            cnt += v;
        }
        if (cnt % 3 != 0) {
            return new int[] {-1, -1};
        }
        if (cnt == 0) {
            return new int[] {0, n - 1};
        }
        int per = cnt / 3;
        int i = find(arr, 1);          // 第一段第一个 1
        int j = find(arr, per + 1);    // 第二段第一个 1
        int k = find(arr, per * 2 + 1);// 第三段第一个 1
        while (k < n && arr[i] == arr[j] && arr[j] == arr[k]) {
            ++i;
            ++j;
            ++k;
        }
        if (k == n) {
            return new int[] {i - 1, j};
        }
        return new int[] {-1, -1};
    }

    private int find(int[] arr, int x) {
        int s = 0;
        for (int i = 0; i < arr.length; ++i) {
            s += arr[i];
            if (s == x) {
                return i;
            }
        }
        return 0;
    }

    // 解法二:显式对齐每段(对照实现)
    public int[] threeEqualParts2(int[] arr) {
        int n = arr.length;
        int cnt = 0;
        for (int v : arr) {
            cnt += v;
        }
        if (cnt % 3 != 0) {
            return new int[] {-1, -1};
        }
        if (cnt == 0) {
            return new int[] {0, n - 1};
        }
        int per = cnt / 3;
        // 三段第一个 1 的位置
        int a = find(arr, 1);
        int b = find(arr, per + 1);
        int c = find(arr, per * 2 + 1);
        // 以第三段(含后缀零)为基准,其有效位数 len = n - c
        int len = n - c;
        // 第一、二段必须能容纳 len 位有效位,且三段互不重叠:
        // 第一段有效位 [a, a+len),第二段有效位 [b, b+len),且 a+len <= b、b+len <= c
        if (a + len > b || b + len > c) {
            return new int[] {-1, -1};
        }
        for (int t = 0; t < len; ++t) {
            if (arr[a + t] != arr[c + t] || arr[b + t] != arr[c + t]) {
                return new int[] {-1, -1};
            }
        }
        // 切分点:第一段结尾 = a+len-1,第二段结尾 = b+len-1,第三段起点 = b+len
        return new int[] {a + len - 1, b + len};
    }

    private static void p(int[] a) {
        System.out.println("[" + a[0] + "," + a[1] + "]");
    }

    // 校验切分 [i, j] 是否合法:三段非空、i+1<j,且三段二进制值相等
    private static boolean valid(int[] arr, int i, int j) {
        if (i < 0 || j < 0) {
            return false;
        }
        int n = arr.length;
        if (i + 1 >= j || j >= n || i >= n) {
            return false;
        }
        long a = 0, b = 0, c = 0;
        for (int t = 0; t <= i; ++t) {
            a = (a << 1) | arr[t];
        }
        for (int t = i + 1; t <= j - 1; ++t) {
            b = (b << 1) | arr[t];
        }
        for (int t = j; t < n; ++t) {
            c = (c << 1) | arr[t];
        }
        return a == b && b == c;
    }

    // 预期无解的用例要求两解都返回 [-1,-1];预期有解的用例要求返回的切分合法
    private static boolean matchEx(int[] arr, int[] g, int[] ex) {
        if (ex[0] == -1) {
            return g[0] == -1 && g[1] == -1;
        }
        return valid(arr, g[0], g[1]);
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[][] cs = {{1,0,1,0,1}, {1,1,0,1,1}, {1,1,0,0,1}, {0,0,0,0}, {1,0,0,1,0,1}, {1,1,0,1,0,0,1,0,0,1,1,0,0}};
        int[][] ex = {{0,3}, {-1,-1}, {0,2}, {0,3}, {0,4}, {-1,-1}};
        for (int t = 0; t < cs.length; ++t) {
            ++total;
            int[] g1 = s.threeEqualParts(cs[t]);
            int[] g2 = s.threeEqualParts2(cs[t]);
            if (matchEx(cs[t], g1, ex[t]) && matchEx(cs[t], g2, ex[t])) {
                ++pass;
            } else {
                System.out.print("FAIL 927-" + (t + 1) + ": got ");
                p(g1); System.out.print("      / "); p(g2);
            }
        }
        if (pass == total) {
            System.out.println("== 0927 PASS ==");
        } else {
            System.out.println("== 0927 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一(计数 + 三指针)O(n)O(1)
解法二(后缀零显式对齐)O(n)O(1)

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

题目描述

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

示例 1

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

示例 2

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

示例 3

  • 输入:graph = [[1,1,0,0],[1,1,1,0],[0,1,1,1],[0,0,1,1]], initial = [0,1]
  • 输出:1
    提示:
  • n == graph.length
  • n == graph[i].length
  • 2 <= n <= 300
  • graph[i][j] 是 0 或 1
  • graph[i][j] == graph[j][i]
  • graph[i][i] == 1
  • 1 <= initial.length < n
  • initial 中每个整数都不同

解题思路

本题与 924 的区别在于:删除节点会完全移除该节点,它不再向邻居传播;而其余初始节点仍会
继续感染各自的连通块。因此先把所有干净节点(不在 initial 中)按边合并成若干连通块,
再考察每个初始节点能接触到哪些干净块:若某块被多个初始节点同时接触,删掉其中任何一个,
该块仍会被其余初始节点感染,无法得救;只有被恰好一个初始节点接触的块,删掉那个节点
才能整块获救。
解法一(并查集 + 独占块累加,主提交):清点块归属后,对每个初始节点累加其"独占块"大小,
该值即删除它之后相比最坏情况减少的感染数,取能救最多(并列取编号最小)的节点。O(n^2 α(n))。
解法二(DFS 分量对照):不用并查集,直接用 DFS 从每个干净节点出发染色分量,等价统计。

代码实现

class Solution {

    // 解法一:并查集(推荐提交)
    public int minMalwareSpread(int[][] graph, int[] initial) {
        int n = graph.length;
        boolean[] bad = new boolean[n];
        for (int x : initial) {
            bad[x] = true;
        }
        UnionFind uf = new UnionFind(n);
        for (int i = 0; i < n; ++i) {
            if (bad[i]) {
                continue;
            }
            for (int j = i + 1; j < n; ++j) {
                if (!bad[j] && graph[i][j] == 1) {
                    uf.union(i, j);
                }
            }
        }
        // g[i]:初始节点 i 接触到的干净块根集合;cnt[root]:该干净块被几个初始节点接触
        java.util.Set<Integer>[] g = new java.util.Set[n];
        for (int i = 0; i < n; ++i) {
            g[i] = new java.util.HashSet<>();
        }
        int[] cnt = new int[n];
        for (int x : initial) {
            for (int j = 0; j < n; ++j) {
                if (!bad[j] && graph[x][j] == 1) {
                    g[x].add(uf.find(j));
                }
            }
            for (int root : g[x]) {
                ++cnt[root];
            }
        }
        int ans = initial[0], mx = -1;
        for (int x : initial) {
            int saved = 0;
            for (int root : g[x]) {
                if (cnt[root] == 1) {
                    saved += uf.size(root);
                }
            }
            if (saved > mx || (saved == mx && x < ans)) {
                ans = x;
                mx = saved;
            }
        }
        return ans;
    }

    // 解法二:DFS 分量统计(对照实现)
    public int minMalwareSpread2(int[][] graph, int[] initial) {
        int n = graph.length;
        boolean[] bad = new boolean[n];
        for (int x : initial) {
            bad[x] = true;
        }
        int[] comp = new int[n];
        java.util.Arrays.fill(comp, -1);
        int compCnt = 0;
        int[] compSize = new int[n];
        for (int i = 0; i < n; ++i) {
            if (!bad[i] && comp[i] == -1) {
                dfsComp(graph, bad, comp, i, compCnt, n);
                ++compCnt;
            }
        }
        // 统计每个分量大小
        for (int i = 0; i < n; ++i) {
            if (comp[i] != -1) {
                ++compSize[comp[i]];
            }
        }
        // 接触计数
        int[] touchCnt = new int[compCnt];
        boolean[][] touched = new boolean[n][compCnt];
        for (int x : initial) {
            for (int j = 0; j < n; ++j) {
                if (!bad[j] && graph[x][j] == 1) {
                    int c = comp[j];
                    if (!touched[x][c]) {
                        touched[x][c] = true;
                        ++touchCnt[c];
                    }
                }
            }
        }
        int ans = initial[0], mx = -1;
        for (int x : initial) {
            int saved = 0;
            for (int c = 0; c < compCnt; ++c) {
                if (touched[x][c] && touchCnt[c] == 1) {
                    saved += compSize[c];
                }
            }
            if (saved > mx || (saved == mx && x < ans)) {
                ans = x;
                mx = saved;
            }
        }
        return ans;
    }

    private void dfsComp(int[][] graph, boolean[] bad, int[] comp, int u, int id, int n) {
        comp[u] = id;
        for (int v = 0; v < n; ++v) {
            if (!bad[v] && graph[u][v] == 1 && comp[v] == -1) {
                dfsComp(graph, bad, comp, v, id, n);
            }
        }
    }

    static class UnionFind {
        int[] p;
        int[] sz;
        UnionFind(int n) {
            p = new int[n];
            sz = new int[n];
            for (int i = 0; i < n; ++i) {
                p[i] = i;
                sz[i] = 1;
            }
        }
        int find(int x) {
            if (p[x] != x) {
                p[x] = find(p[x]);
            }
            return p[x];
        }
        boolean union(int a, int b) {
            int ra = find(a), rb = find(b);
            if (ra == rb) {
                return false;
            }
            if (sz[ra] < sz[rb]) {
                int t = ra;
                ra = rb;
                rb = t;
            }
            p[rb] = ra;
            sz[ra] += sz[rb];
            return true;
        }
        int size(int root) {
            return sz[root];
        }
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[][][] graphs = {
            {{1,1,0},{1,1,0},{0,0,1}},
            {{1,1,0},{1,1,1},{0,1,1}},
            {{1,1,0,0},{1,1,1,0},{0,1,1,1},{0,0,1,1}},
            {{1,1,0},{1,1,0},{0,0,1}}
        };
        int[][] initials = {{0,1}, {0,1}, {0,1}, {0}};
        int[] exp = {0, 1, 1, 0};
        for (int t = 0; t < graphs.length; ++t) {
            ++total;
            int g1 = s.minMalwareSpread(graphs[t], initials[t]);
            int g2 = s.minMalwareSpread2(graphs[t], initials[t]);
            if (g1 == exp[t] && g2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 928-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0928 PASS ==");
        } else {
            System.out.println("== 0928 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析


929. Unique Email Addresses(独特的电子邮件地址)

题目描述

每个有效电子邮件地址都由一个本地名和一个域名组成,以 '@' 符号分隔。除小写字母之外, 电子邮件地址还可以含有一个或多个 '.' 或 '+'。

  • 如果在电子邮件地址的本地名部分中的某些字符之间添加句点('.'),则发往那里的邮件将会 转发到本地名中没有点的同一地址。此规则不适用于域名。
  • 如果在本地名中添加加号('+'),则会忽略第一个加号后面的所有内容。此规则不适用于域名。 给你一个字符串数组 emails,我们会向每个 emails[i] 发送一封电子邮件。 返回实际收到邮件的不同地址数目。

示例 1

示例 2

  • 输入:emails = ["a@leetcode.com","b@leetcode.com","c@leetcode.com"]
  • 输出:3 提示:
  • 1 <= emails.length <= 100
  • 1 <= emails[i].length <= 100
  • emails[i] 由小写英文字母、'+'、'.' 和 '@' 组成
  • 每个 emails[i] 都包含有且仅有一个 '@' 字符

解题思路

规则只作用于 @ 左侧的本地名:忽略 .,并丢弃第一个 + 及其后的内容;域名原样保留。 规范化的地址(变换后本地名 + 原域名)相同的邮件实际送到同一收件人,去重后计数即为答案。 解法一(哈希表,主提交):逐封邮件解析出 @ 位置,遍历本地名字符:遇到 . 跳过, 遇到 + 提前终止,其余字符追加;拼回域名放入 Set,返回集合大小。O(L)。 解法二(split + StringBuilder 对照):用 split("@") 拆分,逻辑等价,写法更贴近题面。

代码实现

class Solution {

    // 解法一:哈希表,手写解析(推荐提交)
    public int numUniqueEmails(String[] emails) {
        java.util.Set<String> set = new java.util.HashSet<>();
        for (String email : emails) {
            int at = email.indexOf('@');
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < at; ++i) {
                char c = email.charAt(i);
                if (c == '.') {
                    continue;
                }
                if (c == '+') {
                    break;
                }
                sb.append(c);
            }
            set.add(sb.toString() + email.substring(at));
        }
        return set.size();
    }

    // 解法二:split 拆分(对照实现)
    public int numUniqueEmails2(String[] emails) {
        java.util.Set<String> set = new java.util.HashSet<>();
        for (String email : emails) {
            String[] parts = email.split("@");
            StringBuilder sb = new StringBuilder();
            for (char c : parts[0].toCharArray()) {
                if (c == '.') {
                    continue;
                }
                if (c == '+') {
                    break;
                }
                sb.append(c);
            }
            set.add(sb.toString() + "@" + parts[1]);
        }
        return set.size();
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        String[][] inputs = {
            {"test.email+alex@leetcode.com","test.e.mail+bob.cathy@leetcode.com","testemail+david@lee.tcode.com"},
            {"a@leetcode.com","b@leetcode.com","c@leetcode.com"},
            {"test.email+alex@leetcode.com","test.e.mail+bob.cathy@leetcode.com","testemail+david@lee.tcode.com","a@leetcode.com"},
            {"m.y+name@email.com","my@email.com"}
        };
        int[] exp = {2, 3, 3, 1};
        for (int t = 0; t < inputs.length; ++t) {
            ++total;
            int g1 = s.numUniqueEmails(inputs[t]);
            int g2 = s.numUniqueEmails2(inputs[t]);
            if (g1 == exp[t] && g2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 929-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0929 PASS ==");
        } else {
            System.out.println("== 0929 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析


930. Binary Subarrays With Sum(和相同的二元子数组)

题目描述

给你一个二元数组 nums,和一个整数 goal,请你统计并返回有多少个和为 goal 的非空子数组。 子数组是数组的一段连续部分。

示例 1

  • 输入:nums = [1,0,1,0,1], goal = 2
  • 输出:4
  • 解释:有 4 个满足题目要求的子数组:[1,0,1]、[1,0,1,0]、[0,1,0,1]、[1,0,1]

示例 2

  • 输入:nums = [0,0,0,0,0], goal = 0
  • 输出:15 提示:
  • 1 <= nums.length <= 3 * 10^4
  • nums[i] 不是 0 就是 1
  • 0 <= goal <= nums.length

解题思路

求元素为 0/1 的数组中和为 goal 的子数组个数,n 较大不能枚举区间。 解法一(前缀和 + 计数,主提交):设 s 为当前前缀和,以当前位置为右端点的合法子数组数等于 之前前缀和为 s-goal 的出现次数。用计数数组维护前缀和出现次数,初始 cnt[0]=1, 边遍历边累加。O(n) 时间、O(n) 空间。 解法二(双指针):数组非负,窗口和随右端扩展单调不减。用 i1 维护和 <= goal 的最大左端 (即和恰大于 goal 的边界),i2 维护和 < goal 的最大左端(即和至少为 goal 的边界), 则右端固定时左端落在 [i1, i2) 的窗口和恰为 goal,个数为 i2 - i1。O(n) 时间、O(1) 空间。

代码实现

class Solution {

    // 解法一:前缀和 + 计数(推荐提交)
    public int numSubarraysWithSum(int[] nums, int goal) {
        int n = nums.length;
        int[] cnt = new int[n + 1];
        cnt[0] = 1;
        int ans = 0, s = 0;
        for (int v : nums) {
            s += v;
            if (s - goal >= 0) {
                ans += cnt[s - goal];
            }
            ++cnt[s];
        }
        return ans;
    }

    // 解法二:双指针(对照实现)
    public int numSubarraysWithSum2(int[] nums, int goal) {
        int n = nums.length;
        int i1 = 0, i2 = 0;
        int s1 = 0, s2 = 0;
        int ans = 0;
        for (int j = 0; j < n; ++j) {
            s1 += nums[j];
            s2 += nums[j];
            while (i1 <= j && s1 > goal) {
                s1 -= nums[i1++];
            }
            while (i2 <= j && s2 >= goal) {
                s2 -= nums[i2++];
            }
            ans += i2 - i1;
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[][] nums = {{1,0,1,0,1}, {0,0,0,0,0}, {0,0,0,0,0}, {1,1,1,1,1}};
        int[] goals = {2, 0, 3, 2};
        int[] exp = {4, 15, 0, 4};
        for (int t = 0; t < nums.length; ++t) {
            ++total;
            int g1 = s.numSubarraysWithSum(nums[t], goals[t]);
            int g2 = s.numSubarraysWithSum2(nums[t], goals[t]);
            if (g1 == exp[t] && g2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 930-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0930 PASS ==");
        } else {
            System.out.println("== 0930 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析


931. Minimum Falling Path Sum(下降路径最小和)

题目描述

给你一个 n x n 的方形整数数组 matrix,请你找出并返回通过 matrix 的下降路径的最小和。 下降路径可以从第一行中的任何元素开始,并从每一行中选择一个元素。在下一行选择的元素和当前行 所选元素最多相隔一列(即位于正下方或者沿对角线向左或者向右的第一个元素)。具体来说,位置 (row, col) 的下一个元素应当是 (row+1, col-1)、(row+1, col) 或者 (row+1, col+1)。

示例 1

  • 输入:matrix = [[2,1,3],[6,5,4],[7,8,9]]
  • 输出:13

示例 2

  • 输入:matrix = [[-19,57],[-40,-5]]
  • 输出:-59 提示:
  • n == matrix.length == matrix[i].length
  • 1 <= n <= 100
  • -100 <= matrix[i][j] <= 100

解题思路

到达 (i,j) 的最小路径和只依赖上一行 j-1, j, j+1 三个位置,是经典网格 DP。 解法一(滚动数组 DP,主提交):f[j] 表示从第一行下降到当前行第 j 列的最小和; 逐行推进时,新的 g[j] = min(f[j-1], f[j], f[j+1]) + matrix[i][j](越界列忽略), 用两个一维数组滚动,最后取 f 的最小值。O(n^2) 时间、O(n) 空间。 解法二(二维 DP 对照):直接开 n x n 表逐行填,语义更直观,空间 O(n^2)。

代码实现

class Solution {

    // 解法一:滚动数组 DP(推荐提交)
    public int minFallingPathSum(int[][] matrix) {
        int n = matrix.length;
        int[] f = new int[n];
        for (int j = 0; j < n; ++j) {
            f[j] = matrix[0][j];
        }
        for (int i = 1; i < n; ++i) {
            int[] g = new int[n];
            for (int j = 0; j < n; ++j) {
                int best = f[j];
                if (j > 0) {
                    best = Math.min(best, f[j - 1]);
                }
                if (j + 1 < n) {
                    best = Math.min(best, f[j + 1]);
                }
                g[j] = best + matrix[i][j];
            }
            f = g;
        }
        int ans = Integer.MAX_VALUE;
        for (int v : f) {
            ans = Math.min(ans, v);
        }
        return ans;
    }

    // 解法二:二维 DP(对照实现)
    public int minFallingPathSum2(int[][] matrix) {
        int n = matrix.length;
        int[][] dp = new int[n][n];
        for (int j = 0; j < n; ++j) {
            dp[0][j] = matrix[0][j];
        }
        for (int i = 1; i < n; ++i) {
            for (int j = 0; j < n; ++j) {
                int best = dp[i - 1][j];
                if (j > 0) {
                    best = Math.min(best, dp[i - 1][j - 1]);
                }
                if (j + 1 < n) {
                    best = Math.min(best, dp[i - 1][j + 1]);
                }
                dp[i][j] = best + matrix[i][j];
            }
        }
        int ans = Integer.MAX_VALUE;
        for (int j = 0; j < n; ++j) {
            ans = Math.min(ans, dp[n - 1][j]);
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[][][] mats = {
            {{2,1,3},{6,5,4},{7,8,9}},
            {{-19,57},{-40,-5}},
            {{1}},
            {{-1,-2},{-3,-4}}
        };
        int[] exp = {13, -59, 1, -6};
        for (int t = 0; t < mats.length; ++t) {
            ++total;
            int g1 = s.minFallingPathSum(mats[t]);
            int g2 = s.minFallingPathSum2(mats[t]);
            if (g1 == exp[t] && g2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 931-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0931 PASS ==");
        } else {
            System.out.println("== 0931 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法二(二维 DP 对照):直接开 n x n 表逐行填,语义更直观,空间 O(n^2)。


932. Beautiful Array(漂亮数组)

题目描述

如果长度为 n 的数组 nums 满足下述条件,则认为该数组是一个漂亮数组:

  • nums 是由范围 [1, n] 的整数组成的一个排列。
  • 对于每个 0 <= i < j < n,均不存在下标 k(i < k < j)使得 2 * nums[k] == nums[i] + nums[j]。 给你整数 n,返回长度为 n 的任一漂亮数组。本题保证对于给定的 n 至少存在一个有效答案。

示例 1

  • 输入:n = 4
  • 输出:[2,1,4,3]

示例 2

  • 输入:n = 5
  • 输出:[3,1,2,5,4] 提示:
  • 1 <= n <= 1000

解题思路

漂亮数组禁止 2 * A[k] == A[i] + A[j](其中 i < k < j)。等式左侧为偶数,因此只要 A[i] 与 A[j] 一奇一偶,无论中间元素如何取值等式都不可能成立。 解法一(分治,主提交):将规模 n 拆成左半边 (n+1)/2(映射为奇数 2x-1)与右半边 n/2(映射为偶数 2x)。仿射变换 x -> kx+b 保持漂亮性,且奇数段、偶数段各自内部 不会有 A[i]、A[j] 同奇偶的冲突;拼接后任意跨越两段的对子一奇一偶,天然安全。 递归直到 n=1 返回 [1]。O(n log n)。 解法二(迭代构造对照):从 [1] 开始,每一轮把当前数组变换为奇数部分 + 偶数部分 (2x-1 与 2x),长度翻倍并始终漂亮,log n 轮后得到答案。与解法一殊途同归。

代码实现

class Solution {

    // 解法一:分治递归(推荐提交)
    public int[] beautifulArray(int n) {
        if (n == 1) {
            return new int[] {1};
        }
        int[] left = beautifulArray((n + 1) >> 1);
        int[] right = beautifulArray(n >> 1);
        int[] ans = new int[n];
        int p = 0;
        for (int x : left) {
            ans[p++] = x * 2 - 1;
        }
        for (int x : right) {
            ans[p++] = x * 2;
        }
        return ans;
    }

    // 解法二:迭代加倍构造(对照实现)
    public int[] beautifulArray2(int n) {
        int[] cur = new int[] {1};
        while (cur.length < n) {
            int[] next = new int[cur.length * 2];
            int p = 0;
            for (int x : cur) {
                next[p++] = x * 2 - 1;
            }
            for (int x : cur) {
                next[p++] = x * 2;
            }
            cur = next;
        }
        // 当前 cur 是 1..2^k 的漂亮排列;保留 <= n 的元素即得 1..n 的漂亮排列
        //(子序列仍满足无不和谐三元组,且恰好含 1..n 各一次)
        int[] ans = new int[n];
        int q = 0;
        for (int x : cur) {
            if (x <= n) {
                ans[q++] = x;
            }
        }
        return ans;
    }

    // 校验数组是否为 1..n 的排列且满足漂亮条件
    public boolean check(int[] a) {
        int n = a.length;
        boolean[] seen = new boolean[n + 1];
        for (int x : a) {
            if (x < 1 || x > n || seen[x]) {
                return false;
            }
            seen[x] = true;
        }
        for (int i = 0; i < n; ++i) {
            for (int k = i + 1; k < n; ++k) {
                for (int j = k + 1; j < n; ++j) {
                    if (2L * a[k] == (long) a[i] + a[j]) {
                        return false;
                    }
                }
            }
        }
        return true;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[] ns = {1, 2, 3, 4, 5, 8, 10};
        for (int t = 0; t < ns.length; ++t) {
            ++total;
            int n = ns[t];
            int[] g1 = s.beautifulArray(n);
            int[] g2 = s.beautifulArray2(n);
            if (s.check(g1) && s.check(g2)) {
                ++pass;
            } else {
                System.out.println("FAIL 932-" + (t + 1) + ": n=" + n);
            }
        }
        if (pass == total) {
            System.out.println("== 0932 PASS ==");
        } else {
            System.out.println("== 0932 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析


933. Number of Recent Calls(最近的请求次数)

题目描述

写一个 RecentCounter 类来计算特定时间范围内最近的请求。 请你实现 RecentCounter 类:

  • RecentCounter() 初始化计数器,请求数为 0。
  • int ping(int t) 在时间 t 添加一个新请求,其中 t 表示以毫秒为单位的某个时间, 然后返回在包含范围 [t - 3000, t] 内发生的请求数量,即新请求加上所有早于或等于 3000 毫秒前的请求。 保证每次对 ping 的调用都使用比之前更大的 t 值。

示例 1

  • 输入:["RecentCounter", "ping", "ping", "ping", "ping"] [[], [1], [100], [3001], [3002]]
  • 输出:[null, 1, 2, 3, 3]
  • 解释:ping(1)=1, ping(100)=2, ping(3001)=3, ping(3002)=3 提示:
  • 1 <= t <= 10^9
  • 保证每次对 ping 调用所使用的 t 值都严格递增
  • 至多调用 ping 方法 10^4 次

解题思路

t 严格递增,早于 t-3000 的请求在后续查询中也不可能重新进入窗口,可以安全丢弃。 解法一(队列,主提交):维护一个单调递增队列,ping(t) 先入队,再从队头弹出所有 < t-3000 的元素,队列大小即窗口内请求数。每个请求至多入队出队一次,均摊 O(1)。 解法二(二分查找):将所有 t 存入数组,由于严格递增有序,用二分定位第一个 >= t-3000 的下标,size - idx 即为答案。单次查询 O(log n)。

代码实现

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

class RecentCounter {
    private Deque<Integer> q = new ArrayDeque<>();

    public RecentCounter() {
    }

    public int ping(int t) {
        q.offer(t);
        while (q.peek() < t - 3000) {
            q.poll();
        }
        return q.size();
    }

    // ===== 本地测试(解法二对照)=====
    private int[] s = new int[10010];
    private int idx;

    public int ping2(int t) {
        s[idx++] = t;
        return idx - lowerBound(t - 3000);
    }

    private int lowerBound(int x) {
        int lo = 0, hi = idx;
        while (lo < hi) {
            int mid = (lo + hi) >>> 1;
            if (s[mid] >= x) {
                hi = mid;
            } else {
                lo = mid + 1;
            }
        }
        return lo;
    }

    public static void main(String[] args) {
        RecentCounter rc = new RecentCounter();
        RecentCounter rc2 = new RecentCounter();
        int pass = 0, total = 0;
        int[] ts = {1, 100, 3001, 3002, 5000, 8000, 11000};
        int[] exp = {1, 2, 3, 3, 3, 2, 2};
        for (int i = 0; i < ts.length; ++i) {
            ++total;
            int g1 = rc.ping(ts[i]);
            int g2 = rc2.ping2(ts[i]);
            if (g1 == exp[i] && g2 == exp[i]) {
                ++pass;
            } else {
                System.out.println("FAIL 933-" + (i + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[i]);
            }
        }
        if (pass == total) {
            System.out.println("== 0933 PASS ==");
        } else {
            System.out.println("== 0933 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析


934. Shortest Bridge(最短的桥)

题目描述

给你一个大小为 n x n 的二元矩阵 grid,其中 1 表示陆地,0 表示水域。 岛是由四面相连的 1 形成的一个最大组,即不会与非组内的任何其他 1 相连。 grid 中恰好存在两座岛。 你可以将任意数量的 0 变为 1,以使两座岛连接起来,变成一座岛。 返回必须翻转的 0 的最小数目。

示例 1

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

示例 2

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

示例 3

  • 输入:grid = [[1,1,1,1,1],[1,0,0,0,1],[1,0,1,0,1],[1,0,0,0,1],[1,1,1,1,1]]
  • 输出:1 提示:
  • n == grid.length == grid[i].length
  • 2 <= n <= 100
  • grid[i][j] 为 0 或 1
  • grid 中恰有两个岛

解题思路

最少翻转次数等价于两座岛之间的最短距离(途经的水域格数,翻转它们即可连成桥)。 解法一(DFS + BFS,主提交):先找到第一座岛的任意一格,用 DFS 把整个岛标记为 2 并入队作为 BFS 起点;然后 BFS 逐层向外扩展,经过一格 0(标记为 2 防止回头计数), 当扩展到另一座岛(grid[x][y]==1)时,当前 BFS 层数即为答案。O(n^2)。 解法二(多源 BFS + 距离数组对照):把两座岛分别标记成 1/2,从其中一座岛的所有格子 同时出发做多源 BFS,记录每个格子到源岛的距离,第一次碰到另一座岛的格子即返回距离。

代码实现

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

class Solution {
    private int[] dirs = {-1, 0, 1, 0, -1};
    private int[][] grid;
    private int n;
    private Deque<int[]> q = new ArrayDeque<>();

    // 解法一:DFS + BFS(推荐提交)
    public int shortestBridge(int[][] grid) {
        this.grid = grid;
        n = grid.length;
        q.clear();
        boolean found = false;
        for (int i = 0; i < n && !found; ++i) {
            for (int j = 0; j < n; ++j) {
                if (grid[i][j] == 1) {
                    dfsMark(i, j);
                    found = true;
                    break;
                }
            }
        }
        int ans = 0;
        while (true) {
            int sz = q.size();
            for (int t = 0; t < sz; ++t) {
                int[] p = q.poll();
                for (int d = 0; d < 4; ++d) {
                    int x = p[0] + dirs[d], y = p[1] + dirs[d + 1];
                    if (x < 0 || x >= n || y < 0 || y >= n) {
                        continue;
                    }
                    if (grid[x][y] == 1) {
                        return ans;
                    }
                    if (grid[x][y] == 0) {
                        grid[x][y] = 2;
                        q.offer(new int[] {x, y});
                    }
                }
            }
            ++ans;
        }
    }

    private void dfsMark(int i, int j) {
        if (i < 0 || i >= n || j < 0 || j >= n || grid[i][j] != 1) {
            return;
        }
        grid[i][j] = 2;
        q.offer(new int[] {i, j});
        for (int d = 0; d < 4; ++d) {
            dfsMark(i + dirs[d], j + dirs[d + 1]);
        }
    }

    // 解法二:多源 BFS(对照实现)
    public int shortestBridge2(int[][] g) {
        int m = g.length;
        boolean[][] seen = new boolean[m][m];
        Deque<int[]> queue = new ArrayDeque<>();
        boolean found = false;
        for (int i = 0; i < m && !found; ++i) {
            for (int j = 0; j < m; ++j) {
                if (g[i][j] == 1) {
                    dfsCollect(g, i, j, queue, m);
                    found = true;
                    break;
                }
            }
        }
        int ans = 0;
        while (!queue.isEmpty()) {
            int sz = queue.size();
            for (int t = 0; t < sz; ++t) {
                int[] p = queue.poll();
                for (int d = 0; d < 4; ++d) {
                    int x = p[0] + dirs[d], y = p[1] + dirs[d + 1];
                    if (x < 0 || x >= m || y < 0 || y >= m || seen[x][y]) {
                        continue;
                    }
                    if (g[x][y] == 1) {
                        return ans;   // 碰到另一座岛,层数即翻转数
                    }
                    seen[x][y] = true;
                    queue.offer(new int[] {x, y});
                }
            }
            ++ans;
        }
        return ans;
    }

    private void dfsCollect(int[][] g, int i, int j, Deque<int[]> queue, int m) {
        if (i < 0 || i >= m || j < 0 || j >= m || g[i][j] != 1) {
            return;
        }
        g[i][j] = 2;
        queue.offer(new int[] {i, j});
        for (int d = 0; d < 4; ++d) {
            dfsCollect(g, i + dirs[d], j + dirs[d + 1], queue, m);
        }
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[][][] grids = {
            {{0,1},{1,0}},
            {{0,1,0},{0,0,0},{0,0,1}},
            {{1,1,1,1,1},{1,0,0,0,1},{1,0,1,0,1},{1,0,0,0,1},{1,1,1,1,1}},
            {{0,0,0,1},{0,0,0,0},{0,0,0,0},{1,0,0,0}}
        };
        int[] exp = {1, 2, 1, 5};
        for (int t = 0; t < grids.length; ++t) {
            ++total;
            int[][] copy = new int[grids[t].length][];
            for (int r = 0; r < grids[t].length; ++r) {
                copy[r] = grids[t][r].clone();
            }
            int g1 = s.shortestBridge(grids[t]);
            int g2 = s.shortestBridge2(copy);
            if (g1 == exp[t] && g2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 934-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0934 PASS ==");
        } else {
            System.out.println("== 0934 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析


935. Knight Dialer(骑士拨号器)

题目描述

象棋骑士有一个独特的移动方式,它可以垂直移动两个方格、水平移动一个方格,或者水平移动两个 方格、垂直移动一个方格(两者都形成一个 L 的形状)。 我们有一个象棋骑士和一个电话垫,骑士只能站在一个数字单元格上(数字 0~9 的 3x4 布局, 其中 1/2/3 为第一行,4/5/6 为第二行,7/8/9 为第三行,0 在第四行第二列)。 给定一个整数 n,返回我们可以拨多少个长度为 n 的不同电话号码。 你可以将骑士放置在任何数字单元格上,然后你应该执行 n-1 次移动来获得长度为 n 的号码。 因为答案可能很大,所以输出答案模 10^9 + 7。

示例 1

  • 输入:n = 1
  • 输出:10

示例 2

  • 输入:n = 2
  • 输出:20

示例 3

  • 输入:n = 3131
  • 输出:136006598 提示:
  • 1 <= n <= 5000

解题思路

骑士在每个数字上能跳到的下一格是固定的,定义"前驱":能一步跳到 d 的数字集合。 0←{4,6} 1←{6,8} 2←{7,9} 3←{4,8} 4←{0,3,9} 5←{} 6←{0,1,7} 7←{2,6} 8←{1,3} 9←{2,4}。 解法一(线性递推,主提交):f[d] 表示长度为当前轮次、且末位数字为 d 的方案数。 初始 f[d]=1(长度为 1);每跳一步(共 n-1 次),g[d] = sum(f[p], p 是 d 的前驱), 全程取模。最终 sum(f) % mod。O(n) 时间、O(1) 空间。 解法二(矩阵快速幂,对照):转移是常系数线性,写成 10x10 转移矩阵 base, 用快速幂计算 base^(n-1) 与全 1 初始向量相乘,把线性递推加速到 O(10^3 log n)。

代码实现

class Solution {
    private static final int MOD = 1_000_000_007;

    // 解法一:线性递推(推荐提交)
    public int knightDialer(int n) {
        long[] f = new long[10];
        java.util.Arrays.fill(f, 1);
        for (int step = 1; step < n; ++step) {
            long[] g = new long[10];
            g[0] = (f[4] + f[6]) % MOD;
            g[1] = (f[6] + f[8]) % MOD;
            g[2] = (f[7] + f[9]) % MOD;
            g[3] = (f[4] + f[8]) % MOD;
            g[4] = (f[0] + f[3] + f[9]) % MOD;
            g[6] = (f[0] + f[1] + f[7]) % MOD;
            g[7] = (f[2] + f[6]) % MOD;
            g[8] = (f[1] + f[3]) % MOD;
            g[9] = (f[2] + f[4]) % MOD;
            f = g;
        }
        long ans = 0;
        for (long v : f) {
            ans = (ans + v) % MOD;
        }
        return (int) ans;
    }

    // 解法二:矩阵快速幂(对照实现)
    private static final long[][] BASE = {
        {0, 0, 0, 0, 1, 0, 1, 0, 0, 0},
        {0, 0, 0, 0, 0, 0, 1, 0, 1, 0},
        {0, 0, 0, 0, 0, 0, 0, 1, 0, 1},
        {0, 0, 0, 0, 1, 0, 0, 0, 1, 0},
        {1, 0, 0, 1, 0, 0, 0, 0, 0, 1},
        {0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
        {1, 1, 0, 0, 0, 0, 0, 1, 0, 0},
        {0, 0, 1, 0, 0, 0, 1, 0, 0, 0},
        {0, 1, 0, 1, 0, 0, 0, 0, 0, 0},
        {0, 0, 1, 0, 1, 0, 0, 0, 0, 0}
    };

    public int knightDialer2(int n) {
        long[] v = new long[10];
        java.util.Arrays.fill(v, 1);               // 长度 1 的初始向量
        long[][] m = BASE;
        int e = n - 1;
        while (e > 0) {
            if ((e & 1) == 1) {
                v = mulVec(v, m);
            }
            m = mul(m, m);
            e >>= 1;
        }
        long ans = 0;
        for (long x : v) {
            ans = (ans + x) % MOD;
        }
        return (int) ans;
    }

    private long[] mulVec(long[] v, long[][] m) {
        long[] r = new long[10];
        for (int j = 0; j < 10; ++j) {
            long sum = 0;
            for (int k = 0; k < 10; ++k) {
                sum = (sum + v[k] * m[k][j]) % MOD;
            }
            r[j] = sum;
        }
        return r;
    }

    private long[][] mul(long[][] a, long[][] b) {
        long[][] c = new long[10][10];
        for (int i = 0; i < 10; ++i) {
            for (int j = 0; j < 10; ++j) {
                long sum = 0;
                for (int k = 0; k < 10; ++k) {
                    sum = (sum + a[i][k] * b[k][j]) % MOD;
                }
                c[i][j] = sum;
            }
        }
        return c;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[] ns = {1, 2, 3, 10, 50, 3131, 5000};
        int[] exp = {10, 20, 46, 14912, 267287516, 136006598, 406880451};
        for (int t = 0; t < ns.length; ++t) {
            ++total;
            int g1 = s.knightDialer(ns[t]);
            int g2 = s.knightDialer2(ns[t]);
            if (g1 == exp[t] && g2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 935-" + (t + 1) + ": n=" + ns[t] + " got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0935 PASS ==");
        } else {
            System.out.println("== 0935 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析


936. Stamping The Sequence(戳印序列)

题目描述

你想要用小写字母组成一个目标字符串 target。开始的时候,序列由 target.length 个 '?' 记号组成。而你有一个小写字母印章 stamp。 在每个回合,你可以将印章放在序列上,并将序列中的每个字母替换为印章上的相应字母。 你最多可以进行 10 * target.length 个回合。 举个例子,如果初始序列为 "?????",而你的印章 stamp 是 "abc",那么在第一回合,你可以 得到 "abc??"、"?abc?"、"??abc"。(请注意,印章必须完全包含在序列的边界内才能盖下去。) 如果可以印出序列,那么返回一个数组,该数组由每个回合中被印下的最左边字母的索引组成。 如果不能印出序列,就返回一个空数组。

示例 1

  • 输入:stamp = "abc", target = "ababc"
  • 输出:[0,2]

示例 2

  • 输入:stamp = "abca", target = "aabcaca"
  • 输出:[3,0,1] 提示:
  • 1 <= stamp.length <= target.length <= 1000
  • stamp 和 target 只包含小写字母。

解题思路

正向盖章会被后续操作覆盖,难以直接判定;改用逆向思考:从 target 出发,逐步把窗口 "还原"成 ?,直到全部变成 ?,然后把操作顺序反转即为正向着印顺序。 解法一(逆向 + 拓扑排序,主提交):对每个起始位置 i(共 n-m+1 个窗口),初始 indeg[i] = m 表示窗口内尚未匹配的位置数;窗口与 target[i+j] 逐位对齐:若 target[i+j] == stamp[j] 则该位置天然匹配,indeg[i]--;否则该位置会被别的窗口覆盖, 建反向边 g[i+j] 记录覆盖它的窗口。入度为 0 的窗口(已可完全揭掉)入队:揭掉后把 窗口内每个未访问位置标记覆盖,并通知依赖这些位置的窗口 indeg--,新的入度 0 者继续。 若全部位置被覆盖,反转队列顺序即答案;否则返回空数组。O(n*(n-m+1))。 解法二(朴素模拟对照):用字符数组模拟,反复扫描所有窗口,凡是"能盖"(与目标一致或为 ?) 的窗口就盖章并记录下来,直到一轮无进展;若数组已完全等于目标则成功,否则失败。

代码实现

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Deque;
import java.util.List;

class Solution {

    // 解法一:逆向 + 拓扑排序(推荐提交)
    public int[] movesToStamp(String stamp, String target) {
        int m = stamp.length(), n = target.length();
        int[] indeg = new int[n - m + 1];
        Arrays.fill(indeg, m);
        List<Integer>[] g = new List[n];
        for (int i = 0; i < n; ++i) {
            g[i] = new ArrayList<>();
        }
        Deque<Integer> q = new ArrayDeque<>();
        for (int i = 0; i < n - m + 1; ++i) {
            for (int j = 0; j < m; ++j) {
                if (target.charAt(i + j) == stamp.charAt(j)) {
                    if (--indeg[i] == 0) {
                        q.offer(i);
                    }
                } else {
                    g[i + j].add(i);   // 位置 i+j 需要被窗口 i 覆盖
                }
            }
        }
        List<Integer> ans = new ArrayList<>();
        boolean[] vis = new boolean[n];
        while (!q.isEmpty()) {
            int i = q.poll();
            ans.add(i);
            for (int j = 0; j < m; ++j) {
                if (!vis[i + j]) {
                    vis[i + j] = true;
                    for (int k : g[i + j]) {
                        if (--indeg[k] == 0) {
                            q.offer(k);
                        }
                    }
                }
            }
        }
        for (int i = 0; i < n; ++i) {
            if (!vis[i]) {
                return new int[0];
            }
        }
        java.util.Collections.reverse(ans);
        int[] res = new int[ans.size()];
        for (int i = 0; i < res.length; ++i) {
            res[i] = ans.get(i);
        }
        return res;
    }

    // 解法二:逆向揭除贪心模拟(对照实现)
    // 正向:在 ? 序列上盖章;逆向:把目标序列揭回全是 ?。
    // 一个窗口可以揭除当且仅当窗口内每个位置要么已揭(?),要么与印章一致,
    // 且窗口内至少有一个位置此前未被揭过(否则揭除无意义)。反复扫描揭除,
    // 直到全部揭完(成功)或某轮无进展(失败)。
    public int[] movesToStamp2(String stamp, String target) {
        int m = stamp.length(), n = target.length();
        char[] cur = target.toCharArray();
        boolean[] done = new boolean[n];   // 该位置是否已揭成 ?
        List<Integer> steps = new ArrayList<>();
        boolean progressed = true;
        int matched = 0;                   // 已揭 ? 的数量
        while (matched < n) {
            progressed = false;
            for (int i = 0; i <= n - m; ++i) {
                boolean ok = true, hasNew = false;
                for (int j = 0; j < m; ++j) {
                    if (cur[i + j] != '?' && cur[i + j] != stamp.charAt(j)) {
                        ok = false;
                        break;
                    }
                    if (!done[i + j]) {
                        hasNew = true;
                    }
                }
                if (ok && hasNew) {
                    for (int j = 0; j < m; ++j) {
                        if (!done[i + j]) {
                            cur[i + j] = '?';
                            done[i + j] = true;
                            ++matched;
                        }
                    }
                    steps.add(i);
                    progressed = true;
                }
            }
            if (!progressed) {
                return new int[0];
            }
            if (steps.size() > 10 * n) {
                return new int[0];
            }
        }
        // 揭除顺序需反转为正向盖章顺序(先揭的窗口最后盖)
        java.util.Collections.reverse(steps);
        int[] res = new int[steps.size()];
        for (int i = 0; i < res.length; ++i) {
            res[i] = steps.get(i);
        }
        return res;
    }

    // 验证正向操作能否得到 target
    public boolean verify(int[] moves, String stamp, String target) {
        int m = stamp.length(), n = target.length();
        char[] cur = new char[n];
        Arrays.fill(cur, '?');
        for (int i : moves) {
            for (int j = 0; j < m; ++j) {
                cur[i + j] = stamp.charAt(j);
            }
        }
        return new String(cur).equals(target);
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        String[] stamps = {"abc", "abca", "abc", "a", "abc", "ab"};
        String[] targets = {"ababc", "aabcaca", "abcabc", "aaaa", "ababa", "abab"};
        boolean[] exp = {true, true, true, true, false, true};
        for (int t = 0; t < stamps.length; ++t) {
            ++total;
            int[] g1 = s.movesToStamp(stamps[t], targets[t]);
            int[] g2 = s.movesToStamp2(stamps[t], targets[t]);
            boolean ok1 = g1.length > 0 && s.verify(g1, stamps[t], targets[t]);
            boolean ok2 = g2.length > 0 && s.verify(g2, stamps[t], targets[t]);
            if (ok1 == exp[t] && ok2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 936-" + (t + 1) + ": stamp=" + stamps[t] + " target=" + targets[t]
                    + " g1=" + java.util.Arrays.toString(g1) + " g2=" + java.util.Arrays.toString(g2));
            }
        }
        // 负例:无法印出
        ++total;
        int[] bad = s.movesToStamp("abc", "axbxc");
        if (bad.length == 0) {
            ++pass;
        } else {
            System.out.println("FAIL 936-bad: exp empty got " + java.util.Arrays.toString(bad));
        }
        if (pass == total) {
            System.out.println("== 0936 PASS ==");
        } else {
            System.out.println("== 0936 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析


937. Reorder Data in Log Files(重新排列日志文件)

题目描述

给你一个日志数组 logs。每条日志都是以空格分隔的字串,其第一个字为字母与数字混合的标识符。 有两种不同类型的日志:

  • 字母日志:除标识符之外,所有字均由小写字母组成。
  • 数字日志:除标识符之外,所有字均由数字组成。 请按下述规则将日志重新排序:
  • 所有字母日志都排在数字日志之前。
  • 字母日志在内容不同时,忽略标识符后,按内容字典顺序排序;在内容相同时,按标识符排序。
  • 数字日志应该保留原来的相对顺序。 返回日志的最终顺序。

示例 1

  • 输入:logs = ["dig1 8 1 5 1","let1 art can","dig2 3 6","let2 own kit dig","let3 art zero"]
  • 输出:["let1 art can","let3 art zero","let2 own kit dig","dig1 8 1 5 1","dig2 3 6"]

示例 2

  • 输入:logs = ["a1 9 2 3 1","g1 act car","zo4 4 7","ab1 off key dog","a8 act zoo"]
  • 输出:["g1 act car","a8 act zoo","ab1 off key dog","a1 9 2 3 1","zo4 4 7"] 提示:
  • 1 <= logs.length <= 100
  • 3 <= logs[i].length <= 100
  • logs[i] 中,字与字之间都用单个空格分隔
  • 题目数据保证 logs[i] 都有一个标识符,并且在标识符之后至少存在一个字

解题思路

排序规则可归纳为一个稳定的自定义比较键:字母日志排在前面,且按 (内容, 标识符) 字典序; 数字日志排在后面且保持原相对顺序(稳定性由 Arrays.sort 的对象排序天然保证)。 解法一(自定义比较器,主提交):用 split(" ", 2) 把每条日志拆成 [id, content]。 两个都是字母日志时先比 content 再比 id;否则字母日志在前(返回 -1/1),两个数字日志 视为相等(返回 0,配合稳定排序保持原顺序)。O(n log n * L)。 解法二(分组 + 拼接对照):先扫描分拣出字母日志列表与数字日志列表,字母日志用比较器排序, 再顺序拼接。逻辑更直白,两种日志数量各为 a/d 时无需在比较器中判断类型。

代码实现

import java.util.Arrays;
import java.util.Comparator;

class Solution {

    // 解法一:自定义比较器(推荐提交)
    public String[] reorderLogFiles(String[] logs) {
        Comparator<String> cmp = (log1, log2) -> {
            String[] p1 = log1.split(" ", 2);
            String[] p2 = log2.split(" ", 2);
            boolean isLetter1 = Character.isLetter(p1[1].charAt(0));
            boolean isLetter2 = Character.isLetter(p2[1].charAt(0));
            if (isLetter1 && isLetter2) {
                int c = p1[1].compareTo(p2[1]);
                return c != 0 ? c : p1[0].compareTo(p2[0]);
            }
            return isLetter1 ? -1 : (isLetter2 ? 1 : 0);
        };
        Arrays.sort(logs, cmp);
        return logs;
    }

    // 解法二:分拣 + 拼接(对照实现)
    public String[] reorderLogFiles2(String[] logs) {
        java.util.List<String> letters = new java.util.ArrayList<>();
        java.util.List<String> digits = new java.util.ArrayList<>();
        for (String log : logs) {
            int sp = log.indexOf(' ');
            if (Character.isLetter(log.charAt(sp + 1))) {
                letters.add(log);
            } else {
                digits.add(log);
            }
        }
        letters.sort((a, b) -> {
            int sp1 = a.indexOf(' '), sp2 = b.indexOf(' ');
            int c = a.substring(sp1 + 1).compareTo(b.substring(sp2 + 1));
            return c != 0 ? c : a.substring(0, sp1).compareTo(b.substring(0, sp2));
        });
        String[] ans = new String[logs.length];
        int p = 0;
        for (String s : letters) {
            ans[p++] = s;
        }
        for (String s : digits) {
            ans[p++] = s;
        }
        return ans;
    }

    private static void p(String[] a) {
        StringBuilder sb = new StringBuilder("[");
        for (int i = 0; i < a.length; ++i) {
            if (i > 0) {
                sb.append(", ");
            }
            sb.append("\"").append(a[i]).append("\"");
        }
        sb.append("]");
        System.out.println(sb);
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        String[][] inputs = {
            {"dig1 8 1 5 1","let1 art can","dig2 3 6","let2 own kit dig","let3 art zero"},
            {"a1 9 2 3 1","g1 act car","zo4 4 7","ab1 off key dog","a8 act zoo"},
            {"a1 9 2 3 1","g1 act car"},
            {"let2 art zero","let1 art can"}
        };
        String[][] exp = {
            {"let1 art can","let3 art zero","let2 own kit dig","dig1 8 1 5 1","dig2 3 6"},
            {"g1 act car","a8 act zoo","ab1 off key dog","a1 9 2 3 1","zo4 4 7"},
            {"g1 act car","a1 9 2 3 1"},
            {"let1 art can","let2 art zero"}   // 内容 "art can" < "art zero"
        };
        for (int t = 0; t < inputs.length; ++t) {
            ++total;
            String[] g1 = s.reorderLogFiles(inputs[t].clone());
            String[] g2 = s.reorderLogFiles2(inputs[t].clone());
            if (Arrays.equals(g1, exp[t]) && Arrays.equals(g2, exp[t])) {
                ++pass;
            } else {
                System.out.print("FAIL 937-" + (t + 1) + ": got ");
                p(g1);
                System.out.print("      exp ");
                p(exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0937 PASS ==");
        } else {
            System.out.println("== 0937 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析


938. Range Sum of BST(二叉搜索树的范围和)

题目描述

给定二叉搜索树的根结点 root,返回值位于范围 [low, high] 之间的所有结点的值的和。

示例 1

  • 输入:root = [10,5,15,3,7,null,18], low = 7, high = 15
  • 输出:32

示例 2

  • 输入:root = [10,5,15,3,7,13,18,1,null,6], low = 6, high = 10
  • 输出:23 提示:
  • 树中节点数目在范围 [1, 2 * 10^4] 内
  • 1 <= Node.val <= 10^5
  • 1 <= low <= high <= 10^5
  • 所有 Node.val 互不相同

解题思路

BST 满足左子树所有值 < 根值 < 右子树所有值,可利用有序性剪枝,不必遍历整棵树。 解法一(DFS 剪枝,主提交):递归访问根,若 low <= val <= high 计入;若 val > low, 左子树中才可能还有 ≥ low 的值,继续递归左子树;若 val < high,右子树中才可能还有 ≤ high 的值,继续递归右子树。O(n)。 解法二(中序遍历 + 累加对照):对 BST 做中序遍历天然有序,收集位于 [low, high] 的 连续一段累加;为展示迭代写法,用栈模拟中序遍历,遇到进入区间的值即累加。

代码实现

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

class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;
    TreeNode(int val) { this.val = val; }
    TreeNode(int val, TreeNode left, TreeNode right) {
        this.val = val;
        this.left = left;
        this.right = right;
    }
}

class Solution {

    // 解法一:DFS 剪枝(推荐提交)
    public int rangeSumBST(TreeNode root, int low, int high) {
        if (root == null) {
            return 0;
        }
        int x = root.val;
        int ans = (low <= x && x <= high) ? x : 0;
        if (x > low) {
            ans += rangeSumBST(root.left, low, high);
        }
        if (x < high) {
            ans += rangeSumBST(root.right, low, high);
        }
        return ans;
    }

    // 解法二:迭代中序遍历(对照实现)
    public int rangeSumBST2(TreeNode root, int low, int high) {
        int sum = 0;
        Deque<TreeNode> stk = new ArrayDeque<>();
        TreeNode cur = root;
        while (cur != null || !stk.isEmpty()) {
            while (cur != null) {
                stk.push(cur);
                cur = cur.left;
            }
            cur = stk.pop();
            if (cur.val >= low && cur.val <= high) {
                sum += cur.val;
            }
            cur = cur.right;
        }
        return sum;
    }

    // 用层序数组建 BST(null 表示缺子节点),供本地测试用
    public static TreeNode build(Integer[] arr) {
        if (arr == null || arr.length == 0 || arr[0] == null) {
            return null;
        }
        TreeNode root = new TreeNode(arr[0]);
        Deque<TreeNode> q = new ArrayDeque<>();
        q.offer(root);
        int i = 1;
        while (!q.isEmpty() && i < arr.length) {
            TreeNode node = q.poll();
            if (arr[i] != null) {
                node.left = new TreeNode(arr[i]);
                q.offer(node.left);
            }
            ++i;
            if (i < arr.length && arr[i] != null) {
                node.right = new TreeNode(arr[i]);
                q.offer(node.right);
            }
            ++i;
        }
        return root;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        Integer[][] trees = {
            {10, 5, 15, 3, 7, null, 18},
            {10, 5, 15, 3, 7, 13, 18, 1, null, 6},
            {1},
            {5, 3, 7, 1, 4, 6, 9}
        };
        int[] lows = {7, 6, 1, 3};
        int[] highs = {15, 10, 1, 7};
        int[] exp = {32, 23, 1, 25};
        for (int t = 0; t < trees.length; ++t) {
            ++total;
            TreeNode r1 = build(trees[t]);
            TreeNode r2 = build(trees[t]);
            int g1 = s.rangeSumBST(r1, lows[t], highs[t]);
            int g2 = s.rangeSumBST2(r2, lows[t], highs[t]);
            if (g1 == exp[t] && g2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 938-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0938 PASS ==");
        } else {
            System.out.println("== 0938 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析


939. Minimum Area Rectangle(最小面积矩形)

题目描述

给你一个 X-Y 平面上的点数组 points,其中 points[i] = [x_i, y_i]。 返回由这些点形成的矩形的最小面积,矩形的边与 X 轴和 Y 轴平行。如果不存在这样的矩形, 则返回 0。

示例 1

  • 输入:points = [[1,1],[1,3],[3,1],[3,3],[2,2]]
  • 输出:4

示例 2

  • 输入:points = [[1,1],[1,3],[3,1],[3,3],[4,1],[4,3]]
  • 输出:2 提示:
  • 1 <= points.length <= 500
  • points[i].length == 2
  • 0 <= x_i, y_i <= 4 * 10^4
  • 所有给定的点都是唯一的。

解题思路

轴对齐矩形可由 "两对不同纵坐标" 决定:左右两条竖线各取两个点,且纵坐标相同。 n <= 500,枚举四个点不可行,但同竖线的点对枚举可行。 解法一(按 x 分组 + 记录 y 对最近 x,主提交):用 TreeMap 把点按 x 分组并对每组 y 排序,从左到右扫描每个 x:对当前竖线上每个点对 (y1, y2)(y1 < y2),若该点对在 更左的竖线出现过(记在 pos 中),则 (x - pos) * (y2 - y1) 是一个候选面积。 每次扫描后把当前 x 记为该点对"最近一次出现"的横坐标,保证与左侧最近的 x 配对以取小。 坐标编码用 y1 * 40001 + y2。O(n^2 log n)。 解法二(枚举对角点 + 集合查重,对照):面积由对角点唯一决定:对每一对点 (x1,y1), (x2,y2)(x1 != x2 且 y1 != y2),若能找到对角 (x1,y2) 与 (x2,y1),则构成矩形, 用点集 O(1) 判定。O(n^2)。

代码实现

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

class Solution {

    // 解法一:按 x 分组 + y 点对(推荐提交)
    public int minAreaRect(int[][] points) {
        TreeMap<Integer, List<Integer>> byX = new TreeMap<>();
        for (int[] p : points) {
            byX.computeIfAbsent(p[0], k -> new ArrayList<>()).add(p[1]);
        }
        Map<Integer, Integer> lastX = new HashMap<>();
        int ans = Integer.MAX_VALUE;
        for (Map.Entry<Integer, List<Integer>> e : byX.entrySet()) {
            int x = e.getKey();
            List<Integer> ys = e.getValue();
            Collections.sort(ys);
            int m = ys.size();
            for (int i = 0; i < m; ++i) {
                int y1 = ys.get(i);
                for (int j = i + 1; j < m; ++j) {
                    int y2 = ys.get(j);
                    int key = y1 * 40001 + y2;
                    Integer prevX = lastX.get(key);
                    if (prevX != null) {
                        ans = Math.min(ans, (x - prevX) * (y2 - y1));
                    }
                    lastX.put(key, x);
                }
            }
        }
        return ans == Integer.MAX_VALUE ? 0 : ans;
    }

    // 解法二:对角点枚举(对照实现)
    public int minAreaRect2(int[][] points) {
        java.util.Set<Integer> set = new java.util.HashSet<>();
        for (int[] p : points) {
            set.add(p[0] * 40001 + p[1]);
        }
        int n = points.length;
        int ans = Integer.MAX_VALUE;
        for (int i = 0; i < n; ++i) {
            int x1 = points[i][0], y1 = points[i][1];
            for (int j = i + 1; j < n; ++j) {
                int x2 = points[j][0], y2 = points[j][1];
                if (x1 == x2 || y1 == y2) {
                    continue;
                }
                if (set.contains(x1 * 40001 + y2) && set.contains(x2 * 40001 + y1)) {
                    ans = Math.min(ans, Math.abs(x2 - x1) * Math.abs(y2 - y1));
                }
            }
        }
        return ans == Integer.MAX_VALUE ? 0 : ans;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[][][] pts = {
            {{1,1},{1,3},{3,1},{3,3},{2,2}},
            {{1,1},{1,3},{3,1},{3,3},{4,1},{4,3}},
            {{1,1},{2,2},{3,3}},
            {{0,0},{0,1},{1,0},{1,1},{0,2},{2,2},{1,2},{2,1}}
        };
        int[] exp = {4, 2, 0, 1};
        for (int t = 0; t < pts.length; ++t) {
            ++total;
            int g1 = s.minAreaRect(pts[t]);
            int g2 = s.minAreaRect2(pts[t]);
            if (g1 == exp[t] && g2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 939-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0939 PASS ==");
        } else {
            System.out.println("== 0939 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析


940. Distinct Subsequences II(不同的子序列 II)

题目描述

给定一个字符串 s,计算 s 的不同非空子序列的个数。因为结果可能很大,所以返回答案需要 对 10^9 + 7 取余。 字符串的子序列是经由原字符串删除一些(也可能不删除)字符但不改变剩余字符相对位置的一个 新字符串。例如,"ace" 是 "abcde" 的一个子序列,但 "aec" 不是。

示例 1

  • 输入:s = "abc"
  • 输出:7
  • 解释:7 个不同的子序列分别是 "a", "b", "c", "ab", "ac", "bc", 以及 "abc"。

示例 2

  • 输入:s = "aba"
  • 输出:6
  • 解释:6 个不同的子序列分别是 "a", "b", "ab", "ba", "aa" 以及 "aba"。

示例 3

  • 输入:s = "aaa"
  • 输出:3
  • 解释:3 个不同的子序列分别是 "a", "aa" 以及 "aaa"。 提示:
  • 1 <= s.length <= 2000
  • s 仅由小写英文字母组成

解题思路

不同子序列按"末字符"分类去重:以字符 c 结尾的所有不同子序列,恰好是此前所有子序列 各追加一个 c 再加上 c 本身;若 f[c] 存以 c 结尾的不同子序列数,则每读到 c, f[c] = sum(f) + 1,旧值被覆盖即完成去重(相同字符结尾的子序列集合不会重复计数)。 解法一(每次求和的 DP,主提交):维护 f[26],每次 f[c] = (sum(f) + 1) % mod, 答案 = sum(f) % mod。O(26n) 时间、O(26) 空间。 解法二(O(1) 增量 DP 对照):维护全局和 ans,新读到 c 时增量 add = ans + 1 - f[c], 再 ans += add、f[c] += add,单步 O(1),总 O(n)。

代码实现

class Solution {
    private static final int MOD = 1_000_000_007;

    // 解法一:f 数组 + 求和 DP(推荐提交)
    public int distinctSubseqII(String s) {
        int[] f = new int[26];
        for (int i = 0; i < s.length(); ++i) {
            int sum = 1;   // 字符本身作为子序列
            for (int v : f) {
                sum = (sum + v) % MOD;
            }
            f[s.charAt(i) - 'a'] = sum;
        }
        int ans = 0;
        for (int v : f) {
            ans = (ans + v) % MOD;
        }
        return ans;
    }

    // 解法二:O(1) 增量 DP(对照实现)
    public int distinctSubseqII2(String s) {
        int[] f = new int[26];
        int ans = 0;
        for (int i = 0; i < s.length(); ++i) {
            int c = s.charAt(i) - 'a';
            int add = (ans + 1 + MOD - f[c]) % MOD;
            ans = (ans + add) % MOD;
            f[c] = (f[c] + add) % MOD;
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        String[] ss = {"abc", "aba", "aaa", "a", "ab", "abcabc", "abaabb"};
        int[] exp = {7, 6, 3, 1, 3, 51, 25};
        for (int t = 0; t < ss.length; ++t) {
            ++total;
            int g1 = s.distinctSubseqII(ss[t]);
            int g2 = s.distinctSubseqII2(ss[t]);
            if (g1 == exp[t] && g2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 940-" + (t + 1) + ": " + ss[t] + " got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0940 PASS ==");
        } else {
            System.out.println("== 0940 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析


941. Valid Mountain Array(有效的山脉数组)

题目描述

给定一个整数数组 arr,如果它是有效的山脉数组就返回 true,否则返回 false。 如果 arr 满足下述条件,那么它是一个山脉数组:

  • arr.length >= 3
  • 在 0 < i < arr.length - 1 条件下,存在 i 使得:
  • arr[0] < arr[1] < ... < arr[i-1] < arr[i]
  • arr[i] > arr[i+1] > ... > arr[arr.length-1]

示例 1

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

示例 2

  • 输入:arr = [3,5,5]
  • 输出:false

示例 3

  • 输入:arr = [0,3,2,1]
  • 输出:true 提示:
  • 1 <= arr.length <= 10^4
  • 0 <= arr[i] <= 10^4

解题思路

山脉数组须先严格递增再严格递减,长度至少 3,且峰不落在两端(否则只是单调段)。 解法一(双指针向中间夹逼,主提交):i 从 0 出发,只要 arr[i] < arr[i+1] 就右移; j 从 n-1 出发,只要 arr[j-1] > arr[j] 就左移;最后 i == j(两段在山峰相遇) 且峰不在端点(i != 0 && j != n-1,由循环边界天然保证)即为山脉。O(n) 时间、O(1) 空间。 解法二(单侧爬升 + 检查下降,对照):从左爬升找到峰,然后必须严格下降到最后;峰不能是 首尾,且下降段必须走到末尾。逻辑分两步更直白。

代码实现

class Solution {

    // 解法一:双指针夹逼(推荐提交)
    public boolean validMountainArray(int[] arr) {
        int n = arr.length;
        if (n < 3) {
            return false;
        }
        int i = 0, j = n - 1;
        while (i + 1 < n && arr[i] < arr[i + 1]) {
            ++i;
        }
        while (j - 1 >= 0 && arr[j - 1] > arr[j]) {
            --j;
        }
        return i == j && i != 0 && i != n - 1;
    }

    // 解法二:爬升 + 下降两段检查(对照实现)
    public boolean validMountainArray2(int[] arr) {
        int n = arr.length;
        if (n < 3) {
            return false;
        }
        int i = 0;
        while (i + 1 < n && arr[i] < arr[i + 1]) {
            ++i;
        }
        if (i == 0 || i == n - 1) {
            return false;   // 峰在两端,无严格下降段或无严格上升段
        }
        while (i + 1 < n && arr[i] > arr[i + 1]) {
            ++i;
        }
        return i == n - 1;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[][] arrs = {
            {2,1}, {3,5,5}, {0,3,2,1}, {0,1,2,3}, {3,2,1,0},
            {1,2,3,4,5}, {5,4,3,2,1}, {1,2,2,1}, {0,1,2,3,2,1}, {1}
        };
        boolean[] exp = {false, false, true, false, false, false, false, false, true, false};
        for (int t = 0; t < arrs.length; ++t) {
            ++total;
            boolean g1 = s.validMountainArray(arrs[t]);
            boolean g2 = s.validMountainArray2(arrs[t]);
            if (g1 == exp[t] && g2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 941-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0941 PASS ==");
        } else {
            System.out.println("== 0941 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析


942. DI String Match(增减字符串匹配)

题目描述

由范围 [0, n] 内所有整数组成的 n + 1 个整数的排列序列可以表示为长度为 n 的字符串 s, 其中:

  • 如果 perm[i] < perm[i + 1],那么 s[i] == 'I';
  • 如果 perm[i] > perm[i + 1],那么 s[i] == 'D'。 给定一个字符串 s,重构排列 perm 并返回它。如果有多个有效排列 perm,则返回其中任何一个。

示例 1

  • 输入:s = "IDID"
  • 输出:[0,4,1,3,2]

示例 2

  • 输入:s = "III"
  • 输出:[0,1,2,3]

示例 3

  • 输入:s = "DDI"
  • 输出:[3,2,0,1] 提示:
  • 1 <= s.length <= 10^5
  • s 只包含字符 "I" 或 "D"

解题思路

用 0..n 各一次构造排列,使相邻元素满足 'I' 升、'D' 降。n 可达 10^5,不可回溯。 贪心思路:遇到 'I' 时,放当前可用数字中的最小值(这样下一位无论多大都能满足严格上升); 遇到 'D' 时,放当前可用数字中的最大值(下一位无论多小都能满足严格下降)。 解法一(双端贪心,主提交):low、high 分别指向剩余最小/最大值,遍历 s, 'I' 取 low++ 放入结果,'D' 取 high-- 放入结果;最后补 low(此时 low==high)。 O(n) 时间、O(n) 空间。 解法二(预填区间对照):思路等价,先把 'I' 位置按升序依次填小值、'D' 位置按降序依次 填大值,再补剩余数,展示另一种实现方式。

代码实现

class Solution {

    // 解法一:双端贪心(推荐提交)
    public int[] diStringMatch(String s) {
        int n = s.length();
        int low = 0, high = n;
        int[] ans = new int[n + 1];
        for (int i = 0; i < n; ++i) {
            if (s.charAt(i) == 'I') {
                ans[i] = low++;
            } else {
                ans[i] = high--;
            }
        }
        ans[n] = low;   // 剩下唯一未用的数
        return ans;
    }

    // 解法二:两遍预填(对照实现)
    public int[] diStringMatch2(String s) {
        int n = s.length();
        int[] ans = new int[n + 1];
        java.util.Arrays.fill(ans, -1);
        int low = 0, high = n;
        // 第一遍:从左往右填 'I',用当前剩余最小值(升序部分渐进)
        for (int i = 0; i < n; ++i) {
            if (s.charAt(i) == 'I') {
                ans[i] = low++;
            }
        }
        // 第二遍:从左往右填 'D',用当前剩余最大值(连续的 D 自然递减)
        for (int i = 0; i < n; ++i) {
            if (s.charAt(i) == 'D') {
                ans[i] = high--;
            }
        }
        for (int i = 0; i <= n; ++i) {
            if (ans[i] == -1) {
                ans[i] = low;   // 剩余唯一值
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        String[] ss = {"IDID", "III", "DDI", "I", "D", "ID", "DI"};
        for (int t = 0; t < ss.length; ++t) {
            ++total;
            int[] g1 = s.diStringMatch(ss[t]);
            int[] g2 = s.diStringMatch2(ss[t]);
            if (check(ss[t], g1) && check(ss[t], g2)) {
                ++pass;
            } else {
                System.out.println("FAIL 942-" + (t + 1) + ": " + ss[t]
                    + " g1=" + java.util.Arrays.toString(g1) + " g2=" + java.util.Arrays.toString(g2));
            }
        }
        if (pass == total) {
            System.out.println("== 0942 PASS ==");
        } else {
            System.out.println("== 0942 FAIL ==");
            System.exit(1);
        }
    }

    // 校验:g 是 0..len(s) 的排列且满足 s 规定的升降
    private static boolean check(String s, int[] g) {
        int n = s.length();
        if (g.length != n + 1) {
            return false;
        }
        boolean[] seen = new boolean[n + 1];
        for (int x : g) {
            if (x < 0 || x > n || seen[x]) {
                return false;
            }
            seen[x] = true;
        }
        for (int i = 0; i < n; ++i) {
            if (s.charAt(i) == 'I' && !(g[i] < g[i + 1])) {
                return false;
            }
            if (s.charAt(i) == 'D' && !(g[i] > g[i + 1])) {
                return false;
            }
        }
        return true;
    }
}

复杂度分析


943. Find the Shortest Superstring(最短超级串)

题目描述

给定一个字符串数组 words,找到以 words 中每个字符串作为子字符串的最短字符串。 如果有多个有效最短字符串满足题目条件,返回其中任意一个即可。 我们可以假设 words 中没有字符串是 words 中另一个字符串的子字符串。

示例 1

  • 输入:words = ["alex","loves","leetcode"]
  • 输出:"alexlovesleetcode"

示例 2

  • 输入:words = ["catg","ctaagt","gcta","ttca","atgcatc"]
  • 输出:"gctaagttcatgcatc" 提示:
  • 1 <= words.length <= 12
  • 1 <= words[i].length <= 20
  • words[i] 由小写英文字母组成
  • words 中的所有字符串互不相同

解题思路

最短超级串就是把所有单词排成一条链,相邻单词尽量重叠,等价于"最大化总重叠长度"的 哈密顿路径问题。n <= 12,可用状态压缩 DP。 解法一(状态压缩 DP,主提交):先预处理 g[i][j] = 把 words[j] 接在 words[i] 之后 能获得的最大重叠长度(枚举重叠长度 k 从 min(leni,lenj) 向下试,匹配后缀/前缀)。 定义 dp[mask][j] = 已用单词集合为 mask、链尾为 words[j] 时的最大重叠; 转移枚举上一个链尾 k(k 在 mask 中不含 j),dp[mask][j] = max(dp[mask^1<<j][k] + g[k][j]) 并记录前驱 pre[mask][j] = k。取 dp[full][j] 最大者倒推链条,反转后拼接 (首单词整串,后续单词只接非重叠部分;未进链的单词按序补尾)。O(2^n * n^2)。 解法二(DFS 剪枝对照):实现等价的回溯搜索,按当前链尾寻找下一单词使重叠最大, 记录全局最优,用于小规模验证。

代码实现

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

class Solution {

    // 解法一:状态压缩 DP(推荐提交)
    public String shortestSuperstring(String[] words) {
        int n = words.length;
        int[][] g = new int[n][n];
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < n; ++j) {
                if (i == j) {
                    continue;
                }
                int lim = Math.min(words[i].length(), words[j].length());
                for (int k = lim; k > 0; --k) {
                    if (words[i].endsWith(words[j].substring(0, k))) {
                        g[i][j] = k;
                        break;
                    }
                }
            }
        }
        int full = (1 << n) - 1;
        int[][] dp = new int[1 << n][n];
        int[][] pre = new int[1 << n][n];
        for (int[] row : pre) {
            Arrays.fill(row, -1);
        }
        for (int mask = 1; mask <= full; ++mask) {
            for (int j = 0; j < n; ++j) {
                if ((mask & (1 << j)) == 0) {
                    continue;
                }
                int prev = mask ^ (1 << j);
                for (int k = 0; k < n; ++k) {
                    if ((prev & (1 << k)) == 0) {
                        continue;
                    }
                    int v = dp[prev][k] + g[k][j];
                    if (v > dp[mask][j]) {
                        dp[mask][j] = v;
                        pre[mask][j] = k;
                    }
                }
            }
        }
        int last = 0;
        for (int j = 1; j < n; ++j) {
            if (dp[full][j] > dp[full][last]) {
                last = j;
            }
        }
        List<Integer> order = new ArrayList<>();
        order.add(last);
        int mask = full;
        while (pre[mask][last] != -1) {
            int k = pre[mask][last];
            mask ^= (1 << last);
            last = k;
            order.add(last);
        }
        java.util.Collections.reverse(order);
        Set<Integer> seen = new HashSet<>(order);
        for (int i = 0; i < n; ++i) {
            if (!seen.contains(i)) {
                order.add(i);
            }
        }
        StringBuilder sb = new StringBuilder(words[order.get(0)]);
        for (int i = 1; i < order.size(); ++i) {
            int a = order.get(i - 1), b = order.get(i);
            sb.append(words[b].substring(g[a][b]));
        }
        return sb.toString();
    }

    // 解法二:DFS 回溯(对照实现,小规模验证)
    private int bestOverlap = -1;
    private List<Integer> bestOrder;

    public String shortestSuperstring2(String[] words) {
        int n = words.length;
        int[][] g = new int[n][n];
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < n; ++j) {
                if (i == j) {
                    continue;
                }
                int lim = Math.min(words[i].length(), words[j].length());
                for (int k = lim; k > 0; --k) {
                    if (words[i].endsWith(words[j].substring(0, k))) {
                        g[i][j] = k;
                        break;
                    }
                }
            }
        }
        bestOverlap = -1;
        bestOrder = null;
        dfs(words, g, 0, 0, -1, new ArrayList<>(), new boolean[n]);
        StringBuilder sb = new StringBuilder(words[bestOrder.get(0)]);
        for (int i = 1; i < n; ++i) {
            sb.append(words[bestOrder.get(i)].substring(g[bestOrder.get(i - 1)][bestOrder.get(i)]));
        }
        return sb.toString();
    }

    private void dfs(String[] words, int[][] g, int cnt, int overlap, int lastIdx,
                     List<Integer> cur, boolean[] used) {
        int n = words.length;
        if (cnt == n) {
            if (overlap > bestOverlap) {
                bestOverlap = overlap;
                bestOrder = new ArrayList<>(cur);
            }
            return;
        }
        for (int i = 0; i < n; ++i) {
            if (used[i]) {
                continue;
            }
            used[i] = true;
            cur.add(i);
            int add = (lastIdx == -1) ? 0 : g[lastIdx][i];
            dfs(words, g, cnt + 1, overlap + add, i, cur, used);
            cur.remove(cur.size() - 1);
            used[i] = false;
        }
    }

    // 校验结果字符串确实包含每个 word
    public boolean verify(String sup, String[] words) {
        for (String w : words) {
            if (!sup.contains(w)) {
                return false;
            }
        }
        return true;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        String[][] inputs = {
            {"alex","loves","leetcode"},
            {"catg","ctaagt","gcta","ttca","atgcatc"},
            {"a","b","c"},
            {"ab","bc","cd","de"},
            {"gcta","atgcatc","ctaagt","ttca","catg"}
        };
        for (int t = 0; t < inputs.length; ++t) {
            ++total;
            String g1 = s.shortestSuperstring(inputs[t]);
            String g2 = s.shortestSuperstring2(inputs[t]);
            boolean ok1 = s.verify(g1, inputs[t]);
            boolean ok2 = s.verify(g2, inputs[t]);
            // 两种解法应得到相同总长度(最长重叠)
            boolean sameLen = g1.length() == g2.length();
            if (ok1 && ok2 && sameLen) {
                ++pass;
            } else {
                System.out.println("FAIL 943-" + (t + 1) + ": len " + g1.length() + " vs " + g2.length());
            }
        }
        if (pass == total) {
            System.out.println("== 0943 PASS ==");
        } else {
            System.out.println("== 0943 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析


944. Delete Columns to Make Sorted(删列造序)

题目描述

给你由 n 个小写字母字符串组成的数组 strs,其中每个字符串长度相等。 这些字符串可以每个一行,排成一个网格。例如,strs = ["abc", "bce", "cae"] 可以排列为: abc bce cae 你需要找出并删除不是按字典序非严格递增排列的列。在上面的例子(下标从 0 开始)中,列 0 ('a', 'b', 'c')和列 2('c', 'e', 'e')都是按字典序非严格递增排列的,而列 1 ('b', 'c', 'a')不是,所以要删除列 1。返回你需要删除的列数。

示例 1

  • 输入:strs = ["cba","daf","ghi"]
  • 输出:1

示例 2

  • 输入:strs = ["a","b"]
  • 输出:0

示例 3

  • 输入:strs = ["zyx","wvu","tsr"]
  • 输出:3 提示:
  • n == strs.length
  • 1 <= n <= 100
  • 1 <= strs[i].length <= 1000
  • strs[i] 由小写英文字母组成

解题思路

每一列是否非严格递增是相互独立的判断:只需逐列从上到下检查是否存在相邻行逆序。 解法一(逐列检查,主提交):列数 m = strs[0].length(),行数 n = strs.length。 对每一列 j,扫描 i = 1..n-1,一旦 strs[i].charAt(j) < strs[i-1].charAt(j) 即 逆序,该列必须删除,计数加一并跳到下一列。O(m * n) 时间、O(1) 空间。 解法二(转置 + 逐行检查对照):思路一致,但把网格转置后对每行做有序性检查, 便于理解"列变成行"的等价性。

代码实现

class Solution {

    // 解法一:逐列检查(推荐提交)
    public int minDeletionSize(String[] strs) {
        int m = strs[0].length();
        int n = strs.length;
        int ans = 0;
        for (int j = 0; j < m; ++j) {
            for (int i = 1; i < n; ++i) {
                if (strs[i].charAt(j) < strs[i - 1].charAt(j)) {
                    ++ans;
                    break;
                }
            }
        }
        return ans;
    }

    // 解法二:转置后检查(对照实现)
    public int minDeletionSize2(String[] strs) {
        int m = strs[0].length();
        int n = strs.length;
        char[][] grid = new char[n][m];
        for (int i = 0; i < n; ++i) {
            grid[i] = strs[i].toCharArray();
        }
        int ans = 0;
        for (int j = 0; j < m; ++j) {
            for (int i = 1; i < n; ++i) {
                if (grid[i][j] < grid[i - 1][j]) {
                    ++ans;
                    break;
                }
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        String[][] strs = {
            {"cba","daf","ghi"},
            {"a","b"},
            {"zyx","wvu","tsr"},
            {"abc","bce","cae"},
            {"a"}
        };
        int[] exp = {1, 0, 3, 1, 0};
        for (int t = 0; t < strs.length; ++t) {
            ++total;
            int g1 = s.minDeletionSize(strs[t]);
            int g2 = s.minDeletionSize2(strs[t]);
            if (g1 == exp[t] && g2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 944-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0944 PASS ==");
        } else {
            System.out.println("== 0944 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析


945. Minimum Increment to Make Array Unique(使数组唯一的最小增量)

题目描述

给你一个整数数组 nums。每次 move 操作将会选择任意一个满足 0 <= i < nums.length 的下标 i,并将 nums[i] 递增 1。 返回使 nums 中的每个值都变成唯一的所需要的最少操作次数。 生成的测试用例保证答案在 32 位整数范围内。

示例 1

  • 输入:nums = [1,2,2]
  • 输出:1
  • 解释:经过一次 move 操作,数组将变为 [1, 2, 3]。

示例 2

  • 输入:nums = [3,2,1,2,1,7]
  • 输出:6
  • 解释:经过 6 次 move 操作,数组将变为 [3, 4, 1, 2, 5, 7]。 提示:
  • 1 <= nums.length <= 10^5
  • 0 <= nums[i] <= 10^5

解题思路

每次只能把数加一,目标是最终互不相同,最小化总增量。 解法一(排序 + 贪心,主提交):排序后,用 y 记录"当前已占用的下一个最小可用值", 对每个 x,把它抬升到 max(y+1, x)(y+1 确保不与前一个数冲突,x 保留原值下界), 代价 y - x 累加。O(n log n) 时间、O(log n) 空间(排序栈)。 解法二(计数 + 线性扫描,对照):结果数组最大值不超过 max(nums) + len(nums), 用计数数组记录每个值出现次数,从 0 起:cnt[i] > 1 时把多出的 cnt[i]-1 个元素 全部推向 i+1(每个一步),累加成本 cnt[i]-1。O(n + max) 时间、O(n + max) 空间。

代码实现

import java.util.Arrays;

class Solution {

    // 解法一:排序 + 贪心(推荐提交)
    public int minIncrementForUnique(int[] nums) {
        Arrays.sort(nums);
        int ans = 0;
        int y = -1;
        for (int x : nums) {
            y = Math.max(y + 1, x);
            ans += y - x;
        }
        return ans;
    }

    // 解法二:计数 + 线性扫描(对照实现)
    public int minIncrementForUnique2(int[] nums) {
        int max = 0;
        for (int x : nums) {
            max = Math.max(max, x);
        }
        int size = max + nums.length;
        int[] cnt = new int[size];
        for (int x : nums) {
            ++cnt[x];
        }
        int ans = 0;
        for (int i = 0; i < size - 1; ++i) {
            if (cnt[i] > 1) {
                int diff = cnt[i] - 1;
                cnt[i + 1] += diff;
                ans += diff;
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[][] nums = {
            {1,2,2},
            {3,2,1,2,1,7},
            {1},
            {1,1,1},
            {0,0,0,0},
            {2,2,2,2,2}
        };
        int[] exp = {1, 6, 0, 3, 6, 10};
        for (int t = 0; t < nums.length; ++t) {
            ++total;
            int g1 = s.minIncrementForUnique(nums[t].clone());
            int g2 = s.minIncrementForUnique2(nums[t].clone());
            if (g1 == exp[t] && g2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 945-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0945 PASS ==");
        } else {
            System.out.println("== 0945 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析


946. Validate Stack Sequences(验证栈序列)

题目描述

给定 pushed 和 popped 两个序列,每个序列中的值都不重复,只有当它们可能是在最初空栈上 进行的推入 push 和弹出 pop 操作序列的结果时,返回 true;否则,返回 false。

示例 1

  • 输入:pushed = [1,2,3,4,5], popped = [4,5,3,2,1]
  • 输出:true

示例 2

  • 输入:pushed = [1,2,3,4,5], popped = [4,3,5,1,2]
  • 输出:false
  • 解释:1 不能在 2 之前弹出。 提示:
  • 1 <= pushed.length <= 1000
  • 0 <= pushed[i] <= 1000
  • pushed 的所有元素互不相同
  • popped.length == pushed.length
  • popped 是 pushed 的一个排列

解题思路

判断一个出栈序列能否由依次按 pushed 入栈(必要时随时弹出)得到。 解法一(栈模拟,主提交):用栈模拟,按 pushed 依次入栈,每次入栈后检查栈顶是否等于 popped 中下一个待出元素,是则弹出并推进 popped 下标;处理完所有 pushed 后, 若 popped 下标已到末尾说明全部弹出成功,返回 true。O(n) 时间、O(n) 空间。 解法二(数组模拟栈对照):用 pushed 数组本身作栈底复用,双指针模拟,节省 Stack 开销, O(n) 时间、O(1) 额外空间。

代码实现

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

class Solution {

    // 解法一:栈模拟(推荐提交)
    public boolean validateStackSequences(int[] pushed, int[] popped) {
        Deque<Integer> stk = new ArrayDeque<>();
        int i = 0;
        for (int x : pushed) {
            stk.push(x);
            while (!stk.isEmpty() && stk.peek() == popped[i]) {
                stk.pop();
                ++i;
            }
        }
        return i == popped.length;
    }

    // 解法二:数组模拟栈(对照实现)
    public boolean validateStackSequences2(int[] pushed, int[] popped) {
        int n = pushed.length;
        int[] stk = new int[n];
        int top = -1;
        int i = 0;
        for (int x : pushed) {
            stk[++top] = x;
            while (top >= 0 && stk[top] == popped[i]) {
                --top;
                ++i;
            }
        }
        return i == n;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[][] pusheds = {
            {1,2,3,4,5},
            {1,2,3,4,5},
            {1},
            {2,1,0},
            {0,1,2,3}
        };
        int[][] poppeds = {
            {4,5,3,2,1},
            {4,3,5,1,2},
            {1},
            {2,1,0},
            {3,2,1,0}
        };
        boolean[] exp = {true, false, true, true, true};
        for (int t = 0; t < pusheds.length; ++t) {
            ++total;
            boolean g1 = s.validateStackSequences(pusheds[t], poppeds[t]);
            boolean g2 = s.validateStackSequences2(pusheds[t], poppeds[t]);
            if (g1 == exp[t] && g2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 946-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0946 PASS ==");
        } else {
            System.out.println("== 0946 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析


947. Most Stones Removed with Same Row or Column(移除最多的同行或同列石头)

题目描述

n 块石头放置在二维平面中的一些整数坐标点上。每个坐标点上最多只能有一块石头。 如果一块石头的同行或者同列上有其他石头存在,那么就可以移除这块石头。 给你一个长度为 n 的数组 stones,其中 stones[i] = [x_i, y_i] 表示第 i 块石头的 位置,返回可以移除的石子的最大数量。

示例 1

  • 输入:stones = [[0,0],[0,1],[1,0],[1,2],[2,1],[2,2]]
  • 输出:5

示例 2

  • 输入:stones = [[0,0],[0,2],[1,1],[2,0],[2,2]]
  • 输出:3

示例 3

  • 输入:stones = [[0,0]]
  • 输出:0 提示:
  • 1 <= stones.length <= 1000
  • 0 <= x_i, y_i <= 10^4
  • 不会有两块石头放在同一个坐标点上

解题思路

同行或同列的石头构成一个连通关系:从一块石头出发,可以经由"同行的石头→同列的石头" 两两传递,因此属于同一连通分量。每个连通分量无论多大,最后只能保留一块石头; 故最大可移除数 = 石头总数 - 连通分量数。 解法一(并查集 + 坐标偏移,主提交):把每个横坐标视为节点,每个纵坐标加上偏移量 10001 视为另一组节点;对每块石头 union(x, y + OFFSET),同行/同列自动连通。 最后统计每个石头的横坐标根节点去重个数(即分量数),返回 n - 分量子。 时间 O(n log m),空间 O(m)。 解法二(两两合并的并查集对照):对石头两两检查是否同行或同列,成功合并一次计数加一, 该计数即为可移除数;O(n^2 α(n)),实现直观但平方复杂度。

代码实现

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

class Solution {

    // 解法一:并查集 + 坐标偏移(推荐提交)
    public int removeStones(int[][] stones) {
        int offset = 10001;
        int max = offset * 2;
        UnionFind uf = new UnionFind(max);
        for (int[] st : stones) {
            uf.union(st[0], st[1] + offset);
        }
        Set<Integer> roots = new HashSet<>();
        for (int[] st : stones) {
            roots.add(uf.find(st[0]));
        }
        return stones.length - roots.size();
    }

    // 解法二:石头两两合并(对照实现)
    public int removeStones2(int[][] stones) {
        int n = stones.length;
        UnionFind uf = new UnionFind(n);
        int merged = 0;
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < i; ++j) {
                if (stones[i][0] == stones[j][0] || stones[i][1] == stones[j][1]) {
                    if (uf.union(i, j)) {
                        ++merged;
                    }
                }
            }
        }
        return merged;
    }

    static class UnionFind {
        int[] p;
        int[] sz;
        UnionFind(int n) {
            p = new int[n];
            sz = new int[n];
            for (int i = 0; i < n; ++i) {
                p[i] = i;
                sz[i] = 1;
            }
        }
        int find(int x) {
            if (p[x] != x) {
                p[x] = find(p[x]);
            }
            return p[x];
        }
        boolean union(int a, int b) {
            int ra = find(a), rb = find(b);
            if (ra == rb) {
                return false;
            }
            if (sz[ra] < sz[rb]) {
                int t = ra;
                ra = rb;
                rb = t;
            }
            p[rb] = ra;
            sz[ra] += sz[rb];
            return true;
        }
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[][][] stones = {
            {{0,0},{0,1},{1,0},{1,2},{2,1},{2,2}},
            {{0,0},{0,2},{1,1},{2,0},{2,2}},
            {{0,0}},
            {{0,0},{0,1},{1,0},{1,1}},
            {{0,0},{0,1},{0,2},{0,3}}
        };
        int[] exp = {5, 3, 0, 3, 3};
        for (int t = 0; t < stones.length; ++t) {
            ++total;
            int g1 = s.removeStones(stones[t]);
            int g2 = s.removeStones2(stones[t]);
            if (g1 == exp[t] && g2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 947-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0947 PASS ==");
        } else {
            System.out.println("== 0947 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

时间 O(n log m),空间 O(m)。


948. Bag of Tokens(令牌放置)

题目描述

你的初始能量为 power,初始分数为 0,只有一包令牌以整数数组 tokens 给出。 你的目标是通过有策略地使用这些令牌以最大化总分数。在一次行动中,你可以用两种方式中的一种 来使用一个未被使用的令牌(但不是对同一个令牌使用两种方式):

  • 朝上:如果你当前至少有 tokens[i] 点能量,可以使用令牌 i,失去 tokens[i] 点能量, 并得到 1 分。
  • 朝下:如果你当前至少有 1 分,可以使用令牌 i,获得 tokens[i] 点能量,并失去 1 分。 在使用任意数量的令牌后,返回我们可以得到的最大分数。

示例 1

  • 输入:tokens = [100], power = 50
  • 输出:0

示例 2

  • 输入:tokens = [200,100], power = 150
  • 输出:1

示例 3

  • 输入:tokens = [100,200,300,400], power = 200
  • 输出:2 提示:
  • 0 <= tokens.length <= 1000
  • 0 <= tokens[i], power < 10^4

解题思路

为了最大化分数,应该"用尽可能少的能量换分",即总是正面使用剩余能量能买得起的最小令牌; 当能量不足以买任何令牌时,若手里有分数,则反面卖出最大令牌以换取最多能量。 解法一(排序 + 双指针贪心,主提交):排序后用 i 指向最便宜的未用令牌、j 指向最贵的。 能量够就 i 正面买下(分数 +1,记录峰值);不够但有分就卖 j(分数 -1,能量 += tokens[j]); 两者都不可则结束。O(n log n)。 解法二(计数贪心对照):值域 [0, 10^4) 较小,用频数数组按值从小到大"买分"、从大到小 "回购能量",体现同一种贪心策略的另一种实现。

代码实现

import java.util.Arrays;

class Solution {

    // 解法一:排序 + 双指针(推荐提交)
    public int bagOfTokensScore(int[] tokens, int power) {
        Arrays.sort(tokens);
        int n = tokens.length;
        int ans = 0, score = 0;
        int i = 0, j = n - 1;
        while (i <= j) {
            if (power >= tokens[i]) {
                power -= tokens[i++];
                ans = Math.max(ans, ++score);
            } else if (score > 0) {
                power += tokens[j--];
                --score;
            } else {
                break;
            }
        }
        return ans;
    }

    // 解法二:频数计数贪心(对照实现)
    public int bagOfTokensScore2(int[] tokens, int power) {
        if (tokens.length == 0) {
            return 0;
        }
        int maxV = 0;
        for (int v : tokens) {
            maxV = Math.max(maxV, v);
        }
        int[] cnt = new int[maxV + 1];
        for (int v : tokens) {
            ++cnt[v];
        }
        int ans = 0, score = 0;
        int lo = 0, hi = maxV;
        int used = 0, n = tokens.length;
        while (used < n && lo <= hi) {
            // 跳过已用完的低值区
            while (lo <= hi && cnt[lo] == 0) {
                ++lo;
            }
            if (lo > hi) {
                break;
            }
            if (power >= lo) {
                --cnt[lo];
                power -= lo;
                ++score;
                ++used;
                ans = Math.max(ans, score);
            } else if (score > 0) {
                // 能量不够,卖出最贵令牌换能量
                while (hi >= lo && cnt[hi] == 0) {
                    --hi;
                }
                --cnt[hi];
                power += hi;
                --score;
                ++used;
            } else {
                break;
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[][] tokens = {
            {100},
            {200, 100},
            {100, 200, 300, 400},
            {},
            {300, 200, 100},
            {10, 20, 30, 40, 50}
        };
        int[] powers = {50, 150, 200, 100, 150, 45};
        int[] exp = {0, 1, 2, 0, 1, 2};
        for (int t = 0; t < tokens.length; ++t) {
            ++total;
            int g1 = s.bagOfTokensScore(tokens[t].clone(), powers[t]);
            int g2 = s.bagOfTokensScore2(tokens[t].clone(), powers[t]);
            if (g1 == exp[t] && g2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 948-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0948 PASS ==");
        } else {
            System.out.println("== 0948 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析


949. Largest Time for Given Digits(给定数字能组成的最大时间)

题目描述

给定一个由 4 位数字组成的数组,返回可以设置的符合 24 小时制的最大时间。 24 小时格式为 "HH:MM",其中 HH 在 00 到 23 之间,MM 在 00 到 59 之间。 最小的 24 小时制时间是 00:00,而最大的是 23:59。从 00:00(午夜)开始算起,过得越久, 时间越大。以长度为 5 的字符串,按 "HH:MM" 格式返回答案。如果不能确定有效时间, 则返回空字符串。

示例 1

  • 输入:arr = [1,2,3,4]
  • 输出:"23:41"

示例 2

  • 输入:arr = [5,5,5,5]
  • 输出:""

示例 3

  • 输入:arr = [0,0,0,0]
  • 输出:"00:00"

示例 4

  • 输入:arr = [0,0,1,0]
  • 输出:"10:00" 提示:
  • arr.length == 4
  • 0 <= arr[i] <= 9

解题思路

4 个数字组成的合法时刻数量很少(4 个位置全排列 24 种,或从大到小枚举 2460 个时刻), 只需找其中最大者。 解法一(从大到小枚举时分 + 计数匹配,主提交):小时 h 从 23 到 0、分钟 m 从 59 到 0 枚举,对每个候选 (h, m) 统计它所需数字的频数是否恰等于 arr 的频数;第一个命中即为 最大合法时间。O(2460*4)。 解法二(全排列枚举对比):枚举 4 个下标的排列,构造 HH 与 MM,合法者取 h*60+m 最大;用 (1<<4-1) 位级去重避免重复下标组合。O(4^3)。

代码实现

class Solution {

    // 解法一:枚举时分 + 计数(推荐提交)
    public String largestTimeFromDigits(int[] arr) {
        int[] cnt = new int[10];
        for (int v : arr) {
            ++cnt[v];
        }
        for (int h = 23; h >= 0; --h) {
            for (int m = 59; m >= 0; --m) {
                int[] need = new int[10];
                ++need[h / 10];
                ++need[h % 10];
                ++need[m / 10];
                ++need[m % 10];
                boolean ok = true;
                for (int d = 0; d < 10; ++d) {
                    if (need[d] != cnt[d]) {
                        ok = false;
                        break;
                    }
                }
                if (ok) {
                    return String.format("%02d:%02d", h, m);
                }
            }
        }
        return "";
    }

    // 解法二:全排列枚举(对照实现)
    public String largestTimeFromDigits2(int[] arr) {
        int ans = -1;
        for (int i = 0; i < 4; ++i) {
            for (int j = 0; j < 4; ++j) {
                for (int k = 0; k < 4; ++k) {
                    if (i == j || i == k || j == k) {
                        continue;
                    }
                    int l = 6 - i - j - k;
                    int h = arr[i] * 10 + arr[j];
                    int m = arr[k] * 10 + arr[l];
                    if (h < 24 && m < 60) {
                        ans = Math.max(ans, h * 60 + m);
                    }
                }
            }
        }
        return ans < 0 ? "" : String.format("%02d:%02d", ans / 60, ans % 60);
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[][] arrs = {
            {1, 2, 3, 4},
            {5, 5, 5, 5},
            {0, 0, 0, 0},
            {0, 0, 1, 0},
            {2, 0, 6, 6},
            {9, 8, 7, 6},
            {0, 4, 0, 0}
        };
        String[] exp = {"23:41", "", "00:00", "10:00", "06:26", "", "04:00"};
        for (int t = 0; t < arrs.length; ++t) {
            ++total;
            String g1 = s.largestTimeFromDigits(arrs[t]);
            String g2 = s.largestTimeFromDigits2(arrs[t]);
            if (g1.equals(exp[t]) && g2.equals(exp[t])) {
                ++pass;
            } else {
                System.out.println("FAIL 949-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0949 PASS ==");
        } else {
            System.out.println("== 0949 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析


950. Reveal Cards In Increasing Order(按递增顺序显示卡牌)

题目描述

牌组中的每张卡牌都对应有一个唯一的整数。你可以按你想要的顺序对这套卡片进行排序。 最初,这些卡牌在牌组里是正面朝下的(即,未显示状态)。 现在,重复执行以下步骤,直到显示所有卡牌为止:

  1. 从牌组顶部抽一张牌,显示它,然后将其从牌组中移出。
  2. 如果牌组中仍有牌,则将下一张处于牌组顶部的牌放在牌组的底部。
  3. 如果仍有未显示的牌,那么返回步骤 1。否则,停止行动。 返回能以递增顺序显示卡牌的牌组顺序。答案中的第一张牌被认为处于牌堆顶部。

示例

  • 输入:deck = [17,13,11,2,3,5,7]
  • 输出:[2,13,3,11,5,17,7] 提示:
  • 1 <= deck.length <= 1000
  • 1 <= deck[i] <= 10^6
  • 对于所有的 i != j,deck[i] != deck[j]
  • 所有卡牌都互不相同,保证答案唯一

解题思路

揭示过程是"取出队首展示、再把新队首挪到队尾",目标是从小到大展示。 正向模拟需要提前知道初始排列,反过来做反而简单:对 deck 从小到大排序后,从大到小 依次把当前元素压回队列,同时逆推"挪到底部"这一步。 解法一(队列逆向构造,主提交):排序 deck 后从大到小遍历:若队列非空,先把队尾元素 移到队首(这是正向"放到底部"的逆操作),再把当前元素插到队首;最后队列从前到后即为 初始牌组顺序。O(n log n)。 解法二(数组下标模拟对照):不用队列,改用布尔数组 + 线性扫描,按"跳过已填位置"的方式 模拟揭示过程来填值,与正向步骤一一对应,O(n^2)。

代码实现

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

class Solution {

    // 解法一:队列逆向构造(推荐提交)
    public int[] deckRevealedIncreasing(int[] deck) {
        Arrays.sort(deck);
        Deque<Integer> q = new ArrayDeque<>();
        for (int i = deck.length - 1; i >= 0; --i) {
            if (!q.isEmpty()) {
                q.offerFirst(q.pollLast());
            }
            q.offerFirst(deck[i]);
        }
        int[] ans = new int[deck.length];
        int p = 0;
        for (int v : q) {
            ans[p++] = v;
        }
        return ans;
    }

    // 解法二:模拟揭示过程填值(对照实现)
    public int[] deckRevealedIncreasing2(int[] deck) {
        Arrays.sort(deck);
        int n = deck.length;
        int[] ans = new int[n];
        boolean[] filled = new boolean[n];
        int idx = 0;   // 当前应填入的牌(从小到大)
        int pos = 0;
        int filledCnt = 0;
        while (filledCnt < n) {
            // 找到下一张要填的牌
            ans[pos] = deck[idx++];
            filled[pos] = true;
            ++filledCnt;
            if (filledCnt == n) {
                break;
            }
            // 模拟"把下一张移到队尾":跳过已填位置,找下一个空位
            pos = nextEmpty(filled, pos, n);
            pos = nextEmpty(filled, pos, n);
        }
        return ans;
    }

    private int nextEmpty(boolean[] filled, int pos, int n) {
        do {
            pos = (pos + 1) % n;
        } while (filled[pos]);
        return pos;
    }

    // 校验:模拟揭示过程,确认展示顺序递增
    public boolean check(int[] deck) {
        Deque<Integer> q = new ArrayDeque<>();
        for (int v : deck) {
            q.offer(v);
        }
        int prev = -1;
        while (!q.isEmpty()) {
            int cur = q.poll();
            if (cur < prev) {
                return false;
            }
            prev = cur;
            if (!q.isEmpty()) {
                q.offer(q.poll());
            }
        }
        return true;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[][] decks = {
            {17, 13, 11, 2, 3, 5, 7},
            {1},
            {1, 2},
            {1, 2, 3},
            {5, 4, 3, 2, 1},
            {1, 3, 2, 4}
        };
        for (int t = 0; t < decks.length; ++t) {
            ++total;
            int[] g1 = s.deckRevealedIncreasing(decks[t].clone());
            int[] g2 = s.deckRevealedIncreasing2(decks[t].clone());
            // g1 是标准答案,直接比较两者与验证
            if (Arrays.equals(g1, g2) && s.check(g1)) {
                ++pass;
            } else {
                System.out.println("FAIL 950-" + (t + 1) + ": got "
                    + Arrays.toString(g1) + " vs " + Arrays.toString(g2));
            }
        }
        if (pass == total) {
            System.out.println("== 0950 PASS ==");
        } else {
            System.out.println("== 0950 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析