The 538 Riddler poses this problem:
Five friends with a lot in common are playing the Riddler Lottery, in which each must choose exactly five numbers from 1 to 70. After they all picked their numbers,
- What is the product of the selected numbers on each ticket?
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:
Friend Selection Product Factors
1 ( 6, 60) 360 [2, 3] + [2, 2, 3, 5]
2 (10, 36) 360 [2, 5] + [2, 2, 3, 3]
3 (12, 30) 360 [2, 2, 3] + [2, 3, 5]
4 (15, 24) 360 [3, 5] + [2, 2, 2, 3]
5 (18, 20) 360 [2, 3, 3] + [2, 2, 5]
And here's a list of the key concepts:
42
.factors(12) == [2, 2, 3]
: two distinct primes factors. But factors(8) == [2, 2, 2]
: one distinct prime factor.(12, 15, 20, 28, 30)
.3024000
.{( 6, 60), (10, 36), (12, 30), (15, 24), (18, 20)}
in my simplified version where each selection has only two numbers.Can I use brute force and enumerate all the possible candidates?
There are (70 choose 5) × (65 choose 5) × (60 choose 5) × (55 choose 5) × (50 choose 5) / 5! or about $10^{31}$ candidates, so no. We'll have to be more clever.
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
.)
from itertools import combinations
from collections import Counter, defaultdict
def factors(n) -> list:
"List of prime factors that multiply together to give n."
return ([] if n == 1
else next([p] + factors(n // p)
for p in range(2, n + 1) if n % p == 0))
distinct = set
numbers = {n for n in range(1, 71) if len(distinct(factors(n))) >= 2}
print(len(numbers), numbers)
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}
Great; we got it down from 70 to 41 possible numbers.
Now the fourth friend's statement says that each friend picks five numbers that have the same product.
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.
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:
prime_counts = Counter(p for n in numbers for p in distinct(factors(n)))
prime_counts
Counter({2: 29, 3: 20, 5: 12, 7: 8, 11: 5, 13: 4, 17: 3, 19: 2, 23: 2, 29: 1, 31: 1})
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}
.
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:
numbers = {n for n in numbers if all(prime_counts[p] >= 5 for p in distinct(factors(n)))}
print(len(numbers), numbers)
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}
There are now only 28 valid numbers; a nice reduction from 70 to 41 to 28.
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.
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.
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:
def multimap(items, key) -> dict:
"A dict of {key(item): [item, ...]}"
result = defaultdict(list)
for x in items:
result[key(x)].append(x)
return result
def product(nums) -> int:
"Multiply nums together (similar to sum(nums))."
result = 1
for num in nums:
result *= num
return result
products = multimap(combinations(numbers, 5), key=product)
Here is an entry in the products
dict:
products[6 * 12 * 14 * 15 * 20]
[(6, 10, 12, 14, 30), (6, 10, 12, 15, 28), (6, 10, 12, 20, 21), (6, 10, 14, 15, 24), (6, 10, 14, 18, 20), (6, 12, 14, 15, 20)]
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.
Let's see how many different products there are, and how many have at least five selections:
len(products), len([n for n in products if len(products[n]) >= 5])
(4042, 2407)
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
.
def k_disjoint(k, selections, start=0, partial_solution=set()) -> list:
"All ways of picking k elements of selections that have all disjoint members."
if len(partial_solution) == k:
yield partial_solution
elif len(partial_solution) + (len(selections) - start) >= k:
for i in range(start, len(selections)):
selection = selections[i]
if all(is_disjoint(selection, s) for s in partial_solution):
yield from k_disjoint(k, selections, i + 1, partial_solution | {selection})
def is_disjoint(A, B) -> bool: return not any(a in B for a in A)
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:
selections = [(6, 60), (10, 36), (12, 30), (15, 24), (18, 20), (10, 35)]
list(k_disjoint(5, selections))
[{(6, 60), (10, 36), (12, 30), (15, 24), (18, 20)}, {(6, 60), (10, 35), (12, 30), (15, 24), (18, 20)}]
But for the six selections whose product is 302,400, there are no disjoint solutions:
selections = [(6, 10, 12, 14, 30),
(6, 10, 12, 15, 28),
(6, 10, 12, 20, 21),
(6, 10, 14, 15, 24),
(6, 10, 14, 18, 20),
(6, 12, 14, 15, 20)]
list(k_disjoint(5, selections))
[]
Now we're ready to solve the problem.
That is, find the number N
in products
that can form 5 disjoint selections.
N = next(n for n in products if any(k_disjoint(5, products[n])))
print(f'The product is {N:,d}; factors are {factors(N)}')
The product is 19,958,400; factors are [2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 5, 5, 7, 11]
I'll compute all of the results for k_disjoint
, and see how many there are:
different_ways = list(k_disjoint(5, products[N]))
print(f'There are {len(different_ways):,d} different ways.')
There are 12,781 different ways.
That's too many to look at all of them, but I can peek at every thousandth one:
different_ways[::1000]
[{(6, 15, 56, 60, 66), (10, 14, 48, 54, 55), (12, 18, 42, 44, 50), (20, 21, 33, 36, 40), (22, 24, 28, 30, 45)}, {(6, 18, 55, 56, 60), (10, 21, 30, 48, 66), (12, 22, 28, 50, 54), (14, 20, 36, 44, 45), (15, 24, 33, 40, 42)}, {(6, 20, 54, 55, 56), (10, 21, 36, 44, 60), (12, 28, 33, 40, 45), (14, 18, 24, 50, 66), (15, 22, 30, 42, 48)}, {(6, 21, 48, 50, 66), (10, 24, 33, 45, 56), (12, 18, 28, 55, 60), (14, 20, 30, 44, 54), (15, 22, 36, 40, 42)}, {(6, 22, 50, 54, 56), (10, 14, 36, 60, 66), (12, 18, 40, 42, 55), (15, 21, 30, 44, 48), (20, 24, 28, 33, 45)}, {(6, 24, 42, 50, 66), (10, 18, 33, 56, 60), (12, 28, 30, 36, 55), (14, 15, 40, 44, 54), (20, 21, 22, 45, 48)}, {(6, 28, 30, 60, 66), (10, 15, 44, 54, 56), (12, 22, 36, 42, 50), (14, 24, 33, 40, 45), (18, 20, 21, 48, 55)}, {(6, 28, 40, 45, 66), (10, 30, 33, 42, 48), (12, 21, 24, 55, 60), (14, 18, 36, 44, 50), (15, 20, 22, 54, 56)}, {(6, 28, 44, 50, 54), (10, 21, 33, 48, 60), (12, 14, 40, 45, 66), (15, 22, 30, 36, 56), (18, 20, 24, 42, 55)}, {(6, 30, 36, 55, 56), (10, 15, 42, 48, 66), (12, 28, 33, 40, 45), (14, 20, 22, 54, 60), (18, 21, 24, 44, 50)}, {(6, 30, 44, 45, 56), (10, 15, 42, 48, 66), (12, 14, 40, 54, 55), (18, 24, 28, 33, 50), (20, 21, 22, 36, 60)}, {(6, 33, 40, 45, 56), (10, 12, 42, 60, 66), (14, 22, 24, 50, 54), (15, 28, 30, 36, 44), (18, 20, 21, 48, 55)}, {(6, 36, 40, 42, 55), (10, 20, 33, 54, 56), (12, 18, 28, 50, 66), (14, 15, 44, 45, 48), (21, 22, 24, 30, 60)}]