Parentheses Problems
A stack (or a single counter) processes bracket sequences left to right, matching each closing bracket against the most recent unmatched opener. This one idea — 'most recent unmatched opener' — solves validity checking, longest valid subsequence, minimum removals, and score counting.
Table of contents
- Before we start
- Picture this first (no code yet)
- First, the slow way (so you feel the pain)
- The turning point
- The one idea to remember
- Problem 1 — Valid Parentheses (LC #20)
- Problem 2 — Longest Valid Parentheses (LC #32)
- Approach A: Stack of indices
- Approach B: Counter (two passes, O(1) space)
- Problem 3 — Minimum Removes to Make Valid (LC #1249)
- Problem 4 — Score of Parentheses (LC #856)
- The toolkit summary
- Common traps
- Check yourself
- Practice problems
Before we start
Parentheses problems look deceptively simple — "is this balanced?" — until the interviewer asks for the longest valid substring, or the minimum brackets to remove, or the score. By the end of this chapter you will be able to:
- Solve the simple validity check in one pass with a counter — no stack needed.
- Find the longest valid parentheses substring in O(n) time two different ways.
- Recognise the three major variants and pick the right tool for each.
Picture this first (no code yet)
A real-life story
You are a stage manager for a play. Actors enter from the left door ( and exit from the right door ). Your job: make sure every exit matches an entrance. You keep a clipboard counting "actors currently on stage."
Every ( — an actor enters — you add 1. Every ) — an actor exits — you subtract 1. Two rules:
- The count must never go negative (someone tried to exit who was never on stage — invalid).
- At the very end, the count must be exactly zero (everyone who entered has left).
If both rules hold, the sequence is valid. That clipboard counter is the entire algorithm for bracket validation.
The moment the count goes negative — say you see )( — an exit with nobody on stage — the sequence is already invalid, no matter what comes after.
First, the slow way (so you feel the pain)
Brute force: for each possible substring, check if it is a valid parentheses sequence. For n = 10 000, there are ~50 million substrings. Checking each takes up to O(n) time. Total: O(n³). For n = 30 000 (a typical LeetCode constraint) that is 27 trillion operations. A single step takes a nanosecond — 27 trillion steps take 27 000 seconds. Nine hours. We need something drastically faster.
The turning point
Pause & think
You have the counter (actors on stage). The string is "(())". Walk through it: what does the counter look like at each step? Now try "()()". Same result at the end — but a different shape. What's different about the path the counter takes, even though both end at 0?
Walk through it
"(())" → 1, 2, 1, 0 — counter rises to 2, then falls back smoothly
"()()" → 1, 0, 1, 0 — counter touches 0 twice in the middle
Both end at 0, both are valid. The difference: "(())" has one long valid block;
"()()" has two shorter valid blocks joined. This matters for "longest valid substring."
The one idea to remember
The entire pattern in one sentence
A stack (or counter) processes brackets left to right, matching each ) against the most recent unmatched ( — if a match is found, both dissolve; if not, the sequence is broken at that position; the stack's remaining contents after a full pass are the unmatched brackets.
Problem 1 — Valid Parentheses (LC #20)
The counter approach works perfectly for a single bracket type. For multiple bracket types ((), [], {}), you need a stack to remember which type is open.
def isValid(s: str) -> bool:
stack = []
matching = {')': '(', ']': '[', '}': '{'}
for ch in s:
if ch in '([{':
stack.append(ch) # push opener
else:
if not stack or stack[-1] != matching[ch]:
return False # no matching opener, or wrong type
stack.pop() # match found — both dissolve
return len(stack) == 0 # all openers must have been matched
Frame-by-frame: "([)]"
ch='(' → stack = ['(']
ch='[' → stack = ['(', '[']
ch=')' → matching[')']='(' but stack[-1]='[' → mismatch → return False ✅
Frame-by-frame: "()[]{}"
'(' → ['(']
')' → stack[-1]='(' == matching[')'] → pop → []
'[' → ['[']
']' → match → []
'{' → ['{']
'}' → match → []
stack empty → True ✅
Problem 2 — Longest Valid Parentheses (LC #32)
This is the hard variant. Return the length of the longest substring that is a valid parentheses sequence.
Approach A: Stack of indices
Instead of storing characters, store indices on the stack. The stack always holds the index of the last unmatched character — the "boundary" before any valid sequence.
def longestValidParentheses(s: str) -> int:
stack = [-1] # -1 is the boundary before the string starts
best = 0
for i, ch in enumerate(s):
if ch == '(':
stack.append(i) # push index of opener
else:
stack.pop() # try to match with the top
if not stack:
stack.append(i) # no match — i becomes the new boundary
else:
best = max(best, i - stack[-1]) # length = current - last boundary
return best
Frame-by-frame: "(()" (expect answer 2 — the inner "()")
stack = [-1]
i=0 '(': push 0. stack = [-1, 0]
i=1 '(': push 1. stack = [-1, 0, 1]
i=2 ')': pop 1. stack = [-1, 0] non-empty → best = max(0, 2-0) = 2
i=3 ')': pop 0. stack = [-1] non-empty → best = max(2, 3-(-1)) = 4
Wait — "(()" has length 3, and the answer should be 2 for "()" inside it. Let me re-trace the string.
Let me trace "(()" carefully:
s = "(()" (length 3)
stack = [-1]
i=0 ch='(': stack = [-1, 0]
i=1 ch='(': stack = [-1, 0, 1]
i=2 ch=')': pop → 1 removed. stack = [-1, 0]. Non-empty.
best = max(0, 2 - 0) = 2.
Answer: 2 ✅ (the valid part is s[1..2] = "()")
Frame-by-frame: ")()())"
stack = [-1]
i=0 ')': pop -1. Stack empty → push 0 (new boundary). stack=[0]
i=1 '(': push 1. stack=[0,1]
i=2 ')': pop 1. stack=[0]. best = max(0, 2-0) = 2.
i=3 '(': push 3. stack=[0,3]
i=4 ')': pop 3. stack=[0]. best = max(2, 4-0) = 4.
i=5 ')': pop 0. Stack empty → push 5 (new boundary). stack=[5]
Answer: 4 ✅ (the valid part is s[1..4] = "()()")
Pause & think
Why do we initialise the stack with [-1] instead of leaving it empty? What would break if we started with an empty stack and saw a match on the very first two characters "()"?
Answer
If the stack were empty at i=1 after popping the ( from i=0, then stack[-1] would error. More importantly, even if we guarded with if stack, we'd have no "last boundary" to subtract from. The -1 sentinel acts as a boundary saying "the valid sequence can start from index 0." When we compute i - stack[-1] after a match, i - (-1) = i + 1 gives the correct length for a valid sequence starting from the very beginning.
Approach B: Counter (two passes, O(1) space)
Scan left-to-right counting opens and closes. When they equalise, record the length. When closes exceed opens, reset both counters (invalid). Then scan right-to-left with reversed roles (catches cases where opens exceed closes at the end).
def longestValidParentheses_twopass(s: str) -> int:
def scan(seq, open_ch, close_ch):
opens = closes = best = 0
for ch in seq:
if ch == open_ch: opens += 1
else: closes += 1
if opens == closes:
best = max(best, 2 * closes)
elif closes > opens:
opens = closes = 0 # reset — invalid prefix
return best
return max(scan(s, '(', ')'), scan(reversed(s), ')', '('))
Two passes, O(n) time, O(1) space — no stack at all.
Problem 3 — Minimum Removes to Make Valid (LC #1249)
Find the minimum number of brackets to remove so the remaining string is valid.
def minRemoveToMakeValid(s: str) -> str:
stack = [] # stores indices of unmatched '('
remove = set() # indices to remove
for i, ch in enumerate(s):
if ch == '(':
stack.append(i)
elif ch == ')':
if stack:
stack.pop() # matched — both stay
else:
remove.add(i) # unmatched ')' — mark for removal
# remaining items in stack are unmatched '('
remove |= set(stack)
return "".join(ch for i, ch in enumerate(s) if i not in remove)
After the loop: stack holds indices of ( that never found a match; remove holds indices of ) that had nothing to match against. Remove them all.
Problem 4 — Score of Parentheses (LC #856)
Score rules: () = 1, (A) = 2 × score(A), AB = score(A) + score(B).
def scoreOfParentheses(s: str) -> int:
stack = [0] # current score at current depth
for ch in s:
if ch == '(':
stack.append(0) # open new depth level
else:
v = stack.pop() # score of what was inside
stack[-1] += max(2 * v, 1) # "()" scores 1; "(A)" scores 2*A
return stack[0]
Frame-by-frame: "(()(()))"
stack=[0]
'(' → [0, 0]
'(' → [0, 0, 0]
')' → pop 0. max(2*0,1)=1. stack=[0, 1]
'(' → [0, 1, 0]
'(' → [0, 1, 0, 0]
')' → pop 0 → 1. stack=[0, 1, 1]
')' → pop 1 → max(2,1)=2. stack=[0, 3]
')' → pop 3 → max(6,1)=6. stack=[6]
Answer: 6 ✅
The toolkit summary
| Problem | Tool | Key idea |
|---|---|---|
| Valid (single type) | Counter | never go negative; end at 0 |
| Valid (multiple types) | Stack of chars | pop and compare type on ) |
| Longest valid substring | Stack of indices | track last unmatched boundary |
| Longest valid (O(1) space) | Two-pass counter | scan LR then RL |
| Minimum removals | Stack of indices + remove set | unmatched = must remove |
| Score | Stack of integers | depth-level accumulator |
Common traps
Watch out for these
- Using a counter for multiple bracket types. A counter can't distinguish
([)]from([]). For mixed types, always use a stack that stores the actual character. - Forgetting the initial
-1sentinel in the longest-valid stack approach. Without it, the firsti - stack[-1]computation fails or gives the wrong length. - Resetting to
opens = closes = 0in the counter approach only on left-to-right scan. The left-to-right scan misses cases where opens accumulate at the end without closes (e.g.,"((()"). The right-to-left scan catches these. Always do both passes. - Returning
stackindices in Minimum Removal, not the count. The problem asks for the resulting string, not just the count. Collect indices into aremoveset, then filter the original string character by character.
Remember this forever
Parentheses — 4 tools
Valid (single type): counter ≥ 0 throughout, == 0 at end.
Valid (multiple types): stack of chars; pop and check type match on ).
Longest valid: stack of indices with -1 sentinel:
stack = [-1]
for i, ch in enumerate(s):
if ch == '(': stack.append(i)
else:
stack.pop()
if not stack: stack.append(i) # new boundary
else: best = max(best, i - stack[-1])
Min removal: stack tracks unmatched (; remove set catches unmatched ).
Key trap: always start the longest-valid stack with [-1].
Check yourself
Why does the two-pass counter approach for longest valid parentheses need both a left-to-right AND right-to-left scan?
Left-to-right: resets when closes > opens (catches ) that break validity early). But for "(()", the left scan sees opens=2, closes=1 at the end — they never equalise and never exceed, so the valid "()" inside is counted via the equal step at i=2. However, "((()" would fail to find the inner () correctly without also scanning right-to-left, which catches the case where excess openers accumulate at the end. The right-to-left scan with reversed roles (open=")", close="(") handles trailing unmatched ( sequences.
In the Score of Parentheses solution, why does `max(2 * v, 1)` correctly handle both `()` and `(A)`?
When we see ), we pop the score accumulated inside the matching (. If nothing was nested inside (plain ()), v = 0 and max(2*0, 1) = 1 — correct base score. If something was nested (v > 0), max(2*v, 1) = 2*v — doubling the inner score. The max handles both cases in one expression without an if.
Practice problems
| Problem | Difficulty | What to notice | Link |
|---|---|---|---|
| Valid Parentheses | Easy | Stack for multiple types; counter for single | LC #20 |
| Longest Valid Parentheses | Hard | Stack of indices with −1 sentinel | LC #32 |
| Minimum Remove to Make Valid | Medium | Stack + index-based remove set | LC #1249 |
| Score of Parentheses | Medium | Depth-level integer stack; max(2*v, 1) | LC #856 |
| Generate Parentheses | Medium | Backtracking (Chapter 12.8) — different family | LC #22 |
Next up: String Building / Simulation — how to build a result string step by step when decoding nested structures (like "3[a2[bc]]") or evaluating expressions with a stack.