题目描述

[!green]

牛客原题: ✅ 补充题 109. 使两类括号合法的最少插入次数

给你一个只包含 (、)、[、] 的字符串 s,请返回将它变为合法括号序列所需插入的最少括号数。

合法序列中的括号必须类型匹配、嵌套顺序正确。你只能插入括号,不能删除原字符或改变原字符的相对顺序。

示例 1:

输入: s = "([[])"
输出: 1
解释: 在最后的右圆括号前插入一个 ],得到 "([[]])"。

示例 2:

输入: s = "([)]"
输出: 2
解释: 可插入 ] 和 [,得到 "([])[]";原有字符的顺序不变。

示例 3:

输入: s = "()[]"
输出: 0
解释: 原字符串已经满足类型匹配和合法嵌套。

提示:

  • 字符串只含 (、)、[、]。
  • 仅允许插入括号,不允许删除或改变原字符顺序。
  • 配对类型及嵌套顺序均需合法。

题意分析

两类括号不仅数量要匹配,类型和嵌套顺序也要正确。只能插入意味着原字符的顺序必须保留,不能通过删除或重排来消除交叉。

最优补全可以按区间拆解:首尾配成一对时处理内部,否则考虑合法部分的拼接。相同子区间会在多种拆法中反复出现,因此用区间动态规划保存最少插入数。

解法:区间 DP 枚举配对与切分

核心思路

[!blue]

这里要优化的是“补成合法括号串的最少插入数”,只用栈判断现有串是否合法还不够。把问题缩小到连续区间:dp[l][r] 表示补全 s[l..r] 的最小插入数。

  • 一个括号缺少自己的另一半,所以 dp[i][i] = 1;空串为 0。
  • 若两端是同类型的一对,例如 ( 与 ),可以先让它们配对,代价等于内部区间;长度为 2 时内部为空,代价为 0。
  • 合法串也可能由两段合法串拼接而成。因此枚举切点 m,比较 dp[l][m] + dp[m+1][r]。不能只检查首尾,否则会漏掉 ()[] 这种并列结构。

按区间长度从短到长计算,内部区间和切开的两段都已经有答案。例如 ([)] 左右数量虽然平衡,但类型交叉,单纯计数无法决定补全费用;区间状态同时保留了顺序与类型关系。

解题步骤

  1. 空串直接返回 0,所有单字符区间初始化为 1。
  2. 按长度递增枚举 [l,r],先尝试把同类型的左右端括号配成一对。
  3. 枚举每个切点,将左右两段的最少插入数相加,与当前候选取较小值。
  4. 返回覆盖整串的 dp[0][n-1]。

代码实现

class Solution {
    public int minInsertions(String s) {
        int n = s.length();

        if (n == 0) {
            return 0;
        }

        int[][] dp = new int[n][n];

        for (int i = 0; i < n; i++) {
            dp[i][i] = 1;
        }

        for (int len = 2; len <= n; len++) {
            for (int l = 0; l + len <= n; l++) {
                int r = l + len - 1;

                dp[l][r] = n;
                char a = s.charAt(l);
                char b = s.charAt(r);

                if ((a == '(' && b == ')') || (a == '[' && b == ']')) {
                    dp[l][r] = len == 2 ? 0 : dp[l + 1][r - 1];
                }

                for (int m = l; m < r; m++) {
                    dp[l][r] = Math.min(dp[l][r], dp[l][m] + dp[m + 1][r]);
                }
            }
        }

        return dp[0][n - 1];
    }
}
func minInsertions(s string) int {
    n := len(s)
    if n == 0 {
        return 0
    }
    dp := make([][]int, n)
    for i := range dp {
        dp[i] = make([]int, n)
        dp[i][i] = 1
    }
    for length := 2; length <= n; length++ {
        for l := 0; l+length <= n; l++ {
            r := l + length - 1
            dp[l][r] = n
            if s[l] == '(' && s[r] == ')' || s[l] == '[' && s[r] == ']' {
                if length == 2 {
                    dp[l][r] = 0
                } else {
                    dp[l][r] = dp[l+1][r-1]
                }
            }
            for m := l; m < r; m++ {
                dp[l][r] = min(dp[l][r], dp[l][m]+dp[m+1][r])
            }
        }
    }
    return dp[0][n-1]
}

复杂度分析

  • 时间复杂度:$O(n^3)$,有 $O(n^2)$ 个区间,每个区间最多枚举 $O(n)$ 个切点。
  • 空间复杂度:$O(n^2)$,用于保存全部区间的最优值。

关键点总结

[!green]

一段平衡序列可由外层匹配括号包住内部,或由两段平衡序列拼接,这两类分解覆盖所有合法补全。

易错点总结

[!yellow]

只数左右括号数量无法处理交叉嵌套;不能把方括号和圆括号混配。

相似题目

题目 难度 关联与区别
1312. 让字符串成为回文串的最少插入次数 困难 都对字符串区间计算最少插入数,先判断两端能否配对再处理内部;括号还可能由两个独立合法区间拼接,因此本题需增加区间切分转移。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/47108670
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!