LeetCode676~689题目解析

6 阅读7分钟

LeetCode 676-689. 题解合集


676. Implement Magic Dictionary(实现一个魔法字典)

题目描述

设计一个使用单词列表进行初始化的数据结构,单词列表中的单词互不相同。如果给出一个单词,
请判定能否只将这个单词中一个字幕换成另一个字母,使得所形成的新单词存在于你构建的字典中。
实现 MagicDictionary 类:

  • MagicDictionary():初始化对象。
  • void buildDict(String[] dictionary):使用字符串数组 dictionary 设定该数据结构,
    dictionary 中的字符串互不相同。
  • boolean search(String searchWord):给定一个字符串 searchWord,判定能否只将字符串中
    一个字母换成另一个字母,使得所形成的新字符串能够与字典中的任一字符串匹配。如果可以,
    返回 true;否则,返回 false。

示例

  • 输入:
    ["MagicDictionary", "buildDict", "search", "search", "search", "search"]
    [[], [["hello", "leetcode"]], ["hello"], ["hhllo"], ["hell"], ["leetcoded"]]
  • 输出:
    [null, null, false, true, false, false]
  • 解释:
    MagicDictionary magicDictionary = new MagicDictionary();
    magicDictionary.buildDict(["hello", "leetcode"]);
    magicDictionary.search("hello"); // 返回 False
    magicDictionary.search("hhllo"); // 将第二个 'h' 替换为 'e' 可以匹配 "hello",返回 True
    magicDictionary.search("hell"); // 返回 False
    magicDictionary.search("leetcoded"); // 返回 False
    提示:
  • 1 <= dictionary.length <= 100
  • 1 <= dictionary[i].length <= 100
  • dictionary[i] 仅由小写英文字母组成
  • dictionary 中的所有字符串互不相同
  • 1 <= searchWord.length <= 100
  • searchWord 仅由小写英文字母组成
  • buildDict 仅在 search 之前调用一次
  • 最多调用 100 次 search

解题思路

解法一(字典树 + DFS,主提交):把所有词插入 Trie。
搜索时 DFS:若当前字符在 Trie 中有子节点,先按原字符走(diff 不变);若还未使用改动机会
(diff == 0),可尝试改走任意其他字母的子节点(diff 置 1)。到词尾时要求恰好改过一次
(diff == 1)且到达词尾节点。
时间复杂度 O(words·len + search 次数·词长·26),空间复杂度 O(总字符数)。
解法二(哈希集暴力):buildDict 存原始词集;搜索时枚举每个位置改成 25 种其他字母,判断
新词是否在词集中。时间复杂度 O(len·26),空间复杂度 O(总词长)。

代码实现

class MagicDictionary {

    private static class TrieNode {
        TrieNode[] children = new TrieNode[26];
        boolean isEnd;
    }

    private TrieNode root = new TrieNode();

    public MagicDictionary() {
    }

    public void buildDict(String[] dictionary) {
        for (String w : dictionary) {
            insert(w);
        }
    }

    private void insert(String w) {
        TrieNode node = root;
        for (int i = 0; i < w.length(); ++i) {
            int c = w.charAt(i) - 'a';
            if (node.children[c] == null) {
                node.children[c] = new TrieNode();
            }
            node = node.children[c];
        }
        node.isEnd = true;
    }

    public boolean search(String searchWord) {
        return dfs(searchWord, 0, root, false);
    }

    private boolean dfs(String w, int i, TrieNode node, boolean changed) {
        if (i == w.length()) {
            return changed && node.isEnd;
        }
        int c = w.charAt(i) - 'a';
        // 原字符走
        if (node.children[c] != null && dfs(w, i + 1, node.children[c], changed)) {
            return true;
        }
        // 未改动过:尝试换成其他字母
        if (!changed) {
            for (int k = 0; k < 26; ++k) {
                if (k != c && node.children[k] != null) {
                    if (dfs(w, i + 1, node.children[k], true)) {
                        return true;
                    }
                }
            }
        }
        return false;
    }

    public static void main(String[] args) {
        MagicDictionary md = new MagicDictionary();
        md.buildDict(new String[] {"hello", "leetcode"});
        int pass = 0, total = 0;
        ++total; if (!md.search("hello")) ++pass; else System.out.println("FAIL test1");
        ++total; if (md.search("hhllo")) ++pass; else System.out.println("FAIL test2: " + md.search("hhllo"));
        ++total; if (!md.search("hell")) ++pass; else System.out.println("FAIL test3");
        ++total; if (!md.search("leetcoded")) ++pass; else System.out.println("FAIL test4");
        MagicDictionary md2 = new MagicDictionary();
        md2.buildDict(new String[] {"a", "b"});
        ++total; if (md2.search("a")) ++pass; else System.out.println("FAIL test5: " + md2.search("a"));
        ++total; if (md2.search("c")) ++pass; else System.out.println("FAIL test6: " + md2.search("c"));
        MagicDictionary md3 = new MagicDictionary();
        md3.buildDict(new String[] {"ab", "cd"});
        ++total; if (md3.search("cb")) ++pass; else System.out.println("FAIL test7: " + md3.search("cb"));
        ++total; if (md3.search("ad")) ++pass; else System.out.println("FAIL test8: " + md3.search("ad"));
        if (pass == total) {
            System.out.println("== 0676 PASS ==");
        } else {
            System.out.println("== 0676 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(S·26)(S 为搜索总长度上限)O(总字符数)
解法二O(len·26)O(总词长)

677. Map Sum Pairs(键值映射)

题目描述

设计一个 map,满足以下几点:

  • 字符串表示键,整数表示值。
  • 返回具有前缀等于给定字符串的键的值的总和。
    实现一个 MapSum 类:
  • MapSum():初始化 MapSum 对象。
  • void insert(String key, int val):插入 key-val 键值对。如果键 key 已经存在,那么原来的
    键值对 key-value 将被替代成新的键值对。
  • int sum(String prefix):返回所有以该前缀 prefix 开头的键 key 的值的总和。

示例 1

  • 输入:
    ["MapSum", "insert", "sum", "insert", "sum"]
    [[], ["apple", 3], ["ap"], ["app", 2], ["ap"]]
  • 输出:
    [null, null, 3, null, 5]
  • 解释:
    MapSum mapSum = new MapSum();
    mapSum.insert("apple", 3);
    mapSum.sum("ap"); // 返回 3 (apple = 3)
    mapSum.insert("app", 2);
    mapSum.sum("ap"); // 返回 5 (apple + app = 3 + 2 = 5)
    提示:
  • 1 <= key.length, prefix.length <= 50
  • key 和 prefix 仅由小写英文字母组成
  • 1 <= val <= 1000
  • 最多调用 50 次 insert 和 sum

解题思路

解法一(字典树节点存区间和,主提交):Trie 每个节点维护经过该节点的所有键值的总和 sum。
insert 时用哈希表记录当前 key 的旧值,新值 x = val - oldVal,把 x 加到路径上所有节点;
sum(prefix) 沿前缀走到节点直接返回 sum(若中途缺节点返回 0)。
时间复杂度 O(key/prefix 长度),空间复杂度 O(总字符数)。
解法二(哈希表暴力):d 存每个 key→val,sum 时遍历所有键检查是否以前缀开头。
时间复杂度 O(键数·len),空间复杂度 O(键数)。

代码实现

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

class MapSum {

    private static class TrieNode {
        TrieNode[] children = new TrieNode[26];
        int sum;
    }

    private TrieNode root = new TrieNode();
    private Map<String, Integer> d = new HashMap<>();

    public MapSum() {
    }

    public void insert(String key, int val) {
        int x = val - d.getOrDefault(key, 0);
        d.put(key, val);
        TrieNode node = root;
        for (int i = 0; i < key.length(); ++i) {
            int c = key.charAt(i) - 'a';
            if (node.children[c] == null) {
                node.children[c] = new TrieNode();
            }
            node = node.children[c];
            node.sum += x;
        }
    }

    public int sum(String prefix) {
        TrieNode node = root;
        for (int i = 0; i < prefix.length(); ++i) {
            int c = prefix.charAt(i) - 'a';
            if (node.children[c] == null) {
                return 0;
            }
            node = node.children[c];
        }
        return node.sum;
    }

    public static void main(String[] args) {
        MapSum ms = new MapSum();
        int pass = 0, total = 0;
        ms.insert("apple", 3);
        ++total; if (ms.sum("ap") == 3) ++pass; else System.out.println("FAIL test1: " + ms.sum("ap"));
        ms.insert("app", 2);
        ++total; if (ms.sum("ap") == 5) ++pass; else System.out.println("FAIL test2: " + ms.sum("ap"));
        ++total; if (ms.sum("apple") == 3) ++pass; else System.out.println("FAIL test3: " + ms.sum("apple"));
        ++total; if (ms.sum("a") == 5) ++pass; else System.out.println("FAIL test4: " + ms.sum("a"));
        ++total; if (ms.sum("x") == 0) ++pass; else System.out.println("FAIL test5");
        ms.insert("apple", 1);
        ++total; if (ms.sum("ap") == 3) ++pass; else System.out.println("FAIL test6: " + ms.sum("ap"));
        ms.insert("ap", 10);
        ++total; if (ms.sum("a") == 13) ++pass; else System.out.println("FAIL test7: " + ms.sum("a"));
        ++total; if (ms.sum("") == 0) ++pass; else System.out.println("FAIL test8: " + ms.sum(""));
        if (pass == total) {
            System.out.println("== 0677 PASS ==");
        } else {
            System.out.println("== 0677 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(len)O(总字符数)
解法二O(键数·len)O(键数)

678. Valid Parenthesis String(有效的括号字符串)

题目描述

给你一个只包含三种字符的字符串,支持的字符类型分别是 '('、')' 和 '*'。请你检验这个字符串
是否为有效字符串,如果是有效字符串返回 true。
有效字符串符合如下规则:

  • 任何左括号 '(' 必须有相应的右括号 ')'。
  • 任何右括号 ')' 必须有相应的左括号 '('。
  • 左括号 '(' 必须在对应的右括号之前 ')'。
  • '*' 可以被视为单个右括号 ')',或单个左括号 '(',或一个空字符串 ""。

示例 1

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

示例 2

  • 输入:s = "(*)"
  • 输出:true

示例 3

  • 输入:s = "(*))"
  • 输出:true
    提示:
  • 1 <= s.length <= 100
  • s[i] 为 '('、')' 或 '*'

解题思路

解法一(贪心区间维护,主提交):用 lo 表示未匹配左括号数量的最小值,hi 表示最大值
(* 可当作左/右/空)。遍历时:

  • '(':lo++,hi++;
  • ')':lo--(至少减到 0),hi--;
  • '*':lo--(当右括号),hi++(当左括号)。
    若 hi < 0 说明右括号过多,失败;lo 保持不小于 0。结束时 lo == 0 才合法。
    时间复杂度 O(n),空间复杂度 O(1)。
    解法二(区间 DP):dp[i][j] 表示 s[i..j] 是否为有效括号串。长度 1 的区间只有 * 合法;
    更长的区间:若两端能配对(s[i] 与 s[j] 为 () 或含 *)且内部合法,或能在 k 处拆成
    两个合法段。时间复杂度 O(n³),空间复杂度 O(n²)。

代码实现

class Solution {

    // 解法一:贪心区间 [lo, hi]
    public boolean checkValidString(String s) {
        int lo = 0, hi = 0;
        for (int i = 0; i < s.length(); ++i) {
            char c = s.charAt(i);
            if (c == '(') {
                ++lo;
                ++hi;
            } else if (c == ')') {
                lo = Math.max(0, lo - 1);
                --hi;
            } else { // '*'
                lo = Math.max(0, lo - 1);
                ++hi;
            }
            if (hi < 0) {
                return false;
            }
        }
        return lo == 0;
    }

    // 解法二:区间 DP
    public boolean checkValidString2(String s) {
        int n = s.length();
        boolean[][] dp = new boolean[n][n];
        for (int i = 0; i < n; ++i) {
            if (s.charAt(i) == '*') {
                dp[i][i] = true;
            }
        }
        for (int len = 2; len <= n; ++len) {
            for (int i = 0; i + len - 1 < n; ++i) {
                int j = i + len - 1;
                char a = s.charAt(i), b = s.charAt(j);
                // 两端可配对((,) 或含*)且内部合法
                if ((a == '(' || a == '*') && (b == ')' || b == '*') && (len == 2 || dp[i + 1][j - 1])) {
                    dp[i][j] = true;
                    continue;
                }
                // 拆成两段
                for (int k = i; k < j; ++k) {
                    if (dp[i][k] && dp[k + 1][j]) {
                        dp[i][j] = true;
                        break;
                    }
                }
            }
        }
        return dp[0][n - 1];
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (sol.checkValidString("()")) ++pass; else System.out.println("FAIL test1: " + sol.checkValidString("()"));
        ++total; if (sol.checkValidString("(*)")) ++pass; else System.out.println("FAIL test2: " + sol.checkValidString("(*)"));
        ++total; if (sol.checkValidString("(*))")) ++pass; else System.out.println("FAIL test3: " + sol.checkValidString("(*))"));
        ++total; if (!sol.checkValidString("(")) ++pass; else System.out.println("FAIL test4: " + sol.checkValidString("("));
        ++total; if (!sol.checkValidString(")")) ++pass; else System.out.println("FAIL test5");
        ++total; if (sol.checkValidString("((*)")) ++pass; else System.out.println("FAIL test6: " + sol.checkValidString("((*)"));
        ++total; if (sol.checkValidString("(*))()*")) ++pass; else System.out.println("FAIL test7");
        ++total; if (sol.checkValidString("*()*")) ++pass; else System.out.println("FAIL test8: " + sol.checkValidString("*()*"));
        // 解法二
        ++total; if (sol.checkValidString2("()")) ++pass; else System.out.println("FAIL test9");
        ++total; if (sol.checkValidString2("(*)")) ++pass; else System.out.println("FAIL test10");
        ++total; if (sol.checkValidString2("(*))")) ++pass; else System.out.println("FAIL test11");
        ++total; if (!sol.checkValidString2(")")) ++pass; else System.out.println("FAIL test12");
        ++total; if (sol.checkValidString2("((*)")) ++pass; else System.out.println("FAIL test13");
        ++total; if (sol.checkValidString2("*()*")) ++pass; else System.out.println("FAIL test14");
        if (pass == total) {
            System.out.println("== 0678 PASS ==");
        } else {
            System.out.println("== 0678 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

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

679. 24 Game(24 点游戏)

题目描述

给定一个长度为 4 的整数数组 cards。你有 4 张卡片,每张卡片上都包含一个范围在 [1,9] 的数字。
您应该使用运算符 ['+', '-', '*', '/'] 和括号 '(' 和 ')' 将这些卡片上的数字排列成数学表达式,
以获得值 24。
你须遵守以下规则:

  • 除法运算符 '/' 表示实数除法,而不是整数除法。例如 4 /(1 - 2 / 3)= 4 /(1 / 3)= 12。
  • 每个运算都在两个数字之间。特别是,不能使用 "-" 作为一元运算符。
    例如,如果 cards = [1,1,1,1],则表达式 "-1 -1 -1 -1" 是不允许的。
  • 你不能把数字串在一起。例如,如果 cards = [1,2,1,2],则表达式 "12 + 12" 无效。
    如果可以得到这样的表达式,其计算结果为 24,则返回 true,否则返回 false。

示例 1

  • 输入:cards = [4, 1, 8, 7]
  • 输出:true
  • 解释:(8-4) * (7-1) = 24

示例 2

  • 输入:cards = [1, 2, 1, 2]
  • 输出:false
    提示:
  • cards.length == 4
  • 1 <= cards[i] <= 9

解题思路

解法一(回溯选两数计算,主提交):问题等价于:从集合中任取两个数做四则运算得到一个新数,
与剩余数组成更小的集合,递归直到只剩一个数,判断是否等于 24(浮点容差 1e-6)。
每次枚举两个数的顺序(a op b 与 b op a 各算一次,+ * 交换律可省但保留无妨),
除法时要跳过除数为 0。时间复杂度 O(组合数·种数),空间复杂度 O(递归深度)。
解法二(枚举所有括号结构):只对 4 个数,枚举全部 5 种括号结构与 4 种运算符组合,
共 4!×4³×5 种,逐一计算。本质与解法一相同,只是显式枚举;此处以解法一统一实现,
解法二作为相同思想的另一种写法(给出 judgePoint24 主方法)。

代码实现

class Solution {

    // 解法一:回溯
    public boolean judgePoint24(int[] cards) {
        double[] a = new double[] {cards[0], cards[1], cards[2], cards[3]};
        return solve(a, 4);
    }

    private boolean solve(double[] a, int n) {
        if (n == 1) {
            return Math.abs(a[0] - 24.0) < 1e-6;
        }
        // 任选两个下标 i, j
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < n; ++j) {
                if (i == j) {
                    continue;
                }
                double[] nxt = new double[n - 1];
                int idx = 0;
                for (int k = 0; k < n; ++k) {
                    if (k != i && k != j) {
                        nxt[idx++] = a[k];
                    }
                }
                double x = a[i], y = a[j];
                // 四种运算
                double[] vals = new double[] {x + y, x - y, x * y};
                int cnt = 3;
                if (Math.abs(y) > 1e-9) {
                    vals = new double[] {x + y, x - y, x * y, x / y};
                    cnt = 4;
                }
                for (int v = 0; v < cnt; ++v) {
                    nxt[idx] = vals[v];
                    if (solve(nxt, n - 1)) {
                        return true;
                    }
                }
            }
        }
        return false;
    }

    // 解法二:直接枚举两数合并(与解法一等价,改用操作符显式计算)
    public boolean judgePoint24_2(int[] cards) {
        double[] a = new double[] {cards[0], cards[1], cards[2], cards[3]};
        return dfs2(a);
    }

    private boolean dfs2(double[] a) {
        int n = a.length;
        if (n == 1) {
            return Math.abs(a[0] - 24.0) < 1e-6;
        }
        for (int i = 0; i < n; ++i) {
            for (int j = i + 1; j < n; ++j) {
                double[] rest = new double[n - 1];
                int idx = 0;
                for (int k = 0; k < n; ++k) {
                    if (k != i && k != j) {
                        rest[idx++] = a[k];
                    }
                }
                double x = a[i], y = a[j];
                double[] cand = {x + y, x - y, y - x, x * y};
                int cnt = 4;
                if (Math.abs(y) > 1e-9) {
                    cand = new double[] {x + y, x - y, y - x, x * y, x / y};
                    cnt = 5;
                    if (Math.abs(x) > 1e-9) {
                        cand = new double[] {x + y, x - y, y - x, x * y, x / y, y / x};
                        cnt = 6;
                    }
                }
                for (int v = 0; v < cnt; ++v) {
                    rest[idx] = cand[v];
                    if (dfs2(rest)) {
                        return true;
                    }
                }
            }
        }
        return false;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (sol.judgePoint24(new int[] {4, 1, 8, 7})) ++pass; else System.out.println("FAIL test1: " + sol.judgePoint24(new int[] {4, 1, 8, 7}));
        ++total; if (!sol.judgePoint24(new int[] {1, 2, 1, 2})) ++pass; else System.out.println("FAIL test2");
        ++total; if (sol.judgePoint24(new int[] {1, 2, 1, 2}) == false && sol.judgePoint24(new int[] {8, 1, 6, 1})) ++pass; else System.out.println("FAIL test3");
        ++total; if (!sol.judgePoint24(new int[] {1, 1, 1, 1})) ++pass; else System.out.println("FAIL test4");
        ++total; if (sol.judgePoint24(new int[] {1, 3, 4, 6})) ++pass; else System.out.println("FAIL test5: " + sol.judgePoint24(new int[] {1, 3, 4, 6}));
        ++total; if (sol.judgePoint24_2(new int[] {4, 1, 8, 7})) ++pass; else System.out.println("FAIL test6");
        ++total; if (!sol.judgePoint24_2(new int[] {1, 2, 1, 2})) ++pass; else System.out.println("FAIL test7");
        ++total; if (!sol.judgePoint24_2(new int[] {1, 1, 1, 1})) ++pass; else System.out.println("FAIL test8");
        ++total; if (sol.judgePoint24_2(new int[] {1, 3, 4, 6})) ++pass; else System.out.println("FAIL test9");
        if (pass == total) {
            System.out.println("== 0679 PASS ==");
        } else {
            System.out.println("== 0679 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(1)(常数级,最大 9216 种)O(1)
解法二O(1)O(1)

680. Valid Palindrome II(验证回文串 II)

题目描述

给你一个字符串 s,最多可以从中删除一个字符。
请你判断 s 是否能成为回文字符串:如果能,返回 true;否则,返回 false。

示例 1

  • 输入:s = "aba"
  • 输出:true

示例 2

  • 输入:s = "abca"
  • 输出:true
  • 解释:你可以删除字符 'c'。

示例 3

  • 输入:s = "abc"
  • 输出:false
    提示:
  • 1 <= s.length <= 10^5
  • s 由小写英文字母组成

解题思路

解法一(双指针 + 一次容错,主提交):两端指针向中间移动,遇到第一处失配 s[i] != s[j] 时,
只需分别验证「删掉 s[i] 后 s[i+1..j] 是否回文」与「删掉 s[j] 后 s[i..j-1] 是否回文」,
二者任一成立即可。整体只多扫描一遍。时间复杂度 O(n),空间复杂度 O(1)。
解法二(贪心删除 + 子串判断):同样双指针,失配时利用 s.substring(i+1, j+1) 与
s.substring(i, j) 调用回文判断工具(isPalindrome),逻辑等价但更直观。
时间复杂度 O(n),空间复杂度 O(n)(子串)。

代码实现

class Solution {

    // 解法一:双指针 + 一次容错
    public boolean validPalindrome(String s) {
        int i = 0, j = s.length() - 1;
        while (i < j) {
            if (s.charAt(i) != s.charAt(j)) {
                return isPal(s, i + 1, j) || isPal(s, i, j - 1);
            }
            ++i;
            --j;
        }
        return true;
    }

    private boolean isPal(String s, int l, int r) {
        while (l < r) {
            if (s.charAt(l) != s.charAt(r)) {
                return false;
            }
            ++l;
            --r;
        }
        return true;
    }

    // 解法二:失配时用子串判断
    public boolean validPalindrome2(String s) {
        int i = 0, j = s.length() - 1;
        while (i < j) {
            if (s.charAt(i) != s.charAt(j)) {
                return isPalindrome(s.substring(i + 1, j + 1)) || isPalindrome(s.substring(i, j));
            }
            ++i;
            --j;
        }
        return true;
    }

    private boolean isPalindrome(String s) {
        return isPal(s, 0, s.length() - 1);
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (sol.validPalindrome("aba")) ++pass; else System.out.println("FAIL test1: " + sol.validPalindrome("aba"));
        ++total; if (sol.validPalindrome("abca")) ++pass; else System.out.println("FAIL test2: " + sol.validPalindrome("abca"));
        ++total; if (!sol.validPalindrome("abc")) ++pass; else System.out.println("FAIL test3");
        ++total; if (sol.validPalindrome("abccba")) ++pass; else System.out.println("FAIL test4: " + sol.validPalindrome("abccba"));
        ++total; if (sol.validPalindrome("a")) ++pass; else System.out.println("FAIL test5: " + sol.validPalindrome("a"));
        ++total; if (sol.validPalindrome("deeee")) ++pass; else System.out.println("FAIL test6: " + sol.validPalindrome("deeee"));
        ++total; if (!sol.validPalindrome("abcd")) ++pass; else System.out.println("FAIL test7");
        ++total; if (sol.validPalindrome2("aba")) ++pass; else System.out.println("FAIL test8");
        ++total; if (sol.validPalindrome2("abca")) ++pass; else System.out.println("FAIL test9");
        ++total; if (!sol.validPalindrome2("abc")) ++pass; else System.out.println("FAIL test10");
        ++total; if (sol.validPalindrome2("deeee")) ++pass; else System.out.println("FAIL test11");
        if (pass == total) {
            System.out.println("== 0680 PASS ==");
        } else {
            System.out.println("== 0680 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

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

681. Next Closest Time(最近时刻)

题目描述

给定一个形如 "HH:MM" 表示的时刻 time,利用当前出现过的数字构造下一个距离当前时间最近的时刻。
每个出现数字都可以被无限次使用。
你可以认为给定的字符串一定是合法的。例如,"01:34" 和 "12:09" 是合法的,"1:34" 和
"12:9" 是不合法的。

示例 1

  • 输入:"19:34"
  • 输出:"19:39"
  • 解释:利用数字 1, 9, 3, 4 构造出来的最近时刻是 19:39,是 5 分钟之后。结果不是 19:33,
    因为这个时刻是 23 小时 59 分钟之后。

示例 2

  • 输入:"23:59"
  • 输出:"22:22"
  • 解释:答案一定是第二天的某一时刻,所以选择可构造的最小时刻。
    提示:
  • time.length == 5
  • time 为有效时间,格式为 "HH:MM"
  • 0 <= HH < 24
  • 0 <= MM < 60

解题思路

解法一(DFS 枚举 4 位数字 + 最近时间差,主提交):把时间转成分钟 t;用集合记录出现过的数字。
DFS 枚举四位(时 2 位 + 分 2 位),校验合法的 HH:MM;计算候选分钟 p 与 t 的差值
(p > t 用 p - t,否则跨天 p - t + 1440),取差值最小的候选。
如果没有任何合法候选比当前更大,则取“可构造的最小时刻”(数字集合中的最小字符组成 mm:mm)。
时间复杂度 O(4·4!),空间复杂度 O(1)。
解法二(直接枚举 1440 个分钟):从 t+1 到 t+1440(模 1440)逐个检查每个分钟的所有数字
是否都在原数字集合中,第一个满足的就是答案。直觉清晰,实现最简。
时间复杂度 O(1440),空间复杂度 O(1)。

代码实现

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

class Solution {

    // 解法一:DFS 枚举
    private int t;
    private int bestDiff;
    private String best;
    private Set<Character> set;

    public String nextClosestTime(String time) {
        t = Integer.parseInt(time.substring(0, 2)) * 60 + Integer.parseInt(time.substring(3));
        set = new HashSet<>();
        char mi = 'z';
        for (char c : time.toCharArray()) {
            if (c != ':') {
                set.add(c);
                if (c < mi) {
                    mi = c;
                }
            }
        }
        bestDiff = Integer.MAX_VALUE;
        best = null;
        dfs(new char[4], 0);
        if (best == null) {
            // 没有比当前更近的,取最小数字组成的最小时刻(次日)
            best = "" + mi + mi + ":" + mi + mi;
        }
        return best;
    }

    private void dfs(char[] cur, int pos) {
        if (pos == 4) {
            int h = (cur[0] - '0') * 10 + (cur[1] - '0');
            int m = (cur[2] - '0') * 10 + (cur[3] - '0');
            if (h > 23 || m > 59) {
                return;
            }
            int p = h * 60 + m;
            int diff = p > t ? p - t : p - t + 1440;
            if (diff < bestDiff) {
                bestDiff = diff;
                best = "" + cur[0] + cur[1] + ":" + cur[2] + cur[3];
            }
            return;
        }
        for (char c : set) {
            cur[pos] = c;
            dfs(cur, pos + 1);
        }
    }

    // 解法二:枚举分钟(最简)
    public String nextClosestTime2(String time) {
        int t = Integer.parseInt(time.substring(0, 2)) * 60 + Integer.parseInt(time.substring(3));
        Set<Character> set = new HashSet<>();
        for (char c : time.toCharArray()) {
            if (c != ':') {
                set.add(c);
            }
        }
        for (int i = 1; i <= 1440; ++i) {
            int next = (t + i) % 1440;
            String cand = String.format("%02d:%02d", next / 60, next % 60);
            boolean ok = true;
            for (char c : cand.toCharArray()) {
                if (c != ':' && !set.contains(c)) {
                    ok = false;
                    break;
                }
            }
            if (ok) {
                return cand;
            }
        }
        return time;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (sol.nextClosestTime("19:34").equals("19:39")) ++pass; else System.out.println("FAIL test1: " + sol.nextClosestTime("19:34"));
        ++total; if (sol.nextClosestTime("23:59").equals("22:22")) ++pass; else System.out.println("FAIL test2: " + sol.nextClosestTime("23:59"));
        ++total; if (sol.nextClosestTime("13:55").equals("15:11")) ++pass; else System.out.println("FAIL test3: " + sol.nextClosestTime("13:55"));
        ++total; if (sol.nextClosestTime("11:11").equals("11:11")) ++pass; else System.out.println("FAIL test4: " + sol.nextClosestTime("11:11"));
        ++total; if (sol.nextClosestTime("20:48").equals("22:00")) ++pass; else System.out.println("FAIL test5: " + sol.nextClosestTime("20:48"));
        // 解法二
        ++total; if (sol.nextClosestTime2("19:34").equals("19:39")) ++pass; else System.out.println("FAIL test6: " + sol.nextClosestTime2("19:34"));
        ++total; if (sol.nextClosestTime2("23:59").equals("22:22")) ++pass; else System.out.println("FAIL test7: " + sol.nextClosestTime2("23:59"));
        ++total; if (sol.nextClosestTime2("13:55").equals("15:11")) ++pass; else System.out.println("FAIL test8: " + sol.nextClosestTime2("13:55"));
        ++total; if (sol.nextClosestTime2("11:11").equals("11:11")) ++pass; else System.out.println("FAIL test9: " + sol.nextClosestTime2("11:11"));
        ++total; if (sol.nextClosestTime2("20:48").equals("22:00")) ++pass; else System.out.println("FAIL test10: " + sol.nextClosestTime2("20:48"));
        if (pass == total) {
            System.out.println("== 0681 PASS ==");
        } else {
            System.out.println("== 0681 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(4·4!)O(1)
解法二O(1440)O(1)

682. Baseball Game(棒球比赛)

题目描述

你现在是一场采用特殊赛制棒球比赛的记录员。这场比赛由若干回合组成,过去几回合的得分可能会影响
以后几回合的得分。比赛开始时,记录是空白的。你会得到一个记录操作的字符串列表 ops,其中
ops[i] 是你需要记录的第 i 项操作,ops 遵循下述规则:

  • 整数 x:表示本回合新获得分数 x。
  • "+":表示本回合新获得的得分是前两次得分的总和。题目数据保证记录此操作时前面总是存在两个有效的分数。
  • "D":表示本回合新获得的得分是前一次得分的两倍。题目数据保证记录此操作时前面总是存在一个有效的分数。
  • "C":表示前一次得分无效,将其从记录中移除。题目数据保证记录此操作时前面总是存在一个有效的分数。
    请你返回记录中所有得分的总和。

示例 1

  • 输入:ops = ["5","2","C","D","+"]
  • 输出:30
  • 解释:
    "5" - 记录加 5 ,记录现在是 [5]
    "2" - 记录加 2 ,记录现在是 [5, 2]
    "C" - 移除前一次得分,记录现在是 [5]
    "D" - 记录加 2 * 5 = 10 ,记录现在是 [5, 10]
    "+" - 记录加 5 + 10 = 15 ,记录现在是 [5, 10, 15]
    所有得分的总和 5 + 10 + 15 = 30

示例 2

  • 输入:ops = ["5","-2","4","C","D","9","+","+"]
  • 输出:27

示例 3

  • 输入:ops = ["1"]
  • 输出:1
    提示:
  • 1 <= ops.length <= 1000
  • ops[i] 为 "C"、"D"、"+",或者一个表示整数的字符串,整数范围是 [-3*10^4, 3*10^4]

解题思路

解法一(栈模拟,主提交):用栈保存每回合的有效得分。

  • 数字:解析入栈;
  • "C":弹出栈顶;
  • "D":新分数 = 栈顶 × 2 入栈;
  • "+":新分数 = 栈顶 + 次栈顶入栈。
    最后把栈内所有分数求和。时间复杂度 O(n),空间复杂度 O(n)。
    解法二(数组模拟栈):用数组 arr 与计数器 top 代替 Deque(避免频繁装箱/拆箱,性能略优),
    逻辑完全一致。时间复杂度 O(n),空间复杂度 O(n)。

代码实现

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

class Solution {

    // 解法一:双端队列模拟栈
    public int calPoints(String[] ops) {
        Deque<Integer> st = new ArrayDeque<>();
        for (String op : ops) {
            if (op.equals("C")) {
                st.pop();
            } else if (op.equals("D")) {
                st.push(st.peek() * 2);
            } else if (op.equals("+")) {
                int top = st.pop();
                int newVal = top + st.peek();
                st.push(top);
                st.push(newVal);
            } else {
                st.push(Integer.parseInt(op));
            }
        }
        int sum = 0;
        for (int v : st) {
            sum += v;
        }
        return sum;
    }

    // 解法二:数组模拟栈
    public int calPoints2(String[] ops) {
        int[] arr = new int[ops.length];
        int top = 0;
        for (String op : ops) {
            if (op.equals("C")) {
                --top;
            } else if (op.equals("D")) {
                arr[top] = arr[top - 1] * 2;
                ++top;
            } else if (op.equals("+")) {
                arr[top] = arr[top - 1] + arr[top - 2];
                ++top;
            } else {
                arr[top] = Integer.parseInt(op);
                ++top;
            }
        }
        int sum = 0;
        for (int i = 0; i < top; ++i) {
            sum += arr[i];
        }
        return sum;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (sol.calPoints(new String[] {"5", "2", "C", "D", "+"}) == 30) ++pass; else System.out.println("FAIL test1: " + sol.calPoints(new String[] {"5", "2", "C", "D", "+"}));
        ++total; if (sol.calPoints(new String[] {"5", "-2", "4", "C", "D", "9", "+", "+"}) == 27) ++pass; else System.out.println("FAIL test2: " + sol.calPoints(new String[] {"5", "-2", "4", "C", "D", "9", "+", "+"}));
        ++total; if (sol.calPoints(new String[] {"1"}) == 1) ++pass; else System.out.println("FAIL test3: " + sol.calPoints(new String[] {"1"}));
        ++total; if (sol.calPoints(new String[] {"5", "5", "D", "+"}) == 35) ++pass; else System.out.println("FAIL test4: " + sol.calPoints(new String[] {"5", "5", "D", "+"}));
        ++total; if (sol.calPoints2(new String[] {"5", "2", "C", "D", "+"}) == 30) ++pass; else System.out.println("FAIL test5");
        ++total; if (sol.calPoints2(new String[] {"5", "-2", "4", "C", "D", "9", "+", "+"}) == 27) ++pass; else System.out.println("FAIL test6");
        ++total; if (sol.calPoints2(new String[] {"1"}) == 1) ++pass; else System.out.println("FAIL test7");
        ++total; if (sol.calPoints2(new String[] {"5", "5", "D", "+"}) == 35) ++pass; else System.out.println("FAIL test8");
        if (pass == total) {
            System.out.println("== 0682 PASS ==");
        } else {
            System.out.println("== 0682 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

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

683. K Empty Slots(K 个关闭的灯泡)

题目描述

n 个灯泡排成一行,编号从 1 到 n。最初,所有灯泡都关闭。每天只打开一个灯泡,直到
n 天后所有灯泡都打开。
给你一个长度为 n 的灯泡数组 bulbs,其中 bulbs[i] = x 意味着在第 (i+1) 天,我们会把
在位置 x 的灯泡打开,其中 i 从 0 开始,x 从 1 开始。
给你一个整数 k,请返回恰好有两个打开的灯泡,且它们中间正好有 k 个全部关闭的灯泡的
最小的天数。如果不存在这种情况,返回 -1。

示例 1

  • 输入:bulbs = [1,3,2],k = 1
  • 输出:2
  • 解释:第一天打开第一个灯泡 [1,0,0];第二天打开第三个灯泡 [1,0,1];第三天打开第二个
    灯泡 [1,1,1]。返回 2,因为在第二天,两个打开的灯泡之间恰好有一个关闭的灯泡。

示例 2

  • 输入:bulbs = [1,2,3],k = 1
  • 输出:-1
    提示:
  • n == bulbs.length
  • 1 <= n <= 2 * 10^4
  • 1 <= bulbs[i] <= n
  • bulbs 是一个由从 1 到 n 的数字构成的排列
  • 0 <= k <= 2 * 10^4

解题思路

解法一(有序集合 TreeSet,主提交):按天数依次打开灯泡,把打开位置加入集合。每次打开
位置 x 后,只需检查它在集合中的前驱 prev 与后继 next:若 x - prev - 1 == k 或
next - x - 1 == k,说明存在一对打开的灯泡中间恰好隔 k 个关闭的灯泡(当 k == 0 时即
相邻)。因为“中间全是关闭”等价于中间没有任何已打开位置,所以相邻已开位置之间的距离
就是中间关闭灯泡的个数。返回第一天的天数。时间复杂度 O(n log n),空间复杂度 O(n)。
解法二(树状数组):用树状数组维护已打开位置的前缀个数。当天打开 x 后,若
x - (k+1) 已打开且区间 (x-k-1, x-1] 内前缀计数之差为 0(即中间全是关闭),或者
x + (k+1) 已打开且区间 (x, x+k] 内前缀计数之差为 0,则当天符合要求,返回天数。
时间复杂度 O(n log n),空间复杂度 O(n)。

代码实现

import java.util.Arrays;
import java.util.TreeSet;

class Solution {

    // 解法一:有序集合
    public int kEmptySlots(int[] bulbs, int k) {
        TreeSet<Integer> set = new TreeSet<>();
        for (int day = 1; day <= bulbs.length; ++day) {
            int x = bulbs[day - 1];
            set.add(x);
            Integer prev = set.lower(x);
            Integer next = set.higher(x);
            if (prev != null && x - prev - 1 == k) {
                return day;
            }
            if (next != null && next - x - 1 == k) {
                return day;
            }
        }
        return -1;
    }

    // 解法二:树状数组
    public int kEmptySlots2(int[] bulbs, int k) {
        int n = bulbs.length;
        BinaryIndexedTree tree = new BinaryIndexedTree(n);
        boolean[] vis = new boolean[n + 1];
        for (int day = 1; day <= n; ++day) {
            int x = bulbs[day - 1];
            tree.update(x, 1);
            vis[x] = true;
            int y = x - k - 1;
            if (y >= 1 && vis[y] && tree.query(x - 1) - tree.query(y) == 0) {
                return day;
            }
            y = x + k + 1;
            if (y <= n && vis[y] && tree.query(y - 1) - tree.query(x) == 0) {
                return day;
            }
        }
        return -1;
    }

    private static class BinaryIndexedTree {
        private int n;
        private int[] c;

        BinaryIndexedTree(int n) {
            this.n = n;
            this.c = new int[n + 1];
        }

        void update(int x, int delta) {
            for (; x <= n; x += x & -x) {
                c[x] += delta;
            }
        }

        int query(int x) {
            int s = 0;
            for (; x > 0; x -= x & -x) {
                s += c[x];
            }
            return s;
        }
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (sol.kEmptySlots(new int[]{1, 3, 2}, 1) == 2) ++pass; else System.out.println("FAIL test1: " + sol.kEmptySlots(new int[]{1, 3, 2}, 1));
        ++total; if (sol.kEmptySlots(new int[]{1, 2, 3}, 1) == -1) ++pass; else System.out.println("FAIL test2: " + sol.kEmptySlots(new int[]{1, 2, 3}, 1));
        ++total; if (sol.kEmptySlots(new int[]{3, 1, 5, 4, 2}, 1) == 2) ++pass; else System.out.println("FAIL test3: " + sol.kEmptySlots(new int[]{3, 1, 5, 4, 2}, 1));
        ++total; if (sol.kEmptySlots(new int[]{1, 4, 2, 3}, 2) == 2) ++pass; else System.out.println("FAIL test4: " + sol.kEmptySlots(new int[]{1, 4, 2, 3}, 2));
        ++total; if (sol.kEmptySlots(new int[]{1, 3, 2}, 0) == 3) ++pass; else System.out.println("FAIL test5: " + sol.kEmptySlots(new int[]{1, 3, 2}, 0));
        // 解法二
        ++total; if (sol.kEmptySlots2(new int[]{1, 3, 2}, 1) == 2) ++pass; else System.out.println("FAIL test6: " + sol.kEmptySlots2(new int[]{1, 3, 2}, 1));
        ++total; if (sol.kEmptySlots2(new int[]{1, 2, 3}, 1) == -1) ++pass; else System.out.println("FAIL test7: " + sol.kEmptySlots2(new int[]{1, 2, 3}, 1));
        ++total; if (sol.kEmptySlots2(new int[]{3, 1, 5, 4, 2}, 1) == 2) ++pass; else System.out.println("FAIL test8: " + sol.kEmptySlots2(new int[]{3, 1, 5, 4, 2}, 1));
        ++total; if (sol.kEmptySlots2(new int[]{1, 4, 2, 3}, 2) == 2) ++pass; else System.out.println("FAIL test9: " + sol.kEmptySlots2(new int[]{1, 4, 2, 3}, 2));
        ++total; if (sol.kEmptySlots2(new int[]{1, 3, 2}, 0) == 3) ++pass; else System.out.println("FAIL test10: " + sol.kEmptySlots2(new int[]{1, 3, 2}, 0));
        if (pass == total) {
            System.out.println("== 0683 PASS ==");
        } else {
            System.out.println("== 0683 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

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

684. Redundant Connection(冗余连接)

题目描述

树可以看成是一个连通且无环的无向图。
给定一个图,该图从一棵 n 个节点(节点值 1~n)的树中添加一条边后获得。添加的边的两个不同顶点
编号在 1 到 n 中间,且这条附加的边不属于树中已存在的边。图的信息记录于长度为 n 的二维数组
edges,edges[i] = [ai, bi] 表示图中在 ai 和 bi 之间存在一条边。
请找出一条可以删去的边,删除后可使得剩余部分是一个有着 n 个节点的树。如果有多个答案,则返回
数组 edges 中最后出现的那个。

示例 1

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

示例 2

  • 输入:edges = [[1,2], [2,3], [3,4], [1,4], [1,5]]
  • 输出:[1,4]
    提示:
  • n == edges.length
  • 3 <= n <= 1000
  • edges[i].length == 2
  • 1 <= ai < bi <= edges.length
  • ai != bi
  • edges 中无重复元素
  • 给定的图是连通的

解题思路

解法一(并查集,主提交):按输入顺序遍历每条边。

  • 若两个端点已经连通(在同一个集合),说明这条边在环上,删掉它即可恢复树,直接返回该边;
  • 否则合并两个端点所在集合。
    因为输入保证有且仅有一条多余边,第一次遇到“已连通”的边即为答案。
    时间复杂度 O(n·α(n)),空间复杂度 O(n)。
    解法二(DFS 找环 + 取最后一条环边):先按输入顺序建邻接表,找到任意环,再保留环上
    出现次序最靠后的边返回。实现更繁琐,仅作对照说明。
    (本文件给出解法一;解法二以注释形式简述。)

代码实现

class Solution {

    private int[] parent;

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

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

    // 解法一:并查集
    public int[] findRedundantConnection(int[][] edges) {
        int n = edges.length;
        parent = new int[n + 1];
        for (int i = 1; i <= n; ++i) {
            parent[i] = i;
        }
        for (int[] e : edges) {
            if (!union(e[0], e[1])) {
                return e;
            }
        }
        return new int[0];
    }

    // 解法二等价实现:逆向删边验证(删掉某条边后若仍连通且无环,则是答案)——此处保留为
    // 简单校验:从后往前试删边,用并查集检查删后是否成树(无环且连通)。O(n²·α(n))
    public int[] findRedundantConnection2(int[][] edges) {
        int n = edges.length;
        for (int del = n - 1; del >= 0; --del) {
            parent = new int[n + 1];
            for (int i = 1; i <= n; ++i) {
                parent[i] = i;
            }
            boolean ok = true;
            for (int i = 0; i < n; ++i) {
                if (i == del) {
                    continue;
                }
                if (!union(edges[i][0], edges[i][1])) {
                    ok = false;
                    break;
                }
            }
            if (ok) {
                int root = find(1);
                boolean all = true;
                for (int i = 2; i <= n; ++i) {
                    if (find(i) != root) {
                        all = false;
                        break;
                    }
                }
                if (all) {
                    return edges[del];
                }
            }
        }
        return new int[0];
    }

    private static boolean eq(int[] a, int[] b) {
        return a[0] == b[0] && a[1] == b[1];
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        int[] r;
        r = sol.findRedundantConnection(new int[][] {{1, 2}, {1, 3}, {2, 3}});
        ++total; if (eq(r, new int[] {2, 3})) ++pass; else System.out.println("FAIL test1: [" + r[0] + "," + r[1] + "]");
        r = sol.findRedundantConnection(new int[][] {{1, 2}, {2, 3}, {3, 4}, {1, 4}, {1, 5}});
        ++total; if (eq(r, new int[] {1, 4})) ++pass; else System.out.println("FAIL test2: [" + r[0] + "," + r[1] + "]");
        r = sol.findRedundantConnection(new int[][] {{1, 2}, {2, 3}, {3, 1}});
        ++total; if (eq(r, new int[] {3, 1})) ++pass; else System.out.println("FAIL test3: [" + r[0] + "," + r[1] + "]");
        // 解法二
        r = sol.findRedundantConnection2(new int[][] {{1, 2}, {1, 3}, {2, 3}});
        ++total; if (eq(r, new int[] {2, 3})) ++pass; else System.out.println("FAIL test4");
        r = sol.findRedundantConnection2(new int[][] {{1, 2}, {2, 3}, {3, 4}, {1, 4}, {1, 5}});
        ++total; if (eq(r, new int[] {1, 4})) ++pass; else System.out.println("FAIL test5");
        r = sol.findRedundantConnection2(new int[][] {{1, 2}, {2, 3}, {3, 1}});
        ++total; if (eq(r, new int[] {3, 1})) ++pass; else System.out.println("FAIL test6");
        if (pass == total) {
            System.out.println("== 0684 PASS ==");
        } else {
            System.out.println("== 0684 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

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

685. Redundant Connection II(冗余连接 II)

题目描述

在本问题中,有根树指满足以下条件的有向图。该树只有一个根节点,所有其他节点都是该根节点的后继。
该树除了根节点之外的每一个节点都有且只有一个父节点,而根节点没有父节点。
输入一个有向图,该图由一个有着 n 个节点(节点值不重复,从 1 到 n)的树及一条附加的有向边构成。
附加的边包含在 1 到 n 中的两个不同顶点间,这条附加的边不属于树中已存在的边。
结果图是一个以边组成的二维数组 edges。每个元素是一对 [ui, vi],用以表示有向图中连接顶点
ui 和顶点 vi 的边,其中 ui 是 vi 的一个父节点。
返回一条能删除的边,使得剩下的图是有 n 个节点的有根树。若有多个答案,返回最后出现在给定二维数组
的答案。

示例 1

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

示例 2

  • 输入:edges = [[1,2],[2,3],[3,4],[4,1],[1,5]]
  • 输出:[4,1]
    提示:
  • n == edges.length
  • 3 <= n <= 1000
  • edges[i].length == 2
  • 1 <= ui, vi <= n

解题思路

有向图多一条边可能造成两种异常:

  1. 某个点入度为 2(候选边为两条指向该点的边);
  2. 存在环(候选边为参与成环的边)。
    解法一(并查集分类处理,主提交):
  • 先统计每个节点的入度,找出唯一入度为 2 的节点,记下指向它的两条边 cand1(较早)、cand2(较晚)。
  • 先把 cand2 暂时不加入并查集,用其余边跑并查集:
  • 若并查集无环,说明删 cand2 可行,返回 cand2;
  • 若有环,说明 cand2 不能删,返回 cand1。
  • 若没有入度为 2 的节点(纯环情形),用无向并查集找形成环的那条边返回(同 684)。
    时间复杂度 O(n·α(n)),空间复杂度 O(n)。
    解法二(枚举删除每条边后验证):从后往前枚举删除的边,删除后检查剩余图是否满足
    “每个节点入度 ≤1、无环、恰好一个根”。取第一个满足的边返回。
    时间复杂度 O(n²·α(n)),空间复杂度 O(n)。

代码实现

import java.util.Arrays;

class Solution {

    private int[] parent;

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

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

    private void init(int n) {
        parent = new int[n + 1];
        for (int i = 1; i <= n; ++i) {
            parent[i] = i;
        }
    }

    // 解法一:入度 + 并查集
    public int[] findRedundantDirectedConnection(int[][] edges) {
        int n = edges.length;
        // 记录每个节点的入度,找出入度为 2 的点 dup
        int[] indeg = new int[n + 1];
        int dup = -1;
        for (int[] e : edges) {
            ++indeg[e[1]];
            if (indeg[e[1]] == 2) {
                dup = e[1];
            }
        }
        if (dup != -1) {
            // 收集指向 dup 的两条边(按出现顺序)
            int first = -1, second = -1;
            for (int i = 0; i < n; ++i) {
                if (edges[i][1] == dup) {
                    if (first == -1) {
                        first = i;
                    } else {
                        second = i;
                    }
                }
            }
            int[] cand1 = edges[first];
            int[] cand2 = edges[second];
            // 尝试删掉 cand2(较晚那条),其余边跑并查集
            init(n);
            boolean circle = false;
            for (int i = 0; i < n; ++i) {
                if (i == second) {
                    continue;
                }
                if (!union(edges[i][0], edges[i][1])) {
                    circle = true;
                    break;
                }
            }
            if (!circle) {
                return cand2;
            }
            return cand1;
        }
        // 无入度为 2:纯环,按无向处理
        init(n);
        for (int[] e : edges) {
            if (!union(e[0], e[1])) {
                return e;
            }
        }
        return new int[0];
    }

    // 解法二:枚举删除每条边验证
    public int[] findRedundantDirectedConnection2(int[][] edges) {
        int n = edges.length;
        for (int del = n - 1; del >= 0; --del) {
            init(n);
            int[] indeg = new int[n + 1];
            boolean ok = true;
            for (int i = 0; i < n; ++i) {
                if (i == del) {
                    continue;
                }
                ++indeg[edges[i][1]];
                if (indeg[edges[i][1]] > 1) {
                    ok = false;
                    break;
                }
                if (!union(edges[i][0], edges[i][1])) {
                    ok = false;
                    break;
                }
            }
            if (!ok) {
                continue;
            }
            // 根:入度为 0 的节点恰一个
            int roots = 0;
            for (int i = 1; i <= n; ++i) {
                if (indeg[i] == 0) {
                    ++roots;
                }
            }
            if (roots == 1) {
                return edges[del];
            }
        }
        return new int[0];
    }

    private static boolean eq(int[] a, int[] b) {
        return a[0] == b[0] && a[1] == b[1];
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        int[] r;
        r = sol.findRedundantDirectedConnection(new int[][] {{1, 2}, {1, 3}, {2, 3}});
        ++total; if (eq(r, new int[] {2, 3})) ++pass; else System.out.println("FAIL test1: [" + r[0] + "," + r[1] + "]");
        r = sol.findRedundantDirectedConnection(new int[][] {{1, 2}, {2, 3}, {3, 4}, {4, 1}, {1, 5}});
        ++total; if (eq(r, new int[] {4, 1})) ++pass; else System.out.println("FAIL test2: [" + r[0] + "," + r[1] + "]");
        r = sol.findRedundantDirectedConnection(new int[][] {{2, 1}, {3, 1}, {4, 2}, {1, 4}});
        ++total; if (eq(r, new int[] {2, 1})) ++pass; else System.out.println("FAIL test3: [" + r[0] + "," + r[1] + "]");
        r = sol.findRedundantDirectedConnection(new int[][] {{1, 2}, {2, 3}, {3, 1}});
        ++total; if (eq(r, new int[] {3, 1})) ++pass; else System.out.println("FAIL test4: [" + r[0] + "," + r[1] + "]");
        // 解法二
        r = sol.findRedundantDirectedConnection2(new int[][] {{1, 2}, {1, 3}, {2, 3}});
        ++total; if (eq(r, new int[] {2, 3})) ++pass; else System.out.println("FAIL test5");
        r = sol.findRedundantDirectedConnection2(new int[][] {{1, 2}, {2, 3}, {3, 4}, {4, 1}, {1, 5}});
        ++total; if (eq(r, new int[] {4, 1})) ++pass; else System.out.println("FAIL test6");
        r = sol.findRedundantDirectedConnection2(new int[][] {{2, 1}, {3, 1}, {4, 2}, {1, 4}});
        ++total; if (eq(r, new int[] {2, 1})) ++pass; else System.out.println("FAIL test7");
        r = sol.findRedundantDirectedConnection2(new int[][] {{1, 2}, {2, 3}, {3, 1}});
        ++total; if (eq(r, new int[] {3, 1})) ++pass; else System.out.println("FAIL test8");
        if (pass == total) {
            System.out.println("== 0685 PASS ==");
        } else {
            System.out.println("== 0685 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

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

686. Repeated String Match(重复叠加字符串匹配)

题目描述

给定两个字符串 a 和 b,寻找重复叠加字符串 a 的最小次数,使得字符串 b 成为叠加后的
字符串 a 的子串,如果不存在则返回 -1。
注意:字符串 "abc" 重复叠加 0 次是 "",重复叠加 1 次是 "abc",重复叠加 2 次是 "abcabc"。

示例 1

  • 输入:a = "abcd", b = "cdabcdab"
  • 输出:3
  • 解释:a 重复叠加三遍后为 "abcdabcdabcd",此时 b 是其子串。

示例 2

  • 输入:a = "a", b = "aa"
  • 输出:2

示例 3

  • 输入:a = "a", b = "a"
  • 输出:1

示例 4

  • 输入:a = "abc", b = "wxyz"
  • 输出:-1
    提示:
  • 1 <= a.length <= 10^4
  • 1 <= b.length <= 10^4
  • a 和 b 由小写英文字母组成

解题思路

解法一(拼接判断,主提交):a 至少重复 ceil(b.length / a.length) 次才可能包含 b。
为了覆盖跨边界匹配,再多复制 1 次,即检查次数 times 与 times+1 两种情况;
如果 b 在其中一种拼接串中出现,返回对应次数,否则 -1。
直接用 String.indexOf(b) 判断子串。时间复杂度 O((times·|a|)·|b|),空间复杂度 O(|a|·times)。
解法二(KMP 优化):重复叠加字符串是周期串,用 KMP 在周期串中找 b,避免每次 indexOf
线性扫。实现上把 a 拼接 ceil(|b|/|a|)+1 次后跑 KMP 匹配,找到最小叠加次数。
时间复杂度 O(|a|·times + |b|),空间复杂度 O(|b|)。

代码实现

class Solution {

    // 解法一:拼接 + indexOf
    public int repeatedStringMatch(String a, String b) {
        int times = (b.length() + a.length() - 1) / a.length();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < times; ++i) {
            sb.append(a);
        }
        if (sb.indexOf(b) != -1) {
            return times;
        }
        sb.append(a);
        if (sb.indexOf(b) != -1) {
            return times + 1;
        }
        return -1;
    }

    // 解法二:KMP 匹配
    public int repeatedStringMatch2(String a, String b) {
        int times = (b.length() + a.length() - 1) / a.length();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < times + 1; ++i) {
            sb.append(a);
        }
        String s = sb.toString();
        int idx = kmp(s, b);
        if (idx == -1) {
            return -1;
        }
        // 匹配起始于 idx,覆盖到 idx + |b|,需要的叠加次数上限为 times+1
        int need = (idx + b.length() + a.length() - 1) / a.length();
        return need <= times + 1 ? need : -1;
    }

    private int kmp(String text, String pat) {
        int n = text.length(), m = pat.length();
        int[] next = new int[m];
        int j = 0;
        for (int i = 1; i < m; ++i) {
            while (j > 0 && pat.charAt(i) != pat.charAt(j)) {
                j = next[j - 1];
            }
            if (pat.charAt(i) == pat.charAt(j)) {
                ++j;
            }
            next[i] = j;
        }
        j = 0;
        for (int i = 0; i < n; ++i) {
            while (j > 0 && text.charAt(i) != pat.charAt(j)) {
                j = next[j - 1];
            }
            if (text.charAt(i) == pat.charAt(j)) {
                ++j;
            }
            if (j == m) {
                return i - m + 1;
            }
        }
        return -1;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (sol.repeatedStringMatch("abcd", "cdabcdab") == 3) ++pass; else System.out.println("FAIL test1: " + sol.repeatedStringMatch("abcd", "cdabcdab"));
        ++total; if (sol.repeatedStringMatch("a", "aa") == 2) ++pass; else System.out.println("FAIL test2: " + sol.repeatedStringMatch("a", "aa"));
        ++total; if (sol.repeatedStringMatch("a", "a") == 1) ++pass; else System.out.println("FAIL test3: " + sol.repeatedStringMatch("a", "a"));
        ++total; if (sol.repeatedStringMatch("abc", "wxyz") == -1) ++pass; else System.out.println("FAIL test4: " + sol.repeatedStringMatch("abc", "wxyz"));
        ++total; if (sol.repeatedStringMatch("ab", "babab") == 3) ++pass; else System.out.println("FAIL test5: " + sol.repeatedStringMatch("ab", "babab"));
        ++total; if (sol.repeatedStringMatch("bb", "bbbb") == 2) ++pass; else System.out.println("FAIL test6: " + sol.repeatedStringMatch("bb", "bbbb"));
        // KMP 解法
        ++total; if (sol.repeatedStringMatch2("abcd", "cdabcdab") == 3) ++pass; else System.out.println("FAIL test7");
        ++total; if (sol.repeatedStringMatch2("a", "aa") == 2) ++pass; else System.out.println("FAIL test8");
        ++total; if (sol.repeatedStringMatch2("abc", "wxyz") == -1) ++pass; else System.out.println("FAIL test9");
        ++total; if (sol.repeatedStringMatch2("ab", "babab") == 3) ++pass; else System.out.println("FAIL test10");
        if (pass == total) {
            System.out.println("== 0686 PASS ==");
        } else {
            System.out.println("== 0686 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(a·times·b)O(a·times)
解法二O(a·times +b)O(b)

687. Longest Univalue Path(最长同值路径)

题目描述

给定一个二叉树的 root,返回最长的路径的长度,这个路径中的每个节点具有相同值。 这条路径可以经过也可以不经过根节点。 两个节点之间的路径长度由它们之间的边数表示。

示例 1

  • 输入:root = [5,4,5,1,1,5]
  • 输出:2

示例 2

  • 输入:root = [1,4,5,4,4,5]
  • 输出:2 提示:
  • 树的节点数的范围是 [0, 10^4]
  • -1000 <= Node.val <= 1000
  • 树的深度将不超过 1000

解题思路

解法一(DFS 返回单侧最长同值边长,主提交):dfs(node) 返回从 node 出发向下延伸的 最长同值路径边数(只走单侧)。对每个节点:

  • 左孩子同值则 leftPath = dfs(left) + 1,否则 0;
  • 右孩子同值则 rightPath = dfs(right) + 1,否则 0;
  • 用 leftPath + rightPath 更新全局答案(路径可在该节点拐弯);
  • 向上返回 max(leftPath, rightPath)(只能带一侧)。 时间复杂度 O(n),空间复杂度 O(height)。 解法二(后序迭代 + 栈记录):用显式栈模拟后序,同样维护每节点单侧最长值。 时间复杂度 O(n),空间复杂度 O(n)。

代码实现

class Solution {

    private static class TreeNode {
        int val;
        TreeNode left;
        TreeNode right;
        TreeNode(int x) { val = x; }
    }

    private int ans;

    // 解法一:DFS
    public int longestUnivaluePath(TreeNode root) {
        ans = 0;
        dfs(root);
        return ans;
    }

    private int dfs(TreeNode root) {
        if (root == null) {
            return 0;
        }
        int left = dfs(root.left);
        int right = dfs(root.right);
        int leftPath = 0, rightPath = 0;
        if (root.left != null && root.left.val == root.val) {
            leftPath = left + 1;
        }
        if (root.right != null && root.right.val == root.val) {
            rightPath = right + 1;
        }
        ans = Math.max(ans, leftPath + rightPath);
        return Math.max(leftPath, rightPath);
    }

    // 解法二:显式后序迭代
    public int longestUnivaluePath2(TreeNode root) {
        if (root == null) {
            return 0;
        }
        ans = 0;
        java.util.Map<TreeNode, int[]> memo = new java.util.HashMap<>();
        java.util.Deque<TreeNode> st = new java.util.ArrayDeque<>();
        st.push(root);
        java.util.Deque<TreeNode> post = new java.util.ArrayDeque<>();
        while (!st.isEmpty()) {
            TreeNode cur = st.pop();
            post.push(cur);
            if (cur.left != null) {
                st.push(cur.left);
            }
            if (cur.right != null) {
                st.push(cur.right);
            }
        }
        while (!post.isEmpty()) {
            TreeNode cur = post.pop();
            int lp = 0, rp = 0;
            if (cur.left != null && cur.left.val == cur.val) {
                lp = memo.getOrDefault(cur.left, new int[] {0})[0] + 1;
            }
            if (cur.right != null && cur.right.val == cur.val) {
                rp = memo.getOrDefault(cur.right, new int[] {0})[0] + 1;
            }
            ans = Math.max(ans, lp + rp);
            memo.put(cur, new int[] {Math.max(lp, rp)});
        }
        return ans;
    }

    // 层序构建(测试辅助)
    private static TreeNode build(Integer[] arr) {
        if (arr == null || arr.length == 0) {
            return null;
        }
        TreeNode root = new TreeNode(arr[0]);
        java.util.Queue<TreeNode> q = new java.util.LinkedList<>();
        q.offer(root);
        int i = 1;
        while (!q.isEmpty() && i < arr.length) {
            TreeNode cur = q.poll();
            if (arr[i] != null) {
                cur.left = new TreeNode(arr[i]);
                q.offer(cur.left);
            }
            ++i;
            if (i < arr.length && arr[i] != null) {
                cur.right = new TreeNode(arr[i]);
                q.offer(cur.right);
            }
            ++i;
        }
        return root;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        TreeNode t1 = build(new Integer[] {5, 4, 5, 1, 1, 5});
        ++total; if (sol.longestUnivaluePath(t1) == 2) ++pass; else System.out.println("FAIL test1: " + sol.longestUnivaluePath(t1));
        TreeNode t2 = build(new Integer[] {1, 4, 5, 4, 4, 5});
        ++total; if (sol.longestUnivaluePath(t2) == 2) ++pass; else System.out.println("FAIL test2: " + sol.longestUnivaluePath(t2));
        TreeNode t3 = build(new Integer[] {1});
        ++total; if (sol.longestUnivaluePath(t3) == 0) ++pass; else System.out.println("FAIL test3: " + sol.longestUnivaluePath(t3));
        TreeNode t4 = build(new Integer[] {1, 1});
        ++total; if (sol.longestUnivaluePath(t4) == 1) ++pass; else System.out.println("FAIL test4: " + sol.longestUnivaluePath(t4));
        TreeNode t5 = build(new Integer[] {1, 1, 1, 1, 1, 1, 1});
        ++total; if (sol.longestUnivaluePath(t5) == 4) ++pass; else System.out.println("FAIL test5: " + sol.longestUnivaluePath(t5));
        // 解法二
        ++total; if (sol.longestUnivaluePath2(t1) == 2) ++pass; else System.out.println("FAIL test6");
        ++total; if (sol.longestUnivaluePath2(t2) == 2) ++pass; else System.out.println("FAIL test7");
        ++total; if (sol.longestUnivaluePath2(t3) == 0) ++pass; else System.out.println("FAIL test8");
        ++total; if (sol.longestUnivaluePath2(t5) == 4) ++pass; else System.out.println("FAIL test9");
        if (pass == total) {
            System.out.println("== 0687 PASS ==");
        } else {
            System.out.println("== 0687 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

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

688. Knight Probability in Chessboard(骑士在棋盘上的概率)

题目描述

在一个 n x n 的国际象棋棋盘上,一个骑士从单元格 (row, column) 开始,并尝试进行 k 次移动。 行和列是从 0 开始的,所以左上单元格是 (0,0),右下单元格是 (n-1, n-1)。 象棋骑士有 8 种可能的走法。每次移动在基本方向上是两个单元格,然后在正交方向上是一个单元格。 每次骑士要移动时,它都会随机从 8 种可能的移动中选择一种(即使棋子会离开棋盘),然后移动到那里。 骑士继续移动,直到它走了 k 步或离开了棋盘。 返回骑士在棋盘停止移动后仍留在棋盘上的概率。

示例 1

  • 输入:n = 3, k = 2, row = 0, column = 0
  • 输出:0.0625

示例 2

  • 输入:n = 1, k = 0, row = 0, column = 0
  • 输出:1.00000 提示:
  • 1 <= n <= 25
  • 0 <= k <= 100
  • 0 <= row, column <= n - 1

解题思路

解法一(三维 DP,主提交):f[h][i][j] 表示从 (i, j) 再走 h 步仍留在棋盘上的概率。 边界 f[0][i][j] = 1。递推:f[h][i][j] = sum(f[h-1][ni][nj]) / 8,(ni, nj) 为 8 个 合法落点(越界则不算概率,因为已经离开棋盘)。答案为 f[k][row][column]。 用滚动数组优化为两层。时间复杂度 O(k·n²·8),空间复杂度 O(n²)。 解法二(记忆化递归):memo[step][i][j] 存从 (i,j) 走 step 步留在棋上的概率,递归自顶向下。 时间复杂度 O(k·n²·8),空间复杂度 O(k·n²)。

代码实现

class Solution {

    private static final int[][] DIRS = {
        {1, 2}, {1, -2}, {-1, 2}, {-1, -2},
        {2, 1}, {2, -1}, {-2, 1}, {-2, -1}
    };

    // 解法一:滚动 DP
    public double knightProbability(int n, int k, int row, int column) {
        double[][] dp = new double[n][n];
        for (int i = 0; i < n; ++i) {
            java.util.Arrays.fill(dp[i], 1.0);
        }
        for (int step = 1; step <= k; ++step) {
            double[][] ndp = new double[n][n];
            for (int i = 0; i < n; ++i) {
                for (int j = 0; j < n; ++j) {
                    double p = 0;
                    for (int[] d : DIRS) {
                        int ni = i + d[0], nj = j + d[1];
                        if (ni >= 0 && ni < n && nj >= 0 && nj < n) {
                            p += dp[ni][nj] / 8.0;
                        }
                    }
                    ndp[i][j] = p;
                }
            }
            dp = ndp;
        }
        return dp[row][column];
    }

    // 解法二:记忆化递归
    private double[][][] memo;
    private int N;

    public double knightProbability2(int n, int k, int row, int column) {
        N = n;
        memo = new double[k + 1][n][n];
        for (int s = 0; s <= k; ++s) {
            for (int i = 0; i < n; ++i) {
                java.util.Arrays.fill(memo[s][i], -1.0);
            }
        }
        return prob(k, row, column);
    }

    private double prob(int step, int i, int j) {
        if (i < 0 || i >= N || j < 0 || j >= N) {
            return 0.0;
        }
        if (step == 0) {
            return 1.0;
        }
        if (memo[step][i][j] >= 0) {
            return memo[step][i][j];
        }
        double p = 0;
        for (int[] d : DIRS) {
            p += prob(step - 1, i + d[0], j + d[1]) / 8.0;
        }
        memo[step][i][j] = p;
        return p;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (Math.abs(sol.knightProbability(3, 2, 0, 0) - 0.0625) < 1e-9) ++pass; else System.out.println("FAIL test1: " + sol.knightProbability(3, 2, 0, 0));
        ++total; if (Math.abs(sol.knightProbability(1, 0, 0, 0) - 1.0) < 1e-9) ++pass; else System.out.println("FAIL test2: " + sol.knightProbability(1, 0, 0, 0));
        ++total; if (Math.abs(sol.knightProbability(3, 0, 0, 0) - 1.0) < 1e-9) ++pass; else System.out.println("FAIL test3");
        ++total; if (Math.abs(sol.knightProbability(8, 30, 6, 4) - 0.00019) < 1e-4) ++pass; else System.out.println("FAIL test4: " + sol.knightProbability(8, 30, 6, 4));
        ++total; if (Math.abs(sol.knightProbability(2, 1, 0, 0) - 0.0) < 1e-9) ++pass; else System.out.println("FAIL test5: " + sol.knightProbability(2, 1, 0, 0));
        // 解法二
        ++total; if (Math.abs(sol.knightProbability2(3, 2, 0, 0) - 0.0625) < 1e-9) ++pass; else System.out.println("FAIL test6");
        ++total; if (Math.abs(sol.knightProbability2(1, 0, 0, 0) - 1.0) < 1e-9) ++pass; else System.out.println("FAIL test7");
        ++total; if (Math.abs(sol.knightProbability2(8, 30, 6, 4) - 0.00019) < 1e-4) ++pass; else System.out.println("FAIL test8");
        if (pass == total) {
            System.out.println("== 0688 PASS ==");
        } else {
            System.out.println("== 0688 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

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

689. Maximum Sum of 3 Non-Overlapping Subarrays(三个无重叠子数组的最大和)

题目描述

给你一个整数数组 nums 和一个整数 k,找出三个长度为 k、互不重叠、且全部数字和最大的 子数组,并返回这三个子数组。 以下标的数组形式返回结果,数组中的每一项分别指示每个子数组的起始位置(下标从 0 开始)。 如果有多个结果,返回字典序最小的一个。

示例 1

  • 输入:nums = [1,2,1,2,6,7,5,1], k = 2
  • 输出:[0,3,5]
  • 解释:子数组 [1,2]、[2,6]、[7,5] 对应的起始下标为 [0,3,5]。也可以取 [2,1],但是结果 [1,3,5] 在字典序上更大。

示例 2

  • 输入:nums = [1,2,1,2,1,2,1,2,1], k = 2
  • 输出:[0,2,4] 提示:
  • 1 <= nums.length <= 2 * 10^4
  • 1 <= nums[i] < 2^16
  • 1 <= k <= floor(nums.length / 3)

解题思路

解法一(滑动窗口,主提交):把三个长度为 k 的窗口看成三根同步右移的指针。维护: 第一段扫描到的最大值 mx1 及其起点 idx1;前两段组合的最大值 mx12 及其两个起点; 再与当前第三段和 s3 相加得到总最大值。每次只比较不相等时才更新(相等时保留靠左的旧值), 从而保证字典序最小。时间复杂度 O(n),空间复杂度 O(1)。 解法二(前缀和 + 预处理左右最佳段):用前缀和快速求任意长度为 k 的子数组和;从左往右 预处理 pre[i]([0, i] 范围内最佳 k 段的和与起点),从右往左预处理 suf[i] ([i, n) 范围内最佳 k 段的和与起点);枚举中间段起点 i,答案由 pre[i-1] + 中间段 + suf[i+k] 组合而成。时间复杂度 O(n),空间复杂度 O(n)。

代码实现

import java.util.Arrays;

class Solution {

    // 解法一:滑动窗口
    public int[] maxSumOfThreeSubarrays(int[] nums, int k) {
        int n = nums.length;
        int[] ans = new int[3];
        int s = 0, s1 = 0, s2 = 0, s3 = 0;
        int mx1 = 0, mx12 = 0;
        int idx1 = 0, idx121 = 0, idx122 = 0;
        for (int i = k * 2; i < n; ++i) {
            s1 += nums[i - k * 2];
            s2 += nums[i - k];
            s3 += nums[i];
            if (i >= k * 3 - 1) {
                if (s1 > mx1) {
                    mx1 = s1;
                    idx1 = i - k * 3 + 1;
                }
                if (mx1 + s2 > mx12) {
                    mx12 = mx1 + s2;
                    idx121 = idx1;
                    idx122 = i - k * 2 + 1;
                }
                if (mx12 + s3 > s) {
                    s = mx12 + s3;
                    ans[0] = idx121;
                    ans[1] = idx122;
                    ans[2] = i - k + 1;
                }
                s1 -= nums[i - k * 3 + 1];
                s2 -= nums[i - k * 2 + 1];
                s3 -= nums[i - k + 1];
            }
        }
        return ans;
    }

    // 解法二:前缀和 + 左右最佳段
    public int[] maxSumOfThreeSubarrays2(int[] nums, int k) {
        int n = nums.length;
        int[] s = new int[n + 1];
        for (int i = 0; i < n; ++i) {
            s[i + 1] = s[i] + nums[i];
        }
        // pre[i] : [0, i] 内最佳 k 段 [和, 起点]
        int[][] pre = new int[n][2];
        for (int i = 0, t = 0, idx = 0; i < n - k + 1; ++i) {
            int cur = s[i + k] - s[i];
            if (cur > t) {
                t = cur;
                idx = i;
            }
            pre[i + k - 1][0] = t;
            pre[i + k - 1][1] = idx;
        }
        // suf[i] : [i, n) 内最佳 k 段 [和, 起点]
        int[][] suf = new int[n][2];
        for (int i = n - k, t = 0, idx = 0; i >= 0; --i) {
            int cur = s[i + k] - s[i];
            if (cur >= t) {
                t = cur;
                idx = i;
            }
            suf[i][0] = t;
            suf[i][1] = idx;
        }
        int[] ans = new int[3];
        int best = 0;
        for (int i = k; i < n - 2 * k + 1; ++i) {
            int cur = s[i + k] - s[i] + pre[i - 1][0] + suf[i + k][0];
            if (cur > best) {
                best = cur;
                ans[0] = pre[i - 1][1];
                ans[1] = i;
                ans[2] = suf[i + k][1];
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (Arrays.equals(sol.maxSumOfThreeSubarrays(new int[]{1, 2, 1, 2, 6, 7, 5, 1}, 2), new int[]{0, 3, 5})) ++pass; else System.out.println("FAIL test1: " + Arrays.toString(sol.maxSumOfThreeSubarrays(new int[]{1, 2, 1, 2, 6, 7, 5, 1}, 2)));
        ++total; if (Arrays.equals(sol.maxSumOfThreeSubarrays(new int[]{1, 2, 1, 2, 1, 2, 1, 2, 1}, 2), new int[]{0, 2, 4})) ++pass; else System.out.println("FAIL test2: " + Arrays.toString(sol.maxSumOfThreeSubarrays(new int[]{1, 2, 1, 2, 1, 2, 1, 2, 1}, 2)));
        ++total; if (Arrays.equals(sol.maxSumOfThreeSubarrays(new int[]{4, 3, 2, 1}, 1), new int[]{0, 1, 2})) ++pass; else System.out.println("FAIL test3: " + Arrays.toString(sol.maxSumOfThreeSubarrays(new int[]{4, 3, 2, 1}, 1)));
        // 解法二
        ++total; if (Arrays.equals(sol.maxSumOfThreeSubarrays2(new int[]{1, 2, 1, 2, 6, 7, 5, 1}, 2), new int[]{0, 3, 5})) ++pass; else System.out.println("FAIL test4: " + Arrays.toString(sol.maxSumOfThreeSubarrays2(new int[]{1, 2, 1, 2, 6, 7, 5, 1}, 2)));
        ++total; if (Arrays.equals(sol.maxSumOfThreeSubarrays2(new int[]{1, 2, 1, 2, 1, 2, 1, 2, 1}, 2), new int[]{0, 2, 4})) ++pass; else System.out.println("FAIL test5: " + Arrays.toString(sol.maxSumOfThreeSubarrays2(new int[]{1, 2, 1, 2, 1, 2, 1, 2, 1}, 2)));
        ++total; if (Arrays.equals(sol.maxSumOfThreeSubarrays2(new int[]{4, 3, 2, 1}, 1), new int[]{0, 1, 2})) ++pass; else System.out.println("FAIL test6: " + Arrays.toString(sol.maxSumOfThreeSubarrays2(new int[]{4, 3, 2, 1}, 1)));
        if (pass == total) {
            System.out.println("== 0689 PASS ==");
        } else {
            System.out.println("== 0689 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

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