AlgoMaster Logo

Longest Valid Parentheses

hardFrequency11 min readUpdated June 23, 2026

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 word "contiguous" matters: we're looking for a substring, not a subsequence, so we can't skip characters.

Valid parentheses can be adjacent or nested, which is what makes the problem harder than simple validation. In ()((), there are two valid portions, () at the start and () inside the second group, but the longest contiguous valid substring is () with length 2. In (())(), the entire string is valid with length 6. The challenge is figuring out where valid substrings start and end, especially when they chain together.

One property drives every efficient solution: an unmatched parenthesis acts as a barrier that separates valid regions. If we can locate these barriers, the answer is the longest stretch of matched characters between them.

Key Constraints:

  • 0 <= s.length <= 3 * 10^4: an O(n^2) solution would do about 9 x 10^8 character comparisons at the upper bound, so O(n) is the target.
  • s[i] is '(' or ')': only two characters, so we never branch on anything other than open versus close.
  • The string can be empty, so every solution must return 0 for an input of length 0.

Approach 1: Brute Force

Intuition

Check every possible substring and see if it forms valid parentheses. For each starting index, try every ending index, validate whether that substring is well-formed, and track the longest one found.

To check validity, walk through the substring with a counter, incrementing for ( and decrementing for ). If the counter ever goes negative, the substring has more closing than opening parens at some prefix, so it is invalid. If it ends at zero, the substring is valid. Since a valid sequence always has even length, we only test even-length substrings.

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 test even-length substrings starting at each index. Starting at index 0, every substring begins with ), so the counter goes negative immediately and none are valid. Starting at index 1, () (indices 1-2) is valid with length 2, and ()() (indices 1-4) is valid with length 4. The substring ()()) is odd-length so we skip it, and ()() followed by the trailing ) would be )()()) from index 0, already rejected. Starting at index 3, () (indices 3-4) is valid with length 2. No other start index yields anything longer. The longest valid substring is ()() with length 4.

Output:

Output4
4

Code

The cubic cost comes from re-scanning overlapping substrings from scratch. The next approach computes the longest valid substring ending at each position in a single pass, reusing the results it has 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, the answer is 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.

The wrapping case relies on dp[i-1] being a contiguous valid block ending exactly at i-1. That block spans indices i - dp[i-1] through i-1, so the character right before it sits at i - dp[i-1] - 1. If that character is (, it pairs with the ) at i and wraps the entire block, which is why we add dp[i-1] + 2. The final dp[matchIdx-1] term stitches on any valid substring that ended just before the wrapping (, handling chains like ()(()).

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):
    • If s[i] = ')' and s[i-1] = '(', set dp[i] = 2 + (i >= 2 ? dp[i-2] : 0).
    • 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).
  3. Return the maximum value in dp.

Example Walkthrough

Trace (()), which exercises both cases of the recurrence. Index 2 closes the inner pair through the direct case: s[2] = ')', s[1] = '(', so dp[2] = 2 + dp[0] = 2. Index 3 is the wrapping case: s[3] = ')' follows another ), and dp[2] = 2, so the match index is 3 - 2 - 1 = 0, where s[0] = '(' matches. That gives dp[3] = dp[2] + 2 = 4, since there is no valid block before index 0. The maximum over the array is 4.

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

Code

The DP approach runs in O(n) time but uses O(n) space for the array. The stack approach reaches the same time bound while making the barrier idea explicit, tracking the positions of unmatched parentheses directly.

Approach 3: Stack

Intuition

Every unmatched parenthesis acts as a boundary between valid regions, so the longest valid substring is the longest gap between consecutive unmatched indices. A stack lets us track these boundaries as we scan, instead of waiting until the end.

The stack holds the indices of parentheses that are still unmatched, plus one base boundary at the bottom. We push the index of every (. When we see a ), we pop, since that ) matches the most recent unmatched (. After the pop, the new top of the stack is the nearest unmatched position to the left, so the valid substring ending at the current index i has length i - stack.peek().

The base boundary handles the start of a valid region. We seed the stack with -1 so that a valid substring beginning at index 0 measures correctly. When a ) has nothing to match (the stack becomes empty after popping), that ) is itself unmatched, so we push its index as the new base boundary for everything that follows.

Algorithm

  1. Initialize a stack with -1 (the base boundary).
  2. For each index i in the string:
    • If s[i] = '(', push i onto the stack.
    • 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().
  3. Return the maximum length.

Example Walkthrough

Trace (()(). The leading ( at index 0 is never matched, so it stays on the stack and serves as the boundary for the two valid pairs to its right. When index 2 pops index 1, the top is 0, giving length 2 - 0 = 2. When index 4 pops index 3, the top is still 0, giving length 4 - 0 = 4. The answer is 4.

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

Code

Both the DP and stack approaches use O(n) space. The final approach drops that to O(1) by replacing the stack with two counters and a second scan.

Approach 4: Two-Pass Counting (Optimal)

Intuition

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

Scan left to right with two counters, open and close. When they are equal, the characters since the last reset form a valid substring of length open + close. When close > open, there are too many closing parens, so reset both to 0 and start fresh.

This single pass misses cases like "(()". Scanning left to right, open stays ahead of close and the two counters never equalize, even though () of length 2 sits inside. A second pass from right to left fixes this, resetting when open > close instead. The two passes together 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

Trace ((), the case that needs both passes. The left-to-right pass keeps open ahead of close the whole way and never records a match, so it returns 0. The right-to-left pass starts from the trailing ), equalizes the counters at the () in the middle, and records length 2 before the leading ( triggers a reset.

1Left-to-Right: open=0, close=0, max=0
0
(
i
1
(
2
)
1/8

Code