Solving the “Miracle Sudoku” in Prolog
benjamincongdon.me
benjamincongdon.me
A CLP(FD/ℤ) solution such as this one has two parts: First, the relevant constraints are posted. Second, a search tries to find concrete solutions. In general, a search is necessary because the constraints by themselves are not sufficient to deduce the unique solution as that would be computationally prohibitive.
You can therefore influence the speed of the logic program in two categorically distinct ways: First, you can post different constraints or try a different formulation altogether. Second, you can try different search heuristics that try to find concrete solutions more efficiently.
Regarding constraint propagation, there is a clear trade-off between strength and efficiency of propagation. For instance, the all_distinct/1 constraint that is used in the sample solution propagates very strongly, and is therefore much slower than the weaker all_different/1 constraint, even though they are completely equivalent from a semantic perspective: Both are true if and only if the given integers are pairwise distinct. It depends on the problem at hand whether the stronger propagation pays off, or whether the weaker propagation is in fact sufficient to quickly find solutions.
Especially for very simple tasks such as Sudoku, quick and weak propagation may well outperform strong and slow propagation.
In this concrete example, I easily achieved a 20-fold speedup by replacing the all_distinct/1 constraint with all_different/1, and using the ff labeling strategy to search for concrete solutions. ff means first-fail, and is often a very good strategy. To get this, I simply textually replaced all occurrences of all_distinct with all_different in the source code, and posted:
?- problem(1, Rows),
sudoku(Rows),
append(Rows, Vs),
labeling([ff], Vs),
maplist(portray_clause, Rows).
Also, in less than 5 minutes, I just produced all 88 solutions for problem 3 which is shown at the end of the article. This is only due to using less powerful propagation, and a better search heuristic.And this is one of the key attractions of constraint logic programming (CLP): You can relatively easily try different search strategies while keeping the model the same.
In Hakan's very nice Picat solution, you see that a specific search heuristic is used, specified as:
solve([ffd,down], Vars).
In addition, Hakan's program uses all_different/1. For a fair comparison between the solutions, constraints with the same propagation strength, and the same search heuristic must be used in both approaches.> You can relatively easily try different search strategies while keeping the model the same.
[1] https://github.com/papers-we-love/papers-we-love/blob/master...
I suspected something like the `ff` labeling approach would improve the solution, but I had no idea how much of a difference `all_different` made vs. `all_distinct`!
Even the documentation for all_different/1 https://www.swi-prolog.org/pldoc/man?predicate=all_different... asks the user to consider using all_distinct instead. I'm afraid I just don't have a mental model of how CLP(FD) works to understand what is meant to "stronger" or "weaker" propagation.
As an example, consider the variables
x=1, y in {2, 3}, z in {2, 3}, w in {1, 2, 3, 4}
with the constraint all_different(x, y, z, w) added (note here that I will use that names for the constraint, and refer to the propagation strength of a propagator).With value-propagation (sometimes called forward-checking), a propagator would deduce the new domains
x=1, y in {2, 3}, z in {2, 3}, w in {2, 3, 4}
by simply removing assigned values from other variables domains. However, there are stronger propagators for the constraint. In particular, there is well-known and reasonably efficient (O(n^2.5)) propagator that is domain-consistent (also known as GAC or generalized arc consistent). That means that it can always remove _all_ values from variables that are in no solution. The above example with that propagator would deduce x=1, y in {2, 3}, z in {2, 3}, w=4
The reasoning is based on something called Hall-sets. In the above example, y and z form a Hall set of size 2, since they must take the values 2 and 3 in any solution. there are also other propagators with different propagation strengths. For all_different, there are also bounds-consistent propagators, that can do more than value propagation, but only on does advanced reasoning based on the bounds of variables.I think that SWI Prolog uses the all_different name for value propagation, and all_distinct for domain propagation. In the CP system Gecode that I'm most familiar with, we use an argument to the distinct-constraint (same constraint, different name) to indicate the desired level of propagation (https://www.gecode.org/doc-latest/reference/group__TaskModel...).
Thanks for those videos!
[0] https://www.youtube.com/channel/UCFFeNyzCEQDS4KCecugmotg/fea...
If we remove rotations and reflections, there may be (far) fewer unique Sudoku puzzles than we think. In fact, there are many sub-reflections that are essentially the 'same' puzzle:
Take the first three rows as a block, 'rotate' to the bottom of the puzzle - its still 'solved'. In fact permute the blocks of three rows in any order, still the 'same' puzzle in a sense.
Same with the first three columns.
Same with the three rows or columns in a block of three - permute them, the solution is 'unchanged'.
Relabel the digits from 1-9 as 9-1 - another 'reflection' which is the same puzzle pattern.
In fact, permute the digits all different ways - its still solved.
I'm not at all sure there isn't actually just one unique pattern to sudoku. But I don't know how to prove that one way or another.
[1] http://www.afjarvis.staff.shef.ac.uk/sudoku/sudgroup.html [2] (PDF) http://www.afjarvis.staff.shef.ac.uk/sudoku/russell_jarvis_s...
But this is caused by the Knight's+King's move constraints (with the "consecutive numbers" constraint prohibiting relabeling of the numbers). There's way more solutions without those extra constraints.
One thing to nitpick is that the number operators (#>) require an import of clpfd, so some readers may not be able to get the code examples to work.
:- use_module(library(clpfd)).
Yeah, always show your imports with your code snippets! :-)1. Solving Sudoku puzzles.
2. Designing new Sudoku puzzles (with additional constraints even.)
3. Designing (e.g. CLP(FD/ℤ)) programs to solve all sudoku puzzles everywhere.
- - - -
How do you pronounce "ℤ" in this context? "see el pee eff dee zed"? Seems too plain. "see el pee eff dee Zahlen" seems too fancy. :-)
Though, of these 9 solutions there are more similarities, i.e. some of them are the same rows except that the positions are switched.
Some other thing. if the lines are sorted, there are only 4 distinct variants.
Also, the solution of the original Miracle Sudoku instance has some invariants, for example the rows are rotations of these numbers 1,5,9,4,8,3,7,2,6 and the columns are (when rows are sorted) rotations of the numbers 1..9.
I've added comments about this in my Picat model: http://hakank.org/picat/miracle_sudoku.pi
9 ways to transpose; 4 rotations, 2 for flipped or not. Multiply those, you get the full 72.
I agree that 1,5,9,4,8,3,7,2,6 pattern rotations is there somewhere, either in the columns or the rows of a solution, but that don't make all solutions the same.
159483726
594837261
Look pretty equivalent to me, which is how you get the 9 transpositions.
I am using joins over constraints written and read in MEMORY tables (including CROSS JOIN, generating an application of which was one of my motivations to do the project). Anything advanced is pushed to the query execution plan.
This is on MySQL; I also hope to explore a SQLite browser version. I aim to generate some performance profiling as a side product.
I find z3 python interface much simpler, more intuitive and more powerful than prolog for this kind of things
It all depends on what you are looking for.