260 lines
11 KiB
YAML
260 lines
11 KiB
YAML
title: Longest Valid Parentheses
|
|
slug: longest-valid-parentheses
|
|
difficulty: hard
|
|
leetcode_id: 32
|
|
leetcode_url: https://leetcode.com/problems/longest-valid-parentheses/
|
|
categories:
|
|
- strings
|
|
- stack
|
|
- dynamic-programming
|
|
patterns:
|
|
- dynamic-programming
|
|
- monotonic-stack
|
|
|
|
description: |
|
|
Given a string containing just the characters `'('` and `')'`, return *the length of the longest valid (well-formed) parentheses substring*.
|
|
|
|
A valid parentheses substring is one where every opening parenthesis `'('` has a corresponding closing parenthesis `')'` and they are properly nested.
|
|
|
|
constraints: |
|
|
- `0 <= s.length <= 3 * 10^4`
|
|
- `s[i]` is `'('` or `')'`
|
|
|
|
examples:
|
|
- input: 's = "(()"'
|
|
output: "2"
|
|
explanation: "The longest valid parentheses substring is \"()\"."
|
|
- input: 's = ")()())"'
|
|
output: "4"
|
|
explanation: "The longest valid parentheses substring is \"()()\"."
|
|
- input: 's = ""'
|
|
output: "0"
|
|
explanation: "An empty string has no valid parentheses."
|
|
|
|
explanation:
|
|
intuition: |
|
|
Imagine you're reading through a string of parentheses and trying to find the longest stretch where they're perfectly balanced.
|
|
|
|
The key insight is that a valid parentheses string can be **broken by unmatched characters**. An unmatched `)` at position `i` means any valid substring must start *after* `i`. Similarly, an unmatched `(` at position `j` means any valid substring ending before `j` cannot extend past it.
|
|
|
|
Think of it like this: unmatched parentheses act as **barriers** that divide the string into segments. Within each segment, we need to find how far the valid matching extends.
|
|
|
|
There are two elegant ways to approach this:
|
|
|
|
1. **Stack approach**: Use a stack to track indices of unmatched `(` characters. When we see a `)`, we either match it with a `(` (pop from stack) or mark it as a barrier. The stack always holds indices that "break" the valid sequence.
|
|
|
|
2. **Dynamic Programming**: For each position, calculate the length of the longest valid substring *ending* at that position. A `)` at position `i` can extend a valid substring if there's a matching `(` available.
|
|
|
|
The stack approach is more intuitive once you see it: we push indices as barriers, and the distance from the current index to the top of the stack gives us the length of the current valid segment.
|
|
|
|
approach: |
|
|
We'll use the **Stack Approach** as our optimal solution:
|
|
|
|
**Step 1: Initialise the stack with a base index**
|
|
|
|
- Push `-1` onto the stack as a "floor" or base index
|
|
- This handles the edge case where a valid substring starts from index `0`
|
|
- The stack will store indices of unmatched `(` characters and barrier positions
|
|
|
|
|
|
|
|
**Step 2: Iterate through each character**
|
|
|
|
- For each character at index `i`:
|
|
- If it's `'('`: push `i` onto the stack (potential start of valid sequence)
|
|
- If it's `')'`: pop from the stack (try to match with a `(`)
|
|
|
|
|
|
|
|
**Step 3: Calculate valid length after each `)`**
|
|
|
|
- After popping for a `)`:
|
|
- If the stack is **empty**: this `)` is unmatched, push `i` as a new barrier
|
|
- If the stack is **not empty**: calculate `i - stack.top()` as the length of the current valid substring
|
|
- Update `max_length` with the maximum value seen
|
|
|
|
|
|
|
|
**Step 4: Return the result**
|
|
|
|
- Return `max_length` after processing all characters
|
|
|
|
|
|
|
|
**Why this works**: The stack always contains indices that "break" valid sequences. The distance from the current index to the stack top represents how far back the current valid sequence extends.
|
|
|
|
common_pitfalls:
|
|
- title: Forgetting the Base Index
|
|
description: |
|
|
Without pushing `-1` initially, the first valid substring starting from index `0` won't be calculated correctly.
|
|
|
|
For example, with `s = "()"`:
|
|
- At index `0`, push `0`
|
|
- At index `1`, pop `0`, stack is now empty
|
|
- Without a base, we can't calculate `1 - (-1) = 2`
|
|
|
|
Always initialise with `-1` to handle edge cases cleanly.
|
|
wrong_approach: "Start with an empty stack"
|
|
correct_approach: "Push -1 as the base index before processing"
|
|
|
|
- title: Confusing Valid Substring vs Total Matches
|
|
description: |
|
|
This problem asks for the longest **contiguous** valid substring, not the total number of matched pairs.
|
|
|
|
For `s = "()(())"`:
|
|
- Total matched pairs: 3 (length 6)
|
|
- But the whole string is one valid substring of length 6
|
|
|
|
For `s = "())()"`:
|
|
- Total matched pairs: 2
|
|
- But longest valid substring is only 2 (`()` at the end or beginning)
|
|
|
|
The unmatched `)` at index 2 breaks the string into separate segments.
|
|
wrong_approach: "Count total matched pairs"
|
|
correct_approach: "Track longest contiguous valid segment"
|
|
|
|
- title: Using O(n) Space When O(1) is Possible
|
|
description: |
|
|
While the stack solution is intuitive and efficient at O(n) space, there's actually an O(1) space solution using two-pass counting.
|
|
|
|
For interviews, the stack approach is typically expected, but knowing the O(1) solution demonstrates deeper understanding.
|
|
wrong_approach: "Only knowing the stack approach"
|
|
correct_approach: "Understand both stack O(n) and two-pass O(1) approaches"
|
|
|
|
- title: Off-by-One Errors in Length Calculation
|
|
description: |
|
|
When calculating `i - stack.top()`, remember that this gives the length, not the ending index.
|
|
|
|
For example, if `i = 5` and `stack.top() = 2`:
|
|
- Length = `5 - 2 = 3` (positions 3, 4, 5)
|
|
- This represents indices 3 through 5 inclusive
|
|
|
|
Make sure your mental model matches: we're measuring distance, not counting indices.
|
|
|
|
key_takeaways:
|
|
- "**Stack for matching problems**: Using a stack to track indices (not just characters) is a powerful technique for parentheses and bracket matching"
|
|
- "**Barrier concept**: Unmatched characters act as barriers that reset the valid substring count"
|
|
- "**Base index trick**: Pushing `-1` as a base handles edge cases elegantly without special-casing"
|
|
- "**Related problems**: Valid Parentheses (#20), Generate Parentheses (#22), and Minimum Add to Make Parentheses Valid (#921) use similar concepts"
|
|
|
|
time_complexity: "O(n). We traverse the string exactly once, and each index is pushed and popped from the stack at most once."
|
|
space_complexity: "O(n). In the worst case (all opening parentheses), the stack holds all n indices. The two-pass approach achieves O(1) space."
|
|
|
|
solutions:
|
|
- approach_name: Stack with Index Tracking
|
|
is_optimal: true
|
|
code: |
|
|
def longest_valid_parentheses(s: str) -> int:
|
|
# Stack stores indices of unmatched '(' and barrier positions
|
|
# Start with -1 as base to handle valid substring starting at index 0
|
|
stack = [-1]
|
|
max_length = 0
|
|
|
|
for i, char in enumerate(s):
|
|
if char == '(':
|
|
# Push index of '(' as potential start of valid sequence
|
|
stack.append(i)
|
|
else:
|
|
# Pop to match this ')' with a '('
|
|
stack.pop()
|
|
|
|
if not stack:
|
|
# Stack empty means this ')' is unmatched
|
|
# Push current index as new barrier
|
|
stack.append(i)
|
|
else:
|
|
# Calculate length of current valid substring
|
|
# Distance from current position to the last barrier
|
|
current_length = i - stack[-1]
|
|
max_length = max(max_length, current_length)
|
|
|
|
return max_length
|
|
explanation: |
|
|
**Time Complexity:** O(n) — Single pass through the string.
|
|
|
|
**Space Complexity:** O(n) — Stack can hold up to n indices in the worst case.
|
|
|
|
The stack maintains a "barrier" at its top, representing the rightmost position that breaks valid parentheses. When we find a valid match, the distance from the current index to this barrier gives us the valid substring length.
|
|
|
|
- approach_name: Dynamic Programming
|
|
is_optimal: false
|
|
code: |
|
|
def longest_valid_parentheses(s: str) -> int:
|
|
if not s:
|
|
return 0
|
|
|
|
n = len(s)
|
|
# dp[i] = length of longest valid substring ending at index i
|
|
dp = [0] * n
|
|
max_length = 0
|
|
|
|
for i in range(1, n):
|
|
if s[i] == ')':
|
|
if s[i - 1] == '(':
|
|
# Case 1: "()" pattern - extends previous valid substring
|
|
dp[i] = (dp[i - 2] if i >= 2 else 0) + 2
|
|
elif i - dp[i - 1] > 0 and s[i - dp[i - 1] - 1] == '(':
|
|
# Case 2: "))" pattern - check if there's matching '('
|
|
# before the valid substring ending at i-1
|
|
dp[i] = dp[i - 1] + 2
|
|
# Add any valid substring before the matching '('
|
|
if i - dp[i - 1] >= 2:
|
|
dp[i] += dp[i - dp[i - 1] - 2]
|
|
|
|
max_length = max(max_length, dp[i])
|
|
|
|
return max_length
|
|
explanation: |
|
|
**Time Complexity:** O(n) — Single pass through the string.
|
|
|
|
**Space Complexity:** O(n) — DP array of size n.
|
|
|
|
For each `)` at position `i`, we determine if it can extend a valid substring:
|
|
- If preceded by `(`, we have a `()` pair adding 2 to whatever came before
|
|
- If preceded by `)`, we look past the valid substring ending at `i-1` to find a matching `(`
|
|
|
|
- approach_name: Two-Pass Counting
|
|
is_optimal: false
|
|
code: |
|
|
def longest_valid_parentheses(s: str) -> int:
|
|
# O(1) space solution using two passes
|
|
|
|
max_length = 0
|
|
left = right = 0
|
|
|
|
# Left to right pass
|
|
for char in s:
|
|
if char == '(':
|
|
left += 1
|
|
else:
|
|
right += 1
|
|
|
|
if left == right:
|
|
# Balanced - this is a valid substring
|
|
max_length = max(max_length, 2 * right)
|
|
elif right > left:
|
|
# Too many ')' - reset counters
|
|
left = right = 0
|
|
|
|
# Right to left pass (handles excess '(' cases)
|
|
left = right = 0
|
|
for char in reversed(s):
|
|
if char == '(':
|
|
left += 1
|
|
else:
|
|
right += 1
|
|
|
|
if left == right:
|
|
max_length = max(max_length, 2 * left)
|
|
elif left > right:
|
|
# Too many '(' - reset counters
|
|
left = right = 0
|
|
|
|
return max_length
|
|
explanation: |
|
|
**Time Complexity:** O(n) — Two passes through the string.
|
|
|
|
**Space Complexity:** O(1) — Only uses counter variables.
|
|
|
|
This clever approach counts left and right parentheses. When counts match, we have a valid substring. We need two passes because a single pass can't handle both excess `(` and excess `)` cases. Left-to-right handles excess `)`, right-to-left handles excess `(`.
|