Add files via upload
This commit is contained in:
parent
a9a18d90b6
commit
8829e7778c
@ -8,48 +8,42 @@
|
|||||||
"\n",
|
"\n",
|
||||||
"# CrossProduct Puzzle\n",
|
"# CrossProduct Puzzle\n",
|
||||||
"\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",
|
"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",
|
||||||
"> *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",
|
"> *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",
|
||||||
"<table class=\"viz full\" data-carpenter-slug=\"wissner-gross.riddler.7\"><thead style=\"border-bottom: none\"><tr><th class=\"number\" style=\"text-align: center;border-right: none;width: 25%;border-left: none\" colspan=\"3\"></th>\n",
|
|
||||||
"<th class=\"number\" style=\"text-align: center;border-right: none;width: 25%;border-left: none\" colspan=\"1\"></th>\n",
|
|
||||||
"</tr></thead><tbody><tr style=\"border-top: 1px solid #cdcdcd\"><td class=\"number\" style=\"text-align: center;border-right: 1px solid #cdcdcd;width: 25%;border-left: 1px solid #cdcdcd\"></td>\n",
|
|
||||||
"<td class=\"number\" style=\"text-align: center;border-right: 1px solid #cdcdcd;width: 25%\"></td>\n",
|
|
||||||
"<td class=\"number\" style=\"text-align: center;border-right: 2px solid;width: 25%\"></td>\n",
|
|
||||||
"<td class=\"number\" style=\"border-right: 1px solid #cdcdcd;width: 25%\">210</td>\n",
|
|
||||||
"</tr><tr><td class=\"number\" style=\"text-align: center;border-right: 1px solid #cdcdcd;width: 25%;border-left: 1px solid #cdcdcd\"></td>\n",
|
|
||||||
"<td class=\"number\" style=\"text-align: center;border-right: 1px solid #cdcdcd;width: 25%\"></td>\n",
|
|
||||||
"<td class=\"number\" style=\"text-align: center;border-right: 2px solid;width: 25%\"></td>\n",
|
|
||||||
"<td class=\"number\" style=\"border-right: 1px solid #cdcdcd;width: 25%\">144</td>\n",
|
|
||||||
"</tr><tr><td class=\"number\" style=\"text-align: center;border-right: 1px solid #cdcdcd;width: 25%;border-left: 1px solid #cdcdcd\"></td>\n",
|
|
||||||
"<td class=\"number\" style=\"text-align: center;border-right: 1px solid #cdcdcd;width: 25%\"></td>\n",
|
|
||||||
"<td class=\"number\" style=\"text-align: center;border-right: 2px solid;width: 25%\"></td>\n",
|
|
||||||
"<td class=\"number\" style=\"border-right: 1px solid #cdcdcd;width: 25%\">54</td>\n",
|
|
||||||
"</tr><tr><td class=\"number\" style=\"text-align: center;border-right: 1px solid #cdcdcd;width: 25%;border-left: 1px solid #cdcdcd\"></td>\n",
|
|
||||||
"<td class=\"number\" style=\"text-align: center;border-right: 1px solid #cdcdcd;width: 25%\"></td>\n",
|
|
||||||
"<td class=\"number\" style=\"text-align: center;border-right: 2px solid;width: 25%\"></td>\n",
|
|
||||||
"<td class=\"number\" style=\"border-right: 1px solid #cdcdcd;width: 25%\">135</td>\n",
|
|
||||||
"</tr><tr><td class=\"number\" style=\"text-align: center;border-right: 1px solid #cdcdcd;width: 25%;border-left: 1px solid #cdcdcd\"></td>\n",
|
|
||||||
"<td class=\"number\" style=\"text-align: center;border-right: 1px solid #cdcdcd;width: 25%\"></td>\n",
|
|
||||||
"<td class=\"number\" style=\"text-align: center;border-right: 2px solid;width: 25%\"></td>\n",
|
|
||||||
"<td class=\"number\" style=\"border-right: 1px solid #cdcdcd;width: 25%\">4</td>\n",
|
|
||||||
"</tr><tr><td class=\"number\" style=\"text-align: center;border-right: 1px solid #cdcdcd;width: 25%;border-left: 1px solid #cdcdcd\"></td>\n",
|
|
||||||
"<td class=\"number\" style=\"text-align: center;border-right: 1px solid #cdcdcd;width: 25%\"></td>\n",
|
|
||||||
"<td class=\"number\" style=\"text-align: center;border-right: 2px solid;width: 25%\"></td>\n",
|
|
||||||
"<td class=\"number\" style=\"border-right: 1px solid #cdcdcd;width: 25%\">49</td>\n",
|
|
||||||
"</tr><tr style=\"border-top: 2px solid;border-bottom: 1px solid #cdcdcd\"><td class=\"number\" style=\"text-align: center;border-right: 1px solid #cdcdcd;width: 25%;border-left: 1px solid #cdcdcd\">6,615</td>\n",
|
|
||||||
"<td class=\"number\" style=\"text-align: center;border-right: 1px solid #cdcdcd;width: 25%\">15,552</td>\n",
|
|
||||||
"<td class=\"number\" style=\"text-align: center;border-right: 2px solid;width: 25%\">420</td>\n",
|
|
||||||
"<td class=\"number\" style=\"border-right: 1px solid #cdcdcd;width: 25%\"></td>\n",
|
|
||||||
"</tr></tbody></table>\n",
|
|
||||||
"\n",
|
"\n",
|
||||||
" \n",
|
" \n",
|
||||||
"# Data Type definitions\n",
|
"# Data type definitions\n",
|
||||||
" \n",
|
" \n",
|
||||||
"Here are the data types we will use in trying to solve CrossProduct puzzles: \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",
|
"- `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",
|
"- `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, `[210, 144, 54, 135, 4, 49]` for the row products, and `[6615, 15552, 420]` for the column products."
|
"- `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": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"from typing import Tuple, List, Set, Iterable, Optional\n",
|
"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",
|
"\n",
|
||||||
"Row = Tuple[int, ...] \n",
|
"Digit = int\n",
|
||||||
|
"Row = Tuple[Digit, ...] \n",
|
||||||
"Table = List[Row] \n",
|
"Table = List[Row] \n",
|
||||||
"Products = List[int] "
|
"Products = List[int] \n",
|
||||||
|
"Puzzle = namedtuple('Puzzle', 'row_prods, col_prods')"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"source": [
|
"source": [
|
||||||
"# Filling in one row\n",
|
"# The puzzles\n",
|
||||||
"\n",
|
"\n",
|
||||||
"A first step in solving the puzzle is filling in a single row of the table.\n",
|
"Here are the puzzles given by 538 Riddler (they promised one a week for four weeks):"
|
||||||
"<br>`fill_row(n, k)` will return the set of all `k`-digit tuples whose product is `n`.\n",
|
|
||||||
"<br>By default there are 3 digits in a row, but any number of digits is allowed."
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -82,14 +79,34 @@
|
|||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"def fill_row(n, k=3) -> Set[Row]:\n",
|
"puzzles = (Puzzle([135, 45, 64, 280, 70], [3000, 3969, 640]),\n",
|
||||||
" \"All permutations of k digits that multiply to n.\"\n",
|
" Puzzle([210, 144, 54, 135, 4, 49], [6615, 15552, 420]))"
|
||||||
" if k == 0:\n",
|
]
|
||||||
" return {()} if n == 1 else set()\n",
|
},
|
||||||
" else:\n",
|
{
|
||||||
" return {(d, *rest) for d in range(1, 10)\n",
|
"cell_type": "markdown",
|
||||||
" if (n / d).is_integer()\n",
|
"metadata": {},
|
||||||
" for rest in fill_row(n // d, k - 1)}"
|
"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",
|
"cell_type": "code",
|
||||||
"execution_count": 3,
|
"execution_count": 4,
|
||||||
"metadata": {},
|
"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": [
|
"source": [
|
||||||
"fill_row(210)"
|
"fill_row(210)"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 4,
|
"execution_count": 5,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [],
|
"outputs": [
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"text/plain": [
|
||||||
|
"{(9, 9, 9)}"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"execution_count": 5,
|
||||||
|
"metadata": {},
|
||||||
|
"output_type": "execute_result"
|
||||||
|
}
|
||||||
|
],
|
||||||
"source": [
|
"source": [
|
||||||
"fill_row(729)"
|
"fill_row(729)"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 5,
|
"execution_count": 6,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [
|
"outputs": [
|
||||||
{
|
{
|
||||||
@ -137,7 +176,7 @@
|
|||||||
" (9, 9, 9, 1)}"
|
" (9, 9, 9, 1)}"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"execution_count": 5,
|
"execution_count": 6,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"output_type": "execute_result"
|
"output_type": "execute_result"
|
||||||
}
|
}
|
||||||
@ -148,65 +187,63 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 6,
|
"execution_count": 7,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [
|
"outputs": [
|
||||||
{
|
{
|
||||||
"data": {
|
"data": {
|
||||||
"text/plain": [
|
"text/plain": [
|
||||||
"{(1, 5, 5, 5, 5, 5),\n",
|
"{(1, 7, 7, 7, 7, 7),\n",
|
||||||
" (5, 1, 5, 5, 5, 5),\n",
|
" (7, 1, 7, 7, 7, 7),\n",
|
||||||
" (5, 5, 1, 5, 5, 5),\n",
|
" (7, 7, 1, 7, 7, 7),\n",
|
||||||
" (5, 5, 5, 1, 5, 5),\n",
|
" (7, 7, 7, 1, 7, 7),\n",
|
||||||
" (5, 5, 5, 5, 1, 5),\n",
|
" (7, 7, 7, 7, 1, 7),\n",
|
||||||
" (5, 5, 5, 5, 5, 1)}"
|
" (7, 7, 7, 7, 7, 1)}"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"execution_count": 6,
|
"execution_count": 7,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"output_type": "execute_result"
|
"output_type": "execute_result"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"source": [
|
"source": [
|
||||||
"fill_row(5**5, 6)"
|
"fill_row(7**5, 6)"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"source": [
|
"source": [
|
||||||
"# Solving the whole puzzle\n",
|
"# Solving a whole puzzle\n",
|
||||||
"\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",
|
"- We can now solve a whole puzzle with a simple brute-force strategy:\n",
|
||||||
"\n",
|
"- For every possible way of filling the first row, try every way of recursively solving the rest of the puzzle. \n",
|
||||||
"The function `solutions` has three main cases to consider:\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",
|
||||||
"- Any puzzle with a non-integer column product is unsolvable. Return without yielding anything.\n",
|
"- `solutions` yields all possible solutions to a puzzle. There are three main cases to consider:\n",
|
||||||
"- The empty puzzle (with no rows) has the empty table, `[]`, as a solution, as long as the column products are all 1. \n",
|
" - A 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). "
|
" - 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",
|
"cell_type": "code",
|
||||||
"execution_count": 7,
|
"execution_count": 8,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"def solutions(row_prods: Products, col_prods: Products) -> Iterable[Table]:\n",
|
"def solve(puzzle) -> Optional[Table]: return next(solutions(puzzle), None)\n",
|
||||||
" \"\"\"Yield table(s) that solve the puzzle.\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 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",
|
" 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",
|
" row_prods, col_prods = puzzle\n",
|
||||||
" return\n",
|
|
||||||
" if not row_prods and all(c == 1 for c in col_prods):\n",
|
" if not row_prods and all(c == 1 for c in col_prods):\n",
|
||||||
" yield []\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 row1 in fill_row(row_prods[0], len(col_prods)):\n",
|
||||||
" for rows in solutions(row_prods[1:], divide(col_prods, row1)):\n",
|
" for rows in solutions(Puzzle(row_prods[1:], divide(col_prods, row1))):\n",
|
||||||
" yield [row1, *rows]\n",
|
" yield [row1, *rows]"
|
||||||
" \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)]"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -218,26 +255,6 @@
|
|||||||
"Here are solutions to the puzzles posed by *The Riddler*:"
|
"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",
|
"cell_type": "code",
|
||||||
"execution_count": 9,
|
"execution_count": 9,
|
||||||
@ -246,7 +263,8 @@
|
|||||||
{
|
{
|
||||||
"data": {
|
"data": {
|
||||||
"text/plain": [
|
"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,
|
"execution_count": 9,
|
||||||
@ -255,8 +273,349 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"source": [
|
"source": [
|
||||||
"# The puzzle that appears at the top of this notebook\n",
|
"[solve(p) for p in puzzles]"
|
||||||
"solve([210, 144, 54, 135, 4, 49], [6615, 15552, 420])"
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": [
|
||||||
|
"<IPython.core.display.Markdown object>"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"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": [
|
||||||
|
"<IPython.core.display.Markdown object>"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"execution_count": 21,
|
||||||
|
"metadata": {},
|
||||||
|
"output_type": "execute_result"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"Markdown(pretty(p, t))"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -270,7 +629,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 10,
|
"execution_count": 22,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [
|
"outputs": [
|
||||||
{
|
{
|
||||||
@ -279,25 +638,27 @@
|
|||||||
"True"
|
"True"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"execution_count": 10,
|
"execution_count": 22,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"output_type": "execute_result"
|
"output_type": "execute_result"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"source": [
|
"source": [
|
||||||
"def test():\n",
|
"def test():\n",
|
||||||
|
" \"Test suite for CrossProduct functions.\"\n",
|
||||||
" assert fill_row(1, 0) == {()}\n",
|
" assert fill_row(1, 0) == {()}\n",
|
||||||
" assert fill_row(2, 0) == set()\n",
|
" assert fill_row(2, 0) == set()\n",
|
||||||
" assert fill_row(9, 1) == {(9,)}\n",
|
" assert fill_row(9, 1) == {(9,)}\n",
|
||||||
" assert fill_row(10, 1) == set()\n",
|
" assert fill_row(10, 1) == set()\n",
|
||||||
" assert fill_row(73, 3) == set()\n",
|
" assert fill_row(73, 3) == set()\n",
|
||||||
" \n",
|
" \n",
|
||||||
" assert solve([], []) == []\n",
|
" assert solve(Puzzle([], [])) == []\n",
|
||||||
" assert solve([], [1]) == []\n",
|
" assert solve(Puzzle([], [1])) == []\n",
|
||||||
" assert solve([], [2]) == None\n",
|
" assert solve(Puzzle([], [2])) == None\n",
|
||||||
" assert solve([5], [5]) == [(5,)]\n",
|
" assert solve(Puzzle([5], [5])) == [(5,)]\n",
|
||||||
" assert solve([0], [0]) == None # Maybe should allow zero as a digit?\n",
|
" assert solve(Puzzle([0], [0])) == None # Maybe should allow zero as a digit?\n",
|
||||||
" \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",
|
" assert fill_row(729, 3) == {(9, 9, 9)} # Unique fill\n",
|
||||||
" \n",
|
" \n",
|
||||||
" assert fill_row(729, 4) == {\n",
|
" assert fill_row(729, 4) == {\n",
|
||||||
@ -312,8 +673,9 @@
|
|||||||
" (9, 9, 3, 3),\n",
|
" (9, 9, 3, 3),\n",
|
||||||
" (9, 9, 9, 1)}\n",
|
" (9, 9, 9, 1)}\n",
|
||||||
" \n",
|
" \n",
|
||||||
" assert max(range(1, 9*9*9 + 1), key=lambda n: len(fill_row(n, 3))) == 72\n",
|
" # 72 has the most ways to fill a 3-digit row\n",
|
||||||
" assert fill_row(72, 3) == { # 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, 8, 9),\n",
|
||||||
" (1, 9, 8),\n",
|
" (1, 9, 8),\n",
|
||||||
" (2, 4, 9),\n",
|
" (2, 4, 9),\n",
|
||||||
@ -339,12 +701,7 @@
|
|||||||
" (9, 4, 2),\n",
|
" (9, 4, 2),\n",
|
||||||
" (9, 8, 1)}\n",
|
" (9, 8, 1)}\n",
|
||||||
" \n",
|
" \n",
|
||||||
" assert solve([6, 120, 504], [28, 80, 162]) == [\n",
|
" assert solve(Puzzle([210, 144, 54, 135, 4, 49], [6615, 15552, 420])) == [\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",
|
|
||||||
" (7, 6, 5), \n",
|
" (7, 6, 5), \n",
|
||||||
" (9, 8, 2), \n",
|
" (9, 8, 2), \n",
|
||||||
" (3, 9, 2), \n",
|
" (3, 9, 2), \n",
|
||||||
@ -352,7 +709,7 @@
|
|||||||
" (1, 4, 1), \n",
|
" (1, 4, 1), \n",
|
||||||
" (7, 1, 7)]\n",
|
" (7, 1, 7)]\n",
|
||||||
" \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",
|
" [(1, 8, 1), \n",
|
||||||
" (8, 1, 1), \n",
|
" (8, 1, 1), \n",
|
||||||
" (1, 1, 1)],\n",
|
" (1, 1, 1)],\n",
|
||||||
@ -366,10 +723,10 @@
|
|||||||
" (1, 8, 1), \n",
|
" (1, 8, 1), \n",
|
||||||
" (1, 1, 1)]]\n",
|
" (1, 1, 1)]]\n",
|
||||||
" \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",
|
" \n",
|
||||||
" assert solve([1470, 720, 270, 945, 12, 343], \n",
|
" assert solve(Puzzle([1470, 720, 270, 945, 12, 343], \n",
|
||||||
" [6615, 15552, 420, 25725]) == [ # 4 column puzzle\n",
|
" [6615, 15552, 420, 25725])) == [ # 4 column puzzle\n",
|
||||||
" (7, 6, 5, 7),\n",
|
" (7, 6, 5, 7),\n",
|
||||||
" (9, 8, 2, 5),\n",
|
" (9, 8, 2, 5),\n",
|
||||||
" (3, 9, 2, 5),\n",
|
" (3, 9, 2, 5),\n",
|
||||||
@ -377,6 +734,20 @@
|
|||||||
" (1, 4, 1, 3),\n",
|
" (1, 4, 1, 3),\n",
|
||||||
" (7, 1, 7, 7)]\n",
|
" (7, 1, 7, 7)]\n",
|
||||||
" \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",
|
" return True\n",
|
||||||
" \n",
|
" \n",
|
||||||
"test()"
|
"test()"
|
||||||
|
Loading…
Reference in New Issue
Block a user