DSA sheet · Stack · Pattern 4: parentheses & scoring

Score of Parentheses

The third question of the parentheses pattern, and the first one about scoring. The teacher warns that it feels hard the first time but is unforgettable once it clicks. She solves it twice: first with a stack of scores, where the strange-looking trick is to push a 0 before starting and a 0 for every opener; then with no stack at all, using only the current depth and a bit shift (1 << depth). Both are O(n) time; the second is O(1) space.

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 can only add to the top and remove from the top: LIFO, Last In First Out.

operationPython listnote
push xstack.append(x)O(1)
popstack.pop()removes and returns the top, O(1)
peekstack[-1]read the top; you may also change it: stack[-1] += 5
empty?not stackcheck before pop / peek
Safety rulePopping or peeking an empty list raises IndexError. In Part A we push a "floor" 0 at the very start; it guarantees the stack is never empty when we peek, as long as the input is balanced (which this problem promises).

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

The parentheses pattern, scoring flavour

In Valid Parentheses the stack held the openers still waiting. A closer always finished the most recent opener. Here the same is true, but instead of the opener character we keep a number for each open level: "the score collected so far inside this bracket". When the bracket closes, we turn that number into the bracket's score and hand it to the level outside. The stack lets us look back to the outer levels, and the teacher's rule is: whenever you need to look back, think stack (it turns an O(n²) look-back into O(n)).

The counter trick

Only ( and ) appear, one family. So in Part B we replace the stack with a single integer depth (how many brackets are open right now), which gives O(1) space.

Bit shift in one line

1 << k means "write 1 in binary and add k zeros on the right", which is 2k. 1<<0 = 1, 1<<1 = 2 (binary 10), 1<<2 = 4 (binary 100), 1<<3 = 8.

Part A · Stack of scores

LeetCode 856

1The question in simple words

You get a balanced string of ( and ) (every opener has its closer in the right place, as in Valid Parentheses). Compute its score with three rules:

rulemeaningexample
() = 1an empty pair is worth 1() → 1
AB = A + Bside by side (siblings) → add()() → 1 + 1 = 2
(A) = 2 × Awrapped in one more bracket (nested) → double(()) → 2 × 1 = 2

The teacher's examples:

  ( ( ( ) ) ( ) )
      └─┘          inner ()           = 1
    └─────┘        wrapped once       = 2 × 1 = 2
            └─┘    sibling ()         = 1      → siblings add: 2 + 1 = 3
  └─────────────┘  outer bracket      = 2 × 3 = 6

2What the constraints tell us

3Intuition: what does a closer need to know?

When we hit a ), we must decide this bracket's score. Two numbers decide it:

  1. The score of what's inside this bracket (call it v). If nothing is inside, v = 0 and the bracket is worth 1. Otherwise it's worth 2 × v. In one formula: max(2 * v, 1).
  2. The score already collected just before this bracket opened, at the same level (its earlier siblings). Siblings add, so our bracket's score must be added to that.

So the stack keeps, for every currently open level, "the score collected so far at that level". The top is the level inside the bracket we're closing (that's v, to be multiplied); the one below it is the level outside (to be added to).

4Building the logic from examples

Why push 0 for every opener?

Opening a bracket starts a new level where nothing has been scored yet. So push 0. The closer will later read that number.

Why push one extra 0 before we even start?

That bottom 0 stands for "the whole string's level", the score collected outside every bracket. When a top-level bracket like the first () in ()(()) closes, its score has to be added somewhere. The extra 0 is that place. At the end it holds the total, which is the answer.

Example ()(()): the stack design

When we reach the first ) of (()) (index 4), the stack is [1, 0, 0]: bottom 1 = the earlier sibling (), then 0 for the outer bracket, then 0 for the inner bracket. The inner pair becomes max(2×0, 1) = 1 and is added to the level below → [1, 1]. At index 5 the outer bracket closes: v = 1 → 2 × 1 = 2, added to the bottom → [3]. Answer 3 = 1 + 2 ✓. The teacher's summary: the top gets multiplied, the one under it gets added to.

The catch: 2 × 0 = 0, so use max

For the simplest (), v = 0, and 2 × 0 = 0. But () must score 1. So the bracket's score is max(2 * v, 1): if anything was inside, double it; if nothing was inside, it's 1.

Doubt: could 2 * v be 1 or less when something is inside?
→ No. Any non-empty inside contains at least one (), so v ≥ 1 and 2v ≥ 2. So max(2v, 1) picks 1 exactly when v = 0 (an empty pair), and 2v otherwise. It is the rules ()=1 and (A)=2A in one line.

The two pops

On a closer the teacher pops twice: once to get v (the inside score), and once to get the outer level's score, then pushes back "outer + this bracket". In Python it's simpler to pop once and add straight into the new top: stack[-1] += score. Same effect: pop v, pop outer, push outer + score.

5Approach steps

  1. stack = [0]: the score of the outermost level.
  2. For each character: ( → push 0 (a new level starts).
  3. ) → v = stack.pop() (inside score); score = max(2 * v, 1); add it to the level outside: stack[-1] += score.
  4. At the end, the only item left is the total → return stack[-1] (or pop it).

6Code (Python)

Score of Parentheses, stack
class Solution:
    def scoreOfParentheses(self, s: str) -> int:
        stack = [0]                     # score of the outermost level
        for ch in s:
            if ch == '(':
                stack.append(0)         # a new level, nothing scored inside yet
            else:
                v = stack.pop()         # what this bracket contains
                score = max(2 * v, 1)   # () = 1, (A) = 2*A
                stack[-1] += score      # siblings add: give it to the outer level
        return stack.pop()

7Code line by line

linewhat it means
stack = [0]The "floor": the score outside all brackets. It collects top-level siblings and finally holds the answer.
stack.append(0)An opener starts a level whose inside score is 0 for now.
v = stack.pop()The closer ends that level. v = total of everything inside it.
score = max(2 * v, 1)Empty inside → 1; otherwise double the inside (nesting).
stack[-1] += scoreThis bracket is a sibling of whatever was already scored at the outer level → add. (The teacher pops it and pushes the sum; same thing.) Safe: the floor 0 means there's always something below.
return stack.pop()Every level is closed; only the floor remains, holding the total.

8Dry run: the teacher's "messy" example ((()()))()

  ( ( ( ) ( ) ) ) ( )
  0 1 2 3 4 5 6 7 8 9      index
      └─┘ └─┘              two inner ()        = 1 + 1 = 2
    └─────────┘            wrapped once        = 4
  └─────────────┘          wrapped again       = 8
                  └─┘      sibling ()          = 1     → total 9
ichwhat we pop (and why)what we push / addstack afteranswer so far
start——floor 0[0]0
0(—push 0[0, 0]0
1(—push 0[0, 0, 0]0
2(—push 0[0, 0, 0, 0]0
3)v = 0 (empty pair)max(0, 1) = 1 added to top[0, 0, 1]0
4(—push 0[0, 0, 1, 0]0
5)v = 0 (empty pair)1 added: 1 + 1[0, 0, 2]0
6)v = 2 (two pairs inside)max(4, 1) = 4 added: 0 + 4[0, 4]0
7)v = 4max(8, 1) = 8 added: 0 + 8[8]8
8(—push 0[8, 0]8
9)v = 01 added: 8 + 1[9]9
after i = 2 (deepest)
0 floor000
after i = 5
0 floor02
after i = 8
8 floor0

Look at i = 8: the extra 0 pushed for the last ( keeps that new bracket's score separate from the 8, so the 8 is added to (8 + 1), not doubled. That's exactly why we push a 0 per opener and keep a floor below.

9Complexity & remember

Remember (stack)Start with [0]. ( → push 0. ) → v = pop; add max(2v, 1) to the new top. Answer = the last number left. Top gets doubled, the one below gets added to.

Part B · Space optimised: depth counter + shift

1The question (same)

Same balanced string, same scoring rules. Now with O(1) extra space.

2Constraints

One bracket family and a balanced string → the counter trick applies. Depth ≤ 25, so 1 << depth is a small number.

3Intuition: only the empty pairs score, and their depth decides how much

The stack solution works inside → out: score the inner pair, then double it on the way out. The teacher flips it to outside → in: when we meet an empty pair (), we already know how many brackets wrap it (the current depth). Each wrap doubles it, so this () contributes

1 << (number of brackets around it) = 2depth

And the total score is just the sum over all empty pairs. Why? Doubling spreads over a sum: 2 × (A + B) = 2A + 2B. So instead of adding the insides and then doubling, we can double each () separately by its own depth and add everything at the end.

Check with ((())): one empty pair, wrapped by 2 brackets → 1 << 2 = 4 (binary 100) ✓. And ((()()))(): two pairs at depth 2 → 4 + 4, one pair at depth 0 → 1. Total 9 ✓.

4Building the logic from examples

Keep a depth count

( → depth += 1. ) → depth -= 1. After decreasing at a closer, depth equals the number of brackets around the pair that just closed. That's the shift amount.

Which closers score? Only the ones right after an opener

In ((()()))(), the closer at index 6 closes a bracket that has stuff inside. Its value was already counted when we scored the inner ()s at their depth (their 1s already include the doubling from this bracket). Counting it again would double count. So: score only when s[i-1] == '(', which means "this closer finishes an empty pair". Every other closer just decreases the depth.

Doubt: can s[i-1] go out of range when i = 0?
→ A balanced string never starts with ), so a closer is never at index 0. In Python, s[-1] wouldn't even crash; it would read the last character, so the guarantee is what keeps us correct, not luck.

5Approach steps

  1. depth = 0, score = 0.
  2. For each index i: if s[i] == '(' → depth += 1.
  3. Else → depth -= 1; if s[i-1] == '(' → score += 1 << depth.
  4. Return score.

6Code (Python)

Score of Parentheses, O(1) space
class Solution:
    def scoreOfParentheses(self, s: str) -> int:
        depth = 0                       # brackets open right now
        score = 0
        for i in range(len(s)):
            if s[i] == '(':
                depth += 1
            else:
                depth -= 1              # now = brackets AROUND this pair
                if s[i - 1] == '(':     # an empty pair "()" just closed
                    score += 1 << depth # 2 ** depth
        return score

7Code line by line

linewhat it means
depth += 1One more bracket is open (counter version of "push").
depth -= 1This bracket closes (counter version of "pop"). Done first, so depth now counts only the wrappers.
if s[i - 1] == '(':Only an empty pair () creates score. Closers of non-empty brackets were already accounted for by their inner pairs.
score += 1 << depth1 doubled once per wrapping bracket = 2depth. Siblings simply add up.

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

index0123456789
s((()()))()yellow = the three empty pairs that score
ichs[i−1]depth afteraddsscore so far
0(—1—0
1(—2—0
2(—3—0
3)(21 << 2 = 44
4(—3—4
5)(21 << 2 = 48
6))1nothing (not an empty pair)8
7))0nothing8
8(—1—8
9)(01 << 0 = 19

Same answer as the stack (9), with just two integers.

9Complexity & remember

Remember (depth)( → depth++. ) → depth--, and if the previous char was (, add 1 << depth. Every empty pair is worth 2(brackets around it).

Part C · Revision page

Stack of scoresDepth + shift
directioninside → out (score the inner, double on exit)outside → in (know the depth when an empty pair appears)
on (push 0depth += 1
on )v = pop; top += max(2v, 1)depth −= 1; if previous is (: score += 1 << depth
start[0] (the floor)depth = score = 0
answerthe last number on the stackscore
time / spaceO(n) / O(n)O(n) / O(1)
If you remember only 5 lines 1. Rules: () = 1, siblings add, nesting doubles.
2. Stack: start with a floor 0; push 0 on (.
3. On ): pop v, add max(2v, 1) to the new top.
4. No stack: each empty pair () is worth 1 << depth; add them up.
5. Score only when the previous character is (.
Mistakes to avoid ✗ forgetting the initial floor 0 (the first top-level ) has nowhere to add, crash)
✗ using 2 * v without max(…, 1) (every () scores 0)
✗ doubling the outer level instead of adding to it (siblings must add)
✗ in the depth version, scoring every closer (double counting)
✗ shifting before decreasing depth (off by one: () would score 2)
test it yourself (paste under either solution)
s = Solution()
print(s.scoreOfParentheses("()"))          # 1
print(s.scoreOfParentheses("()()"))        # 2
print(s.scoreOfParentheses("(())"))        # 2
print(s.scoreOfParentheses("((()))"))      # 4
print(s.scoreOfParentheses("()(())"))      # 3
print(s.scoreOfParentheses("((())())"))    # 6
print(s.scoreOfParentheses("((()()))()"))  # 9

Based on this video: Score of Parentheses | Stack pattern: parentheses & scoring