Brain-Teaser 730: Miss Brain-Teaser
From The Sunday Times, 13th July 1975 [link]
Five girls entered our Village Beauty Contest. Each of the five judges had to put them into a definite order and then allot fifteen marks between them. The aggregate marks for each girl decided the issue. The judges each gave a different maximum mark and also chose a different girl to be top. No girl had two identical marks in her score.
Joe gave the maximum possible to Rose, and he placed Gwen above Linda. Tom gave Ann one more than Sam did. Pam had no zero in her score and although she finished ahead of Rose she didn’t win. Ann scored the only five that was allotted. Dan placed the girls as closely together as he could.
The judge who put Gwen first put Rose last. Brian succeeded in putting them all in their correct final order.
Who won, and what were her individual marks from each judge?
This puzzle is included in the book The Sunday Times Book of Brain-Teasers: Book 1 (1980). The puzzle text above is taken from the book.
[teaser730]
Jim Randell 11:30 am on 30 March 2021 Permalink |
I think this one is easier to do with a bit of scribbling on the back of an envelope.
But here is a Python program that solves the puzzle in 851ms.
The program groups together the possible sets of scores by the maximum number of marks given. And then selects scores that give a different maximum for each judge. We know Joe gives the absolute maximum, and Dan gives scores that as close together as possible. The distribution of scores is chosen such that no contestant receives the same number of marks from two judges. And then we check the other conditions as we go along.
Run: [ @replit ]
from enigma import (irange, group, Accumulator, diff, update, flatten, tuples, seq_all_different, find, map2str, printf) # labels for the contestants, and number of contestants (N) (A, G, L, P, R, N) = irange(0, 5) # decompose <t> into <k> increasing numbers, minimum <m> def decompose(t, k, m=0, ns=[]): if k == 1: if not (t < m): yield ns + [t] else: k_ = k - 1 for n in irange(m, t - k_ * m): yield from decompose(t - n, k_, n + 1, ns + [n]) # collect possible scores, by maximum marks given scores = group(decompose(15, N), by=max) # find scores containing the minimim possible difference (for Dan) r = Accumulator(fn=min, collect=1) for s in flatten(scores.values()): r.accumulate_data(max(b - a for (a, b) in tuples(s, 2)), s) mds = r.data # check a (partial) collection of scores def check(*ss): # consider the scores for the judges: # each judge places a different contestant first fs = set() for s in ss: f = s.index(max(s)) if f in fs: return # G first -> R last if f == G and s.index(min(s)) != R: return fs.add(f) # consider the scores for the contestants: for (i, s) in enumerate(zip(*ss)): # A scores the only 5 if i == A and len(s) == N and 5 not in s: return if i > A and 5 in s: return # P has no zeros if i == P and 0 in s: return # looks OK return 1 # empty scores s0 = [None] * N # generate scores with max marks in <mms> # <f5> current value of the 5 flag def generate(mms, f5): for (k, vs) in scores.items(): if k in mms: for ss in vs: # is a 5 involved? s5 = (5 in ss) if not (f5 + s5 > 1): yield (k, s5, ss) # assign marks <ms> to score <s> starting at index <k>, respecting <gs> def _assign(s, gs, ms, k=0): # are we done? if not ms: yield s elif s[k] is not None: yield from _assign(s, gs, ms, k + 1) else: # fill out index k for (i, v) in enumerate(ms): if v not in gs[k]: yield from _assign(update(s, [(k, v)]), gs, ms[:i] + ms[i + 1:], k + 1) # assign marks <ms>, respecting (i, v) pairs, and current sequences <ss> def assign(ms, ss, ivs=()): # find current marks per contestant gs = (list(zip(*ss)) if ss else [[]] * N) # assign any given values s = list(s0) for (i, v) in ivs: if v in gs[i]: return s[i] = v # and fill out remaining values yield from _assign(s, gs, diff(ms, s)) # select marks for Joe, must include the maximum possible score mxj = max(scores.keys()) for (_, j5, js) in generate([mxj], 0): # Joe gave the max score to R for J in assign(diff(js, [mxj]), [], [(R, mxj)]): # Joe gave G more marks than L if not (J[G] > J[L]): continue if not check(J): continue # select marks for Dan, as close together as possible for ds in mds: mxd = max(ds) if mxd == mxj: continue # is a 5 involved? d5 = (5 in ds) if j5 + d5 > 1: continue for D in assign(ds, [J]): if not check(J, D): continue # select marks for Sam kss = diff(scores.keys(), {mxj, mxd}) for (mxs, s5, ss) in generate(kss, j5 + d5): for S in assign(ss, [J, D]): if not check(J, D, S): continue # select marks for Tom kst = diff(kss, [mxs]) for (mxt, t5, ts) in generate(kst, j5 + d5 + s5): # Tom gave A 1 point more than Sam i = find(ts, S[A] + 1) if i == -1: continue for T in assign(diff(ts, [ts[i]]), [J, D, S], [(A, ts[i])]): if not check(J, D, S, T): continue # select marks for Brian ksb = diff(kst, [mxt]) for (mxb, b5, bs) in generate(ksb, j5 + d5 + s5 + t5): if d5 + j5 + s5 + t5 + b5 != 1: continue for B in assign(bs, [J, D, S, T]): if not check(J, D, S, T, B): continue # find the totals for the contestants pts = list(sum(s) for s in zip(J, D, S, T, B)) if not seq_all_different(pts): continue # P finished ahead of R, but didn't win if not (pts[P] > pts[R] and pts[P] != max(pts)): continue # Brian chose the correct order order = lambda s: sorted(irange(N), key=(lambda i: s[i]), reverse=1) pos = order(pts) if not (order(B) == pos): continue # output solution w = pos[0] ws = tuple(x[w] for x in [J, D, S, T, B]) printf("winner={w} {ws}\n\n A G L P R\n J={J}\n D={D}\n S={S}\n T={T}\n B={B}\n", w="AGLPR"[w], ws=map2str(zip("JDSTB", ws)), )Solution: Linda won. Her marks were: Joe = 0, Dan = 3, Sam = 4, Tom = 2, Brian = 8.
The full scores are:
LikeLike
Frits 1:52 pm on 30 March 2021 Permalink |
Wow, 142 lines of code.
LikeLike
Jim Randell 4:25 pm on 31 March 2021 Permalink |
With a bit of analysis we can get a neater program (although it’s not really any faster – I timed it at 805ms, so it is still under a second).
Looking at the decompositions of 15 into 5 different numbers we see that the maximum possible mark in any decomposition is 9, and there is only one decomposition corresponding to this maximum, so Joe’s marks must be (in some order):
Similarly there is only one decomposition where the marks are consecutive, so this must be Dan’s:
So Dan gives out the only 5 (to Ann), and that is his maximum mark. This means we know where the 5 is, and don’t have to worry about tracking it in the code.
And for the three remaining judges they must give maximum marks of 6, 7, 8 and for each maximum there is only one decomposition for each that does not include 5. So the marks from the remaining judges (Sam, Tom and Brian) are (in some order):
The following program awards Joe’s 9 to Rose, and Dan’s 5 to Ann and fills out the remaining marks recursively.
Run: [ @replit ]
from itertools import permutations from enigma import (irange, diff, update, seq_all_different, map2str, printf) # available scores (highest to lowest) # each sums to 15, and has a different max score = [ (9, 3, 2, 1, 0), # this is J's (5, 4, 3, 2, 1), # this is D's (8, 4, 2, 1, 0), (7, 4, 3, 1, 0), (6, 4, 3, 2, 0), ] # indices for BST (we don't know which is which, yet) BST = (2, 3, 4) # labels for the contestants (and number of contestants) girls = (A, G, L, P, R) = (0, 1, 2, 3, 4) N = 5 # check map <m>, up to row <k> def check(m, k, gs): mk = m[k] # P has no zeros if mk[P] == 0: return s = score[k] # if G is first, R must be last if mk[G] == s[0] and mk[R] != s[4]: return if k == 0: # check for Joe: G > L if not (mk[G] > mk[L]): return else: # no girl recieves the same mark from 2 different judges if any(x in y for (x, y) in zip(mk, gs)): return # looks OK return 1 # associate with each score: m[score-index][contestant] = marks # <gs> = marks for each contestant so far # <fs> = contestants that have been awarded a judges highest mark def solve(m, k=0, gs=[[]] * N, fs=[]): # are we done? if k == N: yield (m, gs) else: # consider possibilities for score k mk = m[k] s = score[k] mx = max(s) # find assigned contestants cs = set(i for i in girls if mk[i] is None) # map the unassigned scores to the unassigned contestants for ms in permutations(diff(s, mk)): # make the updated row mk_ = update(mk, cs, ms) # check the highest score is to a new contestant f = mk_.index(mx) if f in fs: continue # update the map m_ = update(m, [(k, mk_)]) if not check(m_, k, gs): continue gs_ = list(xs + [x] for (xs, x) in zip(gs, mk_)) yield from solve(m_, k + 1, gs_, fs + [f]) # initial map m0 = list([None] * N for _ in irange(N)) m0[0][R] = 9 # J's highest score goes to R m0[1][A] = 5 # D's 5 goes to A # order girls by scores <s> (highest to lowest) order = lambda s: sorted(girls, key=(lambda g: s[g]), reverse=1) # consider possible completed maps for (m, gs) in solve(m0): # calculate the total score for each contestant pts = list(sum(g) for g in gs) if not seq_all_different(pts): continue # P finished ahead of R, but didn't win if not (pts[P] > pts[R] and pts[P] != max(pts)): continue pos = order(pts) # Brian placed the girls in the correct order for B in BST: if order(m[B]) != pos: continue # Tom gave 1 more point to A than Sam for (S, T) in permutations(diff(BST, [B])): if m[S][A] + 1 != m[T][A]: continue # output solution w = pos[0] ws = tuple(x[w] for x in m) js = update("JD???", (B, S, T), "BST") printf("winner = {w} {ws}\n\n A G L P R\n {m}\n", w="AGLPR"[w], ws=map2str(zip(js, ws)), m=map2str(zip(js, m), sep="\n ", enc=""), )In my first program I wrote the [[
assign()]] stuff to avoid permutations that would fail (which did make it run a bit faster). I didn’t bother for this program.LikeLike
Frits 12:42 pm on 31 March 2021 Permalink |
Use denest=1 if not running with PyPy.
It would be nice if an intermediate assignment could be added to SubstitutedExpression (for the sumcols list). It is now calculated multiple times.
from enigma import SubstitutedExpression ''' An Gw Li Pa Ro Joe=[A B C D E] Dan=[F G H I J] Sam=[K L M N O] Tom=[P Q R S T] Brian=[U V W X Y] ''' # column totals sumcols = lambda s: list(map(sum, zip(*s))) # are list a and b ordered the same? def same_order(a, b): s1 = sorted((x, i) for i, x in enumerate(a)) s2 = sorted((x, i) for i, x in enumerate(b)) return all(x[1] == y[1] for x, y in zip(s1, s2)) # the alphametic puzzle p = SubstitutedExpression( [ # row totals must be 15, choose RHS the variable with the most candidates "15 - (A + C + D + E) = B", "15 - (F + G + I + J) = H", "15 - (K + L + N + O) = M", "15 - (P + Q + S + T) = R", "15 - (U + V + X + Y) = W", # Joe placed Gwen above Linda. "B > C", # Dan placed the girls as closely together as he could # Dan's scores has to involve a 5 and may not include maximums 6, 7, 8 or 9 "sorted([G, H, I, J]) == [1, 2, 3, 4]", # the judge who put Gwen first put Rose last. "L != max([K, L, M, N, O]) or O == min([K, L, M, N, O])", "Q != max([P, Q, R, S, T]) or T == min([P, Q, R, S, T])", "V != max([U, V, W, X, Y]) or Y == min([U, V, W, X, Y])", # Tom gave Ann one more than Sam did. "K + 1 = P", # Pam finished ahead of Rose "sum([D, I, N, S, X]) > sum([E, J, O, T, Y])", # the judges each gave a different maximum mark "len(set([9, 5, max([K,L,M,N,O]), max([P,Q,R,S,T]), \ max([U,V,W,X,Y])])) == 5", # the judges chose a different girl to be top (Dan in col 0, Joe in col 4) "len(set([0, 4, \ [i for i, x in enumerate([K,L,M,N,O]) if x == max([K,L,M,N,O])][0],\ [i for i, x in enumerate([P,Q,R,S,T]) if x == max([P,Q,R,S,T])][0],\ [i for i, x in enumerate([U,V,W,X,Y]) if x == max([U,V,W,X,Y])][0]])) == 5", # the aggregate marks for each girl decided the issue "len(set(sumcols([[A,B,C,D,E], [F,G,H,I,J], [K,L,M,N,O], [P,Q,R,S,T], \ [U,V,W,X,Y]]))) == 5", # Pam didn't win "max(enumerate(sumcols([[A,B,C,D,E], [F,G,H,I,J], [K,L,M,N,O], \ [P,Q,R,S,T], [U,V,W,X,Y]])), key=(lambda x: x[1]))[0] != 3", # Brian succeeded in putting them all in their correct final order "same_order(sumcols([[A,B,C,D,E], [F,G,H,I,J], [K,L,M,N,O], [P,Q,R,S,T], \ [U,V,W,X,Y]]), [U,V,W,X,Y])", ], answer="[A,B,C,D,E], [F,G,H,I,J], [K,L,M,N,O], [P,Q,R,S,T], [U,V,W,X,Y],\ max(enumerate(sumcols([[A,B,C,D,E], [F,G,H,I,J], [K,L,M,N,O], \ [P,Q,R,S,T], [U,V,W,X,Y]])), key=(lambda x: x[1]))[0]", # no girl had two identical marks in her score distinct=("ABCDE", "FGHIJ", "KLMNO", "PQRST", "UVWXY", "AFKPU", "BGLQV", "CHMRW", "DINSX", "EJOTY" ), env=dict(sumcols=sumcols, same_order=same_order), # Pam had no zero in her score, Joe gave maximum to Rose (E = 9) # Ann scored the only five (in first column, F = 5)) # first and last column may not contain maximums 6, 7 and 8 s2d=dict(E=9, F=5), digits=(0, 1, 2, 3, 4, 6, 7, 8), # remaining digits d2i=dict([(0, "EFGHJBPDINSX")] + [(k, "EF") for k in range(1, 4)] + [(4, "EFJOTYK")] + [(k, "FGHIJEOTYAKPU") for k in range(6, 8)] + [(8, "FGHIJEOTYKAPUC")]), denest=1, # use denest=1 if not run with PyPy verbose=0 ) judges = ["Joe", "Dan", "Sam", "Tom", "Brian"] girls = ["Agnes", "Gwen", "Linda", "Pam", "Rose"] for (_, ans) in p.solve(): print(girls[ans[-1]], "has won") print(*[judges[i] + ": " + str(x[ans[-1]]) for i, x in enumerate(ans[:-1])])LikeLike
Frits 1:29 pm on 31 March 2021 Permalink |
Faster than Jim’s program with PyPy but slower with Python 3.9.2.
from itertools import permutations # do all columns have different values in the multidimensional array <s> ? diffcols = lambda s: all(len(set(c)) == len(c) for c in list(zip(*s))) # are list a and b ordered the same? def same_order(a, b): s1 = sorted((x, i) for i, x in enumerate(a)) s2 = sorted((x, i) for i, x in enumerate(b)) return all(x[1] == y[1] for x, y in zip(s1, s2)) # decompose <t> into <k> increasing numbers, minimum <m> def decompose(t, k, m=0, ns=[]): if k == 1: if not(t < m): yield ns + [t] else: k_ = k - 1 for n in range(m, t + 1 - k_ * m): yield from decompose(t - n, k_, n + 1, ns + [n]) def check(s, mult, m, ind, imult): if ind in imult: # the judges chose a different girl to be top return False if sum(x[0] == 5 for x in mult) > 1: # Ann scored the only five return False if s[1] == m and s[4] != min(s): # the judge who put Gwen first put Rose last return False if not diffcols(mult): # no girl had two identical marks in her score return False return True judges = ["Joe", "Dan", "Sam", "Tom", "Brian"] girls = ["Agnes", "Gwen", "Linda", "Pam", "Rose"] scores = list(x for y in [list(permutations(s)) for s in decompose(15, 5)] for x in y) # Ann scored the only five. Pam had no zero in her score scores = [x for x in scores if 5 not in x[1:] and 9 not in x[:4] and x[3] != 0] scores9 = [x for x in scores if x[4] == 9] scoresnot9 = [(x, max(x)) for x in scores if x[4] != 9] for D in [x[0] for x in scoresnot9]: # Dan placed the girls as closely together as he could s = sorted(D) if not all(y == (x + 1) for (x, y) in list(zip(s, s[1:]))): continue mD = max(D) # the judge who put Gwen first put Rose last if D[1] == mD and D[4] != min(D): continue iD = D.index(mD) for J in scores9: # Joe gave maximum to Rose iJ = 4 # last position if iJ == iD: continue if J[1] < J[2]: continue # Joe placed Gwen above Linda if not diffcols([D, J]): continue # no identical marks per girl for S in [x[0] for x in scoresnot9 if x[1] not in {mD}]: mS = max(S) iS = S.index(mS) if not check(S, [D, J, S], mS, iS, {iD, iJ}): continue for T in [x[0] for x in scoresnot9 if x[1] not in {mD, mS}]: if T[0] != S[0] + 1: continue # Tom gave Ann one more than Sam mT = max(T) iT = T.index(mT) if not check(T, [D, J, S, T], mT, iT, {iD, iJ, iS}): continue for B in [x[0] for x in scoresnot9 if x[1] not in {mD, mS, mT}]: mB = max(B) iB = B.index(mB) if not check(B, [D, J, S, T, B], mB, iB, {iD, iJ, iS, iT}): continue sumcols = list(map(sum, zip(D, J, S, T, B))) if len(set(sumcols)) != 5: continue # totals must be different winning = max(sumcols) # Pam finished ahead of Rose but she didn’t win. if sumcols[3] < sumcols[4] or sumcols[3] == winning: continue # Brian succeeded in putting them all in their correct final order if not same_order(sumcols, B): continue # Ann scored the only five if [J[0], D[0], S[0], T[0], B[0]].count(5) != 1: continue ind = sumcols.index(winning) print(girls[ind], "has won") print(*[judges[i] + ": " + str(x[ind]) for i, x in enumerate([J, D, S, T, B])])LikeLike
Jim Randell 7:03 pm on 31 March 2021 Permalink |
Here is a solution that uses the analysis given above to determine the five sets of scores (but not the orderings of the sets), and then using the [[ SubstitutedExpression() ]] solver from the enigma.py library to find candidate solutions, followed by a check of the remaining conditions. It runs in 208ms.
Run: [ @replit ]
from enigma import ( SubstitutedExpression, join, encl, seq_all_different, subsets, update, map2str, printf ) SubstitutedExpression.set_default(denest=-1, verbose=0) # ann gwe lin pam ros # joe: A B C D E += 15 # dan: F G H I J += 15 # xxx: K L M N P += 15 # yyy: Q R S T U += 15 # zzz: V W X Y Z += 15 # marks from the judges (joe, dan, xxx, yyy, zzz) = ("ABCDE", "FGHIJ", "KLMNP", "QRSTU", "VWXYZ") # marks for the contestants (ann, gwe, lin, pam, ros) = ("AFKQV", "BGLRW", "CHMSX", "DINTY", "EJPUZ") # indices for top girls tops = "abcde" xset = lambda x: join(x, fn=encl, sep=", ", enc="{}") xseq = lambda x: join(x, fn=encl, sep=", ", enc="()") xsum = lambda x: join(x, fn=encl, sep=" + ", enc="()") top = lambda t: t.index(max(t)) p = SubstitutedExpression( [ # we know the marks from each judge (but not the order) f"{xset(joe)} == {{0, 1, 2, 3, 9}}", f"{xset(dan)} == {{1, 2, 3, 4, 5}}", f"{xset(xxx)} == {{0, 1, 2, 4, 8}}", f"{xset(yyy)} == {{0, 1, 3, 4, 7}}", f"{xset(zzz)} == {{0, 2, 3, 4, 6}}", # Joe placed G above L "B > C", # P finished ahead of R ... f"{xsum(pam)} > {xsum(ros)}", # ... but didn't win f"{xsum(pam)} < max({xsum(gwe)}, {xsum(lin)}, {xsum(ann)})", # each judge chose a different top girl f"top({xseq(joe)}) = {{a}}", f"top({xseq(dan)}) = {{b}}", f"top({xseq(xxx)}) = {{c}}", f"top({xseq(yyy)}) = {{d}}", f"top({xseq(zzz)}) = {{e}}", # whichever of xxx, yyy, zzz placed G top, also placed R bottom f"implies(max({xseq(xxx)}) == {{L}}, min({xseq(xxx)}) == {{P}})", f"implies(max({xseq(yyy)}) == {{R}}, min({xseq(yyy)}) == {{U}})", f"implies(max({xseq(zzz)}) == {{W}}, min({xseq(xxx)}) == {{Z}})", ], # each judge allocates different marks to each girl # no girl had the same marks from different judges distinct=(joe, dan, xxx, yyy, zzz, ros, gwe, lin, ann, pam, tops), # Joe gave 9 to R; Dan gave 5 to A s2d=dict(E=9, F=5), # so the remaining digits are... digits=(0, 1, 2, 3, 4, 6, 7, 8), # P had no 0 from any judge ... d2i={ 0: pam + dan, 1: zzz, 2: yyy, 3: xxx, 4: joe, 6: joe + dan + xxx + yyy + tops, 7: joe + dan + xxx + zzz + tops, 8: joe + dan + yyy + zzz + tops, }, env=dict(top=top), ) # order of contestants girls = (A, G, L, P, R) = (0, 1, 2, 3, 4) # order girls by scores <s> (highest to lowest) order = lambda s: sorted(girls, key=(lambda g: s[g]), reverse=1) # check a candidate solution def check(s): # calcuate the total score for each contestant ms = list(list(s[x] for x in xs) for xs in (ann, gwe, lin, pam, ros)) pts = list(sum(m) for m in ms) if not seq_all_different(pts): return pos = order(pts) # assign xxx, yyy, zzz to B, S, T BST = (xxx, yyy, zzz) for (B, S, T) in subsets(BST, size=len, select="P"): # Tom gave 1 more point to A than Sam if s[S[A]] + 1 != s[T[A]]: continue # Brian placed the girls in the correct order if order(list(s[x] for x in B)) != pos: continue # output solution w = pos[0] ws = ms[w] js = "JD" + update("???", (BST.index(x) for x in (B, S, T)), "BST") ss = list(list(s[x] for x in xs) for xs in (joe, dan, xxx, yyy, zzz)) printf("winner = {w} {ws}\n\n A G L P R\n {m}\n", w="RGLAP"[w], ws=map2str(zip(js, ws)), m=map2str(zip(js, ss), sep="\n ", enc=""), ) # run the solver p.run(check=check)LikeLike