Constraint satisfaction solver
I wrote a constraint satisfaction solver from scratch in Java and Spring Boot, and its min-conflicts search places 1,000 queens in about 15 ms.
What it is
I built a constraint satisfaction problem (CSP) solver in Java 21, and I exposed it as a Spring Boot REST service. A CSP has variables, a set of allowed values for each one, and constraints between them. I wrote every algorithm myself, without a CSP library.
The service solves N-Queens, Sudoku, graph coloring, and number puzzles such as SEND + MORE = MONEY. A browser page shows the search live over a WebSocket.
Backtracking is complete, so it can prove that no solution exists. A min-conflicts local search gives up that guarantee, but it places 1,000 queens in about 15 ms.
How it works
The backtracking search assigns one variable at a time, and it undoes any choice that leads to a dead end. Several techniques make it faster.
- Forward checking runs after each assignment. It removes conflicting values from neighboring variables and stops early when one runs out of values.
- AC-3 runs once before the search. It makes every constraint arc consistent, which means each remaining value has a compatible value in each neighbor.
- MRV (minimum remaining values), the default ordering, picks the variable with the fewest values left, and it breaks ties by the number of constraints.
- Dom/WDeg divides the number of values left by a weighted count of constraints. A constraint gets more weight each time it empties a variable's values.
- A
DomainTrailrecords changes, so a dead end rolls back only theBitSetdomains that changed.
For N-Queens with N of 50 or more, the API switches to min-conflicts. It starts from a greedy placement, and it then moves conflicted queens to their least conflicted column until no conflicts remain.
The live view streams search events over STOMP, a messaging protocol on top of WebSocket. Board updates are capped at about 30 per second.
Decisions
- I replaced full domain copies with a trail. The first version copied every domain at every search node, which allocated memory in proportion to the problem size at each node. My design notes report that the trail made each node about twice as cheap.
- Forward checking is the propagation used during search, and AC-3 runs only before it. Running arc consistency at every node (MAC) removes more values, but it reruns AC-3 each time. My notes record forward checking with good variable ordering as the better default.
- The API picks a strategy by problem size. Backtracking stays the default for small boards, because it can list every solution. Min-conflicts handles large N-Queens, where backtracking slows down badly.
What was hard
Three bugs failed silently, and the tests didn't catch them at first.
- The value selectors returned the live domain. The solver pruned that domain while it explored the first value, so the remaining choices could change under the loop and the search became incomplete. The selectors now loop over a snapshot.
- Under MAC, propagation doesn't enforce a constraint that exposes no arcs, i.e., pairs of variables that AC-3 can check. The sum in a number puzzle is such a constraint. The solver now checks every constraint on each complete assignment.
- Dom/WDeg stored a weight of 1.0 on the first failure, which is the same as the default weight. The first failure now raises the weight to 2.0.
The 200-Queens test also ran about twice as slow in the full test suite as on its own. The calls in ForwardChecker meet many constraint classes in the suite, so the Java JIT compiler stops inlining them. The node count is fixed at 639,950, so I raised the test's time limit to 30 s.
Results
The README reports these backtracking times as the best of several runs.
- A 100-Queens board takes about 15 ms and 185 search nodes.
- A 200-Queens board takes about 8 s and about 640,000 nodes.
- A hard Sudoku takes about 65 ms and about 8,700 nodes.
On the same search tree, the trail cut 100-Queens from 50 ms to 22 ms, and 200-Queens from 16.4 s to 8.1 s. The min-conflicts search places 1,000 queens in about 15 ms. The project has 61 tests, and one of them confirms that SEND + MORE = MONEY has exactly one solution, 9567 + 1085 = 10652.
What I would do next
My design notes list two known limits.
- MAC exists in the code, but the API can't select it. Exposing it would let me measure what AC-3 costs during search.
- Backtracking on N-Queens is sensitive to board size with the natural value order, and it stays fast up to about N = 150.