Updates from Jim Randell Toggle Comment Threads | Keyboard Shortcuts

  • 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

  • Unknown's avatar

    Jim Randell 10:07 am on 14 January 2025 Permalink | Reply
    Tags: by: Graham Smith   

    Teaser 2590: Moving downwards 

    From The Sunday Times, 13th May 2012 [link] [link]

    A three-by-three array of the nine digits 1 – 9 is said to be “downward moving” if each digit is less than the digit to the east of it and to the south of it — see, for example, the keypad on a mobile phone.

    Megan has produced a downward moving array that is different from that on a mobile phone. If she were to tell you the sum of the digits in its left-hand column and whether or not the central digit was even, then you should be able to work out Megan’s array.

    What is Megan’s array?

    [teaser2590]

     
    • Jim Randell's avatar

      Jim Randell 10:09 am on 14 January 2025 Permalink | Reply

      This Python program uses the [[ SubstitutedExpression ]] solver from the enigma.py library to generate all possible “downward moving” arrays. And then uses the [[ filter_unique() ]] function to find solutions that are unique by the required criteria.

      It runs in 74ms. (Internal runtime is 9ms).

      from enigma import (SubstitutedExpression, irange, filter_unique, unpack, chunk, printf)
      
      # for a "downward moving" grid the rows and columns are in ascending order
      # consider the grid:
      #   A B C
      #   D E F
      #   G H I
      exprs = [
        "A < B < C", "D < E < F", "G < H < I",
        "A < D < G", "B < E < H", "C < F < I",
      ]
      
      # make a solver to find downward moving grids
      p = SubstitutedExpression(
        exprs,
        digits=irange(1, 9),
        answer="(A, B, C, D, E, F, G, H, I)",
      )
      
      # look for a non-standard layout, unique by <f>
      non_standard = lambda x: x != (1, 2, 3, 4, 5, 6, 7, 8, 9)
      # f = (sum of digits in LH column, central digit parity)
      f = unpack(lambda A, B, C, D, E, F, G, H, I: (A + D + G, E % 2))
      rs = filter_unique(p.answers(verbose=0), f, st=non_standard).unique
      
      # output solution(s)
      for r in rs:
        printf("{r}", r=list(chunk(r, 3)))
      

      Solution: Megan’s downward moving array looks like this:

      Of the 42 possible downward moving arrays, this is the only one that is unique by LH column sum (11) and parity of the central digit (even).

      (Of course moving from left-to-right and top-to-bottom the numbers are increasing, so maybe this should be considered an “upward moving” array).

      Liked by 1 person

    • Ruud's avatar

      Ruud 8:27 am on 16 January 2025 Permalink | Reply

      import itertools
      import collections
      
      
      collect = collections.defaultdict(list)
      for a in itertools.permutations(range(1, 10)):
          if (
              all(a[row * 3] < a[row * 3 + 1] < a[row * 3 + 2] for row in range(3))
              and all(a[col] < a[col + 3] < a[col + 6] for col in range(3))
              and a != tuple(range(1, 10))
          ):
              collect[a[0] + a[3] + a[6], a[4] % 2].append(a)
      
      for ident, solutions in collect.items():
          if len(solutions) == 1:
              for row in range(3):
                  for col in range(3):
                      print(solutions[0][row * 3 + col], end="")
                  print()
      

      Like

    • GeoffR's avatar

      GeoffR 9:28 am on 18 January 2025 Permalink | Reply

      #   A B C
      #   D E F
      #   G H I
      
      from itertools import permutations
      from collections import defaultdict
      
      MEG = defaultdict(list)
      digits = set(range(1, 10))
      
      for p1 in permutations(digits, 6):
        A, B, C, D, E, F = p1
        if A < B < C and D < E < F and \
          A < D and B < E and C < F:
          q1 = digits.difference(p1)
          for p2 in permutations(q1):
            G, H, I = p2
            if G < H < I:
              if A < D < G and B < E < H and C < F < I:
                MEG[(A + D + G, E % 2)].append([A, B, C, D, E, F, G, H, I])
                
      # check E is even for Meghans array as 5th element(E)
      # ..of standard telephone array is odd i.e. 5
      for k, v in MEG.items():
        if len(v) == 1 and v[0][4] % 2 == 0:
          print("Meghans array: ")
          print(v[0][0], v[0][1], v[0][2])
          print(v[0][3], v[0][4], v[0][5])
          print(v[0][6], v[0][7], v[0][8])
      
      '''
      Meghans array:  
      1 2 5
      3 4 6
      7 8 9
      '''
      
      

      Like

  • Unknown's avatar

    Jim Randell 7:00 am on 12 January 2025 Permalink | Reply
    Tags:   

    Teaser 3251: Number test 

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

    I asked my daughter to find as many three-digit numbers as she could, each of which had the property that the number equalled the sum of the cubes of its individual digits. If she only found one such number, I asked her to give me this number — otherwise, if she had found more than one, to give me the sum of all such numbers found.

    She gave me a prime number, from which I could see that not all answers had been found.

    Which number or numbers did she not find?

    [teaser3251]

     
    • Jim Randell's avatar

      Jim Randell 7:07 am on 12 January 2025 Permalink | Reply

      It is straightforward to test all 3-digit numbers for the required property, and then look for (proper) subsets of the numbers found, to find a subset with a prime sum.

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

      from enigma import (irange, nsplit, subsets, is_prime, diff, printf)
      
      # find 3-digit numbers that are the sum of the cube of their digits
      cb = list(d**3 for d in irange(0, 9))
      ns = list(n for n in irange(100, 999) if n == sum(cb[d] for d in nsplit(n)))
      printf("numbers = {ns}")
      
      # find (proper) subsets that sum to a prime
      for ss in subsets(ns, min_size=1, max_size=len(ns) - 1):
        t = sum(ss)
        if not is_prime(t): continue
        # output solution
        missing = diff(ns, ss)
        printf("sum{ss} = {t}; missing = {missing}")
      

      Solution: The numbers not found are: 371, 407.

      There are only four such numbers (if leading zeros are disallowed):

      153, 370, 371, 407

      And the only strict subset of these with a prime sum is:

      153 + 370 = 523

      If all four numbers had been found, the sum would also have been prime:

      153 + 370 + 371 + 407 = 1301

      Like

    • Frits's avatar

      Frits 2:56 pm on 12 January 2025 Permalink | Reply

      from itertools import combinations
      
      def is_prime(n):            
        if n < 4:
          return n > 1
        if n % 2 == 0 or n % 3 == 0:
          return False
      
        # all primes > 3 are of the form 6n +/- 1
        # start with f=5 (which is prime) and test f, f+2 for being prime
        f, r = 5, int(n**0.5)
        while f <= r:
          if n % f == 0: return False
          if n % (f + 2) == 0: return False
          f += 6
      
        return True
      
      nums = []
      for a in range(1, 10):
        a3 = a * a * a
        for b in range(10):
          a3b3 = a3 + b * b * b
          for c in range(10):
            t = a3b3 + c * c * c
            if t // 100 != a: continue
            if t % 10 != c: continue
            if (t % 100) // 10 != b: continue
            nums += [t]   
      sum_nums = sum(nums)
      
      print("answer(s):")
      for k in range(1, len(nums)):
        # pick <k> numbers from <ns> she has missed
        for s in combinations(nums, k):
          # she gave me a prime number
          if is_prime(sum_nums - sum(s)):
            print(s)
      

      Like

    • ruudvanderham's avatar

      ruudvanderham 12:09 pm on 13 January 2025 Permalink | Reply

      Rather straightforward:

      import istr
      
      istr.repr_mode("str")
      
      solutions = {i for i in istr(range(100, 1000)) if sum(digit**3 for digit in i) == i}
      print(*list(f"missing: {solutions - set(x)}" for r in range(1, len(solutions)) for x in istr.combinations(solutions, r=r) if sum(x).is_prime()))

      Like

  • Unknown's avatar

    Jim Randell 8:29 am on 10 January 2025 Permalink | Reply
    Tags:   

    Teaser 2594: Just enough 

    From The Sunday Times, 10th June 2012 [link] [link]

    I have two identical bags of balls, each containing two reds, two yellows and two blues. Blindfolded throughout, I remove just enough balls from the first to be sure that the removed balls include at least two colours. I place these balls in the second bag. Then I remove some balls from the second bag and put them in the first: I move just enough to be sure that the first bag will now contain at least one ball of each colour.

    What (as a percentage) is the probability that all the balls left in the second bag are the same colour?

    [teaser2594]

     
    • Jim Randell's avatar

      Jim Randell 8:29 am on 10 January 2025 Permalink | Reply

      This Python program determines the minimal number of balls moved in each of the two steps, to meet the required conditions.

      It then generates all possible (equally likely) outcomes using those numbers to determine the proportion that leave all balls the same colour in the second bag.

      from enigma import (Accumulator, irange, multiset, subsets, fdiv, printf)
      
      # initial state of the bags
      bag0 = multiset(R=2, Y=2, B=2)
      
      # move balls <ss> from <X> to <Y>
      def move(X, Y, ss): return (X.difference(ss), Y.combine(ss))
      
      # find minimum subset size to transfer from <X> to <Y>
      # that ensures condition <fn> (on the modified <X>, <Y>)
      def solve(X, Y, fn):
        for k in irange(1, X.size()):
          fail = 0
          for ss in X.subsets(size=k):
            # move ss from X to Y
            (X_, Y_) = move(X, Y, ss)
            if not fn(X_, Y_):
              fail = 1
              break
          if fail == 0: return k
      
      # initially move <k1> balls, such that at least 2 colours are moved
      k1 = solve(bag0, bag0, (lambda X, Y: sum(v > 2 for v in Y.values()) >= 2))
      printf("[k1 = {k1}]")
      
      # find the number of balls required to move back
      r = Accumulator(fn=max)
      # consider all bags with <k1> balls moved
      for ss in bag0.subsets(size=k1):
        (bag1, bag2) = move(bag0, bag0, ss)
        # move balls from bag2 to bag1, such that bag1 has at least one of each colour
        r.accumulate(solve(bag2, bag1, (lambda X, Y: Y.distinct_size() >= 3)))
      k2 = r.value
      printf("[k2 = {k2}]")
      
      # count all possible outcomes (t), and those where the second bag only
      # contains balls of one colour (n)
      t = n = 0
      bag1_0 = bag2_0 = bag0
      
      # step (1) move k1 balls from bag1 to bag2
      for ss1 in subsets(bag1_0, size=k1):
        (bag1_1, bag2_1) = move(bag1_0, bag2_0, ss1)
      
        # step (2) move k2 balls from bag2 to bag1
        for ss2 in subsets(bag2_1, size=k2):
          (bag2_2, bag1_2) = move(bag2_1, bag1_1, ss2)
      
          t += 1
          # does bag2 only contain balls of one colour?
          if bag2_2.distinct_size() == 1: n += 1
      
      # output solution
      printf("P = {n}/{t} = {f:.2%}", f=fdiv(n, t))
      

      Solution: The probability that the balls remaining in the second bag are all the same colour is 5%.

      It is easy to see that the first step requires 3 balls to be moved: if we moved only 2 balls we might choose 2 of the same colour, but with 3 balls they cannot all be of the same colour (as there are only 2 of each colour).

      The second step requires 6 balls to be moved to ensure that there is at least one ball of each colour in the first bag. For example: if the first step moves RRY to bag 2, then bag 1 contains YBB and bag 2 contains RRRRYYYBB, and we need to move at least 6 balls (as if we only choose 5 we might choose YYYBB, which would leave bag 1 with only blue and yellow balls).


      Manually:

      For the first step, moving 3 balls from bag 1 to bag 2, we might move 3 different coloured balls, with a probability of 5/5 × 4/5 × 2/4 = 2/5. And the remaining 3/5 of the time we would remove 2 balls of one colour and 1 ball of a different colour.

      So we have two possible patterns of balls in the bags:

      (2/5) XYZ + XXXYYYZZZ
      (3/5) YZZ + XXXXYYYZZ

      We can then look at moving 6 balls back to the first bag. Which is the same as choosing 3 balls to leave behind, and we want to look at the probability that these balls are the same colour.

      In the first case the probability this happening is: 9/9 × 2/8 × 1/7 = 1/28.

      In the second case we can choose to leave 3 X’s or 3 Y’s to leave behind. The probabilities are:

      P(XXX) = 4/9 × 3/8 × 2/7 = 1/21
      P(YYY) = 3/9 × 2/8 × 1/7 = 1/84

      Giving a final probability of:

      P = (2/5) × (1/28) + (3/5) × (1/21 + 1/84) = 1/20

      Like

  • Unknown's avatar

    Jim Randell 8:07 am on 8 January 2025 Permalink | Reply
    Tags:   

    Teaser 2595: Stylish fences 

    From The Sunday Times, 17th June 2012 [link] [link]

    Farmer Giles has a hexagonal field bounded by six straight fences. The six corners lie on a circle of diameter somewhere around 250 metres. At three alternate corners there are stiles. The angles of the hexagon at the corners without stiles are all the same. All six fences are different whole numbers of metres long. I have just walked in a straight line from one of the stiles to another and the distance walked was a whole number of metres.

    How many?

    [teaser2595]

     
    • Jim Randell's avatar

      Jim Randell 8:07 am on 8 January 2025 Permalink | Reply

      Suppose the field is:

      If the stiles are at A, C, E, then the angles at B, D, F are all equal, which means they are all 120°, and so the lines AC, AE, CE (= paths between stiles) are all equal length and so ACE is an equilateral triangle. (See: [ @wikipedia ]).

      If the diameter of the circle is d, then the length of the paths, p, is given by:

      p = d √(3/4)

      And d is around 250, let’s say in the range [245, 255], so the path is in the range [212.176, 220.836], so we can consider path lengths of 212 .. 221.

      For a given path length p (= AC) we can look for integer fence lengths (x, y) (= (AB, BC)) that complete the triangle ABC. By the cosine rule in ABC:

      p^2 = x^2 + y^2 + xy

      y^2 + xy + (x^2 − p^2) = 0

      which is a quadratic equation in y.

      So we can consider values for x and use the equation to find possible corresponding y values.

      And we need to find (at least) 3 different (x, y) pairs to complete the hexagon, so that all 6 fences are different lengths.

      The following Python program runs in 76ms. (Internal runtime is 7.5ms).

      from enigma import (irange, quadratic, sq, sqrt, printf)
      
      r43 = sqrt(4, 3)
      
      # consider possible path lengths
      for p in irange(212, 221):
        # collect possible (x, y) pairs
        ss = list()
        for x in irange(1, p - 1):
          for y in quadratic(1, x, sq(x) - sq(p), domain='Z'):
            if x < y < p:
              ss.append((x, y))
      
        # we need (at least) 3 different pairs
        if len(ss) < 3: continue
        # output solution
        printf("p={p} d={d:.3f} -> {ss}", d=p * r43)
      

      Solution: The path is 217m long.

      There are actually 4 possible (x, y) pairs, and any 3 of them can be used to construct the hexagon:

      p=217 d=250.570 → [(17, 208), (77, 168), (87, 160), (93, 155)]

      If the puzzle had specified that the diameter of the circle was “around 400 m”, then we would have been able to give the lengths of the 6 fences:

      p=343 d=396.062 → [(37, 323), (112, 273), (147, 245)]

      Like

  • Unknown's avatar

    Jim Randell 6:48 am on 5 January 2025 Permalink | Reply
    Tags:   

    Teaser 3250: Very similar triangles 

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

    I have a piece of A6 paper on which I draw two triangles. The triangles are very similar in two ways. First of all, they are both the same shape. Not only that, the lengths of two of the sides of one of the triangles are the same as the lengths of two of the sides of the other triangle, but one triangle is larger than the other.

    If the sides of the triangles are whole numbers of millimetres and the triangles don’t have any obtuse angles:

    What are the lengths of sides of the larger triangle?

    Note: A sheet of A6 paper measures 105 mm × 148 mm.

    [teaser3250]

     
    • Jim Randell's avatar

      Jim Randell 7:02 am on 5 January 2025 Permalink | Reply

      (See also: Enigma 1198).

      This Python program runs in 67ms. (Internal runtime is 1.5ms).

      from enigma import (irange, hypot, intf, printf)
      
      # consider triangles with sides (a, b, c), (b, c, d)
      # we have: ratio(a, b, c) = ratio(b, c, d)
      M = intf(hypot(105, 148))  # max length
      for a in irange(1, M - 1):
        for b in irange(a + 1, M):
          (c, r) = divmod(b * b, a)
          if c > M: break
          if r != 0: continue
          # check triangle is not obtuse
          if a + b <= c or c * c > a * a + b * b: continue
          (d, r) = divmod(c * c, b)
          if d > M: break
          if r != 0: continue
          # output solution
          printf("{t1} + {t2}", t1=(a, b, c), t2=(b, c, d))
      

      There is only a single candidate solution, and it is easy to demonstrate that the triangles can both fit onto a single piece of A6.

      Solution: The sides of the larger triangle are: 80 mm, 100 mm, 125 mm.

      And the smaller triangle has sides: 64 mm, 80 mm, 100 mm.

      The internal angles of the triangles are (approximately) 40°, 53°, 87°.

      Here is a diagram showing both triangles on a piece of A6 paper (dimensions in mm):

      Like

    • Tony Smith's avatar

      Tony Smith 4:29 pm on 5 January 2025 Permalink | Reply

      In fact there is nothing to stop the triangles overlapping.

      Like

      • Jim Randell's avatar

        Jim Randell 4:57 pm on 5 January 2025 Permalink | Reply

        @Tony: True enough. I was imagining we wanted to cut the triangles out, but that isn’t what the puzzle text says.

        Like

  • Unknown's avatar

    Jim Randell 10:22 pm on 3 January 2025 Permalink | Reply
    Tags:   

    Teaser 2589: A certain age 

    From The Sunday Times, 6th May 2012 [link] [link]

    My cousin and I share a birthday. At the moment my age, in years, is the same as hers but with the order of the two digits reversed. On one of our past birthdays I was five times as old as her but, even if I live to a world record “ripe old age”, there is only one birthday in the future on which my age will be a multiple of hers.

    How old am I?

    [teaser2589]

     
    • Jim Randell's avatar

      Jim Randell 10:22 pm on 3 January 2025 Permalink | Reply

      Suppose my age is XY and my cousin’s age is YX, then we have X > Y.

      This Python program runs in 63ms. (Internal runtime is 306µs).

      from enigma import (irange, subsets, printf)
      
      # ages are the reverse of each other
      for (Y, X) in subsets(irange(1, 9), size=2):
        (XY, YX) = (10 * X + Y, 10 * Y + X)
      
        # look for an age in the past where I was five times the age of the cousin
        past = ((XY - n, YX - n) for n in irange(1, YX - 1))
        x1 = list((x, y) for (x, y) in past if x == 5 * y)
        if not x1: continue
      
        # look for ages in the future where my age is a multiple of hers
        future = ((XY + n, YX + n) for n in irange(1, 130 - XY))
        x2 = list((x, y) for (x, y) in future if x % y == 0)
        if len(x2) != 1: continue
      
        # output solution
        printf("XY={XY} YX={YX} [past={x1} future={x2}]")
      

      Solution: Your age is 62.

      And the cousin’s age is 26.

      17 years ago the ages were 45 and 9 (and 45 = 5 × 9).

      In 10 years time the ages will be 72 and 36 (and 72 = 2 × 36).

      Like

    • Hugo's avatar

      Hugo 7:23 am on 5 January 2025 Permalink | Reply

      The difference between the ages is 9(X – Y).
      That remains the same each year, and at one time it was 4 times the cousin’s age.
      So (X – Y) is either 4 or 8.
      The current ages could be 15, 51; 26, 62; 37, 73; 48, 84; or 59, 95.
      In all cases the difference is 36.
      Ages when one age is an integer multiple of the other are 1, 37; 2, 38; 4, 40; 9, 45; 18, 54; 36, 72.
      Only one of those is allowed to be in the future.

      Alternatively, the current ages could be 19, 91, with difference 72.
      Then the ‘multiple’ ages are 1, 73; 2, 74; 4, 76; 8, 80; 9, 81; 18, 90; 36, 108; 72, 144.
      But two of those are in the future.
      The only valid solution appears to be 26, 62.

      Like

    • Hugo's avatar

      Hugo 7:32 am on 6 January 2025 Permalink | Reply

      I forgot some pairs of ages where one is an integer multiple of the other.
      With a difference 36: 3, 39; 6, 42; 12, 48.
      Similarly with a difference 72, but we’ve eliminated that as a potential solution.

      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