Skip to content
← All posts

Maximum Nesting Depth of Two Valid Parentheses Strings: Depth-Based Splitting

6 min read
leetcodeproblemmediumstringsstacks

You are given a valid parentheses string seq. You need to split it into two subsequences, A and B, such that both A and B are valid parentheses strings (or empty), and max(depth(A), depth(B)) is minimized. Return an array answer of the same length as seq, where answer[i] is 0 if seq[i] belongs to A and 1 if it belongs to B.

This is LeetCode 1111: Maximum Nesting Depth of Two Valid Parentheses Strings, and it is one of the best examples of how a greedy insight about structure can turn a seemingly complex splitting problem into a single-pass solution.

(d=1(d=2)d=2(d=2(d=3)d=3)d=2(d=2)d=2)d=1BAAABBAAAB0123456789A (even depth)B (odd depth)
The string "(()(()()())) " with each paren labeled by its nesting depth. Even-depth parens go to group A (blue), odd-depth to group B (orange).

Why this problem matters

Parentheses nesting is a recurring theme across interview problems. You have probably seen problems about validating parentheses, counting scores, or generating all valid combinations. This problem goes deeper: it asks you to decompose a nested structure into two balanced pieces that share the nesting load as evenly as possible.

The technique it teaches -- splitting based on depth parity -- shows up whenever you need to partition nested or hierarchical structures. It is a beautiful example of how understanding the structure of a problem (nesting depth) leads directly to an optimal strategy, without needing dynamic programming or backtracking.

The key insight

If a string has maximum nesting depth d, the best you can do is split it so that one subsequence handles roughly half the depth and the other handles the rest. The optimal result is ceil(d / 2).

The way to achieve this is surprisingly simple: assign parentheses at even nesting depths to group A and parentheses at odd nesting depths to group B. This interleaving ensures that no single group accumulates more than half the depth.

Why does this work? Think of nesting depth like layers of an onion. Layer 1 wraps layer 2, which wraps layer 3, and so on. If you give every other layer to a different group, neither group ever has two consecutive layers. The maximum depth of either group is at most ceil(d / 2).

The solution

def max_depth_after_split(seq: str) -> list[int]:
    result = []
    depth = 0

    for ch in seq:
        if ch == '(':
            depth += 1
            result.append(depth % 2)
        else:
            result.append(depth % 2)
            depth -= 1

    return result

Let's walk through what each piece does.

We maintain a depth counter that tracks how deeply nested we are at each point. For every opening parenthesis, we increment depth before assigning, because the open paren is entering a new level. For every closing parenthesis, we assign before decrementing, because the close paren is still at its matching open paren's level.

The assignment itself is just depth % 2. When depth is odd, the result is 1 (group B). When depth is even, the result is 0 (group A). That single modulo operation is the entire decision logic.

Notice that we do not need to actually construct the two subsequences or verify they are valid. The depth-parity rule guarantees that matching open and close parens always end up in the same group, which means both subsequences are automatically valid.

The key trick is the order of operations: for (, increment depth first, then record depth % 2. For ), record depth % 2 first, then decrement depth. This ensures each closing paren gets the same group assignment as its matching opening paren.

Visual walkthrough

Initial state. depth = 0, no assignments yet.

(d=0(d=0)d=0(d=0(d=0)d=0)d=0(d=0)d=0)d=0-1-1-1-1-1-1-1-1-1-1ans:

We will scan left to right, tracking depth and assigning each paren to A (0) or B (1).

i=0: '(' opens. depth becomes 1 (odd) -> assign to B.

(d=1(d=0)d=0(d=0(d=0)d=0)d=0(d=0)d=0)d=01-1-1-1-1-1-1-1-1-1ans:

depth = 1. Odd depth means this paren goes to group B. ans[0] = 1.

i=1: '(' opens. depth becomes 2 (even) -> assign to A.

(d=1(d=2)d=0(d=0(d=0)d=0)d=0(d=0)d=0)d=010-1-1-1-1-1-1-1-1ans:

depth = 2. Even depth means this paren goes to group A. ans[1] = 0.

i=2: ')' closes. depth is 2 (even) -> assign to A. depth drops to 1.

(d=1(d=2)d=2(d=0(d=0)d=0)d=0(d=0)d=0)d=0100-1-1-1-1-1-1-1ans:

The closing paren matches the open paren at depth 2, so it also goes to A.

i=3: '(' opens. depth becomes 2 (even) -> assign to A.

(d=1(d=2)d=2(d=2(d=0)d=0)d=0(d=0)d=0)d=01000-1-1-1-1-1-1ans:

depth = 2 again. Even depth -> group A. ans[3] = 0.

i=4: '(' opens. depth becomes 3 (odd) -> assign to B.

(d=1(d=2)d=2(d=2(d=3)d=0)d=0(d=0)d=0)d=010001-1-1-1-1-1ans:

depth = 3. Odd depth -> group B. ans[4] = 1.

i=5: ')' closes. depth is 3 (odd) -> assign to B. depth drops to 2.

(d=1(d=2)d=2(d=2(d=3)d=3)d=0(d=0)d=0)d=0100011-1-1-1-1ans:

Matches the open at depth 3, so it also goes to B.

i=6: ')' closes. depth is 2 (even) -> assign to A. depth drops to 1.

(d=1(d=2)d=2(d=2(d=3)d=3)d=2(d=0)d=0)d=01000110-1-1-1ans:

Matches the open at depth 2, goes to A.

i=7: '(' opens. depth becomes 2 (even) -> assign to A.

(d=1(d=2)d=2(d=2(d=3)d=3)d=2(d=2)d=0)d=010001100-1-1ans:

depth = 2. Even depth -> group A. ans[7] = 0.

i=8: ')' closes. depth is 2 (even) -> assign to A. depth drops to 1.

(d=1(d=2)d=2(d=2(d=3)d=3)d=2(d=2)d=2)d=0100011000-1ans:

Matches the open at depth 2, goes to A.

i=9: ')' closes. depth is 1 (odd) -> assign to B. depth drops to 0. Done.

(d=1(d=2)d=2(d=2(d=3)d=3)d=2(d=2)d=2)d=11000110001ans:

Final answer: [1, 0, 0, 0, 1, 1, 0, 0, 0, 1]. Group A has max depth 2, group B has max depth 1. max(2, 1) = 2.

Notice how the depth counter rises and falls as we scan left to right. Every time it hits an odd value, we assign to B. Every time it hits an even value, we assign to A. The final result splits the original depth-3 string into group A (max depth 2) and group B (max depth 1), achieving an overall max of 2 -- which is ceil(3 / 2).

Complexity analysis

ApproachTimeSpace
Depth parityO(n)O(n)

Time is O(n) where n is the length of the input string. We make a single pass through the string, doing constant work per character.

Space is O(n) for the result array. The depth counter itself is O(1) extra space. If the problem allowed in-place modification (unlikely for this problem format), the auxiliary space would be O(1).

The building blocks

1. Depth tracking with a counter

depth = 0
for ch in seq:
    if ch == '(':
        depth += 1
    else:
        depth -= 1

This is the simplest way to track nesting depth without a full stack. Every ( increases depth by one, every ) decreases it. You will reuse this in problems like "Score of Parentheses," "Minimum Remove to Make Valid Parentheses," and any problem that needs to know "how deep are we right now?"

2. Parity-based assignment

if ch == '(':
    depth += 1
    result.append(depth % 2)
else:
    result.append(depth % 2)
    depth -= 1

The parity trick -- using depth % 2 to alternate between two groups -- is a general strategy for balanced partitioning. Whenever you need to split items into two buckets based on their level in a hierarchy, parity is the first thing to try. The order-of-operations detail (increment before assign for open, assign before decrement for close) is what makes matching pairs land in the same group.

Edge cases

  • Empty string: depth never changes, result is an empty array.
  • Flat string like ()()(): max depth is 1. Every paren is at depth 1 (odd), so everything goes to B. Group A is empty (depth 0), group B has depth 1. max(0, 1) = 1.
  • Deeply nested string like `(((()))): depth goes 1, 2, 3, 4, 4, 3, 2, 1. Parens alternate between A and B, splitting depth 4 into two depth-2 subsequences.
  • Already minimal depth: if max depth is 1, you cannot improve beyond 1. The algorithm handles this correctly.
  • Odd vs even max depth: when max depth is odd (like 3), the split is ceil(3/2) = 2 and floor(3/2) = 1. When even (like 4), the split is perfectly balanced at 2 and 2.

From understanding to recall

You have seen how depth parity cleanly splits a parentheses string into two balanced subsequences. The logic is just a counter and a modulo -- the code practically writes itself once you know the insight.

But the details that trip people up under pressure are real. Do you increment depth before or after assigning for an open paren? What about for a close paren? If you get the order wrong, matching pairs end up in different groups and the subsequences are invalid. These are not conceptual errors. They are recall errors, and they show up when you have not practiced the pattern enough.

Spaced repetition is how you lock in these details. You write the depth tracker, the parity assignment, and the increment order from memory at increasing intervals. After a few rounds, you do not second-guess the order of operations. You just write it.

Related posts

CodeBricks breaks this problem into its depth-tracking and parity-assignment building blocks, then drills them independently with spaced repetition. You type each piece from scratch until the pattern is automatic. When a parentheses splitting problem appears in your interview, you do not hesitate. You just write it.