Turns out, sometimes a hammer is not the best tool for the job.
OverviewSo in mylast post, I haughtily dismissed SAT as burdensome and opaque formulation for solving puzzles like Flow Free. Since then I’ve learned a few things thanks to @asmeurer , whose thought-provokingcomments pointed me towards some interesting sources on the topic.
To succinctly summarize the previous post, I wrote an automated solver for the popular Flow Free mobile puzzle app in C, based on tree search. Although you can naturally frame Flow Free as a constraint satisfaction problem (CSP) , I wrote that I preferred tree search instead, because:
I spent years in grad school coding up search-based planners with A*. When you have a hammer, the whole world looks like a nail.The program I presented ended up running pretty quickly, solving all of the puzzles in my test suite in under 1.6 seconds per puzzle, while weighing in in at 1,910 physical lines of C code.
And that would have been the happy ending to the story, had asmeurer’s comments not prodded me to try out the CSP/SAT solution. The new program , written in python, has a worst-case solution time of about 0.4 seconds on my test cases, using 3x less code than the C solver.
BackgroundAs I recently learned, dependency resolution is another problem that can also be posed as either graph search or as SAT.In a nutshell, the objective is to decide what additional packages to install or uninstall when a user wants to run some desired piece of software. The figures below, from this paper , show how dependency/conflict relationships between packages can be represented in both ways:

Indeed in 2013, the Python distribution Anaconda switched to resolving dependencies using SAT, thanks to developer Ilan Schnell. The underlying engine is the C-based PicoSAT solver, made accessible from Python by Schnell’s pycosat module. As a warm-up to solving the dependency problem, he used pycosat to write a Sudoku solver , which was a helpful and instructive example as I began writing my pycosat-based Flow Free solver.
Conjunctive normal formThe thorniest part of reducing a general CSP to a SAT problem is that the constraints must typically be expressed in conjunctive normal form (CNF) . CNF consists of a number of OR-clauses, all of which must be satisfied conjunctively (i.e. they are all AND-ed together). Each OR-clause is a disjunction over one or more literals consisting of single logical variables or their negations.
Converting to CNF can be a bit tricky: the logical statement
(i.e. “ andare either both true or both false”) gets represented as the two clauses
whereas the Boolean exclusive-or (XOR) of the two variables,
, must be represented as
Logical implications like
become the single clause , and negations of AND-clauses such as (that is, “it is not the case that both and are true”) are straightforwardly converted through DeMorgan’s law to.
Reducing Flow Free to SATWith this basic understanding of CNF, we’re ready to begin reducing Flow Free to SAT. By the way, the discussion in this section pretty much follows the recipe for a SAT-based solver outlined in the StackOverflow answer I linked to in my last post, but it fills in a lot more details that I had to figure out on on my own.
One by one, we will see how to encode the following list of constraints into CNF:
Every cell is assigned a single color. The color of every endpoint cell is known and specified. Every endpoint cell has exactly one neighbor which matches its color. The flow through every non-endpoint cell matches exactly one of the six direction types. The neighbors of a cell specified by its direction type must match its color. The neighbors of a cell not specified by its direction type must not match its color.Let’s define some variables and indices to get started. Say that our puzzle has
cells and colors. We will use , , , to refer to to individual cells, so for instance we have cell . Similarly, we will use the indices to refer to individual colors. Finally, we can define six “direction types” that specify which of a non-endpoint cell’s neighbors are connected to it by some flow (we can conveniently illustrate them with Unicode box drawing characters). The six types are:
Direction type Unicode char Description 1 ─ left-right 2 │ top-bottom 3 ┘ top-left 4 └ top-right 5 ┐ bottom-left 6 ┌ bottom-rightWe will use
to index these direction types. With that setup in mind, let’s address the first constraint on our list above.
Every cell is assigned a single color.We will create
Boolean variables to encode the color of each cell: if the SAT solution sets to true, that means that cell has color. These color variables are handled slightly differently in endpoint cells (i.e. the colored dots that are initially filled in before the puzzle is solved) and normal free cells. Let’s consider normal cells first.
Clearly every cell must have some color, so for each non-endpoint cell
, we can write the clause:
Also, for each non-endpoint cell
, we know that no two color variables can be true, so we get the clauses:
Each one of these comes from negating the AND clause
for two different colors and . Note we havepossible combinations of two colors for each cell.
The color of every endpoint cell is known and specified.Since their colors are specified initially, for each endpoint cell
, we know its true color , so we can simply assert the clause . We can also specify that none of the other colors are present, so for all.
Every endpoint cell has exactly one neighbor which matches its color.This is the neighbor through which a flow originates or terminates. Let us assume endpoint cell
has known color and neighbor cells , , , and . Since at least one neighbor of the cell has color, we can write the clause
Furthermore, no two neighbors of the endpoint cell
both share its color, so we have , and all the other two-way combinations of the neighbors (i.e. and , and , and, and so on).
The flow through every non-endpoint cell matches exactly one of the six direction types.Now, for each non-endpoint cell we introduce up to six Boolean “direction type” variables that specify which two neighbors of the cell are connected to it along a solution path. If variable
is true in the SAT solution, then some flow connects cell to the two of its neighbors specified by type from the table above. Note that some cells can not assume all of the direction types for instance, it is impossible for a flow to have the ┘direction type anywhere in the top row or leftmost column of the puzzle (since those cells are missing the required neighbors above or to the left).
Just like the color variables, we can generate clauses for the direction variables to ensure that:
every non-endpoint cell cell has at least one direction type, and no two directions types are specified for any non-endpoint cell.The former gives us one clause per cell; the latter gives up to 15 additional clauses one for each possible pair of direction types present.
The neighbors of a cell specified by its direction type must match its color.It’s time to consider the interaction between direction type variables and color variables. Each direction type picks out two neighbors of a cell; for example, direction type ┐ selects a cell’s left neighbor and its bottom neighbor. So for any cell
and any direction type applied to it, we get two neighbors and that must have the same color as cell . That means, for all colors:
…bearing in mind that the indices
and are jointly determined by the cell and the direction type. Massaging the statement above into CNF gives us four clauses:
The neighbors of a cell not specified by its direction type must not match its color. This is the constraint that prevents path self-contact. For any neighbor
of which isn’t picked out by direction type , we can say. In CNF, this becomes the clause
…That’s it!We just generated quite a few variables and clauses: our puzzle of size
with colors has color variables and about direction type variables, with clauses over just the color variables and an additional clauses for direction-color interactions. You may recall that SAT runtimes are, in the worst case, exponential in the number of variables. Should we be worried about prohibitively slow solution speeds?nope.
Dealing with cyclesOne point that got raised in the StackOverflow answer is that this particular reduction to SAT does not prevent freestanding cycles from arising in the puzzle. Here’s an example on the left below, we have the initial state of a 14x14 puzzle, and on the right, an invalid solution with a cycle (the 2x2 yellow square in the top left):

Let’s double-check whether this obviously bogus solution meets our constraints from before:
Every cell is assigned a single color. The color of every endpoint cell is known and specified. Every endpoint cell has exactly one neighbor which matches its color. The flow through every non-endpoint cell matches exactly one of the six direction types. The neighbors of a cell specified by its direction type must match its color. The neighbors of a cell not specified by its direction type must not match its color.Unfortunately, the cycle really is allowed. Like the StackOverflow commenter, the only way I can think to prevent them in the SAT specification would be to add O(N^2) additional variables to track whether each cell is rooted the starting point of some flow. I tried going down this path, and it really bogged down the PicoSAT solver.
Instead of adding the extra “rootedness” variables, my program works incrementally by detecting cycles in the SAT solver’s output and emitting additional clauses to prevent them. For instance, in response to the bogus solution on the right above, it would add a clause saying “either cell (4, 3) is not ┌ or (4, 4) is not ┐ or (5,4) is not ┘ or (5, 3) is not └ ”. In other words, it is not the case that all four cells have the direction types causing the cycle.
In the case of this particular puzzle, having been prohibited from forming the yellow cycle above, the incremental solver happily discovers one more cycle (left below) before finding the true solution (right):

It turns out that this kind of incremental solution of SAT problems, where the solver is invoked multiple times as variables and clauses are added, is an application area of sufficient interest that it now merits its own category at the yearly SAT competition .
ConclusionsAs the table below shows, although the C solver is faster than the SAT-based Python solver on most puzzles (especially small ones), the worst-case solution time for the Python solver is much better. Looking at the time differences between them, the maximum in favor of the C solver is 0.356 seconds, on jumbo_14x14_01.txt the only puzzle in the test suite that actually necessitates cycle detection and repair. On the other hand, the maximum difference in favor of the Python solver is 1.197 seconds on the slowest puzzle for the C solver, jumbo_14x14_30.txt .
Add to that the fact that the Python program has far less code than the C version, and SAT starts to look really appealing. I’m going to close off this post with another screen grab of a Facebook exchange that I think really captures the learning experience of this week’s exercise:

Don’t get too attached to your hammers, kids!
Appendix: experimental data Puzzle # SAT vars # clauses Python time C time Difference regular_5x5_01.txt 184 2,031 0.003 0.001 0.002 regular_6x6_01.txt 322 4,353 0.004 0.001 0.003 regular_7x7_01.txt 452 6,418 0.007 0.001 0.006