在问 · 算法题解

678. 有效的括号字符串

中等贪心贪心维护未匹配左括号数的范围力扣原题 ↗

题目描述

输入字符串 s,它只由 '('、')' 和 '*' 三种字符组成。

每个 '*' 可以任意当作一个左括号、一个右括号,或者当作空串去掉。

判断能否通过为每个 '*' 选定含义,使 s 成为合法的括号串:每个左括号都有一个位于它右侧的右括号与之配对,每个右括号也都有一个位于它左侧的左括号与之配对。能则返回 true,否则返回 false。

示例 1
输入:s = "()"
输出:true
示例 2
输入:s = "(*)"
输出:true

解释:把 '*' 当作空串即可。

示例 3
输入:s = "(*))"
输出:true

解释:把 '*' 当作左括号,得到 "(())"。

示例 4
输入:s = "("
输出:false

解释:只有一个左括号,无法配对。

数据范围

  • 1 <= s.length <= 100
  • s[i] 是 '('、')' 或 '*'。

解题思路:贪心维护未匹配左括号数的范围

维护“未匹配左括号数”的可能范围 [lo, hi]。遇到 "(" 两端都加 1;遇到 ")" 两端都减 1;遇到 "*" 时 lo 减 1(当作 ")")、hi 加 1(当作 "(")。如果 hi < 0,说明即使把所有 * 当作 "(" 右括号也太多,直接失败;未匹配数不能为负,所以把 lo 截到 0。结束时 0 在范围内(lo == 0)即可。

为什么正确

读完前缀后,[lo, hi] 中的每个整数都是某种 * 的取法下的未匹配左括号数,且范围之外的值都不可达。

复杂度分析

时间 O(n),空间 O(1)。

易错点

必须先检查 hi < 0 再截断 lo;如果忘了截断 lo,"*(" 这类输入会被误判。最后要检查 lo == 0,而不是 hi == 0。

参考代码

Python、Java、C++、Go、JavaScript、TypeScript 六种语言,逻辑一致。

class Solution:
    def checkValidString(self, s: str) -> bool:
        lo = hi = 0
        for ch in s:
            if ch == '(':
                lo += 1
                hi += 1
            elif ch == ')':
                lo -= 1
                hi -= 1
            else:
                lo -= 1
                hi += 1
            if hi < 0:
                return False
            lo = max(lo, 0)
        return lo == 0

载入编辑器,自己跑一遍 →

其他解法

区间动态规划按定义枚举,最慢;双栈把星号当作备用括号,按位置匹配;贪心只记未匹配左括号数的上下界,常数空间。

解法 2:双栈 时间 O(n),空间 O(n)

用两个栈分别存放左括号和星号的下标。遇到右括号时,优先弹出一个左括号;没有左括号就弹出一个星号;都没有就无效。遍历结束后,剩下的左括号要用其右侧的星号当作右括号来匹配:两栈同时弹出,若左括号下标大于星号下标就无效。最后左括号栈为空即有效。

沿用普通括号匹配的栈思路,容易理解。需要 O(n) 空间存下标。

class Solution:
    def checkValidString(self, s: str) -> bool:
        left, star = [], []
        for i, ch in enumerate(s):
            if ch == '(':
                left.append(i)
            elif ch == '*':
                star.append(i)
            elif left:
                left.pop()
            elif star:
                star.pop()
            else:
                return False
        while left and star:
            if left.pop() > star.pop():
                return False
        return not left

解法 3:区间动态规划 时间 O(n³),空间 O(n²)

dp[i][j] 表示 s[i..j] 能否成为有效括号串。单个星号有效;长度为 2 时,左字符是 ( 或 *、右字符是 ) 或 * 就有效。更长的区间有两种来源:两端可以配成一对且 dp[i + 1][j − 1] 为真;或者存在分割点 k,使 dp[i][k] 和 dp[k + 1][j] 都为真。

直接对应“有效串 = 外层包一对括号或两串拼接”的定义,容易证明正确。时间 O(n³),只适合本题 n ≤ 100 的规模。

class Solution:
    def checkValidString(self, s: str) -> bool:
        n = len(s)
        dp = [[False] * n for _ in range(n)]
        for i in range(n):
            dp[i][i] = s[i] == '*'
        for i in range(1, n):
            dp[i - 1][i] = s[i - 1] in '(*' and s[i] in ')*'
        for i in range(n - 3, -1, -1):
            for j in range(i + 2, n):
                if s[i] in '(*' and s[j] in ')*' and dp[i + 1][j - 1]:
                    dp[i][j] = True
                    continue
                for k in range(i, j):
                    if dp[i][k] and dp[k + 1][j]:
                        dp[i][j] = True
                        break
        return dp[0][n - 1]

贪心 专题的其他题目

查看 贪心 全部题目 →