Algorithm tutorials
Matrix Exponentiation for Linear Recurrences
Turn a linear recurrence or a small step-by-step DP into a matrix and raise it to the n-th power in O(k^3 log n): companion matrices, Fibonacci by fast doubling, augmenting the state with constants, and counting walks of exact length in a graph.
This is an extension topic beyond most interviews; it pays off when a simple DP or recurrence has a tiny state but the number of steps is astronomical (n up to 10^18), which shows up in hard OAs and as a follow-up to Fibonacci-style questions.
Intuition
If one step of a process transforms a small state vector linearly, that step is a matrix M, and n steps are M^n. Fast exponentiation computes M^n with O(log n) matrix multiplications, so a recurrence that would need 10^18 iterations finishes in about 60 squarings.
Fibonacci is the classic example. The state (F(i+1), F(i)) becomes (F(i+1) + F(i), F(i+1)):
| F(i+2) | | 1 1 | | F(i+1) |
| F(i+1) | = | 1 0 | x | F(i) |
so [[1, 1], [1, 0]]^n = [[F(n+1), F(n)], [F(n), F(n-1)]]. Any linear recurrence f(i) = c1*f(i-1) + ... + ck*f(i-k) works the same way with a k x k "companion" matrix: the first row holds the coefficients and the rows below shift the window by one.
Two more ideas extend the reach:
- Constants and extra terms:
f(i) = f(i-1) + f(i-2) + 1is not linear in(f(i-1), f(i-2)), but it is linear in(f(i-1), f(i-2), 1). Add the constant (ori, or a running sum) to the state. - Walks in a graph: if
Ais the adjacency matrix,(A^k)[i][j]counts walks with exactly k edges fromitoj. Every DP of the form "state = current node, one step per round" is a matrix power in disguise.
When to use it in interviews / OAs
- A recurrence or step-by-step DP with at most a few dozen states, but n up to 10^9-10^18.
- Counting sequences or strings of huge length whose next character depends only on the previous one (or previous few).
- Counting walks of an exact length in a small graph.
- The follow-up to "climbing stairs / Fibonacci / tiling": "now n is 10^18".
Template
def mat_mul(A: list[list[int]], B: list[list[int]], mod: int) -> list[list[int]]:
cols = list(zip(*B))
return [[sum(a * b for a, b in zip(row, col)) % mod for col in cols] for row in A]
def mat_pow(M: list[list[int]], n: int, mod: int) -> list[list[int]]:
k = len(M)
result = [[int(i == j) for j in range(k)] for i in range(k)] # identity matrix
while n:
if n & 1:
result = mat_mul(result, M, mod)
M = mat_mul(M, M, mod)
n >>= 1
return result
def linear_recurrence(coeffs: list[int], initial: list[int], n: int, mod: int) -> int:
# f(i) = coeffs[0]*f(i-1) + coeffs[1]*f(i-2) + ...; initial = [f(0), ..., f(k-1)]
k = len(coeffs)
if n < k:
return initial[n] % mod
# companion matrix: first row = coefficients, then shift the window down by one
companion = [coeffs[:]] + [[int(c == r - 1) for c in range(k)] for r in range(1, k)]
P = mat_pow(companion, n - k + 1, mod)
state = initial[::-1] # [f(k-1), f(k-2), ..., f(0)]
return sum(P[0][j] * state[j] for j in range(k)) % mod
For Fibonacci specifically, "fast doubling" squeezes the 2 x 2 matrix power into two identities, F(2k) = F(k) * (2F(k+1) - F(k)) and F(2k+1) = F(k)^2 + F(k+1)^2:
def fib_pair(n: int, mod: int) -> tuple[int, int]:
# returns (F(n), F(n+1))
if n == 0:
return 0, 1
a, b = fib_pair(n >> 1, mod) # a = F(k), b = F(k+1) with k = n // 2
c = a * (2 * b - a) % mod # F(2k)
d = (a * a + b * b) % mod # F(2k+1)
return (d, (c + d) % mod) if n & 1 else (c, d)
Counting walks
def count_walks(adj: list[list[int]], k: int, mod: int) -> list[list[int]]:
# entry [i][j] = number of walks with exactly k edges from i to j
return mat_pow(adj, k, mod)
def knight_dialer(n: int, mod: int = 10**9 + 7) -> int:
# phone keypad: digit -> digits a chess knight can jump to
jumps = {0: (4, 6), 1: (6, 8), 2: (7, 9), 3: (4, 8), 4: (0, 3, 9),
5: (), 6: (0, 1, 7), 7: (2, 6), 8: (1, 3), 9: (2, 4)}
adj = [[int(v in jumps[u]) for v in range(10)] for u in range(10)]
walks = count_walks(adj, n - 1, mod) # a number with n digits is a walk with n - 1 jumps
return sum(map(sum, walks)) % mod
The usual O(n * 10) DP is fine for n up to 10^5 or so; the matrix version answers n = 10^18 in a few hundred tiny multiplications.
Complexity
O(k^3 log n) time and O(k^2) space for a k x k matrix. Fast doubling for Fibonacci is O(log n). Because of the k^3 factor, this is practical in Python for k up to roughly 30-50; beyond that, use the plain DP if n allows it.
Common pitfalls
- Exponent off-by-one: write out the first two or three terms by hand and check that
M^(n-k+1)(or whatever you derived) lands onf(n). - Row vs column vectors: decide whether the state is a column vector (
v' = M v, coefficients in the first row) or a row vector (v' = v M, coefficients in the first column) and stay consistent. - Forgetting the modulus inside multiplication: entries grow exponentially; reduce after every product. In C++/Java make sure a sum of k products fits in 64 bits.
- Negative intermediate values: fast doubling computes
2*b - a, which is negative before the modulo in C++/Java; addmodbefore taking%. - Non-linear steps:
max,minor conditions on the value cannot be expressed as an ordinary matrix product. (The "min-plus" product, where multiplication becomes+and addition becomesmin, handles shortest paths with exactly k edges.)
Interview variations
- Fibonacci, climbing stairs, tribonacci for huge n.
- Tilings of a 2 x n or 3 x n board for huge n: the DP over column profiles is a matrix.
- Knight dialer, count vowel strings with follow rules: walks in a small transition graph.
- Painting an n x 3 grid: transitions between the 12 valid row colorings.
- Recurrences with a constant or polynomial term: augment the state with 1, i, or a running sum.