Add files via upload
This commit is contained in:
parent
4f6ae6f7cb
commit
b607ba7ac3
@ -16,7 +16,7 @@
|
||||
"Sample puzzle:\n",
|
||||
"\n",
|
||||
" \n",
|
||||
"| 6615 | 15552 | 420 | |\n",
|
||||
"| 6615 | 15552 | 420 | [6x3] |\n",
|
||||
"|-------|-------|-------|---|\n",
|
||||
"| ? | ? | ? |**210**|\n",
|
||||
"| ? | ? | ? |**144**|\n",
|
||||
@ -28,7 +28,7 @@
|
||||
"\n",
|
||||
"Solution:\n",
|
||||
"\n",
|
||||
"|6615|15552| 420||\n",
|
||||
"|6615|15552| 420| [6x3]|\n",
|
||||
"|---|---|---|---|\n",
|
||||
"|7|6|5|**210**|\n",
|
||||
"|9|8|2|**144**|\n",
|
||||
@ -50,7 +50,9 @@
|
||||
"- `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 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."
|
||||
]
|
||||
},
|
||||
@ -68,7 +70,8 @@
|
||||
"Digit = int\n",
|
||||
"Row = Tuple[Digit, ...] \n",
|
||||
"Table = List[Row] \n",
|
||||
"Products = List[int] \n",
|
||||
"Product = int\n",
|
||||
"Products = List[Product] \n",
|
||||
"Puzzle = namedtuple('Puzzle', 'row_prods, col_prods')"
|
||||
]
|
||||
},
|
||||
@ -101,7 +104,6 @@
|
||||
"\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",
|
||||
"- 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",
|
||||
" - 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."
|
||||
@ -113,7 +115,7 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"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",
|
||||
" return ({()} 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": [
|
||||
"# Solving a whole puzzle\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",
|
||||
"- `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",
|
||||
@ -334,7 +336,7 @@
|
||||
" \"\"\"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",
|
||||
" head = surround(col_prods + [f'[{len(row_prods)}x{len(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",
|
||||
@ -353,7 +355,7 @@
|
||||
{
|
||||
"data": {
|
||||
"text/markdown": [
|
||||
"|3000|3969|640||\n",
|
||||
"|3000|3969|640|[5x3]|\n",
|
||||
"|---|---|---|---|\n",
|
||||
"|3|9|5|**135**|\n",
|
||||
"|5|9|1|**45**|\n",
|
||||
@ -361,7 +363,7 @@
|
||||
"|5|7|8|**280**|\n",
|
||||
"|5|7|2|**70**|\n",
|
||||
"\n",
|
||||
"|6615|15552|420||\n",
|
||||
"|6615|15552|420|[6x3]|\n",
|
||||
"|---|---|---|---|\n",
|
||||
"|7|6|5|**210**|\n",
|
||||
"|9|8|2|**144**|\n",
|
||||
@ -370,7 +372,7 @@
|
||||
"|1|4|1|**4**|\n",
|
||||
"|7|1|7|**49**|\n",
|
||||
"\n",
|
||||
"|183708|245760|117600||\n",
|
||||
"|183708|245760|117600|[7x3]|\n",
|
||||
"|---|---|---|---|\n",
|
||||
"|7|8|5|**280**|\n",
|
||||
"|3|8|7|**168**|\n",
|
||||
@ -441,7 +443,7 @@
|
||||
{
|
||||
"data": {
|
||||
"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,
|
||||
@ -450,27 +452,16 @@
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"random_table(nrows=2, ncols=3)"
|
||||
"random_table(nrows=5, ncols=3)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 15,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"Puzzle(row_prods=[120, 14], col_prods=[35, 6, 8])"
|
||||
]
|
||||
},
|
||||
"execution_count": 15,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"table_puzzle(_)"
|
||||
"puz = table_puzzle(_)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -490,7 +481,36 @@
|
||||
}
|
||||
],
|
||||
"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",
|
||||
"execution_count": 17,
|
||||
"execution_count": 18,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
@ -511,18 +531,17 @@
|
||||
"text": [
|
||||
"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",
|
||||
"14% 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",
|
||||
" 5% 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",
|
||||
" 1% 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"
|
||||
"15% of random puzzles with 4 rows and 3 cols (12 cells) are well-formed\n",
|
||||
" 4% of random puzzles with 3 rows and 5 cols (15 cells) are well-formed\n",
|
||||
" 4% of random puzzles with 5 rows and 3 cols (15 cells) are well-formed\n",
|
||||
" 4% of random puzzles with 4 rows and 4 cols (16 cells) are well-formed\n",
|
||||
" 2% of random puzzles with 6 rows and 3 cols (18 cells) are well-formed\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"N = 300\n",
|
||||
"for r, c in [(3, 3), (3, 4), (4, 3), (3, 5), (5, 3), (4, 4), (6, 3), (5, 4)]:\n",
|
||||
"N = 200\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",
|
||||
" print(f'{w:3.0%} of random puzzles with {r} rows and {c} cols ({r * c:2} cells) are well-formed')"
|
||||
]
|
||||
@ -535,84 +554,84 @@
|
||||
"\n",
|
||||
"# Speed\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,
|
||||
"execution_count": 19,
|
||||
"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"
|
||||
"CPU times: user 2 s, sys: 76.7 ms, total: 2.08 s\n",
|
||||
"Wall time: 2.02 s\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"100"
|
||||
"1000"
|
||||
]
|
||||
},
|
||||
"execution_count": 18,
|
||||
"execution_count": 19,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"%time len([solve(p) for p in random_puzzles(100, 5, 3)])"
|
||||
"%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. For example, a 10 x 4 puzzle can take from 5 milliseconds to 5 seconds or so:"
|
||||
"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": 19,
|
||||
"execution_count": 20,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"CPU times: user 5.94 ms, sys: 237 µs, total: 6.18 ms\n",
|
||||
"Wall time: 6 ms\n"
|
||||
"CPU times: user 2.83 s, sys: 38.1 ms, total: 2.87 s\n",
|
||||
"Wall time: 2.87 s\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/markdown": [
|
||||
"|645120|201600|1382400|1290240||\n",
|
||||
"|470400|53760|8294400|226800|[10x4]|\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**|"
|
||||
"|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": 19,
|
||||
"execution_count": 20,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"puzz = random_puzzles(1, 10, 4)[0]\n",
|
||||
"[puzz] = random_puzzles(1, 10, 4)\n",
|
||||
"%time Markdown(pretty(puzz))"
|
||||
]
|
||||
},
|
||||
@ -620,9 +639,37 @@
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"What could we do to speed that 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).\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."
|
||||
"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:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"execution_count": 20,
|
||||
"execution_count": 22,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
@ -645,7 +692,7 @@
|
||||
"True"
|
||||
]
|
||||
},
|
||||
"execution_count": 20,
|
||||
"execution_count": 22,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user