Add files via upload

This commit is contained in:
Peter Norvig 2021-02-20 23:42:15 -08:00 committed by GitHub
parent 4f6ae6f7cb
commit b607ba7ac3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -16,7 +16,7 @@
"Sample puzzle:\n", "Sample puzzle:\n",
"\n", "\n",
" \n", " \n",
"| 6615 | 15552 |   420 | |\n", "| 6615 | 15552 |   420 | [6x3] |\n",
"|-------|-------|-------|---|\n", "|-------|-------|-------|---|\n",
"| ? | ? | ? |**210**|\n", "| ? | ? | ? |**210**|\n",
"| ? | ? | ? |**144**|\n", "| ? | ? | ? |**144**|\n",
@ -28,7 +28,7 @@
"\n", "\n",
"Solution:\n", "Solution:\n",
"\n", "\n",
"|6615|15552|  420||\n", "|6615|15552|  420| [6x3]|\n",
"|---|---|---|---|\n", "|---|---|---|---|\n",
"|7|6|5|**210**|\n", "|7|6|5|**210**|\n",
"|9|8|2|**144**|\n", "|9|8|2|**144**|\n",
@ -50,7 +50,9 @@
"- `Digit`: a single digit, from 1 to 9 (but not 0).\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 table of digits that fill in for the \"?\"s; 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, `[6615, 15552, 420]` for the column products, and `[210, 144, 54, 135, 4, 49]` for the row products.\n", "- `Products`: a list of the numbers that corresponding digits must multiply to, e.g. in the puzzle above:\n",
" - `[6615, 15552, 420]` for the column products;\n",
" - `[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." "- `Puzzle`: a puzzle to be solved, as defined by the row products and column products."
] ]
}, },
@ -67,8 +69,9 @@
"\n", "\n",
"Digit = int\n", "Digit = int\n",
"Row = Tuple[Digit, ...] \n", "Row = Tuple[Digit, ...] \n",
"Table = List[Row] \n", "Table = List[Row] \n",
"Products = List[int] \n", "Product = int\n",
"Products = List[Product] \n",
"Puzzle = namedtuple('Puzzle', 'row_prods, col_prods')" "Puzzle = namedtuple('Puzzle', 'row_prods, col_prods')"
] ]
}, },
@ -101,7 +104,6 @@
"\n", "\n",
"- A first step in solving the puzzle is filling in a single row of the table.\n", "- A first step in solving the puzzle is filling in a single row of the table.\n",
"- We will need to respect the row- and column-product constraints.\n", "- We will need to respect the row- and column-product constraints.\n",
"- The strategy is: for every feasible way of filling the first digit, try all ways of recursively filling the rest of the row.\n",
"- `fill_one_row(row_prod=210, col_prods=[6615, 15552, 420])` will return a set of 3-digit tuples where each tuple multiplies to 210, and each digit of the tuple evenly divides the corresponding number in `col_prods`.\n", "- `fill_one_row(row_prod=210, col_prods=[6615, 15552, 420])` will return a set of 3-digit tuples where each tuple multiplies to 210, and each digit of the tuple evenly divides the corresponding number in `col_prods`.\n",
" - If `col_prods` is `[]`, then there is one solution (the 0-length tuple) if `row_prod` is 1, and no solution otherwise.\n", " - If `col_prods` is `[]`, then there is one solution (the 0-length tuple) if `row_prod` is 1, and no solution otherwise.\n",
" - Otherwise, try each digit `d` that divides both the `row_prod` and the first `col_prods`, and then try all ways to fill the rest of the row." " - Otherwise, try each digit `d` that divides both the `row_prod` and the first `col_prods`, and then try all ways to fill the rest of the row."
@ -113,7 +115,7 @@
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"def fill_one_row(row_prod, col_prods) -> Set[Row]:\n", "def fill_one_row(row_prod: Product, col_prods: Products) -> Set[Row]:\n",
" \"All permutations of digits that multiply to `row_prod` and evenly divide `col_prods`.\"\n", " \"All permutations of digits that multiply to `row_prod` and evenly divide `col_prods`.\"\n",
" return ({()} if not col_prods and row_prod == 1 else\n", " return ({()} if not col_prods and row_prod == 1 else\n",
" set() if not col_prods and row_prod != 1 else\n", " set() if not col_prods and row_prod != 1 else\n",
@ -252,7 +254,7 @@
"source": [ "source": [
"# Solving a whole puzzle\n", "# Solving a whole puzzle\n",
"\n", "\n",
"- We can solve a whole puzzle with a similar strategy.\n", "- We can solve a whole puzzle with a similar strategy to filling a row.\n",
"- For every possible way of filling the first row, try every way of recursively solving the rest of the puzzle. \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", "- `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", "- `solutions` yields all possible solutions to a puzzle. There are three main cases to consider:\n",
@ -334,7 +336,7 @@
" \"\"\"A puzzle and the filled-in table as a str that will be pretty in Markdown.\"\"\"\n", " \"\"\"A puzzle and the filled-in table as a str that will be pretty in Markdown.\"\"\"\n",
" row_prods, col_prods = puzzle\n", " row_prods, col_prods = puzzle\n",
" table = table or solve(puzzle)\n", " table = table or solve(puzzle)\n",
" head = surround(col_prods + [''])\n", " head = surround(col_prods + [f'[{len(row_prods)}x{len(col_prods)}]'])\n",
" dash = surround(['---'] * (1 + len(col_prods)))\n", " dash = surround(['---'] * (1 + len(col_prods)))\n",
" rest = [surround(row + (f'**{rp}**',))\n", " rest = [surround(row + (f'**{rp}**',))\n",
" for row, rp in zip(table, row_prods)]\n", " for row, rp in zip(table, row_prods)]\n",
@ -353,7 +355,7 @@
{ {
"data": { "data": {
"text/markdown": [ "text/markdown": [
"|3000|3969|640||\n", "|3000|3969|640|[5x3]|\n",
"|---|---|---|---|\n", "|---|---|---|---|\n",
"|3|9|5|**135**|\n", "|3|9|5|**135**|\n",
"|5|9|1|**45**|\n", "|5|9|1|**45**|\n",
@ -361,7 +363,7 @@
"|5|7|8|**280**|\n", "|5|7|8|**280**|\n",
"|5|7|2|**70**|\n", "|5|7|2|**70**|\n",
"\n", "\n",
"|6615|15552|420||\n", "|6615|15552|420|[6x3]|\n",
"|---|---|---|---|\n", "|---|---|---|---|\n",
"|7|6|5|**210**|\n", "|7|6|5|**210**|\n",
"|9|8|2|**144**|\n", "|9|8|2|**144**|\n",
@ -370,7 +372,7 @@
"|1|4|1|**4**|\n", "|1|4|1|**4**|\n",
"|7|1|7|**49**|\n", "|7|1|7|**49**|\n",
"\n", "\n",
"|183708|245760|117600||\n", "|183708|245760|117600|[7x3]|\n",
"|---|---|---|---|\n", "|---|---|---|---|\n",
"|7|8|5|**280**|\n", "|7|8|5|**280**|\n",
"|3|8|7|**168**|\n", "|3|8|7|**168**|\n",
@ -441,7 +443,7 @@
{ {
"data": { "data": {
"text/plain": [ "text/plain": [
"[(5, 3, 8), (7, 2, 1)]" "[(9, 4, 9), (7, 4, 9), (3, 8, 3), (8, 6, 1), (3, 4, 5)]"
] ]
}, },
"execution_count": 14, "execution_count": 14,
@ -450,27 +452,16 @@
} }
], ],
"source": [ "source": [
"random_table(nrows=2, ncols=3)" "random_table(nrows=5, ncols=3)"
] ]
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 15, "execution_count": 15,
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [],
{
"data": {
"text/plain": [
"Puzzle(row_prods=[120, 14], col_prods=[35, 6, 8])"
]
},
"execution_count": 15,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [ "source": [
"table_puzzle(_)" "puz = table_puzzle(_)"
] ]
}, },
{ {
@ -490,7 +481,36 @@
} }
], ],
"source": [ "source": [
"well_formed(_)" "well_formed(puz)"
]
},
{
"cell_type": "code",
"execution_count": 17,
"metadata": {},
"outputs": [
{
"data": {
"text/markdown": [
"|4536|3072|1215|[5x3]|\n",
"|---|---|---|---|\n",
"|6|6|9|**324**|\n",
"|7|4|9|**252**|\n",
"|9|8|1|**72**|\n",
"|2|8|3|**48**|\n",
"|6|2|5|**60**|"
],
"text/plain": [
"<IPython.core.display.Markdown object>"
]
},
"execution_count": 17,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"Markdown(pretty(puz))"
] ]
}, },
{ {
@ -502,7 +522,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 17, "execution_count": 18,
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
{ {
@ -511,18 +531,17 @@
"text": [ "text": [
"38% of random puzzles with 3 rows and 3 cols ( 9 cells) are well-formed\n", "38% of random puzzles with 3 rows and 3 cols ( 9 cells) are well-formed\n",
"15% of random puzzles with 3 rows and 4 cols (12 cells) are well-formed\n", "15% of random puzzles with 3 rows and 4 cols (12 cells) are well-formed\n",
"14% of random puzzles with 4 rows and 3 cols (12 cells) are well-formed\n", "15% of random puzzles with 4 rows and 3 cols (12 cells) are well-formed\n",
" 6% of random puzzles with 3 rows and 5 cols (15 cells) are well-formed\n", " 4% of random puzzles with 3 rows and 5 cols (15 cells) are well-formed\n",
" 5% of random puzzles with 5 rows and 3 cols (15 cells) are well-formed\n", " 4% of random puzzles with 5 rows and 3 cols (15 cells) are well-formed\n",
" 3% of random puzzles with 4 rows and 4 cols (16 cells) are well-formed\n", " 4% of random puzzles with 4 rows and 4 cols (16 cells) are well-formed\n",
" 1% of random puzzles with 6 rows and 3 cols (18 cells) are well-formed\n", " 2% of random puzzles with 6 rows and 3 cols (18 cells) are well-formed\n"
" 0% of random puzzles with 5 rows and 4 cols (20 cells) are well-formed\n"
] ]
} }
], ],
"source": [ "source": [
"N = 300\n", "N = 200\n",
"for r, c in [(3, 3), (3, 4), (4, 3), (3, 5), (5, 3), (4, 4), (6, 3), (5, 4)]:\n", "for r, c in [(3, 3), (3, 4), (4, 3), (3, 5), (5, 3), (4, 4), (6, 3)]:\n",
" w = sum(map(well_formed, random_puzzles(N, r, c))) / N\n", " w = sum(map(well_formed, random_puzzles(N, r, c))) / N\n",
" print(f'{w:3.0%} of random puzzles with {r} rows and {c} cols ({r * c:2} cells) are well-formed')" " print(f'{w:3.0%} of random puzzles with {r} rows and {c} cols ({r * c:2} cells) are well-formed')"
] ]
@ -535,42 +554,7 @@
"\n", "\n",
"# Speed\n", "# Speed\n",
"\n", "\n",
"How fast is it to solve a random puzzle? We can do a hundred small (5x3) puzzles in under a second:" "How long does it take to solve a random puzzle? We can do a thousand small (5x3) puzzles in about two seconds:"
]
},
{
"cell_type": "code",
"execution_count": 18,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"CPU times: user 179 ms, sys: 7.47 ms, total: 187 ms\n",
"Wall time: 181 ms\n"
]
},
{
"data": {
"text/plain": [
"100"
]
},
"execution_count": 18,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"%time len([solve(p) for p in random_puzzles(100, 5, 3)])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Puzzles that are even a little bit larger can be a lot slower. For example, a 10 x 4 puzzle can take from 5 milliseconds to 5 seconds or so:"
] ]
}, },
{ {
@ -582,28 +566,14 @@
"name": "stdout", "name": "stdout",
"output_type": "stream", "output_type": "stream",
"text": [ "text": [
"CPU times: user 5.94 ms, sys: 237 µs, total: 6.18 ms\n", "CPU times: user 2 s, sys: 76.7 ms, total: 2.08 s\n",
"Wall time: 6 ms\n" "Wall time: 2.02 s\n"
] ]
}, },
{ {
"data": { "data": {
"text/markdown": [
"|645120|201600|1382400|1290240||\n",
"|---|---|---|---|---|\n",
"|2|3|5|5|**150**|\n",
"|3|4|4|4|**192**|\n",
"|6|5|2|3|**180**|\n",
"|8|4|4|8|**1024**|\n",
"|8|4|4|8|**1024**|\n",
"|1|5|2|4|**40**|\n",
"|1|2|9|6|**108**|\n",
"|5|1|3|1|**15**|\n",
"|7|3|8|7|**1176**|\n",
"|8|7|5|2|**560**|"
],
"text/plain": [ "text/plain": [
"<IPython.core.display.Markdown object>" "1000"
] ]
}, },
"execution_count": 19, "execution_count": 19,
@ -612,7 +582,56 @@
} }
], ],
"source": [ "source": [
"puzz = random_puzzles(1, 10, 4)[0]\n", "%time len([solve(p) for p in random_puzzles(1000, 5, 3)])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Puzzles that are even a little bit larger can be a lot slower, and there is huge variability in the time to solve. For example, a single 10 x 4 puzzle can take from a few milliseconds to several seconds:"
]
},
{
"cell_type": "code",
"execution_count": 20,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"CPU times: user 2.83 s, sys: 38.1 ms, total: 2.87 s\n",
"Wall time: 2.87 s\n"
]
},
{
"data": {
"text/markdown": [
"|470400|53760|8294400|226800|[10x4]|\n",
"|---|---|---|---|---|\n",
"|7|1|5|4|**140**|\n",
"|5|1|5|4|**100**|\n",
"|4|5|4|7|**560**|\n",
"|5|8|2|5|**400**|\n",
"|7|7|2|1|**98**|\n",
"|3|8|4|3|**288**|\n",
"|1|1|9|1|**9**|\n",
"|8|6|8|5|**1920**|\n",
"|4|4|8|3|**384**|\n",
"|1|1|9|9|**81**|"
],
"text/plain": [
"<IPython.core.display.Markdown object>"
]
},
"execution_count": 20,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"[puzz] = random_puzzles(1, 10, 4)\n",
"%time Markdown(pretty(puzz))" "%time Markdown(pretty(puzz))"
] ]
}, },
@ -620,9 +639,37 @@
"cell_type": "markdown", "cell_type": "markdown",
"metadata": {}, "metadata": {},
"source": [ "source": [
"What could we do to speed that up?\n", "In general, the time to solve a puzzle can grow exponentially in the number of cells. Consider this one row in a six-column puzzle, with 3,960 possibilities:"
"- We could treat it as a constraint satisfaction problem (CSP), and use a highly-optimized [CSP solver](https://developers.google.com/optimization/cp/cp_solver).\n", ]
"- We could use some of the lessons that CSP solvers use, such as trying the most constrained variables first. If a row- (or column-) product is divisible by 5 (or 7), then one of the digits in the row (or column) must be 5 (or 7). (That's not true of any other digit. For example, if a product is divisible by 2 and 3, it might be that the digits 2 and 3 appear, but perhaps 6 appears instead.) So instead of filling in the table row by row, top to bottom, try filling in the 5s and 7s first, then fill other rows that have a small number of possible fillers." },
{
"cell_type": "code",
"execution_count": 21,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"3960"
]
},
"execution_count": 21,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"n = 5 * 7 * 8 * 9\n",
"len(fill_one_row(2 * n, [n] * 6))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"If the first three rows all had a similar number of possibilities, that would be tens of billions of combinations to try. What could we do to speed things up?\n",
"- We could treat it as a constraint satisfaction problem (CSP), and use a highly-optimized [CSP solver](https://developers.google.com/optimization/cp/cp_solver). A good CSP representation would be to make each cell be a variable with range {1, ... 9}, and with the constraints being that the digit in each cell must evenly divide both the row- and column- constraint for the cell, and the product of the row (or column) must equal the corresponding value.\n",
"- Even without using a professional CSP solver, we could borrow the heuristics they use. In `solve`, we fill in cells in strict top-to-bottom, left-to-right order. It is better to fill in first the cell with the minimum number of possible values. For each cell, find the greatest common divisor of the row- and column-products. For a cell whose gcd is 72, the possible digits are {2, 3, 4, 6, 9}. For a cell whose gcd is 21, the possible digits are {3, 7}. Thus, it is better to fill in the 21 cell first, because you have a 1/2 chance of guessing right, not a 1/5 chance."
] ]
}, },
{ {
@ -636,7 +683,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 20, "execution_count": 22,
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
{ {
@ -645,7 +692,7 @@
"True" "True"
] ]
}, },
"execution_count": 20, "execution_count": 22,
"metadata": {}, "metadata": {},
"output_type": "execute_result" "output_type": "execute_result"
} }