Mathematical Algorithms/Matrix Exponentiation

Lesson 10.54,044 words

Matrix Exponentiation

A linear recurrence advances by a fixed linear rule, so one step is a matrix–vector product and nn steps are a matrix power. Packaging Fibonacci, and any kk-term recurrence, into a transition matrix lets us jump to the nn-th term in O(k3logn)O(k^3 \log n) by exponentiation by squaring — the same doubling trick from modular exponentiation, now over matrices.

╌╌╌╌

Modular exponentiation computed in multiplications by reading the bits of and repeatedly squaring. That lesson noted that the doubling idea is not special to integers: replace multiply with matrix multiply and the same loop computes the -th Fibonacci number in steps. This lesson makes that precise and general. A linear recurrence — a rule that builds each term from a fixed linear combination of the previous few — is repeated application of one fixed matrix. Computing the -th term is then computing the -th power of that matrix, and exponentiation by squaring does it in logarithmic time.

A recurrence is a matrix

Start with Fibonacci: , , and . The naive loop is additions, and the closed form (Binet's formula) involves irrational powers of the golden ratio, awkward to evaluate exactly. Instead, package two consecutive terms into a state vector and ask what one step does to it. The update together with the trivial is a linear map:

Call that matrix . One multiplication by advances the state by one term. Two multiplications advance it by two. Applying a total of times to the initial state lands on . So the whole question reduces to computing the matrix power .

One step of the Fibonacci map. The state holds two consecutive terms; multiplying by slides the window forward by one, so applications carry to

The top-right entry of is directly, so once we can raise to the -th power we can read off with no vector multiply at all. It remains to compute fast.

Exponentiation by squaring, over matrices

Matrix multiplication is associative, which is the only property repeated squaring ever used. So the entire argument from the modular-exponentiation lesson carries over verbatim with the scalar product replaced by the matrix product and the scalar replaced by the identity matrix . Write in binary as ; then

where the powers are the repeated squares of , each the square of the previous. Sweeping the bits of from low to high, we keep a running square and fold it into an accumulator whenever the bit is set.

One multiply doubles the exponent. Each box is the square of the one before it, so the reachable power grows — only boxes to reach
Doubling on matrices: each square halves the exponent's remaining bits; the set bits of select which squares multiply into the accumulator

There are squarings and at most that many accumulator multiplications, so matrix multiplications in all. Each multiplication of two matrices is by the schoolbook method, giving the headline cost.

For Fibonacci , so this is a flat — exponentially faster than the loop once is large, and exact (only integer arithmetic, no floating-point golden ratio). The schoolbook per product can be shaved to with Strassen's algorithm, but for the small typical of recurrences the constant-factor simplicity of the cubic method wins.

Algorithm 1:Mat-Pow(M,n)\textsc{Mat-Pow}(M, n)MnM^n by repeated squaring, O(k3logn)O(k^3 \log n)
  1. 1
    RIR \gets I
    identity, the accumulator
  2. 2
    while n>0n > 0 do
  3. 3
    if nmod2=1n \bmod 2 = 1 then
    low bit set
  4. 4
    RRMR \gets R \cdot M
    fold running square into result
  5. 5
    MMMM \gets M \cdot M
    square for next bit
  6. 6
    nn/2n \gets \lfloor n / 2 \rfloor
  7. 7
    return RR
matrix_power.pypython
from __future__ import annotations

from typing import Optional, Sequence

class Matrix:
  """
    A dense rectangular matrix of integers, stored row by row.\n
    The companion-matrix and walk-counting algorithms in this lesson all\n
    multiply and power square matrices through this one class.\n
  """

  def __init__(self, rows: Sequence[Sequence[int]]) -> None:
    """
      Build a matrix from a sequence of equal-length integer rows.\n
    """
    # copy the rows and record the shape.
    self.rows: list[list[int]] = [list(row) for row in rows]
    self.row_count: int = len(self.rows)
    self.column_count: int = len(self.rows[0]) if self.rows else 0

    # reject ragged input.
    for row in self.rows:
      if len(row) != self.column_count:
        raise ValueError("every row must have the same length")

  @classmethod
  def identity(cls, size: int) -> Matrix:
    """
      The size x size identity matrix — the unit for matrix multiply.\n
    """
    return cls([
      [1 if column == row else 0 for column in range(size)]
      for row in range(size)
    ])

  def __getitem__(self, position: tuple[int, int]) -> int:
    """
      The entry at (row, column).\n
    """
    row, column = position
    return self.rows[row][column]

  def __eq__(self, other: object) -> bool:
    if not isinstance(other, Matrix):
      return NotImplemented
    return self.rows == other.rows

  def __repr__(self) -> str:
    return f"Matrix({self.rows!r})"

  def multiply(self, other: Matrix, modulus: Optional[int] = None) -> Matrix:
    """
      The schoolbook product of this matrix with `other`, optionally\n
      reduced entrywise mod `modulus`. Costs O(rows * inner * columns).\n
    """
    if self.column_count != other.row_count:
      raise ValueError("inner dimensions must match for multiplication")

    # each output entry is a dot product of a left row with a right column.
    product: list[list[int]] = []
    for left_row in self.rows:
      result_row: list[int] = []
      for column in range(other.column_count):

        # dot the row against the column, reducing mod if asked.
        total: int = sum(
          left_row[inner] * other.rows[inner][column]
          for inner in range(self.column_count)
        )
        result_row.append(total % modulus if modulus is not None else total)

      product.append(result_row)
    return Matrix(product)

  def power(self, exponent: int, modulus: Optional[int] = None) -> Matrix:
    """
      This (square) matrix raised to a non-negative `exponent` by repeated\n
      squaring — the same low-to-high bit scan as scalar mod_pow, with the\n
      identity matrix as the accumulator's starting value. O(k^3 log n).\n
    """
    if self.row_count != self.column_count:
      raise ValueError("only square matrices can be raised to a power")
    if exponent < 0:
      raise ValueError("exponent must be non-negative")

    # accumulate from the identity; square repeatedly while scanning the bits.
    result: Matrix = Matrix.identity(self.row_count)
    square: Matrix = self
    remaining: int = exponent

    while remaining > 0:

      # fold the running square into the result when the low bit is set.
      if remaining & 1:
        result = result.multiply(square, modulus)
      square = square.multiply(square, modulus)
      remaining >>= 1

    # the seed identity is unreduced, so reduce it once at the end.
    if modulus is not None:
      result = Matrix([[entry % modulus for entry in row] for row in result.rows])
    return result

  def apply(
    self,
    vector: Sequence[int],
    modulus: Optional[int] = None,
  ) -> list[int]:
    """
      This matrix times a column `vector`, optionally reduced mod `modulus`.\n
    """
    if len(vector) != self.column_count:
      raise ValueError("vector length must match the column count")

    # each output entry dots one row against the vector.
    result: list[int] = []
    for row in self.rows:
      total: int = sum(entry * value for entry, value in zip(row, vector))
      result.append(total % modulus if modulus is not None else total)
    return result

The loop is byte-for-byte the iterative from the previous lesson, with the scalar identity swapped for the matrix identity and $\cdot$ now meaning matrix multiply. To compute a recurrence modulo a prime — the usual demand when the true terms overflow — reduce every entry mod after each multiply, exactly as reduces after each scalar product.

Worked example ( by squaring). Take and compute ; the top-right entry is . The exponent is , so the set bits are at positions and . Build the repeated squares:

Each is the square of the previous — for instance has top-left . The bits select and , so

and reading the entries confirms , , — the Fibonacci identity in a single squaring chain of four multiplies instead of ten additions. The gap only widens with : reaching is multiplies, not additions.

from two of four repeated squares. Bits and of select and ; their product is , whose entries are .

The general -term recurrence

Nothing above used . Suppose a sequence obeys an order- linear recurrence with fixed coefficients ,

Carry the last terms as the state vector . The new top entry is the recurrence itself; every other entry is just a copy shifted down by one. That compute the combination on top, shift the rest down rule is a single matrix, the companion matrix of the recurrence:

The top row holds the coefficients and produces ; the sub-diagonal of s copies each old term down one slot, discarding the oldest. The figure makes the two roles visible: one row that mixes, and a staircase of s that shifts.

Companion matrix of an order- recurrence: top row mixes by the coefficients, the sub-diagonal of s shifts the state down one slot

Then where holds the given initial terms, and delivers 's power in . The pattern generalizes: counting problems whose answer satisfies a fixed linear recurrence — tilings, constrained strings, walk counts — are all handled this way. Climbing Stairs is literally Fibonacci; Count Vowels Permutation and Student Attendance Record II track a constant number of states whose transitions are linear, so each is a small companion (or transfer) matrix raised to the -th power.1

linear_recurrence.pypython
from typing import Optional, Sequence

from matrix_power import Matrix

def companion_matrix(coefficients: Sequence[int]) -> Matrix:
  """
    The companion matrix of an order-k recurrence whose coefficients are\n
    (c1, c2, ..., ck) applied to (a(n-1), ..., a(n-k)). The top row holds\n
    the coefficients; the sub-diagonal of 1s copies each old term down one\n
    slot, discarding the oldest.\n
  """
  order: int = len(coefficients)
  if order == 0:
    raise ValueError("a recurrence needs at least one coefficient")

  # top row is the coefficients; each later row is a shifting 1.
  rows: list[list[int]] = [list(coefficients)]
  for shifted_row in range(1, order):
    row: list[int] = [0 for _ in range(order)]
    row[shifted_row - 1] = 1
    rows.append(row)

  return Matrix(rows)

def linear_recurrence(
  coefficients: Sequence[int],
  initial_terms: Sequence[int],
  index: int,
  modulus: Optional[int] = None,
) -> int:
  """
    The term a(index) of the order-k recurrence\n
    a(n) = c1*a(n-1) + ... + ck*a(n-k), given the k `initial_terms`\n
    a(0), a(1), ..., a(k-1) and the matching `coefficients` (c1, ..., ck).\n
    Computed in O(k^3 log index) matrix multiplications; pass `modulus` to\n
    reduce every entry after each product. The index must be non-negative.\n
  """
  order: int = len(coefficients)
  if len(initial_terms) != order:
    raise ValueError("need exactly one initial term per coefficient")
  if index < 0:
    raise ValueError("index must be non-negative")

  # the first k terms are given outright; no powering needed.
  if index < order:
    value: int = initial_terms[index]
    return value % modulus if modulus is not None else value

  # state holds (a(k-1), a(k-2), ..., a(0)) top-to-bottom; powering the
  # companion matrix (index - (k-1)) times advances it to (a(index), ...).
  state: list[int] = list(reversed(initial_terms))
  transition: Matrix = companion_matrix(coefficients)

  # advance the state to a(index) by powering the companion matrix.
  powered: Matrix = transition.power(index - (order - 1), modulus)
  return powered.apply(state, modulus)[0]

def affine_recurrence(
  coefficients: Sequence[int],
  constant: int,
  initial_terms: Sequence[int],
  index: int,
  modulus: Optional[int] = None,
) -> int:
  """
    The term a(index) of the inhomogeneous recurrence\n
    a(n) = c1*a(n-1) + ... + ck*a(n-k) + `constant`. The trick is to append\n
    a constant 1-track to the state and a row that carries `constant`, so\n
    the augmented (k+1) x (k+1) matrix is again fixed and the same power\n
    method applies. Initial terms and index are as in `linear_recurrence`.\n
  """
  order: int = len(coefficients)
  if len(initial_terms) != order:
    raise ValueError("need exactly one initial term per coefficient")
  if index < 0:
    raise ValueError("index must be non-negative")

  if index < order:
    value: int = initial_terms[index]
    return value % modulus if modulus is not None else value

  # augment the order-k state with a trailing constant 1; the extra column
  # in the top row injects `constant` into each new term, and a final
  # identity row keeps the constant track fixed at 1.
  # top row gains the `constant` column; older rows gain a trailing 0.
  base: Matrix = companion_matrix(coefficients)
  augmented_rows: list[list[int]] = [[*base.rows[0], constant]]
  augmented_rows.extend([*row, 0] for row in base.rows[1:])

  # a final identity row pins the constant track at 1.
  augmented_rows.append([0 for _ in range(order)] + [1])
  transition: Matrix = Matrix(augmented_rows)

  # carry the constant alongside the state, then power as before.
  state: list[int] = list(reversed(initial_terms)) + [1]
  powered: Matrix = transition.power(index - (order - 1), modulus)
  return powered.apply(state, modulus)[0]

Worked example (a tiling count). The number of ways to tile a board with dominoes obeys with — a shifted Fibonacci. A richer case is the -term tribonacci count with , whose companion matrix is

The top row adds the last three terms; the two sub-diagonal s shift the window. Starting from , one multiply gives , so ; another gives , then . To leap to directly, raise to the power by : is matrix multiplies of a fixed matrix, independent of how large the terms grow.

Graph walks: the same matrix, a different reading

The companion matrix is one instance of a broader principle: whenever a process advances by a fixed linear rule, its -step behaviour is a matrix power. The cleanest other instance is counting walks in a graph. Let be the adjacency matrix of a graph, so when there is an edge . Then the matrix power counts paths:

Walk counting as matrix product. Every length- walk from to is a length- walk from to some neighbour followed by the edge ; summing over is exactly

Worked example (counting length- walks). Take the triangle-with-a-tail graph on vertices with edges , , (an undirected triangle), adjacency matrix

Read : there are exactly two length- walks from vertex back to itself, namely and . The off-diagonal counts the single length- walk . Every entry is a sum over the intermediate vertex , exactly , and raising to higher powers counts longer walks with no new idea — just more multiplies, each delivered in of them by .

So how many length- walks? is answered in by raising to the -th power with — and the companion-matrix recurrence above is just this theorem applied to the small state-transition graph of the recurrence. The same power, taken over the boolean semiring (replace with OR, with AND), computes reachability within steps; over the min-plus semiring (replace with , with ) it computes shortest-path lengths, which underlies the all-pairs shortest-path algorithms.

walk_counting.pypython
from __future__ import annotations

from collections.abc import Hashable
from typing import Optional, TypeVar

from graph import Graph
from matrix_power import Matrix

Label = TypeVar("Label", bound=Hashable)

def adjacency_matrix(graph: Graph[Label]) -> tuple[Matrix, list[Label]]:
  """
    The 0/1 adjacency matrix of `graph` and the label order indexing it.\n
    Entry (i, j) is 1 when an edge runs from the i-th to the j-th label.\n
    Parallel edges between the same pair collapse to a single 1.\n
  """
  # fix a label order and an empty size x size grid of zeros.
  ordered_labels: list[Label] = graph.labels
  index_of: dict[Label, int] = {
    label: position for position, label in enumerate(ordered_labels)
  }
  size: int = len(ordered_labels)
  rows: list[list[int]] = [[0 for _ in range(size)] for _ in range(size)]

  # mark a 1 for every edge, collapsing parallel edges.
  for vertex in graph:
    source_index: int = index_of[vertex.label]
    for neighbor in vertex.neighbors():
      rows[source_index][index_of[neighbor.label]] = 1

  return Matrix(rows), ordered_labels

def count_walks(
  graph: Graph[Label],
  source: Label,
  target: Label,
  length: int,
  modulus: Optional[int] = None,
) -> int:
  """
    The number of walks of exactly `length` edges from `source` to `target`\n
    in `graph`, read off (A^length)[source][target]. A length of 0 counts\n
    the empty walk: 1 when source equals target, else 0. O(V^3 log length).\n
  """
  if length < 0:
    raise ValueError("length must be non-negative")

  # build A and recover the label-to-index map.
  matrix, ordered_labels = adjacency_matrix(graph)
  index_of: dict[Label, int] = {
    label: position for position, label in enumerate(ordered_labels)
  }

  # raise A to the length and read off the source-target entry.
  powered: Matrix = matrix.power(length, modulus)
  return powered[index_of[source], index_of[target]]

def total_walks(
  graph: Graph[Label],
  length: int,
  modulus: Optional[int] = None,
) -> int:
  """
    The total number of walks of exactly `length` edges over all ordered\n
    pairs of vertices — the sum of every entry of A^length.\n
  """
  if length < 0:
    raise ValueError("length must be non-negative")

  # power A and sum every entry — walks over all ordered pairs.
  matrix, _ = adjacency_matrix(graph)
  powered: Matrix = matrix.power(length, modulus)
  total: int = sum(entry for row in powered.rows for entry in row)
  return total % modulus if modulus is not None else total
semiring_power.pypython
import math
from typing import Sequence

# A walk of "no edges" has cost zero; an absent walk has infinite cost.
INFINITY: float = math.inf

def boolean_multiply(
  left: Sequence[Sequence[bool]],
  right: Sequence[Sequence[bool]],
) -> list[list[bool]]:
  """
    Boolean matrix product: entry (i, j) is True when some index k has both\n
    left[i][k] and right[k][j] — that is, OR over AND.\n
  """
  # OR over AND: (i, j) is True once any k links left[i][k] to right[k][j].
  size: int = len(left)
  product: list[list[bool]] = [[False for _ in range(size)] for _ in range(size)]
  for row in range(size):
    for column in range(size):
      product[row][column] = any(
        left[row][inner] and right[inner][column] for inner in range(size)
      )
  return product

def boolean_power(
  matrix: Sequence[Sequence[bool]],
  exponent: int,
) -> list[list[bool]]:
  """
    The `exponent`-th boolean power of a square matrix by repeated squaring.\n
    With an adjacency matrix, entry (i, j) of the result is True exactly\n
    when j is reachable from i by a walk of length exactly `exponent`. The\n
    identity is the boolean identity (True on the diagonal). O(V^3 log n).\n
  """
  if exponent < 0:
    raise ValueError("exponent must be non-negative")

  # seed with the boolean identity (True on the diagonal) and a copy of A.
  size: int = len(matrix)
  result: list[list[bool]] = [
    [row == column for column in range(size)] for row in range(size)
  ]
  square: list[list[bool]] = [list(row) for row in matrix]

  # square repeatedly, folding in when the low bit is set.
  remaining: int = exponent
  while remaining > 0:
    if remaining & 1:
      result = boolean_multiply(result, square)
    square = boolean_multiply(square, square)
    remaining >>= 1

  return result

def min_plus_multiply(
  left: Sequence[Sequence[float]],
  right: Sequence[Sequence[float]],
) -> list[list[float]]:
  """
    Min-plus (tropical) matrix product: entry (i, j) is the minimum over k\n
    of left[i][k] + right[k][j] — that is, min over +. INFINITY marks the\n
    absence of an edge or path.\n
  """
  # min over +: (i, j) is the cheapest two-hop path left[i][k] + right[k][j].
  size: int = len(left)
  product: list[list[float]] = [[INFINITY for _ in range(size)] for _ in range(size)]
  for row in range(size):
    for column in range(size):
      product[row][column] = min(
        (left[row][inner] + right[inner][column] for inner in range(size)),
        default=INFINITY,
      )
  return product

def min_plus_power(
  matrix: Sequence[Sequence[float]],
  exponent: int,
) -> list[list[float]]:
  """
    The `exponent`-th min-plus power of a weight matrix by repeated\n
    squaring. With a weighted adjacency matrix (INFINITY off the edges,\n
    0 on the diagonal), entry (i, j) is the lightest walk from i to j using\n
    at most `exponent` edges. The identity is 0 on the diagonal and\n
    INFINITY elsewhere — the min-plus unit. O(V^3 log n).\n
  """
  if exponent < 0:
    raise ValueError("exponent must be non-negative")

  # seed with the min-plus unit (0 on the diagonal, INFINITY off) and a copy.
  size: int = len(matrix)
  result: list[list[float]] = [
    [0.0 if row == column else INFINITY for column in range(size)]
    for row in range(size)
  ]
  square: list[list[float]] = [list(row) for row in matrix]

  # square repeatedly, folding in when the low bit is set.
  remaining: int = exponent
  while remaining > 0:
    if remaining & 1:
      result = min_plus_multiply(result, square)
    square = min_plus_multiply(square, square)
    remaining >>= 1

  return result
graph.pypython
from collections.abc import Hashable, Iterator
from typing import Generic, Optional, TypeVar


Label = TypeVar("Label", bound=Hashable)


class Edge(Generic[Label]):
  """
    A directed connection from `source` to `target`, carrying a weight.\n
  """

  def __init__(
    self,
    source: Vertex[Label],
    target: Vertex[Label],
    weight: float = 1.0,
  ) -> None:
    self.source: Vertex[Label] = source
    self.target: Vertex[Label] = target
    self.weight: float = weight

  def __repr__(self) -> str:
    return f"Edge({self.source.label!r} -> {self.target.label!r}, w={self.weight})"


class Vertex(Generic[Label]):
  """
    A graph vertex: a label plus the list of edges leaving it.\n
  """

  def __init__(self, label: Label) -> None:
    self.label: Label = label
    self.outgoing: list[Edge[Label]] = []

  def neighbors(self) -> list[Vertex[Label]]:
    """
      The vertices reachable from this one by a single edge.\n
    """
    return [edge.target for edge in self.outgoing]

  def edge_to(self, label: Label) -> Optional[Edge[Label]]:
    """
      The outgoing edge to the vertex with `label`, or None.\n
    """
    for edge in self.outgoing:
      if edge.target.label == label:
        return edge
    return None

  def __repr__(self) -> str:
    return f"Vertex({self.label!r})"


class Graph(Generic[Label]):
  """
    A graph of Vertex objects linked by Edge objects.\n
    Pass `directed=True` for a digraph; otherwise each `add_edge` inserts\n
    the reverse edge too.\n
  """

  def __init__(self, directed: bool = False) -> None:
    self.directed: bool = directed
    self._vertices: dict[Label, Vertex[Label]] = {}

  def add_vertex(self, label: Label) -> Vertex[Label]:
    """
      Return the vertex for `label`, creating it if it is absent.\n
    """
    # reuse the existing vertex, or mint and register a fresh one.
    vertex = self._vertices.get(label)
    if vertex is None:
      vertex = Vertex(label)
      self._vertices[label] = vertex
    return vertex

  def add_edge(
    self,
    source_label: Label,
    target_label: Label,
    weight: float = 1.0,
  ) -> None:
    """
      Connect two labels (creating either vertex as needed).\n
      Adds the reverse edge as well when the graph is undirected.\n
    """
    source = self.add_vertex(source_label)
    target = self.add_vertex(target_label)

    # link source to target, and mirror it back when undirected.
    source.outgoing.append(Edge(source, target, weight))
    if not self.directed:
      target.outgoing.append(Edge(target, source, weight))

  def vertex(self, label: Label) -> Vertex[Label]:
    """
      The vertex carrying `label` (raises KeyError if absent).\n
    """
    return self._vertices[label]

  @property
  def vertices(self) -> list[Vertex[Label]]:
    """
      Every vertex, in insertion order.\n
    """
    return list(self._vertices.values())

  @property
  def labels(self) -> list[Label]:
    """
      Every vertex label, in insertion order.\n
    """
    return list(self._vertices)

  def edges(self) -> Iterator[Edge[Label]]:
    """
      Each edge once — an undirected edge is yielded a single time.\n
    """
    # track undirected endpoint pairs so each is emitted only once.
    seen: set[frozenset[Label]] = set()

    for vertex in self._vertices.values():
      for edge in vertex.outgoing:
        # skip an undirected edge already yielded from the other endpoint.
        if not self.directed:
          endpoints = frozenset((edge.source.label, edge.target.label))
          if endpoints in seen:
            continue
          seen.add(endpoints)

        yield edge

  def __contains__(self, label: Label) -> bool:
    return label in self._vertices

  def __iter__(self) -> Iterator[Vertex[Label]]:
    return iter(self._vertices.values())

  def __len__(self) -> int:
    return len(self._vertices)

Faster recurrence solvers and the semiring view

Matrix powering is the textbook route to a linear recurrence's -th term, but it is neither the only route nor always the fastest.

Faster than cubic: Kitamasa and Fiduccia. The cost is dominated by full multiplies. But the answer is a fixed linear combination of the first terms, and the coefficients of that combination are the residue of modulo the recurrence's characteristic polynomial. Computing that polynomial by repeated squaring of polynomials — the Fiduccia / Kitamasa method — costs with schoolbook polynomial multiplication, and when the polynomial products use the FFT.2 For a recurrence of large order this is a decisive improvement over the cubic matrix approach.

Eigen-decomposition and Binet's formula. If the companion matrix is diagonalizable, with diagonal, then and is just the eigenvalues raised to the -th power. For Fibonacci the eigenvalues are the golden ratio and its conjugate, which recovers Binet's formula exactly. The closed form is exact on paper but numerically fragile (the irrational powers must be carried to high precision), which is why the integer matrix power, exact by construction, is the algorithm of choice.3

One idea, three algorithms. The semiring view unifies several algorithms in this course. Over the ordinary ring, counts walks; over the boolean semiring it computes reachability, and the repeated-squaring version is essentially transitive closure; over the min-plus (tropical) semiring it computes shortest walks of bounded length, and squaring the distance matrix times gives the repeated-squaring all-pairs shortest paths algorithm that precedes Floyd–Warshall. The doubling trick on this page is the same one used in those graph algorithms.

Takeaways

  • A linear recurrence of order is one fixed matrix applied repeatedly: pack the last terms into a state vector, and one step is a multiply by the companion matrix (top row = coefficients, sub-diagonal of s = shift).
  • Computing the -th term is computing , done in matrix multiplications by exponentiation by squaring — the identical bit-scanning loop as , with scalar replaced by matrix and by .
  • Total cost is (cubic per multiply, logarithmically many multiplies); for Fibonacci this is a flat , exact and overflow-free under a modulus.
  • The same matrix power counts length- walks in a graph via , and over other semirings (boolean, min-plus) computes reachability and shortest paths — one algebraic idea behind recurrences, walk-counting, and transitive closure.

Footnotes

  1. Skiena, § — Number Theory / Dynamic Programming: linear recurrences advanced by powering a transition matrix in matrix multiplications; the companion-matrix construction.
  2. C. M. Fiduccia, An efficient formula for linear recurrences, SIAM J. Computing 14(1), 1985 — computing via modulo the characteristic polynomial; the same method is widely known in competitive programming as Kitamasa's algorithm.
  3. CLRS, Ch. 4 — Divide-and-Conquer: matrix powering for recurrences; and Problem 31-3 (Binet-style closed forms via eigenvalues) for the golden-ratio decomposition.
Practice

╌╌ END ╌╌