Solving tips
- Recognize nested delimiters as a stack problem: at each '[' suspend the string built so far plus the count you just read, and at ']' resume that most-recent frame.
- Accumulate multi-digit counts with num = num*10 + int(ch), not num = int(ch), and reset cur and num to empty after pushing on '['.
- On ']' the combine order is prev + cur * k (not cur * k + prev), and the count applies to the whole block so don't multiply early at '['.
- Target O(m) time and space where m is the decoded output length; the equivalent recursive-descent version just lets the call stack do the bookkeeping.
Problem
You get an encoded string that uses the rule k[segment]: the segment inside the square brackets is repeated exactly k times (k is a positive integer, always present before a [). Encodings can nest β a bracketed segment may itself contain more k[...] blocks. Plain lowercase letters outside any brackets are copied through unchanged, and digits appear only as repeat counts.
Return the fully decoded string. The input is guaranteed well-formed.
Examples
"3[a]2[bc]" β "aaabcbc" β a three times, then bc twice.
"3[a2[c]]" β "accaccacc" β inner 2[c] becomes cc, so the outer block repeats acc three times.
"2[abc]3[cd]ef" β "abcabccdcdcdef" β two blocks, then a plain tail ef.
Constraints
1 <= s.length <= 30
1 <= k <= 300; the decoded output is guaranteed to fit (at most ~10^5 characters)
s contains only lowercase letters, digits, and square brackets, and is always valid
The input is tiny, but nesting means the output can be large β the expected solution is a single left-to-right pass, linear in the output size.
Think about it first
Hint 1
`"3[a2[c]]"` can't be decoded left to right naively: when you meet the first `]`... which `[` does it close? The most recently opened one β that word again.
Hint 2
Build the current segment as you scan. When you hit `[`, you must *suspend* what you've built (and the repeat count you just read) and start fresh; when you hit `]`, you resume the suspended work. Suspend/resume in last-in-first-out order is a stack of `(previous_string, count)` frames.
Hint 3
One pass: accumulate multi-digit `num` on digits; on `[` push `(current, num)` and reset both; on `]` pop `(prev, k)` and set `current = prev + current * k`; on a letter, append it. `current` at the end is the answer.
TL;DR
One pass with a stack of (previous_string, repeat_count) frames β O(m) time and space, where m is the decoded output length.
Approach 1 β Brute force (expand innermost blocks repeatedly)
An innermost block k[letters] contains no nested brackets, so it can be expanded by plain text substitution. Do that repeatedly until no brackets remain.
import re
class Solution:
def decodeString(self, s: str) -> str:
block = re.compile(r"(\d+)\[([a-z]*)\]")
def expand(m: re.Match) -> str:
count = int(m.group(1))
return count * m.group(2)
while "[" in s:
s = block.sub(expand, s)
return s
Complexity: O(d Β· m) time, where d is the nesting depth and m the output size β each sweep rebuilds the whole (growing) string once per nesting level; O(m) space.
Why the constraints kill it: here it actually passes (the input is only 30 chars), but each level of nesting re-copies the entire partially-decoded string, and with counts up to 300 the intermediate strings are rebuilt wholesale β pure wasted copying that the stack pass avoids. It also leans on the regex engine instead of demonstrating the parsing skill being tested.
Approach 2 β Stack of suspended frames
The insight: decoding is interrupted work. While building some segment, a [ forces you to shelve both the text built so far and the repeat count you just read, then start a fresh segment; the matching ] resumes exactly the most recently shelved frame. LIFO suspend/resume is a stack of (previous_string, count) pairs.
class Solution:
def decodeString(self, s: str) -> str:
stack: list[tuple[str, int]] = [] # (string built before '[', its count)
cur = ""
num = 0
for ch in s:
if ch.isdigit():
num = num * 10 + int(ch) # counts can be multi-digit
elif ch == "[":
stack.append((cur, num))
cur, num = "", 0
elif ch == "]":
prev, k = stack.pop()
cur = prev + cur * k
else:
cur += ch
return cur
Walkthrough on "3[a2[c]]":
| ch | action | num | cur | stack |
|---|
3 | accumulate digit | 3 | "" | β |
[ | push ("", 3), reset | 0 | "" | ("",3) |
a | append | 0 | "a" | ("",3) |
2 | accumulate digit | 2 | "a" | ("",3) |
[ | push ("a", 2), reset | 0 | "" | ("",3) ("a",2) |
c | append | 0 | "c" | ("",3) ("a",2) |
] | pop ("a",2): "a" + "c"*2 | 0 | "acc" | ("",3) |
] | pop ("",3): "" + "acc"*3 | 0 | "accaccacc" | β |
Result: "accaccacc".
Complexity: O(m) time β every output character is written O(1) amortized times (string concatenation in a loop is fine at these sizes; use a list-of-parts if paranoid). O(m) space for the stack frames and result.
Approach 3 β Recursive descent
The insight: the grammar is recursive (segment β letters | k[segment] ...), so let the call stack be the stack. A helper consumes characters via a shared index and calls itself on each bracketed block, returning when it sees ]. This is a textbook recursive descent parser β one mutually recursive function per grammar rule, consuming tokens left to right.
class Solution:
def decodeString(self, s: str) -> str:
self.i = 0
def parse() -> str:
parts: list[str] = []
num = 0
while self.i < len(s):
ch = s[self.i]
self.i += 1
if ch.isdigit():
num = num * 10 + int(ch)
elif ch == "[":
parts.append(num * parse()) # recurse into the block
num = 0
elif ch == "]":
break # this block is done
else:
parts.append(ch)
return "".join(parts)
return parse()
Walkthrough on "3[a2[c]]": the outer parse reads 3, sees [, and recurses. The inner call collects "a", reads 2, recurses again; the innermost call collects "c", hits ], returns "c", which becomes "cc". The middle call hits the next ] and returns "acc", which the outer call multiplies to "accaccacc".
Complexity: O(m) time, O(m) space (recursion depth = nesting depth, plus the output).
Common pitfalls
- Single-digit assumption: counts like
12[ab] need num = num * 10 + int(ch), not num = int(ch).
- Forgetting to reset
cur and num after pushing on [ β the new block must start empty.
- Order of concatenation on
]: itβs prev + cur * k; writing cur * k + prev scrambles nested output.
- Multiplying too early: the count applies to the entire bracketed block, which you donβt know until
] β resist decoding at the [.
Pattern takeaway
When input has nested structure delimited by open/close markers, a stack holds the suspended outer context while you work on the inner one β push your partial state at every opener, pop and combine at every closer. The recursive version is the same idea with the call stack doing the bookkeeping; recognizing that equivalence (explicit stack β recursion) is the transferable skill for every parser-shaped interview problem.