Recent Updates Page 10 Toggle Comment Threads | Keyboard Shortcuts

  • Unknown's avatar

    Jim Randell 6:36 am on 16 March 2025 Permalink | Reply
    Tags:   

    Teaser 3260: Alphabet soup 

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

    Clark had four dice with each of the 24 faces containing a different letter of the alphabet. Up to four could be rotated then placed side by side to spell out words on the topmost faces. Clark could spell out any of the words in just one of the following sayings:

    “Don’t rock the boat”
    “Easy come, easy go”
    “Give a dog a bad name”
    “If the cap fits wear it”
    “Life is what you make it”
    “Make love not war”
    “You reap what you sow”
    “If you can’t beat them join them”
    “Don’t bury your head in the sand”
    “Time and tide wait for no man”

    It was impossible to have any arrangement of 24 letters that could spell the words in this saying but also spell the words in one of the others.

    Which saying was Clark able to spell out?

    [teaser3260]

     
    • Jim Randell's avatar

      Jim Randell 7:07 am on 16 March 2025 Permalink | Reply

      See also: Enigma 687, Teaser 2780, Enigma 1605.

      I adapted the code I wrote for Enigma 687 to solve this puzzle. (Allowing words of different lengths, but not allowing symbols to appear on more than one die).

      I have removed repeated words from the phrases, and also words that are included in other words. Note that each phrase consists of valid words (no repeated letters, no more than 4 letters), and also each phrase has at least one maximal length word.

      This Python 3 program runs in 146ms. (Internal runtime is 72ms).

      from enigma import (
        irange, subsets, seq_ordered as ordered, peek, uniq, append, remove, join, printf
      )
      
      # phrases to check
      phrases = list(set(x.split()) for x in [
        "DONT ROCK THE BOAT",
        "EASY COME GO",  # "EASY COME EASY GO",
        "GIVE DOG BAD NAME",  # "GIVE A DOG A BAD NAME"
        "THE CAP FITS WEAR",  # "IF THE CAP FITS WEAR IT"
        "LIFE IS WHAT YOU MAKE IT",
        "MAKE LOVE NOT WAR",
        "YOU REAP WHAT SOW",  # "YOU REAP WHAT YOU SOW"
        "IF YOU CANT BEAT THEM JOIN",  # "IF YOU CANT BEAT THEM JOIN THEM"
        "DONT BURY YOUR HEAD IN THE SAND",
        "TIME AND TIDE WAIT FOR NO MAN",
      ])
      
      # merge symbols <ss> into dice <ds>; max <k> symbols per die
      def merge(ds, ss, k):
        rs = list(ds)
        for (i, (d, s)) in enumerate(zip(ds, ss)):
          if s in d or s == '?':
            pass
          elif len(d) < k and not any(s in r for r in rs):
            rs[i] = append(d, s)
          else:
            return
        return rs
      
      # construct dice <ds> for words <ws>; max <k> symbols per die
      def construct(ws, ds, n, k):
        # are we done?
        if not ws:
          yield ordered(ordered(d) for d in ds)
        else:
          # choose the next word, and pad to required length
          w = peek(ws)
          w_ = w + '?' * (n - len(w))
          # consider all anagrams of the word
          for ss in subsets(w_, size=len, select='mP'):
            ds_ = merge(ds, ss, k)
            if ds_:
              yield from construct(remove(ws, w), ds_, n, k)
      
      # find sets of <n> <k>-sided dice to display <words>
      def solve(words, n, k):
        ds = list(set() for _ in irange(n))
        # start with a maximal word
        w = peek(w for w in words if len(w) == n)
        for (i, x) in enumerate(w):
          ds[i].add(x)
        # solve for the remaining words
        return uniq(construct(remove(words, w), ds, n, k))
      
      pfmt = lambda ws: join(ws, sep=' ', enc='"')  # format a phrase
      dfmt = lambda ds: join(map(join, ds), sep=', ', enc="[]")  # format a set of dice
      
      # check phrase <ph> can only be made with a non-extendable set of dice
      def check(ph, n=4, k=6):
        ds = None
        # make a set of <n> dice (<k> sided) for this phrase
        for ds in solve(ph, n, k):
          # can we extend the set to make any of the other phrases?
          ds1 = None
          for ph1 in phrases:
            if ph1 == ph: continue
            ds1 = peek(construct(ph1, ds, n, k), default=None)
            if ds1: break
          # can this set of dice be extended?
          if ds1:
            # output an extendable set
            printf("{ph} -> {ds}", ph=pfmt(ph), ds=dfmt(ds))
            printf("+ {ph1} -> {ds1}", ph1=pfmt(ph1), ds1=dfmt(ds1))
            printf()
            return
        # return a non-extendable set
        return ds
      
      # choose one of the phrases
      for ph in phrases:
        # check there are only non-extendable sets for this phrase
        ds = check(ph)
        if ds:
          printf("SOLUTION: {ph} -> {ds}", ph=pfmt(ph), ds=dfmt(ds))
          printf()
      

      Solution: The saying is: “Time and tide wait for no man”.

      For example the following set of dice can make the phrase (_ can be any letter):

      AEO___
      DFMW__
      INR___
      T_____

      But there is no way to fill out the unspecified letters such that any of the other phrases can be made. And this is true for all sets of 4 dice that can be used to spell out the phrase.

      We can see this in the example set because A and E and O are on the same die, which means we cannot make any of the following words:

      BEAT (AE)
      BOAT (AO)
      EASY (AE)
      HEAD (AE)
      MAKE (AE)
      NAME (AE)
      REAP (AE)
      WEAR (AE)

      And one of these words is included in each of the remaining phrases.

      In fact, there are 36 possible sets of partial dice that can be used to make “TIME AND TIDE WAIT FOR NO MAN“.

      All of them have A and E on the same die, so this eliminates all the other phrases apart from “DONT ROCK THE BOAT“.

      Of these 36 sets, 12 of them also have O on same die as A and E, so those eliminate BOAT also (as above).

      And the remaining 24 sets each have at least 2 letters of DONT on the same die, so these are not possible either.

      Like

      • Frits's avatar

        Frits 3:04 pm on 16 March 2025 Permalink | Reply

        @Jim, your latest code (with seq_ordered ) also has the runtime unpredicability issue with CPython. If you solve this please let me know how you did this.

        Like

        • Jim Randell's avatar

          Jim Randell 4:30 pm on 16 March 2025 Permalink | Reply

          In CPython3 if you ask for elements of a set() (for example) they could come out in any order. So the execution path the program takes may be different each time it is run. Sometimes it might hit upon a faster execution path, and sometimes a slower one.

          For a more reproducible execution path you can use PyPy (or in this case PyPy3).

          % repeat 5 python3.14 -c "from enigma import *; print(peek(set(str_upper)))"
          G
          T
          I
          Y
          G
           
          % repeat 5 python2.7 -c "from enigma import *; print(peek(set(str_upper)))"
          A
          A
          A
          A
          A
           
          % repeat 5 pypy -c "from enigma import *; print(peek(set(str_upper)))"
          A
          A
          A
          A
          A
          
          % repeat 5 pypy3 -c "from enigma import *; print(peek(set(str_upper)))"
          A
          A
          A
          A
          A
          

          But if you set the environment variable PYTHONHASHSEED to a fixed integer value, then CPython3 will behave in a repeatable fashion:

          % repeat 5 PYTHONHASHSEED=42 python3.14 -c "from enigma import *; print(peek(set(str_upper)))"  
          J
          J
          J
          J
          J
          

          Like

      • Jim Randell's avatar

        Jim Randell 11:15 am on 19 March 2025 Permalink | Reply

        Here is a faster solution that collects the dice as a map (instead of a collection of sequences), as each symbol can only appear on one die.

        It has an internal runtime of 4.8ms.

        from enigma import (irange, multiset, fail, intersect, subsets, peek, group, remove, update, join, printf)
        
        # phrases to check
        phrases = [
          "DONT ROCK THE BOAT",
          "EASY COME EASY GO",
          "GIVE A DOG A BAD NAME",
          "IF THE CAP FITS WEAR IT",
          "LIFE IS WHAT YOU MAKE IT",
          "MAKE LOVE NOT WAR",
          "YOU REAP WHAT YOU SOW",
          "IF YOU CANT BEAT THEM JOIN THEM",
          "DONT BURY YOUR HEAD IN THE SAND",
          "TIME AND TIDE WAIT FOR NO MAN",
        ]
        
        # analyse the phrases
        words = list()
        for ph in phrases:
          ws = list()
          for w in sorted(ph.split(), key=len, reverse=1):
            w = join(w, sort=1)
            s = set(w)
            fail(len(s) != len(w), "repeated letters found")
            if any(s.issubset(x) for x in ws): continue
            ws.append(w)
          words.append(ws)
        
        # complete dice (<d>, <r>) for words <ws>
        # d = map of symbols to die index
        # r = remaining count of symbols per die
        def construct(ws, d, r):
          # are we done?
          if not ws:
            yield (d, r)
          else:
            # choose the next word (maximal intersection)
            w = max(ws, key=lambda w: len(intersect([w, d.keys()])))
            # find used dice, and unused symbols
            (ds, us) = (list(), list())
            for x in w:
              i = d.get(x)
              if i is None:
                us.append(x)
              else:
                ds.append(i)
            # no die can be used more than once
            if len(ds) != len(set(ds)): return
            # assign unused symbols to the remaining dice
            rs = list(i for (i, x) in r.items() if i not in ds)
            if len(rs) < len(us): return
            for ss in subsets(rs, size=len(us), select='P'):
              d_ = update(d, us, ss)
              r_ = r.difference(ss)
              # and solve for the remaining words
              yield from construct(remove(ws, w), d_, r_)
        
        # find sets of <n> dice (<k>-sided) to display <ws>
        def dice(ws, n, k):
          (d, r) = (dict(), multiset.from_seq(irange(n), count=k))
          # start with the first word
          w = ws[0]
          for (i, x) in enumerate(w):
            d[x] = i
            r.remove(i)
          # and add in the remaining words
          yield from construct(ws[1:], d, r)
        
        # format a set of dice
        def fmt(d):
          g = group(d.keys(), by=d.get)
          return join((join(g[k], sort=1) for k in sorted(g.keys())), sep=", ", enc="[]")
        
        # collect phrases that can be extended
        multiple = set()
        
        # examine all sets for phrase <i>
        def solve(i, n=4, k=6):
          if i in multiple: return
        
          # make a set of 4 dice (6-sided) for this phrase
          d = None
          for (d, r) in dice(words[i], n, k):
            # can we extend the dice to make any of the other phrases?
            for (j, ws1) in enumerate(words):
              if j == i: continue
              (d1, _) = peek(construct(ws1, d, r), default=(None, None))
              if d1:
                multiple.update((i, j))
                # output an extendable set
                printf("\"{ph}\" -> {d}", ph=phrases[i], d=fmt(d))
                printf("+ \"{ph}\" -> {d1}", ph=phrases[j], d1=fmt(d1))
                printf()
                return
          # return an example non-extendable set
          return d
        
        # consider each phrase
        for (i, ph) in enumerate(phrases):
          d = solve(i)
          if d:
            printf("SOLUTION = \"{ph}\" -> {d}", d=fmt(d))
            printf()
        

        Like

    • Frits's avatar

      Frits 2:40 pm on 16 March 2025 Permalink | Reply

      The runtime of this program varies. Normally this is caused by using sets but removing sets as much as possible hasn’t removed the unpredictability of run times.

      [program removed]

      Like

      • Frits's avatar

        Frits 4:36 pm on 16 March 2025 Permalink | Reply

        Luckily no word contains duplicate letters otherwise a fix is needed.

        Like

        • Jim Randell's avatar

          Jim Randell 4:38 pm on 16 March 2025 Permalink | Reply

          @Frits: A letter can only appear on one die, so it wouldn’t be possible to make a word with repeated letters.

          Like

    • Frits's avatar

      Frits 5:28 pm on 16 March 2025 Permalink | Reply

      Thanks. I may try it.

      I found the cause of the unpredictable behaviour in my code. If during the first sort (line 19) the data is sorted on both length and value then the behaviour is predictable again. I’ll send a new version tomorrow.

      Like

    • Frits's avatar

      Frits 8:48 am on 19 March 2025 Permalink | Reply

      from itertools import combinations, permutations
      
      sayings_orig = ["Don't rock the boat",
                      "Easy come, easy go",
                      "Give a dog a bad name",
                      "If the cap fits wear it",
                      "Life is what you make it",
                      "Make love not war",
                      "You reap what you sow",
                      "If you can't beat them join them",
                      "Don't bury your head in the sand",
                      "Time and tide wait for no man"]
                 
      # convert to lowercase and remove non alphabetics
      sayings = [[''.join(c for c in w if c.isalpha()) for w in s.lower().split()]
                  for s in sayings_orig]
                                               
      # remove words that are subsets of other words and
      # sort sayings on length and preserve original index                 
      sayings = list(enumerate(sorted([w1 for i, w1 in enumerate(s) 
          if all(not set(w1).issubset(set(w2)) or (set(w1) == set(w2) and i < j) 
                for j, w2 in enumerate(s) if i != j)], key=len, reverse=True) 
                for s in sayings)) 
      
      # sort by number of 4-character words (descending)                                                            
      sayings.sort(key = lambda x: -len([1 for y in x[1] if len(y) == 4]))                                 
      ln = len(sayings)
            
      # try to place all letters on the dice in <ss> for all words in <ws>
      def solve(ws, ss=[]):
        if not ws:
          yield ss
        else:
          # try to add each letter of <w> to one of the 4 dice in <ss>
          if not ss: 
            ss = ["" for _ in range(4)]
            fn = combinations
            w = ws[0]    
            free_dice = range(4)  
          else:  
            fn = permutations 
            w = list(ws[0])
            free_dice = [n for n in range(4) if len(ss[n]) < 6] 
            removed = set()
            for ltr in ws[0]:
              for n in range(4):
                # letter <ltr> already on a die?
                if ltr in ss[n]:
                  if n in removed: return # two letters of current word on same die
                  if n in free_dice:
                    # credit: B. Gladman
                    free_dice.remove(n)
                    removed.add(n)
                  w.remove(ltr)
                  break
           
          # assign all unassigned letters to the available dice 
          for p in fn(free_dice, len(w)):
            ss_ = ss.copy()
            for i, n in enumerate(p):
              ss_[n] += w[i]
            yield from solve(ws[1:], ss_)
              
      sols, processed = set(range(ln)), set()     
        
      # for all sayings    
      for i in range(ln):    
        # skip if this saying is not the solution
        if (i_o := sayings[i][0]) not in sols: continue
        processed.add(i)
        for dice1 in solve(sayings[i][1]):
          # try to combine other sayings with these dice
          for j in range(ln):
            if j == i: continue
            # skip if saying <j> has been fully processed
            if (j_o := sayings[j][0]) in sols and j in processed: continue
            # skip if both sayings are not the solution
            if i_o not in sols and j_o not in sols: continue
            
            for dice2 in solve(sayings[j][1], dice1):
              print(f"with dice {[''.join(sorted(x)) for x in dice2]} "
                    f"the following sayings can be constructed:")
              print("--", sayings_orig[i_o])
              print("--", sayings_orig[j_o], "\n")
              sols -= {i_o, j_o}
              break # finding one example is enough to remove <j_o> from <sols>
            else: # no break
              continue
            break  
          else: # no break
            continue
          break    
                
      print(f"answer(s): '{' or '.join(sayings_orig[s] for s in sols)}'")
      

      Like

  • Unknown's avatar

    Jim Randell 7:29 am on 14 March 2025 Permalink | Reply
    Tags:   

    Teaser 2568: Ending 2011 

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

    I have written down three positive whole numbers consisting of two primes and a perfect square. Between them they use nine digits and no digit is repeated.

    If you form an addition sum with the three numbers, then what do you end up with as the total? Appropriately enough you end up with 2011.

    What, in increasing order, are the three numbers?

    [teaser2568]

     
    • Jim Randell's avatar

      Jim Randell 7:30 am on 14 March 2025 Permalink | Reply

      This Python program looks at possible squares (without repeated digits), and then checks to see if the remainder can be made from the sum of two primes.

      The year to be made can be specified on the command line.

      It runs in 62ms. (Internal runtime is 3.4ms).

      from enigma import (powers, inf, primes, distinct_chars, arg, printf)
      
      # total to find
      N = arg(2011, 0, int)
      
      # consider possible squares
      for s in powers(1, inf, 2):
        r = N - s
        if r < 5: break
        if distinct_chars(s) is None: continue
      
        # look for pairs of primes that sum to r
        for p in primes.between(2, r // 2):
          q = r - p
          # q is also a prime
          if q not in primes: continue
          # check for 9 different digits
          if distinct_chars(p, q, s) != 9: continue
          # output solution
          printf("{ns} -> {N} [primes = {p}, {q}; square = {s}]", ns=sorted([p, q, s]))
      

      Solution: The numbers are: 263, 841, 907.

      263 and 907 are prime, and 841 = 29^2.


      We could be a bit cleverer with finding pairs of primes that sum to r. For instance, if r is odd, then one of the primes would have to be 2. But it makes the code a bit longer, and the version above is already fast enough, so I haven’t included it.

      Like

    • Frits's avatar

      Frits 5:05 am on 15 March 2025 Permalink | Reply

      Designed for a low internal run time.

      from itertools import compress
      
      # total to find
      N = 2011
      
      # 2 < primes < n 
      def primesbelow(n):
        # rwh_primes1v2(n):
        """ Returns a list of 2 < primes < n for n > 2 """
        sieve = bytearray([True]) * (n // 2 + 1)
        for i in range(1, int(n ** 0.5) // 2 + 1):
          if sieve[i]:
            sieve[2 * i * (i + 1)::2 * i + 1] = \
            bytearray((n // 2 - 2 * i * (i + 1)) // (2 * i + 1) + 1)
        return {*compress(range(3, n, 2), sieve[1:])}
      
      P = primesbelow(1885)  # max prime is 2011 - 23 - 104
      
      # split number <n> in two different odd prime endings, unequal to <o>
      split = lambda n, o: [(i, i2) for i in range(1, 11, 2) 
                           if (i2 := (10 + n - i) % 10) > i and
                           {i, i2}.isdisjoint([5, o])]
      
      # odd prime number endings per odd square number ending
      pes = {se: split(11 - se, se) for se in [1, 5, 9] for i in range(1, 10, 2)}
      pes = {k: {x for v in vs for x in v} for k, vs in pes.items()}
      
      # all three numbers must be odd as 2 + abcd + efgh > 4000
      
      # consider odd possible squares (with at least 2 digits)
      for n in range(5, 45, 2):
        r = N - (sq := n * n)
        # square ending must allow for valid prime endings
        if not (se := pes[sq % 10]): continue
        if len(set(s := str(sq))) != len(s): continue
        
        # look for pairs of odd primes that sum to r 
        for p in sorted(P):
          # a 2-digit square requires a prime of 1xxx
          if '1' in s and len(s) == 2: break
          if p > r // 2: break
          # valid prime number ending
          if (p % 10) not in se: continue
      
          q = r - p
          # q is also a prime, no repeated digits
          if q not in P: continue
          if len(set(s1 := s + str(p) + str(q))) == len(s1) == 9: 
            # output solution
            print("answer:", sorted([sq, p, q]))
      

      Like

    • ruudvanderham's avatar

      ruudvanderham 11:31 am on 15 March 2025 Permalink | Reply

      import istr
      
      primes = [n for n in istr(range(100, 1000)) if n.is_prime() and n.all_distinct()]
      squares = [n * n for n in istr(range(10, 32)) if (n * n).all_distinct()]
      
      for (n1, n2), n3 in istr.product(istr.combinations(primes, r=2), squares):
          if (n1 | n2 | n3).all_distinct() and n1 + n2 + n3 == 2011:
              print(sorted((n1, n2, n3)))
      

      Like

      • Jim Randell's avatar

        Jim Randell 5:01 pm on 15 March 2025 Permalink | Reply

        @Ruud: Note that it is not specified that the three numbers are all 3-digit numbers (although it turns out that the numbers in the solution are).

        But if the puzzle had been set in 1990, the solution would not be three 3-digit numbers:

        % python3 teaser2568.py 1990
        [59, 324, 1607] -> 1990 [primes = 59, 1607; square = 324]
        

        Like

        • Ruud's avatar

          Ruud 5:13 pm on 15 March 2025 Permalink | Reply

          @Jim
          You are right. I had misread the problem description.
          The code below does not assume that the numbers have three digits.

          import istr
          
          primes = [n for n in istr(range(1, 2011)) if n.is_prime() and n.all_distinct()]
          squares = [n * n for n in istr(range(1, 45)) if (n * n).all_distinct()]
          
          for (n1, n2), n3 in istr.product(istr.combinations(primes, r=2), squares):
              if (n1 | n2 | n3).all_distinct() and n1 + n2 + n3 == 2011:
                  print(sorted((n1, n2, n3)))
          

          Like

          • Frits's avatar

            Frits 2:53 am on 16 March 2025 Permalink | Reply

            @Ruud, you would have reported three solutions for year 1990 .

            “Between them they use nine digits” means exactly nine digits.

            Like

    • Frits's avatar

      Frits 4:21 am on 16 March 2025 Permalink | Reply

      712 is the first year that there is a solution.
      Up to year 2011 there are 454 solutions.
      The program runs in 163ms but is not valid for years a little higher than 2011.

      from itertools import compress
      
      # 2 < primes < n 
      def primesbelow(n):
        # rwh_primes1v2(n):
        """ Returns a list of 2 < primes < n for n > 2 """
        sieve = bytearray([True]) * (n // 2 + 1)
        for i in range(1, int(n ** 0.5) // 2 + 1):
          if sieve[i]:
            sieve[2 * i * (i + 1)::2 * i + 1] = \
            bytearray((n // 2 - 2 * i * (i + 1)) // (2 * i + 1) + 1)
        return {*compress(range(3, n, 2), sieve[1:])}
      
      M = 2011
      # max prime is N - 23 - 104
      P = {p for p in primesbelow(M - 126) if len(set(s := str(p))) == len(s)}
      P2 = sorted(p for p in P if p < 100)
      P3 = sorted(p for p in P if 100 <= p < 1000)
      
      c = 0
      for N in range(1, M + 1):
        # consider possible squares (with at least 2 digits)
        for n in range(4 + (N % 2), int(N**.5) + 1, 2):
          r = N - (sq := n * n)
          if r < 120: break # at least a 2-digit and a 3-digit prime 
          if len(set(s := str(sq))) != (ln := len(s)): continue
          
          # determine which primes to use
          # consider the number of digits in the square number
          # 2 : p + q = 7 so 3 and 4
          # 3 : p + q = 6 so 2 and 4  or  3 and 3
          # 4 : p + q = 5 so 2 and 3
          plst = []
          if ln != 2:
            # add list of 2-digit primes
            plst.append([] if '1' in s and ln == 3 else P2)
          if ln != 4:    
            # add list of 3-digit primes
            plst.append([p for p in P3 if r - p < 1000] if ln == 3 else 
                        [] if '1' in s else P3)
          
          for prms in plst:   
            # look for pairs of primes that sum to r
            for p in prms: 
              q = r - p
              if q < p: break
              # q is also a prime, no repeated digits
              if q not in P: continue
              # between them they use nine digits
              if len(set(s1 := s + str(p) + str(q))) == len(s1) == 9: 
                c += 1
                # output solution
                print(c, f"{N = }", "answer:", sorted([sq, p, q])) 
      

      Like

  • Unknown's avatar

    Jim Randell 10:23 am on 11 March 2025 Permalink | Reply
    Tags:   

    Teaser 2556: Dotty squares 

    From The Sunday Times, 18th September 2011 [link] [link]

    On a piece of paper I have drawn a neat, evenly spaced square array of 36 dots, namely six rows of six.

    If I were to ask you how many squares are formed, you might say just 25, for the array seems to consist of 25 little squares. However, lots of sets of four of the dots form the vertices of a square.

    How many ways are there of choosing four of the dots so that they form the vertices of a square?

    [teaser2556]

     
    • Jim Randell's avatar

      Jim Randell 10:25 am on 11 March 2025 Permalink | Reply

      See: Enigma 1723.

      The number of possible squares on an n × n grid of dots is given by:

      S(n) = n²(n² − 1)/12

      See: OEIS A002415.

      So, with a 6×6 grid of dots:

      S(6) = 36 × 35 / 12 = 105

      Solution: There are 105 sets of 4 points that will make a square.


      Here is a constructive program using some routines from the enigma.py library.

      It runs the N=6 case in 74ms. (Internal runtime is 5.6ms).

      from enigma import (irange, subsets, line_bisect, rdiv, arg, printf)
      
      # number of dots on each side of the square
      N = arg(6, 0, int)
      
      # co-ordinates of the dots [0 .. N-1]
      dots = sorted(subsets(irange(N), size=2, select='M'))
      
      # choose 2 of the dots (will be ordered)
      n = 0
      for (a, b) in subsets(dots, size=2):
        # find the vertices of the other diagonal
        (c, d) = sorted(line_bisect(a, b, div=rdiv))
        if not (c in dots and d in dots): continue
        # eliminate duplicates (otherwise each square will appear twice)
        if (c, d) < (a, b): continue
        # output square
        n += 1
        printf("[{n}: ({a}, {b}) -> ({c}, {d})]")
      
      # output total
      printf("N={N}: {n} squares")
      

      Like

  • Unknown's avatar

    Jim Randell 6:20 am on 9 March 2025 Permalink | Reply
    Tags:   

    Teaser 3259: Something to get your teeth into 

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

    My bicycle has a set of three toothed rings (“chainrings”) attached to the pedal mechanism; I’d chosen these, with different numbers of teeth, from offered values of 30, 41, 45, 47, 58. Correspondingly, the rear wheel mechanism is attached to a set of five toothed rings (“sprockets”), each with no more than 20 teeth. A chain passes over one ring from each set, as selected by the gear controls; at any time the gear ratio is the number of teeth on the selected chainring divided by the number of teeth on the selected sprocket.

    There are exactly three pairs of chainring/sprocket combinations that give gear ratios whose difference equals one divided by a whole number more than 100.

    What are the numbers of teeth on the sprockets, in ascending order?

    [teaser3259]

     
    • Jim Randell's avatar

      Jim Randell 6:31 am on 9 March 2025 Permalink | Reply

      Here is a brute force solution.

      It runs in 1.7s.

      from enigma import (Rational, irange, subsets, cproduct, first, printf)
      
      Q = Rational()
      
      # check a collection of chainrings and sprockets
      # for differences in ratios of 1/N where N > 100
      def check(xs, ys):
        # find the gear ratios
        rs = list(Q(x, y) for (x, y) in cproduct([xs, ys]))
        # find differences that are 1/N
        for (a, b) in subsets(rs, size=2):
          d = abs(a - b)
          if d.numerator == 1 and d.denominator > 100:
            yield (a, b)
      
      # check possible chainrings and sprockets
      xss = subsets((30, 41, 45, 47, 58), size=3)  # 3 chainrings
      yss = subsets(irange(5, 20), size=5)  # 5 sprockets
      for (xs, ys) in cproduct([xss, yss]):
        # look for exactly 3 1/N differences
        ds = first(check(xs, ys), count=4)
        if len(ds) == 3:
          printf("chainrings = {xs}, sprockets = {ys}")
      

      Solution: The teeth on the sprockets are: 12, 13, 14, 16, 17.

      And the teeth on the chainring are: 41, 47, 58.

      And the three gear combinations are:

      58/16 − 47/13 = 1/104
      47/16 − 41/14 = 1/112
      41/12 − 58/17 = 1/204


      There are 6 pairs of gear ratios that give the required 1/N values (and they all give different values for N):

      58/16 − 47/13 = 1/104
      41/10 − 45/11 = 1/110
      47/16 − 41/14 = 1/112
      58/18 − 45/14 = 1/126
      41/15 − 30/11 = 1/165
      41/12 − 58/17 = 1/204

      But the only set of 3 N values that gives ≤ 3 numerators and ≤ 5 denominators is: 104, 112, 204, and this set gives exactly 3 numerators and exactly 5 denominators, so the set of cogs is fully determined.

      Like

      • Jim Randell's avatar

        Jim Randell 9:25 am on 9 March 2025 Permalink | Reply

        Here is a much faster approach. It looks for possible 1/N differences (with N > 100), and then assembles possible collections of chainrings and sprockets from 3 of the differences. This gives candidate sets of cogs, and fortunately there is only one that does not exceed the required numbers of chainrings and sprockets).

        It runs in 68ms. (Internal runtime is 4.7ms).

        from enigma import (
          irange, cproduct, subsets, fraction, ordered, flatten, unzip, diff, printf
        )
        
        # possible cogs
        ns = [30, 41, 45, 47, 58]
        ds = list(irange(5, 20))
        
        # find pairs with a difference that is 1/N, N > 100
        pairs = list()
        for ((a, b), (c, d)) in subsets(cproduct([ns, ds]), size=2):
          (p, q) = fraction(abs(a * d - b * c), b * d)
          if p == 1 and q > 100:
            pairs.append(ordered((a, b), (c, d)))
            printf("[abs({a}/{b} - {c}/{d}) -> {p}/{q}]")
        
        # check if pair <p> can be formed from <ns> and <ds>
        check = lambda p, ns, ds: all(n in ns and d in ds for (n, d) in p)
        
        # now choose 3 pairs
        for ps in subsets(pairs, size=3):
          # collect the numerators (chainrings) and denominators (sprockets)
          (cs, ss) = (sorted(set(s)) for s in unzip(flatten(ps)))
          # there are only 3 different chainrings, and 5 different sprockets
          if len(cs) > 3 or len(ss) > 5: continue
        
          # check no additional pairs can be formed
          if any(check(p, cs, ss) for p in diff(pairs, ps)): continue
        
          # output candidate solutions
          printf("chainrings = {cs}, sprockets = {ss}")
          printf("-> pairs = {ps}")
        

        Potentially the program could produce incomplete candidate sets of cogs. However, it turns out that only a single candidate solution is produced, and it is complete, so it doesn’t seem worth adding the code that completes set of cogs (which would be added at line 26), as it wouldn’t get called.

        Like

  • Unknown's avatar

    Jim Randell 9:35 am on 4 March 2025 Permalink | Reply
    Tags:   

    Teaser 2446: [Shuffling cards] 

    From The Sunday Times, 9th August 2009 [link]

    Old Professor Shufflebottom buys a fresh pack of 52 standard playing cards. He has studied various shuffles and knows that, for any shuffle or rearrangement, if he repeatedly performs it, the cards will eventually be returned to their original order. Of the astronomical number of different shuffles possible (1, 2, 3, …, 52), he chooses one requiring the greatest number of repeated shuffles to return the cards to their original order.

    How many times must that shuffle be done before the cards are returned to their original order?

    This puzzle was originally published with no title.

    [teaser2446]

     
    • Jim Randell's avatar

      Jim Randell 9:36 am on 4 March 2025 Permalink | Reply

      By “shuffle” I think we are meant to assume a mathematical permutation of the sequence of 52 cards. (i.e. each time a shuffle is applied to a deck of 52 cards, the top card always ends up in the same position in the shuffled deck, as does the second card, and so on). (See: [ @wikipedia ]

      Maybe it could have read:

      Professor Shufflebottom has devised a machine that shuffles a standard pack of 52 playing cards.

      The machine is configured by setting 52 dials. Each is set with a different number from 1 to 52. The first dial controls the position that the top card in the starting pack will appear in the shuffled pack. The second dial dial controls the position the second card will appear in the shuffled pack, and so on, until all 52 dials are set.

      Professor Shufflebottom sets the dials such that the number of repeated shuffles to return the cards to their original order is as large as possible.

      What is that number of repeated shuffles?


      So the shuffle can be described as a permutation on 52-sequences.

      Any permutation can be broken down into a combination of disjoint cycles, and then the order of the permutation (which is the number of times it needs to be applied for all elements to return to their original positions) is the lowest common multiple of the lengths of the cycles.

      So we need a number of cycles, with a total length of 52, from which we can calculate the order of a permutation consisting of cycles of those lengths. And we are looking for the cycle lengths which give a maximal order.

      The following Python program runs in 631ms (using PyPy).

      from enigma import (Accumulator, irange, decompose, mlcm, arg, printf)
      
      N = arg(52, 0, int)
      printf("[N = {N}]")
      
      # find maximal orders
      r = Accumulator(fn=max, collect=1)
      
      # consider decompositions into 1 .. N tuples
      for k in irange(1, N):
        for ns in decompose(N, k, increasing=1, sep=0, min_v=1):
          # calculate the order
          m = mlcm(*ns)
          r.accumulate_data(m, ns)
      
      printf("max order = {r.value} [from {r.count} decompositions]")
      for ns in r.data:
        printf("  cycles = {ns}")
      

      Solution: The shuffle must be done 180180 times.

      Possible cycle lengths are:

      (3, 4, 5, 7, 9, 11, 13) → sum = 52; lcm = 180180
      (1, 2, 4, 5, 7, 9, 11, 13) → sum = 52; lcm = 180180
      (1, 1, 1, 4, 5, 7, 9, 11, 13) → sum = 52; lcm = 180180

      See also: OEIS A000793.

      Like

      • Jim Randell's avatar

        Jim Randell 9:28 am on 7 March 2025 Permalink | Reply

        Here is a solution that finds the maximal order, without examining all possible decompositions, and allows much larger numbers to be determined.

        It is based on the code given in the OEIS entry, and uses the fact that a maximal decomposition can be constructed that consists of powers of primes, or 1.

        It has an internal runtime of 138µs.

        from enigma import (primes, irange, arg, printf)
        
        N = arg(52, 0, int)
        printf("[N = {N}]")
        
        # compute terms 0 .. n of the landau function
        def landau(n):
          n += 1
          g = [1] * (n + 1)
          for p in primes.between(2, n):
            for j in irange(n, p, step=-1):
              (v, pp) = (g[j], p)
              while pp < j:
                v = max((pp if j == pp else g[j - pp] * pp), v)
                pp *= p
              g[j] = v
          g.pop(0)
          return g
        
        g = landau(N)
        #printf("g[0..{N}] = {g}")
        printf("max order = {r}", r=g[N])
        

        Like

  • Unknown's avatar

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

    Teaser 3258: On grid 

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

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

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

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

    [teaser3258]

     
    • Jim Randell's avatar

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

      Here’s a brute force solution.

      It runs in 177ms (using PyPy).

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

      Solution: The maximum possible points total is: 27.

      Here is a grid that scores 27:

      The scoring numbers are:

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

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

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

      Like

    • Frits's avatar

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

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

      Like

    • Ruud's avatar

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

      Even more brute force:

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

      Liked by 1 person

    • Frits's avatar

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

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

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

      Like

  • Unknown's avatar

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

    Teaser 2548: Planetary line 

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

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

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

    How long must they wait?

    In honour of the current planetary parade.

    [teaser2548]

     
    • Jim Randell's avatar

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

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

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

      t/x = t/y + 1

      t = x.y / |x − y|

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

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

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

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

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

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

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

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

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

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

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

      They are aligned along 90°/270°.

      Like

  • Unknown's avatar

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

    Teaser 2504: [Hidden crosses] 

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

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

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

    How many black cards have a cross on them?

    This puzzle was originally published with no title.

    [teaser2504]

     
    • Jim Randell's avatar

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

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

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

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

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

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

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

      Like

    • ruudvanderham's avatar

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

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

      Like

  • Unknown's avatar

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

    Teaser 3257: Powers of deduction 

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

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

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

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

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

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

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

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

    How many room cards is Mycroft holding?

    [teaser3257]

     
    • Jim Randell's avatar

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

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

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

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

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

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

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

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

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

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

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

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

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

      Solution: Mycroft is holding no room cards.

      Like

      • Jim Randell's avatar

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

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

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

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

        Like

  • Unknown's avatar

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

    Teaser 2585: Easter Teaser 

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

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

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

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

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

    [teaser2585]

     
    • Jim Randell's avatar

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

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

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

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

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

      And the sum of all possible arrangements is 226666440.


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

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

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

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

      total = 34 × 6666660 = 226666440

      which consists entirely of even digits.

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

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

      Like

  • Unknown's avatar

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

    Teaser 2567: A close thing 

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

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

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

    How many goals did the league champions score in total?

    [teaser2567]

     
    • Jim Randell's avatar

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

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

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

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

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

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

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

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

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

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

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

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

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

      Solution: The league champions scored 11 goals in total.


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

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

      Here is an example set of matches:

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

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

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

      Like

    • Frits's avatar

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

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

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

      Like

    • Frits's avatar

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

      A similar approach. It takes 9 seconds to run.

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

      Like

  • Unknown's avatar

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

    Teaser 3256: Domino-do 

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

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

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

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

    [teaser3256]

     
    • Jim Randell's avatar

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

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

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

      This Python 3 program runs in 625ms.

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

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

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

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

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

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

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

      The unused even domino is [2-6].

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

      Like

      • Jim Randell's avatar

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

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

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

        It has an internal runtime of 2.5ms.

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

        Liked by 1 person

    • Frits's avatar

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

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

      Like

    • Frits's avatar

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

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

      Like

  • Unknown's avatar

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

    Teaser 2473: [Valentines cards] 

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

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

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

    This puzzle was originally published with no title.

    [teaser2473]

     
    • Jim Randell's avatar

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

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

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

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

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

      The full list is:

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

      Like

  • Unknown's avatar

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

    Teaser 2562: Chez nous 

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

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

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

    What is my house number?

    [teaser2562]

     
    • Jim Randell's avatar

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

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

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

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

      Solution: Your house number is 112.

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

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

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

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

      Like

  • Unknown's avatar

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

    Teaser 3255: A square for all digits 

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

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

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

    [teaser3255]

     
    • Jim Randell's avatar

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

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

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

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

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

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

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

      Like

      • Jim Randell's avatar

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

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

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

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

        Liked by 1 person

    • Frits's avatar

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

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

      Like

  • Unknown's avatar

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

    Teaser 2575: More teams 

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

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

    How many teams are there now in the league?

    [teaser2575]

     
    • Jim Randell's avatar

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

      The number of teams goes:

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

      where n is a 2-digit number.

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

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

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

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

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

      This Python program looks for the first possible solution.

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

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

      Solution: There are now 17 teams in the league.

      And so there are 272 matches in the current season.

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

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

      Like

      • Frits's avatar

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

        Further analysis (solving a quadratic equation) leads to :

        c = (5a + 1) / 3

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

        Like

        • John Crabtree's avatar

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

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

          Like

  • Unknown's avatar

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

    Teaser 2571: Dog show 

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

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

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

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

    Who won, and what breed was their dog?

    [teaser2571]

     
    • Jim Randell's avatar

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

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

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

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

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

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

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

      The line-up of the competitors was:

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

      Like

  • Unknown's avatar

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

    Teaser 3254: Pizza pans 

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

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

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

    What was the radius of the smallest pizza?

    [teaser3254]

     
    • Jim Randell's avatar

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

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

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

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

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

      One of the possible arrangements looks like this:

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

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

      Like

    • Frits's avatar

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

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

      Like

  • Unknown's avatar

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

    Teaser 2581: Resplendency 

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

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

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

    [teaser2581]

     
    • Jim Randell's avatar

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

      If:

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

      we have:

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

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

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

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

      Solution: There are 24 boys and 8 girls.

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

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

      Like

      • Ruud's avatar

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

        Very straightforward:

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

        Like

    • GeoffR's avatar

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

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

      Like

  • Unknown's avatar

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

    Teaser 2582: Anyone for tennis? 

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

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

    Which of the eight girls played tennis?

    [teaser2582]

     
    • Jim Randell's avatar

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

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

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

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

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

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

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

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

      The initial choices made are:

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

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

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

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

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

      Like

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

Why are you reporting this comment?

Report type
Design a site like this with WordPress.com
Get started