Catalan Numbers (Dyck Paths)
This example demonstrates how to calculate Catalan Numbers using the algebrax library.
Catalan numbers (\(C_n\)) appear in many combinatorial problems, such as counting valid parenthesis sequences or Dyck Paths.
The Problem: Dyck Paths
A Dyck Path of length \(2n\) is a path from \((0, 0)\) to \((2n, 0)\) that:
1. Uses steps \(U = (1, 1)\) (Up) and \(D = (1, -1)\) (Down).
2. Never goes below the x-axis (\(y \ge 0\)).
The number of such paths is the \(n\)-th Catalan number \(C_n\).
We can model this as a Graph Walk problem on a 1D grid (the y-axis heights \(0, 1, 2, \dots\)).
- Nodes: Integer heights \(h \in \{0, 1, \dots, n\}\).
- Edges:
- \(h \to h+1\) (Up step)
- \(h \to h-1\) (Down step)
- Constraint: We cannot go below 0.
We want to find the number of paths from node \(0\) to node \(0\) in exactly \(2n\) steps.
This is equivalent to \((A^{2n})_{0,0}\) where \(A\) is the adjacency matrix of the line graph.
| from algebrax.semiring import StandardSemiring
from algebrax.matrix import dot, power
def catalan_via_matrix(n: int) -> int:
"""
Calculates the n-th Catalan number by counting Dyck paths.
A Dyck path of semi-length n has 2n steps.
We model this as a walk on the integer line [0, n].
"""
if n == 0: return 1
# 1. Construct the Adjacency Matrix for heights 0 to n
# We only need heights up to n because a path of length 2n
# starting at 0 cannot go higher than n and return to 0.
size = n + 1
adjacency = {}
for h in range(size):
adjacency[h] = {}
# Up step (h -> h+1)
if h + 1 < size:
adjacency[h][h+1] = 1
# Down step (h -> h-1)
if h - 1 >= 0:
adjacency[h][h-1] = 1
# 2. Compute A^(2n) using the Standard Semiring (+, *)
# This counts the number of walks of length 2n.
# We use binary exponentiation (matrix power) for efficiency.
semiring = StandardSemiring()
# We need 2n steps
steps = 2 * n
# Result matrix M = A^(2n)
M = power(adjacency, steps, semiring)
# 3. The answer is the number of paths from 0 to 0
# We use .get(0, {}).get(0, 0) to handle sparsity safely
return int(M.get(0, {}).get(0, 0))
# Verify the first few Catalan numbers: 1, 1, 2, 5, 14, 42
expected = [1, 1, 2, 5, 14, 42]
calculated = [catalan_via_matrix(i) for i in range(6)]
print(f"Expected: {expected}")
print(f"Calculated: {calculated}")
assert expected == calculated
|
Alternative: Segner's Recurrence (Convolution)
Catalan numbers also satisfy the recurrence:
$\(C_{n+1} = \sum_{i=0}^{n} C_i C_{n-i}\)$
This is a Convolution. In the algebrax context, this looks like polynomial multiplication.
If we define a polynomial \(P(x) = \sum C_i x^i\), then \(P(x) = 1 + x P(x)^2\).
We can use the Cauchy Product (Discrete Convolution) to compute this iteratively.
| from algebrax.semiring import StandardSemiring
def catalan_via_convolution(n: int) -> int:
if n == 0: return 1
# C[0] = 1
catalan = [1]
for k in range(1, n + 1):
# C_k = sum(C_i * C_{k-1-i}) for i from 0 to k-1
# This is the dot product of the list with its reverse
c_k = 0
for i in range(k):
c_k += catalan[i] * catalan[k - 1 - i]
catalan.append(c_k)
return catalan[n]
print(f"C_5 (Convolution): {catalan_via_convolution(5)}")
# output: 42
|