Start learning free

LESSON 268 OF 348 · BACKTRACKING

Generate Parentheses in TypeScript: Backtracking with Pruning

Practice O(2^n) Read the full explanation

Given n pairs of parentheses, write a function to generate all combinations of well-formed parentheses.

A parentheses string is well-formed if and only if:

  • It is the empty string, or

  • It can be written as AB (A concatenated with B), where A and B are well-formed, or

  • It can be written as (A), where A is well-formed.


Example 1:

Input: n = 3
Output: ["((()))","(()())","(())()","()(())","()()()"]

Example 2:

Input: n = 1
Output: ["()"]

Note: 1 <= n <= 8


STEP 1 OF 9
    Ctrl + Enter to check

    TYPESCRIPT · BACKTRACKING

    Generate Parentheses in TypeScript, explained

    Problem overview

    The method generateParentheses receives n, a number of pairs, and returns every well-formed string made of n opening and n closing parentheses. A string is well-formed when every ( is closed by a later ) and no ) appears before its partner. For example, "(())" and "()()" are well-formed, while ")(" and "(()" are not. The statement limits n to 1 through 8.

    For n = 3 the answer has five strings: ["((()))", "(()())", "(())()", "()(())", "()()()"]. For n = 1 it is just ["()"].

    Approach: build the string one character at a time

    A string of length 2 * n has two choices at every position, so there are 2^(2n) candidates. Building them all and checking each one wastes nearly all the work. It is better to build a string character by character, and stop extending any prefix that can no longer be completed into a well-formed string.

    Track two counters along with the current prefix: open, the number of ( so far, and closed, the number of ). Two rules decide whether a prefix is still worth extending:

    • Never more closes than opens. If closed exceeds open at some point, some ) has no partner, and no later characters can repair that.
    • Not too many unclosed opens. Each ( still unclosed needs a later ). If open - closed is more than the number of positions left, the string cannot finish balanced.

    When a prefix passes both rules and reaches full length, it is well-formed, since both counters are then consistent: the string has the right length, never closes too early, and has no unclosed opens left. It goes into the results. Otherwise try both characters: append ( and recurse, then append ) and recurse. Because the prefix is a string, and strings are immutable, there is nothing to undo after each call.

    Step by step

    1. Prepare the results. In generateParentheses, set this.solutions to an empty array. The class declares solutions: string[]; at the top, because TypeScript needs every property declared.
    2. Start the search with an empty string, a target length of 2 * n and both counters at 0.
    3. Return this.solutions.
    4. Declare generate with current, the target length (named n inside the helper), open and closed. Inside the helper, n is the full string length, twice the number of pairs.
    5. Prune closes. If open < closed, return.
    6. Prune opens. If open is greater than closed + (n - current.length + 1), return. The part n - current.length is the number of slots left, and even if all of them are ), closed grows by at most that much.
    7. Record complete strings. When current.length == n, push current and return.
    8. Try an opening parenthesis: recurse with current + '(' and open + 1.
    9. Try a closing parenthesis: recurse with current + ')' and closed + 1.

    The solution in TypeScript

    class Solution {
        solutions: string[];
        generate(current: string, n: number, open: number, closed: number) {
            if (open < closed) {
                return;
            }
            if (open > closed + (n - current.length + 1)) {
                return;
            }
            if (current.length == n) {
                this.solutions.push(current);
                return;
            }
            this.generate(current + '(', n, open + 1, closed);
            this.generate(current + ')', n, open, closed + 1);
        }
    
        generateParentheses(n: number): string[] {
            this.solutions = [];
            this.generate("", 2 * n, 0, 0);
            return this.solutions;
        }
    }
    

    The + 1 in the second check looks odd but changes nothing. After current.length characters, open + closed equals that length, so open - closed has the same parity (odd or even) as the length. The target length is even, so the slots left have that same parity too. The surplus of opens therefore can never be exactly one more than the slots left, and "greater than slots + 1" prunes exactly the same prefixes as "greater than slots".

    TypeScript details worth knowing:

    • Strings are immutable. current + '(' creates a new string and leaves current alone. Each recursive call gets its own prefix, so no "remove the last character" step is needed, unlike with an array.
    • Shared results. this.solutions is one array on the object, and every call pushes into it. It is reset at the start of each generateParentheses, so calling the method twice on the same object does not mix the results.
    • Chars are strings. TypeScript has no separate character type, so '(' and "(" are the same kind of value.
    • Inferred return type. generate returns nothing, so TypeScript infers void.

    Complexity

    The number of well-formed strings is the Catalan number, C(2n, n) / (n + 1): 1, 2, 5, 14, 42, 132, 429 and 1,430 for n = 1 to 8.

    Time: proportional to the number of search-tree nodes times the cost of building a string. In a test run, n = 8 made 10,975 calls to produce 1,430 results, so pruning keeps the search close to the output size. In general the total grows like 4^n up to a polynomial factor, and each string costs O(n) to build.

    Space: the results take O(n) per string. The recursion is at most 2n deep, with a prefix string in each frame.

    Edge cases and common mistakes

    • n = 1. The search finds only "()".
    • Mixing up n. In generateParentheses it is the number of pairs; in generate it is the string length. The start call passes 2 * n.
    • Checking only at the end. Generating all strings first and validating afterwards also works for tiny n but does far more work than pruning.
    • Allowing a close before its open. Without the first check, strings like ")(" would reach full length. The check on each prefix stops them early.
    • Too many opens. Without the second check, a string like "(((" is only discovered to be hopeless when it reaches full length.
    • Leftover results. Forgetting to reset this.solutions would keep results from a previous call.

    Testing

    • n = 1 gives ["()"].
    • n = 2 gives ["(())", "()()"].
    • n = 3 gives ["((()))", "(()())", "(())()", "()(())", "()()()"].
    • n = 4 gives 14 strings, and n = 8 gives 1,430.
    Keep going with TypeScriptAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free