线性动态规划:最长有效括号匹配子序列长度
题目描述
给定一个只包含 '(' 和 ')' 的字符串 s,找出最长的有效括号匹配子序列的长度。注意:子序列不要求连续,但要求相对顺序不变,且每个左括号必须有对应的右括号匹配。
例如:
s = "()())" 的最长有效括号子序列是 "()()",长度为 4。
s = ")(()())" 的最长有效括号子序列是 "(()())",长度为 6。
解题思路
这个问题是最长有效括号匹配子序列(Longest Valid Parentheses Subsequence),不同于连续子串问题,它允许跳过部分字符。我们可以使用线性动态规划解决,定义 dp[i][j] 为子串 s[i:j] 的最长有效括号子序列长度,但更高效的方法是使用一维 DP 或栈辅助计数。
这里我们采用一维 DP,定义 dp[i] 表示以 i 结尾的子串中,最长有效括号子序列的长度。但直接这样定义不方便,因为括号匹配是成对的,我们考虑用栈+DP结合的方式。
步骤 1:问题分析
有效括号序列的充要条件:
- 左右括号数量相等;
- 任意前缀中左括号数不少于右括号数。
对于子序列问题,我们需要选择一些括号,使得选出的序列是有效的。关键点:每个右括号必须匹配一个在它之前且未被匹配的左括号。
步骤 2:动态规划定义
定义 dp[i] 表示前 i 个字符中,最长有效括号子序列的长度。
我们还需要一个辅助变量 left_count,表示在当前选择中,尚未匹配的左括号的数量。
但这样还不够,因为子序列可以跳过字符,我们需要知道之前有哪些左括号可以被匹配。
更标准的解法:
定义 dp[i][j] 为前 i 个字符中,选取若干字符形成子序列,且该子序列中未匹配的左括号数为 j 时的最大长度(j ≥ 0)。
那么答案就是 dp[n][0](所有左括号都匹配完)。
步骤 3:状态转移
对于每个字符 s[i](索引从 1 开始):
- 如果 s[i] 是 '(':
- 不选这个字符:dp[i][j] = dp[i-1][j]
- 选这个字符:那么未匹配左括号数会变成 j+1,长度+1,所以 dp[i][j+1] = max(dp[i][j+1], dp[i-1][j] + 1)
- 如果 s[i] 是 ')':
- 不选:dp[i][j] = dp[i-1][j]
- 选:只有当 j > 0(有未匹配的左括号)时,才能选这个右括号,匹配掉一个左括号,未匹配数变成 j-1,长度+1,所以 dp[i][j-1] = max(dp[i][j-1], dp[i-1][j] + 1)
初始化:dp[0][0] = 0,其他 dp[0][j] = -inf(不可达)。
步骤 4:简化为一维 DP
因为 dp[i] 只依赖于 dp[i-1],可以滚动数组优化空间。
我们只需两个数组:prev[j] 和 curr[j],分别表示前 i-1 个字符和前 i 个字符的状态。
步骤 5:示例推导
以 s = "()())" 为例(n=5):
初始化:prev[0]=0,其他 prev[j]=-inf。
i=1: '('
选:curr[1] = prev[0]+1 = 1
不选:curr[0] = prev[0] = 0
所以 prev 更新为:prev[0]=0, prev[1]=1, 其他 -inf。
i=2: ')'
j=0: 选不了 ')'(因为 j=0 无左括号匹配),所以 curr[0] = prev[0]=0
j=1: 选')':curr[0] = max(curr[0], prev[1]+1)=max(0,2)=2
不选:curr[1] = prev[1]=1
所以 prev: prev[0]=2, prev[1]=1。
i=3: '('
j=0: 选'(':curr[1] = prev[0]+1=3
不选:curr[0] = prev[0]=2
j=1: 选'(':curr[2] = prev[1]+1=2
不选:curr[1] = max(curr[1], prev[1])=max(3,1)=3
所以 prev: prev[0]=2, prev[1]=3, prev[2]=2。
i=4: ')'
j=0: 不能选')',curr[0]=prev[0]=2
j=1: 选')':curr[0]=max(2, prev[1]+1)=max(2,4)=4
j=2: 选')':curr[1]=max(curr[1], prev[2]+1)=max(?,3)=3(curr[1] 初始为 prev[1]=3)
不选:curr[1]=max(3,3)=3, curr[2]=prev[2]=2
所以 prev: prev[0]=4, prev[1]=3, prev[2]=2。
i=5: ')'
j=0: 不能选')',curr[0]=4
j=1: 选')':curr[0]=max(4, 3+1)=4
j=2: 选')':curr[1]=max(3, 2+1)=3
所以最终 prev[0]=4 为答案。
步骤 6:算法实现要点
- 因为 j 的范围最大为 n,所以空间 O(n)。
- 时间 O(n²) 因为 j 最多 n。
- 实际 j 的范围可以限制到当前可能的最大未匹配左括号数(即到当前位置的 '(' 数量)。
总结
这个问题通过 DP 状态 j 记录未匹配左括号数,从而能够处理子序列匹配问题。每一步根据当前字符是左括号还是右括号,决定是否选择并更新状态。最终 dp[n][0] 就是所有字符考虑完后,完全匹配的最长有效括号子序列长度。