DSA sheet · Stack · Pattern 4: parentheses & scoring

Longest Valid Parentheses

The last (and only "hard") question of the parentheses pattern. The teacher promises it won't feel hard by the end, and teaches three solutions, each one built from the one before:

  1. Brute force: check every substring with a counter-based validity test. O(n³), TLE.
  2. Stack of indexes: instead of brackets we store positions, with a -1 at the bottom, so a length is just i − stack[-1]. O(n) time, O(n) space.
  3. Two passes with two counters (left → right, then right → left). O(n) time, O(1) space.

Prerequisite: Valid Parentheses. She insists you do that one first.

Every part below follows the same order:
① the question in simple words → ② what the constraints tell us → ③ intuition → ④ building the logic from examples → ⑤ approach steps → ⑥ code → ⑦ code line by line → ⑧ dry run → ⑨ complexity & remember

Part 0 · Before starting

What is a stack?

A pile where you add and remove only at the top: LIFO, Last In First Out.

operationPython listnote
push xstack.append(x)O(1)
popstack.pop()remove + return the top, O(1)
peekstack[-1]read the top
empty?not stackcheck before pop / peek
Safety ruleNever pop() or [-1] an empty list (IndexError). In Part B the stack can become empty right after a pop, so we always check if not stack before peeking. That check is also part of the logic: "empty" means "this closer had no partner".

Drawings: stack written left = bottom, right = top. Vertical pictures: top in red.

The parentheses pattern

A closer pairs with the nearest unmatched opener, which is the stack's top. Opener → push; closer → pop its partner. Up to now we pushed characters. Here we need lengths, and a length is a difference of two positions, so we push indexes instead.

The counter trick

With only ( and ), a count can replace the stack when all we need is "how many openers are waiting". Here that idea becomes two counters, left (number of ( seen) and right (number of ) seen), in Part C.

Substring length

A substring from index l to index r (both included) has length r − l + 1. If instead you know the index just before it starts (l − 1), the length is r − (l − 1), with no "+1". Part B uses this second form.

Part A · Brute force: every substring + counter check

LeetCode 32

1The question in simple words

Given a string of ( and ), find the longest substring (a continuous piece) that is a valid, balanced parentheses string. Return its length.

index012345
")()())")()())indexes 1..4 = ()() → length 4 − 1 + 1 = 4
"(()"(()answer 2
""emptyanswer 0

2What the constraints tell us

3Intuition

The most direct idea: try every substring, ask "is this valid?", and remember the longest valid one. Two loops (i = start, j = end) generate the substrings, and a helper checks each one.

4Building the logic: a cheaper validity check

We could reuse the Valid Parentheses stack, but with only round brackets a counter is enough. Walk through the substring: ( → count += 1, ) → count -= 1.

Count goes negative → invalid immediately

Take ()): count 1, 0, −1. Negative means we've seen more closers than openers so far. That extra ) can never be matched, because openers can only come from the left and we've already passed them. So the substring is invalid → return False at once.

Count ends at zero → valid; ends positive → invalid

(()()): 1, 2, 1, 2, 1, 0 → never negative, ends at 0 → valid. But ((): 1, 2, 1 → ends at 1 → one opener never closed → invalid. Positive leftover = that many unclosed openers; negative = extra closers; only exactly 0 (and never negative on the way) is valid.

Small speed-ups in the loops

5Approach steps

  1. best = 0.
  2. For every start i, for every end j = i+1, i+3, i+5, …:
  3. If s[i..j] is valid (counter test) → best = max(best, j − i + 1).
  4. Return best.

6Code (Python)

brute force (correct, but TLE for big inputs)
class Solution:
    def longestValidParentheses(self, s: str) -> int:
        def is_valid(i, j):                  # is s[i..j] balanced?
            count = 0
            for k in range(i, j + 1):
                if s[k] == '(':
                    count += 1
                else:
                    count -= 1
                    if count < 0:            # extra ')' -> can never be fixed
                        return False
            return count == 0                # no unclosed '(' left

        n = len(s)
        best = 0
        for i in range(n):
            for j in range(i + 1, n, 2):     # even lengths only
                if is_valid(i, j):
                    best = max(best, j - i + 1)
        return best

7Code line by line

linewhat it means
count += 1 / count -= 1The counter version of push / pop (one bracket family).
if count < 0: return FalseMore ) than ( so far: this prefix is broken for good.
return count == 0Positive = unclosed openers left ⇒ not valid.
range(i + 1, n, 2)Ends that make the length 2, 4, 6, …
best = max(best, j - i + 1)Length of s[i..j], both ends included.

8Dry run: )()())

isubstrings tried (j)valid?best
0)(, )()(, )()())all start with ) → count −1 at once0
1() (j=2)yes → 22
1()() (j=4)yes → 44
2)(, )())no4
3() (j=4)yes → 24
4))no4
5none (no j)—4

9Complexity & remember

Remember (brute)Valid with one bracket family ⇔ the running count never goes negative and ends at 0. The brute force applies that to every even-length substring: O(n³).

Part B · Stack of indexes (with −1 at the bottom)

1The question (same)

Longest valid substring length, but now in one pass.

2Constraints

n up to 3 × 10⁴ → O(n) is about 3 × 10⁴ steps: instant. Empty string → the loop doesn't run → 0.

3Intuition: store positions, not brackets

Checking validity means looking back for a matching opener → stack (just like Valid Parentheses). But we want lengths. If the stack holds indexes, then when a closer at index i finishes a pair, we can measure how far back the valid run goes.

The key idea: after popping the partner, the new top is the index just before the current valid run starts. It's a "wall": either an unmatched ( still waiting, or the last unmatched ), or the imaginary -1 before the string. So the length of the valid run ending at i is i − stack[-1].

4Building the logic from examples

Why push −1 first? Reason 1: safety

Take the string ). A closer always pops (assuming the top is its opener). With an empty stack that pop would crash. So we put a -1 at the bottom first. Why −1? We're storing indexes, and 0, 1, 2… are all real positions; −1 is the one value that can't be a real index, so it can't be confused with one.

Reason 2: it makes the length formula work

Normally length = right − left + 1. In () at indexes 0..1, after popping index 0 the top is −1, and 1 − (−1) = 2 ✓. The −1 is "the index before the run", so we drop the +1. The teacher's picture: if a valid pair sits at indexes 2..3 and index 1 is still on the stack, the length is 3 − 1 = 2, the same as 3 − 2 + 1.

Popping leaves the stack empty → this closer had no partner → it becomes the new wall

For ) at index 0: pop the −1, now the stack is empty. That means this closer had no opener; nothing valid can extend across it. So we push this index as the new wall: future lengths are measured from here.

Doubt: should we push 0 or the current index i?
→ The teacher first says "push 0" (in her example the index happened to be 0), then corrects it to the current index i. Think of )()())()()()(): the ) at index 5 breaks things; the valid run after it must be measured from 5, not from 0. Pushing i marks exactly where the break happened.

Popping leaves something → measure

If the stack still has something after the pop, the closer matched an opener, and i − stack[-1] is the length of the valid run ending here. Update best.

5Approach steps

  1. stack = [-1], best = 0.
  2. For each index i: if s[i] == '(' → push i.
  3. Else (a closer) → pop (we assume the top is its opener).
  4. If the stack is now empty → push i (this ) is the new wall).
  5. Else → best = max(best, i − stack[-1]).
  6. Return best.

6Code (Python)

Longest Valid Parentheses, stack of indexes
class Solution:
    def longestValidParentheses(self, s: str) -> int:
        stack = [-1]                     # the wall before the string
        best = 0
        for i in range(len(s)):
            if s[i] == '(':
                stack.append(i)          # remember where this opener is
            else:
                stack.pop()              # remove its partner (or the wall)
                if not stack:            # no partner: this ')' is the new wall
                    stack.append(i)
                else:                    # valid run from stack[-1]+1 to i
                    best = max(best, i - stack[-1])
        return best

7Code line by line

linewhat it means
stack = [-1]A fake index before the string: keeps the first pop safe and makes i − top the length.
stack.append(i)An opener's position waits for its closer.
stack.pop()A closer removes the top: its opener if there is one, otherwise the wall.
if not stack: stack.append(i)We just popped the wall, so this ) was unmatched. It becomes the new wall.
best = max(best, i - stack[-1])Everything after the top index up to i is valid. Length = i − top (no +1 because top is the index before the run).

(When she ran it, the teacher had typed the wrong bracket in the if and fixed it on screen: make sure you compare with '('.)

8Dry run: )()())

ichwhat we pop (and why)what we pushstack afterbest so far
start——−1 (wall)[-1]0
0)−1: closer pops; stack becomes empty → no partner0 (new wall)[0]0
1(—1[0, 1]0
2)1: its partner. Top is 0 → length 2 − 0 = 2—[0]2
3(—3[0, 3]2
4)3: partner. Top is 0 → 4 − 0 = 4 (the run ()())—[0]4
5)0: the wall; stack empty → unmatched5 (new wall)[5]4
after i = 0
0 (wall)
after i = 3
0 (wall)3
after i = 5
5 (wall)

Second run, ()(() (an unmatched opener acting as the wall):

ichwhat we pop (and why)what we pushstack afterbest so far
0(—0[-1, 0]0
1)0: partner. Top −1 → 1 − (−1) = 2—[-1]2
2(—2[-1, 2]2
3(—3[-1, 2, 3]2
4)3: partner. Top is 2 (an opener still waiting) → 4 − 2 = 2—[-1, 2]2

The unclosed ( at index 2 stays on the stack and blocks the run, so we don't wrongly join () and () into 4.

9Complexity & remember

Remember (stack)Start [-1]. ( → push i. ) → pop; empty? push i (new wall) : best = max(best, i − top). The top is always "the index just before the current valid run".

Part C · Space optimised: left/right counters, two passes

1The question (same)

Same answer, but with O(1) extra space.

2Constraints

One bracket family again, so counting can replace remembering. Two O(n) passes = O(2n) = still O(n).

3Intuition

Walk left to right counting left = number of ( and right = number of ) since the last reset.

4Building the logic: why one pass is not enough

The teacher's counter-example: ((). Left to right: left 1, left 2, right 1. left stays ahead forever, so left == right never happens and the pass reports 0. But the answer is 2.

Her hint: "pause and think: what if we start from the right?" Going right to left over ((): ) → right 1; ( → left 1 → equal → length 2 ✓.

So do both passes and keep the max. But the reset rule must flip in the second pass:

passreset whenwhy
left → rightright > leftan extra ) can't get an opener from its future (the right side)
right → leftleft > rightmirror image: an extra (, seen from the right, can't get a closer from what's still to come (the left side). E.g. reading (( backwards, nothing further left can close them.
Doubt: why do the two passes together always find the answer?
→ The left-to-right pass only misses a valid piece when extra ( keep left above right around it (like the first ( in (()). In that situation, reading from the right, the closers come first, so the right-to-left pass sees the piece balance out exactly. Extra ) are the reverse: they're handled by resets in the first pass. Every longest valid piece is caught by at least one pass, and the tests check this against the brute force on thousands of strings.

5Approach steps

  1. left = right = best = 0.
  2. Pass 1, i from 0 to n−1: count. If equal → best = max(best, 2 * right); elif right > left → reset both.
  3. Reset left = right = 0.
  4. Pass 2, i from n−1 down to 0: count. If equal → update best; elif left > right → reset both.
  5. Return best.

6Code (Python)

Longest Valid Parentheses, O(1) space
class Solution:
    def longestValidParentheses(self, s: str) -> int:
        best = 0

        left = right = 0
        for ch in s:                         # pass 1: left -> right
            if ch == '(':
                left += 1
            else:
                right += 1
            if left == right:
                best = max(best, 2 * right)
            elif right > left:               # extra ')' : restart after it
                left = right = 0

        left = right = 0
        for ch in reversed(s):               # pass 2: right -> left
            if ch == '(':
                left += 1
            else:
                right += 1
            if left == right:
                best = max(best, 2 * left)
            elif left > right:               # extra '(' : restart before it
                left = right = 0

        return best

7Code line by line

linewhat it means
left = right = 0Counts of ( and ) since the last reset.
if left == right: best = max(best, 2 * right)Balanced piece: it has right pairs, so length 2 × right (or 2 × left, the same).
elif right > left: resetPass 1 only: the piece is broken for good; start again after this ).
for ch in reversed(s)Same walk from the other end, to catch pieces hidden by extra (.
elif left > right: resetPass 2 only: the mirror rule.

8Dry run: (() and )()())

(()

passichleftrightwhat happensbest
1 →0(10left ahead, keep going0
1 →1(20keep going0
1 →2)21still not equal: pass 1 finds nothing0
2 ←2)01right ahead (fine in this direction)0
2 ←1(11equal → 2 × 1 = 22
2 ←0(21left > right → reset2

)()()) (pass 1 already finds it)

ichleftrightwhat happensbest
0)01right > left → reset to 0, 00
1(10—0
2)11equal → 22
3(21—2
4)22equal → 44
5)23right > left → reset4

Pass 2 on this string (read backwards: ) ) ( ) ( )) keeps right ahead the whole time, so it never sees equal counts and adds nothing. The answer stays 4. That's fine: we only need one of the passes to see each piece.

9Complexity & remember

Remember (two passes)→ pass: reset when right > left. ← pass: reset when left > right. Whenever equal, best = max(best, 2 × right). One pass alone fails on (().

The teacher's study tip: three solutions in one video is a lot. Don't just watch; write all three yourself with pen and paper first, then type them into the editor.


Part D · Revision page

Brute forceStack of indexesTwo passes
ideacheck every even-length substring with a counterstack top = index before the current valid runcount ( and ), measure when equal, reset when the wrong side is ahead
startbest = 0stack = [-1]left = right = 0 (twice)
length formulaj − i + 1i − stack[-1]2 × right
"broken" signalcount < 0, or ends ≠ 0stack empty after pop → push i→ right > left; ← left > right
timeO(n³) TLEO(n)O(n)
spaceO(1)O(n)O(1)
pattern pagewhat the stack storescounter version
14 Valid Parenthesesopener charactersnot possible (3 families)
15 Minimum Addopener charactersopen + closing counters
16 Scorescore of each open leveldepth + 1 << depth
17 Longest Validindexes (walls)left/right, two passes
If you remember only 5 lines 1. Need lengths → push indexes, not brackets.
2. Start with [-1]; length = i − stack[-1].
3. A closer that empties the stack becomes the new wall: push i.
4. O(1) space: count left/right; equal → 2 × right; reset when the wrong side is ahead.
5. Do it left→right and right→left; flip the reset rule in the second pass.
Mistakes to avoid ✗ forgetting the initial −1 (crash on a leading ), and wrong lengths)
✗ pushing 0 instead of i when the stack empties
✗ using i − top + 1 (the top is the index before the run)
✗ only one counter pass (fails on (())
✗ same reset rule in both passes (the second must be left > right)
✗ resetting when left > right in the first pass (kills (()))
test it yourself (paste under any solution)
s = Solution()
print(s.longestValidParentheses("(()"))       # 2
print(s.longestValidParentheses(")()())"))    # 4
print(s.longestValidParentheses(""))          # 0
print(s.longestValidParentheses("()(()"))     # 2
print(s.longestValidParentheses("()(())"))    # 6
print(s.longestValidParentheses("(()))())(")) # 4

Based on this video: Longest Valid Parentheses | Stack pattern: parentheses & scoring