# 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 ```