Solving "Who owns the Zebra" programmatically?

In Prolog, we can instantiate the domain just by selecting elements from it :) (making mutually-exclusive choices, for efficiency). Using SWI-Prolog,

select([A|As],S):- select(A,S,S1),select(As,S1).
select([],_). 

left_of(A,B,C):- append(_,[A,B|_],C).  
next_to(A,B,C):- left_of(A,B,C) ; left_of(B,A,C).

zebra(Owns, HS):-     % (* house: color,nation,pet,drink,smokes *)
  HS   = [ h(_,norwegian,_,_,_),    h(blue,_,_,_,_),   h(_,_,_,milk,_), _, _], 
  select([ h(red,brit,_,_,_),       h(_,swede,dog,_,_), 
           h(_,dane,_,tea,_),       h(_,german,_,_,prince)], HS),
  select([ h(_,_,birds,_,pallmall), h(yellow,_,_,_,dunhill),
           h(_,_,_,beer,bluemaster)],                        HS), 
  left_of( h(green,_,_,coffee,_),   h(white,_,_,_,_),        HS),
  next_to( h(_,_,_,_,dunhill),      h(_,_,horse,_,_),        HS),
  next_to( h(_,_,_,_,blend),        h(_,_,cats, _,_),        HS),
  next_to( h(_,_,_,_,blend),        h(_,_,_,water,_),        HS),
  member(  h(_,Owns,zebra,_,_),                              HS).

Runs quite instantly:

?- time( (zebra(Who,HS), writeln(Who), nl, maplist(writeln,HS), nl, false 
          ; writeln("no more solutions!") )).
german

h( yellow, norwegian, cats,   water,  dunhill   )
h( blue,   dane,      horse,  tea,    blend     )
h( red,    brit,      birds,  milk,   pallmall  )
h( green,  german,    zebra,  coffee, prince    )   % (* formatted by hand *)
h( white,  swede,     dog,    beer,   bluemaster)

no more solutions!
% (* 1,706 inferences, 0.000 CPU in 0.070 seconds (0% CPU, Infinite Lips) *)
true.

One poster already mentioned that Prolog is a potential solution. This is true, and it's the solution I would use. In more general terms, this is a perfect problem for an automated inference system. Prolog is a logic programming language (and associated interpreter) that form such a system. It basically allows concluding of facts from statements made using First Order Logic. FOL is basically a more advanced form of propositional logic. If you decide you don't want to use Prolog, you could use a similar system of your own creation using a technique such as modus ponens to perform the draw the conclusions.

You will, of course, need to add some rules about zebras, since it isn't mentioned anywhere... I believe the intent is that you can figure out the other 4 pets and thus deduce the last one is the zebra? You'll want to add rules that state a zebra is one of the pets, and each house can only have one pet. Getting this kind of "common sense" knowledge into an inference system is the major hurdle to using the technique as a true AI. There are some research projects, such as Cyc, which are attempting to give such common knowledge through brute force. They've met with an interesting amount of success.


Here's a solution in Python based on constraint-programming:

from constraint import AllDifferentConstraint, InSetConstraint, Problem

# variables
colors        = "blue red green white yellow".split()
nationalities = "Norwegian German Dane Swede English".split()
pets          = "birds dog cats horse zebra".split()
drinks        = "tea coffee milk beer water".split()
cigarettes    = "Blend, Prince, Blue Master, Dunhill, Pall Mall".split(", ")

# There are five houses.
minn, maxn = 1, 5
problem = Problem()
# value of a variable is the number of a house with corresponding property
variables = colors + nationalities + pets + drinks + cigarettes
problem.addVariables(variables, range(minn, maxn+1))

# Each house has its own unique color.
# All house owners are of different nationalities.
# They all have different pets.
# They all drink different drinks.
# They all smoke different cigarettes.
for vars_ in (colors, nationalities, pets, drinks, cigarettes):
    problem.addConstraint(AllDifferentConstraint(), vars_)

# In the middle house they drink milk.
#NOTE: interpret "middle" in a numerical sense (not geometrical)
problem.addConstraint(InSetConstraint([(minn + maxn) // 2]), ["milk"])
# The Norwegian lives in the first house.
#NOTE: interpret "the first" as a house number
problem.addConstraint(InSetConstraint([minn]), ["Norwegian"])
# The green house is on the left side of the white house.
#XXX: what is "the left side"? (linear, circular, two sides, 2D house arrangment)
#NOTE: interpret it as 'green house number' + 1 == 'white house number'
problem.addConstraint(lambda a,b: a+1 == b, ["green", "white"])

def add_constraints(constraint, statements, variables=variables, problem=problem):
    for stmt in (line for line in statements if line.strip()):
        problem.addConstraint(constraint, [v for v in variables if v in stmt])

and_statements = """
They drink coffee in the green house.
The man who smokes Pall Mall has birds.
The English man lives in the red house.
The Dane drinks tea.
In the yellow house they smoke Dunhill.
The man who smokes Blue Master drinks beer.
The German smokes Prince.
The Swede has a dog.
""".split("\n")
add_constraints(lambda a,b: a == b, and_statements)

nextto_statements = """
The man who smokes Blend lives in the house next to the house with cats.
In the house next to the house where they have a horse, they smoke Dunhill.
The Norwegian lives next to the blue house.
They drink water in the house next to the house where they smoke Blend.
""".split("\n")
#XXX: what is "next to"? (linear, circular, two sides, 2D house arrangment)
add_constraints(lambda a,b: abs(a - b) == 1, nextto_statements)

def solve(variables=variables, problem=problem):
    from itertools  import groupby
    from operator   import itemgetter

    # find & print solutions
    for solution in problem.getSolutionIter():
        for key, group in groupby(sorted(solution.iteritems(), key=itemgetter(1)), key=itemgetter(1)):
            print key, 
            for v in sorted(dict(group).keys(), key=variables.index):
                print v.ljust(9),
            print

if __name__ == '__main__':
    solve()

Output:

1 yellow    Norwegian cats      water     Dunhill  
2 blue      Dane      horse     tea       Blend    
3 red       English   birds     milk      Pall Mall
4 green     German    zebra     coffee    Prince   
5 white     Swede     dog       beer      Blue Master

It takes 0.6 seconds (CPU 1.5GHz) to find the solution.
The answer is "German owns zebra."


To install the constraint module via pip: pip install python-constraint

To install manually:

  • download:

    $ wget https://pypi.python.org/packages/source/p/python-constraint/python-constraint-1.2.tar.bz2#md5=d58de49c85992493db53fcb59b9a0a45

  • extract (Linux/Mac/BSD):

    $ bzip2 -cd python-constraint-1.2.tar.bz2 | tar xvf -

  • extract (Windows, with 7zip):

    > 7z e python-constraint-1.2.tar.bz2
    > 7z e python-constraint-1.2.tar

  • install:

    $ cd python-constraint-1.2
    $ python setup.py install