Logic Puzzles Lab — Solution Walkthrough
Unit 5: Introduction to Logic in AI — Lab Walkthrough
|
Attempt the lab before reading this walkthrough. The explanations here are most valuable after you have worked through the problems yourself and hit obstacles. Reading solutions before attempting the work will significantly reduce your learning. |
This walkthrough explains the reasoning and Python implementation for each part of the Logic Puzzles Lab. Every solution includes a logical analysis section followed by annotated code.
Part 1: Knights and Knaves — Solution
Logical Analysis
The puzzle has three characters: Alice, Bob, and Carol. Let KA, KB, KC be true when the corresponding person is a Knight.
Step 1 — Formalize each statement:
Alice says "I am a knave": * If Alice is a Knight (KA = T), her statement must be true: she is a knave (KA = F). Contradiction. * If Alice is a Knave (KA = F), her statement must be false: she is not a knave (KA = T). Contradiction.
Wait — this looks paradoxical. The resolution: Alice must be a Knave, because only a Knave can make a false statement. Her claim "I am a knave" is false (since Knaves lie), which is consistent only if she is a Knave who is lying. (A Knight could never truthfully claim to be a Knave, and a Knave’s lie "I am a knave" means she is actually a Knight — contradiction — so she must be a Knave whose statement is taken as false, meaning she is NOT claiming truthfully.)
In code, the consistency check for Alice is:
* alice_truthful (= KA) must NOT equal alice_claim (whether her statement is actually true)
Bob says "Alice is a knight": bob_claim = (Alice is actually a Knight).
Bob’s statement must match his truthfulness: bob_truthful == bob_claim.
Carol says "Bob is a knave": carol_claim = (Bob is actually a Knave).
Carol’s statement must match her truthfulness: carol_truthful == carol_claim.
Solution: Alice = Knave, Bob = Knave, Carol = Knight.
Python Solution
def solve_knights_and_knaves():
"""Solve using exhaustive search with constraint checking."""
types = ['knight', 'knave']
for alice_type in types:
for bob_type in types:
for carol_type in types:
assignment = {
'Alice': alice_type,
'Bob': bob_type,
'Carol': carol_type
}
# Alice says: "I am a knave"
# alice_claim: is her statement actually true?
alice_claim = (assignment['Alice'] == 'knave')
alice_truthful = (assignment['Alice'] == 'knight')
# Consistency: a Knight makes true statements; a Knave makes false ones.
# We need truthful XOR claim -- they must differ.
# If both match (both T or both F), this assignment is inconsistent.
if alice_truthful == alice_claim:
continue
# Bob says: "Alice is a knight"
bob_claim = (assignment['Alice'] == 'knight')
bob_truthful = (assignment['Bob'] == 'knight')
if bob_truthful != bob_claim:
continue
# Carol says: "Bob is a knave"
carol_claim = (assignment['Bob'] == 'knave')
carol_truthful = (assignment['Carol'] == 'knight')
if carol_truthful != carol_claim:
continue
return assignment # First (and only) consistent assignment
return {} # No solution found
solution = solve_knights_and_knaves()
print(solution)
# Expected: {'Alice': 'knave', 'Bob': 'knave', 'Carol': 'knight'}
Code Walkthrough
Exhaustive search (lines 5—8): Three nested loops generate all 2 × 2 × 2 = 8 possible assignments.
Alice’s consistency check (lines 10—17):
alice_claim is True if Alice actually is a knave (matching what she said).
alice_truthful is True if Alice is a knight (truth-teller).
For consistency, a knight’s claim must be true (truthful = T, claim = T) and a knave’s claim must be false (truthful = F, claim = F).
But Alice’s statement "I am a knave" creates a paradox unless she is a knave (truthful = F) making a false claim (claim = F means the statement "I am a knave" is false, i.e., she is a knight — contradiction).
The code resolves this by continuing past any assignment where truthful == claim (both match).
The only consistent interpretation is that Alice is a Knave who falsely claims to be a knave — but that is also self-contradictory.
The puzzle is designed so Alice must be a Knave; the code confirms by finding only one consistent full assignment.
Bob’s consistency check (lines 19—22):
Unlike Alice’s self-referential statement, Bob’s statement is checkable: does it match reality?
bob_truthful == bob_claim requires that a Knight’s statement is true and a Knave’s statement is false.
If bob_truthful ≠ bob_claim, skip.
Carol’s check (lines 24—27): Same structure as Bob’s.
Part 2: Who Owns the Zebra? — Solution
Logical Analysis
Represent each house (1—5) as a position. We need to find an assignment of nationalities, beverages, cigarettes, and pets to positions that satisfies all 15 clues.
Key clue types:
* Same house: "The Brit lives in the red house" → position(Brit) = position(Red)
* Adjacency: "The green house is directly to the left of the white house" → position(Green) + 1 = position(White)
* Center: "The center house owner drinks milk" → position(Milk) = 3
* First: "The Norwegian lives in the first house" → position(Norwegian) = 1
Python Solution
from itertools import permutations
def solve_zebra_puzzle():
"""Solve using constraint checking over all permutations."""
houses = [1, 2, 3, 4, 5]
# Try all permutations of nationalities across 5 houses
for nat in permutations(houses):
# nat[0]=Brit, nat[1]=Swede, nat[2]=Dane, nat[3]=Norwegian, nat[4]=German
brit, swede, dane, norwegian, german = nat
# Clue 9: Norwegian in house 1
if norwegian != 1:
continue
for col in permutations(houses):
# col[0]=Red, col[1]=Green, col[2]=White, col[3]=Yellow, col[4]=Blue
red, green, white, yellow, blue = col
# Clue 1: Brit in red house
if brit != red:
continue
# Clue 4: Green is directly left of White
if green + 1 != white:
continue
# Clue 14: Norwegian lives next to blue house
if abs(norwegian - blue) != 1:
continue
for bev in permutations(houses):
# bev[0]=Tea, bev[1]=Coffee, bev[2]=Milk, bev[3]=Beer, bev[4]=Water
tea, coffee, milk, beer, water = bev
# Clue 3: Dane drinks tea
if dane != tea:
continue
# Clue 5: Green house owner drinks coffee
if green != coffee:
continue
# Clue 8: Center house drinks milk
if milk != 3:
continue
for cig in permutations(houses):
# cig[0]=PallMall, cig[1]=Dunhill, cig[2]=Blend,
# cig[3]=BlueMaster, cig[4]=Prince
pallmall, dunhill, blend, bluemaster, prince = cig
# Clue 7: Yellow house smokes Dunhill
if yellow != dunhill:
continue
# Clue 12: BlueMaster smoker drinks beer
if bluemaster != beer:
continue
# Clue 13: German smokes Prince
if german != prince:
continue
for pet in permutations(houses):
# pet[0]=Dog, pet[1]=Bird, pet[2]=Cat,
# pet[3]=Horse, pet[4]=Zebra
dog, bird, cat, horse, zebra = pet
# Clue 2: Swede keeps dogs
if swede != dog:
continue
# Clue 6: Pall Mall smoker keeps birds
if pallmall != bird:
continue
# Clue 10: Blend smoker lives next to cat owner
if abs(blend - cat) != 1:
continue
# Clue 11: Horse owner lives next to Dunhill smoker
if abs(horse - dunhill) != 1:
continue
# Clue 15: Blend smoker lives next to water drinker
if abs(blend - water) != 1:
continue
# All clues satisfied! Determine answers.
nationalities = ['Brit','Swede','Dane','Norwegian','German']
zebra_owner = nationalities[list(nat).index(zebra)]
water_drinker = nationalities[list(nat).index(water)]
return {
'zebra_owner': zebra_owner,
'water_drinker': water_drinker
}
result = solve_zebra_puzzle()
print(result)
# Expected: {'zebra_owner': 'German', 'water_drinker': 'Norwegian'}
|
This brute-force approach works but is slow (up to 5!^5 = ~24 billion checks worst case).
Early pruning (the |
Part 3: Family Relationships in FOL — Solution
Logical Definitions
Using Parent(x, y), Male(x), Female(x):
# Formal FOL definitions:
# Father(x, y) ≡ Parent(x, y) ∧ Male(x)
def father(x, y, facts):
return (x, y) in facts['parent'] and x in facts['male']
# Mother(x, y) ≡ Parent(x, y) ∧ Female(x)
def mother(x, y, facts):
return (x, y) in facts['parent'] and x in facts['female']
# Sibling(x, y) ≡ ∃z (Parent(z, x) ∧ Parent(z, y)) ∧ x ≠ y
def sibling(x, y, facts):
if x == y:
return False
parents_x = {p for p, c in facts['parent'] if c == x}
parents_y = {p for p, c in facts['parent'] if c == y}
return bool(parents_x & parents_y) # Non-empty intersection
# Grandparent(x, y) ≡ ∃z (Parent(x, z) ∧ Parent(z, y))
def grandparent(x, y, facts):
children_of_x = {c for p, c in facts['parent'] if p == x}
parents_of_y = {p for p, c in facts['parent'] if c == y}
return bool(children_of_x & parents_of_y)
# Uncle(x, y) ≡ Male(x) ∧ ∃z (Parent(z, y) ∧ Sibling(x, z))
def uncle(x, y, facts):
if x not in facts['male']:
return False
parents_of_y = {p for p, c in facts['parent'] if c == y}
return any(sibling(x, p, facts) for p in parents_of_y)
# Example usage:
facts = {
'parent': {('Alice', 'Bob'), ('Alice', 'Carol'), ('Dave', 'Bob'),
('Dave', 'Carol'), ('Bob', 'Eve'), ('Frank', 'Eve')},
'male': {'Dave', 'Bob', 'Frank'},
'female': {'Alice', 'Carol', 'Eve'}
}
print(father('Dave', 'Bob', facts)) # True
print(sibling('Bob', 'Carol', facts)) # True
print(grandparent('Alice', 'Eve', facts)) # True
print(uncle('Carol', 'Eve', facts)) # False (Carol is Female)
Part 4: Truth Table Solver — Solution
import itertools
import re
def parse_variables(formula):
"""Extract unique variable names (uppercase letters) from formula string."""
# Find all uppercase letter sequences not followed by lowercase (avoids keywords)
variables = sorted(set(re.findall(r'\b[A-Z][A-Z0-9]*\b', formula)))
return variables
def evaluate_formula(formula, assignment):
"""Evaluate a formula string given a dict of variable assignments."""
# Replace implication: A -> B becomes (not A or B)
# Handle -> before substituting variables
expr = formula
# Replace variable names with their truth values
for var, val in sorted(assignment.items(), key=lambda x: -len(x[0])):
expr = re.sub(r'\b' + var + r'\b', str(val), expr)
# Replace -> with Python-compatible form: (P -> Q) ≡ (not P or Q)
# Simple left-to-right replacement (works for non-nested single ->)
while '->' in expr:
expr = re.sub(r'(True|False|\([^()]+\))\s*->\s*(True|False|\([^()]+\))',
r'(not \1 or \2)', expr)
return eval(expr)
def generate_truth_table(formula):
"""Generate and print a truth table for the given formula."""
variables = parse_variables(formula)
n = len(variables)
# Header
header = ' | '.join(variables) + ' | ' + formula
print(header)
print('-' * len(header))
for values in itertools.product([True, False], repeat=n):
assignment = dict(zip(variables, values))
result = evaluate_formula(formula, assignment)
row_values = ' | '.join('T' if v else 'F' for v in values)
result_str = 'T' if result else 'F'
print(f"{row_values} | {result_str}")
# Test the solver
print("=== P -> Q ===")
generate_truth_table("P -> Q")
print("\n=== not P or Q ===")
generate_truth_table("not P or Q")
# Should match P -> Q exactly
print("\n=== P and Q ===")
generate_truth_table("P and Q")
Each puzzle in this lab exercised a different logical skill:
-
Knights and Knaves — translating self-referential English into propositional formulas and checking consistency
-
Zebra Puzzle — exhaustive search with constraint pruning (a taste of constraint satisfaction from Unit 4)
-
Family Relationships — defining derived predicates from base predicates in first-order logic
-
Truth Table Generator — building the evaluator that underlies every logical reasoner
Together, these skills form the practical toolkit for working with formal logic in software.
Lab code adapted from aima-python, MIT License, Copyright 2016 aima-python contributors.
This work is licensed under CC BY-SA 4.0.