Files
codetutor/backend/data/questions/longest-valid-parentheses.yaml
2025-05-25 11:47:04 +01:00

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
&nbsp;
**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 `(`)
&nbsp;
**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
&nbsp;
**Step 4: Return the result**
- Return `max_length` after processing all characters
&nbsp;
**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 `(`.