← All Posts
Combinatorics II — Advanced· Part 12 of 16

Symbolic Method — Analytic Combinatorics Basics

What Is the Symbolic Method?

The symbolic method, developed by Philippe Flajolet and Robert Sedgewick in their landmark Analytic Combinatorics (2009), provides a systematic pipeline for counting combinatorial objects:

  1. Specify the combinatorial class using a formal grammar of constructions.
  2. Translate each construction into an equation on generating functions — mechanically, with no guesswork.
  3. Analyse the generating function (GF) to extract exact counts or asymptotics.

The beauty of the approach is that step 2 is entirely automatic. Once you describe what objects look like (step 1), a dictionary of translation rules produces the GF equation. You never need an ad-hoc bijection or a flash of insight — the dictionary does the work.

Core idea. A combinatorial class 𝒜 is a set of objects, each with a non-negative integer size, such that the number of objects of each size is finite. The generating function A(z) = Σn≥0 an zn encodes the counting sequence (an). The symbolic method gives rules to build A(z) from simpler classes.

For competitive programming, this means: if you can decompose a structure into smaller pieces using unions, products, sequences, etc., the GF pops out for free. From there you can read off coefficients (exact counts), derive recurrences, or apply singularity analysis for asymptotics.

Why should CP contestants care?

Many DP-based counting problems are secretly asking you to count objects in a combinatorial class. Recognising the underlying construction can:

Admissible Constructions

An admissible construction is one that preserves the size of objects in a predictable way, so the translation to GFs is well-defined. Flajolet and Sedgewick identified the following core constructions. Each has a direct translation for both ordinary generating functions (OGF, unlabelled) and exponential generating functions (EGF, labelled).

The Translation Dictionary (Unlabelled / OGF)

ConstructionNotationOGF TranslationIntuition
Neutral classℰ (single object, size 0) 1The empty object contributes z0 = 1
Atomic class𝒵 (single object, size 1) zOne atom of size 1
Disjoint union𝒜 + ℬ A(z) + B(z)An object is either from 𝒜 or ℬ
Cartesian product𝒜 × ℬ A(z) · B(z)Pair: sizes add, counts convolve
SequenceSeq(𝒜) 1 / (1 − A(z))Ordered list of 𝒜-objects (possibly empty)
Powerset (Multiset)MSet(𝒜) Πn≥1 1/(1−zn)an Unordered collection with repetition
CycleCyc(𝒜) Σk≥1 φ(k)/k · log(1/(1−A(zk))) Necklace-like arrangement

The Translation Dictionary (Labelled / EGF)

ConstructionNotationEGF Translation
Neutral / Atomℰ, 𝒵1, z
Disjoint union𝒜 + ℬÂ(z) + &Bcirc;(z)
Labelled product𝒜 ★ ℬ Â(z) · &Bcirc;(z)
SequenceSeq(𝒜) 1 / (1 − Â(z))
SetSet(𝒜) exp(Â(z))
CycleCyc(𝒜) log(1 / (1 − Â(z)))
Key rule of thumb. For labelled (EGF) constructions: product → multiply EGFs (the binomial convolution handles label redistribution automatically), set → exp, cycle → log. These three cover the vast majority of CP-relevant labelled counting problems.

Composability

These constructions can be nested. For example, a set of cycles is Perm = Set(Cyc(𝒵)). The EGF translation gives:

&Pcirc;(z) = exp(log(1/(1−z))) = 1/(1−z) = Σn≥0 n! · zn/n!

which confirms the well-known fact that there are n! permutations of size n. This kind of one-line derivation is the power of the symbolic method.

Unlabelled vs Labelled

The choice between OGF and EGF depends on whether the atoms in your objects are distinguishable.

Unlabelled (OGF)

Objects have no labels on their atoms. Two objects of the same shape are identical. Use when counting structural types: binary strings, integer compositions, unlabelled trees.

A(z) = Σn≥0 an zn

Product rule: A(z) · B(z) — ordinary convolution.

Labelled (EGF)

Each atom carries a distinct label from {1, 2, …, n}. Two objects with different label assignments are different. Use when counting: permutations, labelled trees, set partitions.

Â(z) = Σn≥0 an zn/n!

Product rule: Â(z) · &Bcirc;(z) — binomial convolution.

Example: Binary Strings (OGF)

A binary string is a sequence of bits, where each bit is either 0 or 1. The atom class is ℬ = {0, 1}, so B(z) = 2z (two atoms of size 1). A binary string is Seq(ℬ):

S(z) = 1/(1 − 2z) = Σn≥0 2n zn

confirming that there are 2n binary strings of length n. No labels involved — pure OGF.

Example: Permutations (EGF)

A permutation of [n] is a sequence of distinct labelled atoms. The atom class is 𝒵 with EGF z. A permutation is Seq(𝒵):

&Pcirc;(z) = 1/(1 − z) = Σn≥0 zn

Reading off the coefficients: an/n! = 1, so an = n!. Exactly right.

Don't mix OGF and EGF products. If you multiply two OGFs, you get ordinary convolution (Σ ak bn−k). If you multiply two EGFs, the coefficients combine via binomial convolution (Σ C(n,k) ak bn−k). Using the wrong one is a common source of errors.

Worked Examples

(a) Binary Trees & Catalan Numbers

A (full) binary tree is either a single leaf or an internal node with a left subtree and a right subtree. Letting 𝒯 denote the class, with the size being the number of internal nodes:

𝒯 = ℰ + 𝒵 × 𝒯 × 𝒯

Translating via the OGF dictionary:

T(z) = 1 + z · T(z)2

This is a quadratic in T(z). Solving:

T(z) = (1 − √(1 − 4z)) / (2z)

(We pick the − root because T(0) must be finite.) Expanding via the generalised binomial theorem gives:

[zn] T(z) = Cn = C(2n, n) / (n+1)

— the Catalan numbers: 1, 1, 2, 5, 14, 42, 132, …

Catalan structures everywhere. The same GF T(z) = (1−√(1−4z))/(2z) counts: full binary trees, ballot sequences, Dyck paths, non-crossing partitions, triangulations of polygons, and many more. Recognising a Catalan structure in a CP problem instantly gives you the answer.

(b) Set Partitions & Bell Numbers

A set partition of [n] is a collection of non-empty, disjoint subsets (blocks) whose union is [n]. In the symbolic framework (labelled):

Partition = Set(Block),   Block = Set≥1(𝒵)

The EGF of a non-empty set of atoms is ez − 1 (subtract the empty set). A set partition is a set of blocks:

&Pcirc;(z) = exp(ez − 1)

The coefficient [zn/n!] gives the Bell number Bn. The first few values: 1, 1, 2, 5, 15, 52, 203, 877, …

This single line — exp(ez − 1) — replaces the entire Bell triangle recurrence and immediately shows the structure.

(c) Permutations by Cycles

Every permutation decomposes uniquely into a set of disjoint cycles. In the symbolic method:

Perm = Set(Cyc(𝒵))

The EGF of a cycle of labelled atoms is Cyc(𝒵) → log(1/(1−z)). Then:

&Pcirc;(z) = exp(log(1/(1−z))) = 1/(1−z)

As expected. But the decomposition is far more powerful than just counting permutations — it lets us count permutations by number of cycles. If we mark each cycle with a variable u:

&Pcirc;(z, u) = exp(u · log(1/(1−z))) = (1/(1−z))u

The coefficient [zn/n!] uk is the (unsigned) Stirling number of the first kind [n/k], counting permutations of n elements with exactly k cycles.

(d) Integer Compositions

A composition of n is an ordered tuple of positive integers summing to n. Symbolically:

Comp = Seq(Seq≥1(𝒵))

Seq≥1(𝒵) has OGF z/(1−z). Then:

C(z) = 1/(1 − z/(1−z)) = (1−z)/(1−2z)

So [zn] C(z) = 2n−1 for n ≥ 1, which matches the well-known result that there are 2n−1 compositions of n.

Singularity Analysis

Once we have a generating function, we often want asymptotics — how fast does an grow? The key tool from analytic combinatorics is singularity analysis: the growth rate of [zn] f(z) is governed by the singularity of f(z) closest to the origin.

Dominant Singularity Principle

If f(z) is analytic in a disc of radius R, then [zn] f(z) grows roughly as R−n. The singularity at |z| = R — the dominant singularity — controls the exponential growth rate.

Transfer Theorems (Flajolet–Odlyzko). Near a dominant singularity ρ, if f(z) behaves like (1 − z/ρ)α, then:
[zn] f(z) ~ ρ−n · n−α−1 / Γ(−α)
This single formula covers a vast number of combinatorial GFs.

Example: Catalan Number Asymptotics

The GF T(z) = (1 − √(1−4z)) / (2z) has a dominant singularity at z = 1/4 (branch point of the square root). Near z = 1/4:

T(z) ~ 1/(2z) · (1 − (1−4z)1/2)

The singular part behaves like (1 − 4z)1/2 with α = 1/2. Applying the transfer theorem with ρ = 1/4:

Cn ~ 4n / (n√(πn))

This asymptotic is remarkably accurate even for moderate n. For n = 10, the true value is 16796 and the estimate gives ≈ 16724.

Exponential Growth Table

Some common GFs and their growth rates derived by singularity analysis:

SequenceGF SingularityAsymptotic
Catalan Cnρ = 1/44n / (n√(πn))
Motzkin Mnρ = 1/33n · c / n3/2
Bell Bn(saddle-point)complex, super-exponential
Partitions p(n)ρ = 1 (essential singularity)exp(π√(2n/3)) / (4n√3)
Convergence pitfall. Singularity analysis requires that the GF is analytic in a region extending beyond the disc of convergence (a "delta-domain"). Lacunary series (with large gaps between non-zero coefficients) or functions with natural boundaries can violate this. In CP you rarely hit these cases, but be aware when using GFs defined by unusual recurrences.

Multiple Singularities

When there are several singularities on the circle of convergence, their contributions add. For example, the GF 1/(1−z2) = 1/((1−z)(1+z)) has singularities at z = 1 and z = −1. The coefficient [zn] equals (1 + (−1)n)/2, which is 1 for even n and 0 for odd n — the contributions from both singularities combine to produce the oscillation.

C++ for GF Manipulation

In competitive programming, we rarely manipulate GFs symbolically at runtime. Instead, we compute the first N coefficients of a GF using polynomial arithmetic. The key operations are polynomial multiplication (via NTT), inversion, and exponentiation.

Generating Catalan Numbers via the Recurrence

From T(z) = 1 + z · T(z)2, we derive the recurrence C0 = 1, Cn = Σk=0n−1 Ck · Cn−1−k. This is O(n2) but simple:

// O(n^2) Catalan numbers from the symbolic equation T = 1 + z*T^2
#include <bits/stdc++.h>
using namespace std;

const int MOD = 1e9 + 7;

vector<long long> catalan(int n) {
    vector<long long> C(n + 1);
    C[0] = 1;
    for (int i = 1; i <= n; i++) {
        C[i] = 0;
        for (int k = 0; k < i; k++) {
            C[i] = (C[i] + C[k] % MOD * C[i - 1 - k]) % MOD;
        }
    }
    return C;
}

int main() {
    int n;
    cin >> n;
    auto C = catalan(n);
    for (int i = 0; i <= n; i++)
        cout << "C[" << i << "] = " << C[i] << "\n";
}

NTT-Based Polynomial Multiplication

When we need to multiply two polynomials (= convolve two coefficient sequences) modulo a prime, NTT gives O(n log n). This is the workhorse behind fast GF manipulation.

// NTT-based polynomial multiplication mod 998244353
#include <bits/stdc++.h>
using namespace std;

const int MOD = 998244353, g = 3;

long long power(long long a, long long b, long long mod) {
    long long res = 1; a %= mod;
    for (; b > 0; b >>= 1) {
        if (b & 1) res = res * a % mod;
        a = a * a % mod;
    }
    return res;
}

void ntt(vector<long long>& a, bool inv) {
    int n = a.size();
    for (int i = 1, j = 0; i < n; i++) {
        int bit = n >> 1;
        for (; j & bit; bit >>= 1) j ^= bit;
        j ^= bit;
        if (i < j) swap(a[i], a[j]);
    }
    for (int len = 2; len <= n; len <<= 1) {
        long long w = inv ? power(g, MOD - 1 - (MOD - 1) / len, MOD)
                         : power(g, (MOD - 1) / len, MOD);
        for (int i = 0; i < n; i += len) {
            long long wn = 1;
            for (int j = 0; j < len / 2; j++) {
                long long u = a[i + j];
                long long v = a[i + j + len / 2] * wn % MOD;
                a[i + j] = (u + v) % MOD;
                a[i + j + len / 2] = (u - v + MOD) % MOD;
                wn = wn * w % MOD;
            }
        }
    }
    if (inv) {
        long long n_inv = power(n, MOD - 2, MOD);
        for (auto& x : a) x = x * n_inv % MOD;
    }
}

vector<long long> multiply(vector<long long> a, vector<long long> b) {
    int result_size = a.size() + b.size() - 1;
    int n = 1;
    while (n < result_size) n <<= 1;
    a.resize(n); b.resize(n);
    ntt(a, false); ntt(b, false);
    for (int i = 0; i < n; i++) a[i] = a[i] * b[i] % MOD;
    ntt(a, true);
    a.resize(result_size);
    return a;
}

int main() {
    // Example: multiply (1 + 2z + 3z^2) by (4 + 5z)
    vector<long long> a = {1, 2, 3}, b = {4, 5};
    auto c = multiply(a, b);
    // c = {4, 13, 22, 15}  i.e. 4 + 13z + 22z^2 + 15z^3
    for (int i = 0; i < (int)c.size(); i++)
        cout << "z^" << i << ": " << c[i] << "\n";
}

Partition Numbers via Euler’s Product

The number of integer partitions of n has the famous OGF:

P(z) = Πk=1∞ 1/(1 − zk)

This is MSet(𝒵 + 𝒵2 + 𝒵3 + …) in the symbolic framework. We compute the first N+1 coefficients in O(N√N) using Euler’s pentagonal theorem, or in O(N log N) by taking the logarithm and using power series:

// Partition numbers p(0..N) in O(N * sqrt(N))
// Uses Euler's pentagonal recurrence:
// p(n) = sum_{k!=0} (-1)^{k+1} * p(n - k(3k-1)/2)
#include <bits/stdc++.h>
using namespace std;

const int MOD = 1e9 + 7;

vector<long long> partitions(int N) {
    vector<long long> p(N + 1, 0);
    p[0] = 1;
    for (int n = 1; n <= N; n++) {
        for (int k = 1; ; k++) {
            int g1 = k * (3 * k - 1) / 2;   // generalised pentagonal
            int g2 = k * (3 * k + 1) / 2;    // negative k term
            if (g1 > n) break;
            long long sign = (k % 2 == 1) ? 1 : MOD - 1;
            p[n] = (p[n] + sign * p[n - g1]) % MOD;
            if (g2 <= n)
                p[n] = (p[n] + sign * p[n - g2]) % MOD;
        }
    }
    return p;
}

int main() {
    int N;
    cin >> N;
    auto p = partitions(N);
    for (int i = 0; i <= min(N, 20); i++)
        cout << "p(" << i << ") = " << p[i] << "\n";
}

Computing Bell Numbers from the EGF

Since the EGF is exp(ez − 1), we can compute Bell numbers by composing power-series operations: first compute the series for ez − 1, then exponentiate it. In practice, the Bell triangle recurrence Bn+1 = Σk=0n C(n,k) Bk is simpler for competitive programming:

// Bell numbers via the Bell triangle — O(n^2)
#include <bits/stdc++.h>
using namespace std;

const int MOD = 1e9 + 7;

vector<long long> bell_numbers(int n) {
    // Bell triangle: row[0] = B_i, then row[j] = row[j-1] + prev_row[j-1]
    vector<long long> B(n + 1);
    vector<long long> row(n + 1);
    B[0] = 1;
    row[0] = 1;
    for (int i = 1; i <= n; i++) {
        vector<long long> nrow(n + 1);
        nrow[0] = row[i - 1];  // last element of previous row
        for (int j = 1; j <= i; j++) {
            nrow[j] = (nrow[j - 1] + row[j - 1]) % MOD;
        }
        B[i] = nrow[0];
        row = nrow;
    }
    return B;
}

int main() {
    int n;
    cin >> n;
    auto B = bell_numbers(n);
    for (int i = 0; i <= n; i++)
        cout << "B[" << i << "] = " << B[i] << "\n";
}

CP Applications

The symbolic method is not just a theoretical framework — it has direct, practical applications in competitive programming.

When to Use GFs vs DP

Prefer GF / Symbolic

  • Problem asks for an mod p for very large n (use matrix exponentiation on the GF recurrence).
  • You need to combine two independent counting sequences (convolution via NTT).
  • The structure is a known combinatorial class (trees, partitions, permutations).
  • You want asymptotic bounds to estimate answer magnitude.

Prefer DP

  • State space has complex constraints that don’t map to standard constructions.
  • Small n makes O(n2) or O(n3) DP feasible and simpler to code.
  • The problem involves optimisation (min/max), not counting.
  • Dependencies between sub-problems are non-trivial (e.g., graph-based DP).

Recognising Symbolic Constructions in Problems

Many CP problems are disguised symbolic specifications. Here are some patterns to watch for:

The pipeline in contest. (1) Read the problem — identify what combinatorial objects are being counted. (2) Write a symbolic specification using ℰ, 𝒵, +, ×, Seq, Set, Cyc. (3) Translate to GF using the dictionary. (4) Extract coefficients — closed form, recurrence, or NTT.

Fast Convolution for Combining Sequences

A very common CP pattern: you have two sequences (a0, …, an) and (b0, …, bm) and need ck = Σi+j=k ai · bj. This is exactly the product of two OGFs, computable in O((n+m) log(n+m)) via NTT.

Applications include: counting paths of length k in graphs (adjacency matrix polynomial), combining independent knapsack-like constraints, and composing transfer matrices.

Lagrange Inversion

When you have an implicit equation like T(z) = z · φ(T(z)), Lagrange inversion gives:

[zn] T(z) = (1/n) [wn−1] φ(w)n

This is how Catalan, Motzkin, and many tree-counting formulas are derived. In contests, if you arrive at an implicit GF equation, Lagrange inversion often yields a closed-form coefficient.

Watch out for coefficient extraction. When you derive a GF, double-check whether it is an OGF or EGF. For an EGF, [zn] gives an/n!, so you must multiply by n! to get the actual count. Forgetting this factor is a classic contest bug.

Summary

The symbolic method provides a mechanical translation from combinatorial descriptions to generating functions. Here is what to remember:

  1. Specify objects using atoms (𝒵), union (+), product (×), Seq, Set, Cyc.
  2. Choose OGF or EGF based on whether objects are unlabelled or labelled.
  3. Translate using the dictionary: union → add, product → multiply, Seq → 1/(1−), Set → exp (EGF), Cyc → log (EGF).
  4. Extract coefficients via closed forms, recurrences, or NTT-based polynomial arithmetic.
  5. Asymptotics: the dominant singularity of the GF determines the exponential growth rate via transfer theorems.
  6. In CP, recognise when a counting problem has a natural symbolic decomposition — the GF then gives you the solution structure (and often, an efficient algorithm via NTT).
Further reading. Flajolet & Sedgewick, Analytic Combinatorics (freely available online) is the definitive reference. For CP-specific applications, see the generating-functions chapters of Concrete Mathematics (Graham, Knuth, Patashnik) and the CP Handbook sections on polynomial techniques.