Why Catalan Numbers Matters
With three pairs of parentheses, how many correctly balanced strings can you write? There are five: ((())), (()()), (())(), ()(()), and ()()(). Choosing arbitrary positions for the three opening parentheses would give 20 strings, but many close a pair before opening it. Catalan numbers count the arrangements that obey this nesting rule.
The same sequence counts several other structures: binary search tree shapes on a fixed set of ordered keys, full binary tree shapes, noncrossing pairings, polygon triangulations, and ways to fully parenthesize an expression. Their connection is a common recursive pattern: choose a split, independently choose a left structure and a right structure, then combine them.
This lesson turns that pattern into dynamic programming. You will derive the recurrence from the first matching parenthesis, trace its products and running totals, implement it in three languages, and recognize when an apparent Catalan problem needs a different size parameter or additional constraints.
Catalan numbers count recursively nested structures with a unique left/right decomposition. For size n ≥ 1, try every possible left size i, multiply the number of left choices by the number of right choices, and add over all splits: C(n) = sum of C(i) × C(n − 1 − i), with C(0) = 1.
Core Concepts and Terminology
The input is a nonnegative integer n. The nth Catalan number C(n) counts balanced parenthesis strings with n pairs. Each string contains n opening and n closing parentheses. In every prefix, the number of opening parentheses is at least the number of closing parentheses; at the end, the two counts are equal. The balance of a prefix is its opening count minus its closing count.
The sequence begins 1, 1, 2, 5, 14, 42, 132, 429 for n = 0 through 7. The value C(0) = 1 represents the one empty string. It is the multiplicative identity needed when one side of a recursive split is empty.
Every nonempty balanced string has a unique decomposition ( + A + ) + B. The closing parenthesis shown is the match for the first opening parenthesis. If A contains i pairs, B contains n − 1 − i pairs. The outer matched pair uses the remaining one pair. For each fixed i, there are C(i) choices for A and C(n − 1 − i) choices for B, giving their product. Different i values describe disjoint cases, so we add those products.
A Dyck path draws the same structure: read ( as a step up and ) as a step down, moving one unit right each time. A balanced string becomes a path from (0, 0) to (2n, 0) that never goes below height zero. The first return to zero identifies the matching parenthesis used in the decomposition.
| Term | Example | Role in the algorithm |
|---|---|---|
| Size n | Three matched pairs | Determines the Catalan index |
| C(n) | C(3) = 5 | Number of valid structures of size n |
| Prefix balance | (()) has balances 1, 2, 1, 0 | Must stay nonnegative and finish at zero |
| Empty structure | The empty string | Gives the base case C(0) = 1 |
| First-return decomposition | (())() = ( + () + ) + () | Identifies one unique split |
| Left size i | i = 1 for A = () | Ranges from 0 through n − 1 |
| Right size | n − 1 − i | Accounts for B after removing the outer pair |
| Product term | C(1) × C(2) = 2 | Counts independent left/right choices for one split |
| DP entry dp[s] | dp[4] = 14 | Stores the completed count for size s |
| Modulus M | C(20) mod 1,000,000,007 = 564120378 | Keeps stored residues bounded |
How Catalan Numbers Works, Step by Step
Allocate an array dp with n + 1 entries, initially zero, and set dp[0] = 1. Process sizes s in increasing order from 1 through n. For each size, start total = 0, then try every left size i from 0 through s − 1. Set right = s − 1 − i and add dp[i] × dp[right] to total. After all splits have been processed, store total in dp[s].
Both referenced sizes are smaller than s. Their entries are therefore complete before we start computing dp[s]. There is no need for recursive calls or an extra table dimension. The outer loop orders the subproblems; the inner loop combines every possible split of the current problem.
For example, when computing C(3), dp already begins [1, 1, 2]. The first split contributes C(0) × C(2) = 2. The next split contributes C(1) × C(1) = 1, increasing total from 2 to 3. The last split contributes C(2) × C(0) = 2, so dp[3] becomes 5.
The implementations below reduce each product and running total modulo M = 1,000,000,007. This changes the stored numbers when counts become large, but it preserves the recurrence modulo M. No division or modular inverse is used.
flowchart TD
A["Read nonnegative n"] --> B{"n negative?"}
B -->|Yes| X["Reject input"]
B -->|No| C["Allocate dp[0..n]; dp[0] = 1; s = 1"]
C --> D{"s at most n?"}
D -->|No| R["Return dp[0..n]"]
D -->|Yes| E["total = 0; i = 0"]
E --> F{"i less than s?"}
F -->|Yes| G["right = s - 1 - i"]
G --> H["term = dp[i] times dp[right] mod M"]
H --> I["total = (total + term) mod M"]
I --> J["i = i + 1"]
J --> F
F -->|No| K["dp[s] = total; s = s + 1"]
K --> D
Worked Example with a Full Trace
Compute the Catalan prefix through n = 4. Initialize dp = [1, 0, 0, 0, 0]. The following trace records all ten product terms and every intermediate total.
- Size s = 1: start total = 0. For i = 0, right = 0, so add 1 × 1 = 1; total becomes 1. Store dp[1] = 1, giving [1, 1, 0, 0, 0].
- Size s = 2: reset total = 0. For i = 0, right = 1, add 1 × 1 = 1; total becomes 1. For i = 1, right = 0, add 1 × 1 = 1; total becomes 2. Store dp[2] = 2, giving [1, 1, 2, 0, 0].
- Size s = 3: reset total = 0. For i = 0, right = 2, add 1 × 2 = 2; total becomes 2. For i = 1, right = 1, add 1 × 1 = 1; total becomes 3. For i = 2, right = 0, add 2 × 1 = 2; total becomes 5. Store dp[3] = 5, giving [1, 1, 2, 5, 0].
- Size s = 4: reset total = 0. For i = 0, right = 3, add 1 × 5 = 5; total becomes 5. For i = 1, right = 2, add 1 × 2 = 2; total becomes 7. For i = 2, right = 1, add 2 × 1 = 2; total becomes 9. For i = 3, right = 0, add 5 × 1 = 5; total becomes 14. Store dp[4] = 14, giving [1, 1, 2, 5, 14].
For size four, the split cases are 0 + 3, 1 + 2, 2 + 1, and 3 + 0 pairs inside A and B. Their counts are 5, 2, 2, and 5. Each complete string has exactly one first matching pair, so the four cases never overlap.
As an independent arithmetic check, C(4) = binomial(8, 4)/(4 + 1) = 70/5 = 14. The closed form will be discussed below; the DP itself requires no division.
Trace It by Hand
You have completed dp[0…3] = [1, 1, 2, 5]. While computing size four, the first two splits have produced total = 7. Predict the product for i = 2 and the new running total.
Reveal: right = 4 − 1 − 2 = 1, so the term is dp[2] × dp[1] = 2 × 1 = 2. The new total is 9. The final split adds dp[3] × dp[0] = 5 × 1 = 5, producing 14.
Now predict C(5) using only completed entries. Its five products are 1 × 14, 1 × 5, 2 × 2, 5 × 1, and 14 × 1. Their sum is 14 + 5 + 4 + 5 + 14 = 42. The middle product counts four ordered pairs of size-two structures: either of two left choices can accompany either of two right choices. To see why addition is different, the i = 2 split for size six contributes C(2) × C(3) = 2 × 5 = 10, not 2 + 5 = 7.
Implementation in Python, C++, and Java
Each program computes the table from C(0) through C(20) once, then prints the same selected entries. The implementation is the quadratic split recurrence, with explicit modulus M = 1,000,000,007. Stored values are Catalan numbers modulo M. They equal the full counts for the small entries shown through C(10), but the C(20) line is a remainder.
Python uses built-in arbitrary-precision integers. C++ uses long long: both multiplicands are at most M − 1, so their product is at most (M − 1)² = 1,000,000,012,000,000,036, safely below the signed 64-bit maximum. It reduces the product before adding it to total. Java uses BigInteger for products, sums, and remainders. The fixed modulus is part of this safety argument; changing it requires checking the C++ product bound again.
All versions reject negative n. Array allocation and running time also limit feasible n: quadratic DP is intended for moderate input sizes, not arbitrarily large indices. The Java allocation uses checked addition for n + 1.
Run Python with Python 3.9 or later. Compile C++ with C++14 or later. Save Java as Main.java, compile with javac Main.java, and run with java Main. Each program prints the following nine lines:
modulus = 1000000007C(0) = 1C(1) = 1C(2) = 2C(3) = 5C(4) = 14C(5) = 42C(10) = 16796C(20) = 564120378
The full value C(20) is 6,564,120,420. Reducing it modulo 1,000,000,007 gives 564,120,378, matching the programs. To obtain full counts, remove modular reductions in Python or Java while retaining arbitrary-precision integers. C++ then needs arbitrary-precision arithmetic or a proven bound on every product and sum; removing % MOD alone does not make the program an exact unbounded counter.
Complexity, Correctness, and Invariants
For n ≥ 1, the outer loop computes sizes 1 through n, and size s tries exactly s splits. The total number of product terms is 1 + 2 + … + n = n(n + 1)/2. Thus the fixed-modulus DP takes Θ(n²) time and Θ(n + 1) working storage, including its returned table. It uses only O(1) additional bounded-size state beyond that table. For n = 0, time and storage are Θ(1).
For a fixed n, best, average, and worst cases have the same loop count: there are no data-dependent choices or early exits in the recurrence. Negative inputs are rejected before allocation. These are unit-cost arithmetic bounds for the fixed modulus. For full counts, Catalan values have Θ(n) bits as n grows, so multiplication and addition are no longer constant-time operations; the exact bit complexity depends on the integer arithmetic algorithms used.
Correctness follows by induction on the computed size. Initially dp[0] = 1 correctly counts the empty structure. Assume every dp[j] with j less than s is correct modulo M. For each left size i, the unique decomposition ( + A + ) + B gives a bijection between size-s structures with that split and pairs of structures counted by dp[i] and dp[s − 1 − i]. Multiplication counts all pairs, and addition combines disjoint split cases. Therefore the completed total is C(s) modulo M, establishing the induction step.
The inner-loop invariant is equally useful when debugging: before processing left size i, total is the sum of all product terms for left sizes 0 through i − 1, modulo M. Completed smaller dp entries remain unchanged. When i reaches s, every split has been included exactly once, so storing total in dp[s] is correct.
| Method | Arithmetic-operation time | Storage | Assumptions and tradeoff |
|---|---|---|---|
| Bottom-up split DP | Θ(n²) | Θ(n + 1) table | Addition and multiplication only; works modulo any positive modulus |
| Memoized recursive split recurrence | Θ(n²) | O(n) cache and call stack | Removes repeated subproblems but retains recursion overhead |
| Plain recursion using only C(0) as its base case | Θ(3ⁿ) calls | O(n) call stack | Recomputes the same smaller sizes many times |
| Multiplicative Catalan recurrence | Θ(n) multiply/divide stages | O(1) big-integer objects when streaming | Exact integer division; bit costs grow with n |
| Closed form via multiplicative binomial evaluation | O(n) multiply/divide stages for one C(n) | O(1) big-integer objects | Computes binomial(2n, n), then divides exactly by n + 1 |
The multiplicative recurrence is C(s + 1) = C(s) × 2(2s + 1)/(s + 2), starting from C(0) = 1. Every stage gives an integer when the multiplication is performed before the division. It is often preferable when the goal is simply to compute full Catalan values; the quadratic DP exposes the decomposition and adapts naturally to many constrained variants.
The table counts arithmetic stages rather than bit operations. “O(1) big-integer objects” does not mean constant bytes: the integers themselves grow. If a method returns every value through n rather than streaming them, retaining that output also requires n + 1 entries.
Before computing size s, all smaller dp entries contain the correct Catalan values modulo M; during the inner loop, total contains exactly the contributions of the splits already visited. The unique decomposition ( + A + ) + B proves that multiplying left/right counts and summing over all s splits counts every size-s structure once.
Variants, Pitfalls, and Optimizations
Keep the empty structure. Set C(0) = 1, not zero. A split with an empty left side still has one possible empty left choice. Multiplying by zero would incorrectly remove every structure whose decomposition contains an empty part, and the recurrence would produce zeros throughout.
Check what n measures. Balanced parentheses use n pairs, while ordinary binary search tree shapes use n nodes with distinct keys in a fixed sorted order. Full ordered binary trees use n internal nodes and therefore have n + 1 leaves and 2n + 1 total nodes. Fully parenthesizing m operands gives C(m − 1) possibilities. Triangulating a convex m-gon gives C(m − 2) possibilities. These are related structures, but their size conventions differ.
Preserve left/right order. For tree shapes and parenthesizations, the left and right parts occupy different positions. Mirroring can create a different structure. Although C(i) × C(s − 1 − i) equals its mirrored term numerically, both split positions must normally be counted. You may optimize a symmetric sum by doubling matched terms and including the middle term once, but that reduces only the constant factor; it remains quadratic overall.
Do not divide modular residues as ordinary integers. The closed form C(n) = binomial(2n, n)/(n + 1) is an exact integer identity. After taking a remainder, ordinary division by n + 1 is generally invalid. Modular division requires an inverse and proof that it exists. The supplied DP avoids this issue and works for composite moduli as well, subject to suitable arithmetic bounds. Another integer identity, C(n) = binomial(2n, n) − binomial(2n, n + 1), follows from reflecting paths that first cross below zero.
Additional constraints usually change the state. If balanced strings have a maximum allowed nesting depth, a position-and-balance DP is a more direct model. If the problem asks to list strings rather than count them, backtracking must emit every valid output, so a fast counting formula does not remove the cost of generation. Catalan growth is approximately 4ⁿ divided by the product of sqrt(pi) and n to the power 3/2, which explains how quickly listing becomes expensive.
Keep C(0) = 1, use right = s − 1 − i, reset total for each new size, and sum all split positions. Match the Catalan index to the structure's size convention, guard fixed-width products, and distinguish a full count from a modular residue; the exact formula does not justify ordinary division after reduction.
Practice Problems and Real-World Use
To recognize a Catalan model, search for a unique separator or root that divides an object into an ordered pair of smaller objects. Then identify the size used by that separator. Prove the decomposition works in both directions: every valid object gives one split, and every allowed pair of smaller objects produces a valid larger object.
The same counts help measure a search space even when the ultimate task is optimization or generation. For example, Catalan numbers count matrix-chain parenthesizations, while the standard matrix-chain cost DP chooses the cheapest grouping using matrix dimensions. Those are different outputs, and the counting recurrence does not by itself solve the optimization problem.
| Problem or application | Concrete example | Main idea tested | Difficulty |
|---|---|---|---|
| Balanced parentheses | Three pairs have C(3) = 5 valid strings | First matching pair gives the split | Beginner |
| Dyck paths | Eight up/down steps with four of each and nonnegative height: C(4) = 14 | Prefix balance and lattice-path correspondence | Beginner |
| Binary search tree shapes | Four fixed distinct keys admit C(4) = 14 shapes | Choose a root; left/right key counts determine subproblems | Intermediate |
| Full ordered binary trees | Two internal nodes give C(2) = 2 shapes | Distinguish internal nodes from leaves | Intermediate |
| Expression grouping | Four operands admit C(3) = 5 full binary parenthesizations | m operands use Catalan index m − 1 | Intermediate |
| Matrix-chain planning | Five matrices admit C(4) = 14 groupings before costs are considered | Separate counting groupings from minimizing multiplication cost | Intermediate |
| Convex polygon triangulation | A convex hexagon has C(4) = 14 triangulations | A fixed boundary edge determines the root triangle | Intermediate |
| Noncrossing perfect matchings | Six labeled points around a circle admit C(3) = 5 pairings | Pair one fixed point; split the remaining points into two intervals | Intermediate |
In the full-tree row, “ordered” means that left and right child positions are distinguished. Every internal node has exactly two children. In the expression and matrix-chain examples, the operand or matrix order stays fixed. In the polygon example, the vertices are fixed and the polygon is convex; arbitrary polygon geometry can change which diagonals are permitted.
For an additional exercise, count the possible output permutations from a fixed increasing input passed through one stack, with each item pushed and popped once. The answer is C(n), and the legal push/pop sequences correspond to balanced parentheses. This uses one stack and a fixed input order; changing the machine changes the counting problem.
Verify small values by independently enumerating balanced strings and rejecting any prefix with negative balance. Compare the DP against binomial(2n, n)/(n + 1) for a range of indices using exact integer arithmetic, then reduce modulo M for comparison with the programs. Include n = 0, negative-input rejection, C(4) = 14, and a value such as C(20) that exceeds M, so indexing, base cases, arithmetic, and output interpretation are all exercised.