“I don't know the numbers”: a math puzzle
alexanderell.is
alexanderell.is
> You are given two eggs, and access to a 100-storey tower. Both eggs are identical. The aim is to find out the highest floor from which an egg will not break when dropped out of a window from that floor. If an egg is dropped and does not break, it is undamaged and can be dropped again. However, once an egg is broken, that’s it for that egg.
> If an egg breaks when dropped from a floor, then it would also have broken from any floor above that. If an egg survives a fall, then it will survive any fall shorter than that.
> The question is: What strategy should you adopt to minimize the number egg drops it takes to find the solution? (And what is the worst case for the number of drops it will take?)
I wrote up a solution for this (along with a generalized analytical solution) on my blog: https://blog.kdheepak.com/the-egg-tower-puzzle
Given:
(1) the ordered sequence of whole numbers (1, 2, ... , 99, 100) and
(2) an unknown value N that is a member of this sequence,
(3) the following tests (a candidate is a guess at the value of N), and two separate threads to run tests:
(A) if candidate <= N : true & thread continues (another candidate can be checked with that thread)
(B) candidate > N : false & thread terminates (no more candidates can be checked with that thread)
Determine the best strategy for finding N (minimize the number of tests while leaving at least one thread alive)
Determine the worst case for the number of tests needed to find N using this optimal strategy.
However, it was short lived since now I know it takes "14 drops" maximum, but not the solution - "which floors should I use the 14 drops on though?"
Maybe I'd be intrigued enough to try and follow the math if that was added in :)
The egg doesn't break when dropped out of the window (i.e., instantaneously). It potentially breaks when it hits the floor after a time delay t. So, the answer is 100 and 0 trials.
ps: This is a joke. Don't take it too seriously.
(The puzzle being solvable means that they can't actually get stuck, but that feels like outside information...)
In fact, if you let it run past 15 rounds (for N=100, at least), and check the number of disqualified candidates between rounds, you’ll find that it does get stuck in after 15 rounds as the candidate list doesn’t change. The flip side of this is that there are also solutions before 15 rounds, too, but with the problem’s original statement, we’re looking at exactly 15.
If Peter had said "I don't know the numbers" at the end there, Sandy can exclude the possibility that the numbers were 77 and 84. However, at this point Peter already knows that Sandy still doesn't know the numbers: Even if the sum Sandy was told was 161, there are still five remaining live possibilities for that sum (62 + 99, 65 + 96, 70 + 91, 76 + 85 and 80 + 81), so Peter gains no new information from Sandy's next statement that Sandy does not know the numbers.
> A census taker approaches a woman leaning on her gate and asks about her children. She says, "I have three children and the product of their ages is seventy–two. The sum of their ages is the number on this gate." The census taker does some calculation and claims not to have enough information. The woman enters her house, but before slamming the door tells the census taker, "I have to see to my eldest child who is in bed with measles." The census taker departs, satisfied.
https://en.wikipedia.org/wiki/Ages_of_Three_Children_puzzle?...
One can include certain clues, like “Those are three beautiful numbers” to indicate those are different, or “You would like them” if either of them is presented as a fan of prime numbers, perfect numbers or something like that. With children’s age, they might be in school, boarding school, went to university, or have children, to indicate they are below or above 6, 14, 18, etc.
The algorithm itself is correct, and the answer it arrives at is correct.
http://jdh.hamkins.org/cheryls-rational-gifts/
Two people are told rational numbers of a particular form. They want to know whose number is larger:
> Albert I don’t know.
> Bernard Neither do I.
> Albert Indeed, I still do not know.
> Bernard And still neither do I.
> Cheryl Well, it is no use to continue that way! I can tell you that no matter how long you continue that back-and-forth, you shall not come to know who has the larger number.
> Albert What interesting new information! But alas, I still do not know whose number is larger.
They keep saying “No matter how much we do this, we still won’t know” and eventually they know. It’s great and I recommend reading it.
https://explainextended.com/2016/12/31/happy-new-year-8/
I prefer this less sterile framing of it. It was the most fun that I ever had with a puzzle, so to anyone scrolling around on this page, I would recommend not jumping straight to the solution :)
if round == 15:
for product in products:
if len(products[product]) == 1:
print('Peter: I do know the numbers')
print(products[product][0])
return
This should not return - it should continue iterating. If the problem (and the code) is correct, then it should only find one answer - but unless you exhaustively search and find exactly one answer you don't know that it's correct.Incidentally, I was also a bit surprised by
candidate_pairs = set()
for i in range(1, N):
for j in range(1, N):
if (i, j) not in candidate_pairs and (j, i) not in candidate_pairs:
candidate_pairs.add((i, j))
That first (i, j) check can never return false and should be omitted, right?And that's true, the `(i, j)` check is spurious - I blame it on it being throwaway blog post code :)
for i in range(1, N):
for j in range(1, j + 1):
candidate_pairs.add((i, j))
Also, conceptually, you don’t have tuples, but multisets. If you were to use those, you wouldn’t need that if at all.Multiset isn’t built-in to python, so you’d need to use an external package. Alternatively, write a class for storing pairs of ints i, j that enforces i ≤ j.
>>> a = set( (frozenset((1,2)), frozenset((3,4)) ) )
>>> frozenset((2,1)) in a
True
You would use that to, for example, store the knowledge that 6 can be written as 3+3, by using a multiset {3,3} instead of a tuple (3,3)
They both need to say they know, otherwise we have no reason to believe Sandy has figured it out.
And they both need to be right, which we also have no reason to believe at the moment.
both players can just encode the number they know in binary and say “I don’t know” for a zero and “I know” for a 1.
Peter: "My number is [X]." Sandy: I know the answer!
edit: After rereading the problem they weren’t told the parameters of the problem or even that they are participating in a puzzle.
How do you figure? If Peter is given 97 (a prime), how is he supposed to know that a) he's been given a product (puzzle doesn't say he was told it's a product) and b) if he knows he's been given a product how is he supposed to know 97 is prime? He wasn't told that in the puzzle either.
The primes below that can be present in products that have multiple possible answers: eg. we can't eliminate 47 right off the bat because Peter could have been told a product like 3290.
So each person could have been sharing one binary digit of their number with each round. Or they might have used a completely different encoding. We simply cannot know.
1. is a person that solves logical puzzles if possible
2. knows that the other one + (insert point 1 here).
3. knows that the other one + (2)
4. knows that the other one + (3)
...
Common knowledge is a special kind of knowledge for a group of agents. There is common knowledge of p in a group of agents G when all the agents in G know p, they all know that they know p, they all know that they all know that they know p, and so on ad infinitum. It can be denoted as {\displaystyle C_{G}p}.
-- Create Table #x(v) from 1..99
WITH x AS (SELECT n FROM (VALUES (0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) v(n))
SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS v INTO #x
FROM x ones, x tens ORDER BY 1
;
DELETE FROM #x WHERE v > 99
;
-- Create Candidate #c(x, y, s, p) with pair (x,y) (x <= y) and sum `s` and product `p`
SELECT x.v AS x, y.v AS y, x.v + y.v AS s, x.v \* y.v AS p INTO #c FROM #x x, #x y WHERE x.v <= y.v
;
-- Peter: I don’t know the numbers.
DELETE FROM #c WHERE p IN (SELECT p FROM (SELECT p, COUNT(*) AS n FROM #c GROUP BY p) AS d WHERE n < 2)
-- Sandy: I don’t know the numbers.
DELETE FROM #c WHERE s IN (SELECT s FROM (SELECT s, COUNT(*) AS n FROM #c GROUP BY s) AS d WHERE n < 2)
-- Peter: I don’t know the numbers.
DELETE FROM #c WHERE p IN (SELECT p FROM (SELECT p, COUNT(*) AS n FROM #c GROUP BY p) AS d WHERE n < 2)
-- Sandy: I don’t know the numbers.
DELETE FROM #c WHERE s IN (SELECT s FROM (SELECT s, COUNT(*) AS n FROM #c GROUP BY s) AS d WHERE n < 2)
-- Peter: I don’t know the numbers.
DELETE FROM #c WHERE p IN (SELECT p FROM (SELECT p, COUNT(*) AS n FROM #c GROUP BY p) AS d WHERE n < 2)
-- Sandy: I don’t know the numbers.
DELETE FROM #c WHERE s IN (SELECT s FROM (SELECT s, COUNT(*) AS n FROM #c GROUP BY s) AS d WHERE n < 2)
-- Peter: I don’t know the numbers.
DELETE FROM #c WHERE p IN (SELECT p FROM (SELECT p, COUNT(*) AS n FROM #c GROUP BY p) AS d WHERE n < 2)
-- Sandy: I don’t know the numbers.
DELETE FROM #c WHERE s IN (SELECT s FROM (SELECT s, COUNT(*) AS n FROM #c GROUP BY s) AS d WHERE n < 2)
-- Peter: I don’t know the numbers.
DELETE FROM #c WHERE p IN (SELECT p FROM (SELECT p, COUNT(*) AS n FROM #c GROUP BY p) AS d WHERE n < 2)
-- Sandy: I don’t know the numbers.
DELETE FROM #c WHERE s IN (SELECT s FROM (SELECT s, COUNT(*) AS n FROM #c GROUP BY s) AS d WHERE n < 2)
-- Peter: I don’t know the numbers.
DELETE FROM #c WHERE p IN (SELECT p FROM (SELECT p, COUNT(*) AS n FROM #c GROUP BY p) AS d WHERE n < 2)
-- Sandy: I don’t know the numbers.
DELETE FROM #c WHERE s IN (SELECT s FROM (SELECT s, COUNT(*) AS n FROM #c GROUP BY s) AS d WHERE n < 2)
-- Peter: I don’t know the numbers.
DELETE FROM #c WHERE p IN (SELECT p FROM (SELECT p, COUNT(*) AS n FROM #c GROUP BY p) AS d WHERE n < 2)
-- Sandy: I don’t know the numbers.
DELETE FROM #c WHERE s IN (SELECT s FROM (SELECT s, COUNT(*) AS n FROM #c GROUP BY s) AS d WHERE n < 2)
-- Peter: I do know the numbers.
SELECT * FROM #c WHERE p IN (SELECT p FROM (SELECT p, COUNT(*) AS n FROM #c GROUP BY p) AS d WHERE n < 2)
/*
x y s p
77 84 161 6468
*/Another similar puzzle, with more logic and fewer numbers, is this one which was nicely written up on xkcd: https://xkcd.com/blue_eyes.html
But there's not a writeup there. That's just a statement of the problem.
This is not the hardest logic puzzle in the world, though stating the answer can be tricky. If it takes one day for a solitary blue-eyed person to realize he's the one with blue eyes and leave, then it takes 100 days for a group of 100 blue-eyed people to realize they have blue eyes and leave.
The blue eyes puzzle relies on everyone receiving input from the same synchronized digital clock ("Every night at midnight, a ferry stops at the island"), which is unusual for a logic puzzle. The puzzle here is similarly discretized, but it's more a pure question of sequence - each line of dialog happens after the previous line, and that's all that matters.
If you get stuck, try imagine a different number of blue/brown eyes on the island and what that might change
import itertools
import collections
candidates = [s for s in itertools.product(range(100), range(100)) if s[0]<=s[1]]
def solutions_filter(sols, agg, flip=False):
aggregates = [agg(s) for s in sols]
d = collections.defaultdict(lambda:0)
for s in aggregates:
d[s] = d[s]+1
if not flip:
return [s for s in sols if d[agg(s)] != 1]
else:
return [s for s in sols if d[agg(s)] == 1]
ns = candidates
for i in range(7):
ns = solutions_filter(ns, lambda x:x[0]*x[1])
ns = solutions_filter(ns, lambda x:x[0]+x[1])
print(solutions_filter(ns, lambda x:x[0]*x[1], flip=True))
(Usually, next step is variable names, comments, etc.) library(dplyr)
combos <- expand.grid(x = 1:99, y = 1:99) |>
filter(x <= y) |> # Remove dupes
mutate(sum = x + y, prod = x * y)
find_singletons <- function(col = c("sum", "prod")) {
singletons <- combos %>% group_by(!!sym(col)) %>% tally() %>% filter(n == 1)
which(combos[[col]] %in% singletons[[col]])
}
for (i in 1:7) {
# Peter: I don't know the numbers.
combos <- combos[-find_singletons("prod"),]
# Sandy: I don't know the numbers.
combos <- combos[-find_singletons("sum"),]
}
# Peter: I know the numbers.
combos[find_singletons("prod"),]- both R and Python; or
- neither R nor Python:
Which of the solutions do you find more readable?
(I have my own opinion, but I'm biased)
- Use of higher-order operations, like filters and aggregations, to avoid loops.
- Passing around functions (the * and + side are the same, except for one operation) to avoid repeated code
This results in:
- Less code. Defects per KLOC tends to be pretty constant, so shorter / higher-level programs are typically less buggy.
- In particular, less repeated code. Cut-and-paste introduces a whole slew of potential defects.
- Less dependence on order-of-operations, which eliminates whole classes of errors, from off-by-ones to data structures in intermediate inconsistent states.
The relevant book here is still SICP.
sub solutions-filter(@sols, &agg, Bool:D :$flip = False) {
my @aggregates = @sols.map({ agg($_) });
my %d;
%d{$_} += %d{$_} ?? %d{$_} !! 1 for @aggregates;
return @sols.grep({ %d{ agg($_) } == 1 }) if $flip;
return @sols.grep({ %d{ agg($_) } != 1 });
}
my @candidates = ((1..99) X (1..99)).grep({ $_[0] ≤ $_[1] });
my @ns = @candidates;
for 1..7 {
@ns = solutions-filter @ns, { $_[0] * $_[1] };
@ns = solutions-filter @ns, { $_[0] + $_[1] };
}
say solutions-filter(@ns, { $_[0] * $_[1] }, :flip); data class Candidate(val x: Int, val y: Int)
fun List<Candidate>.filterCandidates(knows: Boolean, aggBy: (Candidate) -> Int) =
groupBy(aggBy)
.values
.filter {value -> if (knows) {value.count() == 1} else {value.count() > 1} }
.flatten()
fun main() {
var cs = (1..99).flatMap { x -> (x..99).map { y -> Candidate(x, y) } }
repeat(7) {
cs = cs.filterCandidates(knows = false) { (x, y) -> x * y }
cs = cs.filterCandidates(knows = false) { (x, y) -> x + y }
}
cs = cs.filterCandidates(knows = true) { (x, y) -> x * y }
println(cs)
}