From 8829e7778c334bfd83ab09e9e27198a6a7bd3fa9 Mon Sep 17 00:00:00 2001 From: Peter Norvig Date: Thu, 18 Feb 2021 20:47:53 -0800 Subject: [PATCH] Add files via upload --- ipynb/CrossProduct.ipynb | 625 +++++++++++++++++++++++++++++++-------- 1 file changed, 498 insertions(+), 127 deletions(-) diff --git a/ipynb/CrossProduct.ipynb b/ipynb/CrossProduct.ipynb index b70aed7..9e85659 100644 --- a/ipynb/CrossProduct.ipynb +++ b/ipynb/CrossProduct.ipynb @@ -8,48 +8,42 @@ "\n", "# CrossProduct Puzzle\n", "\n", - "The 538 Riddler [poses a puzzle](https://fivethirtyeight.com/features/can-you-cross-like-a-boss/) called ***CrossProduct***, which works like this:\n", - "> *Fill in each empty cell of the table with a single digit, 1—9, so that the product of the digits in each row equals the number to the right of the row, and the product of the digits in each column equals the number below the column.*\n", + "The 538 Riddler [poses a type of puzzle](https://fivethirtyeight.com/features/can-you-cross-like-a-boss/) called ***CrossProduct***, which works like this:\n", + "> *Replace each \"?\" in the table with a single digit so that the product of the digits in each row equals the number to the right of the row, and the product of the digits in each column equals the number above the column.*\n", + "> \n", + "> | 6615 | 15552 |   420 | |\n", + "|-------|-------|-------|---|\n", + "| ? | ? | ? |**210**|\n", + "| ? | ? | ? |**144**|\n", + "| ? | ? | ? |**54**|\n", + "| ? | ? | ? |**135**|\n", + "| ? | ? | ? |**4**|\n", + "| ? | ? | ? |**49**|\n", + ">\n", + "> *This is the solution:*\n", + ">\n", + ">|6615|15552|  420||\n", + "|---|---|---|---|\n", + "|7|6|5|**210**|\n", + "|9|8|2|**144**|\n", + "|3|9|2|**54**|\n", + "|5|9|3|**135**|\n", + "|1|4|1|**4**|\n", + "|7|1|7|**49**|\n", + "\n", + "\n", + "\n", "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
210
144
54
135
4
49
6,61515,552420
\n", "\n", " \n", - "# Data Type definitions\n", + "# Data type definitions\n", " \n", "Here are the data types we will use in trying to solve CrossProduct puzzles: \n", + "- `Digit`: a single digit, from 1 to 9 (but not 0).\n", "- `Row`: a sequence of digits that forms a row in the table, e.g. `(7, 6, 5)`.\n", - "- `Table`: a filled-in table (but not the row and column products): a list of rows, e.g. `[(7, 6, 5), (9, 8, 2), ...]`.\n", - "- `Products`: a list of the numbers that corresponding digits must multiply to, e.g. in the puzzle above, `[210, 144, 54, 135, 4, 49]` for the row products, and `[6615, 15552, 420]` for the column products." + "- `Table`: a table of digits that fill in for the \"?\"s; a list of rows, e.g. `[(7, 6, 5), (9, 8, 2), ...]`.\n", + "- `Products`: a list of the numbers that corresponding digits must multiply to, e.g. in the puzzle above, `[6615, 15552, 420]` for the column products, and `[210, 144, 54, 135, 4, 49]` for the row products.\n", + "- `Puzzle`: a puzzle to be solved, as defined by the row products and column products." ] }, { @@ -59,21 +53,24 @@ "outputs": [], "source": [ "from typing import Tuple, List, Set, Iterable, Optional\n", + "from numpy import divide, prod, transpose\n", + "from random import randint\n", + "from collections import namedtuple\n", "\n", - "Row = Tuple[int, ...] \n", + "Digit = int\n", + "Row = Tuple[Digit, ...] \n", "Table = List[Row] \n", - "Products = List[int] " + "Products = List[int] \n", + "Puzzle = namedtuple('Puzzle', 'row_prods, col_prods')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "# Filling in one row\n", + "# The puzzles\n", "\n", - "A first step in solving the puzzle is filling in a single row of the table.\n", - "
`fill_row(n, k)` will return the set of all `k`-digit tuples whose product is `n`.\n", - "
By default there are 3 digits in a row, but any number of digits is allowed." + "Here are the puzzles given by 538 Riddler (they promised one a week for four weeks):" ] }, { @@ -82,14 +79,34 @@ "metadata": {}, "outputs": [], "source": [ - "def fill_row(n, k=3) -> Set[Row]:\n", - " \"All permutations of k digits that multiply to n.\"\n", - " if k == 0:\n", - " return {()} if n == 1 else set()\n", - " else:\n", - " return {(d, *rest) for d in range(1, 10)\n", - " if (n / d).is_integer()\n", - " for rest in fill_row(n // d, k - 1)}" + "puzzles = (Puzzle([135, 45, 64, 280, 70], [3000, 3969, 640]),\n", + " Puzzle([210, 144, 54, 135, 4, 49], [6615, 15552, 420]))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Filling in one row\n", + "\n", + "- A first step in solving the puzzle is filling in a single row of the table.\n", + "- `fill_row(product, k)` will return the set of all `k`-digit tuples whose product is `product`.\n", + "- In the non-trivial case, pair every first digit, `d`, that divides the product with every way of filling the rest of the row:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "def fill_row(product, k=3) -> Set[Row]:\n", + " \"All permutations of `k` digits that multiply to `product`.\"\n", + " return ({()} if k == 0 and product == 1 else\n", + " set() if k == 0 and product != 1 else\n", + " {(d, *rest) for d in range(1, 10)\n", + " if (product / d).is_integer()\n", + " for rest in fill_row(product // d, k - 1)})" ] }, { @@ -101,25 +118,47 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 4, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "{(5, 6, 7), (5, 7, 6), (6, 5, 7), (6, 7, 5), (7, 5, 6), (7, 6, 5)}" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "fill_row(210)" ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 5, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "{(9, 9, 9)}" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "fill_row(729)" ] }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 6, "metadata": {}, "outputs": [ { @@ -137,7 +176,7 @@ " (9, 9, 9, 1)}" ] }, - "execution_count": 5, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } @@ -148,65 +187,63 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{(1, 5, 5, 5, 5, 5),\n", - " (5, 1, 5, 5, 5, 5),\n", - " (5, 5, 1, 5, 5, 5),\n", - " (5, 5, 5, 1, 5, 5),\n", - " (5, 5, 5, 5, 1, 5),\n", - " (5, 5, 5, 5, 5, 1)}" + "{(1, 7, 7, 7, 7, 7),\n", + " (7, 1, 7, 7, 7, 7),\n", + " (7, 7, 1, 7, 7, 7),\n", + " (7, 7, 7, 1, 7, 7),\n", + " (7, 7, 7, 7, 1, 7),\n", + " (7, 7, 7, 7, 7, 1)}" ] }, - "execution_count": 6, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "fill_row(5**5, 6)" + "fill_row(7**5, 6)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "# Solving the whole puzzle\n", + "# Solving a whole puzzle\n", "\n", - "We can now solve the whole puzzle with a simple brute-force strategy: for every possible way of filling the first row, try every way of recursively solving the rest of the puzzle, and yield a table for each way that works. We'll define the function `solutions` to yield all possible solutions, and `solve` to find just the first solution. (A well-formed puzzle will have exactly one solution, but some puzzles might have no solution, or multiple solutions.)\n", - "\n", - "The function `solutions` has three main cases to consider:\n", - "- Any puzzle with a non-integer column product is unsolvable. Return without yielding anything.\n", - "- The empty puzzle (with no rows) has the empty table, `[]`, as a solution, as long as the column products are all 1. \n", - "- In the general case, call `fill_row` to get all possible ways to fill the first row, and recursively call `solutions` to get all the possible ways of filling the rest of the rows (making sure to pass in an altered `col_prods` that accounts for the first row). " + "- We can now solve a whole puzzle with a simple brute-force strategy:\n", + "- For every possible way of filling the first row, try every way of recursively solving the rest of the puzzle. \n", + "- `solve` finds the first solution to a puzzle. (A well-formed puzzle has exactly one solution, but some might have more or less.)\n", + "- `solutions` yields all possible solutions to a puzzle. There are three main cases to consider:\n", + " - A puzzle with no rows has the empty table, `[]`, as a solution, as long as the column products are all 1.\n", + " - A puzzle with rows might have solutions, as long as the column products are all integers. Call `fill_row` to get all possible ways to fill the first row, and for each one recursively call `solutions` to get all the possible ways of filling the rest of the rows (making sure to pass in an altered `col_prods` where each element is divided by the corresponding element in the first row).\n", + " - Otherwise there are no solutions." ] }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 8, "metadata": {}, "outputs": [], "source": [ - "def solutions(row_prods: Products, col_prods: Products) -> Iterable[Table]:\n", - " \"\"\"Yield table(s) that solve the puzzle.\n", + "def solve(puzzle) -> Optional[Table]: return next(solutions(puzzle), None)\n", + "\n", + "def solutions(puzzle) -> Iterable[Table]:\n", + " \"\"\"Yield all tables that solve the puzzle.\n", " The product of the digits in row r must equal row_prods[r], for all r.\n", " The product of the digits in column c must equal col_prods[c], for all c.\"\"\"\n", - " if not all(c == int(c) for c in col_prods):\n", - " return\n", + " row_prods, col_prods = puzzle\n", " if not row_prods and all(c == 1 for c in col_prods):\n", " yield []\n", - " elif row_prods:\n", + " elif row_prods and all(c == int(c) for c in col_prods):\n", " for row1 in fill_row(row_prods[0], len(col_prods)):\n", - " for rows in solutions(row_prods[1:], divide(col_prods, row1)):\n", - " yield [row1, *rows]\n", - " \n", - "def solve(r, c) -> Optional[Table]: return next(solutions(r, c), None)\n", - " \n", - "def divide(A, B) -> List[float]: return [a / b for a, b in zip(A, B)]" + " for rows in solutions(Puzzle(row_prods[1:], divide(col_prods, row1))):\n", + " yield [row1, *rows]" ] }, { @@ -218,26 +255,6 @@ "Here are solutions to the puzzles posed by *The Riddler*:" ] }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[(3, 9, 5), (5, 9, 1), (8, 1, 8), (5, 7, 8), (5, 7, 2)]" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "solve([135, 45, 64, 280, 70], [3000, 3969, 640])" - ] - }, { "cell_type": "code", "execution_count": 9, @@ -246,7 +263,8 @@ { "data": { "text/plain": [ - "[(7, 6, 5), (9, 8, 2), (3, 9, 2), (5, 9, 3), (1, 4, 1), (7, 1, 7)]" + "[[(3, 9, 5), (5, 9, 1), (8, 1, 8), (5, 7, 8), (5, 7, 2)],\n", + " [(7, 6, 5), (9, 8, 2), (3, 9, 2), (5, 9, 3), (1, 4, 1), (7, 1, 7)]]" ] }, "execution_count": 9, @@ -255,8 +273,349 @@ } ], "source": [ - "# The puzzle that appears at the top of this notebook\n", - "solve([210, 144, 54, 135, 4, 49], [6615, 15552, 420])" + "[solve(p) for p in puzzles]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Those are the correct solutions. However, we could make the solutions prettier:" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "from IPython.display import Markdown\n", + "\n", + "def pretty(puzzle, table=None) -> str:\n", + " \"\"\"A puzzle and the filled-in table as a str that will be pretty in Markdown.\"\"\"\n", + " row_prods, col_prods = puzzle\n", + " table = table or solve(puzzle)\n", + " head = surround(col_prods + [''])\n", + " dash = surround(['---'] * (1 + len(col_prods)))\n", + " rest = [surround(row + (f'**{rp}**',))\n", + " for row, rp in zip(table, row_prods)]\n", + " return '\\n'.join([head, dash, *rest])\n", + "\n", + "def surround(items, delim='|') -> str: \n", + " \"\"\"Like str.join, but delimiter is outside items as well as between.\"\"\"\n", + " return delim + delim.join(map(str, items)) + delim" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/markdown": [ + "|3000|3969|640||\n", + "|---|---|---|---|\n", + "|3|9|5|**135**|\n", + "|5|9|1|**45**|\n", + "|8|1|8|**64**|\n", + "|5|7|8|**280**|\n", + "|5|7|2|**70**|\n", + "\n", + "|6615|15552|420||\n", + "|---|---|---|---|\n", + "|7|6|5|**210**|\n", + "|9|8|2|**144**|\n", + "|3|9|2|**54**|\n", + "|5|9|3|**135**|\n", + "|1|4|1|**4**|\n", + "|7|1|7|**49**|" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Markdown('\\n\\n'.join(map(pretty, puzzles)))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Making new well-formed puzzles\n", + "\n", + "Can we make new well-formed puzzles (those with exactly one solution)? One approach is to:\n", + "- Make a table filled with random digits (`random_table`).\n", + "- Make a puzzle from the row and column products of the table (`table_puzzle`).\n", + "- Check if each puzzle is `well-formed` (that is, has a single solutions).\n", + "- Repeat `ntables` times (`random_puzzles`)." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "def random_table(nrows, ncols) -> Table:\n", + " \"Make a table of random digits of the given size.\"\n", + " return [tuple(randint(1, 9) for c in range(ncols))\n", + " for r in range(nrows)]\n", + "\n", + "def table_puzzle(table) -> Puzzle:\n", + " \"Given a table, compute the puzzle it is a solution for.\"\n", + " return Puzzle([prod(row) for row in table], \n", + " [prod(col) for col in transpose(table)])\n", + "\n", + "def well_formed(puzzle) -> bool: \n", + " \"Does the puzzle have exactly one solution?\"\n", + " S = solutions(puzzle)\n", + " first, second = next(S, None), next(S, None)\n", + " return first is not None and second is None\n", + "\n", + "def random_puzzles(ntables, nrows=6, ncols=3) -> Iterable[Puzzle]:\n", + " \"Generate `ntables` random tables and return the well-formed puzzles from them.\"\n", + " puzzles = (table_puzzle(random_table(nrows, ncols)) \n", + " for _ in range(ntables))\n", + " return [puzzle for puzzle in puzzles if well_formed(puzzle)]" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[(4, 2, 5), (6, 7, 9)]" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "random_table(nrows=2, ncols=3)" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Puzzle(row_prods=[40, 378], col_prods=[24, 14, 45])" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "table_puzzle(_)" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "well_formed(_)" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[Puzzle(row_prods=[144, 42, 189, 320, 56], col_prods=[16128, 784, 1620]),\n", + " Puzzle(row_prods=[243, 192, 3, 147, 315], col_prods=[567, 10584, 1080]),\n", + " Puzzle(row_prods=[60, 12, 49, 105, 150], col_prods=[945, 2800, 210]),\n", + " Puzzle(row_prods=[240, 162, 50, 98, 120], col_prods=[5376, 1575, 2700]),\n", + " Puzzle(row_prods=[15, 14, 216, 315, 512], col_prods=[1008, 1120, 6480]),\n", + " Puzzle(row_prods=[90, 200, 80, 45, 343], col_prods=[22680, 1120, 875]),\n", + " Puzzle(row_prods=[10, 147, 112, 192, 56], col_prods=[5376, 336, 980]),\n", + " Puzzle(row_prods=[441, 160, 100, 175, 12], col_prods=[3360, 900, 4900]),\n", + " Puzzle(row_prods=[320, 12, 45, 378, 18], col_prods=[150, 15552, 504]),\n", + " Puzzle(row_prods=[343, 120, 90, 35, 84], col_prods=[945, 1344, 8575]),\n", + " Puzzle(row_prods=[150, 36, 15, 98, 75], col_prods=[504, 2625, 450]),\n", + " Puzzle(row_prods=[256, 162, 147, 10, 504], col_prods=[3360, 3888, 2352]),\n", + " Puzzle(row_prods=[24, 45, 60, 252, 315], col_prods=[1512, 19440, 175]),\n", + " Puzzle(row_prods=[160, 135, 147, 50, 90], col_prods=[1400, 9450, 1080])]" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "list(random_puzzles(200, 5, 3))" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[Puzzle(row_prods=[210, 56, 75, 40, 180, 189], col_prods=[875, 51030, 26880]),\n", + " Puzzle(row_prods=[324, 245, 432, 200, 50, 6], col_prods=[1680, 12600, 97200])]" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "list(random_puzzles(200, 6, 3))" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[]" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "list(random_puzzles(200, 7, 3))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "I've done this several times, and it looks like about 5% of the random 5×3 tables, 2% of the random 6×3 tables, and less than 1% of the random 7×3 tables are well-formed puzzles. \n", + "\n", + "How fast is it to find a solution? Very fast for small puzzles:" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 3.45 ms, sys: 802 µs, total: 4.26 ms\n", + "Wall time: 3.54 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "[[(3, 9, 5), (5, 9, 1), (8, 1, 8), (5, 7, 8), (5, 7, 2)],\n", + " [(7, 6, 5), (9, 8, 2), (3, 9, 2), (5, 9, 3), (1, 4, 1), (7, 1, 7)]]" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time [solve(p) for p in puzzles]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For larger puzzles it is slower. For example, a 10 x 5 puzzle usually takes between 1/10 second and 10 seconds:" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 124 ms, sys: 3.91 ms, total: 128 ms\n", + "Wall time: 126 ms\n" + ] + } + ], + "source": [ + "p = table_puzzle(random_table(10, 5))\n", + "%time t = solve(p)" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "data": { + "text/markdown": [ + "|1814400|645120|77157360|829440|1905120||\n", + "|---|---|---|---|---|---|\n", + "|3|8|6|6|7|**6048**|\n", + "|3|4|5|3|4|**720**|\n", + "|2|9|7|6|9|**6804**|\n", + "|6|4|9|4|5|**4320**|\n", + "|4|2|7|5|8|**2240**|\n", + "|4|7|6|8|3|**4032**|\n", + "|5|5|9|2|7|**3150**|\n", + "|7|4|6|3|3|**1512**|\n", + "|6|1|2|8|1|**96**|\n", + "|5|2|9|1|3|**270**|" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Markdown(pretty(p, t))" ] }, { @@ -270,7 +629,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 22, "metadata": {}, "outputs": [ { @@ -279,25 +638,27 @@ "True" ] }, - "execution_count": 10, + "execution_count": 22, "metadata": {}, "output_type": "execute_result" } ], "source": [ "def test():\n", + " \"Test suite for CrossProduct functions.\"\n", " assert fill_row(1, 0) == {()}\n", " assert fill_row(2, 0) == set()\n", " assert fill_row(9, 1) == {(9,)}\n", " assert fill_row(10, 1) == set()\n", " assert fill_row(73, 3) == set()\n", " \n", - " assert solve([], []) == []\n", - " assert solve([], [1]) == []\n", - " assert solve([], [2]) == None\n", - " assert solve([5], [5]) == [(5,)]\n", - " assert solve([0], [0]) == None # Maybe should allow zero as a digit?\n", - " \n", + " assert solve(Puzzle([], [])) == []\n", + " assert solve(Puzzle([], [1])) == []\n", + " assert solve(Puzzle([], [2])) == None\n", + " assert solve(Puzzle([5], [5])) == [(5,)]\n", + " assert solve(Puzzle([0], [0])) == None # Maybe should allow zero as a digit?\n", + " assert solve(Puzzle([2, 12], [3, 8])) == [(1, 2), (3, 4)]\n", + "\n", " assert fill_row(729, 3) == {(9, 9, 9)} # Unique fill\n", " \n", " assert fill_row(729, 4) == {\n", @@ -312,8 +673,9 @@ " (9, 9, 3, 3),\n", " (9, 9, 9, 1)}\n", " \n", - " assert max(range(1, 9*9*9 + 1), key=lambda n: len(fill_row(n, 3))) == 72\n", - " assert fill_row(72, 3) == { # 72 has the most ways to fill a 3-digit row\n", + " # 72 has the most ways to fill a 3-digit row\n", + " assert max(range(1, 100), key=lambda n: len(fill_row(n, 3))) == 72\n", + " assert fill_row(72, 3) == { \n", " (1, 8, 9),\n", " (1, 9, 8),\n", " (2, 4, 9),\n", @@ -339,12 +701,7 @@ " (9, 4, 2),\n", " (9, 8, 1)}\n", " \n", - " assert solve([6, 120, 504], [28, 80, 162]) == [\n", - " (1, 2, 3), \n", - " (4, 5, 6), \n", - " (7, 8, 9)]\n", - " \n", - " assert solve([210, 144, 54, 135, 4, 49], [6615, 15552, 420]) == [\n", + " assert solve(Puzzle([210, 144, 54, 135, 4, 49], [6615, 15552, 420])) == [\n", " (7, 6, 5), \n", " (9, 8, 2), \n", " (3, 9, 2), \n", @@ -352,7 +709,7 @@ " (1, 4, 1), \n", " (7, 1, 7)]\n", " \n", - " assert sorted(solutions([8, 8, 1], [8, 8, 1])) == [ # Multi-solution puzzle\n", + " assert sorted(solutions(Puzzle([8, 8, 1], [8, 8, 1]))) == [ # Multi-solution puzzle\n", " [(1, 8, 1), \n", " (8, 1, 1), \n", " (1, 1, 1)],\n", @@ -366,10 +723,10 @@ " (1, 8, 1), \n", " (1, 1, 1)]]\n", " \n", - " assert not list(solutions([8, 8, 1], [8, 8, 2])) # Unsolvable puzzle\n", + " assert not list(solutions(Puzzle([8, 8, 1], [8, 8, 2]))) # Unsolvable puzzle\n", " \n", - " assert solve([1470, 720, 270, 945, 12, 343], \n", - " [6615, 15552, 420, 25725]) == [ # 4 column puzzle\n", + " assert solve(Puzzle([1470, 720, 270, 945, 12, 343], \n", + " [6615, 15552, 420, 25725])) == [ # 4 column puzzle\n", " (7, 6, 5, 7),\n", " (9, 8, 2, 5),\n", " (3, 9, 2, 5),\n", @@ -377,6 +734,20 @@ " (1, 4, 1, 3),\n", " (7, 1, 7, 7)]\n", " \n", + " puzz = Puzzle([6, 120, 504], [28, 80, 162])\n", + " table = [(1, 2, 3), \n", + " (4, 5, 6), \n", + " (7, 8, 9)]\n", + " assert solve(puzz) == table\n", + " assert table_puzzle(table) == puzz\n", + " assert well_formed(puzz)\n", + " \n", + " assert not well_formed(Puzzle([7, 7], [7, 7]))\n", + " assert well_formed(Puzzle([64, 224, 189, 270, 405, 144, 105], \n", + " [308700, 12960, 1119744]))\n", + " \n", + " assert surround((1, 2, 3)) == '|1|2|3|'\n", + " \n", " return True\n", " \n", "test()"