经典考题6 - 括号

361 阅读7分钟

知识点

  1. rolling state:遇到"(" +1,遇到")" -1,来判断是否平衡。中间任意时刻平衡被破坏(state < 0)都invalid
    • 不同类型括号 or 找invalid括号的index 时,需要用stack来辅助保存当下的状态
    • stack可以用来保存 x3(由近及远):
      • 不同类型的括号
      • 括号index
      • 平行的前一层的sum/score
  2. backtracking:括号生成(考虑和permutation一样去重)

Rolling State

1614. 括号的最大嵌套深度(Easy)

在这里插入图片描述

思路:

  • 忽略数字和符号,只统计圆括号:遇到"(" +1,遇到")" -1
  • 深度 = max rolling state

代码:

class Solution:
    def maxDepth(self, s: str) -> int:
        depth = 0
        ans = 0
        for c in s:
            if c == '(':
                depth += 1
            elif c == ')':
                depth -= 1
            ans = max(ans, depth)
        return ans


921. 使括号有效的最少添加(Medium)

在这里插入图片描述

思路:

  • 扫描中:一旦rolling state < 0(不平衡),则补上一个"(",使得state再次平衡
  • 扫描完:若最终的rolling state > 0,表示"("过多,需要补充state个")"使之平衡
  • 最终答案res = 扫描中补充的count个"(" + 扫描完后补充的state个的")"

代码:

class Solution:
    def minAddToMakeValid(self, s: str) -> int:
        state, count = 0, 0
        for c in s:
            if c == '(':
                state += 1
            elif c == ')':
                state -= 1
                if state < 0:
                    count += 1
                    state += 1 # state再次平衡
        return count + state


Rolling State + Stack辅助 / two-pass

20. 有效的括号(Easy)

在这里插入图片描述 思路1:暴力解

  • 如果当前char是左括号,则直接放入stack
  • 如果当前char是右括号,则检查pop出的左括号和当前右括号是否匹配

代码1:

class Solution:
    def isValid(self, s: str) -> bool:
        stack = []
        parentheses = {'(': ')', '[': ']', '{': '}'}
        for char in s:
            if char in parentheses:
                stack.append(char)
            else:
                if stack == [] or parentheses[stack.pop()] != char:
                    return False
        return len(stack) == 0

思路2:“预测结果”巧妙解 ❤️

  • 预测的结果放进去

代码2:

class Solution:
    def isValid(self, s: str) -> bool:
        stack = []
        parentheses = {'(': ')', '[': ']', '{': '}'}
        for char in s:
            if char in parentheses:
                stack.append(parentheses[char]) # 预测的结果
            else:
                if not stack or char != stack.pop():
                    return False
        return len(stack) == 0


1249. 移除无效的括号(Medium)

在这里插入图片描述

思路1:

  • 一次性找到所有的invalid的左右括号
  • stack用来存左括号:左括号push,右括号pop

代码1:一次性删除

class Solution:
    def minRemoveToMakeValid(self, s: str) -> str:
        stack = []
        indexes_to_remove = set()
        for i, c in enumerate(s):
            if c == '(':
                stack.append(i)  # 相当于state++
            elif c == ')':
                if stack:  # 正常情况state--
                    stack.pop()
                else:  # invalid,当前有括号需要删除
                    indexes_to_remove.add(i)
        # 有多余的左括号也需要全部删除
        while stack:
            indexes_to_remove.add(stack.pop())
        
        # 剔除需要删除的括号
        res = ''
        for i, c in enumerate(s):
            if i not in indexes_to_remove:
                res += c
        return res

思路2:Two Pass(non-stack)

  • 从前往后pass 1:删除所有invalid ")"
  • 从后往前pass 2:删除所有invalid "("
    • 从左往右,扫描多余的")";从右往左,扫描多余的"("

代码2:

class Solution:
    def minRemoveToMakeValid(self, s: str) -> str:
        
        def removeHelper(toSave, toRemove, string: str):
            state = 0
            res = ''
            for c in string:
                if c == toSave:
                    state += 1
                elif c == toRemove:
                    if state == 0: # 这个toRemove多余了
                        continue
                    state -= 1
                res += c
            return res
        
        # pass 1: 从前往后,remove all invalid ")"
        res = removeHelper('(', ')', s)
        # pass 2: 从后往前,remove all invalid "("
        res = removeHelper(')', '(', res[::-1])
        return res[::-1]


856. 括号的分数(Medium)

在这里插入图片描述

思路1:stack + 递归

  • 因为已经确定是「平衡括号字符串」,所以第一个"("必定在index=start=0处
  • 把整个string拆分成两部分,用stack确定和第一个"("匹配的")"的位置end,并以此作为分割点
    • 左半段:根据len(s[start+1:end]),判定左半段 = 1 OR 2 * scoreOfParentheses(s[start+1:end])
    • 右半段:直接递归处理 score2 = scoreOfParentheses(s[end:])
    • score_total = score_left + score_right

代码1:

class Solution:
    def scoreOfParentheses(self, s: str) -> int:
        stack = []
        start, end = 0, 0
        for i, c in enumerate(s):
            if c == '(':
                stack.append(i)
            else:
                stack.pop()
                if not stack:
                    end = i
                    break
        score_left = 1 if end - start == 1 else 2 * self.scoreOfParentheses(s[start + 1:end])
        score_right = 0 if end == len(s) - 1 else self.scoreOfParentheses(s[end + 1:])
        return score_left + score_right

思路2.1:巧妙运用stack

  • stack里存的是当前"("前一层的score
    • ie:AB形式,遇到了B的第一个"(",此时需要把A的score压入stack
  • 遇到")"的时候需要将前一层的score * 2并且和更前面的score求和,再保存会有stack
    • ie:AB形式,遇到了B的最后一个")",当前score_total = B的score * 2 + A的score
  • cur = 当前这一层的score

代码2.1:

class Solution:
    def scoreOfParentheses(self, s: str) -> int:
        stack = []
        cur = 0
        for c in s:
            if c == '(':
                stack.append(cur)
                cur = 0 # 开始计算新的一层的score
            else:
                cur = stack.pop() + max(2 * cur, 1) # 当前这一层算完了
        return cur

思路2.2:巧妙运用stack

  • stack里存的是现在所有当前frame的score

代码2.2:

class Solution:
    def scoreOfParentheses(self, s: str) -> int:
        stack = [0]
        for c in s:
            if c == '(':
                stack.append(0)
            else:
                # 遇到AB的形式
                B = stack.pop()  # 当前正在处理的括号里的值
                A = stack.pop()  # 前面"平行的"A的值(已计算好)
                stack.append(A + max(2 * B, 1))
        return stack.pop()


32. 最长有效括号(Hard)

在这里插入图片描述

思路1:stack

  • 一边判断括号有效性,一边统计最大长度
  • 利用栈来判断括号合理性
    • 遇到每个左括号,下标放入栈中,用于后续的匹配右括号
    • 遇到每个右括号,先弹出栈顶来匹配
      • 弹出后为空 -> 当前右括号没有被匹配,用其下标来更新栈底「最后一个没有被匹配的右括号的下标」,此时的栈顶就是栈底,因为是空栈
      • 弹出不为空 -> 当前右括号被匹配,当前下标-栈顶 为当前右括号为结尾的最大长度子串
    • 骚操作:保持栈底为「至今最后一个没有被匹配的右括号的下标」,用于计算最大长度,便于边界处理

代码1:

class Solution:
    def longestValidParentheses(self, s: str) -> int:
        stack = [-1]
        ans = 0
        for i, c in enumerate(s):
            if c == '(':
                stack.append(i)
            else:
                stack.pop()
                if not stack:  # 当前")"无法被匹配
                    stack.append(i)
                else:
                    ans = max(ans, i - stack[-1])
        return ans

思路2:Two Pass(non-stack)

套娃题,stack 或者 two-pass 都可以!

  • pass 1(从左到右):
    • 可以规避")"过多无法匹配的情况
    • 但无法规避出现"("过多而无法更新ans的情况
  • pass 2(从右往左):
    • 可以规避"("过多无法匹配的情况
    • 但无法规避出现")"过多而无法更新ans的情况

代码2:

class Solution:
    def longestValidParentheses(self, s: str) -> int:
        count_left, count_right, max_len = 0, 0, 0
        # 从左往右,扫描多余的")"
        for c in s:
            if c == '(':
                count_left += 1
            else:
                count_right += 1
            if count_left == count_right:  # 匹配
                max_len = max(max_len, 2 * count_left)
            elif count_left < count_right:  # ")"过多,重新开始计数
                count_left, count_right = 0, 0
        count_left, count_right = 0, 0
        # 从右往左,扫描多余的"("
        for c in s[::-1]:
            if c == '(':
                count_left += 1
            else:
                count_right += 1
            if count_left == count_right:  # 匹配
                max_len = max(max_len, 2 * count_left)
            elif count_left > count_right:  # "("过多,重新开始计数
                count_left, count_right = 0, 0
        return max_len

思路3:DP

  • dp[i] = 以 s[i] 结尾的最长有效括号的长度
  • 当 s[i] 为 (,必然不可能组成有效的括号,dp[i] = 0
  • 当 s[i] 为 )
    • if s[i-1] == (,那么 dp[i] = dp[i-2] + 2
    • ==if s[i-1] == ) and s[i-dp[i-1] - 1] == (,那么 dp[i] = dp[i-1] + 2 + dp[i-dp[i-1]-2](因为可能还可以接上更前面的匹配括号)==

代码3:

class Solution:
    def longestValidParentheses(self, s: str) -> int:
        dp = [0] * len(s)  # dp[i] = 以s[i]结尾的最长有效括号长度
        max_len = 0
        for i in range(1, len(s)):
            if s[i] == ')':
                if s[i - 1] == '(':
                    dp[i] = (dp[i - 2] if i - 2 >= 0 else 0) + 2
                else:
                    if s[i - dp[i - 1] - 1] == '(' and i - dp[i - 1] - 1 >= 0:
                        dp[i] = dp[i - 1] + 2 + dp[i - dp[i - 1] - 2]  # 因为可能可以接上更前面的有效括号,所以要加上前面的长度
            else:
                dp[i] = 0
            max_len = max(max_len, dp[i])
        return max_len


Backtracking

题目中要求「返回所有结果」 => 显然是backtracking

301. 删除无效的括号(Hard)

在这里插入图片描述

思路:

  • 先通过rolling state统计分别有多少个“(”“)”需要被remove
  • backtracking:
    • 对于每一个括号,可以选择remove / not remove
    • 对于每一个digit,不可remove

代码:

class Solution {
  private Set<String> ans = new HashSet<>();

  public List<String> removeInvalidParentheses(String s) {
    int rml = 0, rmr = 0;
    for (int i = 0; i < s.length(); i++) {
      if (s.charAt(i) == '(') rml++;
      else if (s.charAt(i) == ')') {
        if (rml != 0) rml--;
        else rmr++;
      }
    }
    dfs(s, 0, rml, rmr, 0, new StringBuilder());
    return new ArrayList<>(ans);
  }

  private void dfs(String s, int idx, int l, int r, int state, StringBuilder sb) {
    // 终止条件
    if (l < 0 || r < 0 || state < 0) return;
    if (idx == s.length()) {
      if (l == 0 && r == 0 && state == 0) ans.add(sb.toString());
      return;
    }
    // 处理节点 + 递归
    char c = s.charAt(idx);
    if (c == '(') {
      dfs(s, idx + 1, l - 1, r, state, sb); // remove "("
      dfs(s, idx + 1, l, r, state + 1, sb.append(c)); // keep "("
      sb.deleteCharAt(sb.length() - 1); // backtrack
    } else if (c == ')') {
      dfs(s, idx + 1, l, r - 1, state, sb); // remove ")"
      dfs(s, idx + 1, l, r, state - 1, sb.append(c)); // keep ")"
      sb.deleteCharAt(sb.length() - 1); // backtrack
    } else { // digit
      dfs(s, idx + 1, l, r, state, sb.append(c));
      sb.deleteCharAt(sb.length() - 1); // backtrack
    }
  }
}


241. 为运算表达式设计优先级(Medium)

在这里插入图片描述

思路:DFS + 记忆化递归

  • 在每个运算符号处做分割,分成leftright两部分
  • leftright进行递归计算,并将其递归产生的值们分别两两相加/减/乘

代码:

class Solution:
    def __init__(self):
        self.memo = {}
    
    def diffWaysToCompute(self, expression: str) -> List[int]:
        def dfs(string) -> List[int]:
            if string in self.memo:
                return self.memo[string]
            ans = []
            for i, c in enumerate(string):
                if c in '+-*':
                    left, right = dfs(string[:i]), dfs(string[i + 1:])
                    for l in left:
                        for r in right:
                            ans.append(eval(f'{l}{c}{r}'))
            self.memo[string] = ans if ans else [int(string)]
            return self.memo[string]  # 如果expression中只有数字,完全没有运算符
        
        return dfs(expression)


22. 括号生成(Medium)

在这里插入图片描述

思路:

  • k组匹配的括号对 = 在 k-1组匹配的括号对 的每一个空档,插入一对"()"
  • 用set去重

代码1: ❤️

class Solution:
    def generateParenthesis(self, n: int) -> List[str]:
        def dfs(k, res):
            if k == 1:
                return res
            ans = set()
            for string in res:
                for i in range(len(string)):
                    ans.add(string[:i] + '()' + string[i:])
            return dfs(k - 1, ans)
        
        return list(dfs(n, {'()'}))

代码2:

class Solution:
    def __init__(self):
        self.ans = set()
    
    def generateParenthesis(self, n: int) -> List[str]:
        def dfs(k, path) -> None:
            if k == 1:
                self.ans.add(path)
                return
            for i in range(len(path)):
                new_path = path[:i] + '()' + path[i:]
                dfs(k - 1, new_path)
        
        dfs(n, '()')
        return list(self.ans)


Reference: