AlgoMaster Logo

Longest Valid Parentheses

Last Updated: April 1, 2026

hard
6 min read

Understanding the Problem

We need to find the length of the longest contiguous substring that forms a valid sequence of parentheses. A valid sequence means every ( has a matching ), and they're properly nested. The key word here is "contiguous" -- we're looking for a substring, not a subsequence. We can't skip characters.

What makes this tricky is that valid parentheses can be adjacent or nested. For example, in ()((), there are two valid portions: () at the start and () inside the second group, but the longest contiguous valid substring is just () with length 2. Meanwhile, (())() is entirely valid with length 6. So the challenge is figuring out where valid substrings start and end, especially when they chain together.

The key observation: an unmatched parenthesis acts as a "barrier" that separates valid regions. If we can identify these barriers, we can measure the lengths of valid regions between them.

Key Constraints:

  • 0 <= s.length <= 3 * 10^4 -- We can afford up to O(n^2), but O(n) is ideal. O(n) stack or two-pass solutions are what interviewers expect for a Hard problem.
  • s[i] is '(' or ')' -- Only two characters to handle. No other characters to worry about.
  • String can be empty -- We need to handle the empty string edge case (return 0).

Approach 1: Brute Force

Intuition

The most straightforward approach: check every possible substring and see if it forms valid parentheses. For each starting index, try every possible ending index, and validate whether that substring is a well-formed parentheses sequence. Track the longest one found.

To check if a substring is valid, we can use a simple counter. Walk through the substring, incrementing for ( and decrementing for ). If the counter ever goes negative, the substring is invalid (too many closing parens). If it ends at zero, the substring is valid.

Algorithm

  1. For each starting index i from 0 to n-1, do the following:
  2. For each ending index j from i+2 to n (step by 2, since valid parentheses always have even length):
  3. Check if the substring s[i..j] is valid by scanning with a counter.
  4. If valid, update the maximum length.
  5. Return the maximum length found.

Example Walkthrough

Input:

Inputs=)()())
0
)
1
(
2
)
3
(
4
)
5
)

We check all substrings: )( is invalid, )() is invalid, () at indices 1-2 is valid (length 2), ()() at indices 1-4 is valid (length 4), )()()) is invalid. The longest valid substring is ()() with length 4.

Output:

Output4
4

Code

For every starting index, we're re-scanning overlapping substrings from scratch. What if we could determine the longest valid substring ending at each position in a single pass, building on results we've already computed?

Approach 2: Dynamic Programming

Intuition

Define dp[i] as the length of the longest valid parentheses substring that ends at index i. If we can fill this array in one pass, we just return its maximum value.

A ) at index i can form a valid substring in two ways:

  1. Direct match: If s[i-1] = '(', then these two characters form (). The valid length is 2 + dp[i-2] (the 2 from this pair, plus whatever valid substring ended just before it).
  1. Wrapping match: If s[i-1] = ')' and there's already a valid substring ending at i-1, then we need to look past that valid substring. If s[i - dp[i-1] - 1] = '(', that opening paren matches our closing paren at i. The length becomes dp[i-1] + 2 + dp[i - dp[i-1] - 2] (the inner valid part, the new matching pair, plus any valid part before the opening paren).

If s[i] = '(', then dp[i] = 0 because no valid substring can end with an opening paren.

Algorithm

  1. Create an array dp of size n, initialized to 0.
  2. Iterate from index 1 to n-1 (index 0 can never end a valid substring by itself):

a. If s[i] = ')' and s[i-1] = '(', set dp[i] = 2 + (i >= 2 ? dp[i-2] : 0).

b. If s[i] = ')' and s[i-1] = ')', check the character at position i - dp[i-1] - 1. If that's '(', set dp[i] = dp[i-1] + 2 + (i - dp[i-1] >= 2 ? dp[i - dp[i-1] - 2] : 0).

  1. Return the maximum value in dp.

Example Walkthrough

1Initialize: dp = [0, 0, 0, 0, 0], scan from i=1
0
(
1
)
2
(
3
(
4
)
1/5
1dp initialized to all zeros
0
0
1
0
2
0
3
0
4
0
1/5

Code

The DP approach is O(n) time but uses O(n) space. Can we find a solution that uses only O(1) extra space?

Approach 3: Stack

Intuition

Every unmatched parenthesis acts as a boundary between valid regions. If we could find the indices of all unmatched parentheses, the longest valid substring would be the longest gap between consecutive unmatched indices.

A stack gives us a clean way to find these boundaries. We push indices onto the stack. When we see a ) that matches a ( on the stack, we pop. Whatever remains on the stack after processing the whole string are the unmatched positions. But we don't even need to wait until the end -- we can compute the answer as we go.

The trick: start with -1 on the stack as a "base" boundary. When we pop a matching (, the current valid length is i - stack.peek(). If we pop and the stack is empty, push the current index as the new base boundary (because this ) is unmatched).

Algorithm

  1. Initialize a stack with -1 (the base boundary).
  2. For each index i in the string:

a. If s[i] = '(', push i onto the stack.

b. If s[i] = ')', pop from the stack.

  • If the stack is now empty, push i (new base boundary).
  • Otherwise, update max length as i - stack.peek().
  1. Return the maximum length.

Example Walkthrough

1Initialize: stack=[-1], max=0
0
(
i
1
(
2
)
3
(
4
)
1/7
1Stack initialized with base boundary -1
-1
Top
1/7

Code

Both the DP and stack approaches use O(n) space. What if we could solve this with just a few counter variables?

Approach 4: Two-Pass Counting (Optimal)

Intuition

Instead of storing indices or DP values, we just count opening and closing parentheses as we scan.

Scan left to right. Maintain two counters: open and close. Every time they're equal, we have a valid substring of length open + close. When close > open, the substring is broken, so reset both to 0.

But this misses cases like "(()". Scanning left to right, open stays ahead of close and they never equalize, even though there's a valid () of length 2 inside. The fix: do a second pass from right to left, this time resetting when open > close. Together, the two passes catch every valid substring.

Algorithm

  1. Initialize open = 0, close = 0, maxLen = 0.
  2. Left-to-right pass: increment open for (, close for ). When equal, update max. When close > open, reset.
  3. Reset counters. Right-to-left pass: same logic but reset when open > close.
  4. Return maxLen.

Example Walkthrough

1Left-to-Right: i=0 ')' → close>open, reset
0
)
i
1
(
2
)
3
(
4
)
5
)
1/6

Code