Learn/DSA Patterns
DSA PatternsStringsmedium11 min read

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.

#stack#parentheses#brackets#strings#greedy#beginner#interview
Table of contents

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 emptyTrue

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

ProblemToolKey idea
Valid (single type)Counternever go negative; end at 0
Valid (multiple types)Stack of charspop and compare type on )
Longest valid substringStack of indicestrack last unmatched boundary
Longest valid (O(1) space)Two-pass counterscan LR then RL
Minimum removalsStack of indices + remove setunmatched = must remove
ScoreStack of integersdepth-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 -1 sentinel in the longest-valid stack approach. Without it, the first i - stack[-1] computation fails or gives the wrong length.
  • Resetting to opens = closes = 0 in 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 stack indices in Minimum Removal, not the count. The problem asks for the resulting string, not just the count. Collect indices into a remove set, 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

ProblemDifficultyWhat to noticeLink
Valid ParenthesesEasyStack for multiple types; counter for singleLC #20
Longest Valid ParenthesesHardStack of indices with −1 sentinelLC #32
Minimum Remove to Make ValidMediumStack + index-based remove setLC #1249
Score of ParenthesesMediumDepth-level integer stack; max(2*v, 1)LC #856
Generate ParenthesesMediumBacktracking (Chapter 12.8) — different familyLC #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.