LeetCode 32:最长有效括号 - 动态规划核心思路详解

52 阅读3分钟

LeetCode 32 题“最长有效括号”是括号类问题的进阶题型。给定一个只包含 '(' 和 ')' 的字符串,要求找出最长的连续有效括号子串的长度。关键点在于:子串必须连续,且括号要正确匹配。

常见解法有栈、动态规划、双指针等。其中动态规划的思路最贴近“逐步构建最优解”的思维过程,非常适合用来锻炼状态定义与转移的能力。

核心状态定义

我们定义一个长度为 n+1 的数组 f,其中:

  • f[0] = 0(空字符串)
  • f[i+1] 表示以 s[i](第 i 个字符,下标从 0 开始)结尾的最长有效括号子串长度

为什么长度设为 n+1?这样可以让 f[i+1] 对应 s[i],避免频繁讨论 i-1 越界的问题

遍历过程中:

  • 如果 s[i] == '(':以左括号结尾不可能形成新的有效括号对,因此 f[i+1] = 0
  • 如果 s[i] == ')':才有形成有效括号的可能,需要分类讨论

状态转移:遇到 ')' 时的两种情况

情况一:前一个字符 s[i-1] == '('

这意味着 "...( )",当前 ')' 可以与紧邻的 '(' 直接匹配,形成一对 "()"。 此时,以 i 结尾的有效长度 = 以 i-2 结尾的有效长度 + 2

示例:s = "()()"

  • i=1 时,s[1]=')',s[0]='(' → f[2] = f[0] + 2 = 2
  • i=3 时,s[3]=')',s[2]='(' → f[4] = f[2] + 2 = 4

情况二:前一个字符 s[i-1] == ')'

这意味着当前 ')' 的前面可能形成一段最长有效括号(长度为 f[i])。 贪心地考虑,我们需要“跳过”这段最长有效括号是最优的,看看它前面是否还有一个 '(' 可以与当前 ')' 匹配。

计算跳过后的位置:pos = i - f[i] - 1 (i - 1 是前一个位置,减去 f[i] 就是跳过以 i-1 结尾的那段有效括号,再减 1 得到前一个位置)

注意判断pos >= 0 ,因为我们需要判断 s[pos]

  • 如果 pos < 0:越界,无法匹配 → f[i+1] = 0
  • 如果 pos >= 0 且 s[pos] == ')':前面也是右括号,无法匹配 → f[i+1] = 0
  • 如果 pos >= 0 且 s[pos] == '(':可以匹配!

此时,以 i 结尾的有效长度 = 当前匹配的 2 + 中间那段有效括号长度 f[i] + pos 之前能形成的最长有效括号 f[pos]

示例:s = "(()())"

  • i=2:')',s[1]='(' → 情况一,f[3] = f[1] + 2 = 2

  • i=5:')',s[4]=')' → 情况二

    • f[5] = 2(以位置4结尾的长度)
    • pos = 5 - 2 - 1 = 2
    • s[2] = ')'?不是,是 '(' → 可以匹配
    • f[6] = f[5] + 2 + f[2] = 2 + 2 + 0 = 4?等一下,应该是 6!

仔细看:f[pos] 是 f[3](因为 pos=2,对应 f[3]=2) 正确计算:f[6] = f[5] + 2 + f[pos] = 2 + 2 + f[3] = 2 + 2 + 2 = 6

完美!整个 "(()())" 都被覆盖。

完整代码实现

C++

class Solution {
public:
    int longestValidParentheses(string s) {
        int n = s.size();
        vector<int> f(n + 1, 0);

        for (int i = 0; i < n; i++) {
            if (s[i] == ')') {
                if (i >= 1 && s[i - 1] == '(') {
                    f[i + 1] = f[i - 1] + 2;
                } else if (i >= 1 && s[i - 1] == ')') {
                    int pos = i - f[i] - 1;
                    if (pos >= 0 && s[pos] == '(') {
                        f[i + 1] = f[i] + 2 + f[pos];
                    }
                }
            }
        }
        
        return ranges::max(f);
    }
};

Go

func longestValidParentheses(s string) int {
    n := len(s)
    f := make([]int, n+1)
    for i, ss := range s {
        if ss == ')' {
            if i >= 1 && s[i-1] == '(' {
                f[i+1] = f[i-1] + 2
            } else if i >= 1 && s[i-1] == ')'{
                pos := i - f[i] - 1
                if pos >= 0 && s[pos] == '(' {
                    f[i+1] = f[i] + 2 + f[pos]
                }
            }
        }
    }
    return slices.Max(f);
}

Python

class Solution:
    def longestValidParentheses(self, s: str) -> int:
        n = len(s)
        f = [0] * (n + 1)

        for i, ch in enumerate(s):
            if ch == ')':
                if i > 0 and s[i - 1] == '(':
                    f[i + 1] = f[i - 1] + 2
                elif s[i - 1] == ')':
                    pos = i - f[i] - 1
                    if pos >= 0 and s[pos] == '(':
                        f[i + 1] = f[i] + 2 + f[pos]
                        
        return max(f)

JavaScript

/**
 * @param {string} s
 * @return {number}
 */
var longestValidParentheses = function(s) {
    const n = s.length;
    const f = Array(n+1).fill(0);
    for (let i = 0; i < n; i++) {
        if (s[i] == ')') {
            if (i >= 1 && s[i-1] == '(') {
                f[i+1] = f[i-1] + 2;
            } else if (i >= 1 && s[i-1] == ')') {
                const pos = i - f[i] - 1;
                if (pos >= 0 && s[pos] == '(') {
                    f[i+1] = f[i] + 2 + f[pos]
                }
            }
        }
    }
    return Math.max(...f)
};