给你一个只包含 '(' 和 ')' 的字符串,找出最长有效(格式正确且连续)括号 子串 的长度。
左右括号匹配,即每个左括号都有对应的右括号将其闭合的字符串是格式正确的,比如 "(()())"。
示例 1:
输入: s = "(()"
输出: 2
解释: 最长有效括号子串是 "()"
示例 2:
输入: s = ")()())"
输出: 4
解释: 最长有效括号子串是 "()()"
示例 3:
输入: s = ""
输出: 0
提示:
0 <= s.length <= 3 * 104s[i]为'('或')'
要点(一句话)
令 dp[i] 表示 以索引 i 结尾 的最长有效括号子串的长度(若 s[i] == '(' 则 dp[i] = 0)。
当 s[i] == ')' 时:
- 如果
s[i-1] == '(':dp[i] = (i >= 2 ? dp[i-2] : 0) + 2。 - 否则(
s[i-1] == ')')且i - dp[i-1] - 1 >= 0且s[i - dp[i-1] - 1] == '(':
dp[i] = dp[i-1] + 2 + ( (i - dp[i-1] - 2 >= 0) ? dp[i - dp[i-1] - 2] : 0 )。
答案是 max(dp[i])。
下面详细解释每一条为什么成立,并做逐步演示。
直观与状态定义(为什么用 dp[i])
我们关注“以 i 结尾 的最长有效括号长度”,因为一个有效括号子串要么包含位置 i(结尾在 i),要么不包含,考虑“以 i 结尾”能把问题分解为更小子问题。
dp[i] 的意义(牢记):
dp[i]= 以索引i为结尾的最长有效括号子串的字符长度(不是个数、不是下标差)。- 如果
s[i]是'(',不可能以i结尾形成合法的 “以)结尾” 子串,所以dp[i] = 0。
转移公式的推导(分两种情况)
情况 A:s[i] == ')' 且 s[i-1] == '(' —— 形如 "..." + "()"
- 末尾是
"()",这对()自身贡献2个长度。 - 在这对
()之前,可能有一段以i-2结尾的合法子串(长度为dp[i-2]),它可以直接拼接在这对()左侧。 - 所以:
dp[i] = dp[i-2] + 2(当i-2 < 0时dp[i-2]视为0)。
图示(索引):
... [valid ending at i-2] ( )
i-1 i
因此公式成立。
情况 B:s[i] == ')' 且 s[i-1] == ')' —— 形如 "..." + "))"(要看更前面是否有 '(' 来匹配最右边的 ')')
- 假设
dp[i-1]表示以i-1结尾的最长有效长度,形象上i-1之前有一段[...]是合法的,长度为dp[i-1],那么这段合法子串的起始索引是i-1 - dp[i-1] + 1,所以最左端的索引就是i - dp[i-1]减去 1 等等 —— 我们只需要知道,在i - dp[i-1] - 1这个位置是否有一个'(',如果有,那么这个'('正好可以与当前的s[i](即最右的')')匹配,形成(...)的包裹,从而把dp[i-1]扩展并加上 2。
更清楚地:
- 末尾
dp[i-1]覆盖了从i - dp[i-1]到i-1的一段(含)。 - 检查
pos = i - dp[i-1] - 1:如果pos >= 0且s[pos] == '(',那么s[pos]与s[i]匹配,形成一对,把dp[i-1]扩展为dp[i-1] + 2。 - 还可能在
pos-1之前有一段合法子串(即dp[pos-1]),它们也能与当前这段拼接,所以还要加上dp[pos-1](即dp[i - dp[i-1] - 2])作为前缀贡献。
因此:
dp[i] = dp[i-1] + 2 + dp[i - dp[i-1] - 2] (若 pos = i - dp[i-1] -1 >=0 且 s[pos]=='(')
举个小图(下标)帮助记忆:
... [A] ( [B] ) ) <- 当前i在最右 )
pos i-dp[i-1] ... i-1 i
[B] 是 dp[i-1] 那段,( at pos 匹配当前 ) at i,[A] 是 dp[pos-1]。
边界与索引条件
-
在上面公式中要确保索引合法:
i - dp[i-1] - 1 >= 0才能检查s[pos]是否'('。i - dp[i-1] - 2 >= 0才能取dp[i - dp[i-1] - 2],否则视为0。
-
初始
dp全为0,因为单个位置单独不能有以它结尾的有效长度(若为')'则存在可能 >0)。
代码(通俗、注释清晰版)
class Solution {
public int longestValidParentheses(String s) {
int n = s.length();
if (n <= 1) return 0;
int[] dp = new int[n]; // dp[i] = 以 i 结尾的最长有效括号长度
int max = 0;
for (int i = 1; i < n; i++) {
if (s.charAt(i) == ')') {
// 情况 A: ...() (i-1 是 '(')
if (s.charAt(i - 1) == '(') {
dp[i] = (i >= 2 ? dp[i - 2] : 0) + 2;
}
// 情况 B: ...)) (i-1 是 ')')
else {
int prevLen = dp[i - 1]; // 以 i-1 结尾的有效长度
int pos = i - prevLen - 1; // 可能与 i 匹配的 '(' 的位置
if (pos >= 0 && s.charAt(pos) == '(') {
dp[i] = prevLen + 2;
// 如果 pos-1 >= 0,还要把 pos-1 之前的合法长度加上
if (pos - 1 >= 0) dp[i] += dp[pos - 1];
}
}
if (dp[i] > max) max = dp[i];
}
// 如果 s[i] == '(',dp[i] 保持 0
}
return max;
}
}
逐字符详细演示(两个例子)
下面用 表格方式 逐位展示 i, s[i], dp[i] 如何计算,帮助你一步步“看见”算法在做什么。
例子 1:s = "(()())"(期望结果 6)
字符串下标与字符:
i : 0 1 2 3 4 5
s : ( ( ) ( ) )
初始化:dp = [0,0,0,0,0,0], max = 0
-
i=1, s[1]='(' → dp[1]=0
-
i=2, s[2]=')' 且 s[1]='(' → 情况 A:dp[2] = dp[0] + 2 = 0 + 2 = 2 → max=2
dp = [0,0,2,0,0,0] -
i=3, s[3]='(' → dp[3]=0
-
i=4, s[4]=')' 且 s[3]='(' → 情况 A:dp[4] = dp[2] + 2 = 2 + 2 = 4 → max=4
dp = [0,0,2,0,4,0] -
i=5, s[5]=')' 且 s[4]=')' → 情况 B:
- prevLen = dp[4] = 4
- pos = 5 - 4 - 1 = 0
- s[0] == '(' 成立 → dp[5] = prevLen + 2 = 4 + 2 = 6
- pos-1 = -1 < 0,所以不用加 dp[pos-1]
→ dp[5] = 6,max=6
dp = [0,0,2,0,4,6]
最终 max = 6。
例子 2:s = ")()())"(LeetCode 常见例子,期望结果 4)
下标与字符:
i : 0 1 2 3 4 5
s : ) ( ) ( ) )
初始化:dp=[0,0,0,0,0,0], max=0
-
i=1, s[1]='(' → dp[1]=0
-
i=2, s[2]=')' 且 s[1]='(' → 情况 A:dp[2] = dp[0] + 2 = 0 + 2 = 2 → max=2
dp=[0,0,2,0,0,0] -
i=3, s[3]='(' → dp[3]=0
-
i=4, s[4]=')' 且 s[3]='(' → 情况 A:dp[4] = dp[2] + 2 = 2 + 2 = 4 → max=4
dp=[0,0,2,0,4,0] -
i=5, s[5]=')' 且 s[4]=')' → 情况 B:
- prevLen = dp[4] = 4
- pos = 5 - 4 - 1 = 0
- s[0] == ')',不是 '(',所以不能匹配 → dp[5]=0
dp=[0,0,2,0,4,0]
最终 max = 4(来自 dp[4])。
这说明最后的 ) 并不能和最前面的 ) 之前的 ( 配对。
为什么这个 DP 正确(直观证明)
-
dp[i]只依赖于之前更小的dp值(如dp[i-1],dp[i-2],dp[pos-1]),这是典型的“最优子结构”,即以i结尾的最长有效括号子串要么:- 以
i-1结尾并被新的)扩展(情况 B),或者 - 直接由
()结尾并附加dp[i-2](情况 A)。
- 以
-
公式覆盖了所有可能使
s[i]成为')'的合法结尾场景。若无法构成合法结尾(例如s[i]=='('或找不到要匹配的'('),dp[i]=0恰当。 -
每次我们在
i时更新max = max(max, dp[i]),因此最终max为整串中任意结尾位置的最大值,即最长有效括号。
复杂度
- 时间复杂度:
O(n)(一次线性遍历,常数时间更新dp[i])。 - 空间复杂度:
O(n)(一个长度为 n 的dp数组)。
(如果想空间 O(1) 实现可以用栈或双向扫描,但这里不展开——DP 是直观且常用的)
常见易错点(提醒)
- 忘记
i - dp[i-1] - 1 >= 0的边界判断会越界。 - 在情况 B 中,除了
dp[i-1] + 2,还要记得把pos-1之前的dp[pos-1]加上(可能为 0)。 dp[i]存的是长度(字符数),不是数量或起始索引。- 对
s[i-1]=='('(情况 A)要用dp[i-2]而不是dp[i-1],因为i-1是(。