Lights out solver or story about how to solve N^2 equations in O(N^3) time
Did you ever need to wake up at 5 am to go to the morning lecture where you are fighting for your life to stay awake and learn anything? Well, I know I did and it was a time when I kept myself focused by playing some logic games in the background. You know logic games like Minesweeper, simple games that once you learn you can solve absentmindedly. One game I kept thinking about over and over is Flip(which is an implementation of “Lights Out”). Computers solve this game by just solving a system of linear equations, but I wanted to find a method that 5 am me could absentmindedly use. Well, I didn’t find it but I found a way to solve that specific system of equations with much fewer operations. And that different way of solving it is the topic of this blog post.
What is lights out?
Lights Out is a very simple puzzle game consisting of a \(M{\times}N\) grid of lights that are either toggled on or off. We can click a light to make it and adjacent lights (not counting diagonal neighbors) change their state, so either from on to off or from off to on. Our goal is to switch all lights off.
Below are a few examples to get basic intuition:
We can see that lights (2,3), (3,2), (3,3), (3,4), and (4,3) changed states.
We can see that only lights (1,1), (1,2), and (2,1) changed states as in the corner we don’t have lights (0,1) nor (1,0).
Light (3,3) didn’t change its state as it was affected by both clicks (3,2) and (3,4), making it change state twice, which is equivalent to not changing its state.
If you want to play a bit with that puzzle game yourself I recommend playing it on Simon Tatham’s Portable Puzzle Collection.
We already can get some basic intuitions on how solutions for this game might be structured from the below properties:
- Clicking on the same light twice is the same as not clicking on it.
- The order of clicking on lights has no difference in the result as they affect lights independently.
Therefore, it is easy to reason that our solution will be in the form of a \(M{\times}N\) boolean matrix that represents if we should click on a specific light. With \(M{\times}N\) it would give \(2^{MN}\) possible combinations of clicks to try, but as I already spoiled in the introduction, we can use linear algebra to find the correct combination.
Basic solving via Gauss–Jordan elimination
Turns out the effects clicks have on tiles can be shown as a matrix multiplication in modulo 2. Let’s show it on \(3{\times}3\) grid.
Our variables will be also in \(3{\times}3\) matrix of boolean variables showing if we should click on them or not
\[\begin{equation} \left[ \begin{array}{ccc} \mathtt{x{{_1}}ˏ{_1}} & \mathtt{x{{_1}}ˏ{_2}} & \mathtt{x{{_1}}ˏ{_3}} \\ \mathtt{x{{_2}}ˏ{_1}} & \mathtt{x{{_2}}ˏ{_2}} & \mathtt{x{{_2}}ˏ{_3}} \\ \mathtt{x{{_3}}ˏ{_1}} & \mathtt{x{{_3}}ˏ{_2}} & \mathtt{x{{_3}}ˏ{_3}} \\ \end{array} \right] \end{equation}\]
or in flattened form they would be:
\(x = \left[ \begin{array}{c} \mathtt{x_{1}ˏ_1} \\ \mathtt{x_{2}ˏ_1} \\ \mathtt{x_{3}ˏ_1} \\ \mathtt{x_{1}ˏ_2} \\ \mathtt{x_{2}ˏ_2} \\ \mathtt{x_{3}ˏ_2} \\ \mathtt{x_{1}ˏ_3} \\ \mathtt{x_{2}ˏ_3} \\ \mathtt{x_{3}ˏ_3} \\ \end{array} \right]\)
We can represent states of lights also in matrix form
\(\left[ \begin{array}{ccc} \mathtt{b_{1}ˏ_1} & \mathtt{b_{1}ˏ_2} & \mathtt{b_{1}ˏ_3} \\ \mathtt{b_{2}ˏ_1} & \mathtt{b_{2}ˏ_2} & \mathtt{b_{2}ˏ_3} \\ \mathtt{b_{3}ˏ_1} & \mathtt{b_{3}ˏ_2} & \mathtt{b_{3}ˏ_3} \\ \end{array} \right] = \left[ \begin{array}{ccc} 1 & 0 & 1 \\ 0 & 0 & 1 \\ 1 & 1 & 0 \\ \end{array} \right]\)
or again in flattened form
\(b = \left[ \begin{array}{c} 1 \\ 0 \\ 1 \\ 0 \\ 0 \\ 1 \\ 1 \\ 1 \\ 0 \\ \end{array} \right]\)
The effect that clicks have on lights can be represented by the below matrix
\(M = \left[ \begin{array}{ccccccccc} 1 & 1 & 0 & 1 & 0 & 0 & 0 & 0 & 0 \\ 1 & 1 & 1 & 0 & 1 & 0 & 0 & 0 & 0 \\ 0 & 1 & 1 & 0 & 0 & 1 & 0 & 0 & 0 \\ 1 & 0 & 0 & 1 & 1 & 0 & 1 & 0 & 0 \\ 0 & 1 & 0 & 1 & 1 & 1 & 0 & 1 & 0 \\ 0 & 0 & 1 & 0 & 1 & 1 & 0 & 0 & 1 \\ 0 & 0 & 0 & 1 & 0 & 0 & 1 & 1 & 0 \\ 0 & 0 & 0 & 0 & 1 & 0 & 1 & 1 & 1 \\ 0 & 0 & 0 & 0 & 0 & 1 & 0 & 1 & 1 \\ \end{array} \right]\)
We know that our solution needs to satisfy below system of equations in modulo 2.
\(Mx = b\)
In this example it would be:
\(\left[ \begin{array}{ccccccccc} 1 & 1 & 0 & 1 & 0 & 0 & 0 & 0 & 0 \\ 1 & 1 & 1 & 0 & 1 & 0 & 0 & 0 & 0 \\ 0 & 1 & 1 & 0 & 0 & 1 & 0 & 0 & 0 \\ 1 & 0 & 0 & 1 & 1 & 0 & 1 & 0 & 0 \\ 0 & 1 & 0 & 1 & 1 & 1 & 0 & 1 & 0 \\ 0 & 0 & 1 & 0 & 1 & 1 & 0 & 0 & 1 \\ 0 & 0 & 0 & 1 & 0 & 0 & 1 & 1 & 0 \\ 0 & 0 & 0 & 0 & 1 & 0 & 1 & 1 & 1 \\ 0 & 0 & 0 & 0 & 0 & 1 & 0 & 1 & 1 \\ \end{array} \right] \cdot \left[ \begin{array}{c} \mathtt{x_{1}ˏ_1} \\ \mathtt{x_{2}ˏ_1} \\ \mathtt{x_{3}ˏ_1} \\ \mathtt{x_{1}ˏ_2} \\ \mathtt{x_{2}ˏ_2} \\ \mathtt{x_{3}ˏ_2} \\ \mathtt{x_{1}ˏ_3} \\ \mathtt{x_{2}ˏ_3} \\ \mathtt{x_{3}ˏ_3} \\ \end{array} \right] = \left[ \begin{array}{c} 1 \\ 0 \\ 1 \\ 0 \\ 0 \\ 1 \\ 1 \\ 1 \\ 0 \\ \end{array} \right]\)
\[\begin{align} \mathtt{x_{1}ˏ_1} + \mathtt{x_{1}ˏ_2} + \mathtt{x_{2}ˏ_1} &= 1 \\ \mathtt{x_{1}ˏ_1} + \mathtt{x_{2}ˏ_1} + \mathtt{x_{2}ˏ_2} + \mathtt{x_{3}ˏ_1} &= 0 \\ \mathtt{x_{2}ˏ_1} + \mathtt{x_{3}ˏ_1} + \mathtt{x_{3}ˏ_2} &= 1 \\ \mathtt{x_{1}ˏ_1} + \mathtt{x_{1}ˏ_2} + \mathtt{x_{1}ˏ_3} + \mathtt{x_{2}ˏ_2} &= 0 \\ \mathtt{x_{1}ˏ_2} + \mathtt{x_{2}ˏ_1} + \mathtt{x_{2}ˏ_2} + \mathtt{x_{2}ˏ_3} + \mathtt{x_{3}ˏ_2} &= 0 \\ \mathtt{x_{2}ˏ_2} + \mathtt{x_{3}ˏ_1} + \mathtt{x_{3}ˏ_2} + \mathtt{x_{3}ˏ_3} &= 1 \\ \mathtt{x_{1}ˏ_2} + \mathtt{x_{1}ˏ_3} + \mathtt{x_{2}ˏ_3} &= 1 \\ \mathtt{x_{1}ˏ_3} + \mathtt{x_{2}ˏ_2} + \mathtt{x_{2}ˏ_3} + \mathtt{x_{3}ˏ_3} &= 1 \\ \mathtt{x_{2}ˏ_3} + \mathtt{x_{3}ˏ_2} + \mathtt{x_{3}ˏ_3} &= 0 \end{align}\]
So now solving our game has been simplified to solving a system of linear equations in modulo 2.
For systems in modulo 2 we can use Gauss-Jordan elimination as it uses just elementary row operations.
Here is an example implementation in Julia
function gauss_jordan!(M,b)
=1
pivotfor col ∈ axes(M,2)
=findfirst(M[pivot:(size(M)[1]),col]) # look for pivot row
next_pivotisnothing(next_pivot) && continue # if there is no pivot row continue
+=pivot-1 # get index of pivot row as julia arrays start at 1
next_pivot
# swap pivot
:], M[next_pivot, :] = M[next_pivot, :], M[pivot, :]
M[pivot, = b[next_pivot], b[pivot]
b[pivot], b[next_pivot]
for row ∈ axes(M,1)
if row ≠ pivot && M[row,col]
# xor all rows that are not pivot and that have 1 in pivot column
:] .⊻= M[pivot, :]
M[row,:] .⊻= b[pivot, :]
b[row,end
end
+=1
pivotend
return (M, b)
end
It is a simple implementation of Gauss-Jordan elimination in modulo 2. It is simpler than its counterpart for normal linear systems as instead of addition and subtraction we use xoring which in Julia is ⊻ operator. We also don’t need to worry about multiplication as values in the matrix are boolean.
It is also worth noting that this simple implementation has a time complexity of \(O(N^3)\) where N is the dimension of our \(N{\times}N\) Matrix.
Let’s see how well it solves a random \(3\times3\) grid. Also because values in the M matrix and b vector are boolean I can showcase equations as plots. Below is our system of equations after Gauss-Jordan elimination:
Nice we got an identity matrix which means there is a singular solution. Let’s see how it deals with a random \(5\times5\) grid.
We see that we didn’t get the identity matrix and that rank of our matrix is only 23. Usually, it would mean that we have infinitely many solutions but because we are in modulo 2 and our variables can only have 2 values (true, false) it means we have 4 solutions that depend on values for the last 2 variables. In this blog post, we will not tackle the problem of finding a solution with the least amount of clicks and just focus on finding any solution.
Now having Gauss-Jordan elimination we can write our solver:
function naive_solver!(lights)
=size(lights)
dims=create_M_matrix(dims)
M=vec(lights) # flatten b vector
bgauss_jordan!(M,b)
return reshape(find_solution(M,b),dims) # reshape flattened solution into the proper dimensions
end
Code is extremely simple as we are basically just creating our M matrix, flatting our lights and just applying Gauss-Jordan elimination and finding a solution. It is worth noting that my “find_solution” function if there are many solutions, chooses one where free variables are 0.
This solver has a time complexity of \(O((NM)^3)\) as the most expensive part of this solver is Gauss-Jordan elimination that works on \(NM{\times}NM\) matrix.
If you would look for YouTube videos or other blog posts about Lights Out this Gauss-Jordan elimination would be an algorithm used in a solver.
But we can do better turns out we can “cheat” this specific system of equations to solve it much faster and it starts with a method called “Lights Chasing”.
Lights Chasing
Lights Chasing is a strategy of clearing lights row by row. It starts by clicking under all turned-on lights in row 1. This makes the entire first row dark. Then it clicks under all turned-on lights in row 2 and this operation repeats until only turned-on lights left are on the bottom row.
Let’s show an example.
In the first row, we have lights (1,1) and (1,3) turned up so we click on (2,1) and (2,3)
Now on the second row, only (2,1) light is on so we need to click on (3,1)
Now we are left with lights where only the bottom row has lights on
Now after Lights Chasing way to solve it is to use a lookup table for the bottom rows of lights that shows what lights to click on the top row. After those clicks from the lookup table, we should apply the Lights Chasing algorithm again and our grid should be solved. For this specific bottom row we need to click on lights (1,1) and (1,3).
Now we need to do the Lights Chasing algorithm again
As we can see we solved \(3{\times}3\) puzzle by using the Lights Chasing algorithm twice and by knowing specific lights to turn on in the top row. It is also worth noting that the Lights Chasing algorithm has time complexity \(O(NM)\) as you need to linearly go row by row and apply clicks.
At first, it doesn’t seem to help us much as we would need a lookup table, and constructing it on the fly seems too computationally expensive. After all on the top row, we have \(2^M\) combinations that we would need to try, each time doing the Lights Chasing algorithm. But luckily for us Lights Chasing has useful mathematical properties that we can exploit to create a lookup table efficiently by solving part of our system of equations.
Mathematical properties of Lights Chasing
Lights Chasing algorithm can also be expressed as matrix multiplication. Here I will show it for \(3\times3\) grid.
To clear the first row we need to click on all lights below the lights that are turned on on the first row. Therefore first we shift all lights in the first row to the row below which can be described by the matrix below:
\(S_{1} = \left[ \begin{array}{ccccccccc} 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ \end{array} \right]\)
Then we need to multiply the shift matrix by M matrix to make elements in the first row “click” lights below it:
\(MS_{1} = \left[ \begin{array}{ccccccccc} 1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 1 & 0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 \\ 1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 \\ 1 & 0 & 0 & 1 & 0 & 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 1 & 0 & 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 1 & 0 & 0 \\ \end{array} \right]\)
Now we need to add an identity matrix to keep all the light’s states to get the matrix for clearing the first row:
\(C_{1} = I + MS_{1} = \left[ \begin{array}{ccccccccc} 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 1 & 1 & 0 & 1 & 0 & 0 & 0 & 0 & 0 \\ 1 & 0 & 1 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 1 & 0 & 0 & 1 & 1 & 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 1 & 0 & 1 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 1 & 0 & 0 & 1 & 1 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 1 & 0 & 1 \\ \end{array} \right]\)
And we got \(C_1\) matrix that does the first row part of Lights Chasing. We can create a similar matrix \(C_2\) for the second row. Now to get the matrix for the full algorithm we need to multiply our \(C_1\) by \(C_2\) to get the full algorithm for \(3\times3\) grid.
\(C = C_{2} \cdot C_{1} = \left[ \begin{array}{ccccccccc} 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 1 & 1 & 1 & 0 & 1 & 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 & 1 & 1 & 0 & 1 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 1 & 0 & 0 & 0 & 1 & 0 & 1 & 1 & 1 \\ \end{array} \right]\)
Multiplication by this matrix is equivalent to the Lights Chasing algorithm. We can also find by matrix multiplication what clicks will our Lights Chasing algorithm do:
\(S = S_{2} \cdot C_{1} + S_{1} = \left[ \begin{array}{ccccccccc} 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 1 & 1 & 0 & 1 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 \\ 1 & 0 & 0 & 1 & 1 & 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 1 & 0 & 0 & 1 & 1 & 0 \\ \end{array} \right]\)
With relation between \(C\) and \(S\) matrixes being:
\(C = I + MS\)
This construction of Lights Chasing matrix can be done for grids of any size.
Because Lights Chasing can be described by matrix multiplication then it is distributive with respect to matrix addition. This property is enough to create a fast algorithm for generating a lookup table for our fast solver but before that why does using a lookup table even work? To find out let’s see how our system of equations changes when we apply Lights Chasing.
\(CMx = Cb\)
\(\left[ \begin{array}{ccccccccc} 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 1 & 0 & 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 1 & 0 & 0 & 1 & 0 & 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 1 & 0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 \\ \end{array} \right] \cdot \left[ \begin{array}{c} \mathtt{x_{1}ˏ_1} \\ \mathtt{x_{2}ˏ_1} \\ \mathtt{x_{3}ˏ_1} \\ \mathtt{x_{1}ˏ_2} \\ \mathtt{x_{2}ˏ_2} \\ \mathtt{x_{3}ˏ_2} \\ \mathtt{x_{1}ˏ_3} \\ \mathtt{x_{2}ˏ_3} \\ \mathtt{x_{3}ˏ_3} \\ \end{array} \right] = \left[ \begin{array}{ccccccccc} 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 1 & 1 & 1 & 0 & 1 & 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 & 1 & 1 & 0 & 1 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 1 & 0 & 0 & 0 & 1 & 0 & 1 & 1 & 1 \\ \end{array} \right] \cdot \left[ \begin{array}{c} \mathtt{b_{1}ˏ_1} \\ \mathtt{b_{2}ˏ_1} \\ \mathtt{b_{3}ˏ_1} \\ \mathtt{b_{1}ˏ_2} \\ \mathtt{b_{2}ˏ_2} \\ \mathtt{b_{3}ˏ_2} \\ \mathtt{b_{1}ˏ_3} \\ \mathtt{b_{2}ˏ_3} \\ \mathtt{b_{3}ˏ_3} \\ \end{array} \right]\)
\[\begin{align} 0 &= 0 \\ 0 &= 0 \\ \mathtt{x_{1}ˏ_2} + \mathtt{x_{1}ˏ_3} &= \mathtt{b_{1}ˏ_1} + \mathtt{b_{1}ˏ_3} + \mathtt{b_{2}ˏ_1} + \mathtt{b_{2}ˏ_2} + \mathtt{b_{3}ˏ_1} \\ 0 &= 0 \\ 0 &= 0 \\ \mathtt{x_{1}ˏ_1} + \mathtt{x_{1}ˏ_2} + \mathtt{x_{1}ˏ_3} &= \mathtt{b_{2}ˏ_1} + \mathtt{b_{2}ˏ_2} + \mathtt{b_{2}ˏ_3} + \mathtt{b_{3}ˏ_2} \\ 0 &= 0 \\ 0 &= 0 \\ \mathtt{x_{1}ˏ_1} + \mathtt{x_{1}ˏ_2} &= \mathtt{b_{1}ˏ_1} + \mathtt{b_{1}ˏ_3} + \mathtt{b_{2}ˏ_2} + \mathtt{b_{2}ˏ_3} + \mathtt{b_{3}ˏ_3} \end{align}\]
or more simply:
\[\begin{align} \mathtt{x_{1}ˏ_2} + \mathtt{x_{1}ˏ_3} &= \mathtt{b_{1}ˏ_1} + \mathtt{b_{1}ˏ_3} + \mathtt{b_{2}ˏ_1} + \mathtt{b_{2}ˏ_2} + \mathtt{b_{3}ˏ_1} \\ \mathtt{x_{1}ˏ_1} + \mathtt{x_{1}ˏ_2} + \mathtt{x_{1}ˏ_3} &= \mathtt{b_{2}ˏ_1} + \mathtt{b_{2}ˏ_2} + \mathtt{b_{2}ˏ_3} + \mathtt{b_{3}ˏ_2} \\ \mathtt{x_{1}ˏ_1} + \mathtt{x_{1}ˏ_2} &= \mathtt{b_{1}ˏ_1} + \mathtt{b_{1}ˏ_3} + \mathtt{b_{2}ˏ_2} + \mathtt{b_{2}ˏ_3} + \mathtt{b_{3}ˏ_3} \end{align}\]
And we got interesting results. Turns out that after Lights Chasing algorithm we only are left with 3 equations that correspond to lights on the bottom row. We also see that those equations only use variables corresponding to lights on the top row. This isn’t an isolated property of \(3{\times}3\) grid you can find the same simplification emerging for grids of any size. To prove it we require just 1 lemma which I will show below with a small intuitive explanation. Showing full formal proof would be out of the scope of this specific blog post but with the intuition below creating such proof shouldn’t be a problem.
Lemma 1 If there exists a solution to Lights Out game that has no clicks in the top row then the Lights Chasing algorithm solves Lights Out game.
If we wanted to write this lemma in vector form for our \(3\times3\) grid we would have
if \(x = \left[ \begin{array}{c} 0 \\ \mathtt{x_{2}ˏ_1} \\ \mathtt{x_{3}ˏ_1} \\ 0 \\ \mathtt{x_{2}ˏ_2} \\ \mathtt{x_{3}ˏ_2} \\ 0 \\ \mathtt{x_{2}ˏ_3} \\ \mathtt{x_{3}ˏ_3} \\ \end{array} \right]\) and \(Mx = b\) then \(x = Sb\)
To see why this lemma works let’s analyze the below example.
The left plot shows what clicks solve the puzzle on the right plot. We see that like in our lemma assumption, there are no clicks on the top row. We also see that in the top row lights (1,2) and (1,4) are on. And because there are no clicks in the top row the only way that lights (1,2) and (1,4) are on is if the lights below them got clicked. So now let’s apply Lights Chasing on the top row and click on (2,2) and (2,4)
Now both rows 1 and 2 have no clicks. And we can see that lights (2,1) and (2,5) are on. And with the same logic as before the only way that lights (2,1) and (2,5) are on is if lights below got clicked. So let’s do another row of Lights Chasing.
The same reasoning as before still applies. Rows 1,2,3 and 4 have no clicks and the only way light (4,3) is on is if light (5,3) is clicked.
And like that, we solved this particular puzzle by using Lights Chasing. I hope it is easy to see how this reasoning can be extended to grids of any size.
With an understanding of this lemma, we can rewrite our system of equations by decomposing x vector into clicks on the top row and clicks everywhere else.
Let
\(x_{t} = \left[ \begin{array}{c} \mathtt{x_{1}ˏ_1} \\ 0 \\ 0 \\ \mathtt{x_{1}ˏ_2} \\ 0 \\ 0 \\ \mathtt{x_{1}ˏ_3} \\ 0 \\ 0 \\ \end{array} \right]\), \(x_{b} = \left[ \begin{array}{c} 0 \\ \mathtt{x_{2}ˏ_1} \\ \mathtt{x_{3}ˏ_1} \\ 0 \\ \mathtt{x_{2}ˏ_2} \\ \mathtt{x_{3}ˏ_2} \\ 0 \\ \mathtt{x_{2}ˏ_3} \\ \mathtt{x_{3}ˏ_3} \\ \end{array} \right]\)
\(x = x_{t} + x_{b}\)
Let’s plug this decomposition into the original equations and apply Lights Chasing to it:
\(CMx = Cb\)
\(\mathrm{CM}\left( x_{t} + x_{b} \right) = Cb\)
\(CMx_{t} + CMx_{b} = Cb\)
We know from our lemma that if there are no clicks in top row then Lights Chasing solves. Therefore \(CMx_b=0\)
\(CMx_{t} = Cb\)
Therefore we can solve for \(x_t\)
\[\begin{align} \mathtt{x_{1}ˏ_2} + \mathtt{x_{1}ˏ_3} &= \mathtt{b_{1}ˏ_1} + \mathtt{b_{1}ˏ_3} + \mathtt{b_{2}ˏ_1} + \mathtt{b_{2}ˏ_2} + \mathtt{b_{3}ˏ_1} \\ \mathtt{x_{1}ˏ_1} + \mathtt{x_{1}ˏ_2} + \mathtt{x_{1}ˏ_3} &= \mathtt{b_{2}ˏ_1} + \mathtt{b_{2}ˏ_2} + \mathtt{b_{2}ˏ_3} + \mathtt{b_{3}ˏ_2} \\ \mathtt{x_{1}ˏ_1} + \mathtt{x_{1}ˏ_2} &= \mathtt{b_{1}ˏ_1} + \mathtt{b_{1}ˏ_3} + \mathtt{b_{2}ˏ_2} + \mathtt{b_{2}ˏ_3} + \mathtt{b_{3}ˏ_3} \end{align}\]
After solving for \(x_t\). We can apply those clicks to our grid. We can do that by adding \(Mx_t\) to both sides of our equation,
\(Mx = b\)
\(Mx + M \cdot x_{t} = b + M \cdot x_{t}\)
\(M\left( x + x_{t} \right) = b + M \cdot x_{t}\)
because of modulo 2 we know that: \(x+x_t=x_b\)
\(Mx_{b} = b + M \cdot x_{t}\)
Notice it is the original game equation without clicks in the top row in the solution. That means we can apply our lemma to it, getting the value of \(x_b\) without solving any additional equations:
\(x_{b} = S\left( b + Mx_{t} \right)\)
So from our system of \(N{\times}M\) equations we get system with \(M\) equations:
\(CMx_{t} = Cb\)
\(x_{b} = S\left( b + Mx_{t} \right)\)
And because our \(x_t\) has only \(M\) non-zero variables our system of equations got reduced from solving for \(NM\) variables to solving for only \(M\) variables. Normally it wouldn’t really be faster than the Gauss-Jordan solver because of all the required matrix multiplications and operations needed to create \(C\) and \(S\) matrixes but we can do those operations more efficiently and mostly skip those costly operations.
New and improved solver
The algorithm for our solver is a bit more complicated than simple Gauss-Jordan elimination:
- For each top light that can be clicked, find out what lights are left at the bottom row after Lights Chasing algorithm.
- Use Lights Chasing on our puzzle.
- Solve a new system of equations using Gauss-Jordan where we find what buttons should be pressed on the top row to clear the bottom row after Lights Chasing.
- Apply clicks from step 3 and use Lights Chasing again.
- Xor all of our clicks we have done in steps 2 and 4 to delete redundant clicks( like clicking the same light twice).
function lights_chasing_solver!(lights)
=size(lights)
dims=Matrix{Bool}(undef, dims[2], dims[2])
equations
for col in axes(lights,2)
=zeros(Bool, dims) # create empty grid
equation_lightssingle_click!(equation_lights,(1,col)) # click on top row
chase_lights!(equation_lights)
:,col]=equation_lights[end,:] # add bottom row lights to our matrix
equations[end
=chase_lights!(lights) # do Lights Chasing in place
clicks=deepcopy(lights[end, :])
bottom_row_lightsgauss_jordan!(equations, bottom_row_lights)
=find_solution(equations, bottom_row_lights)
top_row_clicks
1,:].⊻=top_row_clicks # save top row clicks
clicks[
for (index,click) in enumerate(top_row_clicks)
&& single_click!(lights, (1,index)) # apply top row clicks
click end
.⊻= chase_lights!(lights)
clicks return clicks
end
So what is the time complexity of this algorithm? Well first I will reiterate that this algorithm can be used both row by row and column by column here it is implemented row by row for simplicity’s sake. We create equations that require us to use \(min(N,M)\) times Light Chasing giving a complexity of \(O(min(N,M)NM)\) after we do Light Chasing and we solve system of equations that have \(min(N,M)\) variables giving as complexity of \(O(min(N,M)^3)\) after that we apply \(min(N,M)\) clicks and do Lights Chasing algorithm again. For most grids, the time complexity of our new solver is \(O(min(N,M)^3)\).
But is it really faster?
Theoretical time complexities are all well and good but is our new solver faster? Well, to test it let’s measure the time it takes for our solver to solve random \(N{\times}N\) grids. We will plot times for both algorithms on log-scaled plots.
We can see that our Lights Chasing solver already for \(N\approx10\) is faster than the naive solver. We can also clearly see differences in slope. But does time complexity match our theoretical ones? For that, we need to calculate the slope of those points on a log-scaled plot.
For Lights Chasing solver:
───────────────────────────────────────────────────────────────────────
Coef. Std. Error t Pr(>|t|) Lower 95% Upper 95%
───────────────────────────────────────────────────────────────────────
(Intercept) 9.88391 0.195427 50.58 <1e-10 9.43325 10.3346
log2(N) 2.72422 0.0435225 62.59 <1e-11 2.62385 2.82458
───────────────────────────────────────────────────────────────────────
For basic solver:
───────────────────────────────────────────────────────────────────────
Coef. Std. Error t Pr(>|t|) Lower 95% Upper 95%
───────────────────────────────────────────────────────────────────────
(Intercept) 2.31128 1.49913 1.54 0.1838 -1.54235 6.1649
log2(N) 5.39454 0.41104 13.12 <1e-04 4.33793 6.45115
───────────────────────────────────────────────────────────────────────
We need to look at the coefficient for the log2(N) variable to find the exponent of our N in time complexity. We expect the slope for the naive solver to be 6 and the slope for our Lights Chasing solver to be 3. We can see that the coefficients are statistically different from each other but they are not exactly equal to our theoretical ones. Those theoretical time complexities are of course theoretical and are important for \(N\to\infty\) but for small \(N\) they can be inaccurate because of constant factors.
So we did it. We solved Lights Out more efficiently. At the same time, we solved the linear system of equations in modulo 2 by solving fever number of equations. And turns out this toy example of reducing the number of equations to solve can be generalized
Generalization
When I discovered this method I wondered if it could be expanded for more systems of equations in modulo 2. Maybe other version of Lights Out where lights have more than 2 states? Well, I didn’t need to wonder as it was already done in publication Solving systems of linear equations through zero forcing set and application to lights out. It not only generalizes this method for modulo 2 systems but for any field. It says in the paper that they were motivated by the work of Wang who found an algorithm for solving Lights Out for square grids in \(O(N^3)\) time. After reading google translate of Wang’s work is clear he had the same type of algorithm as presented here.