diff --git a/ipynb/RiddlerLottery.ipynb b/ipynb/RiddlerLottery.ipynb
index aecbf52..aa7f1e2 100644
--- a/ipynb/RiddlerLottery.ipynb
+++ b/ipynb/RiddlerLottery.ipynb
@@ -4,55 +4,48 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "\n",
- "
Peter Norvig
Nov 2019
\n",
+ "Peter Norvig
Nov 2019
revised May 2021
\n",
"\n",
"# Riddler Lottery\n",
"\n",
"The 538 Riddler [poses](https://fivethirtyeight.com/features/can-you-decode-the-riddler-lottery/) this problem:\n",
"\n",
- "> Five friends with a lot in common are playing the [Riddler Lottery](https://fivethirtyeight.com/features/can-you-decode-the-riddler-lottery/), in which each must choose exactly five numbers from 1 to 70. After they all picked their numbers,\n",
- "- The first friend notices that no number was selected by two or more friends. \n",
- "- The second friend observes that all 25 selected numbers are composite (i.e., not prime). \n",
- "- The third friend points out that each selected number has at least two distinct prime factors. \n",
- "- The fourth friend excitedly remarks that the product of selected numbers on each ticket is exactly the same. \n",
- "- The fifth friend is left speechless. (You can tell why all these people are friends.)\n",
+ "> Five friends are playing the [Riddler Lottery](https://fivethirtyeight.com/features/can-you-decode-the-riddler-lottery/), in which each player selects exactly five integers from 1 to 70. After they all picked their numbers,\n",
+ "1. The first player states that no number was selected by two or more players. \n",
+ "2. The second player observes that all the selected numbers are composite (i.e., not prime). \n",
+ "3. The third player points out that each selected number has at least two distinct prime factors. \n",
+ "4. The fourth player notes that each player's 5 numbers multiply to the same product. \n",
+ "5. The fifth player is left speechless. \n",
+ ">\n",
+ "> That leads to two questions:\n",
+ "> - What is the unique product, $P$, that each player's 5 numbers multiply together to?\n",
+ "> - In how many different ways could the selections be made so that the statements above are true?\n",
"\n",
- "> 1. What is the product of the selected numbers on each ticket?\n",
- "2. How many _different_ ways could the friends have selected five numbers each so that all their statements are true?\n",
+ "# Analysis\n",
"\n",
- "# Preliminary Analysis\n",
+ "As an example, consider a version of the problem where each player selects 2 numbers, not 5. Here is one solution:\n",
"\n",
- "The fourth friend's statement was a bit unclear, but I take it to mean that each friend multiplied together their own five numbers, and they all got the same product. To be concrete, here's an example of a solution in a simplified version of the problem where each friend only selects two tickets, not five:\n",
+ " \n",
+ "|Player |Selection| Product |Prime Factors|\n",
+ "|-|-|-|-|\n",
+ "|1 | 6, 60 | 360 | {2, 3}, {2, 3, 5}\n",
+ "|2 | 10, 36 | 360 | {2, 5}, {2, 3}\n",
+ "|3 | 12, 30 | 360 | {2, 3}, {2, 3, 5}\n",
+ "|4 | 15, 24 | 360 | {3, 5}, {2, 3}\n",
+ "|5 | 18, 20 | 360 | {2, 3}, {2, 5}\n",
"\n",
- " Friend Selection Product Factors\n",
- " 1 ( 6, 60) 360 [2, 3] + [2, 2, 3, 5]\n",
- " 2 (10, 36) 360 [2, 5] + [2, 2, 3, 3]\n",
- " 3 (12, 30) 360 [2, 2, 3] + [2, 3, 5]\n",
- " 4 (15, 24) 360 [3, 5] + [2, 2, 2, 3]\n",
- " 5 (18, 20) 360 [2, 3, 3] + [2, 2, 5]\n",
"\n",
- "And here's a list of the key concepts:\n",
"\n",
- "- **number**: An integer from 1 to 70, e.g. the int `42`.\n",
- "- **factors**: Every positive integer has a unique prime factorization, e.g. `factors(12) == [2, 2, 3]`: two distinct primes factors. But `factors(8) == [2, 2, 2]`: one distinct prime factor.\n",
- "- **selection**: A collection of 5 numbers, e.g. the sorted tuple `(12, 15, 20, 28, 30)`.\n",
- "- **product**: The result of multiplying together the 5 numbers in a selection, e.g. the int `3024000`.\n",
- "- **candidate**: A candidate solution is a set of 5 selections e.g. `{( 6, 60), (10, 36), (12, 30), (15, 24), (18, 20)}` in my simplified version where each selection has only two numbers.\n",
- "- **solution**: A solution is a candidate that satisifes each of the four friends' statements.\n",
+ "The key concepts:\n",
"\n",
- "Can I use brute force and enumerate all the possible candidates? \n",
+ "- **Numbers**: The integers from 1 to 70. \n",
+ "- **Selection**: A sorted tuple of numbers, e.g. `(6, 60)` for 2 numbers, or `(12, 15, 20, 28, 30)` for 5.\n",
+ "- **Candidate**: A set of 5 selections e.g. `{(6, 60), (10, 36), (12, 30), (15, 24), (18, 21)}`.\n",
+ "- **Solution**: A candidate that satisfies statements 1–4.\n",
+ "- **Distinct prime factors**: `factors(20) == {2, 5}` and `factors(9) == {3}`, so 20 is valid but not 9.\n",
+ "- **Product**: the result of multiplying numbers together, e.g. `prod((6, 60)) == 360`.\n",
"\n",
- "There are (70 choose 5) × (65 choose 5) × (60 choose 5) × (55 choose 5) × (50 choose 5) / 5! or [about](https://www.wolframalpha.com/input/?i=%2870+choose+5%29+*+%2865+choose+5%29+*+%2860+choose+5%29+*+%2855+choose+5%29+*+%2850+choose+5%29+%2F+5%21) $10^{31}$ candidates, so no. We'll have to be more clever."
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# Valid Numbers\n",
- "\n",
- "There will be fewer candidates to consider if we can reduce the number of valid numbers to select from. The __third__ friend stated that the numbers all have at least two distinct prime factors. So let's find the numbers that have that property. (The numbers we are dealing with are small, so don't worry about the inefficiency of my function `factors`.)"
+ "An implementation of the key concepts:\n"
]
},
{
@@ -61,8 +54,51 @@
"metadata": {},
"outputs": [],
"source": [
- "from itertools import combinations\n",
- "from collections import Counter, defaultdict"
+ "from typing import *\n",
+ "\n",
+ "numbers = range(1, 71)\n",
+ "Selection = Tuple[int, ...] # 5 ints in the full puzzle\n",
+ "Candidate = Set[Selection]\n",
+ "Solution = Set[Selection]\n",
+ "\n",
+ "def factors(n) -> Set[int]:\n",
+ " \"\"\"The set of distinct prime factors of n.\"\"\"\n",
+ " if n == 1:\n",
+ " return set()\n",
+ " else:\n",
+ " p = next(i for i in range(2, n + 1) if n % i == 0)\n",
+ " return {p, *factors(n // p)}\n",
+ " \n",
+ "def prod(numbers: Iterable[int]) -> int:\n",
+ " \"\"\"The product of a collection of numbers.\"\"\"\n",
+ " result = 1\n",
+ " for n in numbers:\n",
+ " result *= n\n",
+ " return result"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Brute force solution?\n",
+ "\n",
+ "Could we generate every possible candidate, and test each one? There are (70 choose 5)5 or [about](https://www.google.com/search?q=%2870+choose+5%29^5) $10^{35}$ candidate solutions, so: **NO**. \n",
+ "\n",
+ "We'll have to be more clever. I have three ideas; I will consider only:\n",
+ "\n",
+ "- Numbers with at least 2 prime factors.\n",
+ "- Factors with at least 5 valid numbers.\n",
+ "- Sets of 25 numbers whose product is a fifth power."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Numbers with at least 2 prime factors\n",
+ "\n",
+ "Every valid numbers must have at least two distinct prime factors (by statement 3):"
]
},
{
@@ -71,37 +107,37 @@
"metadata": {},
"outputs": [
{
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "41 {6, 10, 12, 14, 15, 18, 20, 21, 22, 24, 26, 28, 30, 33, 34, 35, 36, 38, 39, 40, 42, 44, 45, 46, 48, 50, 51, 52, 54, 55, 56, 57, 58, 60, 62, 63, 65, 66, 68, 69, 70}\n"
- ]
+ "data": {
+ "text/plain": [
+ "41"
+ ]
+ },
+ "execution_count": 2,
+ "metadata": {},
+ "output_type": "execute_result"
}
],
"source": [
- "def factors(n) -> list:\n",
- " \"List of prime factors that multiply together to give n.\"\n",
- " return ([] if n == 1\n",
- " else next([p] + factors(n // p) \n",
- " for p in range(2, n + 1) if n % p == 0))\n",
+ "numbers = [n for n in numbers if len(factors(n)) >= 2]\n",
"\n",
- "distinct = set\n",
- "\n",
- "numbers = {n for n in range(1, 71) if len(distinct(factors(n))) >= 2}\n",
- "\n",
- "print(len(numbers), numbers)"
+ "len(numbers)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "Great; we got it down from 70 to 41 possible numbers.\n",
+ "Good! We got it down from 70 to 41 valid numbers.\n",
"\n",
- "Now the __fourth__ friend's statement says that each friend picks five numbers that have the same product. \n",
- "In my simplified version where the friends pick two numbers each, they all picked a selection with the product 360. The prime factorization of 360 is `[2, 2, 2, 3, 3, 5]`; that means that all five friends had to find a different way of allocating these factors to their numbers. \n",
+ "# Factors with at least 5 valid numbers\n",
"\n",
- "For each number that is selected by any friend, and for each prime factor $p$ of that number, it must be the case that there are at least four other valid numbers that also have $p$ as a factor; otherwise the product couldn't be the same for all the friends. So let's count in how many numbers each prime appears:"
+ "All five players make a selection with the same product (by statement 4). Therefore, if one player selects a number that has the factor $p$, then that player's product will be divisible by $p$, and therefore every player must select some number that has the factor $p$, otherwise their product would be different.\n",
+ "\n",
+ "For example, the valid numbers with 11 as a factor are {22, 33, 44, 55, 66}, so if one player selects one of those numbers, then the other players must select the others.\n",
+ "\n",
+ "The valid numbers with 13 as a factor are {26, 39, 52, 65}, so no player can select any of those numbers, because there aren't enough of them for all five players.\n",
+ "\n",
+ "So let's count how many valid numbers each prime factor appears in:"
]
},
{
@@ -131,18 +167,14 @@
}
],
"source": [
- "prime_counts = Counter(p for n in numbers for p in distinct(factors(n)))\n",
- "\n",
- "prime_counts"
+ "Counter(p for n in numbers for p in factors(n))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "This says that the prime factor 2 appears in 29 valid numbers and the prime factor 31 appears in only 1 valid number. Only factors that appear in at least 5 valid numbers can be part of a solution, so that's `{2, 3, 5, 7, 11}`.\n",
- "\n",
- "Let's update the set of valid numbers to contain only numbers $n$ such that every prime factor $p$ of $n$ appears in at least 5 valid numbers:"
+ "Only the prime factors `{2, 3, 5, 7, 11}` have a count of at least 5. Let's update the set of valid numbers to contain only numbers made from valid factors:"
]
},
{
@@ -151,96 +183,91 @@
"metadata": {},
"outputs": [
{
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "28 {6, 10, 12, 14, 15, 18, 20, 21, 22, 24, 28, 30, 33, 35, 36, 40, 42, 44, 45, 48, 50, 54, 55, 56, 60, 63, 66, 70}\n"
- ]
+ "data": {
+ "text/plain": [
+ "28"
+ ]
+ },
+ "execution_count": 4,
+ "metadata": {},
+ "output_type": "execute_result"
}
],
"source": [
- "numbers = {n for n in numbers if all(prime_counts[p] >= 5 for p in distinct(factors(n)))}\n",
+ "valid_factors = {2, 3, 5, 7, 11}\n",
"\n",
- "print(len(numbers), numbers)"
+ "numbers = [n for n in numbers if factors(n).issubset(valid_factors)]\n",
+ "\n",
+ "len(numbers)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "There are now only 28 valid numbers; a nice reduction from 70 to 41 to 28.\n",
+ "Great! Now we're down to 28 numbers! \n",
"\n",
- "# Valid Selections\n",
+ "# Sets of 25 numbers whose product is a fifth power\n",
"\n",
- "Now let's switch attention from individual numbers to selections of five numbers. There are (28 choose 5) = 98,280 possible selections; a manageable number. But that means there are (98,280 choose 5) = $\\approx 10^{23}$ candidate solutions; an unmanageable number.\n",
+ "The five players will together select 25 of these 28 valid numbers, and since there are only (28 choose 25) = 3,276 combinations of 25 numbers, we can quickly check to see which ones might lead to solutions, according to this reasoning:\n",
+ "- Every player's selection of 5 numbers has the same product, which we are calling $P$.\n",
+ "- Therefore the product of all 25 numbers in a solution must be $P^5$ (although we don't yet know what $P$ is).\n",
+ "- A set of 25 numbers whose product is not a perfect fifth power **cannot** lead to any solutions.\n",
+ "- A set of 25 numbers whose product is a perfect fifth power **might** lead to solution(s).\n",
"\n",
- "My first thought to reduce the number of candidates is to say that we should only consider candidates where all five selections in the candidate have the same product. To do that, we can group selections by product.\n",
- "\n",
- "We'll make `products` be a `dict` where each key is the product of the five numbers in a selection, and the corresponding value is a list of all the selections of five numbers with that product:"
+ "We can check which combinations of 25 numbers multiply to a fifth power:"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
- "outputs": [],
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "1"
+ ]
+ },
+ "execution_count": 5,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
"source": [
- "def multimap(items, key) -> dict:\n",
- " \"A dict of {key(item): [item, ...]}\"\n",
- " result = defaultdict(list)\n",
- " for x in items:\n",
- " result[key(x)].append(x)\n",
- " return result\n",
+ "from itertools import combinations\n",
"\n",
- "def product(nums) -> int: \n",
- " \"Multiply nums together (similar to sum(nums)).\"\n",
- " result = 1\n",
- " for num in nums:\n",
- " result *= num\n",
- " return result\n",
+ "def is_fifth_power(i: int) -> bool: return i == round(i ** (1/5)) ** 5\n",
"\n",
- "products = multimap(combinations(numbers, 5), key=product)"
+ "result = [c for c in combinations(numbers, 25) if is_fifth_power(prod(c))]\n",
+ "\n",
+ "len(result)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "Here is an entry in the `products` dict:"
+ "There's only one combination of 25 numbers that works! \n",
+ "\n",
+ "That's good news; we can use the `result` to update `numbers` and compute $P$:"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- "[(6, 10, 12, 14, 30),\n",
- " (6, 10, 12, 15, 28),\n",
- " (6, 10, 12, 20, 21),\n",
- " (6, 10, 14, 15, 24),\n",
- " (6, 10, 14, 18, 20),\n",
- " (6, 12, 14, 15, 20)]"
- ]
- },
- "execution_count": 6,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
+ "outputs": [],
"source": [
- "products[6 * 12 * 14 * 15 * 20]"
+ "numbers = result[0]\n",
+ "P = round(prod(numbers) ** (1/5))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "This says there are 6 selections whose product is 6 * 12 * 14 * 15 * 20 = 302,400; if there were a way to choose 5 of these 6 with all distinct numbers, that would be a solution. Sadly, there is no such way. Since every one of the selections contains a 6; we can't even choose two disjoint selections, let alone five.\n",
- "\n",
- "Let's see how many different products there are, and how many have at least five selections:"
+ "# Answer to the first question"
]
},
{
@@ -251,7 +278,7 @@
{
"data": {
"text/plain": [
- "(4042, 2407)"
+ "19958400"
]
},
"execution_count": 7,
@@ -260,69 +287,77 @@
}
],
"source": [
- "len(products), len([n for n in products if len(products[n]) >= 5])"
+ "P"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "It seems reasonable to go through all the products and see if one of the 29,506 with 5 or more selections can come up with 5 disjoint selections. The function `k_disjoint(k, selections)` finds all ways to choose `k` different elements of `selections` such that there is no shared number among any selection. The function keeps track of a `partial_solution`—a set of previously-found selections—as it recursively searches for a complete solution. Any new selection must be disjoint from all the selections in `partial_solution`. "
+ "**19,958,400** is \"the unique product $P$ that each player's 5 numbers multiply to.\" \n",
+ "\n",
+ "At this point we know the 25 numbers that will form any solution:"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
- "outputs": [],
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "6 10 12 14 15 18 20 21 22 24 28 30 33 36 40 42 44 45 48 50 54 55 56 60 66\n"
+ ]
+ }
+ ],
"source": [
- "def k_disjoint(k, selections, start=0, partial_solution=set()) -> list:\n",
- " \"All ways of picking k elements of selections that have all disjoint members.\"\n",
- " if len(partial_solution) == k:\n",
- " yield partial_solution\n",
- " elif len(partial_solution) + (len(selections) - start) >= k:\n",
- " for i in range(start, len(selections)):\n",
- " selection = selections[i]\n",
- " if all(is_disjoint(selection, s) for s in partial_solution):\n",
- " yield from k_disjoint(k, selections, i + 1, partial_solution | {selection})\n",
- " \n",
- "def is_disjoint(A, B) -> bool: return not any(a in B for a in A)"
+ "print(*numbers)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "Here's an example of how `k_disjoint` works. Out of the following six selections (which in this simplified example have only two numbers each, not five), there are two ways to pick five selections without having a duplicate number:"
+ "However, we haven't found any solutions yet and we don't know how many solutions there are."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Finding solutions\n",
+ "\n",
+ "`solutions(numbers, P, r)` will search through combinations of `numbers` to generate all solutions such that every selection in the solution consists of `r` numbers with product $P$. \n",
+ "\n",
+ "`solutions` yields the empty solution if there are no numbers remaining to choose from. Otherwise it considers each way to combine a `first` selection with a set of `rest` selections, where the `first` is any `r` numbers and the `rest` is any set of selections formed from the numbers that \n",
+ "were not used in `first`, and are lexicographically greater than `first` (so that we don't generate multiple permutations of the same solution). "
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- "[{(6, 60), (10, 36), (12, 30), (15, 24), (18, 20)},\n",
- " {(6, 60), (10, 35), (12, 30), (15, 24), (18, 20)}]"
- ]
- },
- "execution_count": 9,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
+ "outputs": [],
"source": [
- "selections = [(6, 60), (10, 36), (12, 30), (15, 24), (18, 20), (10, 35)]\n",
- "list(k_disjoint(5, selections))"
+ "def solutions(numbers, P, r=5) -> Iterator[Solution]:\n",
+ " \"\"\"Yield solutions that are selections of `r` `numbers` with product `P`.\"\"\"\n",
+ " if not numbers:\n",
+ " yield set()\n",
+ " else:\n",
+ " yield from ({first, *rest}\n",
+ " for first in combinations(numbers, r)\n",
+ " if prod(first) == P\n",
+ " for rest in solutions([n for n in numbers \n",
+ " if n > first[0] and n not in first], P, r))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "But for the six selections whose product is 302,400, there are no disjoint solutions:"
+ "We can very quickly find a solution:"
]
},
{
@@ -330,10 +365,22 @@
"execution_count": 10,
"metadata": {},
"outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "CPU times: user 2.12 ms, sys: 2 µs, total: 2.12 ms\n",
+ "Wall time: 2.12 ms\n"
+ ]
+ },
{
"data": {
"text/plain": [
- "[]"
+ "{(6, 15, 56, 60, 66),\n",
+ " (10, 14, 48, 54, 55),\n",
+ " (12, 18, 42, 44, 50),\n",
+ " (20, 21, 33, 36, 40),\n",
+ " (22, 24, 28, 30, 45)}"
]
},
"execution_count": 10,
@@ -342,24 +389,14 @@
}
],
"source": [
- "selections = [(6, 10, 12, 14, 30),\n",
- " (6, 10, 12, 15, 28),\n",
- " (6, 10, 12, 20, 21),\n",
- " (6, 10, 14, 15, 24),\n",
- " (6, 10, 14, 18, 20),\n",
- " (6, 12, 14, 15, 20)]\n",
- "list(k_disjoint(5, selections))"
+ "%time next(solutions(numbers, P))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "Now we're ready to solve the problem.\n",
- "\n",
- "# 1. What is the product of the selected numbers on each ticket?\n",
- "\n",
- "That is, find the number `N` in `products` that can form 5 disjoint selections."
+ "But it takes longer (about 6 seconds) to find all the solutions:"
]
},
{
@@ -371,23 +408,20 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "The product is 19,958,400; factors are [2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 5, 5, 7, 11]\n"
+ "CPU times: user 6.36 s, sys: 13 ms, total: 6.37 s\n",
+ "Wall time: 6.37 s\n"
]
}
],
"source": [
- "N = next(n for n in products if any(k_disjoint(5, products[n])))\n",
- "\n",
- "print(f'The product is {N:,d}; factors are {factors(N)}')"
+ "%time all_solutions = list(solutions(numbers, P))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "# 2. How many different ways could the friends have selected five numbers?\n",
- "\n",
- "I'll compute all of the results for `k_disjoint`, and see how many there are:"
+ "# Answer to the second question"
]
},
{
@@ -396,107 +430,96 @@
"metadata": {},
"outputs": [
{
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "There are 12,781 different ways.\n"
- ]
+ "data": {
+ "text/plain": [
+ "12781"
+ ]
+ },
+ "execution_count": 12,
+ "metadata": {},
+ "output_type": "execute_result"
}
],
"source": [
- "different_ways = list(k_disjoint(5, products[N]))\n",
- "print(f'There are {len(different_ways):,d} different ways.')"
+ "len(all_solutions)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "That's too many to look at all of them, but I can peek at every thousandth one:"
+ "**12,781** is \"how many different ways could the selections be made so that the statements are true.\" \n",
+ "\n",
+ "(There is some ambiguity about what \"different\" means: 12,781 is the answer if you think of a set of players each choosing a set of numbers. If the ordering of the players matters, multiply by $5! = 120$, and if the ordering of the numbers in each selection matters, multiply by $5!^5$."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# To do\n",
+ "\n",
+ "- You could explore solutions with different numbers of players (default 5), selected numbers per player (default 5), range of valid numbers (default 1 to 70), and minimum number of distinct factors for selected numbers (default 2).\n",
+ "\n",
+ "\n",
+ "- You could answer the trivia question \"why didn't I just import [`numpy.prod`](https://numpy.org/doc/stable/reference/generated/numpy.prod.html)?\"\n",
+ "\n",
+ "\n",
+ "- If 6 seconds is too long to wait, you could speed up `solutions` by changing from the generate-and-test approach of looking at all `combinations(numbers, r)` and then checking to see if the product is `P`, to an incremental approach that only considers partial results that could lead to a product `P`.\n",
+ "\n",
+ "\n",
+ "- You could implement a completely different approach to solving the problem (actually the one I thought of first): \n",
+ " 1. Get the valid numbers down to 28.\n",
+ " 2. Consider all (28 choose 5) = 98,280 possible selections of 5 numbers.\n",
+ " 3. Group selections by their products, e.g., a dict: `{19958400: [(6, 15, 56, 60, 66), ...]}`.\n",
+ " 4. For each product in the dict that has at least 5 selections in its list:\n",
+ " - Find all combinations of 5 selections that consist of distinct numbers. \n",
+ " \n",
+ "\n",
+ "- You could add more unit tests to the following meager test suite:"
]
},
{
"cell_type": "code",
"execution_count": 13,
"metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- "[{(6, 15, 56, 60, 66),\n",
- " (10, 14, 48, 54, 55),\n",
- " (12, 18, 42, 44, 50),\n",
- " (20, 21, 33, 36, 40),\n",
- " (22, 24, 28, 30, 45)},\n",
- " {(6, 18, 55, 56, 60),\n",
- " (10, 21, 30, 48, 66),\n",
- " (12, 22, 28, 50, 54),\n",
- " (14, 20, 36, 44, 45),\n",
- " (15, 24, 33, 40, 42)},\n",
- " {(6, 20, 54, 55, 56),\n",
- " (10, 21, 36, 44, 60),\n",
- " (12, 28, 33, 40, 45),\n",
- " (14, 18, 24, 50, 66),\n",
- " (15, 22, 30, 42, 48)},\n",
- " {(6, 21, 48, 50, 66),\n",
- " (10, 24, 33, 45, 56),\n",
- " (12, 18, 28, 55, 60),\n",
- " (14, 20, 30, 44, 54),\n",
- " (15, 22, 36, 40, 42)},\n",
- " {(6, 22, 50, 54, 56),\n",
- " (10, 14, 36, 60, 66),\n",
- " (12, 18, 40, 42, 55),\n",
- " (15, 21, 30, 44, 48),\n",
- " (20, 24, 28, 33, 45)},\n",
- " {(6, 24, 42, 50, 66),\n",
- " (10, 18, 33, 56, 60),\n",
- " (12, 28, 30, 36, 55),\n",
- " (14, 15, 40, 44, 54),\n",
- " (20, 21, 22, 45, 48)},\n",
- " {(6, 28, 30, 60, 66),\n",
- " (10, 15, 44, 54, 56),\n",
- " (12, 22, 36, 42, 50),\n",
- " (14, 24, 33, 40, 45),\n",
- " (18, 20, 21, 48, 55)},\n",
- " {(6, 28, 40, 45, 66),\n",
- " (10, 30, 33, 42, 48),\n",
- " (12, 21, 24, 55, 60),\n",
- " (14, 18, 36, 44, 50),\n",
- " (15, 20, 22, 54, 56)},\n",
- " {(6, 28, 44, 50, 54),\n",
- " (10, 21, 33, 48, 60),\n",
- " (12, 14, 40, 45, 66),\n",
- " (15, 22, 30, 36, 56),\n",
- " (18, 20, 24, 42, 55)},\n",
- " {(6, 30, 36, 55, 56),\n",
- " (10, 15, 42, 48, 66),\n",
- " (12, 28, 33, 40, 45),\n",
- " (14, 20, 22, 54, 60),\n",
- " (18, 21, 24, 44, 50)},\n",
- " {(6, 30, 44, 45, 56),\n",
- " (10, 15, 42, 48, 66),\n",
- " (12, 14, 40, 54, 55),\n",
- " (18, 24, 28, 33, 50),\n",
- " (20, 21, 22, 36, 60)},\n",
- " {(6, 33, 40, 45, 56),\n",
- " (10, 12, 42, 60, 66),\n",
- " (14, 22, 24, 50, 54),\n",
- " (15, 28, 30, 36, 44),\n",
- " (18, 20, 21, 48, 55)},\n",
- " {(6, 36, 40, 42, 55),\n",
- " (10, 20, 33, 54, 56),\n",
- " (12, 18, 28, 50, 66),\n",
- " (14, 15, 44, 45, 48),\n",
- " (21, 22, 24, 30, 60)}]"
- ]
- },
- "execution_count": 13,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
+ "outputs": [],
"source": [
- "different_ways[::1000]"
+ "def is_solution(candidate) -> bool:\n",
+ " \"\"\"Is this a valid solution?\"\"\"\n",
+ " numbers = [n for selection in candidate for n in selection]\n",
+ " products = {prod(selection) for selection in candidate}\n",
+ " return (len(numbers) == len(set(numbers)) # Statement 1\n",
+ " and all(len(factors(n)) >= 2 for n in numbers) # Statement 3\n",
+ " and len(products) == 1) # Statement 4\n",
+ "\n",
+ "assert factors(9) == {3}\n",
+ "assert factors(10) == {2, 5}\n",
+ "assert factors(42) == {2, 3, 7}\n",
+ "assert factors(41) == {41}\n",
+ "assert factors(1) == set()\n",
+ "assert factors(168000) == {2, 3, 5, 7}\n",
+ "\n",
+ "assert prod([2, 3, 7]) == 42\n",
+ "assert prod([41]) == 41\n",
+ "assert prod([]) == 1\n",
+ "assert prod([2, 3, 2, 5, 2, 5, 2, 5, 2, 7, 2]) == 168000\n",
+ "\n",
+ "assert is_fifth_power(100000)\n",
+ "assert is_fifth_power(1234567890 ** 5)\n",
+ "assert not is_fifth_power(99999)\n",
+ "assert not is_fifth_power(1234567890 ** 5 + 1)\n",
+ "\n",
+ "assert P == 19958400\n",
+ "assert numbers == (6, 10, 12, 14, 15, 18, 20, 21, 22, 24, 28, 30, 33, \n",
+ " 36, 40, 42, 44, 45, 48, 50, 54, 55, 56, 60, 66)\n",
+ "assert len(all_solutions) == 12781\n",
+ "assert all(is_solution(s) for s in all_solutions)\n",
+ "\n",
+ "assert is_solution({(6, 60), (10, 36), (12, 30), (15, 24), (18, 20)})\n",
+ "assert not is_solution({(6, 60), (10, 60), (12, 30), (15, 24), (18, 20)}) #1\n",
+ "assert not is_solution({(9, 40), (10, 36), (12, 30), (15, 24), (18, 20)}) #3\n",
+ "assert not is_solution({(7, 60), (10, 36), (12, 30), (15, 24), (18, 20)}) #4"
]
}
],
@@ -516,7 +539,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.7.2"
+ "version": "3.7.6"
}
},
"nbformat": 4,