Add files via upload

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

View File

@ -10,7 +10,11 @@
"\n", "\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", "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",
"\n", "\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", ">*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",
"\n",
"Sample puzzle:\n",
"\n",
" \n", " \n",
"| 6615 | 15552 |   420 | |\n", "| 6615 | 15552 |   420 | |\n",
"|-------|-------|-------|---|\n", "|-------|-------|-------|---|\n",
@ -21,7 +25,8 @@
"| ? | ? | ? |**4**|\n", "| ? | ? | ? |**4**|\n",
"| ? | ? | ? |**49**|\n", "| ? | ? | ? |**49**|\n",
"\n", "\n",
"*This is the solution:*\n", "\n",
"Solution:\n",
"\n", "\n",
"|6615|15552|  420||\n", "|6615|15552|  420||\n",
"|---|---|---|---|\n", "|---|---|---|---|\n",
@ -31,6 +36,8 @@
"|5|9|3|**135**|\n", "|5|9|3|**135**|\n",
"|1|4|1|**4**|\n", "|1|4|1|**4**|\n",
"|7|1|7|**49**|\n", "|7|1|7|**49**|\n",
" \n",
"\n",
"\n", "\n",
"\n", "\n",
"\n", "\n",
@ -56,7 +63,7 @@
"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 numpy import divide, prod, transpose\n",
"from random import randint\n", "from random import randint\n",
"from collections import namedtuple\n", "from collections import namedtuple, Counter\n",
"\n", "\n",
"Digit = int\n", "Digit = int\n",
"Row = Tuple[Digit, ...] \n", "Row = Tuple[Digit, ...] \n",
@ -71,7 +78,7 @@
"source": [ "source": [
"# The puzzles\n", "# The puzzles\n",
"\n", "\n",
"Here are the puzzles given by 538 Riddler (they promised one a week for four weeks):" "Here are the puzzles given by 538 Riddler (they promised one a week for four weeks; so far we've seen three):"
] ]
}, },
{ {
@ -80,8 +87,10 @@
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"puzzles = (Puzzle([135, 45, 64, 280, 70], [3000, 3969, 640]),\n", "puzzles = (\n",
" Puzzle([210, 144, 54, 135, 4, 49], [6615, 15552, 420]))" " Puzzle([135, 45, 64, 280, 70], [3000, 3969, 640]),\n",
" Puzzle([210, 144, 54, 135, 4, 49], [6615, 15552, 420]),\n",
" Puzzle([280, 168, 162, 360, 60, 256, 126], [183708, 245760, 117600]))"
] ]
}, },
{ {
@ -91,8 +100,11 @@
"# Filling in one row\n", "# Filling in one row\n",
"\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",
"- `fill_row(product, k)` will return the set of all `k`-digit tuples whose product is `product`.\n", "- We will need to respect the row- and column-product constraints.\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:" "- 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."
] ]
}, },
{ {
@ -101,20 +113,20 @@
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"def fill_row(product, k=3) -> Set[Row]:\n", "def fill_one_row(row_prod, col_prods) -> Set[Row]:\n",
" \"All permutations of `k` digits that multiply to `product`.\"\n", " \"All permutations of digits that multiply to `row_prod` and evenly divide `col_prods`.\"\n",
" return ({()} if k == 0 and product == 1 else\n", " return ({()} if not col_prods and row_prod == 1 else\n",
" set() if k == 0 and product != 1 else\n", " set() if not col_prods and row_prod != 1 else\n",
" {(d, *rest) for d in range(1, 10)\n", " {(d, *rest) for d in range(1, 10)\n",
" if (product / d).is_integer()\n", " if (row_prod / d).is_integer() and (col_prods[0] / d).is_integer()\n",
" for rest in fill_row(product // d, k - 1)})" " for rest in fill_one_row(row_prod // d, col_prods[1:])})"
] ]
}, },
{ {
"cell_type": "markdown", "cell_type": "markdown",
"metadata": {}, "metadata": {},
"source": [ "source": [
"For example:" "Some examples:"
] ]
}, },
{ {
@ -125,7 +137,7 @@
{ {
"data": { "data": {
"text/plain": [ "text/plain": [
"{(5, 6, 7), (5, 7, 6), (6, 5, 7), (6, 7, 5), (7, 5, 6), (7, 6, 5)}" "{(5, 6, 7), (7, 6, 5)}"
] ]
}, },
"execution_count": 4, "execution_count": 4,
@ -134,7 +146,7 @@
} }
], ],
"source": [ "source": [
"fill_row(210)" "fill_one_row(210, [6615, 15552, 420]) # There are two ways to fill the first row"
] ]
}, },
{ {
@ -154,7 +166,7 @@
} }
], ],
"source": [ "source": [
"fill_row(729)" "fill_one_row(729, [90, 90, 90])"
] ]
}, },
{ {
@ -183,7 +195,7 @@
} }
], ],
"source": [ "source": [
"fill_row(729, 4)" "fill_one_row(729, [90, 90, 90, 90])"
] ]
}, },
{ {
@ -194,12 +206,7 @@
{ {
"data": { "data": {
"text/plain": [ "text/plain": [
"{(1, 7, 7, 7, 7, 7),\n", "{(1, 9, 9, 9), (3, 3, 9, 9), (3, 9, 3, 9), (3, 9, 9, 3)}"
" (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": 7, "execution_count": 7,
@ -208,7 +215,35 @@
} }
], ],
"source": [ "source": [
"fill_row(7**5, 6)" "fill_one_row(729, [30, 90, 90, 90])"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{(1, 7, 7, 7, 7, 7, 7, 7, 7),\n",
" (7, 1, 7, 7, 7, 7, 7, 7, 7),\n",
" (7, 7, 1, 7, 7, 7, 7, 7, 7),\n",
" (7, 7, 7, 1, 7, 7, 7, 7, 7),\n",
" (7, 7, 7, 7, 1, 7, 7, 7, 7),\n",
" (7, 7, 7, 7, 7, 1, 7, 7, 7),\n",
" (7, 7, 7, 7, 7, 7, 1, 7, 7),\n",
" (7, 7, 7, 7, 7, 7, 7, 1, 7),\n",
" (7, 7, 7, 7, 7, 7, 7, 7, 1)}"
]
},
"execution_count": 8,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"fill_one_row(7**8, [7]*9)"
] ]
}, },
{ {
@ -217,7 +252,7 @@
"source": [ "source": [
"# Solving a whole puzzle\n", "# Solving a whole puzzle\n",
"\n", "\n",
"- We can now solve a whole puzzle with a simple brute-force strategy:\n", "- We can solve a whole puzzle with a similar strategy.\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",
@ -228,7 +263,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 8, "execution_count": 9,
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
@ -242,9 +277,9 @@
" 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 and all(c == int(c) for c in col_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", " yield from ([row1, *rows]\n",
" for rows in solutions(Puzzle(row_prods[1:], divide(col_prods, row1))):\n", " for row1 in fill_one_row(row_prods[0], col_prods)\n",
" yield [row1, *rows]" " for rows in solutions(Puzzle(row_prods[1:], list(divide(col_prods, row1)))))"
] ]
}, },
{ {
@ -258,17 +293,18 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 9, "execution_count": 10,
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
{ {
"data": { "data": {
"text/plain": [ "text/plain": [
"[[(3, 9, 5), (5, 9, 1), (8, 1, 8), (5, 7, 8), (5, 7, 2)],\n", "[[(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)]]" " [(7, 6, 5), (9, 8, 2), (3, 9, 2), (5, 9, 3), (1, 4, 1), (7, 1, 7)],\n",
" [(7, 8, 5), (3, 8, 7), (9, 6, 3), (9, 8, 5), (3, 5, 4), (4, 8, 8), (9, 2, 7)]]"
] ]
}, },
"execution_count": 9, "execution_count": 10,
"metadata": {}, "metadata": {},
"output_type": "execute_result" "output_type": "execute_result"
} }
@ -281,12 +317,14 @@
"cell_type": "markdown", "cell_type": "markdown",
"metadata": {}, "metadata": {},
"source": [ "source": [
"Those are the correct solutions. However, we could make the solutions prettier:" "# Prettier solutions\n",
"\n",
"Those are the correct solutions. However, we could make them look nicer:"
] ]
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 10, "execution_count": 11,
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
@ -309,7 +347,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 11, "execution_count": 12,
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
{ {
@ -330,13 +368,23 @@
"|3|9|2|**54**|\n", "|3|9|2|**54**|\n",
"|5|9|3|**135**|\n", "|5|9|3|**135**|\n",
"|1|4|1|**4**|\n", "|1|4|1|**4**|\n",
"|7|1|7|**49**|" "|7|1|7|**49**|\n",
"\n",
"|183708|245760|117600||\n",
"|---|---|---|---|\n",
"|7|8|5|**280**|\n",
"|3|8|7|**168**|\n",
"|9|6|3|**162**|\n",
"|9|8|5|**360**|\n",
"|3|5|4|**60**|\n",
"|4|8|8|**256**|\n",
"|9|2|7|**126**|"
], ],
"text/plain": [ "text/plain": [
"<IPython.core.display.Markdown object>" "<IPython.core.display.Markdown object>"
] ]
}, },
"execution_count": 11, "execution_count": 12,
"metadata": {}, "metadata": {},
"output_type": "execute_result" "output_type": "execute_result"
} }
@ -349,18 +397,18 @@
"cell_type": "markdown", "cell_type": "markdown",
"metadata": {}, "metadata": {},
"source": [ "source": [
"# Making new well-formed puzzles\n", "# Making new puzzles\n",
"\n", "\n",
"Can we make new well-formed puzzles (those with exactly one solution)? One approach is to:\n", "Can we make new puzzles? Can we make well-formed ones (those with exactly one solution)? Here is an approach:\n",
"- Make a table filled with random digits (`random_table`).\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", "- 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 `N` times (`random_puzzles`).\n",
"- Repeat `ntables` times (`random_puzzles`)." "- Optionally, check if puzzles are `well-formed`.\n"
] ]
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 12, "execution_count": 13,
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
@ -374,31 +422,29 @@
" return Puzzle([prod(row) for row in table], \n", " return Puzzle([prod(row) for row in table], \n",
" [prod(col) for col in transpose(table)])\n", " [prod(col) for col in transpose(table)])\n",
"\n", "\n",
"def random_puzzles(N, nrows, ncols) -> List[Puzzle]: \n",
" \"Return a list of `N` random puzzles.\"\n",
" return [table_puzzle(random_table(nrows, ncols)) for _ in range(N)]\n",
"\n",
"def well_formed(puzzle) -> bool: \n", "def well_formed(puzzle) -> bool: \n",
" \"Does the puzzle have exactly one solution?\"\n", " \"Does the puzzle have exactly one solution?\"\n",
" S = solutions(puzzle)\n", " S = solutions(puzzle)\n",
" first, second = next(S, None), next(S, None)\n", " first, second = next(S, None), next(S, None)\n",
" return first is not None and second is None\n", " return first is not None and second is None"
"\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", "cell_type": "code",
"execution_count": 13, "execution_count": 14,
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
{ {
"data": { "data": {
"text/plain": [ "text/plain": [
"[(4, 2, 5), (6, 7, 9)]" "[(5, 3, 8), (7, 2, 1)]"
] ]
}, },
"execution_count": 13, "execution_count": 14,
"metadata": {}, "metadata": {},
"output_type": "execute_result" "output_type": "execute_result"
} }
@ -409,16 +455,16 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 14, "execution_count": 15,
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
{ {
"data": { "data": {
"text/plain": [ "text/plain": [
"Puzzle(row_prods=[40, 378], col_prods=[24, 14, 45])" "Puzzle(row_prods=[120, 14], col_prods=[35, 6, 8])"
] ]
}, },
"execution_count": 14, "execution_count": 15,
"metadata": {}, "metadata": {},
"output_type": "execute_result" "output_type": "execute_result"
} }
@ -429,16 +475,16 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 15, "execution_count": 16,
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
{ {
"data": { "data": {
"text/plain": [ "text/plain": [
"True" "False"
] ]
}, },
"execution_count": 15, "execution_count": 16,
"metadata": {}, "metadata": {},
"output_type": "execute_result" "output_type": "execute_result"
} }
@ -448,36 +494,10 @@
] ]
}, },
{ {
"cell_type": "code", "cell_type": "markdown",
"execution_count": 16,
"metadata": {}, "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": [ "source": [
"list(random_puzzles(200, 5, 3))" "How likely are random puzzles (of various sizes) to be well-formed?"
] ]
}, },
{ {
@ -486,137 +506,123 @@
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
{ {
"data": { "name": "stdout",
"text/plain": [ "output_type": "stream",
"[Puzzle(row_prods=[210, 56, 75, 40, 180, 189], col_prods=[875, 51030, 26880]),\n", "text": [
" Puzzle(row_prods=[324, 245, 432, 200, 50, 6], col_prods=[1680, 12600, 97200])]" "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"
] ]
},
"execution_count": 17,
"metadata": {},
"output_type": "execute_result"
} }
], ],
"source": [ "source": [
"list(random_puzzles(200, 6, 3))" "N = 300\n",
] "for r, c in [(3, 3), (3, 4), (4, 3), (3, 5), (5, 3), (4, 4), (6, 3), (5, 4)]:\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')"
"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", "cell_type": "markdown",
"metadata": {}, "metadata": {},
"source": [ "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", "We see that most puzzles are not well-formed. Smaller sizes are more likely to yield well-formed puzzles.\n",
"\n", "\n",
"How fast is it to find a solution? Very fast for small puzzles:" "# Speed\n",
"\n",
"How fast is it to solve a random puzzle? We can do a hundred small (5x3) puzzles in under a second:"
] ]
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 19, "execution_count": 18,
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
{ {
"name": "stdout", "name": "stdout",
"output_type": "stream", "output_type": "stream",
"text": [ "text": [
"CPU times: user 3.45 ms, sys: 802 µs, total: 4.26 ms\n", "CPU times: user 179 ms, sys: 7.47 ms, total: 187 ms\n",
"Wall time: 3.54 ms\n" "Wall time: 181 ms\n"
] ]
}, },
{ {
"data": { "data": {
"text/plain": [ "text/plain": [
"[[(3, 9, 5), (5, 9, 1), (8, 1, 8), (5, 7, 8), (5, 7, 2)],\n", "100"
" [(7, 6, 5), (9, 8, 2), (3, 9, 2), (5, 9, 3), (1, 4, 1), (7, 1, 7)]]"
] ]
}, },
"execution_count": 19, "execution_count": 18,
"metadata": {}, "metadata": {},
"output_type": "execute_result" "output_type": "execute_result"
} }
], ],
"source": [ "source": [
"%time [solve(p) for p in puzzles]" "%time len([solve(p) for p in random_puzzles(100, 5, 3)])"
] ]
}, },
{ {
"cell_type": "markdown", "cell_type": "markdown",
"metadata": {}, "metadata": {},
"source": [ "source": [
"For larger puzzles it is slower. For example, a 10 x 5 puzzle usually takes between 1/10 second and 10 seconds:" "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:"
] ]
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 20, "execution_count": 19,
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
{ {
"name": "stdout", "name": "stdout",
"output_type": "stream", "output_type": "stream",
"text": [ "text": [
"CPU times: user 124 ms, sys: 3.91 ms, total: 128 ms\n", "CPU times: user 5.94 ms, sys: 237 µs, total: 6.18 ms\n",
"Wall time: 126 ms\n" "Wall time: 6 ms\n"
]
}
],
"source": [
"p = table_puzzle(random_table(10, 5))\n",
"%time t = solve(p)"
] ]
}, },
{
"cell_type": "code",
"execution_count": 21,
"metadata": {},
"outputs": [
{ {
"data": { "data": {
"text/markdown": [ "text/markdown": [
"|1814400|645120|77157360|829440|1905120||\n", "|645120|201600|1382400|1290240||\n",
"|---|---|---|---|---|---|\n", "|---|---|---|---|---|\n",
"|3|8|6|6|7|**6048**|\n", "|2|3|5|5|**150**|\n",
"|3|4|5|3|4|**720**|\n", "|3|4|4|4|**192**|\n",
"|2|9|7|6|9|**6804**|\n", "|6|5|2|3|**180**|\n",
"|6|4|9|4|5|**4320**|\n", "|8|4|4|8|**1024**|\n",
"|4|2|7|5|8|**2240**|\n", "|8|4|4|8|**1024**|\n",
"|4|7|6|8|3|**4032**|\n", "|1|5|2|4|**40**|\n",
"|5|5|9|2|7|**3150**|\n", "|1|2|9|6|**108**|\n",
"|7|4|6|3|3|**1512**|\n", "|5|1|3|1|**15**|\n",
"|6|1|2|8|1|**96**|\n", "|7|3|8|7|**1176**|\n",
"|5|2|9|1|3|**270**|" "|8|7|5|2|**560**|"
], ],
"text/plain": [ "text/plain": [
"<IPython.core.display.Markdown object>" "<IPython.core.display.Markdown object>"
] ]
}, },
"execution_count": 21, "execution_count": 19,
"metadata": {}, "metadata": {},
"output_type": "execute_result" "output_type": "execute_result"
} }
], ],
"source": [ "source": [
"Markdown(pretty(p, t))" "puzz = random_puzzles(1, 10, 4)[0]\n",
"%time Markdown(pretty(puzz))"
]
},
{
"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."
] ]
}, },
{ {
@ -630,7 +636,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 22, "execution_count": 20,
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
{ {
@ -639,7 +645,7 @@
"True" "True"
] ]
}, },
"execution_count": 22, "execution_count": 20,
"metadata": {}, "metadata": {},
"output_type": "execute_result" "output_type": "execute_result"
} }
@ -647,11 +653,11 @@
"source": [ "source": [
"def test():\n", "def test():\n",
" \"Test suite for CrossProduct functions.\"\n", " \"Test suite for CrossProduct functions.\"\n",
" assert fill_row(1, 0) == {()}\n", " assert fill_one_row(1, []) == {()}\n",
" assert fill_row(2, 0) == set()\n", " assert fill_one_row(2, []) == set()\n",
" assert fill_row(9, 1) == {(9,)}\n", " assert fill_one_row(9, [9]) == {(9,)}\n",
" assert fill_row(10, 1) == set()\n", " assert fill_one_row(10, [10]) == set()\n",
" assert fill_row(73, 3) == set()\n", " assert fill_one_row(73, [360, 360, 360]) == set()\n",
" \n", " \n",
" assert solve(Puzzle([], [])) == []\n", " assert solve(Puzzle([], [])) == []\n",
" assert solve(Puzzle([], [1])) == []\n", " assert solve(Puzzle([], [1])) == []\n",
@ -660,23 +666,14 @@
" assert solve(Puzzle([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",
" assert solve(Puzzle([2, 12], [3, 8])) == [(1, 2), (3, 4)]\n", " assert solve(Puzzle([2, 12], [3, 8])) == [(1, 2), (3, 4)]\n",
"\n", "\n",
" assert fill_row(729, 3) == {(9, 9, 9)} # Unique fill\n", " assert fill_one_row(729, [90, 126, 81]) == {(9, 9, 9)} # Unique fill\n",
" \n", " \n",
" assert fill_row(729, 4) == {\n", " assert fill_one_row(729, [90, 126, 81, 30]) == {\n",
" (1, 9, 9, 9),\n", " (3, 9, 9, 3), (9, 3, 9, 3), (9, 9, 3, 3), (9, 9, 9, 1)}\n",
" (3, 3, 9, 9),\n",
" (3, 9, 3, 9),\n",
" (3, 9, 9, 3),\n",
" (9, 1, 9, 9),\n",
" (9, 3, 3, 9),\n",
" (9, 3, 9, 3),\n",
" (9, 9, 1, 9),\n",
" (9, 9, 3, 3),\n",
" (9, 9, 9, 1)}\n",
" \n", " \n",
" # 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 max(range(1, 100), key=lambda n: len(fill_one_row(n, [5*7*8*9]*3))) == 72\n",
" assert fill_row(72, 3) == { \n", " assert fill_one_row(72, [72, 72, 72]) == { \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",