8.9 Longest Valid Parentheses
Source:
src/main/kotlin/stack/LongestValidParanthesis.ktPattern: stack of indices with a base · Core page
The Problem
Given a string of ( and ), return the length of the longest valid (well-formed) parentheses substring.
- Constraints: $1 \le n \le 3 \times 10^4$; only parentheses.
Examples
Input: s = "(()" -> Output: 2 (the substring "()")
Input: s = ")()())" -> Output: 4 (the substring "()()")
Intuition — store indices on the stack, keep a base index for broken chains
The 8.1 matching stack answers “is the whole string valid?”; here a substring may be valid while the whole isn’t. The stack must track where matches begin — so it holds indices, not characters, and keeps a sentinel -1 as the “base” before any valid chain:
stack = [-1] # base: the position before the current chain
for i in s.indices:
if s[i] == '(': stack.push(i)
else:
stack.pop() # match the last '(' (or remove the base)
if stack.empty(): stack.push(i) # unmatched ')': new base at i
else: maxLen = max(maxLen, i - stack.last())
Why -1 initially and i on unmatched ')'? A valid chain is a contiguous block; its length is end - base. The base is the index just before the block starts. When a ')' finds no '(' to match (stack empties), the chain breaks — the current i becomes the new base, and length measurement restarts. The -1 sentinel makes the very first chain measure from index 0.
Why does popping a '(' and reading i - stack.last() give the length? After matching, the stack top is the last unmatched index — either the chain’s base or an earlier '(' that starts an enclosing valid block. i - top is the distance: exactly the matched segment’s length. This is the 8.7/8.3 “stack holds indices for spans” idiom.
Approach 1 — DP (also O(n))
dp[i] = longest valid ending at i, with dp[i] = 2 + dp[i-1] + dp[i - dp[i-1] - 2] on ')': correct, more bookkeeping.
Approach 2 — Stack of indices (the repo’s version, optimal)
class LongestValidParanthesis {
/**
* @param s parentheses string
* @return length of the longest valid substring
*/
fun longestValidParentheses(s: String): Int {
val stack = ArrayDeque<Int>()
stack.addLast(-1) // base before the first chain
var maxLen = 0
for (i in s.indices) {
if (s[i] == '(') {
stack.addLast(i) // push the index of '('
} else {
stack.removeLast() // match the last '(' (or drop the base)
if (stack.isEmpty()) {
stack.addLast(i) // unmatched ')': new base, chain broken
} else {
maxLen = maxOf(maxLen, i - stack.last())
}
}
}
return maxLen
}
}
import java.util.*;
public class LongestValidParentheses {
/**
* @param s parentheses string
* @return length of the longest valid substring
*/
public int longestValidParentheses(String s) {
Deque<Integer> stack = new ArrayDeque<>();
stack.push(-1); // base before the first chain
int max = 0;
for (int i = 0; i < s.length(); i++) {
if (s.charAt(i) == '(') {
stack.push(i); // push the index of '('
} else {
stack.pop(); // match the last '(' (or drop the base)
if (stack.isEmpty()) {
stack.push(i); // unmatched ')': new base
} else {
max = Math.max(max, i - stack.peek());
}
}
}
return max;
}
}
#include <stack>
#include <string>
class LongestValidParentheses {
public:
/**
* @param s parentheses string
* @return length of the longest valid substring
*/
int longestValidParentheses(std::string s) {
std::stack<int> st;
st.push(-1); // base before the first chain
int max = 0;
for (int i = 0; i < (int)s.size(); i++) {
if (s[i] == '(') {
st.push(i); // push the index of '('
} else {
st.pop(); // match the last '(' (or drop the base)
if (st.empty()) {
st.push(i); // unmatched ')': new base
} else {
max = std::max(max, i - st.top());
}
}
}
return max;
}
};
def longest_valid_parentheses(s: str) -> int:
"""
@param s: parentheses string
@return: length of the longest valid substring
"""
stack = [-1] # base before the first chain
max_len = 0
for i, c in enumerate(s):
if c == "(":
stack.append(i) # push the index of '('
else:
stack.pop() # match the last '(' (or drop the base)
if not stack:
stack.append(i) # unmatched ')': new base, chain broken
else:
max_len = max(max_len, i - stack[-1])
return max_len
#![allow(unused)]
fn main() {
impl Solution {
/// @param s parentheses string
/// @return length of the longest valid substring
pub fn longest_valid_parentheses(s: String) -> i32 {
let mut stack: Vec<i32> = vec![-1]; // base before the first chain
let mut max_len = 0;
for (i, c) in s.bytes().enumerate() {
if c == b'(' {
stack.push(i as i32); // push the index of '('
} else {
stack.pop(); // match the last '(' (or drop the base)
if stack.is_empty() {
stack.push(i as i32); // unmatched ')': new base
} else {
max_len = max_len.max(i as i32 - stack[stack.len() - 1]);
}
}
}
max_len
}
}
}
Dry run
Input: s = ")()())".
stack = [-1], maxLen = 0
i=0 ')': pop -> empty. push 0. stack=[0]. (base reset: chain broken before index 0)
i=1 '(': push 1. stack=[0,1]
i=2 ')': pop 1. top=0 -> maxLen = max(0, 2-0) = 2. ("()" at 1..2)
i=3 '(': push 3. stack=[0,3]
i=4 ')': pop 3. top=0 -> maxLen = max(2, 4-0) = 4. ("()()" at 1..4)
i=5 ')': pop 0 -> empty. push 5. stack=[5]. (final ')' breaks the chain)
Output: 4 ✓
The base-index mechanics: i=0’s unmatched ')' makes index 0 the base, so the chain starting at 1 measures 2 - 0 = 2 and 4 - 0 = 4 — the base precedes the whole valid block. The final ')' pops the base and re-seeds at 5, correctly ending the measurement. "(()" gives 2 the same way: ( push 0, ( push 1, ) pop 1 → 2 - 0 = 2.
Complexity
Time. One pass:
$$ T(n) = O(n) $$
Space. The stack:
$$ S(n) = O(n) $$
Variants & follow-ups
- Valid Parentheses (8.1) — the whole-string checker; this page’s stack stores indices and a base to handle broken chains.
- Minimum Add To Make Parentheses Valid (
stack/MinimumAddtoMakeParenthesesValid.kt) — count the unmatched: the base-reset logic in counting form. - Longest Valid Parentheses (DP) — the
dp[i]alternative:dp[i] = 2 + dp[i-1] + dp[i - dp[i-1] - 2]whens[i] == ')'and its match exists. - Interview follow-up: “Why indices on the stack instead of characters?” A character stack can verify matching but can’t measure distance — the length of a matched segment is
i - stack.top(), which requires the base index. The-1/reset base is what makes the measurement restart correctly after broken chains.