Updates from March, 2025 Toggle Comment Threads | Keyboard Shortcuts

  • Unknown's avatar

    Jim Randell 1:03 am on 2 March 2025 Permalink | Reply
    Tags:   

    Teaser 3258: On grid 

    From The Sunday Times, 2nd March 2025 [link] [link]

    Using all the digits 1 to 9 in a 3 by 3 grid, I have devised a game. In this grid, if each of the three rows, three columns and two diagonals are read both forwards and backwards, there are 16 three-digit numbers.

    Points are awarded to each of these three-digit numbers; with 1 point awarded if it’s a prime number, 3 points if it’s a perfect square, 6 points if a perfect cube; with the points being cumulative if more than one applies.

    What is the highest number of points that can be awarded?

    [teaser3258]

     
    • Jim Randell's avatar

      Jim Randell 1:27 am on 2 March 2025 Permalink | Reply

      Here’s a brute force solution.

      It runs in 177ms (using PyPy).

      from enigma import (
        defaultdict, Accumulator, primes, powers, nsplit, irange, subsets, rev, printf
      )
      
      # points for 3-digit primes, squares, cubes
      cats = [
        (primes.between(100, 999), 1),  # prime -> +1
        (powers(10, 31, 2), 3),  # square -> +3
        (powers(5, 9, 3), 6),  # cube -> +6
      ]
      # collect points for 3-digit sequences, in both directions
      pts = defaultdict(int)
      for (ns, p) in cats:
        for n in ns:
          ds = nsplit(n)
          if len(set(ds)) < len(ds): continue
          pts[ds] += p
          pts[rev(ds)] += p
      
      # find maximal grids
      r = Accumulator(fn=max, collect=1)
      for ss in subsets(irange(1, 9), size=len, select='P'):
        (A, B, C, D, E, F, G, H, I) = ss
        # eliminate repeated solutions
        if not (A < C and A < G and A < I and B < D): continue
        # score this grid
        p = sum(pts.get(k, 0) for k in [
          (A, B, C), (D, E, F), (G, H, I),  # rows
          (A, D, G), (B, E, H), (C, F, I),  # columns
          (A, E, I), (C, E, G),  # diagonals
        ])
        r.accumulate_data(p, ss)
      
      # output solution(s)
      printf("max points = {r.value} [from {r.count} grids]")
      for (A, B, C, D, E, F, G, H, I) in r.data:
        printf("-> [ {A} {B} {C} | {D} {E} {F} | {G} {H} {I} ]")
      

      Solution: The maximum possible points total is: 27.

      Here is a grid that scores 27:

      The scoring numbers are:

      137 is prime → +1
      169 = 13^2 → +3
      961 = 31^2 → +3
      324 = 18^2 → +3
      587 is prime → +1
      125 = 5^3 → +6
      521 is prime → +1
      729 = 27^2 = 9^3 → +9
      total = 27

      As the numbers are read in both directions this grid can be rotated and reflected to give 7 additional grids using the same numbers.

      These can be seen by removing the check at line 25 from the program.

      Like

    • Frits's avatar

      Frits 1:22 pm on 2 March 2025 Permalink | Reply

      from enigma import SubstitutedExpression
      from itertools import permutations
      
      # determine valid primes 123 - 987
      P = {3, 5, 7}
      P |= {n for n in range(11, 100, 2) if all(n % p for p in P)}
      P = {n for n in range(123, 988, 2) if all(n % p for p in P) and
           len(set(str(n))) == 3}
           
      sqs = {sq for n in range(10, 32) if len(set(str(sq := n * n))) == 3}
      cbs = {cb for n in range(5, 10) if len(set(str(cb := n * n * n))) == 3}
      
      # make dictionary of points per number
      d = {(n := int(''.join(p))): 1 
           if n in P else 3 if n in sqs else 6 if n in cbs else 0 
           for p in permutations("123456789", 3)}
           
      # check numbers both square and cube        
      for c in cbs:
        if c in sqs:
          d[c] = 9  
      
      # return points for number <n>
      pts = lambda n: d[n]
          
      rows = "ABC DEF GHI".split()
      cols = list(''.join(x) for x in zip(*rows))
      diags = ["AEI", "CEG"]
      vars = rows + cols + diags
      vars += [v[::-1] for v in vars]
      ans = ' + '.join(f"pts({v})" for v in vars)
      
      exprs = []
      # avoid rotations, reflections etc
      exprs.append("A < C")  
      exprs.append("A < G")   
      exprs.append("A < I")  
      exprs.append("B < D") 
      # calculate one of the variables
      exprs.append("45 - (A + B + C + D + E + F + G + I) = H") 
        
      # the alphametic puzzle
      p = SubstitutedExpression(
        exprs,
        answer="@ans, (ABC, DEF, GHI)",
        macro=dict([("ans", ans)]),
        d2i=dict([(1, "CDGI")] +
                 [(k, "A") for k in range(7, 9)] +
                 [(9, "AB")]),
        env=dict(pts=pts),
        digits=range(1, 10),
        verbose=0,    # use 256 to see the generated code
      ) 
      
      mx, sols = 0, []
      # collect answers
      for t, m in sorted(p.answers(), reverse=True):
        if mx and t != mx: break
        sols.append(m)
        mx = t
        
      print("answer:", mx)
      print()
      print("Example(s):")
      # print examples
      for s in sols:
        print()
        print('\n'.join(str(x) for x in s))  
      

      Like

    • Ruud's avatar

      Ruud 7:41 pm on 2 March 2025 Permalink | Reply

      Even more brute force:

      import istr
      
      
      def is_square(n):
          square_root = int(n) ** (1.0 / 2.0)
          return round(square_root) ** 2 == n
      
      
      def is_cube(n):
          cube_root = int(n) ** (1.0 / 3.0)
          return round(cube_root) ** 3 == n
      
      
      max_score = 0
      
      for a, b, c, d, e, f, g, h, i in istr.permutations(istr.digits("1-9")):
          total = 0
          for n in (a | b | c, d | e | f, g | h | i, a | d | g, b | e | h, c | f | i, a | e | i, c | e | g):
              for m in n, n[::-1]:
                  total += m.is_prime() + 3 * is_square(m) + 6 * is_cube(m)
          max_score = max(max_score, total)
      
      print(max_score)
      

      Liked by 1 person

    • Frits's avatar

      Frits 6:11 am on 4 March 2025 Permalink | Reply

      Using Jim’s trick of also storing the points of the reversed entry.

      from itertools import permutations
      
      # determine valid primes 123 - 987
      P = {3, 5, 7}
      P |= {n for n in range(11, 100, 2) if all(n % p for p in P)}
      P = {str(n) for n in range(123, 988, 2) if all(n % p for p in P)}
           
      sqs = {str(n * n) for n in range(10, 32)}
      cbs = {str(n * n * n) for n in range(5, 10)}
      
      # make dictionary of points per three-digits tuple
      d = {tuple(int(x) for x in p): 1 
           if (s := ''.join(p)) in P else 3 if s in sqs else 6 if s in cbs else 0 
           for p in permutations("123456789", 3)}
                
      # check numbers both square and cube        
      for c in cbs:
        if c in sqs:
          d[tuple(int(x) for x in c)] =  9  
          
      # let every entry also contain the points of the reversed entry 
      d = {k: v + d[k[::-1]] for k, v in d.items()}
      
      # return points for sequence of three digits <s>
      pts = lambda s: d[s]
      
      alldgts = set(range(1, 10))
      sols, mx = [], 0
      
      # A B C
      # D E F
      # G H I
          
      for A in range(1, 7):
        # eliminate repeated solutions
        for C, G, I in permutations(range(A + 1, 10), 3):
          r5 = alldgts - {A, C, G, I}
          for B in r5 - {9}:  # B < D
            r4 = r5 - {B}
            p4 = pts((A, B, C))
            for D in [x for x in r4 if x > B]:
              p3 = p4 + pts((A, D, G))
              for E, F, H in permutations(r4 - {D}):
                p0 = (p3 + pts((D, E, F)) + pts((G, H, I)) + pts((B, E, H)) + 
                           pts((C, F, I)) + pts((A, E, I)) + pts((C, E, G))) 
                if p0 >= mx:
                  if p0 > mx:
                    sols = []
                    mx = p0
                  sols.append([(A, B, C), (D, E, F), (G, H, I)])
                
      print("answer:", mx)
      print()
      print("Example(s):")
      # print examples
      for s in sols:
        print()
        print('\n'.join(str(x) for x in s))  
      

      Like

  • Unknown's avatar

    Jim Randell 5:07 pm on 26 February 2025 Permalink | Reply
    Tags:   

    Teaser 2548: Planetary line 

    From The Sunday Times, 24th July 2011 [link] [link]

    George and Martha are studying a far-off galaxy consisting of 10 planets circling a sun clockwise, all in the same plane. The planets are Unimus (taking one year to circle the sun), Dubious (two years), Tertius (three years), and so on up to Decimus (10 years).

    At one instant today the sun and all 10 planets were in one straight line. Martha said it will be ages before that happens again. “Don’t let’s be greedy,” said George. “How long must we wait until at least nine planets and the sun are in a straight line?”

    How long must they wait?

    In honour of the current planetary parade.

    [teaser2548]

     
    • Jim Randell's avatar

      Jim Randell 5:08 pm on 26 February 2025 Permalink | Reply

      Assuming the planets are in circular orbits, and they can be aligned on opposite sides of the sun.

      If we have a faster planet that makes a half orbit in period x, and a slower planet that makes a half orbit in period y, then, if they are initially in alignment, the time t taken for the fast planet to get one half orbit ahead of the slow planet is given by:

      t/x = t/y + 1

      t = x.y / |x − y|

      For for any collection of planets we can choose one of the planets, calculate the time taken for it to align with each of the other planets separately, and then calculate the LCM of these values to find the earliest time when they all align.

      This Python program runs in 68ms. (Internal runtime is 211µs).

      from enigma import (Rational, Accumulator, mlcm, mgcd, subsets, call, arg, printf)
      
      Q = Rational()
      
      # orbital periods of the planets
      orbits = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
      
      k = arg(9, 0, int)
      
      # find the earliest (half-orbit) alignment of the planets <ss>
      def align(ss):
        x = ss[0]
        qs = list(Q(x * y, 2 * abs(x - y)) for y in ss[1:])
        # calculate the LCM of the fractions
        n = call(mlcm, (q.numerator for q in qs))
        d = call(mgcd, (q.denominator for q in qs))
        return Q(n, d)
      
      # find minimal times
      r = Accumulator(fn=min, collect=1)
      
      # consider k sized subsets
      for ss in subsets(orbits, size=k):
        # calculate the alignment times for this set
        t = align(ss)
        printf("[{ss} -> {t} ({f})]", f=float(t))
        r.accumulate_data(t, ss)
      
      # output solution
      printf("min time = {r.value} ({f}) years", f=float(r.value))
      for ss in r.data:
        printf("-> {ss}")
      

      Solution: 9 planets and the sun will align after 180 years.

      If all the planets start at an angle of 0°, then in 180 years we have the following alignment:

      Planet 1: 0°
      Planet 2: 0°
      Planet 3: 0°
      Planet 4: 0°
      Planet 5: 0°
      Planet 6: 0°
      Planet 7: 257.14°
      Planet 8: 180°
      Planet 9: 0°
      Planet 10: 0°

      We see that planets 1, 2, 3, 4, 5, 6, 9, 10 are in their original positions, and planet 8 is on the opposite side of the sun (but still in a straight line with the other planets). Planet 7 is the only one that is off the line.

      After 1260 years all the planets will have completed a whole number of half-orbits (all except 8 have returned to their original positions, but 8 is on the opposite side of the sun), and after 2520 years (= LCM of 1 .. 10) all the planets have returned to their original positions and the cycle will start again. The cycle of alignments is as follows:

      0, 2520 years = all planets at 0°
      180, 540, 900, 1620, 1980, 2340 years = planets 1, 2, 3, 4, 5, 6, 9, 10 at 0°; planet 8 at 180°; planet 7 out of alignment
      360, 720, 1080, 1440, 1800, 2160 years = planets 1, 2, 3, 4, 5, 6, 8, 9, 10 at 0°; planet 7 out of alignment
      420, 2100 years = planets 1, 2, 3, 4, 5, 6, 7, 10 at 0°; planet 8 at 180°; planet 9 out of alignment
      630, 1890 years = planets 1, 2, 3, 5, 6, 7, 9, 10 at 0°; planet 4 at 180°; planet 8 out of alignment
      840, 1680 years = planets 1, 2, 3, 4, 5, 6, 7, 8, 10 at 0°; planet 9 out of alignment

      But we can have alignments other than at 0°/180°:

      For example the earliest alignment of the 5 planets 1, 3, 5, 7, 9 is after 78.75 years:

      They are aligned along 90°/270°.

      Like

  • Unknown's avatar

    Jim Randell 9:49 am on 25 February 2025 Permalink | Reply
    Tags:   

    Teaser 2504: [Hidden crosses] 

    From The Sunday Times, 19th September 2010 [link] [link]

    Here are 18 black cards and there is a cross on the back of some of them.

    The clue in any gap indicates how many crosses there are on cards that are adjacent to that gap.

    How many black cards have a cross on them?

    This puzzle was originally published with no title.

    [teaser2504]

     
    • Jim Randell's avatar

      Jim Randell 9:50 am on 25 February 2025 Permalink | Reply

      Here is a solution using the [[ SubstitutedExpression ]] solver from the enigma.py library.

      It runs in 80ms. (And the internal runtime of the generated program is just 53µs).

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      #  +---+---+---+---+---+---+
      #  | A |   | B |   | C |!=0|
      #  +---+---+---+---+---+---+
      #  | 1 | D | >2| E | <2| F |
      #  +---+---+---+---+---+---+
      #  | G |   | H |!=1| I |   |
      #  +---+---+---+---+---+---+
      #  |   | J |   | K | >1| L |
      #  +---+---+---+---+---+---+
      #  | M | >2| N |!=3| P |   |
      #  +---+---+---+---+---+---+
      #  |!=2| Q |   | R | =1| S |
      #  +---+---+---+---+---+---+
      
      --distinct=""
      --digits="0,1"
      
      # the clues
      "C + F != 0"
      "A + D + G = 1"
      "B + D + E + H > 2"
      "C + E + F + I < 2"
      "E + H + I + K != 1"
      "I + K + L + P > 1"
      "J + M + N + Q > 2"
      "K + N + P + R != 3"
      "M + Q != 2"
      "P + R + S = 1"
      
      --answer="sum([A, B, C, D, E, F, G, H, I, J, K, L, M, N, P, Q, R, S])"
      --template=""
      

      Solution: 10 of the black cards have a cross on them.

      There are 4 possible arrangements, that can be summarised as follows:

      8 of the crosses are marked X, and the remaining 2 crosses appear in squares marked a and b.

      Like

    • ruudvanderham's avatar

      ruudvanderham 12:26 pm on 1 March 2025 Permalink | Reply

      import itertools
      
      
      def s(*args):
          return sum(a[arg] for arg in args)
      
      
      for a in itertools.product((0, 1), repeat=18):
          if (
              s(2, 5) != 0
              and s(0, 3, 6) == 1
              and s(1, 3, 4, 7) > 2
              and s(2, 4, 5, 8) < 2
              and s(4, 7, 8, 10) != 1
              and s(8, 10, 11, 14) > 1
              and s(9, 12, 13, 15) > 2
              and s(10, 13, 14, 16) != 3
              and s(12, 15) != 2
              and s(14, 16, 17) == 1
          ):
              print(a, sum(a))
      

      Like

  • Unknown's avatar

    Jim Randell 6:42 am on 23 February 2025 Permalink | Reply
    Tags:   

    Teaser 3257: Powers of deduction 

    From The Sunday Times, 23rd February 2025 [link] [link]

    One evening, Sherlock Holmes challenges Watson and Mycroft to a game of Cluedo.

    He places one each of the six “person” cards, six “weapon” cards and nine “room” cards into the murder bag, shuffles the rest and deals them out equally.

    Mycroft sees his cards and says, “If I guessed what was in the murder bag now, my chance of being correct is one in an integer that is a power greater than two”.

    “I’m saying nothing”. Watson replies. “I’d only give something away”.

    “Commendably prudent”, Sherlock says, “but the same is true for you”.

    Watson studies his cards, then looks agitated. “How could you know that?”

    “Sherlock isn’t cheating”. Mycroft reassures him. “He didn’t even know how many person cards you’re holding”.

    How many room cards is Mycroft holding?

    [teaser3257]

     
    • Jim Randell's avatar

      Jim Randell 8:14 am on 23 February 2025 Permalink | Reply

      After the culprit is selected there are 5 person, 5 weapon and 8 room cards remaining, and these are distributed among the 3 players (so, 6 cards each).

      If a player is holding p person cards, w weapon cards, and r room cards, then the probability of making a correct accusation from the cards he is not holding is:

      P = (1/(6 − p)) × (1/(6 − w)) × (1/(9 − r))
      1/P = (6 − p) × (6 − w) × (9 − r)

      This Python program finds possible (p, w, r) hands that have a 1/(n^k) chance of giving a correct guess, where k > 2.

      from enigma import (decompose, irange, inf, powers, first, lt, printf)
      
      # there are only a few higher powers than squares to consider
      (M, pows) = (6 * 6 * 9, set())
      for k in irange(3, inf):
        ps = first(powers(2, inf, k), count=lt(M))
        if not ps: break
        pows.update(ps)
      
      # find hands of 6 cards, with a 1/power chance of a correct guess
      for (p, w, r) in decompose(6, 3, increasing=0, sep=0, min_v=0):
        if p > 5 or w > 5: continue
        N = (6 - p) * (6 - w) * (9 - r)
        if N not in pows: continue
        # output possible hands
        printf("p={p} w={w} r={r} -> N={N}")
      

      The only cards that give a required 1/power chance are:

      (p w r) = (3 3 0) = 1/81 (81 = 3^4)
      (p w r) = (1 1 4) = 1/125 (125 = 5^3)

      Mycroft declares he has one of these hands, and Sherlock declares Watson has one of these hands. It is not possible for there to be more than one (3 3 0) hand, but if there are a (3 3 0) and a (1 1 4) then the remaining hand is also a (1 1 4), and if there are two (1 1 4) hands then the remaining hand is a (3 3 0), and so Sherlock also has one of these hands.

      Hence the three hands are (1 1 4) (1 1 4) (3 3 0) (in some order). So when Mycroft declares he has one of these, Sherlock can see that he (Sherlock) also has one, and so Sherlock can declare that Watson must too have one of these hands.

      Which means Mycroft and Sherlock both know that each of them has one of these three hands, and if either of them has (3 3 0) they know the other two must both have (1 1 4).

      So Sherlock must be holding (1 1 4) (as Mycroft states that he does not know which of the possible hands Watson is holding), and the only way Mycroft can be certain that Sherlock has (1 1 4) is if he (Mycroft) is holding (3 3 0).

      And so: Mycroft = (3 3 0), Watson = (1 1 4), Sherlock = (1 1 4).

      Solution: Mycroft is holding no room cards.

      Like

      • Jim Randell's avatar

        Jim Randell 9:44 pm on 23 February 2025 Permalink | Reply

        I’ve not seen another solution to this puzzle yet, so here is a complete solution.

        The following Python program runs in 60ms. (Internal runtime is 476µs).

        from enigma import (
          namedtuple, decompose, irange, inf, powers, first, lt,
          subsets, filter_unique, unpack, intersect, seq2str, printf
        )
        
        # there are only a few higher powers than squares to consider
        (M, pows) = (6 * 6 * 9, set())
        for k in irange(3, inf):
          ps = first(powers(2, inf, k), count=lt(M))
          if not ps: break
          pows.update(ps)
        
        # find hands of 6 cards, with a 1/power chance of a correct guess
        Hand = namedtuple('Hand', 'p w r')
        hs = list()
        for (p, w, r) in decompose(6, 3, increasing=0, sep=0, min_v=0):
          if p > 5 or w > 5: continue
          N = (6 - p) * (6 - w) * (9 - r)
          if N not in pows: continue
          # output possible hands
          printf("[({p}, {w}, {r}) -> N={N}]")
          hs.append(Hand(p, w, r))
        
        # collect possible scenarios
        ss = list()
        # M and W each have one of these hands
        for (M, W) in subsets(hs, size=2, select='M'):
          # and S has the remaining cards
          S = Hand(5 - M.p - W.p, 5 - M.w - W.w, 8 - M.r - W.r)
          if S.p < 0 or S.w < 0 or S.r < 0: continue
          ss.append((M, W, S))
        
        # find situations where S _cannot_ determine W.p
        ss1 = filter_unique(ss, unpack(lambda M, W, S: S), unpack(lambda M, W, S: W.p)).non_unique
        
        # find situations where M _can_ fully determine S's hand
        ss2 = filter_unique(ss, unpack(lambda M, W, S: M), unpack(lambda M, W, S: S)).unique
        
        # look for common situations
        for (M, W, S) in intersect([ss1, ss2]):
          printf("M={M} W={W} S={S}", M=seq2str(M), W=seq2str(W), S=seq2str(S))
        

        Like

  • Unknown's avatar

    Jim Randell 12:09 pm on 21 February 2025 Permalink | Reply
    Tags:   

    Teaser 2585: Easter Teaser 

    From The Sunday Times, 8th April 2012 [link] [link]

    I took the letters A, E, E, R, S and T and wrote down the long list of all possible different combinations of them. So, in alphabetical order, my list was AEERST, AERTS, …, EASTER, …, TEASER, …, TSREEA. I then assigned each of the letters a different odd digit, turning my list into a list of numbers. Surprisingly, the grand total of my list of numbers contained no odd digit.

    What is E+A+S+T+E+R?

    This completes the archive of Teaser puzzles from 2012, so there is now a complete archive of puzzles (and solutions) from Teaser 2569 (December 2011) to the most recent puzzle Teaser 3256 (February 2025), along with many earlier puzzles. There are another 21 puzzles to post from 2011 before all puzzles from the 2020 book are posted.

    Between S2T2 and Enigmatic Code there are now 3577 puzzles available.

    [teaser2585]

     
    • Jim Randell's avatar

      Jim Randell 12:09 pm on 21 February 2025 Permalink | Reply

      The following Python runs in 59ms. (Internal runtime is 367µs).

      from enigma import (irange, multiset, repdigit, subsets, nsplit, map2str, printf)
      
      word = 'EASTER'
      m = multiset.from_seq(word)
      
      # calculate multiplier for sum of letters
      n = m.size()
      p = repdigit(n) * m.multinomial() // n
      
      # even/odd digits
      evens = set(irange(0, 9, step=2))
      odds = irange(1, 9, step=2)
      
      # now assign odd digits to each letter
      rs = set()
      ks = sorted(m.keys())
      for vs in subsets(odds, size=len(ks), select='P'):
        d = dict(zip(ks, vs))
        # calculate the sum of the letters in the word
        s = sum(d[x] for x in word)
        # calculate the sum of all the permutations
        t = s * p
        # check the total contains only even digits
        if not evens.issuperset(nsplit(t)): continue
        # answer is sum of the letters in word
        printf("[sum = {s}; {d} -> {t}]", d=map2str(d))
        rs.add(s)
      
      printf("answer = {rs}", r=sorted(rs))
      

      Solution: E+A+S+T+E+R = 34.

      E is 9, and A, R, S, T are 1, 3, 5, 7 in some order.

      And the sum of all possible arrangements is 226666440.


      For EASTER there are 360 different arrangements of the letters (there are 2 E‘s that are not distinguishable).

      There are 6 possible positions each letter can appear in. And each of the 6 letters (including repeats) appears in each position 60 times.

      So each letter contributes 60 × 111111 = 6666660 times its own value to the total sum.

      With the sum of the letters in EASTER = 1 + 3 + 5 + 7 + 2×9 = 34 we arrive at a total sum of:

      total = 34 × 6666660 = 226666440

      which consists entirely of even digits.

      And we see that reordering the values of the non-repeated letters does not change the total.

      Using two copies of any odd digit other than 9 gives a total with at least one odd digit.

      Like

  • Unknown's avatar

    Jim Randell 8:30 am on 18 February 2025 Permalink | Reply
    Tags: by: Neil MacKinnon   

    Teaser 2567: A close thing 

    From The Sunday Times, 4th December 2011 [link] [link]

    Our football league consists of five teams, each playing each other once, earning three points for a win, one for a draw. Last season was close. At the end, all five teams had tied on points and on “goal difference”.

    So the championship was decided on “goals scored”, which, for the five teams, consisted of five consecutive numbers. The scores in the games were all different, including a no-score draw, and no team scored more than five goals in any game.

    How many goals did the league champions score in total?

    [teaser2567]

     
    • Jim Randell's avatar

      Jim Randell 8:30 am on 18 February 2025 Permalink | Reply

      I started doing some analysis of the puzzle, in order to write a program, but the solution fell out:

      There are 5 teams and each team plays 4 matches and has the same number of points. So the number of points must be a multiple of 5.

      There are 10 matches in total, and 2 or 3 points are awarded in each match, so the total number of points is between 20 (all draws) and 30 (all wins).

      So the total number of points must be 20, 25, or 30. However we are told there is a 0-0 draw, so the total cannot be 30, and there are only 6 possible different scores in drawn matches, so the total cannot be 20. Hence the total number of points awarded must be 25, and each team must have gained 5 points in their 4 matches. So each team must win 1 match, draw 2 matches, and lose the remaining match.

      So there are 5 matches that are won (each by one of the teams), and 5 matches that are drawn.

      The sum of the goal differences must be 0 (each goal for one team is against some other team), and so if the goal differences are all the same, they must all be 0.

      Hence the winning margin for each team in their won game must be balanced by the losing margin in their lost game, and so all the won games are won by the same margin. And to have 5 different scores the winning margin must be 1 goal (i.e. the games are 1-0, 2-1, 3-2, 4-3, 5-4). And these account for 25 goals scored.

      The remaining goals scored are accounted for by the 5 drawn matches, which are 0-0 and 4 of {1-1, 2-2, 3-3, 4-4, 5-5}.

      But the numbers of goals scored by each team are 5 consecutive numbers, say (x, x + 1, x + 2, x + 3, x + 4), which means the total number of goals scored is 5x + 10, i.e. a multiple of 5.

      And so the missing draw must be 5-5, leaving 20 goals scored in the drawn matches.

      So the total number of goals scored is 25 + 20 = 45 = 5x + 10.

      Hence x = 7, and the total goals scored by each team are (7, 8, 9, 10, 11).

      Solution: The league champions scored 11 goals in total.


      But I wanted to find a possible arrangement of matches, and so here is a program that does that:

      from enigma import (multiset, compare, subsets, diff, rev, unzip, flatten, printf)
      
      # won matches
      wins = [(1, 0), (2, 1), (3, 2), (4, 3), (5, 4)]
      # drawn matches
      draws = [(0, 0), (1, 1), (2, 2), (3, 3), (4, 4)]
      
      # choose matches from <wins>, <draws> that along with existing matches <ms>
      # give 1 win, 1 loss, 2 draws, and <g> goals scored (and conceded)
      def choose(g, wins, draws, ms=[]):
        # count how many wins/draws/losses we already have
        m = multiset.from_seq(compare(x, y) for (x, y) in ms)
        (w, d, l) = (m.count(x) for x in [1, 0, -1])
        if w > 1 or l > 1 or d > 2: return
        # choose wins, losses, draws to make up the missing goals
        for ws in subsets(wins, size=1 - w):
          for ws_ in subsets(diff(wins, ws), size=1 - l):
            ls = tuple(rev(x) for x in ws_)
            for ds in subsets(draws, size=2 - d):
              # calculate goals for/against
              rs = flatten([ws, ls, ds])
              (f, a) = (sum(xs) for xs in unzip(flatten([rs, ms])))
              if not (f == a == g): continue
              # return (<chosen matches>, <remaining wins>, <remaining draws>)
              yield (rs, diff(wins, ws, ws_), diff(draws, ds))
      
      # find matches for (A, B, C, D, E) that give goals scored of (gA, gB, gC, gD, gE)
      def solve(gA, gB, gC, gD, gE):
        # choose a set of matches for A
        for (msA, wsA, dsA) in choose(gA, wins, draws):
          # and an order the matches
          for (AB, AC, AD, AE) in subsets(msA, size=len, select='P'):
      
            # choose the remaining matches for B
            for (msB, wsB, dsB) in choose(gB, wsA, dsA, [rev(AB)]):
              # and an order for the matches
              for (BC, BD, BE) in subsets(msB, size=len, select='P'):
      
                # choose the remaining matches for C
                for (msC, wsC, dsC) in choose(gC, wsB, dsB, [rev(AC), rev(BC)]):
                  # and an order for the matches
                  for (CD, CE) in subsets(msC, size=len, select='P'):
      
                    # the remaining match is DE
                    for ((DE,), wsD, dsD) in choose(gD, wsC, dsC, [rev(AD), rev(BD), rev(CD)]):
      
                      # check goals for/against E
                      for _ in choose(gE, wsD, dsD, [rev(AE), rev(BE), rev(CE), rev(DE)]):
      
                        # return the matches
                        yield (AB, AC, AD, AE, BC, BD, BE, CD, CE, DE)
      
      # find solutions
      for (AB, AC, AD, AE, BC, BD, BE, CD, CE, DE) in solve(11, 10, 9, 8, 7):
        printf("AB={AB} AC={AC} AD={AD} AE={AE} BC={BC} BD={BD} BE={BE} CD={CD} CE={CE} DE={DE}")
        break  # we only need one solution
      

      Here is an example set of matches:

      A vs B = 3-3
      A vs C = 3-4
      A vs D = 4-4
      A vs E = 1-0
      B vs C = 1-1
      B vs D = 2-1
      B vs E = 4-5
      C vs D = 2-3
      C vs E = 2-2
      D vs E = 0-0

      In total the program finds 200 ways to assign the matches so that the total goals scored for (A, B, C, D, E) are (11, 10, 9, 8, 7).

      The program finds an example set of matches in 3.3ms, and takes 209ms to find all 200 sets.

      Like

    • Frits's avatar

      Frits 10:33 am on 22 February 2025 Permalink | Reply

      Only using the fact that each team has 1 win, 1 loss and 2 draws.
      The program prints all 200 possibilities. It runs for 50 seconds.

      from itertools import permutations
      
      pts = lambda s: sum([3 if x > y else 0 if x < y else 1 for x, y in s])
      gdiff = lambda s: sum([x - y for x, y in s])
      gscored = lambda s: sum([x for x, _ in s])
      rev = lambda s: s[::-1]
      output = lambda s: '   '.join(', '.join(f"{x}-{y}" for x, y in z) for z in s)
      
      # determine 4 matches for each of <k> teams out of remaining games <gs>
      def solve(k, gs, gf=0, ss=[]):
        if k == 0:
          # draw 0-0 must exist (more likely at the end of <ss>)
          if any((0, 0) in ss[i] for i in range(3, -1, -1)): 
            yield ss, gf
        else:  
          # pick <k - 1> games from remaining games <gs>
          for p in permutations(gs, k - 1):
            gs_ = gs.difference(p)
            gs_ -= {rev(x) for x in p} 
            gd = gdiff(p)
            
            if not ss:
              # reverse score may not be played by the same player
              if any((y, x) in p for x, y in p if x != y): continue
            
              # we need at least one draw out of the 3 games
              if sum([x == y for x, y in p]) == 0: continue
              
              # process goals "for" for the 4th game
              for g4_f in range(6): 
                # goal difference must be zero
                if (g4 := (g4_f, gd + g4_f)) not in gs_: continue
                if pts(g := p + (g4, )) != 5: continue
                yield from solve(k - 1, gs_ - {g4, rev(g4)}, gscored(g) - 1, [g])
            else:   
              # already known games for this player
              known = tuple(rev(x[len(ss) - 1]) for x in ss)
              n3 = known + p
              
              # we need at least one draw out of the 3 games
              if sum([x == y for x, y in n3]) == 0: continue
              
              gs_ = gs_.difference(known)
              gs_ -= {rev(x) for x in known} 
              
              # goals "for" for the 4th game
              g4_f = gf - gscored(n3)     
              # goal difference must be zero
              if (g4 := (g4_f, gdiff(n3) + g4_f)) not in gs_: continue
              
              if pts(g := n3 + (g4, )) != 5: continue
              # reverse score may not be played by the same player
              if any((y, x) in g for x, y in g if x != y): continue
              
              yield from solve(k - 1, gs_ - {g4, rev(g4)}, gf - 1, ss + [g])
      
      games = {(i, j) for i in range(6) for j in range(6)}  
      sols = set()
      cnt = 0 
      # determine matches of teams A, B, C and D (decreasing scored goals)
      for (a, b, c, d), gf in solve(4, games):
        # check matches of team E that scored four goals less than A
        e = (rev(a[3]), rev(b[3]), rev(c[3]), rev(d[3]))
        if gscored(e) != gf: continue 
        if gdiff(e) != 0: continue
        if pts(e) != 5: continue  
        
        # integrity check
        abcde = a + b + c + d + e
        if any(abcde.count((x, y)) > 1 + (x == y) for x, y in abcde): continue
              
        cnt += 1
        print(f"{cnt:>3} ", output([a, b, c, d, e]))
        sols.add(str(gf + 4))
        
      print("answer:", ' or '.join(sols))  
      

      Like

    • Frits's avatar

      Frits 3:02 am on 23 February 2025 Permalink | Reply

      A similar approach. It takes 9 seconds to run.

      from enigma import SubstitutedExpression
       
      '''
      A vs B = A-B
      A vs C = C-D
      A vs D = E-F
      A vs E = G-H
      B vs C = I-J
      B vs D = K-L
      B vs E = M-N
      C vs D = O-P
      C vs E = Q-R
      D vs E = S-T
      '''
      
      pts = lambda s: sum([3 if x > y else 0 if x < y else 1 for x, y in s])
      gdiff = lambda s: sum([x - y for x, y in s])
      gscored = lambda s: sum([x for x, _ in s])
      # return sorted tuple
      stup = lambda x, y: (x, y) if x < y else (y, x)
      alldiff = lambda *s: len(set(p := [stup(x, y) for x, y in s])) == len(p)
      output = lambda s: '   '.join(', '.join(f"{x}-{y}" for x, y in z) for z in s)
      
      
      # check if both known games for C, D and E have zero goal difference 
      # (if win and loss)     
      def check(ss):
        chk = (1, 2, 3)  # check known games in C,D and E 
        # get known games for C, D and E (no need to reverse them)
        games_per_team = [[x[i] for x in ss] for i in chk]
        for gs in games_per_team:
          gd = [x - y for x, y in gs if x != y]
          # two non-draws?
          if len(gd) == 2 and sum(gd):
            return False
              
        return True     
      
      # the alphametic puzzle
      p = SubstitutedExpression(
        
        [ # all goal differences must be zero and 
          # we assume 1 win, 1 loss and 2 draws
          "gdiff([(A, B), (C, D), (E, F)]) + G = H",
          "(points := pts(gsa := [(A, B), (C, D), (E, F), (G, H)])) == 5",
          "alldiff(*gsa)",
          
          "(goalsa := gscored(gsa)) - B - I - K - 1 = M",
          "alldiff(*gsa, (I, J))",
          "gdiff([(B, A), (I, J), (K, L)]) + M = N",
          "pts(gsb := [(B, A), (I, J), (K, L), (M, N)]) == points",
          "alldiff(*gsa, *gsb[1:])",
          # check the already known games of C, D and E
          "check([gsa, gsb])",
          
          "goalsa - D - J - O - 2 = Q",
          "gdiff([(D, C), (J, I), (O, P)]) + Q = R",
          "pts(gsc := [(D, C), (J, I), (O, P), (Q, R)]) == points",
          "alldiff(*gsa, *gsb[1:], *gsc[2:])",
          
          "goalsa - F - L - P - 3 = S",
          "goalsa - H - N - R - 4 = T",
          "pts(gsd := [(F, E), (L, K), (P, O), (S, T)]) == points",
          "gdiff(gsd) == 0",  
          
          "pts(gse := [(H, G), (N, M), (R, Q), (T, S)]) == points",
          "gdiff(gse) == 0",  
          
          "(0, 0) in (*gsa, *gsb[1:], *gsc[2:], *gsd[3:])",
          "alldiff(*gsa, *gsb[1:], *gsc[2:], *gsd[3:])",
        ],
        answer="(gsa, gsb, gsc, gsd, gse), goalsa",
        base=6,
        distinct="",
        env=dict(pts=pts, alldiff=alldiff,gdiff=gdiff,gscored=gscored, check=check),
        reorder=0,
        verbose=0,    # use 256 to see the generated code
      ) 
      
      sols = set()
      # print answers
      for i, (gs, ga) in enumerate(p.answers(), 1):
        print(f"--{i:>4}-- {output(gs)}")
        sols.add(str(ga))
      
      print("answer:", ' or '.join(sols))  
      

      Like

  • Unknown's avatar

    Jim Randell 7:14 am on 16 February 2025 Permalink | Reply
    Tags:   

    Teaser 3256: Domino-do 

    From The Sunday Times, 16th February 2025 [link] [link]

    I have a standard set of 28 dominoes with a number of spots from zero to six at each end, and each possible pair of numbers occurring once in the set. When two dominoes in a line touch, the two adjacent ends must have the same number of spots.

    I started a line by placing one of the dominoes on a table. Then, in correct domino style, I placed another adjacent to it at the right-hand end. I continued in this way (placing each domino at either end of the line) until more than 14 dominoes were lined up. At each stage the total number of spots on the table was a different odd number less than 100. In fact, the last two totals were prime but the first two totals were not.

    From the left, what were the first six dominoes in the line (in the form 1-4, 4-0, …)?

    [teaser3256]

     
    • Jim Randell's avatar

      Jim Randell 8:25 am on 16 February 2025 Permalink | Reply

      The totals are all (different) odd numbers, so we must start with an odd domino (that is not prime), and each subsequently placed domino must be even (and not [0-0]). So at most one of the other even dominoes can remain unused.

      Here is a constructive program. It starts by find possible placements for the first and second dominoes (an odd domino, and then a non-zero even domino, such that the totals are both non-prime), and then considers viable sets of at least 13 more even dominoes, that give a prime total for the entire line, and then looks for the final placement (that also gives a prime total after the penultimate placement), and then tries to construct a sequence of plays that gives a viable line of dominoes.

      This Python 3 program runs in 625ms.

      from enigma import (irange, subsets, is_prime, filter2, rev, remove, unpack, delete, multiset, printf)
      
      # a standard set of dominoes
      ds = list(subsets(irange(0, 6), size=2, select='R'))
      ds.remove((0, 0))  # but we can't play [0-0], as totals must be all different
      
      # canonical form of a domino
      def canonical(d): (x, y) = d; return (d if x < y else (y, x))
      
      # add <k> dominoes from <ds> to the line <ss> (totals so far = <ts>)
      # return (<domino line>, <totals>, <unused dominoes>)
      def solve(k, ds, ss, ts):
        # are we done?
        if not k:
          yield (ss, ts, ds)
        else:
          # find candidates for the next domino
          ps = list()
          for (x, y) in ds:
            # place right
            if ss[-1][-1] == x:
              ps.append((-1, (x, y)))
            if x != y and ss[-1][-1] == y:
              ps.append((-1, (y, x)))
            # place left
            if ss[0][0] == x:
              ps.append((0, (y, x)))
            if x != y and ss[0][0] == y:
              ps.append((0, (x, y)))
          # make the placement
          for (i, d) in ps:
            t = ts[-1] + sum(d)
            ds_ = remove(ds, canonical(d))
            ss_ = (ss + [d] if i == -1 else [d] + ss)
            yield from solve(k - 1, ds_, ss_, ts + [t])
      
      # split the dominoes into odd and even
      (odds, evens) = filter2((lambda x: sum(x) % 2 == 1), ds)
      
      # collect required answers
      rs = multiset()
      
      # we start with an odd (non-prime) domino
      for d in odds:
        t = sum(d)
        if is_prime(t): continue
      
        # find a placement for the second (even) domino
        for (ss2, ts2, ds2) in solve(1, evens, [d], [t]):
          # which gives a non-prime total
          if is_prime(ts2[-1]): continue
          # the first placement is on the right, so reflect if necessary
          if canonical(ss2[1]) == d: ss2 = rev(rev(x) for x in ss2)
          # count the number of times each end occurs
          m = multiset.from_seq(*ss2)
      
          # look for a set of at least 13 additional even dominoes to place
          for ds in subsets(ds2, min_size=13):
            # calculate total number of pips (must be a prime, < 100)
            t = ts2[-1] + sum(sum(x) for x in ds)
            if not (t < 100 and is_prime(t)): continue
            # in the complete line at most 2 ends can appear an odd number of times
            if sum(n % 2 for n in m.combine(*ds).values()) > 2: continue
      
            # consider the final domino, so the penultimate total is prime
            for (i, dx) in enumerate(ds):
              tx = t - sum(dx)
              if not is_prime(tx): continue
      
              # place all the other dominoes
              printf("[trying {ss2} -> {dx}]")
              dsx = delete(ds, [i])
              for (ssx, tsx, _) in solve(len(dsx), dsx, ss2, ts2):
                # can we place the final domino?
                for (ss, ts, _) in solve(1, [dx], ssx, tsx):
                  printf("[{d} -> {ss} {ts}]")
                  rs.add(tuple(ss[:6]))
      
      # output the answer(s)
      for (ss, n) in rs.most_common():
        printf("{ss} ... [{n} solutions]")
      

      Solution: The leftmost dominoes are: [5-3] [3-3] [3-1] [1-1] [1-5] [5-5].

      There are two possibilities for the completed line of dominos, each uses the same set of 15 dominos:

      [5-3] [3-3] [3-1] [1-1] [1-5] [5-5] [5-4] [4-2] [2-2] [2-0] [0-4] [4-4] [4-6] [6-6] [6-0]
      [5-3] [3-3] [3-1] [1-1] [1-5] [5-5] [5-4] [4-2] [2-2] [2-0] [0-6] [6-6] [6-4] [4-4] [4-0]

      The lines are the same for the leftmost 10 dominoes, and then the rightmost 5 dominoes may appear in either direction.

      The first play is the odd domino [5-4] (total = 9; odd, not prime), and this is followed on the right by [4-2] (total = 15; odd, not prime).

      The remaining dominoes are played (in any viable order) until there is only [5-3] remaining to place on the left end. Before it is played the total is 89 (odd, prime), and after it is played the total is 97 (odd, prime).

      The unused even domino is [2-6].

      An even domino must have ends that are the same parity, and so since we start with an odd domino (which must have ends that are of different parities) the even numbers accumulate on the even side of the initial domino, and the odd numbers accumulate on the odd side of the initial domino. And when the line is complete every end apart from the two outside ends must be paired with its neighbour, and so must appear an even number of times. The exceptions are the outside ends of the line, so there must be two ends (one of them even, and one of them odd) that appear an odd number of times. And this is the basis for my faster program below that just looks for viable final arrangements of dominoes.

      Like

      • Jim Randell's avatar

        Jim Randell 10:52 am on 18 February 2025 Permalink | Reply

        My original program finds all possible play sequences that give a viable line of dominoes, but there are a lot of them.

        However, for the most part, we don’t care about the order the dominoes are played in (only the first two moves, and the last move), so here is my program modified to just find viable domino lines.

        It has an internal runtime of 2.5ms.

        from enigma import (irange, subsets, is_prime, filter2, rev, remove, unpack, multiset, printf)
        
        # a standard set of dominoes
        ds = list(subsets(irange(0, 6), size=2, select='R'))
        ds.remove((0, 0))  # but we can't play [0-0], as totals must be all different
        
        # canonical form of a domino
        def canonical(d): (x, y) = d; return (d if x < y else (y, x))
        
        # number of pips on a domino
        pips = sum
        
        # mirror a sequence of dominoes
        mirror = lambda ds: rev(rev(d) for d in ds)
        
        # add <k> dominoes from <ds> to the line <ss>
        # return (<domino line>, <unused dominoes>)
        def solve(k, ds, ss):
          # are we done?
          if not k:
            yield (ss, ds)
          else:
            # find candidates for the next domino
            ps = list()
            for (x, y) in ds:
              # place right
              if ss[-1][-1] == x:
                ps.append((x, y))
              if x != y and ss[-1][-1] == y:
                ps.append((y, x))
            # make the placement
            for p in ps:
              yield from solve(k - 1, remove(ds, canonical(p)), ss + [p])
        
        # split the dominoes into odd and even
        (odds, evens) = filter2((lambda x: pips(x) % 2 == 1), ds)
        
        # we start with an odd (non-prime) domino
        for d in odds:
          if is_prime(pips(d)): continue
          # consider both starting orientations
          for d1 in (d, rev(d)):
            # find a placement for the second (even) domino
            for (ss2, ds2) in solve(1, evens, [d1]):
              # which gives a non-prime total
              t = sum(pips(d) for d in ss2)
              if is_prime(t): continue
              # the first placement is on the right, so reflect if necessary
              if canonical(ss2[1]) == d: ss2 = mirror(ss2)
              # count the number of times each end occurs
              m = multiset.from_seq(*ss2)
        
              # look for a set of at least 13 additional even dominoes to place
              for ds in subsets(ds2, min_size=13):
                # calculate total number of pips (must be a prime, < 100)
                tz = t + sum(pips(x) for x in ds)
                if not (tz < 100 and is_prime(tz)): continue
                # in the complete line only the 2 ends are unpaired
                if sum(n % 2 for n in m.combine(*ds).values()) != 2: continue
        
                # consider the final domino, so the penultimate total is prime
                for dx in ds:
                  tx = tz - pips(dx)
                  if not is_prime(tx): continue
        
                  # split the remaining even dominoes by parity
                  (ds0, ds1) = filter2((lambda x: x[0] % 2 == 0), ds)
                  # extend the line to the right and left
                  (dsr, dsl) = ((ds0, ds1) if ss2[-1][-1] % 2 == 0 else (ds1, ds0))
                  for (ssr, _) in solve(len(dsr), dsr, [ss2[-1]]):
                    for (ssl, _) in solve(len(dsl), dsl, [rev(ss2[0])]):
                      # check the final domino is played
                      if not (canonical(ssr[-1]) == dx or canonical(ssl[-1]) == dx): continue
                      # construct the final domino line
                      rs = mirror(ssl) + ssr
                      # output solution
                      printf("{ss2} ... [{dx}]")  # first two and last placements
                      printf("-> {rs}")  # completed line
        

        Liked by 1 person

    • Frits's avatar

      Frits 6:33 pm on 17 February 2025 Permalink | Reply

      from enigma import SubstitutedExpression, is_prime, tuples
      
      # return sorted tuple
      stup = lambda x, y: (x, y) if x <= y else (y, x)
       
      # invalid digit / symbol assignments
      d2i = dict()
      for dgt in range(0, 8):
        vs = set() 
        # only the ends may be seven (unused)
        if dgt == 7: vs.update('BCDEFGHIJKLMNOP')
        # HIJKLMNOP may not be odd 
        if dgt % 2: vs.update('HIJKLMNOP')
        if dgt in {1, 3, 5}: vs.update('Q')
        # ABCDEFG may not be even 
        if dgt % 2 == 0: vs.update('ABCDEFG')
        d2i[dgt] = vs   
      
      # the alphametic puzzle
      p = SubstitutedExpression(
        [
        # there are 15 dominoes with an even sum of which one may be unused
        # (6 with an odd number of spots and 9 with an even number of spots).
        # for the first domino (GH) pick one with an odd sum
        
        # temporarily the 2nd domino may be placed on both sides of the 1st domino
        # so if we find a solution the mirrored version is also valid
        
        # notation ABCDEF means a line of 5 dominoes (A-B)(B-C)(C-D)(D-E)(E-F)
        
        # odd nums   oe   even nums  
        # ABCDEF     GH   IJKLMNOPQ        (value 7 means unused)
        
        # 1st total is not prime
        "not is_prime(G + H)",
        # 2nd total is not prime (for now check both sides of 1st domino)
        "not is_prime(F + G + G + H) or not is_prime(G + H + H + I)",
        
        # at most one even sum is not needed
        "A + Q != 14",
       
        # a domino may not occur twice  
        "len(set(so := [stup(x, y) for x, y in tuples(@odds + (G, ))])) == len(so)",
        
        # a domino may not occur twice (following lines added for performance) 
        "stup(J, K) not in {stup(H, I)}",
        "stup(K, L) not in {stup(H, I), stup(I, J)}",
        "stup(L, M) not in {stup(H, I), stup(I, J), stup(J, K)}",
        "stup(M, N) not in {stup(H, I), stup(I, J), stup(J, K), stup(K, L)}",
        "stup(N, O) not in {stup(H, I), stup(I, J), stup(J, K), stup(K, L)," +
        "stup(L, M)}",
        "stup(O, P) not in {stup(H, I), stup(I, J), stup(J, K), stup(K, L)," +
        "stup(L, M), stup(M, N)}",
        "stup(P, Q) not in {stup(H, I), stup(I, J), stup(J, K), stup(K, L)," +
        "stup(L, M), stup(M, N), stup(N, O)}", 
        
        # domino (0, 0) may not be present
        "(0, 0) not in tuples((H, ) + @evens)",
        
        # a domino may not occur twice (final check)
        "len(set(se := [stup(x, y) for x, y in tuples((H, ) + @evens)])) == len(se)",
        
        # last total is prime and less than 100
        "(tot := 2 * sum(@odds + (G, H) + @evens) - (A if A < 7 else 14 + B) - " +
        "(Q if Q < 7 else 14 + P)) < 100 " +
        "and is_prime(tot)",
          
        # penultimate total is prime  
        "is_prime(tot - (A + B if A < 7 else B + C)) or " +
        "is_prime(tot - (P + Q if Q < 7 else O + P))",
        ],  
        macro=dict([("odds", "(A, B, C, D, E, F)")] +
                   [("evens", "(I, J, K, L, M, N, O, P, Q)")]),
        answer="@odds, (G, H), @evens",
        d2i=d2i,
        # a number may not be surrounded by the same number
        distinct=("AC","CE","EG","BD","DF","HJ","JL","LN","NP","IK","KM","MO","OQ"),
        base=8,       # domino values 0-6, 7 is used for n/a
        env=dict(stup=stup),
        reorder=0,
        verbose=0,    # use 256 to see the generated code
      ) 
      
      sols = set()
      # check possible solutions
      for ans in p.answers():
        # right now a mirrored version might also be possible
        mirror = tuple(x[::-1] for x in ans[::-1]) 
        for i, (f, m, l) in enumerate((ans, mirror)):
          # check if 2nd domino requirement is valid for the right-hand side
          if not is_prime(sum(m) + m[-1] + l[0]):
            f7 = (f + m)[:7] if f[0] < 7 else (f + m)[1:8]
            f6 = tuple(f"{x}-{y}" for x, y in tuples(f7))
            sols.add(f6)
            
      for f6 in sols:
        print("answer:", ', '.join(f6))
      

      Like

    • Frits's avatar

      Frits 1:15 pm on 20 February 2025 Permalink | Reply

      from itertools import product
      
      # primes below 100
      P = {3, 5, 7}
      P |= {2} | {x for x in range(11, 100, 2) if all(x % p for p in P)}
      
      # return sorted tuple
      stup = lambda x, y: (x, y) if x <= y else (y, x)
      
      # dominoes with even sum, except (0, 0)
      evens = [(i, j) for j in range(7) for i in range(0 if j else 1, j + 1) 
               if(i + j) % 2 == 0]
      o_evens = [(x, y) for x, y in evens if x % 2]      # with odd spots
      e_evens = [(x, y) for x, y in evens if x % 2 == 0] # with even spots         
      
      # add <k> dominoes from set <ds> to the line <ss>
      def solve(k, ds, ss):
        # are we done?
        if not k:
          yield ss
        else:
          # place next domino to the right
          for (x, y) in ds:
            if ss[-1][-1] == x:
              yield from solve(k - 1, ds.difference([stup(x, y)]), ss + [(x, y)])
            if x != y and ss[-1][-1] == y:
              yield from solve(k - 1, ds.difference([stup(y, x)]), ss + [(y, x)])
      
      # the total of all 15 even sum dominoes is 4*(1+3+5) + 5*(0+2+4+6) = 96
      t_even = 96  
      sols = set()
      
      # process all odd non-prime domino sums
      for odd_sum in range(1, 12, 2):
        if odd_sum in P: continue
        t = t_even + odd_sum
        # odd nums   oe   even nums  
        # ABCDEF     GH   IJKLMNOPQ       
        
        # we can't have all 9 dominoes with even spots otherwise both 2, 4 and 6 
        # would need to occur at least three times. Only with 2 of them this is
        # possible (using the H and the Q position) so position Q will be unused.
        missing_sums = [i for i in range(2, 13, 2) if (t - i) in P]
        
        for msum in missing_sums:
          # which even spots dominoes have sum <msum> ?
          
          # domino (x, x) in this case is not possible (x in {2, 4, 6}) as it forces
          # the two other numbers in {2, 4, 6} on positions H and P
          # so we can't make 3 dominoes with 0 anymore (position H or P is needed).
          for m in set(stup(d, msum - d) for d in range(0, 7, 2) 
                       if 0 <= msum - d <= 6 and 2 * d != msum):
            # domino <m> is not used           
            for G in (1, 3, 5):
               H = odd_sum - G
               if not (0 <= H <= 6): continue
               
               # add dominoes with even spots and an even sum
               es = list(solve(len(e_evens) - 1, set(e_evens).difference([m]), 
                               [(G, H)]))
               
               # add dominoes with odd spots and an even sum
               os = list(solve(len(o_evens), set(o_evens), [(H, G)]))
               
               # combine them into ...
               for e, o in product(es, os):
                 # ... a line of 15 dominoes
                 ds = [x[::-1] for x in o[1:][::-1]] + e
                 
                 t = sum(sum(x) for x in ds)
                 # check penultimate domino
                 if all((t - sum(ds[i])) not in P for i in [-1, 0]): continue
                 
                 # check whether 2nd domino (at right-hand) is not prime
                 if (odd_sum + sum(e[1])) not in P:
                   sols.add(tuple(ds[:6]))
                 if (odd_sum + sum(o[1])) not in P:
                   dsrev = [x[::-1] for x in ds[::-1]]
                   sols.add(tuple(dsrev[:6]))  
                   
      for sol in sols:
        print(f"answer: {', '.join(f'{x}-{y}' for x, y in sol)}")
      

      Like

  • Unknown's avatar

    Jim Randell 9:34 am on 14 February 2025 Permalink | Reply
    Tags:   

    Teaser 2473: [Valentines cards] 

    From The Sunday Times, 14th February 2010 [link]

    Ann, Beth, Cleo and Di each sent a card to a boy she would like to be her Valentine, one of Ed, Fin, Guy and Huw. Likewise, each of the boys sent a card to his choice of Valentine, one of the girls. Ann’s Valentine’s Valentine’s Valentine was Ed; Beth’s Valentine’s Valentine’s Valentine’s Valentine was Cleo, and Fin’s Valentine’s Valentine’s Valentine’s Valentine’s Valentine’s Valentine was Guy. Only Cleo, Ed and Huw received at least as many cards as their Valentines did.

    Who (in order) are Ann’s, Beth’s, Cleo’s and Di’s Valentines?

    This puzzle was originally published with no title.

    [teaser2473]

     
    • Jim Randell's avatar

      Jim Randell 9:34 am on 14 February 2025 Permalink | Reply

      This Python program considers all possible recipients of the cards, and then eliminates candidates where the given conditions are not met. (A simple performance improvement would be to remove recipient candidates that do not include people we know received a card (i.e. C, E, G)).

      It runs in 166ms. (Internal runtime is 85ms).

      from enigma import (subsets, cproduct, multiset, map2str, printf)
      
      # multiple lookups on dict <d>
      def nget(d, k, n):
        while n > 0:
          k = d[k]
          n -= 1
        return k
      
      # labels for the girls and boys
      (gs, bs) = (tuple("ABCD"), tuple("EFGH"))
      
      # choose targets for the girls and boys
      for (tgs, tbs) in cproduct(subsets(xs, size=len, select='M') for xs in (bs, gs)):
        # combine them into a single map
        v = dict(zip(gs, tgs))
        v.update(zip(bs, tbs))
      
        # check the conditions:
        # "A's Valentine's Valentine's Valentine was E"
        if not (nget(v, 'A', 3) == 'E'): continue
      
        # "B's Valentine's Valentine's Valentine's Valentine was C"
        if not (nget(v, 'B', 4) == 'C'): continue
      
        # "F's Valentine's Valentine's Valentine's Valentine's Valentine's Valentine was G"
        if not (nget(v, 'F', 6) == 'G'): continue
      
        # count the targets of the cards
        m = multiset.from_seq(tgs, tbs)
      
        # only C, E, H received at least as many cards as their Valentines did
        if not all((m.count(x) >= m.count(v[x])) == (x in "CEH") for x in gs + bs): continue
      
        # output solution
        printf("{v}", v=map2str(v, arr="->", sep=" ", enc=""))
      

      Solution: Ann, Beth, Cleo, Di sent their cards to Huw, Ed, Guy, Ed.

      The full list is:

      A → H
      B → E
      C → G
      D → E
      E → C
      F → B or D
      G → C
      H → D or B

      Like

  • Unknown's avatar

    Jim Randell 9:21 am on 11 February 2025 Permalink | Reply
    Tags: by: Roger Webster   

    Teaser 2562: Chez nous 

    From The Sunday Times, 30th October 2011 [link] [link]

    A sign-writer stocks signs of house numbers written in words, from “one” up to my own house number. The signs are kept in boxes according to the number of letters used. (e.g., all copies of “seven” and “fifty” are in the same box). Each box contains at least two different signs. Boxes are labelled showing the number of letters used, (e.g. the box holding “fifty” is labelled “five”).

    Naturally, the sign-writer has used signs from his own stock for the labels. To label all the boxes he needed to take signs from half of the boxes.

    What is my house number?

    [teaser2562]

     
    • Jim Randell's avatar

      Jim Randell 9:21 am on 11 February 2025 Permalink | Reply

      I used the [[ int2words() ]] routine from the enigma.py library to turn the numbers into English text.

      The following Python program runs in 61ms. (Internal runtime is 406µs).

      from enigma import (defaultdict, irange, inf, int2words, cache, printf)
      
      # integer -> number of letters
      i2k = cache(lambda n: sum(x.isalpha() for x in int2words(n)))
      
      # group numbers by letter count
      g = defaultdict(list)
      
      # consider increasing numbers
      for n in irange(1, inf):
        # translate the number to words, and count the letters
        k = i2k(n)
        g[k].append(n)
        if n < 50: continue
      
        # each box contains at least two different signs
        if any(len(vs) < 2 for vs in g.values()): continue
      
        # to label the boxes requires signs from exactly half the boxes
        ks = list(g.keys())
        ss = set(i2k(n) for n in ks)
        if not (2 * len(ss) == len(ks) and ss.issubset(ks)): continue
      
        # output solution
        printf("n = {n}; {x} boxes", x=len(ks))
        for k in sorted(ks):
          printf("{k} -> {vs}", vs=g[k])
        printf("labels from {ss}; {x} boxes", ss=sorted(ss), x=len(ss))
        printf()
        break
      

      Solution: Your house number is 112.

      The 14 boxes are labelled (with the length of the label indicated in parentheses):

      three (5)
      four (4)
      five (4)
      six (3)
      seven (5)
      eight (5)
      nine (4)
      ten (3)
      eleven (6)
      twelve (6)
      sixteen (7)
      seventeen (9)
      eighteen (8)
      nineteen (8)

      And to label these boxes you need to use labels from the following 7 boxes:

      3, 4, 5, 6, 7, 8, 9

      Like

  • Unknown's avatar

    Jim Randell 7:47 am on 9 February 2025 Permalink | Reply
    Tags:   

    Teaser 3255: A square for all digits 

    From The Sunday Times, 9th February 2025 [link] [link]

    I have written down a set of whole numbers, each greater than 1 and less than 100. I have squared each number and written the answers down in a list. The list contains all of the digits from 0 to 9 precisely once. Interestingly, for any two of the numbers, there is no common factor other than 1.

    Naturally the sum of the digits in the squared set is 45, but what is the sum of the digits in the original set of numbers that I wrote down?

    [teaser3255]

     
    • Jim Randell's avatar

      Jim Randell 7:59 am on 9 February 2025 Permalink | Reply

      This Python program runs in 68ms. (Internal runtime is 1.2ms).

      from enigma import (irange, gcd, dsum, seq2str, printf)
      
      # digits (as strings)
      digits = set("0123456789")
      
      # allocate squares from <d> for the remaining digits in <ds>
      # return (<ss> = squares, <rs> = roots)
      def solve(d, ds, ss, rs):
        # are we done?
        if not ds:
          yield (ss, rs)
        else:
          # choose the next square
          r_ = rs[-1]
          for (s, r) in d.items():
            # and check roots are pairwise coprime
            if r < r_ and ds.issuperset(s) and all(gcd(x, r) == 1 for x in rs):
              # solve for the remaining digits
              yield from solve(d, ds.difference(s), ss + [s], rs + [r])
      
      # record squares; map root -> square (as string)
      d = dict()
      
      # consider the largest square in the set
      for i in irange(2, 99):
        s = str(i * i)
        if len(set(s)) != len(s): continue
      
        # find other squares to use the remaining digits
        for (ss, rs) in solve(d, digits.difference(s), [s], [i]):
          # calculate total digit sum of the roots
          t = sum(dsum(r) for r in rs)
          # output solution
          printf("squares = {ss}, roots = {rs} -> t = {t}", ss=seq2str(ss))
      
        # record this square
        d[s] = i
      

      Solution: The sum of the digits in the original set of numbers is: 24.

      There are two possible original sets (with the same total digit sum):

      13, 28, 55 (digit sum = 24)
      squares → 169, 784, 3025

      28, 31, 55 (digit sum = 24)
      squares → 784, 961, 3025

      Like

      • Jim Randell's avatar

        Jim Randell 1:31 pm on 9 February 2025 Permalink | Reply

        We can treat finding the sets of squares that use each of the digits 0-9 exactly once as an exact cover problem (which we have encountered before, see Enigma 1712). And the [[ mcover() ]] routine allows us to check viable sets for those that are pairwise coprime as we go along.

        This gives a shorter program, which runs in 1.1ms.

        from enigma import (multiset, irange, is_coprime, dsum, printf)
        
        # possible squares; map root -> digit content (multiset of characters)
        d = dict((r, multiset.from_seq(str(r * r))) for r in irange(2, 99))
        
        # target is exactly one occurrence of each digit 0-9
        tgt = multiset.from_seq("0123456789")
        
        # find exact covers, with pairwise coprime roots
        for rs in tgt.mcover(d, reject=(lambda rs: not is_coprime(*rs))):
          # calculate total digit sum of roots
          t = sum(dsum(r) for r in rs)
          # output solution
          ss = list(r * r for r in rs)
          printf("squares = {ss}, roots = {rs} -> t = {t}", rs=sorted(rs), ss=sorted(ss))
        

        Liked by 1 person

    • Frits's avatar

      Frits 8:29 am on 9 February 2025 Permalink | Reply

      from itertools import combinations
      from math import gcd
      
      # squares with different digits
      sqs = [n2 for n in range(2, 100) if len(n2 := str(n * n)) == len(set(n2))]
      
      # dictionary of squares per digit
      d = {c: [s for s in sqs if c in s] for c in "0123456789"}
      
      # sort dictionary on frequency (optional)
      d = dict(sorted(d.items(), key=lambda x: len(x[1])))
      
      # get a list of squares that use digits 0-9 exactly once
      def solve(d, dgts="", ss=[]):
        if (ln_dgts := len(dgts)) == 10:
          yield ss
        else:
          # squares for next unused digit
          for sq in next(iter(d.values())):
            # refresh dictionary and remove empty entries
            d_ = {k: [v for v in vs if all(x not in v for x in sq)] 
                      for k, vs in d.items()}
            d_ = {k: vs for k, vs in d_.items() if vs}
            # digits not yet used must still have candidate squares
            if len(d_) + ln_dgts + len(sq) != 10: continue
            yield from solve(d_, dgts + sq, ss + [int(sq)])
       
      sols = set()    
      for ss in solve(d):
        # for any two of the numbers, there is no common factor other than 1
        if any(gcd(x, y) > 1 for x, y in combinations(ss, 2)): continue
        orig = [int(x**.5) for x in ss]
        sols.add(str(sum([int(x) for o in orig for x in str(o)])))
        
      print(f"answer(s): {' or '.join(sols)}")
      

      Like

  • Unknown's avatar

    Jim Randell 8:16 am on 7 February 2025 Permalink | Reply
    Tags:   

    Teaser 2575: More teams 

    From The Sunday Times, 29th January 2012 [link] [link]

    In our local pub soccer league each team plays each of the others at home and away during the season. Two seasons ago we had a two-digit number of teams. The number increased the next year, and again this year, but this time the increase was one less than the previous season. This season the number of games played will be over three times the number played two seasons ago. Also, this season’s increase in games played actually equals the number played two seasons ago.

    How many teams are there now in the league?

    [teaser2575]

     
    • Jim Randell's avatar

      Jim Randell 8:16 am on 7 February 2025 Permalink | Reply

      The number of teams goes:

      n → n + (k + 1) → n + (2k + 1)

      where n is a 2-digit number.

      So the current season has an odd number of teams (≥ 3) more than the number of teams two seasons ago.

      Each pair of teams plays twice, so for n teams the number of matches in a season is:

      M(n) = 2 C(n, 2) = n (n − 1)

      and if the numbers of matches are A → B → C we have:

      C > 3A
      C − B = A; or: A + B = C

      This Python program looks for the first possible solution.

      It runs in 66ms. (Internal runtime is just 38µs).

      from enigma import (irange, inf, printf)
      
      # number of matches for n teams = 2 * C(n, 2)
      M = lambda n: n * (n - 1)
      
      def solve():
        # consider a league with c teams (current season)
        for c in irange(13, inf):
          C = M(c)
          # consider an odd number of teams less than this (two seasons ago)
          for a in irange(c - 3, 10, step=-2):
            A = M(a)
            if 3 * A >= C or a > 99: continue
            b = (a + c + 1) // 2
            B = M(b)
            if A + B == C:
              yield ((a, b, c), (A, B, C))
      
      # find the first solution
      for ((a, b, c), (A, B, C)) in solve():
        printf("{a} teams, {A} matches -> {b} teams, {B} matches -> {c} teams, {C} matches")
        break
      

      Solution: There are now 17 teams in the league.

      And so there are 272 matches in the current season.

      Two years ago there were 10 teams in the league, and the season consisted of 90 matches. (The current season has more than 3× this number of matches).

      One year ago there were 14 teams in the league, and the season consisted of 182 matches, and 182 + 90 = 272.

      Like

      • Frits's avatar

        Frits 4:07 pm on 7 February 2025 Permalink | Reply

        Further analysis (solving a quadratic equation) leads to :

        c = (5a + 1) / 3

        and C greater than 3 * A means a^2 -11a + 1 less than 0.
        a =10 is still valid but already at a = 11 the condition is not met.

        Like

        • John Crabtree's avatar

          John Crabtree 4:58 am on 14 February 2025 Permalink | Reply

          Frits, nicely done. It is useful to know the intermediate result, ie n = 3k – 2

          Like

  • Unknown's avatar

    Jim Randell 9:15 am on 4 February 2025 Permalink | Reply
    Tags:   

    Teaser 2571: Dog show 

    From The Sunday Times, 1st January 2012 [link] [link]

    Five dogs were entered by owners Alan, Brian, Colin, David and Edward, whose surnames are Allen, Bryan, Collins, Davis and Edwards. No owner had the same first and second initials, no two had the same pair of initials and none shared an initial with their breed.

    Colin was in position 1, David Allen’s dog was in position 4, Brian’s corgi was not next to the collie; the chow and doberman were at opposite ends. The other breed was a dachshund.

    In the voting, dogs eliminated in order were Mr Bryan’s, the corgi, David’s and the doberman.

    Who won, and what breed was their dog?

    [teaser2571]

     
    • Jim Randell's avatar

      Jim Randell 9:17 am on 4 February 2025 Permalink | Reply

      Initially, I found the wording of this puzzle a bit confusing.

      I think the situation is that the five competitors are lined up (I assigned them numbers 1 – 5 in the line), they are then inspected (in order) by the judges, who then deliberate, eliminating one competitor at a time until there is only the winner remaining. Although we are given the order of the elimination, this has nothing to do with the order of the line-up, but serves to tell us that the descriptions of the four eliminations identify four different competitors.

      I used the [[ SubstitutedExpression() ]] solver from the enigma.py library to check most of the required constraints. (I found it was easier to check the “no competitors shared the same pair of initials” condition after the slots in the line-up had been filled out).

      The following Python program runs in 73ms. (Internal runtime is 4.9ms).

      from enigma import (SubstitutedExpression, irange, seq_all_different, ordered, printf)
      
      # we allocate the competitor numbers: 1 - 5 to:
      #
      #  owner:   A B C D E  [A = Alan; B = Brian; C = Colin; D = David; E = Edward]
      #  surname: F G H I J  [F = Allen; G = Bryan; H = Collins; I = Davis; J = Edwards]
      #  breed:   K L M N P  [K = corgi; L = collie; M = chow; N = doberman; P = dachshund]
      
      # the distinct groups:
      distinct = [
        # each group is a permutation of the digits 1-5
        'ABCDE', 'FGHIJ', 'KLMNP',
        # no owner has the same first/last initial
        # no breed shares an initial with either of its owners names
        'AF', 'BG', 'CHKLM', 'DINP', 'EJ',
        # elimination order is: Bryan (G), corgi (K), David (D), doberman (N)
        # so these must all be in different positions
        'GKDN',
      ]
      
      # assignments we are given:
      #   Colin (C) is competitor 1
      #   David Allen (D F) is competitor 4
      s2d = dict(C=1, D=4, F=4)
      
      # invalid assignments we are given:
      #   the chow (M) and the doberman (N) were at opposite ends (i.e. 1 and 5)
      d2i = dict.fromkeys([2, 3, 4], "MN")
      
      # additional constraints
      exprs = [
        # Brian (B)'s corgi (K) was not next to the collie (L)
        "B = K",
        "abs(K - L) > 1",
      ]
      
      # construct the puzzle
      p = SubstitutedExpression(
        exprs,
        base=6, digits=irange(1, 5),
        distinct=distinct, s2d=s2d, d2i=d2i,
        answer="((A, B, C, D, E), (F, G, H, I, J), (K, L, M, N, P))",
      )
      
      # labels for owners / surnames / breeds
      owners = "Alan Brian Colin David Edward".split()
      surnames = "Allen Bryan Collins Davis Edwards".split()
      dogs = "corgi collie chow doberman dachshund".split()
      
      # solve the puzzle and fill out the slots in the line-up
      for ss in p.answers(verbose=0):
        slots = dict((k, [None] * 3) for k in irange(1, 5))
        for (i, (s, labels)) in enumerate(zip(ss, [owners, surnames, dogs])):
          for (k, v) in zip(s, labels):
            slots[k][i] = v
        # check: no two owners share the same pair of initials
        if not seq_all_different(ordered(fn[0], sn[0]) for (fn, sn, br) in slots.values()): continue
      
        # output the competitors, and identify the winner
        for k in sorted(slots.keys()):
          (fn, sn, br) = slots[k]
          eliminated = (sn == 'Bryan' or br == 'corgi' or fn == 'David' or br == 'doberman')
          printf("({k}) {fn} {sn}, {br}{w}", w=('' if eliminated else ' [WINNER]'))
        printf()
      

      Solution: The winner was the dachshund, owned by Alan Collins.

      The line-up of the competitors was:

      (1) Colin Edwards, doberman
      (2) Brian Davis, corgi
      (3) Alan Collins, dachshund [WINNER]
      (4) David Allen, collie
      (5) Edward Bryan, chow

      Like

  • Unknown's avatar

    Jim Randell 7:02 am on 2 February 2025 Permalink | Reply
    Tags:   

    Teaser 3254: Pizza pans 

    From The Sunday Times, 2nd February 2025 [link] [link]

    In a large pan, James baked three identical circular pizzas whose radius was a whole number of cm (less than 75). He laid them on a platter so that one pizza overlapped the other two. The pizza centres formed a right-angled triangle, with sides that were whole numbers of cm. The two lengths of overlap and the gap between the two non-overlapping pizzas (all measured along the lines joining the pizza centres) were all whole numbers of cm and could have formed another right-angled triangle.

    He baked a fourth, smaller, circular pizza and it just fitted inside the triangle formed by the centres of the other three. Even if you knew the radii of the pizzas, you couldn’t work out the size of those right-angled triangles.

    What was the radius of the smallest pizza?

    [teaser3254]

     
    • Jim Randell's avatar

      Jim Randell 7:22 am on 2 February 2025 Permalink | Reply

      The following Python program runs in 69ms. (Internal runtime is 571µs).

      from enigma import (defaultdict, irange, pythagorean_triples, divc, rdiv, printf)
      
      # generate possible dimensions
      def generate():
        # consider pythagorean triples (x, y, z)
        for (x, y, z) in pythagorean_triples(225):
      
          # consider possible radii for the pizza (R)
          for R in irange(divc(y + 1, 2), 74):
            RR = 2 * R
            (X, Y, Z) = (RR - y, RR - x, RR + z)
            if not (X * X + Y * Y == Z * Z): continue
      
            # calculate inradius of X, Y, Z (r = area / semiperimeter)
            r = rdiv(X * Y, X + Y + Z)
      
            # return (<radii>, <large triangle>, <small triangle>)
            yield((R, r), (X, Y, Z), (x, y, z))
      
      # collect triangles by radii (R, r)
      d = defaultdict(list)
      for ((R, r), Ts, ts) in generate():
        printf("[R={R} {Ts} -> {ts} r={r}]")
        d[(R, r)].append((Ts, ts))
      
      # look for non-unique radii pairs
      for ((R, r), vs) in d.items():
        if len(vs) < 2: continue
        # output solution
        printf("({R}, {r}) -> {vs}")
      

      Solution: The smaller pizza has a radius of 30 cm.

      And the larger pizzas have radii of 60cm. (So each pizza is 120cm across! – that’s a big pizza).

      One of the possible arrangements looks like this:

      The overlaps are 10 cm and 24 cm, and the gap is 26 cm.

      And the other possible arrangement looks quite similar, but the overlaps are 15 cm and 20 cm, and the gap is 25 cm.

      Like

    • Frits's avatar

      Frits 6:36 am on 3 February 2025 Permalink | Reply

      from enigma import pythagorean_triples
      
      # let x, y, z be the lengths of the triangle of the overlaps and gap
      # let R be the radius of the 3 identical pizzas
      # RR = 2 * R
      # (X, Y, Z) = (RR - y, RR - x, RR + z)
      # right-angled: 4 * R**2 = 4 * (y * R + x * R + z * R)
      #               so R must be equal to y + x + z  
      
      seen = set()
      # consider pythagorean triples (X, Y, Z)
      for (X, Y, Z) in pythagorean_triples(4 * 75 - 1):
        # small and big radius
        # (x, y, z) = sorted([R2 - X, R2 - Y, Z - R2])
        # x + y + z = 2R - X - Y + Z = 2R - 2r = R --> R = 2r
        R = (X + Y - Z)  # r = R / 2
        
        if (R2 := 2 * R) > 2 * 75: continue
        
        # x, y, z must be positive numbers
        if min(R2 - X, R2 - Y, Z - R2) < 1: continue
        
        if R in seen:
          print(f"answer: {r} cm")
        seen.add(R)  
      

      Like

  • Unknown's avatar

    Jim Randell 10:39 am on 31 January 2025 Permalink | Reply
    Tags: ,   

    Teaser 2581: Resplendency 

    From The Sunday Times, 11th March 2012 [link] [link]

    Our local Hibernian band will lead the St Patrick’s Day parade next Saturday, resplendent in their new tunics and finery. The total cost of this was one thousand pounds and it was generously paid for by the parish council. Each tunic cost the same two-figure number of pounds (and that number happens to be double the number of girls in the band). In addition, each girl in the band has some extra finery, and the cost of this for each girl was the same as the cost of the tunic but with the two digits in reverse order.

    How many boys and how many girls are there in the band?

    [teaser2581]

     
    • Jim Randell's avatar

      Jim Randell 10:40 am on 31 January 2025 Permalink | Reply

      If:

      b = number of boys
      g = number of girls
      t = cost of tunic (pounds)
      f = cost of finery (pounds) = reverse of t

      we have:

      t = 2 × g
      t × b + (t + f) × g = 1000

      Hence b, g, f can all be expressed in terms of t, which is a 2-digit even number.

      This Python program runs in 66ms. (Internal runtime is 57µs).

      from enigma import (irange, cproduct, div, printf)
      
      # the cost of each tunic is XY, a 2-digit even number
      # the cost of each finery is YX, a 2 digit number
      for (X, Y) in cproduct([irange(1, 9), [2, 4, 6, 8]]):
        (t, f) = (10 * X + Y, 10 * Y + X)
      
        # calculate the number of girls and boys
        g = t // 2
        b = div(1000 - (t + f) * g, t)
        if b is None or b < 0: continue
      
        # output solution
        printf("b={b} g={g}; t={t} f={f}")
      

      Solution: There are 24 boys and 8 girls.

      The tunics cost £ 16, and the finery costs £ 61.

      £16 × 24 + £(16 + 61) × 8 = £1000

      Like

      • Ruud's avatar

        Ruud 3:58 pm on 2 February 2025 Permalink | Reply

        Very straightforward:

        import peek
        import istr
        
        for n_girls in istr(range(5, 50)):
            tunic_cost = 2 * n_girls
            finery_cost = tunic_cost.reversed()
            girls_total = n_girls * (tunic_cost + finery_cost)
            boys_total = 1000 - girls_total
            if finery_cost[0] != 0 and boys_total.is_divisible_by(tunic_cost):
                n_boys = boys_total / tunic_cost
                peek(girls_total, boys_total, n_girls, n_boys, tunic_cost, finery_cost)
        

        Like

    • GeoffR's avatar

      GeoffR 9:03 am on 1 February 2025 Permalink | Reply

      # the cost of each tunic is XY, a 2-digit even number
      # the cost of each finery is YX, a 2 digit number
      
      for X in range(1, 9):
        for Y in range(2, 10, 2):
          XY, YX = 10*X + Y, 10*Y + X
          for girls in range(1, 100):
            # cost of a tunic is double the number of girls
            if XY != 2 * girls:continue
            for boys in range(1, 100):
              tunics = (boys + girls) * XY
              finery = girls * YX
              # total cost = £1,000
              if tunics + finery == 1000:
                print(f"Band = {boys} boys and {girls} girls.")
      
      # Band = 24 boys and 8 girls.
      
      
      

      Like

  • Unknown's avatar

    Jim Randell 10:41 am on 28 January 2025 Permalink | Reply
    Tags:   

    Teaser 2582: Anyone for tennis? 

    From The Sunday Times, 18th March 2012 [link] [link]

    The girls of St Trinian’s choose two games from the four on offer. In Felicity’s gang of four, no game was chosen by both Daphne and Erica; Chloe and Miss Brown chose lacrosse; Miss Smith chose netball; and only Miss Jones excluded hockey. In Harriet’s gang of four, Clara, Debbie and Ellen all chose netball but only one of the four chose hockey. To avoid having two girls with the same first name initial in any one game, one of the girls in the second group (but not Debbie) moved from one game to another. This meant that there was an odd number of these [eight] girls playing each game.

    Which of the eight girls played tennis?

    [teaser2582]

     
    • Jim Randell's avatar

      Jim Randell 10:42 am on 28 January 2025 Permalink | Reply

      We want each of the sports to end up with an odd number of participants after the swap.

      So before the swap there must be two sports that have an even number of participants (and the other two sports have an odd number of participants). And the swap must occur from one of the sports with an even number of participants to the other sport with an even number of participants. Leaving all sports with an odd number.

      Furthermore, the girl who swaps (who is in the second group, and shares and initial with a member of the first group, and is not Debbie – so must be Clara or Ellen), must have chosen one of the even sports (that was also chosen by her counterpart with the same initial), and not chosen the other even sport (that was also not chosen by her counterpart with the same initial, otherwise the swap would not remedy the situation). And this must be the only situation where a sport has been chosen by two girls with the same initial.

      This Python program uses the [[ SubstitutedExpression() ]] solver from the enigma.py library to determine possible choices for each of the groups separately, and then combines possibilities to consider possible choices for the 8 girls, and then checks the remaining conditions for the combined choices.

      It runs in 82ms. (Internal runtime is 11.5ms).

      from enigma import (
        SubstitutedExpression, cproduct, chain, filter2, multiset,
        singleton, diff, join, printf
      )
      
      # possible choices are:
      #
      #   k   L N H T
      #   0 = - - x x (HT)
      #   5 = x x - - (LN)
      #   1 = - x - x (NT)
      #   4 = x - x - (LH)
      #   2 = - x x - (NH)
      #   3 = x - - x (LT)
      #
      # such that for a choice k the unchosen options are (5 - k)
      
      # map choice numbers to sports (0 - 5)
      choice = ['HT', 'NT', 'NH', 'LT', 'LH', 'LN']
      
      # sets for the four sports
      macros = {
        'lacrosse': "{3, 4, 5}",
        'netball': "{1, 2, 5}",
        'hockey': "{0, 2, 4}",
        'tennis': "{0, 1, 3}",
      }
      
      # group 1:
      # for sport choices allocate: C, D, E, F to k-values (0-5)
      # for surnames allocate: B, S, J to values 0-3 (= C - F) [distinct]
      exprs1 = [
        # "no game was chosen by both D and E"
        "D + E == 5",
        # "C chose lacrosse"
        "C in @lacrosse",
        # "Miss Brown (who is not C) also chose lacrosse"
        "B != 0",
        "[C, D, E, F][B] in @lacrosse",
        # "Miss Smith chose netball"
        "[C, D, E, F][S] in @netball",
        # only Miss Jones excluded hockey
        "all((x in @hockey) == (i != J) for (i, x) in enumerate([C, D, E, F]))",
      ]
      
      names1 = "Chloe Daphne Erica Felicity".split()
      surnames1 = "Brown Smith Jones".split()
      
      def solve1():
        # find the first group
        p1 = SubstitutedExpression(
          exprs1, base=6, macro=macros,
          distinct="BSJ", d2i={ 4: "BSJ", 5: "BSJ" },
        )
        for s in p1.solve(verbose=0):
          # map names to choices
          d1 = dict((x, choice[s[x[0]]]) for x in names1)
          # map names to surnames
          sn = dict((names1[s[x[0]]], x) for x in surnames1)
          yield (d1, sn)
      
      
      # group 2:
      # for sport choices allocate: C, D, E, H to k-values (0-5)
      exprs2 = [
        # "C, D, E all chose netball"
        "{C, D, E}.issubset(@netball)",
        # "only one of C, D, E, H chose hockey"
        "sum(x in @hockey for x in [C, D, E, H]) == 1",
      ]
      
      names2 = "Clara Debbie Ellen Harriet".split()
      
      def solve2():
        # find the second group
        p2 = SubstitutedExpression(exprs2, base=6, macro=macros, distinct="")
        for s in p2.solve(verbose=0):
          # map names to choices
          d2 = dict((x, choice[s[x[0]]]) for x in names2)
          yield d2
      
      
      # output a group
      def output(g, sn=None):
        for k in sorted(g.keys()):
          s = (sn.get(k) if sn else None)
          name = (k + " " + s if s else k)
          printf("{name} -> {v}", v=join(g[k], sep=' + '))
        printf()
      
      # choose solutions for each group
      for ((g1, sn), g2) in cproduct([solve1(), solve2()]):
        # make the combined solution
        g = dict(chain(g1.items(), g2.items()))
        # map each sport to a list of girls
        s = dict((k, list()) for k in 'LNHT')
        for (k, vs) in g.items():
          for v in vs:
            s[v].append(k)
      
        # find sports with even and odd numbers of participants
        (even, odd) = filter2((lambda k: len(s[k]) % 2 == 0), s.keys())
        # there must be 2 even sports
        if len(even) != 2: continue
      
        # find sports that have more than one participant with a given initial
        ms = list()
        for (k, vs) in s.items():
          ds = list(x for (x, n) in multiset.from_seq(v[0] for v in vs).items() if n > 1)
          if ds: ms.append((k, ds))
        # there must be just one sport with just one duplicated initial
        if not (len(ms) == 1 and len(ms[0][1]) == 1): continue
        (x, i) = (ms[0][0], ms[0][1][0])
        # the duplicate sport must have an even number of participants
        # and we must swap to the other sport with an even number of participants
        y = singleton(diff(even, {x}))
        if y is None: continue
        # the duplicate initial must be C or E
        swap = other = None
        if i == 'C':
          # Clara needs to swap from x to y
          (swap, other) = ('Clara', 'Chloe')
        elif i == 'E':
          # Ellen needs to swap from x to y
          (swap, other) = ('Ellen', 'Erica')
        # check the swap fixes the issue
        if swap is None or not (x in g[swap] and y not in g[swap] and y not in g[other]): continue
      
        # final list for tennis
        ts = list(s['T'])
        if x == 'T': ts.remove(swap)
        if y == 'T': ts.append(swap)
      
        # output solution:
        # first output the choices for each group
        output(g1, sn)
        output(g2)
        # output the swap, and the final list for tennis
        printf("{swap} swaps {x} -> {y}")
        printf("-> tennis = {ts}", ts=join(ts, sep=", ", sort=1))
        printf()
      

      Solution: The girls playing tennis are: Clara, Debbie, Erica.

      The initial choices made are:

      Chloe = lacrosse + hockey
      Daphne Brown = lacrosse + hockey
      Erica Jones = netball + tennis
      Felicity Smith = netball + hockey

      Clara = netball + tennis
      Debbie = netball + tennis
      Ellen = lacrosse + netball
      Harriet = netball + hockey

      But both Erica and Ellen have chosen netball, so there would be 2 girls with the same initial in the netball group.

      To fix this Ellen swaps from netball to hockey, giving the following assignments:

      lacrosse = Chloe, Daphne, Ellen (CDE, 3)
      netball = Clara, Debbie, Erica, Felicity, Harriet (CDEFH, 5)
      hockey = Chloe, Daphne, Ellen, Felicity, Harriet (CDEFH, 5)
      tennis = Clara, Debbie, Erica (CDE, 3)

      Like

  • Unknown's avatar

    Jim Randell 1:45 am on 26 January 2025 Permalink | Reply
    Tags:   

    Teaser 3253: Romanesque Numerals 

    From The Sunday Times, 26th January 2025 [link] [link]

    Equations in an archaic number system have been discovered:

    BBC + BC = CBA
    ABA#AB + ABAACA = ССВ
    АСААС × ААС = ACAABABA

    One letter (shown as #) is unclear, and could be either A or B. Letters basically represented different positive whole numbers; they might appear in any order in a string, but could contribute positively or negatively. The letter that was rightmost counted as a plus, but otherwise its basic value was compared with that of its right neighbour: if larger, it would count as plus; if smaller, as minus; if the same, then the same sign as that neighbour. (This works for interpreting Roman numerals too, e.g., VII = 5 + 1 + 1 = 7, and XLIX = −10 + 50 − 1 + 10 = 49).

    What are the basic values of A, B & C, in that order?

    [teaser3253]

     
    • Jim Randell's avatar

      Jim Randell 2:17 am on 26 January 2025 Permalink | Reply

      This Python program runs in 73ms. (Internal runtime is 4.9ms).

      from enigma import (irange, inf, decompose, tuples, rev, printf)
      
      # evaluate string <s> for symbol assignments <d>
      def val(s, d):
        # process the values in reverse order
        vs = rev(d[x] for x in s)
        # start with the rightmost
        rs = vs[0:1]
        # and process the rest in pairs
        for (x, y) in tuples(vs, 2):
          if y > x:
            rs.append(y)
          elif y < x:
            rs.append(-y)
          else:
            rs.append(rs[-1])
        return sum(rs)
      
      # evaluate strings <ss> for symbol assignments <d>
      def vals(ss, d): return list(val(s, d) for s in ss)
      
      # consider increasing totals for A + B + C
      for (A, B, C) in decompose(irange(6, inf), 3, increasing=0, sep=1, min_v=1):
        d = dict(A=A, B=B, C=C)
      
        # evaluate the given expressions:
        # [1] BBC + BC = CBA
        (a1, a2, a3) = vals(["BBC", "BC", "CBA"], d)
        if not (a1 + a2 == a3): continue
      
        # [2] (ABAAAB | ABABAB) + ABAACA = CCB"
        (b1a, b1b, b2, b3) = vals(["ABAAAB", "ABABAB", "ABAACA", "CCB"], d)
        if not (b1a + b2 == b3 or b1b + b2 == b3): continue
      
        # [3] ACAAC * AAC = ACAABABA
        (c1, c2, c3) = vals(["ACAAC", "AAC", "ACAABABA"], d)
        if not (c1 * c2 == c3): continue
      
        # output solution
        printf("A={A} B={B} C={C}")
        break  # we only need the first solution
      

      Solution: A=4; B=3; C=10.

      We have:

      BBC → [−3, −3, +10] = 4
      BC → [−3, +10] = 7
      CBA → [+10, −3, +4] = 11
      BBC + BC = CBA → 4 + 7 = 11

      ABAAAB → [+4, −3, +4, +4, +4, +3] = 16
      ABAACA → [+4, −3, −4, −4, +10, +4] = 7
      CCB → [+10, +10, +3] = 23
      ABAAAB + ABAACA = CCB → 16 + 7 = 23

      ACAAC → [−4, +10, −4, −4, +10] = 8
      AAC → [−4, −4, +10] = 2
      ACAABABA → [−4, +10, +4, +4, −3, +4, −3, +4] = 16
      ACAAC × AAC = ACAABABA → 8 × 2 = 16

      Note that these are not necessarily the simplest way to write these numbers. For example in the first sum 4 is denoted by BBC, when it would be simpler to use A.

      More compact forms of the sums are:

      4 + 7 = 11 → A + AB = AAB
      16 + 7 = 23 → CBB + BC = CCB
      8 × 2 = 16 → AA × AAC = ACC

      Like

      • Jim Randell's avatar

        Jim Randell 1:29 pm on 3 February 2025 Permalink | Reply

        If we relax the condition on the values being positive integers, we can find further solutions using (non-zero) rational numbers.

        For example:

        C = −2/9; A = 4/9; B = 5/9

        gives:

        BBC + BC = CBA → 8/9 + 1/3 = 11/9
        ABAAAB + ABAACA = CCB → −2/3 + 5/3 = 1
        ACAAC × AAC = ACAABABA → 4/3 × 2/3 = 8/9

        Other rational values that work are:

        A = -23/435; B = 161/870; C = 299/435
        A = -38/1225; B = 228/1225; C = 874/1225

        Like

    • Ruud's avatar

      Ruud 12:21 pm on 26 January 2025 Permalink | Reply

      import sys
      import itertools
      
      expressions = """\
      BBC + BC == CBA
      ABAXAB + ABAACA == CCB
      ACAAC * AAC == ACAABABA
      """.splitlines()
      
      
      def calc(s):
          l = [-1] + [globals()[ch] for ch in reversed(s)]
          result = 0
          for value, value_right in zip(l[1:], l):
              if value_right > value:
                  sign = -1
              if value_right < value:
                  sign = 1
              result += sign * value
          return str(result)
      
      
      def eval_expression(expression):
          return eval("".join(part if set(part) - set("ABCX") else calc(part) for part in expression.split()))
      
      
      for n in itertools.count(4):
          for A, B, C in itertools.permutations(range(20), 3):
              for X in (A, B, C):
                  if all(eval_expression(expression) for expression in expressions):
                      print(f"{A=} {B=} {C=}")
                      sys.exit()
      
      

      Like

      • Ruud's avatar

        Ruud 6:42 pm on 27 January 2025 Permalink | Reply

        I see now that # (X in my code) can only be A or B.
        So the second for loop should read
        for X in (A, B)
        It doesn’t change the solution, by the way.

        Like

    • Frits's avatar

      Frits 8:37 am on 3 February 2025 Permalink | Reply

      From the first expression we can deduce that B may not be greater than A and C.
      From the last expression we can deduce that A and C are both even.

      Like

  • Unknown's avatar

    Jim Randell 7:58 am on 24 January 2025 Permalink | Reply
    Tags:   

    Teaser 2583: Playing cards 

    From The Sunday Times, 25th March 2012 [link] [link]

    Oliver wrote down some numbers and then used a letter-for-digit code to make them into words. In this way his numbers became JACK, QUEEN, KING and ACE. In fact JACK and KING were perfect squares, QUEEN was odd, and ACE was a prime. Furthermore, appropriately enough, the sum of the four was divisible by 52.

    What number was represented by QUEEN?

    [teaser2583]

     
    • Jim Randell's avatar

      Jim Randell 7:59 am on 24 January 2025 Permalink | Reply

      Here is a solution using the [[ SubstitutedExpression ]] solver from the enigma.py library.

      The following run file executes in 79ms. (Internal runtime of the generated code is 3.8ms).

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      # JACK and KING are perfect squares
      "is_square(JACK)"
      "is_square(KING)"
      
      # QUEEN is odd
      "N % 2 = 1"
      
      # ACE is prime
      "is_prime(ACE)"
      
      # total sum is divisible by 52
      "(JACK + QUEEN + KING + ACE) % 52 = 0"
      
      --answer="QUEEN"
      

      Solution: QUEEN = 89115.

      We have:

      JACK = 2704 = 52^2
      KING = 4356 = 66^2
      QUEEN = 89115 (odd)
      ACE = 701 (prime)
      sum = 97056 = 52 × 1863

      Like

    • GeoffR's avatar

      GeoffR 3:52 pm on 24 January 2025 Permalink | Reply

      
      % A Solution in MiniZinc
      include "globals.mzn";
      
      var 1..9:J; var 1..9:A; var 0..9:C; var 1..9:K;
      var 1..9:Q; var 0..9:U; var 0..9:E; var 0..9:N;
      var 0..9:I; var 0..9:G; 
      
      var 1000..9999:JACK == 1000*J + 100*A + 10*C + K;
      var 10000..99999:QUEEN == 10000*Q + 1000*U + 110*E + N;
      var 1000..9999:KING == 1000*K + 100*I + 10*N + G;
      var 100..999:ACE == 100*A + 10*C + E;
      
      constraint all_different([J,A,C,K,Q,U,E,N,I,G]);
      
      predicate is_prime(var int: x) = 
      x > 1 /\ forall(i in 2..1 + ceil(sqrt(int2float(ub(x))))) ((i < x) -> (x mod i > 0));
          
      predicate is_sq(var int: c) =
         let {
            var 0..ceil(pow(int2float(ub(c)),(1/2))): z
         } in z*z = c;
         
      constraint is_sq(JACK) /\ is_sq(KING);
      constraint QUEEN mod 2 == 1;
      constraint is_prime(ACE);
      constraint (JACK + QUEEN + KING + ACE) mod 52 == 0;
       
      solve satisfy;
      
      output["[JACK, QUEEN, KING, ACE ] =" ++ show ( [JACK, QUEEN, KING, ACE] ) ];
      
      % [JACK, QUEEN, KING, ACE ] =[2704, 89115, 4356, 701]
      % ----------
      % ==========
       
      
      

      Like

    • Ruud's avatar

      Ruud 5:57 pm on 24 January 2025 Permalink | Reply

      import istr
      
      for j, a, c, k, q, u, n, i, g, e in istr.permutations(istr.digits()):
          if (
              (j | a | c | k).is_square()
              and (k | i | n | g).is_square()
              and (q | u | e | e | n).is_odd()
              and (a | c | e).is_prime()
              and ((j | a | c | k) + (k | i | n | g) + (q | u | e | e | n) + (a | c | e)).is_divisible_by(52)
          ):
              print(f"queen={q | u | e | e | n}")
      

      Like

    • GeoffR's avatar

      GeoffR 2:17 pm on 25 January 2025 Permalink | Reply

      from itertools import permutations
      from enigma import is_prime
      sq4 = [x * x for x in range(32,100)]
      digits = set(range(10))
      
      # First stage permutation
      for p1 in permutations(digits, 4):
        k, i, n, g = p1
        if k == 0:continue
        king = 1000*k + 100*i + 10*n + g
        if king not in sq4:continue
        
        # Second stage permutation
        q1 = digits.difference(p1)
        for p2 in permutations(q1, 4):
          j, a, c, e = p2
          if 0 in (j, a): continue
          jack = 1000*j + 100*a + 10*c + k
          if jack not in sq4:continue
          ace = 100*a + 10*c + e
          if not is_prime(ace):continue
          
          # Third stage permutation
          q2 = q1.difference(p2) 
          for p3 in permutations(q2):
            q, u = p3
            if q == 0:continue
            queen = 10000*q + 1000*u + 110*e + n
            if queen % 2 == 1:
              if (jack + king + ace + queen) % 52 == 0:
                print(f"QUEEN = {queen}")
      
      
      
      

      Like

  • Unknown's avatar

    Jim Randell 7:49 am on 21 January 2025 Permalink | Reply
    Tags:   

    Teaser 2511: [Medallions] 

    From The Sunday Times, 7th November 2010 [link] [link]

    I have a collection of silver medallions, each of which weighs an exact whole number of grams. If I take pairs of them in every possible combination, I can weigh out an almost complete sequence of 16 consecutive weights, with only two weights, 100 grams and 101 grams, missing from the sequence. Even so, I can still manage an unbroken sequence of 13 weights, with only one of them, a prime number of grams, duplicated.

    What, in ascending order, are the weights in grams of my medallions?

    This puzzle was originally published with no title.

    [teaser2511]

     
    • Jim Randell's avatar

      Jim Randell 7:50 am on 21 January 2025 Permalink | Reply

      I reused the code written to solve Enigma 1600, which is a similar puzzle.

      With 6 weights we can combine them in pairs to make C(6, 2) = 15 pairs, so to make exactly 14 values only one of them is repeated.

      So we can either make:

      [13 values], [not 100, 101], [102] = [87..102] \ {100, 101}
      [99], [not 100, 101], [13 values] = [99..114] \ {100, 101}

      This Python 3 program runs in 68ms. (Internal runtime is 1.6ms).

      from enigma import (irange, diff, C, div, multiset, subsets, is_prime, printf)
      
      # extend <ss> with <k> more values to make numbers in <ns>
      def pairs(ns, ss, k):
        # are we done?
        if k == 0 or not ns:
          if k == 0 and not ns:
            yield ss
        elif C(k, 2) + k * len(ss) >= len(ns):
          # add in the next smallest value
          for x in irange(ss[-1] + 1, max(ns) - ss[-1]):
            # and solve for the remaining numbers
            ns_ = diff(ns, set(x + y for y in ss))
            yield from pairs(ns_, ss + [x], k - 1)
      
      # find <k> values that can be used in pairs to make <ns>
      def solve(ns, k):
        # suppose the values start [0, a, ...]
        # then the pairs start [a, ...]
        m = ns[-1] - ns[0] + 5 - 2 * k
        for a in irange(1, m):
          # reduce the numbers so that they start at a
          d = div(ns[0] - a, 2)
          if d is None: continue
          vs = list(n - 2 * d for n in ns)
          # solve the reduced list
          for ss in pairs(vs[1:], [0, a], k - 2):
            # return (<values>, <pair sums>)
            ss = tuple(v + d for v in ss)
            ps = sorted(x + y for (x, y) in subsets(ss, size=2))
            yield (ss, ps)
      
      # consider a sequence of 16 consecutive values that includes 100, 101
      for i in [87, 99]:
        ns = diff(irange(i, i + 15), {100, 101})
        for (ss, ps) in solve(ns, 6):
          # now find what values we can make, and check duplicates
          (ds, xs) = multiset.from_seq(ps).differences(ns)
          if xs or ds.size() != 1: continue
          # the duplicate value is prime
          d = ds.peek()
          if not is_prime(d): continue
          # output solution
          printf("{ss} -> {ns}; duplicate = {d}")
      

      Solution: The weights of the medallions are: 43, 44, 45, 46, 49, 53 grams.

      Taken in pairs these make the values:

      87, 88, 89 (twice), 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 102

      Like

  • Unknown's avatar

    Jim Randell 6:58 am on 19 January 2025 Permalink | Reply
    Tags:   

    Teaser 3252: Family tree 

    From The Sunday Times, 19th January 2025 [link] [link]

    George and Martha have five daughters; they are, in order of arrival, Andrea [eldest], Bertha, Caroline, Dorothy and Elizabeth [youngest]. They now have ages that are all [different] prime numbers in the range 28 to 60. The average [of the numbers] is also a prime number, different from the other five.

    Each daughter has a son, Adam, Brian, Colin, David and Edward respectively. They are all mathematics students, and are studying how to calculate square roots without using a calculator. One of them was given a perfect square with three or four digits (none repeated) and told to work out the square root. This he did accurately, getting his mother’s age, but he noted that the sum of the digits of that perfect square was also prime.

    Which boy was it, and what is his mother’s age?

    [teaser3252]

     
    • Jim Randell's avatar

      Jim Randell 7:10 am on 19 January 2025 Permalink | Reply

      This Python program runs in 65ms. (Internal runtime is 141us).

      from enigma import (primes, subsets, iavg, nsplit, printf)
      
      # choose the five ages (youngest (E) to eldest (A))
      for ps in subsets(primes.between(28, 60), size=5):
        # the mean is a different prime
        m = iavg(ps)
        if m is None or m in ps or m not in primes: continue
      
        # one of the primes has a square with a prime digit sum
        for (t, p) in zip("EDCBA", ps):
          s = p * p
          ds = nsplit(s)
          if not (len(set(ds)) == len(ds) and sum(ds) in primes): continue
          # output solution
          printf("ages = {ps}, mean = {m} -> {t} = {p}, square = {s}")
      

      Solution: David did the calculation. His mother is 37.

      The ages are:

      Andrea = 59
      Bertha = 47
      Caroline = 41
      Dorothy = 37
      Elizabeth = 31
      → mean = 43

      And the square of Dorothy’s age is 37^2 = 1369, which has a digit sum of 19.

      Like

    • Frits's avatar

      Frits 9:50 am on 19 January 2025 Permalink | Reply

      from enigma import SubstitutedExpression
      
      # invalid digit / symbol assignments
      d2i = dict()
      for dgt in range(0, 10):
        vs = set()
        if dgt < 2 or dgt > 5: vs.update('ACEGI')
        if dgt % 2 == 0 or dgt == 5: vs.update('BDFHJ')
        d2i[dgt] = vs
      
      # the alphametic puzzle
      p = SubstitutedExpression(
        [
          "AB > 28",
          "is_prime(AB)",
          "CD > AB",
          "is_prime(CD)",
          "EF > CD",
          "is_prime(EF)",
          "GH > EF",
          "is_prime(GH)",
          "IJ > GH",
          "is_prime(IJ)",
          # the mean is a different prime
          "is_prime(mean := (AB + CD + EF + GH + IJ) / 5)",
          "mean not in {AB, CD, EF, GH, IJ}",
          
          # the sum of the digits of that perfect square was also prime
          "len(res := [(i, m) for i, m in enumerate([AB, CD, EF, GH, IJ]) \
               if len(set((s := str(m * m)))) == len(s) and \
                  is_prime(sum([int(x) for x in s]))] ) > 0",
        ],
        answer="res",
        d2i=d2i,
        distinct="",
        reorder=0,
        verbose=0,    # use 256 to see the generated code
      )
      
      boys = "Edward David Colin Brian Adam".split()
      # print answers
      for ans in p.answers():
        for a in ans:
          print(f"the boy was {boys[a[0]]} and his mother’s age was {a[1]}")
      

      Like

      • Frits's avatar

        Frits 10:19 am on 19 January 2025 Permalink | Reply

        @Jim, I just noticed that “is_prime(2.5)” yields “True”.
        Are you going to change the subroutine?

        Like

        • Jim Randell's avatar

          Jim Randell 10:32 am on 19 January 2025 Permalink | Reply

          @Frits: If you don’t know if you are passing a non-negative integer you can turn on argument validation with the [[ validate=1 ]] parameter to check it for you (a ValueError exception will be raised on invalid input).

          Like

    • ruudvanderham's avatar

      ruudvanderham 2:34 pm on 20 January 2025 Permalink | Reply

      import istr
      
      istr.repr_mode("int")
      primes = [n for n in range(28, 61) if istr.is_prime(n)]
      
      for ages in istr.combinations(primes, 5):
          if (average := int(sum(ages)) / 5) in primes and not (average in ages):
              for age, name in zip(ages, "Adam Brian Colin David Edward".split()):
                  if sum(i for i in age**2).is_prime() and (age**2).all_distinct():
                      print(f"Name boy={name} Mother's age={age} [Ages={ages} Average={int(average)}]")

      Like

  • Unknown's avatar

    Jim Randell 7:42 am on 17 January 2025 Permalink | Reply
    Tags: ,   

    Teaser 2588: On-line 

    From The Sunday Times, 29th April 2012 [link] [link]

    Linesmen Alf and Bert were checking the railway line from Timesborough to Teaserton. Alf started at Timesborough, Bert at Teaserton, and they walked towards each other at their own steady speeds. A train from Timesborough, travelling at constant speed, reached Alf at noon and passed him ten seconds later. At 12:06 pm the train reached Bert and passed him eight seconds later.

    At what time did the two men meet?

    [teaser2588]

     
    • Jim Randell's avatar

      Jim Randell 7:42 am on 17 January 2025 Permalink | Reply

      This puzzle is solved with a straightforward bit of analysis.

      Suppose A is walking with a speed of a (units/s) and B with speed of b, and the train travels with a speed of v.

      The train takes 10s to pass A, so in that 10s A walks a distance 10a, and the front of the train travels a distance 10v (in the same direction), so the length of the train t is:

      t = 10(v − a)

      The train takes 8s to pass B, and in that 8s B walks a distance 8a, and the front of the train travels a distance 8v (in the opposite direction), and so the length of the train can also be expressed as:

      t = 8(v + b)

      So:

      10(v − a) = 8(v + b)
      v = 5a + 4b

      The front of the train is level with A at noon, and at 12:06pm (i.e. 360 seconds later) it is level with B.

      So the train has travelled a distance of 360v in that time, and A has travelled a distance of 360a, so at 12:06 pm the distance between A and B is:

      d = 360(v − a)

      And the time taken for A and B to reduce this distance to zero is:

      360(v − a) / (a + b)
      = 360 (4a + 4b) / (a + b)
      = 360 × 4

      So it takes another 4× 6 minutes = 24 minutes after 12:06 pm for A and B to meet.

      Solution: Alf and Bert met at 12:30 pm.

      Like

c
Compose new post
j
Next post/Next comment
k
Previous post/Previous comment
r
Reply
e
Edit
o
Show/Hide comments
t
Go to top
l
Go to login
h
Show/Hide help
shift + esc
Cancel
Design a site like this with WordPress.com
Get started