In [1]:
from z3 import *

The problem¶

Color the following graph with red, yellow, and blue such that no two adjacent nodes have the same color

In [2]:
from IPython.display import Image
Image(filename='/e/graph.jpg') 
Out[2]:
In [3]:
nodes = ['A', 'B', 'C', 'D', 'E']
edges = [('A', 'C'), ('A', 'E'), ('B', 'C'), ('B', 'E'), ('C', 'D'), ('D', 'E')]
colors = ['r', 'b', 'y']

Propositional variables are created by calling Bool with the name of the variable as an argument. Z3 treats variable objects with the same name identically, so future objects returned by calling Bool with an identical argument will refer to the same variable.

Here we define one variable for each node-color pairing.

In [4]:
vs = []
for node in nodes:
    for color in colors:
        vs.append(Bool('{}{}'.format(node, color)))
In [5]:
vs
Out[5]:
[Ar, Ab, Ay, Br, Bb, By, Cr, Cb, Cy, Dr, Db, Dy, Er, Eb, Ey]

The encoding¶

Define constraints which prohibit adjacent vertices being assigned the same color.

In [6]:
adjacent_not_same = []
for (n1, n2) in edges:
    for color in colors:
        v1 = z3.Bool('{}{}'.format(n1, color))
        v2 = z3.Bool('{}{}'.format(n2, color))
        adjacent_not_same.append(Or(Not(v1), Not(v2)))
In [7]:
adjacent_not_same, len(adjacent_not_same)
Out[7]:
([Or(Not(Ar), Not(Cr)),
  Or(Not(Ab), Not(Cb)),
  Or(Not(Ay), Not(Cy)),
  Or(Not(Ar), Not(Er)),
  Or(Not(Ab), Not(Eb)),
  Or(Not(Ay), Not(Ey)),
  Or(Not(Br), Not(Cr)),
  Or(Not(Bb), Not(Cb)),
  Or(Not(By), Not(Cy)),
  Or(Not(Br), Not(Er)),
  Or(Not(Bb), Not(Eb)),
  Or(Not(By), Not(Ey)),
  Or(Not(Cr), Not(Dr)),
  Or(Not(Cb), Not(Db)),
  Or(Not(Cy), Not(Dy)),
  Or(Not(Dr), Not(Er)),
  Or(Not(Db), Not(Eb)),
  Or(Not(Dy), Not(Ey))],
 18)

Now we define constraints which state that each vertex must be assigned a color.

In [8]:
at_least_one_color = []
for node in nodes:
    disj = []
    for color in colors:
        disj.append(z3.Bool('{}{}'.format(node, color)))
    at_least_one_color.append(Or(disj))
In [9]:
at_least_one_color
Out[9]:
[Or(Ar, Ab, Ay),
 Or(Br, Bb, By),
 Or(Cr, Cb, Cy),
 Or(Dr, Db, Dy),
 Or(Er, Eb, Ey)]

And finally, constraints which limit each vertex to at most one color.

In [10]:
at_most_one_color = []
for node in nodes:
    for i in range(len(colors)):
        for j in range(i+1, len(colors)):
            at_most_one_color.append(Or([Not(Bool('{}{}'.format(node, colors[i]))), 
                                        Not(Bool('{}{}'.format(node, colors[j])))]))
In [11]:
at_most_one_color
Out[11]:
[Or(Not(Ar), Not(Ab)),
 Or(Not(Ar), Not(Ay)),
 Or(Not(Ab), Not(Ay)),
 Or(Not(Br), Not(Bb)),
 Or(Not(Br), Not(By)),
 Or(Not(Bb), Not(By)),
 Or(Not(Cr), Not(Cb)),
 Or(Not(Cr), Not(Cy)),
 Or(Not(Cb), Not(Cy)),
 Or(Not(Dr), Not(Db)),
 Or(Not(Dr), Not(Dy)),
 Or(Not(Db), Not(Dy)),
 Or(Not(Er), Not(Eb)),
 Or(Not(Er), Not(Ey)),
 Or(Not(Eb), Not(Ey))]

Finding solutions¶

In [12]:
s = Solver()
s.add(adjacent_not_same)
s.add(at_least_one_color)
s.add(at_most_one_color)

The string representation of a solver object is the set of constraints that have been asserted on it

In [13]:
s
Out[13]:
[¬Ar ∨ ¬Cr, ¬Ab ∨ ¬Cb, ¬Ay ∨ ¬Cy, ¬Ar ∨ ¬Er, ¬Ab ∨ ¬Eb, ¬Ay ∨ ¬Ey, ¬Br ∨ ¬Cr, ¬Bb ∨ ¬Cb, ¬By ∨ ¬Cy, ¬Br ∨ ¬Er, ¬Bb ∨ ¬Eb, ¬By ∨ ¬Ey, ¬Cr ∨ ¬Dr, ¬Cb ∨ ¬Db, ¬Cy ∨ ¬Dy, ¬Dr ∨ ¬Er, ¬Db ∨ ¬Eb, ¬Dy ∨ ¬Ey, Ar ∨ Ab ∨ Ay, Br ∨ Bb ∨ By, Cr ∨ Cb ∨ Cy, Dr ∨ Db ∨ Dy, Er ∨ Eb ∨ Ey, ¬Ar ∨ ¬Ab, ¬Ar ∨ ¬Ay, ¬Ab ∨ ¬Ay, ¬Br ∨ ¬Bb, ¬Br ∨ ¬By, ¬Bb ∨ ¬By, ¬Cr ∨ ¬Cb, ¬Cr ∨ ¬Cy, ¬Cb ∨ ¬Cy, ¬Dr ∨ ¬Db, ¬Dr ∨ ¬Dy, ¬Db ∨ ¬Dy, ¬Er ∨ ¬Eb, ¬Er ∨ ¬Ey, ¬Eb ∨ ¬Ey]
In [14]:
s.check()
Out[14]:
sat

If the solver returns sat, then a satisfying assignment can be obtained with its model method

In [15]:
s.model()
Out[15]:
[Dr = True, Ab = False, Dy = False, Cy = False, By = False, Ar = True, Br = True, Cr = False, Eb = True, Bb = False, Cb = True, Ay = False, Db = False, Er = False, Ey = False]

It's a bit unnatural to read the assignment this way, so we'll pretty-print it by adjacency

In [16]:
for v1, v2 in edges:
    v1_color = None
    v2_color = None
    for color in colors:
        if is_true(s.model().eval(z3.Bool('{}{}'.format(v1, color)))):
            v1_color = color
        if is_true(s.model().eval(z3.Bool('{}{}'.format(v2, color)))):
            v2_color = color
    print('{}{} -- {}{}'.format(v1, v1_color, v2, v2_color))
Ar -- Cb
Ar -- Eb
Br -- Cb
Br -- Eb
Cb -- Dr
Dr -- Eb

Although the solution Z3 obtained isn't the same one shown in the drawing, it is a valid coloring of the graph nonetheless

In [17]:
from IPython.display import Image
Image(filename='/e/graph.jpg') 
Out[17]:
In [ ]: