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

Transfer Matrix Method — Counting Paths via Matrix Exponentiation

The Transfer Matrix Idea

Many combinatorial problems reduce to counting walks of length n in a finite directed graph. The transfer matrix method exploits a beautifully simple fact from linear algebra: if A is the adjacency matrix of a graph with k vertices, then An[i][j] equals the number of walks of exactly n steps from vertex i to vertex j.

Why does this work? Consider the base case: A1[i][j] is 1 exactly when there is an edge from i to j — i.e., a walk of length 1. For the inductive step, a walk of length n from i to j must pass through some intermediate vertex m at step n−1. The number of such walks is An−1[i][m] · A[m][j], and summing over all m gives the matrix product (An−1 · A)[i][j] = An[i][j].

Connection to Linear Recurrences

The transfer matrix method is deeply connected to linear recurrences. If a counting sequence satisfies an = c1an−1 + c2an−2 + … + ckan−k, we can encode the recurrence as a k×k companion matrix and compute an via matrix exponentiation in O(k3 log n). This turns a problem that might require O(n) DP into O(k3 log n), which is crucial when n can be 1018.

Core insight. Whenever you have a DP with a fixed number of states k and transitions that don't depend on the step index, you can write the transition as a k×k matrix and compute the n-th step via fast matrix power. Time: O(k3 log n). This is the universal recipe.

The "transfer matrix" is the matrix T whose entry T[s][t] equals the number of ways to transition from state s to state t in one step. After n steps the answer is read from Tn. In the simplest case T is just the adjacency matrix, but in more complex scenarios the states represent DP profiles, automaton nodes, or other abstractions.

Matrix Exponentiation Refresher

Fast matrix exponentiation is the engine that powers the transfer matrix method. We compute Mn via repeated squaring in O(k3 log n), where k is the matrix dimension. All arithmetic is typically done modulo a prime (109+7).

Algorithm

function mat_pow(M, n):
  result ← identity matrix of size k×k
  while n > 0:
    if n is odd:
      result ← result × M
    M ← M × M
    n ← n / 2 // integer division
  return result

C++ Template

const int MOD = 1e9 + 7;

struct Matrix {
    int n;
    vector<vector<long long>> a;
    Matrix(int _n) : n(_n), a(_n, vector<long long>(_n, 0)) {}
    Matrix operator*(const Matrix& o) const {
        Matrix res(n);
        for (int i = 0; i < n; i++)
            for (int k = 0; k < n; k++) if (a[i][k])
                for (int j = 0; j < n; j++)
                    res.a[i][j] = (res.a[i][j] + a[i][k] * o.a[k][j]) % MOD;
        return res;
    }
};

Matrix mat_pow(Matrix m, long long p) {
    int n = m.n;
    Matrix res(n);
    for (int i = 0; i < n; i++) res.a[i][i] = 1; // identity
    while (p > 0) {
        if (p & 1) res = res * m;
        m = m * m;
        p >>= 1;
    }
    return res;
}

The if (a[i][k]) guard in the multiplication skips zero entries, which can significantly speed up sparse transfer matrices — a common scenario in profile DP where most transitions are invalid.

TLE pitfall: matrix size. The complexity is O(k3 log n). For k = 100 and n = 1018, that's ~60 × 106 operations per exponentiation — tight but feasible. For k > 200, you'll almost certainly TLE under typical 2-second time limits. Always check whether the number of DP states fits within this budget before committing to a matrix exponentiation approach.

Profile Dynamic Programming

Profile DP (also called "broken profile" DP) is the most common application of the transfer matrix method in competitive programming. The canonical problem: count the number of ways to tile an m×n grid with 1×2 dominoes.

Setting Up States

We process the grid column by column (left to right). After placing dominoes in the first j columns, some cells in column j+1 are already occupied by horizontal dominoes that started in column j. The profile (or "state") of column j is a bitmask of m bits, where bit i = 1 means row i of the next column is already filled.

There are 2m possible profiles. The transfer matrix T is a 2m × 2m matrix where T[mask1][mask2] = 1 if there exists a valid way to fill column j such that the incoming profile is mask1 and the outgoing profile (into column j+1) is mask2.

Building the Transfer Matrix

To compute T, we iterate over all pairs (mask1, mask2) and check validity. For each column, we process rows top to bottom. At each row i:

Worked Example: 3×n Tiling

For m = 3, there are 23 = 8 possible profiles. The transfer matrix T is 8×8. We build it by the rules above. The answer for a 3×n grid is (Tn)[0][0] — we start with an empty profile (mask = 0) and need to end with an empty profile (all cells of the last column fully covered).

For 3×n, a well-known closed form exists: the count is 0 when n is odd, and follows a linear recurrence an = 4·an−2 − an−4 for even n (with a0 = 1, a2 = 3). The matrix approach confirms this and generalizes effortlessly to any m.

// Build transfer matrix for m x n domino tiling
// m = number of rows (small), n = number of columns (large)
void build_transfer(int m, Matrix& T) {
    int S = 1 << m;
    // Try all ways to fill a column given incoming mask
    function<void(int, int, int)> fill = [&](int row, int in_mask, int out_mask) {
        if (row == m) {
            T.a[in_mask][out_mask] = 1;
            return;
        }
        if ((in_mask >> row) & 1) {
            // row already filled by horizontal domino from prev column
            fill(row + 1, in_mask, out_mask);
        } else {
            // Option 1: place horizontal domino extending into next column
            fill(row + 1, in_mask, out_mask | (1 << row));
            // Option 2: place vertical domino (need row+1 also free)
            if (row + 1 < m && !((in_mask >> (row + 1)) & 1)) {
                fill(row + 2, in_mask, out_mask);
            }
        }
    };
    for (int mask = 0; mask < S; mask++)
        fill(0, mask, 0);
}

long long count_tilings(int m, long long n) {
    int S = 1 << m;
    Matrix T(S);
    build_transfer(m, T);
    Matrix res = mat_pow(T, n);
    return res.a[0][0];
}
Key insight for tiling. The answer is always Tn[0][0] because we start and end with a completely empty profile. This pattern — start state to end state via matrix power — recurs in virtually every transfer matrix application.

Naive DP — O(2m · n)

Process columns one by one, maintaining a DP table of 2m states. Works when n is moderate (≤ 106), but fails for n up to 1018.

Matrix Exp — O(22m · 2m · log n)

= O(8m log n). The matrix is 2m × 2m. Feasible for m ≤ 6 (matrix 64×64) and any n up to 1018.

Counting Strings with Forbidden Patterns

A powerful application of the transfer matrix method: count the number of strings of length n over an alphabet Σ that avoid a set of forbidden substrings P = {p1, p2, …, pt}. The idea is to combine an Aho-Corasick automaton with matrix exponentiation.

Step 1: Build the Aho-Corasick Automaton

Insert all forbidden patterns into a trie and compute failure links (just like the standard Aho-Corasick construction). Mark any node that corresponds to the end of a forbidden pattern — or any node whose suffix link chain reaches such a node — as a forbidden state.

Step 2: Construct the Transfer Matrix

Let the automaton have k non-forbidden states (including the root). Build a k×k transfer matrix T where T[u][v] = number of characters c ∈ Σ such that the automaton transitions from state u to state v on character c, and v is not a forbidden state.

Step 3: Exponentiate

The answer is the sum of all entries in row 0 (the root) of Tn:

answer = Σj Tn[0][j]

Each entry Tn[0][j] counts the number of valid strings of length n that leave the automaton in non-forbidden state j.

// Aho-Corasick + matrix exponentiation for forbidden substrings
// Alphabet size = alpha (e.g., 26 for lowercase letters)
struct AhoCorasick {
    vector<array<int, 26>> go;
    vector<int> fail, is_end;
    int new_node() {
        go.push_back({});
        go.back().fill(-1);
        fail.push_back(0);
        is_end.push_back(0);
        return (int)go.size() - 1;
    }
    AhoCorasick() { new_node(); } // root = 0

    void add(const string& s) {
        int cur = 0;
        for (char c : s) {
            int ch = c - 'a';
            if (go[cur][ch] == -1) go[cur][ch] = new_node();
            cur = go[cur][ch];
        }
        is_end[cur] = 1;
    }

    void build() {
        queue<int> q;
        for (int c = 0; c < 26; c++) {
            if (go[0][c] == -1) go[0][c] = 0;
            else { fail[go[0][c]] = 0; q.push(go[0][c]); }
        }
        while (!q.empty()) {
            int u = q.front(); q.pop();
            is_end[u] |= is_end[fail[u]]; // propagate forbidden
            for (int c = 0; c < 26; c++) {
                if (go[u][c] == -1)
                    go[u][c] = go[fail[u]][c];
                else {
                    fail[go[u][c]] = go[fail[u]][c];
                    q.push(go[u][c]);
                }
            }
        }
    }

    Matrix transfer_matrix() {
        int sz = (int)go.size();
        // Collect non-forbidden states
        vector<int> id(sz, -1);
        int cnt = 0;
        for (int i = 0; i < sz; i++)
            if (!is_end[i]) id[i] = cnt++;
        Matrix T(cnt);
        for (int u = 0; u < sz; u++) if (!is_end[u]) {
            for (int c = 0; c < 26; c++) {
                int v = go[u][c];
                if (!is_end[v])
                    T.a[id[u]][id[v]]++;
            }
        }
        return T;
    }
};

long long count_valid_strings(vector<string>& forbidden, long long n) {
    AhoCorasick ac;
    for (auto& s : forbidden) ac.add(s);
    ac.build();
    Matrix T = ac.transfer_matrix();
    Matrix res = mat_pow(T, n);
    long long ans = 0;
    for (int j = 0; j < res.n; j++)
        ans = (ans + res.a[0][j]) % MOD;
    return ans;
}
Watch the matrix size! The Aho-Corasick automaton can have up to L = Σ|pi| nodes. If L > 150, the matrix is too large for exponentiation under typical constraints. Check the total pattern length before choosing this approach. For a single pattern of length L, you get an L×L matrix, which is fine for L ≤ ~100.

Linear Recurrences via Transfer Matrix

The most foundational application: given a linear recurrence an = c1·an−1 + c2·an−2 + … + ck·an−k with known initial values a0, …, ak−1, compute an for astronomically large n.

The Companion Matrix

Define the state vector vn = (an, an−1, …, an−k+1)T. The recurrence becomes vn = C · vn−1, where C is the companion matrix:

C = | c1 c2 c3 ... c_{k-1} c_k |
    | 1 0 0 ... 0 0 |
    | 0 1 0 ... 0 0 |
    | . . . ... . . |
    | 0 0 0 ... 1 0 |

Therefore vn = Cn · v0, and an is the first component of this product.

Fibonacci Example

The Fibonacci recurrence Fn = Fn−1 + Fn−2 has the companion matrix:

C = | 1 1 |
    | 1 0 |

Then (Fn+1, Fn)T = Cn · (F1, F0)T. Computing Cn takes O(23 log n) = O(8 log n) — about 240 multiplications for n = 1018.

General k-th Order Recurrence

// Solve: a[n] = c[0]*a[n-1] + c[1]*a[n-2] + ... + c[k-1]*a[n-k]
// init[] = { a[0], a[1], ..., a[k-1] }
long long solve_recurrence(vector<long long> c, vector<long long> init, long long n) {
    int k = (int)c.size();
    if (n < k) return init[n] % MOD;

    // Build companion matrix
    Matrix C(k);
    for (int j = 0; j < k; j++) C.a[0][j] = c[j] % MOD;
    for (int i = 1; i < k; i++) C.a[i][i - 1] = 1;

    Matrix Cn = mat_pow(C, n);
    long long ans = 0;
    for (int j = 0; j < k; j++)
        ans = (ans + Cn.a[0][j] % MOD * (init[j] % MOD)) % MOD;
    return (ans + MOD) % MOD;
}
Cayley-Hamilton optimization. The characteristic polynomial of the companion matrix has degree k. By the Cayley-Hamilton theorem, Ck can be written as a linear combination of I, C, C2, …, Ck−1. This means we can reduce computing Cn to computing a polynomial of degree < k in C, which takes O(k2 log n) instead of O(k3 log n). This technique — known as Kitamasa's method or Berlekamp-Massey + Bostan-Mori — is essential when k is large (~1000).

Kitamasa / Bostan-Mori Sketch

Instead of exponentiating the full k×k matrix, we compute xn mod p(x) where p(x) is the characteristic polynomial. This polynomial modular exponentiation uses O(k log k) per multiplication step (via NTT) and O(log n) steps, giving O(k log k · log n) total — much faster than O(k3 log n) for large k.

Concretely, Bostan-Mori computes the n-th term of a rational generating function P(x)/Q(x) in O(k log k · log n) by repeatedly halving n:

After O(log n) iterations, n reaches 0 and the answer is P(0)/Q(0).

C++ Implementation

Below are complete, contest-ready implementations that tie together the ideas above.

Complete Matrix Struct with Multiply and Power

#include <bits/stdc++.h>
using namespace std;

const long long MOD = 1e9 + 7;

struct Matrix {
    int n;
    vector<vector<long long>> a;
    Matrix() : n(0) {}
    Matrix(int _n) : n(_n), a(_n, vector<long long>(_n, 0)) {}

    static Matrix identity(int n) {
        Matrix I(n);
        for (int i = 0; i < n; i++) I.a[i][i] = 1;
        return I;
    }

    Matrix operator*(const Matrix& o) const {
        assert(n == o.n);
        Matrix res(n);
        for (int i = 0; i < n; i++)
            for (int k = 0; k < n; k++) {
                if (!a[i][k]) continue; // skip zeros
                for (int j = 0; j < n; j++)
                    res.a[i][j] = (res.a[i][j] + a[i][k] * o.a[k][j]) % MOD;
            }
        return res;
    }

    Matrix power(long long p) const {
        Matrix res = identity(n);
        Matrix base = *this;
        while (p > 0) {
            if (p & 1) res = res * base;
            base = base * base;
            p >>= 1;
        }
        return res;
    }
};

Profile DP: Domino Tiling (Full Solution)

// Count ways to tile an m x n grid with 1x2 dominoes
// m <= 6 (small), n up to 1e18
int main() {
    int m;
    long long n;
    cin >> m >> n;

    int S = 1 << m;
    Matrix T(S);

    // Build transfer matrix via recursive column filling
    function<void(int, int, int)> fill = [&](int row, int in_mask, int out_mask) {
        if (row == m) {
            T.a[in_mask][out_mask] = 1;
            return;
        }
        if ((in_mask >> row) & 1) {
            // cell already filled by horizontal domino from previous column
            fill(row + 1, in_mask, out_mask);
        } else {
            // horizontal domino: occupies this cell and extends into next column
            fill(row + 1, in_mask, out_mask | (1 << row));
            // vertical domino: occupies this cell and the one below
            if (row + 1 < m && !((in_mask >> (row + 1)) & 1))
                fill(row + 2, in_mask, out_mask);
        }
    };

    for (int mask = 0; mask < S; mask++)
        fill(0, mask, 0);

    Matrix res = T.power(n);
    cout << res.a[0][0] << "\n";
    return 0;
}

Aho-Corasick + Matrix Exp: String Counting (Full Solution)

// Count strings of length n over {a..z} avoiding all forbidden substrings
int main() {
    int t;
    long long n;
    cin >> t >> n;

    AhoCorasick ac; // as defined in the section above
    for (int i = 0; i < t; i++) {
        string s; cin >> s;
        ac.add(s);
    }
    ac.build();

    Matrix T = ac.transfer_matrix();
    Matrix res = T.power(n);

    long long ans = 0;
    for (int j = 0; j < res.n; j++)
        ans = (ans + res.a[0][j]) % MOD;
    cout << ans << "\n";
    return 0;
}

Count Paths of Exact Length k in a Graph

// Given a directed graph with V vertices and edges,
// count the number of walks of exactly length k from src to dst.
int main() {
    int V, E;
    long long k;
    int src, dst;
    cin >> V >> E >> k >> src >> dst;

    Matrix A(V);
    for (int i = 0; i < E; i++) {
        int u, v;
        cin >> u >> v;
        A.a[u][v]++; // handles multi-edges
    }

    Matrix res = A.power(k);
    cout << res.a[src][dst] << "\n";
    return 0;
}

CP Applications

The transfer matrix method appears across a wide range of competitive programming problems. Here are the major categories and representative examples.

1. Grid Tiling

Tile an m×n grid with various tile shapes. The transfer matrix dimension is 2m (or more for complex tiles). Classic examples:

2. Counting Paths of Fixed Length

Given a graph, count walks of exactly k steps between two vertices. This is the most direct application of Ak.

3. String Counting with Constraints

Count strings avoiding forbidden substrings (Aho-Corasick + matrix exp) or matching a regular language (DFA + matrix exp):

4. Linear Recurrences

Any problem where the answer satisfies a linear recurrence with constant coefficients and n is large:

Pattern recognition tip. If a DP has these three properties, reach for the transfer matrix: (1) fixed number of states, (2) transitions independent of the step index, (3) n is large (up to 1018). This combination is a nearly certain signal for matrix exponentiation.

5. Advanced: Semiring Generalizations

Matrix exponentiation works over any semiring, not just (+, ×) over integers mod p. Common semirings in CP:

Summary

The transfer matrix method is one of the most versatile tools in the competitive programmer's arsenal. Here is the recipe at a glance:

  1. Identify the states. These might be bitmask profiles, automaton nodes, or abstract DP states. The number of states k determines feasibility.
  2. Build the transfer matrix T. T[s][t] = number of ways to go from state s to state t in one step.
  3. Exponentiate. Compute Tn in O(k3 log n). Read the answer from the appropriate entry.

When to Use

  • Fixed-size state space (≤ ~200 states)
  • Step-independent transitions
  • Large n (up to 1018)
  • Counting or optimization on sequences/paths

Complexity Checklist

  • k ≤ 8: trivial — instant
  • k ≤ 64: comfortable under 2s
  • k ≤ 200: tight, check constants
  • k > 200: likely TLE, consider Cayley-Hamilton / Kitamasa
Final takeaway. Every DP with homogeneous transitions over a fixed state set is secretly a transfer matrix problem. Once you see the pattern, the implementation is mechanical: define states, build T, exponentiate, read off the answer. Practice recognising this structure — it unlocks dozens of otherwise-intractable problems.