diff --git a/ipynb/KenKen.ipynb b/ipynb/KenKen.ipynb
new file mode 100644
index 0000000..7377d8e
--- /dev/null
+++ b/ipynb/KenKen.ipynb
@@ -0,0 +1,993 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "
Peter Norvig
2016, 2021
\n",
+ "\n",
+ "# KenKen Puzzles\n",
+ "\n",
+ "[KenKen](https://en.wikipedia.org/wiki/KenKen) is a fill-in-the-grid puzzle game, similar to Sudoku (solved [in another notebook](Sudoku.ipynb)), but with arithmetic as well as logic. \n",
+ "\n",
+ "An example puzzle and solution:\n",
+ "\n",
+ "
\n",
+ "\n",
+ "The rules of KenKen are:\n",
+ "\n",
+ "- Every **square** of the *N*×*N* grid must be filled with a **digit** from 1 to *N*.\n",
+ "- No digit can appear twice in any **row** or **column**.\n",
+ "- Thick lines divide the grid into contiguous groups of squares called **cages**:\n",
+ " - Each cage is annotated with a **target number** and an arithmetic **operator**. For example:\n",
+ " - \"11 +\" means the digits in the cage must add to 11 (e.g. `5 + 6`).\n",
+ " - \"240 ×\" means the digits must form a product of 240 (e.g. `4 × 5 × 3 × 4`).\n",
+ "
(Two `4`s in a cage are ok if they are in different rows and columns.)\n",
+ " - \"2 ÷\" (or \"2 /\") means the digits, in some order, must form a quotient of 2 (e.g. `6 / 3` ).\n",
+ " - \"3 -\" means the digits, in some order, must form a difference of 3 (e.g. `4 - 1`).\n",
+ " - \"5 =\" means the cage must have one square, which is filled with `5`.\n",
+ " - Evaluation is always left to right: `6 - 3 - 2 - 1` = `(((6 - 3) - 2) - 1)` = `0`\n",
+ " \n",
+ " \n",
+ "# Strategy for Solving Puzzles\n",
+ "\n",
+ "The KenKen-solving program uses **constraint propagation** and **search**:\n",
+ " - For each square, keep track of the set of digits that might possibly fill the square.\n",
+ " - Use the constraints on rows, columns, and cages to eliminate some possible digits in some squares.\n",
+ " - **Constraints** on one square can **propagate** to other squares in the row, column, or cage.\n",
+ " - If that doesn't solve the puzzle, then **search**: pick a square and guess a digit to fill the square.\n",
+ " - Recursively search from there.\n",
+ " - If that leads to a solution, great; if it leads to a contradiction, back up and guess a different digit.\n",
+ " \n",
+ "\n",
+ "# Data Types\n",
+ "\n",
+ "Throughout the program the following variable names represent the following data types:\n",
+ "- `r` is a **row** label, e.g. `'A'`\n",
+ "- `c` is a **column** label, e.g. `'3'`\n",
+ "- `s` is a **square** label, e.g. `'A3'`\n",
+ "- `d` is a **digit**, e.g. `9`\n",
+ "- `u` is a **unit** (a row or column), e.g. `('A1', 'A2', 'A3', 'A4', 'A5', 'A6')`\n",
+ "- `cage` is a Cage, consisting of a **target number**, an **operator**, and a list of squares\n",
+ "- `values` is a dict of {square: set_of_possible_digits}, e.g. `{'A1': {5, 6}, 'B1': {6, 5}, ...}`\n",
+ " - A `values` dict is a **solution** when every square has been filled with a single digit, and they satisfy all the constraints.\n",
+ "- `kk` or `kenken` is a KenKen object, which describes the puzzle. It has these attributes:\n",
+ " - `.N`: the number of squares on a side\n",
+ " - `.digits`: the set of digits allowed: `{1, ..., N}`\n",
+ " - `.squares`: a set of all squares in the grid\n",
+ " - `.cages`: a list of all cages in the grid\n",
+ " - `.cagefor`: a dict where `.cagefor[s]` is the cage that square `s` is part of.\n",
+ " - `.unitsfor`: a dict where `.unitsfor[s]` is a tuple of the two units for square `s`.\n",
+ " - `.units`: a set of all units\n",
+ " - `.peers`: a dict where `.peers[s]` is a set of the `2 * (N - 1)` squares in the same column or row as `s`.\n",
+ "- `cage_descriptions` are strings, for example `\"11 + A1 B1\"` means a cage whose target is 11, whose operator is addition, and whose squares are `A1` and `B1`. A semicolon separates cage descriptions."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from typing import Dict, Set, List, Optional\n",
+ "from itertools import product as crossproduct\n",
+ "from collections import defaultdict\n",
+ "from operator import add, sub, mul, truediv\n",
+ "from dataclasses import dataclass\n",
+ "from functools import reduce"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "Square = str\n",
+ "Unit = tuple\n",
+ "Values = Dict[Square, Set[int]] # The possible values that fill in the squares\n",
+ "\n",
+ "class KenKen:\n",
+ " \"\"\"Describes a KenKen puzzle, but not the values used to fill in the puzzle squares.\"\"\"\n",
+ " def __init__(self, N, cage_descriptions, sep=\";\"):\n",
+ " self.N = N\n",
+ " self.digits = set(range(1, N + 1))\n",
+ " self.cols = '123456789'[:N]\n",
+ " self.rows = 'ABCDEFGHI'[:N]\n",
+ " self.squares = {r + c for r in self.rows for c in self.cols}\n",
+ " self.cages = [make_cage(description) for description in cage_descriptions.split(sep)]\n",
+ " self.cagefor = {s: first(cage for cage in self.cages if s in cage.squares)\n",
+ " for s in self.squares}\n",
+ " self.unitsfor= {s: (Unit(s[0]+c for c in self.cols), Unit(r+s[1] for r in self.rows))\n",
+ " for s in self.squares}\n",
+ " self.units = union(self.unitsfor.values())\n",
+ " self.peers = {s: union(self.unitsfor[s]) - {s}\n",
+ " for s in self.squares}\n",
+ " s1 = sorted(self.squares)\n",
+ " s2 = sorted(s for cage in self.cages for s in cage.squares)\n",
+ " assert s1 == s2, f\"Cage descriptions don't cover all squares:\\n{s1}\\n{s2}\"\n",
+ "\n",
+ "@dataclass\n",
+ "class Cage:\n",
+ " \"\"\"A collection of squares that must evaluate to `target` when `op` is applied to them.\"\"\"\n",
+ " target: int\n",
+ " op: str\n",
+ " squares: List[Square]\n",
+ " \n",
+ "def make_cage(description) -> Cage:\n",
+ " \"\"\"Make a cage from a description such as '3 + A1 A2'.\"\"\"\n",
+ " num, op, *squares = description.upper().split()\n",
+ " assert op in OPS\n",
+ " return Cage(int(num), op, squares)\n",
+ "\n",
+ "OPS = {'+': add, '-': sub, '×': mul, '*': mul, 'X': mul, '/': truediv, '÷': truediv, '=': add}\n",
+ " \n",
+ "def first(iterable) -> Optional[object]: return next(iter(iterable), None)\n",
+ "\n",
+ "def union(sets) -> set: return set().union(*sets)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Example Puzzle \n",
+ "\n",
+ "Let's make a `KenKen` object for the example puzzle (repeated here):\n",
+ "\n",
+ "
"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "kk6 = KenKen(6, \"\"\"\n",
+ " 11 + A1 B1; 2 / A2 A3; 20 × A4 B4; 6 × A5 A6 B6 C6; \n",
+ " 3 - B2 B3; 3 / B5 C5;\n",
+ " 240 × C1 C2 D1 D2; 6 × C3 C4; \n",
+ " 6 × D3 E3; 7 + D4 E4 E5; 30 × D5 D6; \n",
+ " 6 × E1 E2; 9 + E6 F6;\n",
+ " 8 + F1 F2 F3; 2 / F4 F5\"\"\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "{('A1', 'A2', 'A3', 'A4', 'A5', 'A6'),\n",
+ " ('A1', 'B1', 'C1', 'D1', 'E1', 'F1'),\n",
+ " ('A2', 'B2', 'C2', 'D2', 'E2', 'F2'),\n",
+ " ('A3', 'B3', 'C3', 'D3', 'E3', 'F3'),\n",
+ " ('A4', 'B4', 'C4', 'D4', 'E4', 'F4'),\n",
+ " ('A5', 'B5', 'C5', 'D5', 'E5', 'F5'),\n",
+ " ('A6', 'B6', 'C6', 'D6', 'E6', 'F6'),\n",
+ " ('B1', 'B2', 'B3', 'B4', 'B5', 'B6'),\n",
+ " ('C1', 'C2', 'C3', 'C4', 'C5', 'C6'),\n",
+ " ('D1', 'D2', 'D3', 'D4', 'D5', 'D6'),\n",
+ " ('E1', 'E2', 'E3', 'E4', 'E5', 'E6'),\n",
+ " ('F1', 'F2', 'F3', 'F4', 'F5', 'F6')}"
+ ]
+ },
+ "execution_count": 4,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "kk6.units"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "[Cage(target=11, op='+', squares=['A1', 'B1']),\n",
+ " Cage(target=2, op='/', squares=['A2', 'A3']),\n",
+ " Cage(target=20, op='×', squares=['A4', 'B4']),\n",
+ " Cage(target=6, op='×', squares=['A5', 'A6', 'B6', 'C6']),\n",
+ " Cage(target=3, op='-', squares=['B2', 'B3']),\n",
+ " Cage(target=3, op='/', squares=['B5', 'C5']),\n",
+ " Cage(target=240, op='×', squares=['C1', 'C2', 'D1', 'D2']),\n",
+ " Cage(target=6, op='×', squares=['C3', 'C4']),\n",
+ " Cage(target=6, op='×', squares=['D3', 'E3']),\n",
+ " Cage(target=7, op='+', squares=['D4', 'E4', 'E5']),\n",
+ " Cage(target=30, op='×', squares=['D5', 'D6']),\n",
+ " Cage(target=6, op='×', squares=['E1', 'E2']),\n",
+ " Cage(target=9, op='+', squares=['E6', 'F6']),\n",
+ " Cage(target=8, op='+', squares=['F1', 'F2', 'F3']),\n",
+ " Cage(target=2, op='/', squares=['F4', 'F5'])]"
+ ]
+ },
+ "execution_count": 5,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "kk6.cages"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "{'A2', 'A3', 'A4', 'A5', 'A6', 'B1', 'C1', 'D1', 'E1', 'F1'}"
+ ]
+ },
+ "execution_count": 6,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "kk6.peers['A1']"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "(('A1', 'A2', 'A3', 'A4', 'A5', 'A6'), ('A1', 'B1', 'C1', 'D1', 'E1', 'F1'))"
+ ]
+ },
+ "execution_count": 7,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "kk6.unitsfor['A1']"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Verifying a Solution\n",
+ "\n",
+ "A solution to a KenKen puzzle is a dict of the form `{square: {value}, ...}` such that:\n",
+ "- Every square has only a single possible value remaining.\n",
+ "- Every unit (row or column) has each of the digits 1 to *N* as values, once each.\n",
+ "- Every cage has a collection of values that, in some order, evaluates to the cage's target under the cage's operator."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 23,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def is_solution(values, kenken) -> bool:\n",
+ " \"\"\"Is `values` a solution to `kenken`?\"\"\"\n",
+ " def singleton(s) -> bool: return len(values[s]) == 1\n",
+ " def pandigits(u) -> bool: return union(values[s] for s in u) == kenken.digits\n",
+ " def cage_good(c) -> bool: return cage_solved(c, [first(values[s]) for s in c.squares])\n",
+ " return (all(singleton(s) for s in kenken.squares) and \n",
+ " all(pandigits(u) for u in kenken.units) and \n",
+ " all(cage_good(c) for c in kenken.cages))\n",
+ "\n",
+ "def cage_solved(cage, numbers) -> bool:\n",
+ " \"\"\"Do `numbers`, in some order, solve the equation in this cage?\"\"\"\n",
+ " op = OPS[cage.op]\n",
+ " return any(reduce(op, nums) == cage.target\n",
+ " for nums in orderings(op, numbers))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "What orderings of the numbers in a cage do we have to try? That depends on the operator:\n",
+ "- Addition and multiplication are commutative, so we only need to try one order:\n",
+ " - `1 + 2 + 3 + 4` is the same as `4 + 3 + 2 + 1` (or any other order).\n",
+ "- Subtraction and division are not commutative: \n",
+ " - `4 - 3 - 2 - 1` is different from `1 - 2 - 3 - 4`. \n",
+ "\n",
+ "Remember, the grouping is always left-to-right: `(((4 - 3) - 2) - 1)`.\n",
+ "\n",
+ "With *n* numbers in a subtraction or division cage you might think there are *n*! different possible results, but actually there are only *n*, because only the choice of which number goes first matters; any order of the remaining numbers gives the same result:\n",
+ " - `4 - 3 - 2 - 1` = `4 - (3 + 2 + 1)`, so all 6 permutations of `(3, 2, 1)` give the same result, `-2`.\n",
+ " - `3 - 4 - 2 - 1` = `3 - (4 + 2 + 1)`, so all 6 permutations of `(4, 2, 1)` give the same result, `-4`.\n",
+ " - `2 - 4 - 3 - 1` = `2 - (4 + 3 + 1)`, so all 6 permutations of `(4, 3, 1)` give the same result, `-6`.\n",
+ " - `1 - 4 - 3 - 2` = `1 - (4 + 3 + 2)`, so all 6 permutations of `(4, 3, 2)` give the same result, `-8`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def orderings(op, numbers: List[int]) -> List[List[int]]:\n",
+ " \"\"\"What orderings of `numbers` do we try with `op` to try to reach a target?\"\"\"\n",
+ " if op in {add, mul}: # commutative, only need one ordering\n",
+ " return [numbers] \n",
+ " else: # try all rotations, to put each number first in one of the orderings\n",
+ " return [numbers[i:] + numbers[:i] for i in range(len(numbers))]"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Constraint Propagation\n",
+ "\n",
+ "Similar to [the Sudoku program](Sudoku.ipynb), we propagate constraints with these two functions:\n",
+ "- `fill` fills in square *s* with digit *d*. It does this by eliminating all the other digits.\n",
+ "- `eliminate` eliminates a digit *d* as a possibility for a square *s*, and checks if there are other possible digits that are consistent with the other squares.\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def fill(kenken, values, s, d) -> Optional[Values]:\n",
+ " \"\"\"Eliminate all the other values (except d) from values[s] and propagate.\n",
+ " Return values, except return None if the eliminations are not possible.\"\"\"\n",
+ " ok = all(eliminate(kenken, values, s, d2) for d2 in values[s] if d2 != d)\n",
+ " return values if ok else None\n",
+ "\n",
+ "def eliminate(kk: KenKen, values, s, d) -> bool:\n",
+ " \"\"\"Eliminate d from values[s]; return True if all consistency checks are True.\"\"\"\n",
+ " if d not in values[s]:\n",
+ " return True ## Already eliminated\n",
+ " values[s] = values[s] - {d}\n",
+ " return (values[s] and\n",
+ " arc_consistent(kk, values, s) and \n",
+ " dual_consistent(kk, values, s, d) and \n",
+ " cage_consistent(kk, values, s))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The function `eliminate` removes the digit `d` from consideration for square `s`, and makes these checks for consistency, propagating constraints from squares to peers along the way:\n",
+ "\n",
+ "1. If a square *s* is reduced to one value *d2*, then eliminate *d2* from the peers.\n",
+ "2. If a unit *u* is reduced to only one place for a value *d*, then put it there.\n",
+ "3. If some value assignments are no longer valid within the cage for `s`, then eliminate them."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def arc_consistent(kk, values, s) -> bool:\n",
+ " \"\"\"If a square s is reduced to one value d2, then eliminate d2 from the peers.\"\"\"\n",
+ " [d2, *others] = values[s]\n",
+ " return others or all(eliminate(kk, values, s2, d2) for s2 in kk.peers[s])\n",
+ " \n",
+ "def dual_consistent(kk, values, s, d) -> bool:\n",
+ " \"\"\"If a unit u is reduced to only one place for a value d, then put it there.\"\"\"\n",
+ " def place_for_d(u) -> bool:\n",
+ " places = [s for s in u if d in values[s]]\n",
+ " return places and (len(places) > 1 or fill(kk, values, places[0], d))\n",
+ " return all(place_for_d(u) for u in kk.unitsfor[s])\n",
+ "\n",
+ "def cage_consistent(kk, values, s) -> bool:\n",
+ " \"\"\"Make sure that there is some assignment that satisfies the cage for square s,\n",
+ " and eliminate the values that are impossible.\"\"\"\n",
+ " cage = kk.cagefor[s]\n",
+ " possible = possible_cage_values(values, cage)\n",
+ " return (possible and\n",
+ " all(eliminate(kk, values, s1, d)\n",
+ " for s1 in cage.squares\n",
+ " for d in values[s1] if d not in possible[s1]))\n",
+ "\n",
+ "def possible_cage_values(values, cage) -> Values:\n",
+ " \"\"\"Return a dict of {s: possible_digits} for each square s in the cage.\"\"\"\n",
+ " possible = defaultdict(set)\n",
+ " for numbers in crossproduct(*[values[s] for s in cage.squares]):\n",
+ " if cage_solved(cage, numbers):\n",
+ " for (s, v) in zip(cage.squares, numbers):\n",
+ " possible[s].add(v)\n",
+ " return possible"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The initial dict of possible values is created by starting with the assumption that any square could be any digit, and then applying the cage constraints:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 12,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def initial_values(kenken) -> Values:\n",
+ " \"\"\"Assign initial values to each square, just considering cages, not rows/columns.\"\"\"\n",
+ " values = {s: kenken.digits for s in kenken.squares}\n",
+ " for cage in kenken.cages:\n",
+ " values.update(possible_cage_values(values, cage))\n",
+ " return values"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 13,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "{'B6': {1, 2, 3, 6},\n",
+ " 'B5': {1, 2, 3, 6},\n",
+ " 'C6': {1, 2, 3, 6},\n",
+ " 'D3': {1, 2, 3, 6},\n",
+ " 'A6': {1, 2, 3, 6},\n",
+ " 'D6': {5, 6},\n",
+ " 'E2': {1, 2, 3, 6},\n",
+ " 'D4': {1, 2, 3, 4, 5},\n",
+ " 'E3': {1, 2, 3, 6},\n",
+ " 'A3': {1, 2, 3, 4, 6},\n",
+ " 'A5': {1, 2, 3, 6},\n",
+ " 'F5': {1, 2, 3, 4, 6},\n",
+ " 'F1': {1, 2, 3, 4, 5, 6},\n",
+ " 'A2': {1, 2, 3, 4, 6},\n",
+ " 'E5': {1, 2, 3, 4, 5},\n",
+ " 'F4': {1, 2, 3, 4, 6},\n",
+ " 'B3': {1, 2, 3, 4, 5, 6},\n",
+ " 'D2': {2, 3, 4, 5, 6},\n",
+ " 'B2': {1, 2, 3, 4, 5, 6},\n",
+ " 'F6': {3, 4, 5, 6},\n",
+ " 'C1': {2, 3, 4, 5, 6},\n",
+ " 'F2': {1, 2, 3, 4, 5, 6},\n",
+ " 'B4': {4, 5},\n",
+ " 'C3': {1, 2, 3, 6},\n",
+ " 'E4': {1, 2, 3, 4, 5},\n",
+ " 'C2': {2, 3, 4, 5, 6},\n",
+ " 'A1': {5, 6},\n",
+ " 'F3': {1, 2, 3, 4, 5, 6},\n",
+ " 'E1': {1, 2, 3, 6},\n",
+ " 'A4': {4, 5},\n",
+ " 'D1': {2, 3, 4, 5, 6},\n",
+ " 'D5': {5, 6},\n",
+ " 'B1': {5, 6},\n",
+ " 'E6': {3, 4, 5, 6},\n",
+ " 'C4': {1, 2, 3, 6},\n",
+ " 'C5': {1, 2, 3, 6}}"
+ ]
+ },
+ "execution_count": 13,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "values = initial_values(kk6)\n",
+ "values"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We see, for example, that the two squares in the \"11 +\" cage, `A1` and `B1`, must be either 5 or 6, because no other two digits sum to 11:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 14,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "Cage(target=11, op='+', squares=['A1', 'B1'])"
+ ]
+ },
+ "execution_count": 14,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "cage = kk6.cagefor['A1']\n",
+ "cage "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 15,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "assert possible_cage_values(values, cage) == {'A1': {5, 6}, 'B1': {5, 6}}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Search\n",
+ "\n",
+ "We use depth-first-search to find a solution:\n",
+ "- If all squares have exactly one possible value, that's a solution.\n",
+ "- Otherwise, choose an unfilled square *s* with the minimum number of possible values.\n",
+ "- For each possible value *d* for square *s*, try filling *s* with *d* and recursively searching for a solution.\n",
+ " - Recursive calls use `values.copy()`, so that if we have to back up, `values` is unchanged. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 16,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def solve(kenken) -> Values:\n",
+ " \"\"\"Solve a KenKen puzzle with search, assert the solution is valid, and return the solution.\"\"\"\n",
+ " values = search(kenken, initial_values(kenken))\n",
+ " assert is_solution(values, kenken)\n",
+ " return values\n",
+ "\n",
+ "def search(kenken, values) -> Optional[Values]:\n",
+ " \"\"\"Using depth-first search and constraint propagation, find values that solve puzzle.\"\"\"\n",
+ " if values is None:\n",
+ " return None ## Failed earlier\n",
+ " if all(len(values[s]) == 1 for s in kenken.squares):\n",
+ " assert is_solution(values, kenken)\n",
+ " return values ## Solved!\n",
+ " ## Chose the unfilled square s with the fewest possibilities and try each possible value for s\n",
+ " s = min((s for s in kenken.squares if len(values[s]) > 1), key=lambda s: len(values[s]))\n",
+ " return first_true(search(kenken, fill(kenken, values.copy(), s, d))\n",
+ " for d in values[s])\n",
+ "\n",
+ "def first_true(iterable): return first(x for x in iterable if x)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We can see that search solves the example puzzle, finding a single value for every square:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 17,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "{'B6': {3},\n",
+ " 'B5': {2},\n",
+ " 'C6': {1},\n",
+ " 'D3': {1},\n",
+ " 'A6': {2},\n",
+ " 'D6': {6},\n",
+ " 'E2': {3},\n",
+ " 'D4': {2},\n",
+ " 'E3': {6},\n",
+ " 'A3': {3},\n",
+ " 'A5': {1},\n",
+ " 'F5': {3},\n",
+ " 'F1': {1},\n",
+ " 'A2': {6},\n",
+ " 'E5': {4},\n",
+ " 'F4': {6},\n",
+ " 'B3': {4},\n",
+ " 'D2': {4},\n",
+ " 'B2': {1},\n",
+ " 'F6': {4},\n",
+ " 'C1': {4},\n",
+ " 'F2': {2},\n",
+ " 'B4': {5},\n",
+ " 'C3': {2},\n",
+ " 'E4': {1},\n",
+ " 'C2': {5},\n",
+ " 'A1': {5},\n",
+ " 'F3': {5},\n",
+ " 'E1': {2},\n",
+ " 'A4': {4},\n",
+ " 'D1': {3},\n",
+ " 'D5': {5},\n",
+ " 'B1': {6},\n",
+ " 'E6': {5},\n",
+ " 'C4': {3},\n",
+ " 'C5': {6}}"
+ ]
+ },
+ "execution_count": 17,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "solve(kk6)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Display\n",
+ "\n",
+ "The `values` dict is not a visually pleasing portrayal of the solution. Let's develop a better one, using the `HTML` and `display` functions from the `IPython.core.display` module:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 18,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from IPython.core.display import HTML, display\n",
+ "\n",
+ "def show(kenken, values=None):\n",
+ " \"\"\"Display a KenKen puzzle, with the values filled in.\"\"\"\n",
+ " if values is None:\n",
+ " values = solve(kenken)\n",
+ " result = ['\\n']\n",
+ " for r in kenken.rows:\n",
+ " result += ['']\n",
+ " for c in kenken.cols:\n",
+ " s = r + c\n",
+ " result += [cell(kenken, s, values.get(s, set(['X'])))]\n",
+ " result += ['
']\n",
+ " return display(HTML(''.join(result)))\n",
+ "\n",
+ "def cell(kenken, s, val) -> str:\n",
+ " \"\"\"HTML for a single table cell; if it is the first cell in a cage, show target/op.\n",
+ " Make a thick border with every neighboring cell that is not in the same cage.\"\"\"\n",
+ " cage = kenken.cagefor[s]\n",
+ " T, R, B, L = [1 if same_cage(kenken, s, s2) else 8 for s2 in neighbors(s)]\n",
+ " borders = f\"border: solid black; border-width: {T}px {R}px {B}px {L}px;\"\n",
+ " header = f'{cage.target}{cage.op}' if s == min(cage.squares) else ''\n",
+ " nums = ''.join(map(str, val))\n",
+ " return f'{header} {nums} | ' \n",
+ "\n",
+ "def neighbors(s: Square) -> List[Square]: \n",
+ " \"\"\"The neighboring squares in [top, right, bottom, left] directions.\"\"\"\n",
+ " r, c = map(ord, s)\n",
+ " return [chr(r + Δr) + chr(c + Δc)\n",
+ " for (Δr, Δc) in ((-1, 0), (0, 1), (1, 0), (0, -1))]\n",
+ "\n",
+ "def same_cage(kenken, s1, s2) -> bool:\n",
+ " \"\"\"Are squares s1 and s2 in the same cage in kenken?\"\"\"\n",
+ " return kenken.cagefor.get(s1) == kenken.cagefor.get(s2)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 19,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "11+ 5 | 2/ 6 | 3 | 20× 4 | 6× 1 | 2 |
6 | 3- 1 | 4 | 5 | 3/ 2 | 3 |
240× 4 | 5 | 6× 2 | 3 | 6 | 1 |
3 | 4 | 6× 1 | 7+ 2 | 30× 5 | 6 |
6× 2 | 3 | 6 | 1 | 4 | 9+ 5 |
8+ 1 | 2 | 5 | 2/ 6 | 3 | 4 |
"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "show(kk6)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "That looks much better!\n",
+ "\n",
+ "# More Puzzles\n",
+ "\n",
+ "We can solve more puzzles. Here is a list of ten puzzles of various sizes:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 20,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "kenkens = [\n",
+ " KenKen(4, \"7 + A1 B1; 2 / C1 D1; 1 - A2 A3; 3 - B2 B3; 2 / A4 B4; 3 = C2; 12 × C3 C4 D4; 2 / D2 D3\"),\n",
+ " KenKen(4, \"1 - a1 a2; 2 / a3 b3; 5 + a4 b4; 2 / b1 c1; 5 + b2 c2; 1 - c3 d3; 6 x c4 d4; 4 x d1 d2\"),\n",
+ " KenKen(4, \"48 x a1 a2 a3 b1; 5 + a4 b4; 2 / b2 c2; 12 x b3 c3; 5 + c1 d1; 2 - d2 d3; 1 - c4 d4\"),\n",
+ " KenKen(5, \"\"\"12 × a1 b1; 2 - a2 b2; 4 - a3 a4; 2 / a5 b5; 2 / b3 b4; 2 / c1 c2; 15 + c3 c4 c5 d3;\n",
+ " 3 - d1 e1; 16 × d2 e2 e3; 1 - d4 e4; 2 - d5 e5\"\"\"),\n",
+ " KenKen(5, \"\"\"8 + a1 b1 c1; 2 - a2 a3; 4 × a4 a5 b5; 2 / b2 b3; 1 - b4 c4; 3 = c5;\n",
+ " 4 - c2 d2; 10 + c3 d3 d4; 48 × d1 e1 e2; 2 / e3 e4; 3 - d5 e5\"\"\"),\n",
+ " KenKen(5, \"\"\"3 - a1 a2; 1 - a3 a4; 15 × a5 b5 b4; 12 × b1 b2 c2; 10 + b3 c3 d3;\n",
+ " 1 = c1; 2 / c4 c5; 2 / d1 d2; 20 × d4 d5 e5; 8 + e1 e2; 3 + e3 e4\"\"\"),\n",
+ " kk6,\n",
+ " KenKen(7, \"\"\"3 - a1 b1; 108 × a2 a3 b3; 13 + a4 b4 b5; 2 / a5 a6; 13 + a7 b6 b7;\n",
+ " 3 - b2 c2; 70 × c1 d1 e1; 5 = d2; 504 × c3 c4 d3 e3 e4; 60 × c5 d4 d5 e5;\n",
+ " 4 - c6 c7; 1 - d6 d7; 6 - e6 e7; 2 / f1 g1; 2 / g2 g3; 30 × e2 f2 f3; \n",
+ " 140 × f4 f5 g4; 1 - g5 g6; 14 + f6 f7 g7\"\"\"),\n",
+ " KenKen(7, \"\"\"3 = a1; 15 + a2 a3 b3; 12 + a4 a5 a6; 1 - a7 b7;\n",
+ " 6 / b1 b2; 7 + b4 b5; 7 = b6; 8 × c1 c2; 2 / c3 c4; 35 × c5 c6 c7;\n",
+ " 11 + d1 e1 e2; 5 - d2 d3; 3 / d4 e4; 30 × d5 d6; 9 + d7 e7; 2 - e5 e6;\n",
+ " 8 + e3 f3 g3; 24 × f1 f2; 35 × g1 g2; 10 + f4 f5; 2 × f6 f7; 6 + g4 g5; 3 - g6 g7\"\"\"),\n",
+ " KenKen(8, \"\"\"3 / a1 a2; 9 + a3 a4; 210 × a5 b5 c5 b4 c4; 19 + a6 b6 c6; 90 × a7 a8 b7;\n",
+ " 10 × b1 b2; 8 - b3; 4 / b8 c8;\n",
+ " 7 + c1 d1; 7 = c2; 1 - c3 d3; 4 = c7; 2 / d2 e2; 17 + d4 e4 f4; 6 / d5 d6; 6 × d7 d8;\n",
+ " 16 + e1 f1 f2 g2; 7 = e3; 5 - e5 e6; 12 × e7 e8 f8; 7 + f3 g3; 80 × f5 f6 f7;\n",
+ " 8 / g1 h1; 4 - h2 h3; 1 - g4 h4; 15 + g5 h5 h6; 1 = g6; 12 + g7 h7; 1 - g8 h8\"\"\")\n",
+ "]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 21,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "7+ 4 | 1- 2 | 3 | 2/ 1 |
3 | 3- 1 | 4 | 2 |
2/ 2 | 3= 3 | 12× 1 | 4 |
1 | 2/ 4 | 2 | 3 |
"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "1- 3 | 4 | 2/ 2 | 5+ 1 |
2/ 2 | 5+ 3 | 1 | 4 |
1 | 2 | 1- 4 | 6X 3 |
4X 4 | 1 | 3 | 2 |
"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "48X 3 | 4 | 2 | 5+ 1 |
2 | 2/ 1 | 12X 3 | 4 |
5+ 1 | 2 | 4 | 1- 3 |
4 | 2- 3 | 1 | 2 |
"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "12× 4 | 2- 3 | 4- 1 | 5 | 2/ 2 |
3 | 5 | 2/ 2 | 4 | 1 |
2/ 1 | 2 | 15+ 5 | 3 | 4 |
3- 2 | 16× 4 | 3 | 1- 1 | 2- 5 |
5 | 1 | 4 | 2 | 3 |
"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "8+ 2 | 2- 3 | 5 | 4× 1 | 4 |
5 | 2/ 2 | 4 | 1- 3 | 1 |
1 | 4- 5 | 10+ 2 | 4 | 3= 3 |
48× 4 | 1 | 3 | 5 | 3- 2 |
3 | 4 | 2/ 1 | 2 | 5 |
"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "3- 5 | 2 | 1- 4 | 3 | 15× 1 |
12× 4 | 1 | 10+ 2 | 5 | 3 |
1= 1 | 3 | 5 | 2/ 4 | 2 |
2/ 2 | 4 | 3 | 20× 1 | 5 |
8+ 3 | 5 | 3+ 1 | 2 | 4 |
"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "11+ 5 | 2/ 6 | 3 | 20× 4 | 6× 1 | 2 |
6 | 3- 1 | 4 | 5 | 3/ 2 | 3 |
240× 4 | 5 | 6× 2 | 3 | 6 | 1 |
3 | 4 | 6× 1 | 7+ 2 | 30× 5 | 6 |
6× 2 | 3 | 6 | 1 | 4 | 9+ 5 |
8+ 1 | 2 | 5 | 2/ 6 | 3 | 4 |
"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "3- 4 | 108× 6 | 3 | 13+ 7 | 2/ 1 | 2 | 13+ 5 |
1 | 3- 7 | 6 | 2 | 4 | 5 | 3 |
70× 7 | 4 | 504× 1 | 3 | 60× 5 | 4- 6 | 2 |
2 | 5= 5 | 7 | 1 | 6 | 1- 3 | 4 |
5 | 30× 3 | 4 | 6 | 2 | 6- 7 | 1 |
2/ 3 | 2 | 5 | 140× 4 | 7 | 14+ 1 | 6 |
6 | 2/ 1 | 2 | 5 | 1- 3 | 4 | 7 |
"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "3= 3 | 15+ 5 | 6 | 12+ 4 | 7 | 1 | 1- 2 |
6/ 6 | 1 | 4 | 7+ 5 | 2 | 7= 7 | 3 |
8× 2 | 4 | 2/ 3 | 6 | 35× 1 | 5 | 7 |
11+ 1 | 5- 2 | 7 | 3/ 3 | 30× 5 | 6 | 9+ 4 |
7 | 3 | 8+ 2 | 1 | 2- 6 | 4 | 5 |
24× 4 | 6 | 5 | 10+ 7 | 3 | 2× 2 | 1 |
35× 5 | 7 | 1 | 6+ 2 | 4 | 3- 3 | 6 |
"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "3/ 6 | 2 | 9+ 1 | 8 | 210× 7 | 19+ 4 | 90× 3 | 5 |
10× 2 | 5 | 8- 8 | 1 | 3 | 7 | 6 | 4/ 4 |
7+ 3 | 7= 7 | 1- 6 | 5 | 2 | 8 | 4= 4 | 1 |
4 | 2/ 8 | 5 | 17+ 7 | 6/ 1 | 6 | 6× 2 | 3 |
16+ 5 | 4 | 7= 7 | 6 | 5- 8 | 3 | 12× 1 | 2 |
7 | 1 | 7+ 3 | 4 | 80× 5 | 2 | 8 | 6 |
8/ 8 | 3 | 4 | 1- 2 | 15+ 6 | 1= 1 | 12+ 5 | 1- 7 |
1 | 4- 6 | 2 | 3 | 4 | 5 | 7 | 8 |
"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "CPU times: user 240 ms, sys: 4.26 ms, total: 244 ms\n",
+ "Wall time: 243 ms\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%time\n",
+ "for kk in kenkens:\n",
+ " show(kk)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "It took only about 1/4 second to correctly solve (and print) all 10 puzzles. That's not bad, but it is a lot slower than the [Sudoku solver](SudokuJava.ipynb). I think part of the problem is that the code to handle cages could be more efficient; it could cache results instead of recomputing them, and it could consider the possible values for all the squares in a cage together, rather than just consider the values for each square independently. For example, in a 7×7 grid with a four-square \"105 ×\" cage, `possible_cage_values` computes that each square must be one of `{1, 2, 3, 5, 6, 7}`, but it would be better to make use of the fact that the four squares must be either `{1, 5, 6, 7}` (in some order) or `{2, 3, 5, 7}`.\n",
+ "\n",
+ "# Tests\n",
+ "\n",
+ "Here are some unit tests to give partial code coverage, and to show how the subfunctions work:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 22,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "True"
+ ]
+ },
+ "execution_count": 22,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "def tests():\n",
+ " \"\"\"Unit tests.\"\"\"\n",
+ " for kk in kenkens:\n",
+ " assert len(kk.units) == 2 * kk.N\n",
+ " assert len(kk.peers) -- kk.N ** 2\n",
+ " assert all(len(kk.peers[s]) == 2 * kk.N - 2 for s in kk.squares)\n",
+ " \n",
+ " kk4 = kenkens[0]\n",
+ " soln = {'A1': {4}, 'A2': {2}, 'A3': {3}, 'A4': {1}, \n",
+ " 'B1': {3}, 'B2': {1}, 'B3': {4}, 'B4': {2}, \n",
+ " 'C1': {2}, 'C2': {3}, 'C3': {1}, 'C4': {4}, \n",
+ " 'D1': {1}, 'D2': {4}, 'D3': {2}, 'D4': {3}}\n",
+ " assert solve(kk4) == soln\n",
+ " assert is_solution(soln, kk4)\n",
+ " \n",
+ " cage = kk6.cagefor['D4']\n",
+ " assert cage == Cage(target=7, op='+', squares=['D4', 'E4', 'E5'])\n",
+ " assert cage == make_cage('7 + d4 e4 e5')\n",
+ " assert cage_solved(cage, [2, 1, 4])\n",
+ " assert not cage_solved(cage, [2, 1, 3])\n",
+ " \n",
+ " assert orderings(add, [2, 1, 4]) == [[2, 1, 4]]\n",
+ " assert orderings(sub, [2, 1, 4]) == [[2, 1, 4], [1, 4, 2], [4, 2, 1]]\n",
+ " \n",
+ " vals = {'D4': kk6.digits, 'E4': kk6.digits, 'E5': kk6.digits}\n",
+ " assert possible_cage_values(vals, cage) == {\n",
+ " 'D4': {1, 2, 3, 4, 5},\n",
+ " 'E4': {1, 2, 3, 4, 5},\n",
+ " 'E5': {1, 2, 3, 4, 5}}\n",
+ " \n",
+ " assert first([0, 1, 2, 3]) == 0\n",
+ " assert first({3}) == 3\n",
+ " \n",
+ " assert first_true([0, '', False, 4]) == 4\n",
+ " assert first_true([0, '', False]) == None\n",
+ " \n",
+ " assert union([{1, 2, 3}, {3, 4, 5}]) == {1, 2, 3, 4, 5}\n",
+ " \n",
+ " assert neighbors('B2') == ['A2', 'B3', 'C2', 'B1']\n",
+ " \n",
+ " assert same_cage(kk6, 'A1', 'B1')\n",
+ " assert not same_cage(kk6, 'A1', 'A2')\n",
+ "\n",
+ " return True\n",
+ "\n",
+ "tests()"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.7.6"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}