Brain-Teaser 351: Birthday party
From The Sunday Times, 28th January 1968 [link]
Some years ago the Bell family were holding their usual annual special birthday party. Four members of the family, of four different generations, had birthdays on the same day of the year. They were old Adam, his son Enoch, Enoch’s son Joseph and Joseph’s son David. On this occasion David remarked that the sum of any three of their four ages was a perfect square.
Some years later old Adam died on his birthday, but it so happened that on the very same day David’s son Samuel was born, and the annual party was continued in subsequent years.
In 1967 at the usual party Samuel made exactly the same remark that David had made, on the previous occasion.
In what year did Adam die and how old was he then?
(Perhaps I should mention that no one survived to be 100!).
This puzzle is included in the book Sunday Times Brain Teasers (1974).
[teaser351]











Jim Randell 10:03 am on 24 March 2024 Permalink |
This Python program considers possible potential gaps between generations, and then looks for ages that makes any subset of size 3 sum to a square. Once we have found potential candidates we look for a pair with suitable overlapping gaps.
It runs in 221ms. (Internal runtime is 144ms).
# generate possible (<gaps>, <ages>) def generate(): # suppose the gaps between generations are in the range [15, 40] # which means the sum of the 3 gaps lies between 45 and 97 for g in irange(45, 97): for (p, q, r) in decompose(g, 3, increasing=0, sep=0, min_v=15, max_v=40): # consider lowest age for a in irange(1, 98 - g): b = a + p c = b + q d = c + r t = a + b + c + d if not all(is_square(t - x) for x in (a, b, c, d)): continue printf("[({r}, {q}, {p}) -> ({d}, {c}, {b}, {a})]") yield ((r, q, p), (d, c, b, a)) # look for gaps (p, q, r) -> (q, r, s) for (((p, q, r), (A1, E1, J1, D1)), ((q_, r_, s), (E2, J2, D2, S2))) in subsets(generate(), size=2, select='P'): if not (q_ == q and r_ == r and E2 > E1): continue # event 2 is in 1967 y2 = 1967 y1 = y2 - (E2 - E1) printf("{y1} (A={A1} E={E1} J={J1} D={D1}) -> {y2} (E={E2} J={J2} D={D2} S={S2})") b = y1 - A1 # A's birth year d = y2 - S2 # A's death year = S's birth year a = d - b # A's age at death printf("-> A born {b}; died {d}, aged {a}")Solution: Adam died in 1953, on his 96th birthday.
There are only three viable candidate lists:
An age of 1 might be a little young to be making observations, but in any case only the final two candidates have suitable overlapping gaps.
So, at the first event we have:
And at the second event:
This is 32 years later than the first event, and as it happened in 1967 the first event was in 1935.
So Adam was born in 1857, and Samuel was born in 1953, the same year Adam died. So Adam died on his 96th birthday.
LikeLike
Frits 12:40 pm on 24 March 2024 Permalink |
Faster and probably easier to understand.
I have also not put in an age restriction for the youngest family member to make observations.
from enigma import SubstitutedExpression, subsets # invalid digit / symbol assignments d2i = dict() for d in range(0, 100): vs = set() if d > 69: vs.update('A') # not excluding young fathers (with such names) for i in range(4): if not (10 * i <= d < 10 * (i + 7)): vs.update('ABCD'[i]) d2i[d] = vs # the alphametic puzzle p = SubstitutedExpression( [ # increasing ages A, B, C and D "B > (A + 10)", "C > (B + 10)", "is_square(A + B + C)", "D > (C + 10)", # the sum of any three of their four ages was a perfect square "all(is_square(x + y + z) for x, y, z in subsets(@ages, 3))" ], base=100, macro=dict([("ages", "[D, C, B, A]")]), answer="@ages", d2i=d2i, distinct="", verbose=0, # use 256 to see the generated code ) # a, e, j, d # e + n, j + n, d + n, s for (a, e, j, d), (e2, j2, d2, s) in subsets(p.answers(), 2, select="P"): # same age increase for Enoch, Joseph and David if len(set([e2 - e, j2 - j, d2 - d])) != 1: continue n = e2 - e # some years later old Adam died on his birthday if s >= n - 2: continue print(f"answer: Adam died in {1967 - s} at the age of {a + n - s}")LikeLike
Jim Randell 1:34 pm on 24 March 2024 Permalink |
@Frits: Yes, it a good idea to check three of the values sum to a square before moving on to the fourth.
This Python program runs in 70ms. (Internal runtime is 11ms).
from enigma import (irange, powers, decompose, subsets, is_square, singleton, printf) # generate possible (<gaps>, <ages>) def generate(): # choose possible squares for b + c + d for ta in powers(10, 15): for (b, c, d) in decompose(ta, 3, increasing=1, sep=15, min_v=16, max_v=99): # find values for a for a in irange(1, b - 15): t = ta + a if not all(is_square(t - x) for x in (b, c, d)): continue printf("[ages = ({d}, {c}, {b}, {a})]") yield (d, c, b, a) # look for gaps (p, q, r) -> (q, r, s) for ((A1, E1, J1, D1), (E2, J2, D2, S2)) in subsets(generate(), size=2, select='P'): g = singleton({E2 - E1, J2 - J1, D2 - D1}) if g is None or not (g > 0): continue # event 2 is in 1967 y2 = 1967 y1 = y2 - g printf("{y1} (A={A1} E={E1} J={J1} D={D1}) -> {y2} (E={E2} J={J2} D={D2} S={S2})") b = y1 - A1 # A's birth year d = y2 - S2 # A's death year = S's birth year a = d - b # A's age at death if not (d > y1 and a < 100): continue printf("-> A born {b}; died {d}, aged {a}")LikeLike
Frits 2:21 pm on 24 March 2024 Permalink |
It is getting more and more annoying to have to do an import or specify a function for a normal operation like ceil.
This program performs the same as Jim’s 2nd program and seems to have more requirements checks than Jim.
@Jim, it is not immediately clear to me from your code that Adam didn’t die as a centenarian or that Samuels’ age isn’t too high.
from itertools import permutations from math import ceil # minimal difference between generations diff = 15 mxA = 98 # if Adam dies one year after the party he still is only 99 d6 = 6 * diff sqs = set(i * i for i in range(ceil(d6**.5), int((4 * mxA - d6)**.5) + 1)) sols = [] for A in range(1, mxA - 3 * diff + 1): for B in range(A + diff, mxA - 2 * diff + 1): for C in range(B + diff, mxA - diff + 1): if (A + B + C) not in sqs: continue for D in range(C + diff, mxA + 1): # borrowed from Jim if any((A + B + C + D - x) not in sqs for x in [D, C, B, A]): continue sols.append([D, C, B, A]) # a, e, j, d # e + n, j + n, d + n, s for (a, e, j, d), (e2, j2, d2, s) in permutations(sols, 2): # same age increase for Enoch, Joseph and David if len(set([e2 - e, j2 - j, d2 - d])) != 1: continue n = e2 - e # some (1 or more) years later Adam died on his birthday and # no one (read Adam) survived to be 100 if s >= n or a + n - s > 99: continue print(f"answer: Adam died in {1967 - s} at the age of {a + n - s}")LikeLike
Jim Randell 2:59 pm on 24 March 2024 Permalink |
@Frits: Yes, for completeness we can add a check to ensure
a < 100andd > y1.Fortunately it doesn’t remove the single candidate solution.
LikeLike
Frits 8:29 pm on 24 March 2024 Permalink |
Inspired by Brian. Fastest approach sofar (5ms with PyPy).
from itertools import combinations # minimal difference between generations diff = 15 sqs = [sq for n in range(1, 20) if 3 * (diff + 1) < (sq := n * n) <= 3 * (99 - diff)] # find sets of four different values, all less # than 100, for which any three add to a square quads = [] # pick four squares (a + b + c, a + b + d, a + c + d, b + c + d) for sq1, sq2, sq3, sq4 in combinations(sqs, 4): a, r = divmod(sq1 + sq2 + sq3 - 2 * sq4, 3) if r or a < 1: continue if (d := sq4 - sq1 + a) > 99: continue b, c = sq2 - a - d, sq3 - a - d # check difference between generations if any(y - x < diff for x, y in zip((a, b, c), (b, c, d))): continue quads.append((a, b, c, d)) # consider the two events mentioned for p in quads: for q in quads: if p == q: continue # ages in 1967 (d, j, e, a), (s, d_, j_, e_) = p, q # the age gap between the two events must # be the same for David, Joseph and Enoch if len({d_ - d, j_ - j, e_ - e}) == 1: gap = e_ - e if s < gap and (ad := a + gap - s) < 100: print(f"Adam died in {1967 - s} at age {ad}.")LikeLike
Jim Randell 10:01 pm on 24 March 2024 Permalink |
Starting from the squares is an even better idea.
If we have four ages (a, b, c, d) (in ascending order), and each 3-subset sums to a square number, we have:
The squares also appear in ascending order, and the difference between consecutive squares is the difference between consecutive ages.
Then by summing the equations we get:
And given the squares we can recover the ages:
Here’s my version. It has an internal runtime of 349µs.
from enigma import (powers, sqrtc, sqrtf, subsets, div, singleton, printf) # generate possible ages def generate(): g = 15 # min generation gap # find possible sets of 4 squares m = 3 + 3*g for (u2, v2, w2, x2) in subsets(powers(sqrtc(m), sqrtf(300 - m)), size=4): # check generation gaps if v2 - u2 < g or w2 - v2 < g or x2 - w2 < g: continue # 3(a + b + c + d) = u^2 + v^2 + w^2 + x^2 t = div(u2 + v2 + w2 + x2, 3) if t is None: continue # calculate (a, b, c, d) (a, b, c, d) = (t - x2, t - w2, t - v2, t - u2) if not (0 < a < b < c < d < 100): continue printf("[ages = ({d}, {c}, {b}, {a})]") yield (d, c, b, a) # look for ages at the 2 events for ((A1, E1, J1, D1), (E2, J2, D2, S2)) in subsets(generate(), size=2, select='P'): # gap between events g = singleton({E2 - E1, J2 - J1, D2 - D1}) if g is None or not (g > 0): continue # event 2 is in 1967 y2 = 1967 y1 = y2 - g printf("{y1} (A={A1} E={E1} J={J1} D={D1}) -> {y2} (E={E2} J={J2} D={D2} S={S2})") b = y1 - A1 # A's birth year d = y2 - S2 # A's death year = S's birth year # check timeline if not (y1 + 1 < d < 100 + b): continue # output solution printf("-> A born {b}; died {d}, aged {a}", a=d - b)LikeLike