# Adjacency Matrices in Advent Of Code
## The iterate-step pattern
One common pattern for solvers on advent of code to use is what I call `iterate step`. In this pattern you write some function $f : S \rightarrow S$, and then apply $f$ to your initial state $s_0$ repeatedly. For instance, in [2021 day 14](https://adventofcode.com/2021/day/14) you might write
```haskell
type S = String
type Rules = Map (Char,Char) Char
step :: Rules -> S -> S
step rs = \s -> (head s :) $ concat $ zipWith go s (tail s)
where
go :: Char -> Char -> String
go a b = case Map.lookup (a,b) rs of
Nothing -> [b]
Just c -> [c,b]
part1 s rs = fin $ count $ iterate (step rs) s !! 10
```
Where `fin` above is just the function which goes from a count per letter to whatever specific thing they ask for (in this case the max value minus the min value).
## Improving iterate-step for Part 2
There are a few typical failure modes for the `iterate step` pattern. By far the most common failure mode is that your state type $S$ grows too large. For instance, consider this naive solution to [2021 day 6](https://adventofcode.com/2021/day/6):
```python=
def step(fish):
return [
f
for old in fish
for f in ([6,8] if old==0 else [old-1])
]
def part1(fish):
for _ in range(80):
fish = step(fish) # This is the iterate-step pattern in python
return len(fish)
```
Here we can go to part 2 by changing from storing a list of all fish to a list of the counts of all fish:
```python=
def step(fish_counts):
return {
age: fish_counts.get(age+1,0) + (fish_counts[0] if age in [6,8] else 0)
for age in range(9)
}
def part2(fish):
fish_counts = {
age:len([f for f in fish if f==age])
for age in range(9)
}
for _ in range(256):
fish_counts = step(fish_counts) # This is the iterate-step pattern in python
return sum(fish_counts.values())
```
## But what if there was a Part 3?
This is all well in good if you want to do 256 steps. But what happens if you want to do $10^{12}$ steps? Obviously, you cannot actually run the step function that often.
This is where adjacency matrices can help. An _adjacency matrix_ is a way to encode state changes that is particularly useful if your problem is _linearly reducible_. For instance, in day 6 2021 (the fish problem), it is clear that if you knew `part2([0])`, `part2([1])`, up to `part2([8])`, you could compute `part2` of arbitrary inputs.
To construct an adjacency matrix you must first encode your state into a state _vector_. I will call the function from a state to a vector $V$. For some cases this is easy. For instance, in the fish example, an obvious vector is $V(s)=(a_i)$ where $a_i=\text{# of fish aged }i$. For the example input `3,4,3,1,2`, this would create
$$
V(s_0)=
\begin{pmatrix}
0&1&1&2&1&0&0&0&0
\end{pmatrix}
^T
$$
After one day you end up `2,3,2,0,1`
$$
V(s_1)=
\begin{pmatrix}
1&1&2&1&0&0&0&0&0
\end{pmatrix}
^T
$$
And after two days you end up with `1,2,1,6,0,8`
$$
V(s_2)=
\begin{pmatrix}
1&2&1&0&0&0&1&0&1
\end{pmatrix}
^T
$$
I claim that there exists a matrix $A$ such that $V(s_{i+1})=AV(s_i)$, and name that matrix the adjacency matrix.
### Why Linear Algebra?
Before constructing this matrix, note why this would immediately solve our problem for even extremely large numbers of steps. The proof is somewhat simple: recursively applying the above formula, we see that $V(s_4)=A(A(A(AV(s))))$. But matrix multiplication is associative, so this is equivalent to $V(s_4)=(A*A*A*A)V(s)=A^4V(s)$. We can compute $A^4$ by repeated squaring in 3 moves; while each move may take longer than the `step` function above, you will in general compute $V(s_i)$ in $O(\log i)$ moves. What's more interesting, since the entire work is in computing $A^i$, if you had two different initial states $s_0$ and $s_0'$, computing both answers takes as much time as computing either answer.
### Computing the adjacency matrix
Computing the adjacency matrix is simple. To encode our earlier intuition that if we knew `part2([0])`, `part2([1])`, up to `part2([8])` we would know the entire behavior, we look at the result of `step([0])` etc. We have the following (note the transposition):
$$
V(step([0])) =
\begin{pmatrix}
0&0&0&0&0&0&1&0&1
\end{pmatrix}
^T \\
V(step([1])) =
\begin{pmatrix}
1&0&0&0&0&0&0&0&0
\end{pmatrix}
^T \\
V(step([2])) =
\begin{pmatrix}
0&1&0&0&0&0&0&0&0
\end{pmatrix}
^T \\
\dots \\
V(step([8])) =
\begin{pmatrix}
0&0&0&0&0&0&0&1&0
\end{pmatrix}
^T
$$
Then, the matrix is simply
$$
A =
\begin{pmatrix}
0 & 1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\
0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 & 0 \\
0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 \\
0 & 0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 \\
0 & 0 & 0 & 0 & 0 & 1 & 0 & 0 & 0 \\
0 & 0 & 0 & 0 & 0 & 0 & 1 & 0 & 0 \\
1 & 0 & 0 & 0 & 0 & 0 & 0 & 1 & 0 \\
0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 1 \\
1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0
\end{pmatrix}
$$
## Final Code
Here is code that would compute `part2` for the 2021 day 6:
```python=
adjacency = np.array([
[(1 if j == i+1 or (j==0 and i in [6,8]) else 0) for j in range(9)]
for i in range(9)
], dtype = np.longlong)
p2m = np.linalg.matrix_power(adjacency, 256)
def part2(fish):
fish_counts = np.array([
len([f for f in fish if f==age])
for age in range(9)
], dtype = np.longlong)
return sum(np.matmul(p2m, fish_counts))
part2([3,4,3,1,2]) # 26984457539
```
And here is code that will compute the hypothetical part 3, which wanted you to find the value at $10^{12}$. Note that we overflow the `np.ulonglong` data type: we leave it as an exercise to the reader to implement finding ie the precise last 15 digits of this value.
```python=
N = 10 ** 12
adjacency = np.array([
[(1 if j == i+1 or (j==0 and i in [6,8]) else 0) for j in range(9)]
for i in range(9)
], dtype = np.ulonglong)
p3m = np.linalg.matrix_power(adjacency, N)
def part3(fish):
fish_counts = np.array([
len([f for f in fish if f==age])
for age in range(9)
], dtype = np.ulonglong)
return sum(np.matmul(p3m, fish_counts)) % np.iinfo(np.ulonglong).max
part3([3,4,3,1,2]) # 1.178868630105016e+19 % np.iinfo(np.ulonglong).max
```