Prolog: Examples of Combinatorial Problems

In these notes we’ll see how Prolog can be used to solve various combinatorial problems.

Pythagorean Triples

Three integers \((a, b, c)\) form a Pythagorean triple if \(a^2 + b^2 = c^2\). Here’s a Prolog functions that tests for Pythagorean triples:

is_triple(A, B, C) :-
  D is C*C - A*A - B*B,  % must use "is" for arithmetic
  D = 0.

For example:

?- is_triple(3, 4, 5).
true.

?- is_triple(4, 5, 7).
false.

We can use member to generate solutions like this:

solve_triple1(A, B, C) :-
    member(A, [1,2,3,4,5,6,7,8,9,10]),
    member(B, [1,2,3,4,5,6,7,8,9,10]),
    member(C, [1,2,3,4,5,6,7,8,9,10]),
    is_triple(A, B, C).

And here are the results:

?- solve_triple1(A, B, C).
A = 3,
B = 4,
C = 5 ;
A = 4,
B = 3,
C = 5 ;
A = 6,
B = 8,
C = 10 ;
A = 8,
B = 6,
C = 10 ;
false.

This style of algorithm is sometimes referred to as generate and test. We generate a candidate solution, and then test it to determine if it is valid.

Four different Pythagorean triples are calculated. However, (3, 4, 5) and (4, 3, 5) are essentially the same, as are (6, 8, 10) and (8, 6, 10). So lets require that the triples be in order:

solve_triple2(A, B, C) :-
    member(A, [1,2,3,4,5,6,7,8,9,10]),
    member(B, [1,2,3,4,5,6,7,8,9,10]),
    A =< B,                            % =< is less than or equal to
    member(C, [1,2,3,4,5,6,7,8,9,10]),
    B =< C,
    is_triple(A, B, C).

The results are little easier to read:

?- solve_triple2(A, B, C).
A = 3,
B = 4,
C = 5 ;
A = 6,
B = 8,
C = 10 ;
false.

Of course, a limitation of these two functions is that the values for A, B, and C are restricted to the numbers from 1 to 10. One way to fix that is as follows:

solve_triple3(N, A, B, C) :-
    between(1, N, A),
    between(1, N, B),
    A =< B,
    between(1, N, C),
    B =< C,
    is_triple(A, B, C).

between(Lo, Hi, N) is a standard function in SWI-Prolog that can generate integers from Lo to Hi (inclusive). For example:

?- between(1, 4, N).
N = 1 ;
N = 2 ;
N = 3 ;
N = 4.

Many programmers are impressed by the brevity and clarity of these sorts of Prolog programs. But it’s worth noting that we can implement this same style of generate and test straightforwardly in other languages. For instance, in Python we could write this:

def solve_triple(n):            # Python
    for a in xrange(1, n + 1):
        for b in xrange(1, n + 1):
            if a < b:
                for c in xrange(1, n + 1):
                    if b < c:
                        if is_triple(a, b, c):
                            print a, b, c

While backtracking is not a built-in part of Python as in Prolog, the flow of control jumps around in the same way due to how and when loops terminate. Note that when the program is at the line if is_triple(a, b, c), then there are four places the flow of control could jump to next.

Go is similar:

func solve_triple(n int) {         // Go
    for a := 1; a <= n; a++ {
        for b := 1; b <= n; b++ {
            if a < b {
                for c := 1; c <= n; c++ {
                    if b < c {
                        if a*a + b*b == c*c {
                            fmt.Println(a, b, c)
                        }
                    }
                }
            }
        }
    }
}

If you remove all the indentation and squint, it looks a little closer to Prolog version:

func solve_triple(n int) {     // Go
    for a := 1; a <= n; a++ {
    for b := 1; b <= n; b++ {
    if a < b {
    for c := 1; c <= n; c++ {
    if b < c {
        if a*a + b*b == c*c {
            fmt.Println(a, b, c)
}}}}}}}

All of these examples do the simplest form of backtracking. For example, when they’ve looped through all values of c, they jump up to the b for-loop to get the next value of b. It’s never the case that the jump from c to a. This is a limitation, since in some problems it might be (vastly) more efficient to backtrack to some other value.

The non-Prolog functions are not quite the same as the Prolog one because Prolog lets you stop after you get any solution, while the other functions run to completion. However, you could simulate Prolog using generators in Python, or goroutines in Go.

Generating Bit Strings

Suppose we want to generate a list of all bit strings of length n. One way to do this relies on this very small knowledge base:

bit(0).
bit(1).

This says that 0 and 1 are bits.

To generate all bit strings of length 3, we can do this:

?- bit(A), bit(B), bit(C), X=[A, B, C].
A = B, B = C, C = 0,
X = [0, 0, 0] ;
A = B, B = 0,
C = 1,
X = [0, 0, 1] ;
A = C, C = 0,
B = 1,
X = [0, 1, 0] ;
A = 0,
B = C, C = 1,
X = [0, 1, 1] ;
A = 1,
B = C, C = 0,
X = [1, 0, 0] ;
A = C, C = 1,
B = 0,
X = [1, 0, 1] ;
A = B, B = 1,
C = 0,
X = [1, 1, 0] ;
A = B, B = C, C = 1,
X = [1, 1, 1].

Lets trace this to understand how it works. When the query begins, A is assigned the value 0, B is assigned the value 0, and then C is assigned the value 0. This is a valid assignment, and so we have the first bit string: [0, 0, 0]. When the user enter ;, Prolog backtracks to C, which means it unassigns C and then tries to find another value that satisfies C. It does: it finds that C could be 1. And so we have the second bit string: [0, 0, 1]. Prolog backtracks to C, unassigns it, and discovers that there are no other values that it can assign to C. So now it backtracks to B, and unassigns B. It tries to find a different assignment to B, and ends up assigning 1 to B. Now it tries to find a value for C. The first value it finds is 0, and so C gets assigned that. This gives the third bit string: [0, 1, 0]. Backtracking and continues in this fashion until all bit strings have been generated.

While this is a simple way to generate bit strings in Prolog, it would be more convenient to have a function that calculates all bit strings of length N, e.g.:

?- nbits(3, Bits).
Bits = [0, 0, 0] ;
Bits = [0, 0, 1] ;
Bits = [0, 1, 0] ;
Bits = [0, 1, 1] ;
Bits = [1, 0, 0] ;
Bits = [1, 0, 1] ;
Bits = [1, 1, 0] ;
Bits = [1, 1, 1] ;
false

nbits is a little bit tricky to write for new Prolog programmers, but it is well worth the effort to understand:

nbits(1, [B]) :-     % base case
  bit(B).
nbits(N, [B|Bs]) :-  % recursive case
  N > 1,
  bit(B),
  N1 is N - 1,
  nbits(N1, Bs).

Study this function: understand every line!

Four-by-four Sudoku

Sudoku puzzles are a popular number puzzle that turn out to be easily representable in Prolog. Typically, they are played on a 9-by-9 grid of cells, where the goal is to put the numbers 1 to 9 into each cell such that:

  • Each row is a permutation of 1 to 9.
  • Each column is a permutation of 1 to 9.
  • Each of the 9 3-by-3 sub-squares contain a permutation of 1 to 9.

Sudoku puzzles start with some numbers already filled in, and then, through a process of careful deduction, you try to fill in all the other numbers. It’s possible that some puzzles might have more than one solution.

A four-by-four Sudoku puzzle is the same idea, except it is played on a 4-by-4 grid instead of a 9-by-9, e.g.:

A B  C D
E F  G H

I J  K L
M N  O P

Each row and column must be a permutation of the numbers 1 to 4. Also, the 4 2-by-2 sub-squares must also be permutations of 1 to 4.

Here’s how we could solve this puzzle in Prolog:

solution(A, B, C, D,
         E, F, G, H,
         I, J, K, L,
         M, N, O, P) :-
    % row constraints
    permutation([1, 2, 3, 4], [A, B, C, D]),
    permutation([1, 2, 3, 4], [E, F, G, H]),
    permutation([1, 2, 3, 4], [I, J, K, L]),
    permutation([1, 2, 3, 4], [M, N, O, P]),

    % column constraints
    permutation([1, 2, 3, 4], [A, E, I, M]),
    permutation([1, 2, 3, 4], [B, F, J, N]),
    permutation([1, 2, 3, 4], [C, G, K, O]),
    permutation([1, 2, 3, 4], [D, H, L, P]),

    % sub-square constraints
    permutation([1, 2, 3, 4], [A, B, E, F]),  % upper left
    permutation([1, 2, 3, 4], [C, D, G, H]),  % upper right
    permutation([1, 2, 3, 4], [I, J, M, N]),  % lower left
    permutation([1, 2, 3, 4], [K, L, O, P]).  % lower right

The built-in function permutation generates permutations of the members of a list, e.g.:

?- permutation([1, 2, 3], Perm).
Perm = [1, 2, 3] ;
Perm = [1, 3, 2] ;
Perm = [2, 1, 3] ;
Perm = [2, 3, 1] ;
Perm = [3, 1, 2] ;
Perm = [3, 2, 1] ;
false.

We can make the output nicer like this:

sudoku(A, B, C, D,
       E, F, G, H,
       I, J, K, L,
       M, N, O, P) :-
    solution(A, B, C, D,
             E, F, G, H,
             I, J, K, L,
             M, N, O, P),
    nl,
    write('A solution to this puzzle is'),
    nl,
    printrow(A, B, C, D),
    printrow(E, F, G, H),
    printrow(I, J, K, L),
    printrow(M, N, O, P).

printrow(P, Q, R, S) :-
    write(' '), write(P), write(' '), write(Q),
    write(' '), write(R), write(' '), write(S), nl.

To run it, supply a starting 4-by-4 Sudoku grid:

?- sudoku(
|    1, 4, _, _,
|    _, _, 4, _,
|    2, _, _, _,
|    _, _, _, _
|    ).

A solution to this puzzle is
 1 4 2 3
 3 2 4 1
 2 1 3 4
 4 3 1 2
true ;

A solution to this puzzle is
 1 4 2 3
 3 2 4 1
 2 3 1 4
 4 1 3 2
true ;

A solution to this puzzle is
 1 4 3 2
 3 2 4 1
 2 3 1 4
 4 1 2 3
true ;
false.

Solving the 4-by-4 Sudoku puzzle is so straightforward that it is natural to try the same approach with the full 9-by-9 version (or even bigger!). However, while it will work in principle, there are so many possibilities to search through in the 9-by-9 version that the program won’t finish in a reasonable amount of time.

To see why, first notice that the program does not use any knowledge specific to Sudoku. It solves the puzzle by pure brute force: it generates all possible permutations and tests which ones satisfy the constraints.

We can get a rough estimate of the number of different 9-by-9 Sudoku boards as follows. Each row has \(9! = 362880\) possible permutations, and since there are 9 rows there are \(9!^9 \approx 1.1 \times 10^{51}\) possibilities for this generate-and-test program to try.

Assuming we could generate a trillion (\(10^{12}\)) permutations per second, it would take on the order of \(10^{39}\) seconds to solve, which is about \(3 \times 10^{29}\) centuries.

People can obviously solve 9-by-9 puzzles much more quickly than that. They do it by using knowledge specific to Sudoku to more efficiently figure out numbers. Our program has no knowledge about Sudoku other than what counts as a solution. With more work, it is possible to add extra knowledge to make it faster, but we don’t do that here.

The SEND MORE MONEY Puzzle

The SEND MORE MONEY puzzle is a classic cryptarithmetic puzzle that can be solved neatly in Prolog. The puzzle asks you to replace letters with numbers that makes this equation true: SEND + MORE = MONEY. Each letter stands for a single digit, 0 to 9, and different letters are different digits (so, for example, S and E can’t be the same). Also, S and M can’t be 0 because numbers don’t start with 0.

There are a couple of ways to solve this problem in Prolog. One way is like this:

alpha1(S, E, N, D, M, O, R, Y, [S, E, N, D, M, O, R, E, M, O, N, E, Y]) :-
    between(1, 9, S),
    between(0, 9, E),
    E \=S,
    between(0, 9, N),
    N \= E, N \= S,
    between(0, 9, D),
    D \= N, D \= E, D \= S,
    between(1, 9, M),
    M \= D, M \= N, M \= E, M \= S,
    between(0, 9, O),
    O \= M, O \= D, O \= N, O \= E, O \= S,
    between(0, 9, R),
    R \= O, R \= M, R \= D, R \= N, R \= E, R \= S,
    between(0, 9, Y),
    Y \= R, Y \= O, Y \= M, Y \= D, Y \= N, Y \= E, Y \= S,
    Send is S*1000 + E*100 + N*10 + D,
    More is M*1000 + O*100 + R*10 + E,
    Money is M*10000 + O*1000 + N*100 + E*10 + Y,
    Money is Send + More.

This takes a few seconds to run on a typical desktop computer, and it finds the one unique solution:

?- alpha1(S, E, N, D, M, O, R, Y, Sol).
S = 9,
E = 5,
N = 6,
D = 7,
M = 1,
O = 0,
R = 8,
Y = 2,
Sol = [9, 5, 6, 7, 1, 0, 8, 5, 1|...] .

Importantly, as soon as values are assigned to variables, we enforce the dis- equality constraints so that failure will happen as soon as possible. It would run much more slowly if, say, you put all the \= constraints in one group at the bottom.

An even more efficient approach is to mimic how people do addition:

alpha2(S, E, N, D, M, O, R, Y, [S, E, N, D, M, O, R, E, M, O, N, E, Y]) :-
    between(0, 9, D), between(0, 9, E),
    Y is (D + E) mod 10,
    C1 is (D + E) div 10,
    between(0, 9, N), between(0, 9, R),
    E is (N + R + C1) mod 10,
    C2 is (N + R + C1) div 10,
    between(0, 9, O),
    N is (E + O + C2) mod 10,
    C3 is (E + O + C2) div 10,
    between(0, 9, S), between(1, 9, M),
    O is (S + M + C3) mod 10,
    M is (S + M + C3) div 10,
    all_diff([S, E, N, D, M, O, R, Y]).

Again, we interleave constraints with calls to between for efficiency. The variables C1, C2, and C3 are the carry values of the additions of the individual digits.

The all_diff function succeeds just when all the elements on the list passed to it are different, e.g.:

?- all_diff([6, 8, 4, 11, 1, 5]).
true.

?- all_diff([6, 8, 4, 11, 1, 4]).
false.

Here’s an implementation:

all_diff([]).
all_diff([X|Xs]) :- not(member(X,Xs)), all_diff(Xs).

What’s nice about this general approach is that the program is essentially just a list of the constraints on the variables. We then let Prolog do the work of finding satisfying values.

While this approach is great in theory, in practice it typically requires a lot of extra problem-specific code to make such programs run efficiently.

Map Coloring

Map coloring is a classic computational problem. By a map is meant a graph, i.e. a set of vertices connected by edges.

In a map coloring problem, the task is to assign to each vertex a color such that no two edges connected by an edge share the same color. In general, this is computationally challenging problem, i.e. determining whether or not a graph can be 3-colored is NP-complete.

One way to represent a map 3-coloring problem in Prolog is as follows:

% allowable colors
color(red).
color(green).
color(blue).

% vertices A and B are neighbors if they are adjacent
% and have different colors
neighbor(A, B) :-
    color(A),
    color(B),
    A \= B.

% for a map consisting of a given list of vertices,
% we list the edges between the vertices
map1([A, B, C, D]) :-
        neighbor(A, B),
        neighbor(B, C),
        neighbor(C, A),
        neighbor(D, A),
        neighbor(D, B),
        neighbor(D, C).

map2([A, B, C, D, E]) :-
        neighbor(A, B),
        neighbor(B, C),
        neighbor(C, D),
        neighbor(D, E),
        neighbor(E, A).

While this is a nice example of a Prolog program, it is not the most efficient way to solve a coloring problem. For large maps, it is far too inefficient: if Prolog did smarter backtracking, or used heuristics specific to map coloring, it could do much better.