目录

题目描述

678. 有效的括号字符串

image-20250510230114043

image-20250510230129200

题意分析

给定只含 ()* 三种字符的字符串,其中每个 * 可以独立地被解释成左括号、右括号或空字符串,问是否存在某一组解释,使整个字符串成为合法的括号序列。

合法的含义有两条:任意前缀中右括号数量都不超过左括号数量;整串左右括号数量相等。第一条是逐位的约束,第二条是全局的约束,两条都必须满足。

约束里 n 不超过 100,量级很小,甚至允许「按位置与未匹配数量两个维度枚举全部可行状态」这种平方级做法通过;但 * 的数量也可能接近 100,把每个 * 的三种解释全部枚举出来是指数级,一定超时。题目只问「是否存在」,不要求输出任何一种具体的解释方案,这说明中间过程无需保存分配细节。

需要留意的边界情形:空串合法;单个 * 合法(当作空串);以 ) 开头必定不合法;*) 合法(* 当左括号);(* 也合法(* 当右括号);全为 * 的串无论长度奇偶都合法。

解法:贪心维护未匹配左括号范围

核心思路

问题关键: 每个 * 都有三种解释,逐个枚举需要 $O(3^k)$。判断括号是否合法其实只依赖一个状态:当前前缀还有多少个未匹配的左括号,并且这个数量在任何合法前缀中都不能为负。

为什么选择区间贪心: 扫描一个前缀后,可行的未匹配数量构成连续区间。确定的括号会让整个区间平移;* 可以让数量减一、不变或加一,仍不会产生空洞。因此不必保存所有方案,只维护最小值 low 和最大值 high,把 $O(n^2)$ 的状态 DP 压成一次扫描。

不变量: 扫描完当前前缀后,[low, high] 恰好表示所有仍满足前缀合法性的解释中,未匹配左括号数的可行范围。

转移规则如下:

  • 遇到 (low++high++
  • 遇到 )low--high--
  • 遇到 *:最少可当右括号,最多可当左括号,所以 low--high++

正确性: 上述转移覆盖了当前字符对区间两端的全部可能。若 high < 0,连未匹配数最多的方案都出现右括号过剩,说明不存在合法解释;若只有 low < 0,只是部分方案非法,将下界截为 0 即可。区间连续性保证截断后仍精确表示全部可行数量。扫描结束时 low == 0,等价于存在一种解释能把所有左括号恰好匹配完。

解题步骤

  1. 初始化 low = high = 0,表示空前缀的未匹配数为 0。
  2. 从左到右扫描,根据字符更新上下界。
  3. 每轮先检查 high < 0,若成立立即返回 false
  4. 再执行 low = max(low, 0),去掉未匹配数为负的非法方案。
  5. 扫描结束返回 low == 0

口述示例: "(*))" 的区间依次是 [1,1][0,2][0,1][0,0],最终存在配平方案,所以返回 true

边界与反例:) 开头会立刻令 high < 0;全是 * 时始终可以把它们当空串。不能只检查最终数量,例如 ")(" 的左右括号总数相等,但第一个前缀已经非法。

代码实现

class Solution {
    public boolean checkValidString(String s) {
        int low = 0;
        int high = 0;

        for (int i = 0; i < s.length(); i++) {
            char ch = s.charAt(i);
            if (ch == '(') {
                low++;
                high++;
            } else if (ch == ')') {
                low--;
                high--;
            } else {
                low--;
                high++;
            }

            if (high < 0) {
                return false;
            }
            low = Math.max(low, 0);
        }
        return low == 0;
    }
}
func checkValidString(s string) bool {
    low, high := 0, 0

    for i := 0; i < len(s); i++ {
        if s[i] == '(' {
            low++
            high++
        } else if s[i] == ')' {
            low--
            high--
        } else {
            low--
            high++
        }

        if high < 0 {
            return false
        }
        if low < 0 {
            low = 0
        }
    }
    return low == 0
}

复杂度分析

  • 时间复杂度为 $O(n)$,只从左到右扫描一次,每个字符做常数次运算。
  • 空间复杂度为 $O(1)$,只维护区间的两个端点。

关键点总结

  • low 表示“最少还可能剩多少左括号”,high 表示“最多还能剩多少左括号”。
  • high < 0 代表所有方案都失败;low < 0 只代表部分方案失败,二者不能同样处理。
  • 前缀合法性必须在每轮检查,后面的左括号不能补救前面已经多出的右括号。
  • 结束时检查 low == 0,因为只需存在一种能配平的解释,并不要求所有解释都配平。

易错点总结

  • low < 0 就返回 false:如 "*()",把 * 当空串即可合法。
  • 循环结束检查 high == 0:如 "(*",虽然 high > 0,仍可把 * 当右括号。
  • high < 0 的检查放到循环外:后续的 ( 可能掩盖此前已非法的前缀。
  • * 固定解释成一种括号:会丢掉另外两种可行选择。
  • 只统计三类字符的总数,不维护前缀:无法识别 ")(" 这类顺序错误。

相似题目

题目 难度 考察点
20. 有效的括号 简单 字符全部确定但有三种括号类型,必须用栈记录待匹配的具体符号,不存在可行区间
22. 括号生成 中等 要输出全部合法方案而非判定可行性,用回溯并靠左右括号剩余量剪枝
32. 最长有效括号 困难 求最长合法子串的长度,需要用栈保存下标或按位置做状态转移,不只是判合法
301. 删除无效的括号 困难 串中混有字母,要删最少字符并输出所有互不相同的结果,重点在先算删除数量再搜索去重
856. 括号的分数 中等 输入保证合法,重点变成沿嵌套层次累积分值,与合法性判断无关
1249. 移除无效的括号 中等 只需给出任意一个合法结果,两趟扫描标记待删下标后重建字符串即可
1614. 括号的最大嵌套深度 简单 输入已保证合法,只需记录计数器的历史最大值,没有任何不确定字符