Updates from November, 2022 Toggle Comment Threads | Keyboard Shortcuts

  • Unknown's avatar

    Jim Randell 4:22 pm on 18 November 2022 Permalink | Reply
    Tags:   

    Teaser 3139: Checkmate 

    From The Sunday Times, 20th November 2022 [link] [link]

    Our chess club is divided into sections, with each section having the same number of players. The two oldest members will soon be retiring from playing and we will change the number of sections. The number of sections will change by one and so will the number of players in each section, but all the sections will still have the same number of players. This will result in there being a grand total of 63 fewer matches per year if each member plays all the other members of their section once per year.

    How many players are in each section at the moment?

    [teaser3139]

     
    • Jim Randell's avatar

      Jim Randell 4:56 pm on 18 November 2022 Permalink | Reply

      If the players are split into k sections, each consisting of n players, then the number of matches in each section is C(n, 2), and the total number of matches is m = k.C(n, 2).

      We can use some analysis to simplify the puzzle to two cases, one of which has no solution for positive integers, and the other that gives us the required solution.

      However, here is a constructive Python program that considers the total numbers of players t (up to 2000), and generates the possible (n, k, m) values for each candidate t value. It then looks for two t values that differ by 2, and give n and k values that differ by 1, and m values that differ by 63.

      This Python program runs in 85ms.

      Run: [ @replit ]

      from enigma import (irange, divisors_pairs, tri, cproduct, tuples, printf)
      
      # generate (k, n, m) values for t players
      def generate(t):
        # divide them into k sections of n players each
        for (k, n) in divisors_pairs(t, every=1):
          if n < 2: continue
          # calculate the total number of matches
          m = k * tri(n - 1)
          yield (t, k, n, m)
      
      # solve for numbers of players that differ by 2
      def solve(N):
        for (zs, ys, xs) in tuples((list(generate(t)) for t in irange(1, N)), 3):
          for ((t, k, n, m), (t_, k_, n_, m_)) in cproduct([xs, zs]):
            # check the differences in the corresponding k, n, m values
            if abs(k - k_) == 1 and abs(n - n_) == 1 and m - m_ == 63:
              # output solution
              printf("{k} sections of {n} players (= {m} matches) -> {k_} sections of {n_} players (= {m_} matches)")
      
      # solve the puzzle (up to 2000 players)
      solve(2000)
      

      Solution: There are 10 players in each section.

      The club starts with 110 players, formed into 11 sections of 10 players each. There are 45 matches among the players in each section, and 495 matches in total.

      When 2 players leave there are 108 players remaining. These are formed into 12 sections of 9 players each. There are 36 matches among the players in each section, and 432 matches in total. This is 63 fewer matches.


      Manually from:

      (n ± 1)(k ± 1) = nk − 2

      we can see that (for n + k > 3) there are 2 cases:

      [Case 1] (n, k)(n + 1, k − 1)k = n − 1.

      k C(n, 2) − (k − 1) C(n + 1, 2) = 63
      n[(n − 1)(n − 1) − (n − 2)(n + 1)] = 126
      n(3 − n) = 126
      n² − 3n + 126 = 0

      Which has no solution in positive integers.

      [Case 2] (n, k)(n − 1, k + 1)k = n + 1.

      k C(n, 2) − (k + 1) C(n − 1, 2) = 63
      (n − 1)[(n + 1)n − (n + 2)(n − 2)] = 126
      (n − 1)(n + 4) = 126
      n² + 3n − 130 = 0
      (n − 10)(n + 13) = 0
      n = 10
      k = 11

      And so there are 11 sections, each with 10 players.

      Like

    • GeoffR's avatar

      GeoffR 3:49 pm on 19 November 2022 Permalink | Reply

      
      % A Solution in MiniZinc
      include "globals.mzn";
      
      % Assumed No. of persons in each section before and after retirement
      var 2..25:persB; var 1..23:persA;
      
      % Assumed No. of sections before and after retirement
      var 2..25:sectB; var 1..25:sectA;
      
      % No of matches before and after retirement
      % UB = 25 * (25 * 24//2)
      var 1..7500: matchesB; var 1..7500: matchesA;
      
      % Change in number of sections is one due to two people retiring
      constraint abs(sectA - sectB) == 1;
      
      % Change in total number of persons is two
      constraint sectB * persB - 2 == sectA * persA
      /\ abs (persB - persA) == 1;
      
      % Find total number of matches before and after two people retire
      constraint 2 * matchesB = sectB * persB * (persB - 1);
      constraint 2 * matchesA = sectA * persA * (persA - 1);
      
      % Change in number of total matches is given as 63
      constraint matchesB - matchesA == 63;
      
      solve satisfy;
      
      output ["Persons per section before two retire = " ++ show(persB)
      ++ "\n" ++ "Number of sections before two retire = " ++ show(sectB)
      ++ "\n" ++ "Persons per section after two retire = " ++ show(persA)
      ++ "\n" ++ "Number of sections after two retire = " ++ show(sectA) ];
      
      
      

      Like

    • GeoffR's avatar

      GeoffR 6:21 pm on 19 November 2022 Permalink | Reply

      
      # Assumed max no players and sections is 24 for both groups
      # Suffix 'B' for before matches and 'A' for after matches
      for playersB in range(1, 25):
        for sectionsB in range(1, 25):
          before_matches = sectionsB * (playersB * (playersB - 1) // 2)
          for playersA in range(1, 25):
            # The number of players will change by one 
            if abs(playersA - playersB) != 1:
              continue
            for sectionsA in range(1, 25):
              # The number of sections will change by one 
              if abs(sectionsA - sectionsB) != 1:
                continue
              # two players retire
              if sectionsB * playersB - 2 != sectionsA * playersA:
                continue
              after_matches = sectionsA * (playersA * (playersA - 1) // 2)
              
              # Differences in total number of matches
              if before_matches - after_matches != 63:
                continue
              # output player and sections data
              print(f"Players before two retire = {playersB} No.")
              print(f"Sections  before two retire = {sectionsB} No.")
              print(f"Players after two retire = {playersA} No.") 
              print(f"Sections  after two retire = {sectionsA} No.")
      
      

      Like

  • Unknown's avatar

    Jim Randell 4:36 pm on 11 November 2022 Permalink | Reply
    Tags:   

    Teaser 3138: Primus inter impares 

    From The Sunday Times, 13th November 2022 [link] [link]

    Just nine Prime Ministers held office in the Kingdom of Primea during the 20th century. No two Prime Ministers held office at the same time, none had more than one period in office, and the gap between successive Prime Ministers’ terms was never more than a month. Each held office for a period of time in which the number of whole years was a different prime number (e.g. holding office from 1910 to 1915 could cover four or five whole years) and no Prime Minister served for more than 30 years. Appropriately, they all took office in prime years, but there was no change of Prime Minister in 1973.

    In which years during the 20th century did new Prime Ministers in Primea take up office?

    [teaser3138]

     
    • Jim Randell's avatar

      Jim Randell 5:12 pm on 11 November 2022 Permalink | Reply

      This Python program runs in 51ms. (Internal runtime is 554µs).

      Run: [ @replit ]

      from enigma import (primes, append, tuples, printf)
      
      # possible term lengths
      terms = set(primes.between(0, 30))
      
      # select <k> years from <years>
      # return (<start-years>, <term-lengths>)
      def solve(k, years, ys=[], ts=[]):
        # are we done?
        if k == 0:
          # no-one served more than 30 years
          if ys and ys[-1] > 1968:
            yield (ys, ts)
        elif not k > len(years):
          # choose the next year
          for (i, y) in enumerate(years):
            # consider possible term lengths
            g = y - ys[-1]
            for t in terms.intersection({g, g - 1}):
              if t in ts: continue
              # solve for the remaining years
              yield from solve(k - 1, years[i + 1:], append(ys, y), append(ts, t))
      
      # consider the start year of the incumbent at the start of the century
      for y0 in primes.between(1870, 1901):
        # fill out the 8 starting years in the 20th century
        years = list(primes.between(max(y0 + 1, 1901), 2000))
        years.remove(1973)  # but not 1973
        for (ys, ts) in solve(8, years, [y0]):
          # output solution
          for ((y1, y2), t) in zip(tuples(ys, 2), ts):
            printf("{y1} - {y2} = {t} years")
          printf("{y2} - .... = ? years")
          printf()
      

      Solution: The years are: 1901, 1907, 1931, 1949, 1979, 1993, 1997, 1999.

      The incumbent at the start of the 20th century began office in 1889, so we have:

      1. 1889 – 1901 = 11 years
      2. 1901 – 1907 = 5 years
      3. 1907 – 1931 = 23 years
      4. 1931 – 1949 = 17 years
      5. 1949 – 1979 = 29 years
      6. 1979 – 1993 = 13 years
      7. 1993 – 1997 = 3 years
      8. 1997 – 1999 = 2 years
      9. 1999 – …. = (7 or 19 years)

      There are two primes left that could correspond to the length of the term of the incumbent at the end of the 20th century. Note that it is not required that the term of the successor (if any) starts in a prime year.

      Like

      • Frits's avatar

        Frits 1:24 pm on 12 November 2022 Permalink | Reply

        @Jim, as I don’t have access to a PC I can’t publish/test a program myself.

        I think a check is needed to see if the last Prime Minister can serve a prime number of years and which ends after the 20th century (if high prime numbers already have been used this may not be possible anymore).

        Like

        • Hugh Casement's avatar

          Hugh Casement 7:17 am on 13 November 2022 Permalink | Reply

          Frits, the wording of the puzzle is vague on that score, but I think if we do allow the last term of office to extend beyond the end of the century then we get multiple solutions. Jim (line 25) would seem to agree with me that we need to start looking before the beginning of the century.

          Like

        • Jim Randell's avatar

          Jim Randell 8:18 am on 13 November 2022 Permalink | Reply

          @Frits: I did wonder about that. I check the final PM of the 20th century does not have a term that is longer than 30 years (line 12), and when I saw that there was only a single possible solution I didn’t worry about the actual length of the term, as it starts very close to the end of the century, and there are 2 unused primes available.

          Here is the code with an extra check to ensure the final term length is possible. (I also include code to specify the first and last years of the 20th century. I am using the “strict” definition 1901-2000, but you get the same answer with the “popular” usage of 1900-1999).

          Run: [ @replit ]

          from enigma import (primes, append, tuples, printf)
          
          # first/last years for 20th century
          (first, last) = (1901, 2000)
          
          # possible term lengths
          terms = set(primes.between(0, 30))
          
          # select <k> years from <years>
          # return (<start-years>, <term-lengths>)
          def solve(k, years, ys=[], ts=[]):
            # are we done?
            if k == 0:
              yield (ys, ts)
            elif not k > len(years):
              # choose the next year
              for (i, y) in enumerate(years):
                # consider possible term lengths
                g = y - ys[-1]
                for t in terms.intersection({g, g - 1}):
                  if t in ts: continue
                  # solve for the remaining years
                  yield from solve(k - 1, years[i + 1:], append(ys, y), append(ts, t))
          
          # consider the start year of the incumbent at the start of the century
          for y0 in primes.between(first - 31, first):
            # fill out the 8 starting years in the 20th century
            years = list(primes.between(max(y0 + 1, first), last))
            years.remove(1973)  # but not 1973
            for (ys, ts) in solve(8, years, [y0]):
              # check possible term lengths for the incumbent at the end of the century
              fs = sorted(t for t in terms.difference(ts) if ys[-1] + t > last)
              if not fs: continue
              # output solution
              for ((y1, y2), t) in zip(tuples(ys, 2), ts):
                printf("{y1} - {y2} = {t} years")
              printf("{y2} - .... = {fs} years")
              printf()
          

          But it seems that it may be possible that the final PM is still in office at the time the puzzle was set (having served less than 30 years so far), so we can’t say what the length of the term will be yet.

          Like

  • Unknown's avatar

    Jim Randell 4:21 pm on 4 November 2022 Permalink | Reply
    Tags:   

    Teaser 3137: Common names 

    From The Sunday Times, 6th November 2022 [link] [link]

    Eight friends met at a party; their ages in whole numbers of years were all different. They were Alan, Cary, James, Lucy, Nick, Ricky, Steve and Victor, with Lucy being the youngest. For each of them the square of their age was a three-figure number consisting of three different digits. Furthermore, for any two of them, the squares of their ages had at least one digit in common precisely when their names had at least one letter in common.

    In alphabetical order of their names, what are the eight ages?

    [teaser3137]

     
    • Jim Randell's avatar

      Jim Randell 4:49 pm on 4 November 2022 Permalink | Reply

      This Python program runs in 61ms. (Internal runtime is 9.2ms).

      Run: [ @replit ]

      from enigma import (irange, sq, nsplit, diff, intersect, append, delete, subsets, printf)
      
      # model a symmetric relation
      class Relation(set):
        # check if x is related to y
        def __call__(self, x, y):
          return (x, y) in self or (y, x) in self
      
      # names
      names = "ALAN CARY JAMES LUCY NICK RICKY STEVE VICTOR".split()
      
      # names are related when they share a letter
      N = Relation((x, y) for (x, y) in subsets(names, size=2) if intersect([x, y]))
      
      # find 3-digits squares with no repeated digits
      sqs = dict()
      for i in irange(10, 31):
        ds = set(nsplit(sq(i)))
        if len(ds) == 3:
          sqs[i] = ds
      
      # values are related when their squares share a digit
      V = Relation((x, y) for ((x, sx), (y, sy)) in subsets(sqs.items(), size=2) if intersect([sx, sy]))
      
      # assign values to remaining names
      # names = remaining names
      # ss = used (name, value) pairs
      # vs = remaining values
      def solve(names, ss, vs):
        if not names:
          yield ss
        elif not len(names) > len(vs):
          # choose a value for the next name
          n = names[0]
          for (i, v) in enumerate(vs):
            # check values have digits in common when names have letters in common
            if all(N(n, n_) == V(v, v_) for (n_, v_) in ss):
              # solve for the remaining names
              yield from solve(names[1:], append(ss, (n, v)), delete(vs, [i]))
      
      # choose an age for LUCY (the youngest)
      n0 = "LUCY"
      ks = sorted(sqs.keys())
      for (i, k) in enumerate(ks):
        # solve for the remaining names
        for ss in solve(diff(names, {n0}), [(n0, k)], ks[i + 1:]):
          # output solution
          for (n, v) in sorted(ss):
            printf("{n} -> {v} [{s}]", s=sq(v))
          printf()
      

      Solution: The ages are: 19, 31, 29, 16, 25, 23, 28, 27.

      With squares in square brackets:

      ALAN = 19 [361]
      CARY = 31 [961]
      JAMES = 29 [841]
      LUCY = 16 [256]
      NICK = 25 [625]
      RICKY = 23 [529]
      STEVE = 28 [784]
      VICTOR = 27 [729]

      Like

    • Paul Byrne's avatar

      Paul Byrne 9:54 pm on 14 November 2022 Permalink | Reply

      Hi Jim
      Thanks for all the good work on these Teasers.
      Love the simplicity of your website and your solutions.
      Re 3137 Teaser
      Is 24 instead of 31 also a correct answer? Keep up the good work!

      Like

      • Jim Randell's avatar

        Jim Randell 9:17 am on 15 November 2022 Permalink | Reply

        @Paul: Thanks for the feedback.

        We can’t swap CARY for 24 in the solution I give above, as 24² = 576, and CARY and JAMES share the letter A, so their squares need to share a digit. But 576 and 841 (= 29²) don’t have any digits in common.

        Like

    • Paul Byrne's avatar

      Paul Byrne 10:21 am on 16 November 2022 Permalink | Reply

      Hi Jim, thank you very much for your response.

      Forgive me I should’ve made myself clearer.
      If Cary becomes 576, and James 784, and Steve 841, can it then work as an alternative?

      Like

      • Jim Randell's avatar

        Jim Randell 12:14 pm on 16 November 2022 Permalink | Reply

        @Paul: My program performs an exhaustive search, so there is only one solution to the puzzle.

        If we had:

        CARY = 24 [576]
        JAMES = 28 [784]
        STEVE = 29 [841]

        Then {LUCY, NICK, RICKY} must correspond to {16 [256], 23 [529], 25 [625]} in some order.

        LUCY is the youngest, so we have:

        LUCY = 16 [256]

        But then ALAN has to have digits in common with CARY [576], LUCY [256], JAMES [784], but not STEVE [841]

        Which means for ALAN we need to find a square with a 7 and a 5 or a 6. The only candidate is 24 [576], but that is already used by CARY, so it is not possible to find a value for ALAN in this scenario.

        Like

    • Paul Byrne's avatar

      Paul Byrne 5:17 pm on 16 November 2022 Permalink | Reply

      Hi Jim
      Alas I’m thwarted!
      Thanks for this and all your efforts.
      They are all appreciated.
      Regards Paul

      Like

  • Unknown's avatar

    Jim Randell 4:32 pm on 28 October 2022 Permalink | Reply
    Tags:   

    Teaser 3136: Fill cells mentally, my dear Watson 

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

    Moriarty’s papers were alight. Holmes memorised a 3×3 grid of different 4-digit values in ascending order as illustrated, from A (lowest) to I (highest), noticing that one numeral wasn’t used. Three cells, isolated from one another (no corner or edge contact), held squares of squares. Three others, similarly isolated, held cubes of non-squares. The other three held squares of non-squares. Holmes told Watson these features, specifying only the lowest value. “Not square”, remarked Watson.

    “True, and many grids match these facts. However, if I told you the positions of the squares of squares in the grid, you could deduce a majority of the other eight values (apart from the lowest one)”, replied Holmes.

    In ascending order, which values did Watson know certainly?

    [teaser3136]

     
    • Jim Randell's avatar

      Jim Randell 5:50 pm on 28 October 2022 Permalink | Reply

      I found 2 scenarios where Watson (when told the smallest number and the positions of the “squares of squares” in the grid) could work out 5 of the other numbers that must be in the grid.

      Since Watson says “Not square” I am assuming we want the scenario where the lowest number in the grid (A) is not a square (i.e. it is a cube of a non-square).

      The following Python program runs in 81ms. (Internal runtime is 29ms).

      Run: [ @replit ]

      from enigma import (
        defaultdict, irange, sq, cb, is_square, cproduct, union, diff,
        subsets, update, peek, intersect, join, printf
      )
      
      # we have 3 disjoint categories of 4-digit numbers:
      # cat1 = "square of a square" (i.e. a power of 4)
      cat1 = list(str(sq(sq(i))) for i in irange(6, 9))
      # cat2 = "cube of a non-square"
      cat2 = list(str(cb(i)) for i in irange(10, 21) if not is_square(i))
      # cat3 = "square of a non-square"
      cat3 = list(str(sq(i)) for i in irange(32, 99) if not is_square(i))
      
      # sets of isolated cells
      iso0 = [(0, 2, 6), (0, 2, 7), (0, 2, 8), (0, 5, 6), (0, 6, 8)] # with cell 0
      isox = [(1, 6, 8), (2, 3, 8), (2, 6, 8)] # without cell 0
      
      # collect candidate grids
      g = defaultdict(list)
      
      # fill out cells <cs> in grid <ns> with values from <cat>
      # making sure digit content (including <ds>) does not exceed 9
      def fill(ns, cs, cat, ds):
        # are we done?
        if not cs:
          yield (ns, ds)
        else:
          # fill out the next cell
          i = cs[0]
          lo = peek((ns[j] for j in irange(i - 1, 0, step=-1) if ns[j]), default='0000')
          hi = peek((ns[j] for j in irange(i + 1, 8) if ns[j]), default='AAAA')
          for n in cat:
            if not (n > lo): continue
            if not (n < hi): break
            ds_ = ds.union(n)
            if not (len(ds_) > 9):
              yield from fill(update(ns, [i], [n]), cs[1:], cat, ds_)
      
      # choose cat1, cat2 cells; cell 0 (A) is not a square
      for (cs1, cs2) in cproduct([isox, iso0]):
        # remaining cells are cat3
        cs = union([cs1, cs2])
        if len(cs) != 6: continue
        cs3 = diff(irange(0, 8), cs)
      
        # fill out the cells
        ns0 = [None] * 9
        # choose cat1 values
        for vs1 in subsets(cat1, size=3):
          ds1 = union(vs1)
          if len(ds1) > 9: continue
          ns1 = update(ns0, cs1, vs1)
      
          # fill out cat2 and cat3 values
          for (ns2, ds2) in fill(ns1, cs2, cat2, ds1):
            for (ns3, ds3) in fill(ns2, cs3, cat3, ds2):
              if len(ds3) == 9:
                # group grids by (<cell 0>, <cat1 positions>)
                g[(ns3[0], cs1)].append(ns3)
      
      # consider the groups
      for (k, vs) in g.items():
        # can we determine at least 5 additional values?
        xs = intersect(vs)
        if len(xs) < 6: continue
      
        # output solution
        (A, cs1) = (k[0], join("ABCDEFGHI"[x] for x in k[1]))
        printf("(A={A}, cat1={cs1}) -> {xs}", xs=join(sorted(xs), sep=", ", enc="()"))
      

      Solution: Watson knew for certain the grid contained: 1331, 2401, 4096, 4913, 5832, 6561.

      Watson was told that the lowest number was 1331, and the “squares of squares” were in positions: C, D, I.

      As 1331 = 11³, Watson can deduce that the cubes are in positions: A, F, G. And the squares of non-squares are in positions: B, E, H.

      And so he can deduce the cubes are: A=1331, F=4913, G=5832. And the powers of 4 are: C=2401, D=4096, I=6561.

      Together these 6 values use all the digits except 7.

      So the grid can be completed with any squares of non-squares that don’t use the digit 7, and fit in the appropriate position.

      The candidates are:

      B = 1369, 1444, 1521, 1600, 1681, 1849, 1936, 2025, 2116, 2209, 2304
      E = 4225, 4356, 4489, 4624, 4900
      H = 5929, 6084, 6241, 6400

      So there are 220 possible grids.

      Here is one possibility:

      red = squares of squares
      blue = cubes of non-squares
      yellow = squares of non-squares

      Like

  • Unknown's avatar

    Jim Randell 4:27 pm on 21 October 2022 Permalink | Reply
    Tags:   

    Teaser 3135: Rhythmic gymnastics 

    From The Sunday Times, 23rd October 2022 [link] [link]

    Skaredahora used three rhythmic patterns of quaver beats in a short, purely percussive, composition. His “Dotykam” rhythm has accents on beats 1, 4, 6, 7, 8, 10 & 11; “Kluc” has accents on beats 1, 8 and one particular beat in between; and “Omacka” has accents on beats 1, 2, 5, 6 & 10. Several percussion instruments are involved, each playing one of the three rhythms, but starting at different times. Overall the patterns overlap, with every beat in the composition being filled by an accent from exactly one of the instruments, and all the patterns finishing by the end of the composition.

    What is the other beat of Kluc, and what is the order of appearance of the rhythmic patterns (e.g. DOOKD)?

    [teaser3135]

     
    • Jim Randell's avatar

      Jim Randell 4:51 pm on 21 October 2022 Permalink | Reply

      This Python program considers possible additional accent positions for pattern K, and then fills out patterns with an accent on every beat, but no overlaps, until there is a continuous sequence of accents.

      It runs in 54ms. (Internal runtime is 372µs).

      Run: [ @replit ]

      from enigma import (irange, inf, update, peek, printf)
      
      # the rhythm patterns
      D = [1, 4, 6, 7, 8, 10, 11]
      K = [1, None, 8]  # a beat in the range [2, 7] is missing
      O = [1, 2, 5, 6, 10]
      
      # disjoint union of two sets (or None)
      def disjoint_union(ss, xs):
        ss = set(ss)
        for x in xs:
          if x in ss: return
          ss.add(x)
        return ss
      
      # fill out patterns from <ps> with an accent on every beat
      # starting from <s>
      def solve(ps, s=1, vs=[], bs=set()):
        # we are done if there are no missing beats
        if bs and len(bs) == max(bs):
          yield vs
        else:
          # find the first missing beat
          b = peek(k for k in irange(s, inf) if k not in bs)
          # and choose a pattern to start here
          for (k, pattern) in ps.items():
            bs_ = disjoint_union(bs, (x + b - 1 for x in pattern))
            if bs_ is not None:
              yield from solve(ps, s + 1, vs + [(k, b)], bs_)
      
      # consider the missing beat in pattern K
      for k in irange(2, 7):
        for vs in solve(dict(D=D, K=update(K, [1], [k]), O=O)):
          printf("k={k} vs={vs}")
      

      Solution: Kluc has an additional accent on beat 5. The order of the patterns as: OOKKDDK.

      The composition lasts for 33 beats (or a multiple of 33 beats). The patterns being:

      O @ 1: (1, 2, 5, 6, 10)
      O @ 3: (3, 4, 7, 8, 12)
      K @ 9: (9, 13, 16)
      K @ 11: (11, 15, 18)
      D @ 14: (14, 17, 19, 20, 21, 23, 24)
      D @ 22: (22, 25, 27, 28, 29, 31, 32)
      K @ 26: (26, 30, 33)

      Which covers all beats from 1 – 33.

      In order for the composition to end after 33 beats with each beat being accented by exactly one of the instruments, we need pattern K to end after the last accented beat. Pattern D can have 0 or 1 non-accented beats after the last accented beat. And pattern O could have up to 21 non-accented beats after the last accented beat.

      Like

    • NigelR's avatar

      NigelR 10:47 am on 25 October 2022 Permalink | Reply

      Less elegant than Jim’s solution, but gets the job done!

      rhy = [[1, 4, 6, 7, 8, 10, 11],[1, 2, 8],[1, 2, 5, 6,10]] 
      patts=('DO','KL','OM')
      ntotxt = lambda k : [[patts[j[0]],j[1]] for j in k]  #makes print out more intelligible using labels
      score = lambda k : sorted([y+x[1]-1 for x in k for y in rhy[x[0]]]) # creates list of beat numbers filled
      gap = lambda k : sorted([x for x in range(1,len(k)+1) if x not in k]) # identifies gaps in beat sequence
      for n in range(2,8): # iterate for missing value in KL
          rhy[1][1] = n
          seq=[[0,1]] #Initialise seq.  Entries in seq are in format:[pattern no.,starting beat]
          while seq:
              beats=score(seq)
              if dup:=len(beats)-len(set(beats)): #if overlap of beats found, go back in sequence
                  while seq and seq[-1][0]==2:
                      seq.pop()                
                  if not seq:break
                  seq[-1][0]+=1   #try next pattern in exisiting sequence
              else: #no overlaps in current sequence
                  if g:=gap(beats):  # start next pattern at first gap in sequence
                      seq.append([0,g[0]])                
                  else: # if no gap and no overlaps, solution has been found
                      print('solution found: ',ntotxt(seq), '   n=',n)
                      exit()                         
          print('options exhausted for n='         ,n)
      

      Like

  • Unknown's avatar

    Jim Randell 4:00 pm on 15 October 2022 Permalink | Reply
    Tags: ,   

    Teaser 3134: Stringing along [revised] 

    From The Sunday Times, 16th October 2022 [link] [link]

    An artist hammered thin nails from a pack of 40 into a board to form the perimeter of a rectangle with a 1 cm gap between adjacent nails. He created a work of art by stringing a long piece of wire from one nail to another, such that no section of wire was parallel to an edge of the rectangle. The wire started and ended at two different nails, no nail was used more than once and the length of the wire was a whole number of cm. No longer wire was possible that satisfied the above conditions.

    What were the dimensions of the rectangle and the length of the wire chain (in cm)?

    This is a revised version of the puzzle posted as Teaser 3134. The underlined text has been changed from the previously published version on The Sunday Times website.

    This is the version of the puzzle that appeared in the printed newspaper.

    [teaser3134]

     
    • Jim Randell's avatar

      Jim Randell 4:01 pm on 15 October 2022 Permalink | Reply

      Having solved the (more general) previously published version of this puzzle [link], it is a simple case of adapting my program to account for the additional condition.

      For this version I am assuming we want to find the maximum achievable total length of wire between the nails that can be made using a rectangle constructed from up to 40 nails, with the wire strung between nails where no nail is used more than once and consecutive nails are on different edges of the rectangle (i.e. each linear section of wire must cross part of the interior of the rectangle, and be a whole number of centimetres long), and no linear section of wire is parallel to the edges of the rectangle.

      You can think of the wire being available in various stiff lengths with looped ends, but these individual links can only ever fit between nails that are a whole number of centimetres apart. We wish to make a chain of these wire links, where each link goes from the end of the previous link to a currently unoccupied nail. What is the longest possible chain that can be made, on a rectangle whose perimeter is constructed from no more than 40 nails spaced 1 cm apart, where each link must cross the interior of the rectangle not parallel to the edges of the rectangle, and the start and end of the complete chain are on different nails.

      This Python program runs in 104ms.

      Run: [ @replit ]

      from enigma import (irange, ihypot, chain, Accumulator, printf)
      
      # generate possible <x> by <y> rectangles, using (up to) <n> nails
      def generate(n):
        for y in irange(2, n // 4):
          for x in irange(2, (n - 2 * y) // 2):
            yield (x, y)
      
      # find paths by adding nails to <vs>
      def solve(x, y, nails, vs, d=0):
        # return the current path
        yield (d, vs)
        # choose the next nail
        (x1, y1) = vs[-1]
        for v in nails:
          (x2, y2) = v
          (dx, dy) = (x2 - x1, y2 - y1)
          if dx == 0 or dy == 0: continue
          dd = ihypot(dx, dy)
          if dd is not None:
            yield from solve(x, y, nails.difference([v]), vs + [v], d + dd)
      
      # collect maximal distance paths
      r = Accumulator(fn=max, collect=1)
      
      # consider possible rectangles
      for (x, y) in generate(40):
        # collect possible edge nails
        nails = set()
        for i in irange(0, x - 1):
          nails.update([(i, 0), (x - i, y)])
        for i in irange(0, y - 1):
          nails.update([(0, y - i), (x, i)])
        # start in the bottom/left quadrant
        botm = ((i, 0) for i in irange(0, x // 2))
        left = ((0, i) for i in irange(1, y // 2))
        for v in chain(botm, left):
          for (d, vs) in solve(x, y, nails.difference([v]), [v]):
            r.accumulate_data(d, (x, y, vs))
      
      # output solution(s)
      printf("max wire length = {r.value} cm")
      for (x, y, vs) in r.data:
        printf("-> {x} cm by {y} cm grid {vs}")
      

      Solution: The rectangle was 12 cm × 8 cm. The total length of wire used was 102 cm.


      Here is a diagram of the maximal length layout:

      Like

      • Jim Randell's avatar

        Jim Randell 11:28 am on 28 October 2022 Permalink | Reply

        In my program above, in order to get a total distance that is a whole number we require the individual segments between consecutive pins in the stringing to be a whole number.

        This is a reasonable assumption. As we can show that if we have a collection of positive integers, and at least one of them has a square root that is irrational, then the sum of the square roots of the entire collection is irrational. (See below [*]).

        However, this does not mean we cannot construct stringings that are very close to an integer value using segments that are not themselves integers. And this lets us find solutions that are much longer than the published solution.

        For example here is a stringing on the 12cm × 8 cm grid that has a total length of 440.999925 cm (i.e. 750 nanometres short of 441 cm). It would be practically impossible to tell this apart from a 441 cm length of wire.

        Note that in this construction every pin is visited.

        If we want more accuracy we can get within 6nm of 438 cm.


        [*] The core of the proof is:

        Suppose x, y are integers, such that at least one of √x and √y are irrational.

        Then (x − y) is also an integer, and:

        (x − y) = (√x + √y)(√x − √y)

        If (√x + √y) is rational (= p), then:

        (√x − √y) = (x − y)/(√x + √y) is also rational (= q)

        But then:

        √x = (p + q)/2 is rational; and
        √y = (p − q)/2 is rational

        Which is a contradiction, hence the sum (√x + √y) is irrational.

        Like

    • Hugh Casement's avatar

      Hugh Casement 10:16 am on 16 October 2022 Permalink | Reply

      The change in wording makes quite a difference to the total length, at least with the solution I worked out.

      The puzzle does not seem to be well thought out. That the nails are thin clearly means that we are to ignore their diameter (and the thickness of the wire). But with centimetres as the unit they are not nails at all but pins, and it’s fine fuse wire. Inches would have been somewhat less unrealistic.

      I also feel that if the “artist” had allowed himself more nails, the resulting pattern could have been more interesting. For example, 12 occurs in the Pythagorean triangles (9, 12, 15) and (12, 16, 20) as well as (5, 12, 13), which would presumably give more scope. But the length + width of the rectangle must not exceed 20, so we are much restricted.

      Like

      • Jim Randell's avatar

        Jim Randell 12:03 pm on 17 October 2022 Permalink | Reply

        @Hugh: Yes for the original wording I found a maximal solution with 24 links. For this revised version there are only 10 links. The rectangle was the same in both cases.

        Like

  • Unknown's avatar

    Jim Randell 4:00 pm on 7 October 2022 Permalink | Reply
    Tags:   

    Teaser 3133: Primary road network 

    From The Sunday Times, 9th October 2022 [link] [link]

    I was recently studying a large map that showed all the towns and major roads in a country. Every town had at least one road leading to it and every road led from one town to another. The roads only met at towns and all joined together to make a network with lots of blank areas in between, which I happily coloured in, needing just four different colours.

    I counted up the number of areas (excluding the area around the outside of the network) and the number of towns, and discovered that both numbers were prime. Also, when I took these two numbers with the number of roads, the three numbers together used every digit from 0 to 9 precisely once.

    In increasing order, what were the three numbers?

    [teaser3133]

     
    • Jim Randell's avatar

      Jim Randell 4:31 pm on 7 October 2022 Permalink | Reply

      This is an exercise in the properties of planar graphs [@wikipedia].

      We can use Euler’s formula:

      V – E + F = 2

      where:

      V = the number of vertices in graph (= the number of towns on the map)
      E = the number of edges in the graph (= the number of roads on the map)
      F = the number of enclosed regions in the graph (including the region around the graph)
      F = (the number of enclosed/coloured regions on the map) + 1

      This Python program runs in 93ms. (Internal run time is 38.6ms).

      Run: [ @replit ]

      from enigma import (primes, distinct_chars, ordered, printf)
      
      # consider V = the number of towns, it is prime
      for V in primes.between(3, 1000):
        if distinct_chars(V) is None: continue
      
        # consider A = the number of enclosed areas, it is also prime
        for A in primes.between(5, 2 * V - 5):
          if distinct_chars(V, A) is None: continue
      
          # calculate E = the number of roads
          E = V + A - 1
          if E > 3 * V - 6: continue
          if distinct_chars(V, A, E) != 10: continue
      
          # output solution
          ns = ordered(V, A, E)
          printf("{ns} [V={V} A={A} E={E}]")
      

      Solution: The three numbers are: 607, 829, 1435.


      We can construct a viable map as follows:

      Consider the following diagram:

      If the central area is an odd n-gon, coloured with the first colour, then it is surrounded by n regions, and as n is odd we require an additional 3 colours to colour these. So at least 4 colours are required, and the 4-colour theorem tells that we don’t need more than 4 colours.

      And together the central and surrounding areas contribute:

      2n vertices
      3n edges
      (n + 1) areas

      And adding p petals (we may add multiple layers of elongated petals if necessary), adds:

      0 vertices
      p edges
      p areas

      And a stalk of length s adds:

      s vertices
      s edges
      0 areas

      So using: n = 413, p = 193, s = 3, gives a graph with:

      826 + 0 + 3 = 829 vertices
      1239 + 193 + 3 = 1435 edges
      414 + 193 + 0 = 607 areas

      Which provides a viable map.

      Like

      • Jim Randell's avatar

        Jim Randell 8:33 am on 8 October 2022 Permalink | Reply

        Using the formula:

        E = p + q − 1

        where p and q are primes, and (E, p, q) is pandigital, we see that E must be 4-digits, so p and q can only be (2, 4) or (3, 3) digits.

        And without further planarity checks there is only one candidate solution, which we can find using the [[ SubstitutedExpression() ]] solver from the enigma.py library.

        The following Python program runs in 83ms. (Internal runtime is 28.4ms)

        Run: [ @replit ]

        from enigma import (SubstitutedExpression, sprintf as f, printf)
        
        # symbols for the digits
        (terms, result) = ("ABCDEF", "GHIJ")
        # consider (2,4) and (3, 3) digit terms
        for i in (2, 3):
          (x, y) = (terms[:i], terms[i:])
          exprs = [ f("is_prime({x})"), f("is_prime({y})"), f("{x} + {y} - 1 = {result}") ]
          if len(x) == len(y): exprs.append(f("{x} < {y}", x=x[0], y=y[0]))
          p = SubstitutedExpression(exprs, answer=f("({x}, {y}, {result})"))
          for ans in p.answers(verbose=0):
            printf("{ans}")
        

        Like

    • Frits's avatar

      Frits 12:31 am on 8 October 2022 Permalink | Reply

      This program considers up to 9999 number of towns.
      I stopped programming when the run time got under 10ms.

      NB The final P4 list is always logically empty, I didn’t remove the code to process 4-digit number of towns .

        
      from itertools import combinations
      
      # formula E = V + A - 1
      
      # V = the number of towns
      # A = the number of enclosed areas
      # E = the number of roads
      sols = set()
      
      P = [3, 5, 7]
      P += [x for x in range(11, 33, 2) if all(x % p for p in P)]
      P += [x for x in range(33, 1000, 2) if all(x % p for p in P)]
      
      # if V has 3 digits then E must be 1xxx
      # E must have at least 4 digits so V + 2V - 5 - 1 > 999 --> 3V > 1005
      # 3-digit primes
      P3 = [p for p in P if p > 335 and 
                           len(s := str(p)) == len(set(s)) and
                          '1' not in s]
      
      # first process 3-digit number of towns
      for V, A in combinations(P3, 2):
        if V + A < 1024: continue
        E = V + A - 1
        if len(set(str(1000000 * E + 1000 * A + V))) != 10: continue
        sols.add(tuple(sorted([V, A, E])))
        
      P = [3, 5, 7]
      P += [x for x in range(11, 100, 2) if all(x % p for p in P)]
      
      # if V has 4 digits then the 2nd digit of V, E must be resp 9, 0
      # if A ends in 1 then V and E will end in the same digit
      
      # 2-digit primes
      P2 = [p for p in P if p > 9 and 
                            all(y not in (s := str(p)) for y in "019") and
                            len(s) == len(set(s))]
                            
      # 4-digit primes
      P4 = [x for x in range(1001, 10000, 2) if all(x % p for p in P)]
      
      # if V has 4 digits the 2nd digit must be a nine (and may not use a zero)
      # otherwise E will use some of the same digits
      # if V ends in 1 then A and E will end in the same digit
      P4 = [p for p in P4 if (s := str(p))[1] == '9' and 
                             '0' not in s and
                             len(s) == len(set(s)) and
                             s[-1] != '1']
                             
      # numbers in P2 and P4 always end in 3 or 7  (1, 5, and 9 are not allowed)
      # so E must end in 9
      P2 = [p for p in P2 if '9' not in (s := str(p)) and p not in {37, 73}]
      P4 = [p for p in P4 if '9' not in (s := str(p)) and
                              all(y not in s[:-1] for y in "37")]
        
      # process 4-digit number of towns  
      for V in P4:
        # if V has 4 digits (x9yz) then A must be at least 101 - yz
        for A in [p for p in P2 if p >= 101 - V % 100]:
          E = V + A - 1
          if len(set(str(1000000 * E + 1000 * A + V))) != 10: continue
          sols.add(tuple(sorted([V, A, E])))
          
      # print solutions
      for s in sols:    
        print(s)
      

      Like

    • Hugh Casement's avatar

      Hugh Casement 6:32 am on 9 October 2022 Permalink | Reply

      I too had the idea of using Euler’s formula. At first I thought that a town with only a single road leading to it would spoil things, but then realized that they cancel out.

      Most unlikely that there would be a four-digit number of towns and only a two-digit number of regions between the roads, or vice versa. In any case, three plus three quickly yields a solution.
      I’ll spare you my program code, but Basic’s ability to handle strings was useful in determining whether all ten digits are present.

      Like

    • GeoffR's avatar

      GeoffR 11:44 am on 9 October 2022 Permalink | Reply

      I based my program on (3/3) and (2/4) digit splits for towns/areas, with 4 digits for roads.

      from enigma import nsplit, is_prime
       
      # form lists of 2, 3 and 4 digit primes
      # with non-repeating digits to check digit splits
      pr2 = [n for n in range(11, 99) if is_prime(n) \
             and len(set(str(n))) == 2]
      pr3 = [n for n in range(100, 999) if is_prime(n) \
             and len(set(str(n))) == 3]
      pr4 = [n for n in range(1000, 9999) if is_prime(n) \
             and len(set(str(n))) == 4 ]
       
      # check if (town/area) digits split is (3, 3)
      for town in pr3:
        A, B, C = nsplit(town)
        for area in pr3:
          if area == town:continue
          D, E, F = nsplit(area)
          if len(set((A, B, C, D, E, F))) != 6:continue
          roads = town + area - 1
          if roads in pr4: continue
          if roads < 1000 or roads > 9999:continue
          R1, R2, R3, R4 = nsplit(roads)
          if len(set((A,B,C,D,E,F,R1,R2,R3,R4))) == 10:
            if town < area:
              print(f"1.The three numbers were {town}, {area} and {roads}.")
       
      # check if (town/area) digits split is (2,4)
      for town2 in pr2:
        a, b = nsplit(town2)
        for area2 in pr4:
          c, d, e, f = nsplit(area2)
          if len(set((a, b, c, d, e, f))) == 6:
            roads2 = town2 + area2 - 1
            if roads2 < 1000 or roads2 > 9999: continue
            R5, R6, R7, R8 = nsplit(roads2)
            if len(set((a,b,c,d,e,f,R5,R6,R7,R8))) == 10:
              print(f"2.The three numbers were {town2}, {area2} and {roads2}.")
      

      Like

  • Unknown's avatar

    Jim Randell 4:35 pm on 30 September 2022 Permalink | Reply
    Tags:   

    Teaser 3132: Bilateral symmetry 

    From The Sunday Times, 2nd October 2022 [link] [link]

    My son, at a loose end after A-levels, asked me for a mental challenge. As we’d been discussing palindromes, I suggested he try to find an arrangement of the digits 1 to 9 with the multiplication symbol “×” to give a palindrome as the answer, and he came up with:

    29678 × 1453 = 43122134.

    I said: “Now try to find the smallest such palindromic product starting in 4, where the last digit of the smallest number is still 3”. He found that smallest product, and, interestingly, if he added the two smaller numbers instead of multiplying them, then added 100, he also got a palindrome.

    What is the smallest product?

    [teaser3132]

     
    • Jim Randell's avatar

      Jim Randell 4:55 pm on 30 September 2022 Permalink | Reply

      Using the [[ SubstitutedExpression() ]] solver from the enigma.py library.

      This Python program runs in 98ms.

      Run: [ @replit ]

      from enigma import (
        Accumulator, SubstitutedExpression, irange,
        is_npalindrome as is_npal, sprintf as f, printf
      )
      
      # find the smallest product
      r = Accumulator(fn=min, collect=1)
      
      # symbols to use
      syms = "ABCDEFGHI"
      
      n = len(syms)
      for i in irange(1, n // 2):
      
        # construct the alphametic puzzle; X < Y
        (X, Y) = (syms[:i], syms[i:])
        (x, y) = (X[-1], Y[-1])
        # X * Y is palindromic and starts (and ends) in the digit 4
        exprs = [ f("is_npal({X} * {Y})"), f("({x} * {y}) % 10 = 4") ]
        if len(X) == len(Y): exprs.append(f("{X[0]} < {Y[0]}"))
        p = SubstitutedExpression(
          exprs,
          digits=irange(1, 9),  # digits are 1-9
          s2d={ x: 3 },  # final digit of X is 3
          answer=f("({X}, {Y})"),
          env=dict(is_npal=is_npal),
        )
        # solve it
        for (s, (x, y)) in p.solve(verbose=0):
          z = x * y
          printf("[{x} * {y} = {z}]")
          r.accumulate_data(z, (x, y))
      
      # output solution
      printf("min product = {r.value}")
      for (x, y) in r.data:
        v = x + y + 100
        printf("  = {x} * {y}; {x} + {y} + 100 = {v} [{s}palindrome]", s=('' if is_npal(v) else 'not '))
      

      Solution: The smallest product is 40699604.

      Ignoring the final palindromic addition check, there are 3 candidate palindromic products that meet the criteria (in decreasing order)

      424393424 = 7163 × 59248
      43122134 = 1453 × 29678
      40699604 = 23 × 1769548

      The final one gives the answer to the puzzle, and is also the only one where the sum of the multiplicands and 100 is also palindromic.

      There are also two further palindromic products where the larger of the multiplicands ends in the digit 3:

      449757944 = 56219743 × 8
      49900994 = 167453 × 298

      Like

      • Frits's avatar

        Frits 10:22 am on 1 October 2022 Permalink | Reply

        @Jim,

        I expected “for i in irange(5, n – 1):” ( where the last digit of the smallest number is still 3)

        Like

    • GeoffR's avatar

      GeoffR 10:10 am on 3 October 2022 Permalink | Reply

      The only way I could find a MiniZinc solution was to assume that one of the multipliers was two digits long, so this is not a rigorous solution. Although I coded requirements for the second palindrome, as stated in the teaser, I found it was not strictly necessary to find the smallest palindromic product.

      
      % A Solution in MiniZinc
      include "globals.mzn";
      
      % re-using Frits' toNum function
      function var int: toNum(array[int] of var int: a) =
           let { int: len = length(a) }in
               sum(i in 1..len) (
               ceil(pow(int2float(10), int2float(len-i))) * a[i]
                );
                
      % Digits for the two numbers being multiplied together
      % which are AB and CDEFGHI
      var 1..9:A; var 1..9:B; var 1..9:C; var 1..9:D; var 1..9:E; 
      var 1..9:F; var 1..9:G; var 1..9:H; var 1..9:I;
      
      constraint all_different ([A,B,C,D,E,F,G,H,I]);
      
      var 10..99:AB;
      var 1000000.. 9999999:CDEFGHI;
      
      % last digits of the two numbers 
      constraint B == 3 /\ I == 8;
      
      % abcdefgh - main product
      var 1..9:a; var 0..9:b; var 0..9:c; var 0..9:d;
      var 0..9:e; var 0..9:f; var 0..9:g; var 1..9:h;
      
      % mnopqrst - 2nd palindrome
      var 1..9:m; var 0..9:n; var 0..9:o; var 0..9:p;
      var 0..9:q; var 0..9:r; var 0..9:s; var 1..9:t;
      
      % Two palindromes
      var 40000000..45000000:abcdefgh;
      var 1000000..2000000:mnopqrs;
      
      % Two numbers being multiplied together
      constraint AB == toNum([A,B]);
      constraint CDEFGHI == toNum([C,D,E,F,G,H,I]);
      
      % Main and 2nd palindromes
      constraint abcdefgh == toNum([a,b,c,d,e,f,g,h]);
      constraint mnopqrs == toNum([m,n,o,p,q,r,s]); 
      
      % check main product is palindromic
      constraint (a == 4 /\ h == 4) /\ b == g /\ c == f /\ d == e;
      
      % main palindromic product
      constraint CDEFGHI * AB == abcdefgh;
      
      % form 2nd palindrome 
      constraint mnopqrs == CDEFGHI + AB + 100;
      constraint m = s /\ n == r /\ o == q;
      
      % find smallest palindromic product
      solve minimize(abcdefgh);
      
      output["Smallest palindrome = " ++ show(abcdefgh) ++ "\n" ++
      "Sum is: " ++show(CDEFGHI) ++ " * " ++ show(AB) ++ " = " ++ 
       show(abcdefgh) ++ "\n2nd palindrome = "  ++ show(mnopqrs)];
      
      

      Like

    • NigelR's avatar

      NigelR 11:28 am on 3 October 2022 Permalink | Reply

      Shorter but not quicker! The palin lambda proves I was listening last week, though!!

      from itertools import combinations as comb, permutations as perm
      #test whether number 'num' is palindromic
      palin = lambda num:sum(str(num)[n]!=str(num)[-n-1] for n in range(len(str(num))//2)) == 0
      digs=['1','2','4','5','6','7','8','9']
      res=999999999
      #work through possible values for 'left' of two smaller numbers
      for i in range(1,8):
          for x in comb(digs,i):
              for y in perm(x):
                  l = int("".join(y))
                  #work through possible values for 'right' of two smaller numbers (ending in 3)
                  for v in perm([w for w in digs if w not in y]):
                      r=int("".join(v))*10+3
                      if r>l:continue   #number ending in 3 is smallest number
                      prod=l*r
                      #test for output conditions
                      if str(prod)[0]!='4':continue
                      if not palin(prod):continue
                      if not palin(l+r+100):continue
                      if prod<res:res=prod
      print('Smallest valid product is:', res)
      

      Like

      • NigelR's avatar

        NigelR 11:36 am on 3 October 2022 Permalink | Reply

        Given that the number ending in ‘3’ is the smaller of the two numbers, I could have made line 7
        ‘for i in range(5,8)’, which shaves a bit of time off.

        Like

      • Frits's avatar

        Frits 3:19 pm on 3 October 2022 Permalink | Reply

        Easier: palin = lambda num: (s := str(num)) == s[::-1]

        The “for x .. for y ..” can be replaced by “for y in perm(digs, i):”

        Like

        • NigelR's avatar

          NigelR 12:31 pm on 4 October 2022 Permalink | Reply

          Thanks Frits . Much neater – and I now know how to assign a variable within an expression. Onwards and upwards!

          Like

      • Frits's avatar

        Frits 10:57 pm on 3 October 2022 Permalink | Reply

        I spent some time in making a generic version of GeoffR’s Minizinc program.

        As the numFirst and numAsOf functions do not work with “var int” parameters I had to call them several times.

        Using the fact that the first digit of the largest number must be a 1 (as p1 + p2 plus 100 is palindromic and has to end in 1) didn’t help to bring the run time under one second.

         
        % A Solution in MiniZinc
        include "globals.mzn";
        
        % return <n>-th digit of number <a> with length<len>
        function var int: nthDigit(var int: a, var int: len, var int: n) =
          ((a mod pows[len + 2 - n]) div pows[len + 1 - n]);                        
                       
        % return number of the first <len> digits        
        function var int: numFirst(array[int] of var int: a, var int: len) =
          sum(i in 1..len) (pows[len + 1 - i] * a[i]);            
        
        % return number as of the <b>-th digit                        
        function var int: numAsOf(array[int] of var int: a, var int: b) =
          let { int: len = length(a) }in
               sum(i in b..len) (pows[len + 1 - i] * a[i]);  
        
        % count how many digits have a correct mirror digit              
        function var int: palin(var int: a, var int: len, var int: b) =
          sum(i in 1..b) (
              nthDigit(a, len, i) == nthDigit(a, len, len + 1 - i)
          );        
        
        % digits used in the two numbers
        var 1..9: a; var 1..9: b; var 1..9: c; var 1..9: d;
        var 1..9: e; var 1..9: f; var 1..9: g; var 1..9: h; var 1..9: i;
        
        % make a tables of powers of 10
        set of int: pows = {pow(10, j) | j in 0..8};
        
        constraint i == 8;  % largest number has to end in 8
        
        constraint all_different ([a, b, c, d, e, f, g, h, i]);
        
        var 1..9999: p1;           % smallest number
        var 1..99999999: p2;       % largest number
        var 1..999999999: prod;    % product
        var 8..9: L;               % length palindrome
        var 1..4: L1;              % length smallest number
        
        % set smallest number to p1
        constraint p1 == numFirst([a, b, c, d, e, f, g, h, i], L1);
        % set largest number to p2          
        constraint p2 == numAsOf([a, b, c, d, e, f, g, h, i], L1 + 1);
        
        constraint p1 mod 10 == 3;    % last digit of smallest number is 3
        
        % first digit of product must be a 4  (needed for performance)
        constraint nthDigit(prod, L, 1) == 4;
                   
        constraint prod == p1 * p2;
        
        % set length variable L
        constraint (prod  > 99999999 /\ L == 9) \/
                   (prod <= 99999999 /\ L == 8);
        
        % product should be a palindrome
        constraint palin(prod, L, 4) == 4;
        
        % find smallest palindromic product
        solve minimize(prod);
         
        output["Smallest palindrome = " ++ show(prod)];
        

        Like

    • GeoffR's avatar

      GeoffR 9:24 am on 4 October 2022 Permalink | Reply

      @Frits:
      An excellent full programming solution in MiniZinc, with some new functions.

      Like

    • GeoffR's avatar

      GeoffR 8:50 am on 20 October 2022 Permalink | Reply

      # last digits of a = 3 and for b = 8 with a < b
      for a in range(13, 100, 10):  # UB assumed value
          # check 8-digit products up to teaser stated product
          for b in range(100008, 43122134 // a, 10):
              prod1 = a * b
              if prod1 < 40000004:continue
              
              # we need all digits in range 1..9 in prod1
              all_dig = str(a) + str(b)
              if '0' in all_dig:continue
              if len(set(all_dig)) != 9:continue
              
              # check product is a palindrome
              if str(prod1) == str(prod1)[::-1]:
                  # 2nd palindrome condition
                  pal2 = a + b + 100
                  if str(pal2) == str(pal2)[::-1]:
                      print(f"Sum is {a} * {b} = {prod1}")
      
      # Sum is 23 * 1769548 = 40699604
      
      
      
      

      Like

  • Unknown's avatar

    Jim Randell 4:33 pm on 23 September 2022 Permalink | Reply
    Tags:   

    Teaser 3131: Heads up savings 

    From The Sunday Times, 25th September 2022 [link] [link]

    Little Spencer saves 5p coins in a jar, and when they reach £10, deposits them in his savings account. He likes playing with the coins. In one game, after first turning them all heads up, he places them in a row on the table. Starting from the left, he then turns over every 2nd coin until he has reached the end of the row. He then again starts from the left, and this time turns over every 3rd coin. He repeats this for every 4th, 5th coin etc, until finally he turned over just one coin, the last in the row.

    At the end of the game I could see that if Spencer had exactly 75 per cent more coins he would have an increase of 40 per cent in the number showing heads. However, if he had exactly 50 per cent fewer coins, he would have a decrease of 40 per cent in the number showing heads.

    What is the value of his 5p savings?

    There are now 750 Teaser puzzles available on the S2T2 site.

    [teaser3131]

     
    • Jim Randell's avatar

      Jim Randell 5:09 pm on 23 September 2022 Permalink | Reply

      (See also: Puzzle #08).

      Here is a constructive solution.

      It runs in 52ms. (Internal runtime is 409µs).

      Run: [ @replit ]

      from enigma import (irange, csum, div, printf)
      
      # play the game with <n> coins
      def coins(n):
        # each coin starts out showing heads = 1
        vs = [1] * n
        # allow the coins to be indexed from 1
        vs.insert(0, None)
        # every <k> coins
        for k in irange(2, n):
          # flip coin k, 2k, 3k, ....
          for i in irange(k, n, step=k):
            vs[i] ^= 1
        vs.pop(0)
        return vs
      
      # it is enough to calculate one sequence, and then use prefixes
      heads = list(csum(coins(350), empty=1))
      
      # consider initial number of coins
      for n in irange(1, 200):
        # we need 1.75 times the number of coins, and 0.5 times the number
        n1 = div(175 * n, 100)
        n2 = div(50 * n, 100)
        if n1 is None or n2 is None: continue
      
        # h1 is 1.4 times h; h2 is 0.6 times h
        (h, h1, h2) = (heads[x] for x in (n, n1, n2))
        if not (100 * h1 == 140 * h and 100 * h2 == 60 * h): continue
      
        # output solution
        printf("{n} coins = {h} heads; {n1} coins = {h1} heads; {n2} coins = {h2} heads")
      

      Solution: The total value of the coins is £1.40.

      Spencer has 28 coins.

      After performing the procedure there are 5 coins remaining heads up (= coins 1, 4, 9, 16, 25).

      If he had 1.75× the number of coins (= 49 coins), then 7 would remain heads up (= coins 1, 4, 9, 16, 25, 36, 49).

      And if he had 0.5× the number of coins (= 14 coins), then 3 would remain heads up (= coins 1, 4, 9).

      Like

      • Jim Randell's avatar

        Jim Randell 5:46 pm on 23 September 2022 Permalink | Reply

        Using the fact that the coins that remain heads up are exactly those in the perfect square numbered positions (numbering from 1), we can get a shorter (and faster) program.

        This Python program runs in 51ms. (Internal runtime is 221µs).

        Run: [ @replit ]

        from enigma import (irange, isqrt, div, printf)
        
        # play the game with <n> coins, return the number of heads
        heads = isqrt
        
        # consider initial number of coins
        for n in irange(1, 200):
          # we need 1.75 times the number of coins, and 0.5 times the number
          n1 = div(175 * n, 100)
          n2 = div(50 * n, 100)
          if n1 is None or n2 is None: continue
        
          # h1 is 1.4 times h, h2 is 0.6 times h
          (h, h1, h2) = (heads(x) for x in (n, n1, n2))
          if not (100 * h1 == 140 * h and 100 * h2 == 60 * h): continue
        
          # output solution
          printf("{n} coins = {h} heads; {n1} coins = {h1} heads; {n2} coins = {h2} heads")
        

        Like

    • NigelR's avatar

      NigelR 10:50 am on 26 September 2022 Permalink | Reply

      Irrespective of the number of times coin n in the row is flipped, its final H/T depends on whether n has an odd or even number of factors. (I hadn’t spotted the elegance of the perfect squares!).
      PS: I think the answer sought was actually the value of the coins, not the number.

      
      # Test whether n has an even number of factors
      countfac = lambda n: True if [1 if n % i == 0 else 0 for i in range(1, (n//2)+1)].count(1) %2==0 else False
      heads={x:1 for x in range(1,200)} #initialise heads as dictionary 
      for x in range(2,200):
          heads[x]=heads[x-1]
          if countfac(x): heads[x]+=1  #heads[n] now has cumulative number of heads for row of n coins
      #test for output conditions. Number of coins must be divisible by 4 if 75% greater is integer 
      for x in range(4,200,4):
          if x*1.75>200:continue
          y=int(x*1.75)
          z=int(x/2)
          if heads[y]!=heads[x]*1.4:continue
          if heads[z]!=heads[x]*0.6:continue
          value = divmod(5*x,100)
          print(x ,'coins in row, ',heads[x],'of which are heads. Value of savings is £',value[0],'and',value[1],'p')
      

      Like

      • Jim Randell's avatar

        Jim Randell 2:24 pm on 26 September 2022 Permalink | Reply

        @NigelR: I left determining the total value of a certain number of 5p coins as a simple exercise for the reader ;-).

        You could shorten this line of code somewhat:

        countfac = lambda n: True if [1 if n % i == 0 else 0 for i in range(1, (n // 2) + 1)].count(1) % 2 == 0 else False
        

        (True if ... else False) is just the same as evaluating ... (in a boolean context):

        countfac = lambda n: [1 if n % i == 0 else 0 for i in range(1, (n // 2) + 1)].count(1) % 2 == 0
        

        and then in the list comprehension, (1 if ... else 0) is also the same as ... (in a boolean context; in Python False and True are just 0 and 1 in disguise):

        countfac = lambda n: [n % i == 0 for i in range(1, (n // 2) + 1)].count(1) % 2 == 0
        

        and we don’t need to construct the list, just to count the number of 1’s in it:

        countfac = lambda n: sum(n % i == 0 for i in range(1, (n // 2) + 1)) % 2 == 0
        

        Also note that Python’s builtin range function does not include the endpoint. So if you want to go to 350 in line 4 you need to specify a stop value of 351. Similarly in line 8, to check up to 200 coins you need to specify a stop value of 201.

        (I tend to use the inclusive irange() function from the enigma.py library, which includes both endpoints, to avoid this kind of “fencepost” error).

        Like

        • NigelR's avatar

          NigelR 8:40 pm on 26 September 2022 Permalink | Reply

          JIm: Thank you so much for taking the time to unpick my messy code and for your very helpful advice – your countfac is much simpler and I’m now wiser! I couldn’t work out what on earth I’d done in the lambda an hour after writing it!
          Agree on the stop value of 351, but I did think about the 200/201 and concluded that Spencer would have banked the lot if it had reached 200, and hence he would only have been playing with up to 199 coins. Perhaps I’m overthinking it!

          Like

        • Jim Randell's avatar

          Jim Randell 10:08 am on 27 September 2022 Permalink | Reply

          Yes the (200, 350) case is a bit of a grey area. I decided he might like one last play with his coins before he banked them, so I might as well check it, as I prefer solutions that exhaustively explore the solution space.

          As it turns out it doesn’t provide an answer, so it doesn’t really matter if you check it or not.

          Like

    • NigelR's avatar

      NigelR 10:58 am on 26 September 2022 Permalink | Reply

      … and,of course, the 75% greater number is only hypothetical and hence can be greater than 200. My line 4 should go to 350, and line 10 is unnecessary.

      Like

  • Unknown's avatar

    Jim Randell 5:01 pm on 16 September 2022 Permalink | Reply
    Tags:   

    Teaser 3130: Making squares 

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

    Liam has nine identical dice. Each die has the usual numbers of spots from 1 to 6 on the faces, with the numbers of spots on opposite faces adding to 7. He sits at a table and places the dice in a 3×3 square block arrangement.

    As I walk round the table I see that (converting numbers of spots to digits) each vertical face forms a different three-figure square number without a repeating digit.

    As Liam looks down he sees six three-digit numbers (reading left to right and top to bottom) formed by the top face of the block, three of which are squares. The total of the six numbers is less than 2000.

    What is that total?

    [teaser3130]

     
    • Jim Randell's avatar

      Jim Randell 5:29 pm on 16 September 2022 Permalink | Reply

      At first I found multiple solutions. But you can find a unique answer to the puzzle if you ensure the dice really are identical.

      This Python program runs in 261ms.

      Run: [ @replit ]

      from enigma import (powers, nsplit, subsets, seq_all_same_r, nconcat, irange, printf)
      
      # some useful routines for checking dice ...
      
      # value on opposite face
      #      0  1  2  3  4  5  6
      opp = [0, 6, 5, 4, 3, 2, 1]
      
      # valid edge?
      edge = lambda x, y: x != y and x != opp[y]
      
      # construct (x, y) -> z corner maps for a right handed die
      def corners(x, y, z):
        d = dict()
        for (a, b, c) in [(x, y, z), (x, z, opp[y]), (x, opp[y], opp[z]), (x, opp[z], y)]:
          for (p, q, r) in [(a, b, c), (b, c, a), (c, a, b)]:
            d[(p, q)] = r
            d[(opp[p], opp[r])] = opp[q]
        return d
      
      # valid corner?
      # -> 0 if invalid; 1 if right-handed; -1 if left-handed
      def corner(x, y, z, d=corners(1, 2, 3)):
        r = d.get((x, y), 0)
        if r == z: return 1
        if r == opp[z]: return -1
        return 0
      
      # now on with the puzzle ...
      
      # possible 3-digit squares (as numbers) / vertical squares (as digits)
      (sqs, sqvs) = (set(), set())
      for n in powers(10, 31, 2):
        ds = nsplit(n)
        if min(ds) < 1 or max(ds) > 6: continue
        sqs.add(n)
        if len(set(ds)) == 3: sqvs.add(ds)
      
      # consider the layout:
      #
      #     W V U
      #    +-----+
      #  K |A B C| T
      #  L |D E F| S
      #  M |G H I| R
      #    +-----+
      #     N P Q
      
      # choose the squares around the edges (all different)
      for ((K, L, M), (N, P, Q), (R, S, T), (U, V, W)) in subsets(sqvs, size=4, select="P"):
      
        # choose the top faces for the corner dice
        for (A, C, G, I) in subsets(irange(1, 6), size=4, select="M"):
          corners = [(A, W, K), (G, M, N), (I, Q, R), (C, T, U)]
          r = seq_all_same_r(corner(*vs) for vs in corners)
          if not (r.same and r.value != 0): continue
      
          # choose the remaining top faces
          for (B, D, E, F, H) in subsets(irange(1, 6), size=5, select="M"):
            edges = [(D, L), (H, P), (F, S), (B, V)]
            if not all(edge(*vs) for vs in edges): continue
      
            # construct the 6 top numbers
            numbers = [(A, B, C), (D, E, F), (G, H, I), (A, D, G), (B, E, H), (C, F, I)]
            ns = list(nconcat(*vs) for vs in numbers)
            # the sum is less than 2000
            t = sum(ns)
            if not (t < 2000): continue
            # three of them are squares
            if sum(n in sqs for n in ns) != 3: continue
      
            # output solution
            printf("{t} <- {hs} {vs}; [{K}{L}{M}, {N}{P}{Q}, {R}{S}{T}, {U}{V}{W}]", hs=ns[:3], vs=ns[3:])
      

      (or a faster variation on [@replit]).

      Solution: The total is 1804.

      The dice are laid out as follows:

      So the total is: (115 + 441 + 445) + (144 + 144 + 515) = 1804.

      Of the six numbers read off from the top of the arrangement the square numbers are: 441 (= 21²) and 144 (twice; = 12²).

      Note that each of the corner dice is left-handed (i.e. a mirror image of a “standard” die), and so, as the dice are all identical, they must all be left-handed.

      If we are allowed to mix left- and right-handed dice, then there are many possible layouts (and many possible answers to the puzzle).

      Like

      • Frits's avatar

        Frits 9:50 pm on 16 September 2022 Permalink | Reply

        Thanks to Jim, hopefully with the correct solution.

           
        from itertools import permutations, product
        from functools import reduce
        
        # convert digits to number
        d2n = lambda s: reduce(lambda x,  y: 10 * x + y, s)
        
        # 3-digit squares using digits 1-6
        sqs = [s for x in range(10, 27) 
               if not any(y in (s := str(x * x)) for y in "7890")]
        
        # 3-digit squares with different digits       
        side_sqs = [int(s) for s in sqs if len(set(s)) == 3]
        sqs = set(int(s) for s in sqs)
        
        #    +-------+   so bottom = 4, left = 5, back = 6
        #   /   3   /.    
        #  /       /2
        # +-------+ .
        # |   1   | 
        # |       |.
        # +-------+ 
        
        # we have nine identical dice 
        # so with two clockwise adjacent faces, say (2, 3),
        # the upper face is fixed (I have set it to 6)
        d = dict()
        # set the number facing up for clockwise pair (a, b)
        d[(1, 2)] = 4
        d[(1, 3)] = 2
        d[(2, 3)] = 6
        d[(2, 6)] = 4
        d[(3, 5)] = 6
        d[(3, 6)] = 2
        d[(4, 5)] = 1
        d[(4, 6)] = 5
        d[(5, 6)] = 3
        
        # add decreasing pairs
        for k, v in d.copy().items():
          d[k[::-1]] = 7 - v
          
        for p in permutations(side_sqs, 4):
         
          edges = []
          corners = []
          # calculate candidate numbers facing up for each corner and each edge
          for x, y in zip(p, p[1:] + (p[0], )):
            # connect squares x and y
            corner_sides = (x % 10, y // 100)
            
            if corner_sides not in d: break
            corners.append([d[corner_sides]])
            edges.append([i for i in range(1, 7) 
                          if i not in {(x % 100) // 10, 7 - ((x % 100) // 10)}])
          
          # each corner should have only one candidate
          if len(corners) != 4: continue
          
          edges = edges[1:] + [edges[0]]
          
          # 3x3 block of dice
          block = [corners[2], edges[1], corners[1],
                   edges[2], range(1, 7), edges[0],  
                   corners[3], edges[3], corners[0]]
         
          for p2 in product(*block):
           # six three-digit numbers on top face
           F = [d2n([p2[3 * i + j] for j in range(3)]) for i in range(3)] + \
               [d2n([p2[3 * j + i] for j in range(3)]) for i in range(3)]
           # the total of the six numbers is less than 2000 ...
           if sum(F) >= 2000: continue
           # ... three of which are squares .
           if sum([f in sqs for f in F]) != 3: continue
             
           print("    ", str(p[2])[::-1])
           print(str(p[3])[0], p2[:3], str(p[1])[2])
           print(str(p[3])[1], p2[3:6], str(p[1])[1])
           print(str(p[3])[2], p2[6:], str(p[1])[0])
           print("    ", p[0])
           print()
           print("total:", sum(F))
        

        Like

        • Frits's avatar

          Frits 10:03 pm on 16 September 2022 Permalink | Reply

          The side squares should be seen when walking anti-clockwise around the table (so the top and right squares are printed reversed).

          Like

    • Hugh Casement's avatar

      Hugh Casement 7:17 am on 17 September 2022 Permalink | Reply

      Are they left-handed dice? I can’t find any solution with standard dice (such as Frits shows).
      Or perhaps you have to walk round the table with your head upside down.

      Like

      • Jim Randell's avatar

        Jim Randell 7:25 am on 17 September 2022 Permalink | Reply

        @Hugh: Yes. There is only a solution if left-handed dice are used (at least in the corners of the layout – and as the dice are identical then the rest must be left-handed too).

        Like

      • Frits's avatar

        Frits 9:40 am on 17 September 2022 Permalink | Reply

        I set up my dice right-handed (I didn’t even know the term right-handed) based on numbers facing up when going clock wise. However, the corner arrangements in the block have to be read anti-clockwise so I should have used the 7-complement of my hardcoded values.

        My solution is the same as Jim’s and is left-handed. I will post a new version checking both left-handed and right-handed dice (using Brian Gladman’s function for determining the hardcoded values).

        Like

    • Frits's avatar

      Frits 1:52 pm on 17 September 2022 Permalink | Reply

      Supporting right-hand and left-hand dice and with a recursive version of Brian Gladman’s function for third face values.

         
      from itertools import permutations, product
      from functools import reduce
       
      # find the third face anti-clockwise around a dice vertex
      # given two faces in anti-clockwise order
      def f3(fi, se, rh=True):
        # invalid first/second combination
        if fi == se or fi + se == 7: return 0
         
        # only calculate for 3 low increasing pairs 
        if (fi, se) in {(1, 2), (1, 3), (2, 3)}:
          c = 0 if rh else 7  # complement related
          return abs(c - (fi * (2 * se - 1)) % 9)            # artificial formula
         
        # as of now work towards low increasing pairs
        if se < fi: return 7 - f3(se, fi, rh)
        elif fi > 3 and se > 3: return f3(7 - fi, 7 - se, rh) # double complement
        elif fi > 3: return 7 - f3(7 - fi, se, rh)
        elif se > 3: return 7 - f3(fi, 7 - se, rh)
          
      # convert digits to number
      d2n = lambda s: reduce(lambda x,  y: 10 * x + y, s)
       
      # 3-digit squares using digits 1-6
      sqs = [s for x in range(10, 27) 
             if not any(y in (s := str(x * x)) for y in "7890")]
       
      # 3-digit squares with different digits       
      side_sqs = [int(s) for s in sqs if len(set(s)) == 3]
      sqs = set(int(s) for s in sqs)
         
      for p in permutations(side_sqs, 4):
        
        # check both right-handed and left-handed dice
        for rh in [1, 0]:
          edges = []
          corners = []
          # calculate candidate numbers facing up for each corner and each edge
          for x, y in zip(p, p[1:] + (p[0], )):
            # connect squares x and y and calculate top face
            top = f3(x % 10, y // 100, rh)
            if not top: break
            corners.append([top])
            edges.append([i for i in range(1, 7) 
                          if i not in {(x % 100) // 10, 7 - ((x % 100) // 10)}])
          else:    
            edges = edges[1:] + [edges[0]]    # rearrange 
       
            # 3x3 block of dice
            block = [corners[2], edges[1], corners[1],
                     edges[2], range(1, 7), edges[0],  
                     corners[3], edges[3], corners[0]]
       
            for p2 in product(*block):
             # six three-digit numbers on top face
             F = [d2n([p2[3 * i + j] for j in range(3)]) for i in range(3)] + \
                 [d2n([p2[3 * j + i] for j in range(3)]) for i in range(3)]
             # the total of the six numbers is less than 2000 ...
             if sum(F) >= 2000: continue
             # ... three of which are squares 
             if sum([f in sqs for f in F]) != 3: continue
       
             print("    ", str(p[2])[::-1])
             print(str(p[3])[0], p2[:3], str(p[1])[2])
             print(str(p[3])[1], p2[3:6], str(p[1])[1])
             print(str(p[3])[2], p2[6:], str(p[1])[0])
             print("    ", p[0], "\n")
             print("total:", sum(F), 
                   "(right-handed" if rh else "(left-handed", "dice)")
      

      Like

    • Frits's avatar

      Frits 11:39 pm on 17 September 2022 Permalink | Reply

      Based on Jim’s first posted program. This program runs in 90ms.

         
      from enigma import SubstitutedExpression
       
      # consider the layout:
      #
      #     W V U
      #    +-----+
      #  K |A B C| T
      #  L |D E F| S
      #  M |G H I| R
      #    +-----+
      #     N P Q
      
      # corners for a right-handed die
      die = {(1, 2, 3), (1, 3, 5), (1, 5, 4), (1, 4, 2), 
             (6, 4, 5), (6, 5, 3), (6, 3, 2), (6, 2, 4)}
       
      # check faces anti-clockwise around a corner (= x, y, z)
      # 1 = right-handed, -1 = left-handed
      def corner(x, y, z):
        ss = {x, y, z}
        return (1 if die.intersection({(x, y, z), (y, z, x), (z, x, y)}) else -1)
      
      # the alphametic puzzle
      p = SubstitutedExpression(
        [
         "W != 7 - K",
         "is_square(KLM)",
         "U != 7 - T",
         "is_square(UVW)",
         "R != 7 - Q",
         "is_square(RST)",
         "N != 7 - M",
         "is_square(NPQ)",
         
         "seq_all_different([KLM, NPQ, RST, UVW])",
         # corner checks
         "A not in {7 - W, K, 7 - K}",
         "C not in {7 - U, T, 7 - T}",
         "I not in {7 - R, Q, 7 - Q}",
         "G not in {7 - M, N, 7 - N}",
         # edge checks
         "D != 7 - L", 
         "B != 7 - V", 
         "F != 7 - S", 
         "H != 7 - P", 
         
         "seq_all_same(corner(*vs) for vs in [(A, W, K), (G, M, N), \
                                              (I, Q, R), (C, T, U)])",
         
         "sum([ABC, DEF, GHI, ADG, BEH, CFI]) < 2000",
                                           
         "sum([1 for x in [ABC, DEF, GHI, ADG, BEH, CFI] if is_square(x)]) == 3",
        ],
        answer="(ABC, DEF, GHI), (KLM, NPQ, RST, UVW), \
                sum([ABC, DEF, GHI, ADG, BEH, CFI])",
        
        distinct=("KLM","NPQ","RST","WVU","AWK","UCT","IRQ","MGN", \
                  "DL","BV","FL","HP"),
        env=dict(corner=corner),
        digits=range(1, 7),
        reorder=0,
        denest=32,    # [CPython] work around statically nested block limit
        verbose=0,    # use 256 to see the generated code
      )
      
      # print answers
      for (_, ans) in p.solve():
        print(" ", str(ans[1][3])[::-1])
        print(f"{str(ans[1][0])[0]} {ans[0][0]} {str(ans[1][2])[2]}")
        print(f"{str(ans[1][0])[1]} {ans[0][1]} {str(ans[1][2])[1]}")
        print(f"{str(ans[1][0])[2]} {ans[0][2]} {str(ans[1][2])[0]}")
        print(" ", ans[1][1], "  sum", ans[2], "\n")
      

      Like

    • Frits's avatar

      Frits 11:02 pm on 22 September 2022 Permalink | Reply

      More efficient version.

         
      from itertools import permutations, product
      from functools import reduce
      from collections import defaultdict
      
      # find the third face anti-clockwise around a dice vertex
      # given two faces in anti-clockwise order
      def f3(fi, se, rh=True):
        # invalid first/second combination
        if fi == se or fi + se == 7: return 0
        
        # only hardcode for 3 low increasing pairs 
        match (fi, se):
          case (1, 2): return 3 if rh else 4
          case (1, 3): return 5 if rh else 2
          case (2, 3): return 1 if rh else 6
          case _:
            # work towards low increasing pairs
            if se < fi: return 7 - f3(se, fi, rh)
            elif fi > 3 and se > 3: return f3(7 - fi, 7 - se, rh)
            elif fi > 3: return 7 - f3(7 - fi, se, rh)
            elif se > 3: return 7 - f3(fi, 7 - se, rh)
      
      # convert digits to number
      d2n = lambda s: reduce(lambda x,  y: 10 * x + y, s)
      
      # 3-digit squares using digits 1-6
      sqs = [s for x in range(10, 27) 
             if not any(y in (s := str(x * x)) for y in "7890")]
       
      # map hundreds and units digits to tens digits in squares
      hu2ts = defaultdict(set)
      for s in sqs:
        h, t, u = (int(d) for d in s)
        hu2ts[h, u].add(t)
       
      # 3-digit squares with different digits       
      side_sqs = [int(s) for s in sqs if len(set(s)) == 3]
      sqs = set(int(s) for s in sqs)
        
      for p in permutations(side_sqs, 4):
        # check both right-handed and left-handed dice
        for rh in [1, 0]:
          edges = []
          corners = []
          # calculate candidate numbers facing up for each corner and each edge
          for x, y in zip(p, p[1:] + (p[0], )):
            # connect squares x and y and calculate top face
            top = f3(x % 10, y // 100, rh)
            if not top: break
            corners.append(top)
            edges.append([i for i in range(1, 7) 
                          if i not in {(x % 100) // 10, 7 - ((x % 100) // 10)}])
          else:    
            edges = edges[1:] + [edges[0]]    # rearrange 
            
            # c[2] e[1] c[1]   
            # e[2]  .   e[0]
            # c[3] e[3] c[0]
            
            # skip if sum of the four top numbers that don't use the center
            # is already too high
            if 100 * (2 * corners[2] + corners[1] + corners[3]) + 4 * 10 + \
               corners[3] + 2 + corners[0] + corners[1] + 222 > 1999:
              continue
            
            # which of these four top numbers can make a square number
            ts = []
            for i, (a, b) in enumerate([(1, 0), (2, 1), (2, 3), (3, 0)]):
              # can we form a square with 2 corners?
              if len(hu2ts[corners[a], corners[b]]):
                # which edge candidates result in a square
                cands = hu2ts[corners[a], corners[b]] & set(edges[i])
                if cands:
                  ts.append((i, cands))
            
            if not ts: continue    # we can't make a square
            elif len(ts) == 1:
              # only one top number can make a square, reduce the edge candidates
              edges[ts[0][0]] = list(ts[0][1])
           
            for e4 in product(*edges):
              # four three-digit numbers on top face (without the center)
              T4 = [d2n([corners[1], e4[0], corners[0]]),
                    d2n([corners[2], e4[1], corners[1]]),
                    d2n([corners[2], e4[2], corners[3]]),
                    d2n([corners[3], e4[3], corners[0]])]
              
              if sum(T4) + 222 > 1999: continue
              
              # center die
              for c in range(1, 7):
                # add two missing top numbers
                T6 = T4 + [d2n([e4[1], c, e4[3]]), d2n([e4[2], c, e4[0]])]
                
                # the total of the six numbers is less than 2000 ...
                if sum(T6) >= 2000: continue
                # ... three of which are squares 
                if sum([t in sqs for t in T6]) != 3: continue
               
                print(" ", str(p[2])[::-1])
                print(str(p[3])[0], T6[1], str(p[1])[2])
                print(str(p[3])[1], T6[3], str(p[1])[1])
                print(str(p[3])[2], T6[5], str(p[1])[0])
                print(" ", p[0], end="      ")
                print("total:", sum(T6), 
                      "(right-handed" if rh else "(left-handed", "dice)")
      

      Like

  • Unknown's avatar

    Jim Randell 4:46 pm on 9 September 2022 Permalink | Reply
    Tags:   

    Teaser 3129: Bounce count 

    From The Sunday Times, 11th September 2022 [link] [link]

    At the local arcade, Claire and David played an air hockey game, using a square table with small pockets at each corner, on which a very small puck can travel 1m left-right and 1m up-down between the perimeter walls. Projecting the puck from a corner, players earn a token for each bounce off a wall, until the puck drops into a pocket.

    In their game, one puck travelled 1m further overall than its left-right distance (and the other, travelled 2m further). Claire’s three-digit number of tokens was a cube, larger than David’s number which was triangular (1 + 2 + 3 + …). Picking up an extra token, they found they could split their collection into two piles, one consisting of a cube number of tokens and the other a square.

    How many tokens did they end up with?

    I’ve modified the wording slightly to remove a typo and improve clarity.

    [teaser3129]

     
    • Jim Randell's avatar

      Jim Randell 5:23 pm on 9 September 2022 Permalink | Reply

      With this kind of puzzle it is easier to reflect the table, rather than reflecting the puck. (Assuming the puck bounces in a well behaved fashion). See: Teaser 1917.

      If a play has x bounces off the left/right sides, and y bounces of the top/bottom sides, then in order for the play to be viable, (x + 1) and (y + 1) must be coprime, and:

      score = x + y
      left/right distance = x + 1
      top/bottom distance = y + 1
      distance travelled, d = hypot(x + 1, y + 1)

      It seems we need to find scores C (a cube) and D (a triangular number) such that (C + D + 1) can be expressed as the sum of a cube and a square.

      This Python program runs in 71ms. (Internal run time is 14.6ms).

      Run: [ @replit ]

      from enigma import (
        coprime_pairs, is_square, sq, is_cube,
        is_triangular, cproduct, powers, inf, printf
      )
      
      # generate (x, y, z) values, where z is d - x
      def generate():
        # consider coprime pairs
        for (a, b) in coprime_pairs(200):
          d = is_square(sq(a) + sq(b))
          if d is None: continue
          for (x, y) in [(a, b), (b, a)]:
            z = d - x
            if z in {1, 2}:
              yield (x - 1, y - 1, z)
      
      # find possible values for C and D
      (Cs, Ds) = (list(), list())
      for (x, y, z) in generate():
        t = x + y
        if 99 < t < 1000 and is_cube(t):
          Cs.append((x, y, z, t))
        if t < 1000 and is_triangular(t):
          Ds.append((x, y, z, t))
      
      # can total t be decomposed into a square and a cube?
      def is_sq_cb(t):
        for c in powers(1, inf, 3):
          s = t - c
          if s < 1: return False
          if is_square(s): return True
      
      # choose values for C and D
      for ((Cx, Cy, Cz, Ct), (Dx, Dy, Dz, Dt)) in cproduct([Cs, Ds]):
        T = Ct + Dt + 1
        # Cz/Dz are different; Ct > Dt; T is a square + a cube
        if Cz != Dz and Dt < Ct and is_sq_cb(T):
          # output solution
          printf("T={T} [C=(x={Cx} y={Cy} z={Cz} t={Ct}); D=(x={Dx} y={Dy} z={Dz} t={Dt})]")
      

      Solution: They ended up with a total of 171 tokens.

      C won 125 (= 5^3) tokens on her go. The puck made 111 left/right bounces and 14 up/down bounces. The left/right distance travelled was 112m and the total distance travelled was 113m. So C’s overall distance was 1m more than the total left/right distance.

      D won 45 (= tri(9)) tokens on his go. The puck made 34 left/right bounces and 11 up/down bounces. The left/right distance travelled was 35m and the total distance travelled was 37m. So D’s overall distance was 2m more than the total left/right distance.

      Combining their tokens with 1 extra token give 125 + 45 + 1 = 171 tokens in total.

      And 171 = 144 + 27 = 12^2 + 3^3.

      Like

      • Jim Randell's avatar

        Jim Randell 9:33 pm on 9 September 2022 Permalink | Reply

        Faster (and a bit shorter) to use [[ pythagorean_triples() ]] to generate the (x, y, z) values. And we don’t need to check the coprime condition if we just look at primitive pythagorean triples.

        This Python program runs in 54ms. (Internal run time is 579µs).

        Run: [ @replit ]

        from enigma import (
          pythagorean_triples, is_square, is_cube, is_triangular,
          cproduct, powers, inf, ifirst, lt, printf
        )
        
        # generate (x, y, z) values, where z is d - x
        def generate():
          for (a, b, c) in pythagorean_triples(1001, primitive=1):
            z = c - b
            if z in {1, 2}:
              yield (b - 1, a - 1, z)
        
        # find possible values for C and D
        (Cs, Ds) = (list(), list())
        for (x, y, z) in generate():
          t = x + y
          if 99 < t < 1000 and is_cube(t):
            Cs.append((x, y, z, t))
          if t < 1000 and is_triangular(t):
            Ds.append((x, y, z, t))
        
        # can total t be decomposed into a square and a cube?
        def is_sq_cb(t):
          return any(is_square(t - c) for c in ifirst(powers(1, inf, 3), count=lt(t)))
        
        # choose values for C and D
        for ((Cx, Cy, Cz, Ct), (Dx, Dy, Dz, Dt)) in cproduct([Cs, Ds]):
          T = Ct + Dt + 1
          # Cz/Dz are different; Ct > Dt; T is a square + a cube
          if Cz != Dz and Dt < Ct and is_sq_cb(T):
            # output solution
            printf("T={T} [C=(x={Cx} y={Cy} z={Cz} t={Ct}); D=(x={Dx} y={Dy} z={Dz} t={Dt})]")
        

        Like

        • Frits's avatar

          Frits 9:48 am on 10 September 2022 Permalink | Reply

          @Jim, how do you guarantee one puck travelled 1m and the other 2m?

          Like

        • Frits's avatar

          Frits 9:54 am on 10 September 2022 Permalink | Reply

          Can’t both C and D not have a difference of 1 m? or both 2m?

          Like

          • Jim Randell's avatar

            Jim Randell 9:55 am on 10 September 2022 Permalink | Reply

            No. Line 30 ensures the z values are different.

            Like

            • Frits's avatar

              Frits 10:00 am on 10 September 2022 Permalink | Reply

              @you are right, I only saw line 10. I may have overlooked it as I use z as the hypotenuse.

              Like

    • Frits's avatar

      Frits 8:31 pm on 9 September 2022 Permalink | Reply

         
      from itertools import product
      from enigma import is_square, is_cube, is_triangular
      
      # can number <n> be decomposed into a square and a cube?
      def check(n):
        for i in range(int(n**(1/3)) + 1):
          if is_square(n - i**3): 
            return True
        return False  
      
      g1 = []
      g2 = []
      # travelled distance is hypothenuse z
      # for one person one side is z - 1, for the other person it is z - 2
      for z in range(1, 1003):
        # store sides of a right triangle (x, y, z) if x + y - 2 < 1000 
        # z**2 - (z - 1)**2 = 2z - 1
        if (rt := is_square(2 * z - 1)) and (z - 1 + rt - 2) < 1000:
          g1.append((z, z - 1, rt))
        # z**2 - (z - 2)**2 = 4(z - 1) 
        if (rt := is_square(4 * (z - 1))) and (z - 2 + rt - 2) < 1000:
          g2.append((z, z - 2, rt))  
      
      # check if number of earned tokens x + y - 2 is cube or triangular
      g1 = [(x + y - 2, (c, t), (z, x, y)) for z, x, y in g1 
            if ((c := is_cube(x + y - 2)) and x + y - 2 > 99) or 
              (t := is_triangular(x + y - 2))]    
      g2 = [(x + y - 2, (c, t), (z, x, y)) for z, x, y in g2 
            if ((c := is_cube(x + y - 2)) and x + y - 2 > 99) or 
              (t := is_triangular(x + y - 2))]     
      
      # check all combinations
      for p1, p2 in product(g1, g2):
        # there should be at least one cube and one triangular number
        if any(p1[1][i] == p2[1][i] == None for i in range(2)): continue
        # cube should be higher than triangular number
        if p1[0] == p2[0]: continue
        if p1[0] > p2[0] and p1[1][0] == None: continue
        if p2[0] > p1[0] and p2[1][0] == None: continue
        
        # can we arrange all their tokens plus one into a cube and a square?
        if check(p1[0] + p2[0] + 1):
          print("answer:", p1[0], "and", p2[0])
      

      Like

      • Frits's avatar

        Frits 11:31 am on 10 September 2022 Permalink | Reply

        Easier to read.

           
        from enigma import is_square, is_cube, is_triangular, ceil
        
        # can number <n> be decomposed into a square and a cube?
        def check(n):
          for i in range(1, ceil(n**(1/3))):
            if is_square(n - i**3): 
              return True
          return False  
        
        # travelled distance is hypothenuse z of a right triangle
        # for one person one side is z - 1, for the other person it is z - 2
        
        cubes, tris = [], []
        for z in range(1, 1003):
          # 1m or 2m further
          for f in range(1, 3):
            # calculate other side (besides z and z - f)
            other = is_square(2 * f * z - f**2)          # z**2 - (z - f)**2
            if not other: continue
            # for a right triangle (x, y, z) we earn x + y - 2 tokens
            tkns = z - f + other - 2
            if is_cube(tkns) and tkns > 99:
              cubes.append((tkns, f, (z - f, other))) 
            if is_triangular(tkns):
              tris.append((tkns, f, (z - f, other))) 
        
        # check all combinations
        for c in cubes:
          for t in tris:
            # cube larger than triangular and using both 1m further and 2m further
            if t[0] >= c[0] or t[1] == c[1]: continue
            # can we arrange all their tokens plus one into a cube and a square?
            if check(c[0] + t[0] + 1):
              print("answer:", c[0], "and", t[0])  
        

        Like

        • Jim Randell's avatar

          Jim Randell 3:38 pm on 10 September 2022 Permalink | Reply

          @Frits: Yes, I think that is much clearer.

          Although I don’t see a check to ensure that the left/right and up/down distances are coprime. This is necessary for a valid path. If this is not the case the puck will hit a pocket before it reaches the end of the path.

          Like

          • Frits's avatar

            Frits 9:14 pm on 10 September 2022 Permalink | Reply

            You are right, I had to verify the coprime thing with the following graph.
            If both distances share a factor then there is at least one point on the hypothenuse (besides the end points) where both x and y are whole numbers (and thus the puck drops prematurely in a pocket).

             
              |               f(x) = b - (x * b) / a   
            b-|               suppose b and a share factor k, k > 1
              |\              so a = k * a2, b = k * b2
              | \
              |  \            f(x) = b - (x * b2 / k) / (a2 / k) 
              |   \           if x = a2 then f(x) = b - b2 (integer)
              |    \
              +---------
                    a
            

            Like

  • Unknown's avatar

    Jim Randell 4:29 pm on 2 September 2022 Permalink | Reply
    Tags:   

    Teaser 3128: Tetrahedral towers 

    From The Sunday Times, 4th September 2022 [link] [link]

    I have a large number (but fewer than 2000) of identical spherical bonbons, arranged exactly as a tetrahedral tower, having the same number of bonbons along each of the six edges of the tower, with each bonbon above the triangular base resting on just three bonbons in the tier immediately below.

    I apportion all my bonbons between all my grandchildren, who have different ages in years, not less than 5, so that each grandchild can exactly arrange his or her share as a smaller tetrahedral tower, having the same number of tiers as his or her age in years.

    The number of my grandchildren is the largest possible in these circumstances.

    How many tiers were in my original tower? And how old in years are the eldest and youngest of my grandchildren?

    [teaser3128]

     
    • Jim Randell's avatar

      Jim Randell 4:46 pm on 2 September 2022 Permalink | Reply

      This Python program constructs the tetrahedral numbers we are interested in, and then looks for subsets that also sum to a tetrahedral number.

      It runs in 178ms.

      Run: [ @replit ]

      from enigma import (irange, inf, tri, reverse, subsets, Accumulator, printf)
      
      # compute tetrahedral numbers, n >=5, tetra[n] < 2000
      tetra = dict()
      t = 0
      for n in irange(1, inf):
        t += tri(n)
        if not (t < 2000): break
        if n < 5: continue
        tetra[n] = t
      rev = reverse(tetra)
      
      # consider possible ages of grandchildren
      r = Accumulator(fn=max, collect=1)
      for ns in subsets(sorted(tetra.keys()), min_size=2):
        t = sum(tetra[n] for n in ns)
        n = rev.get(t)
        if n:
          r.accumulate_data(len(ns), (ns, n))
      
      # output solution
      printf("{r.value} grandchildren")
      for (ns, n) in r.data:
        printf("-> ages = {ns}; tetra[{n}] = {t}", t=tetra[n])
      

      Solution: The original tower had 19 tiers. The eldest grandchild is 12 years old. The youngest grandchild is 5 years old.

      In total there are tetra[19] = 1330 bonbons.

      There are 8 grandchildren. Their ages are: 5, 6, 7, 8, 9, 10, 11, 12.

      And:

      tetra[5] + tetra[6] + tetra[7] + tetra[8] + tetra[9] + tetra[10] + tetra[11] + tetra[12]
      = 35 + 56 + 84 + 120 + 165 + 220 + 286 + 364
      = 1330
      = tetra[19]


      There are possible sets of grandchildren ages of size: 2, 3, 4, 5, 6, 7, 8:

      2: (8, 14) → 15

      3: (5, 6, 12) → 13
      3: (11, 12, 15) → 19

      4: (5, 6, 9, 14) → 16
      4: (5, 8, 11, 19) → 21
      4: (5, 11, 12, 13) → 18
      4: (6, 8, 13, 18) → 21
      4: (6, 9, 10, 19) → 21
      4: (8, 9, 11, 17) → 20
      4: (8, 11, 12, 14) → 19

      5: (5, 6, 7, 9, 10) → 14
      5: (5, 7, 11, 13, 15) → 20
      5: (6, 7, 10, 12, 16) → 20

      6: (5, 6, 7, 8, 9, 10) → 15
      6: (5, 6, 7, 8, 9, 15) → 18
      6: (5, 6, 7, 10, 14, 16) → 21
      6: (5, 7, 8, 11, 13, 14) → 20
      6: (6, 7, 9, 10, 13, 14) → 20
      6: (6, 7, 9, 11, 12, 16) → 21
      6: (6, 9, 10, 11, 12, 15) → 21
      6: (7, 8, 9, 10, 11, 13) → 19

      7: (6, 8, 9, 10, 11, 12, 14) → 21

      8: (5, 6, 7, 8, 9, 10, 11, 12) → 19

      The largest number of grandchildren (8) has only one corresponding set, and this gives the answer to the puzzle.

      Like

      • Jim Randell's avatar

        Jim Randell 10:33 pm on 2 September 2022 Permalink | Reply

        Or a bit longer, but faster.

        This Python program runs in 56ms. (Internal run time is 1.7ms).

        Run: [ @replit ]

        from enigma import (irange, inf, tri, reverse, append, Accumulator, printf)
        
        # compute tetrahedral numbers, n >=5, tetra[n] < 2000
        tetra = dict()
        t = mx = 0
        for n in irange(1, inf):
          t += tri(n)
          if not (t < 2000): break
          if n < 5: continue
          tetra[n] = t
          mx = t
        rev = reverse(tetra)
        
        # make sums of tetras
        sums = {(): 0}
        for k in sorted(tetra.keys()):
          v = tetra[k]
          xs = dict((append(ns, k), t + v) for (ns, t) in sums.items() if not (t + v > mx))
          sums.update(xs)
        
        # look for sums that are themselves tetras
        r = Accumulator(fn=max, collect=1)
        for (ns, t) in sums.items():
          n = rev.get(t)
          if n:
            r.accumulate_data(len(ns), (ns, n))
        
        # output solution
        printf("{r.value} grandchildren")
        for (ns, n) in r.data:
          printf("-> ages = {ns}; tetra[{n}] = {t}", t=tetra[n])
        

        Like

    • Robert Brown's avatar

      Robert Brown 5:21 pm on 2 September 2022 Permalink | Reply

      I found this:

      How many tiers were in my original tower, and how old in years are the eldest and youngest of my grandchildren?

      Like

    • GeoffR's avatar

      GeoffR 10:33 pm on 2 September 2022 Permalink | Reply

      # Full tetrahedral list below 2000, starting at 5th number
      # Ref: http://oeis.org/A000292 - assume youngest age = 5
      
      # List starting from 5th tetrahedral number
      Tet_Nums = [35, 56, 84, 120, 165, 220, 286, 364, 455, 560,
      680, 816, 969, 1140, 1330, 1540, 1771]
      
      # Dictionary to reference tetrahedral numbers and ages
      ages = range(5, 22)
      TN = dict(zip(Tet_Nums, ages))
      
      # Iterate from both ends of tetrahedral numbers list
      # Largest possible number needed from end of list
      for a in reversed(Tet_Nums):
        nsum = 0
        L =[]  # list of grandchildren's tetrahedral numbers
        for b in (Tet_Nums):
          nsum += b
          L.append(b) 
          if nsum > a:
            continue
          if nsum == a:
            print('Tiers in original tower = ', TN[nsum], "no.")
            for x in L:
              print('Grandchild\'s Age =', TN[x], "years")
            exit()   # single solution only needed
      
      

      Like

      • Jim Randell's avatar

        Jim Randell 3:15 pm on 4 September 2022 Permalink | Reply

        @GeoffR: It looks like this code looks for sets of consecutive ages starting from 5. Which is somewhat more specific than the puzzle text requires.

        Like

    • GeoffR's avatar

      GeoffR 5:03 pm on 4 September 2022 Permalink | Reply

      @Jim: Yes, you are correct..

      Like

  • Unknown's avatar

    Jim Randell 6:56 pm on 26 August 2022 Permalink | Reply
    Tags:   

    Teaser 3127: Putting in a shift 

    From The Sunday Times, 28th August 2022 [link] [link]

    Next month’s four week rota for Monday to Friday dinner duties starting on Monday 1st is covered by five teachers each having the following duty allocations. Ann, Ben and Ed each have four, Celia six and Dave has two. Strangely, all the prime number dates (1 is not a prime) are covered by Ben, Celia and Dave, while the others are covered by Ann, Celia and Ed. After working a duty, nobody works on either of the following two shifts, so anyone working on a Friday will not work on the following Monday or Tuesday. Celia has no duties on Mondays while Ben and Ed have none on Wednesdays.

    In date order, who is on duty from Monday to Friday of the first week?

    [teaser3127]

     
    • Jim Randell's avatar

      Jim Randell 7:32 pm on 26 August 2022 Permalink | Reply

      This Python program runs in 56ms. (Internal run time is 2.8ms).

      Run: [ @replit ]

      from enigma import (enigma, irange, multiset, append, chunk, join, printf)
      
      # dates to allocate (1 - 26 without Sat (6) or Sun (0))
      dates = list(n for n in irange(1, 26) if n % 7 not in {0, 6})
      
      # counts for each worker
      count = dict(A=4, B=4, C=6, D=2, E=4)
      
      # primes
      primes = set(enigma.primes.between(1, 26))
      
      # allocate candidates to a list of dates
      def solve(ds, i=0, vs=()):
        if i == len(ds):
          # check prime numbers are covered by B, C, D
          if set(v for (d, v) in zip(ds, vs) if d in primes) != set('BCD'): return
          # and non-primes are covered by A, C, E
          if set(v for (d, v) in zip(ds, vs) if d not in primes) != set('ACE'): return
          # valid solution
          yield vs
        else:
          # consider the next date
          k = ds[i]
          # day of week (Mon = 1 ... Fri = 5)
          w = k % 7
          # consider candidates for next shift
          for v in ('BCD' if k in primes else 'ACE'):
            if w == 1:
              if v == 'C': continue
            elif w == 3:
              if v in 'BE': continue
            if v not in vs[-2:] and vs.count(v) < count[v]:
              # solve for the rest
              yield from solve(ds, i + 1, append(vs, v))
      
      # format a week
      fmt = lambda x: join(x, sep=" ", enc="()")
      
      # find possible rotas and collect solutions (= rota for first week)
      ss = multiset()
      for vs in solve(dates):
        weeks = list(chunk(vs, 5))
        printf("[{weeks}]", weeks=join((fmt(x) for x in weeks), sep=" "))
        ss.add(weeks[0])
      
      # output solutions
      for (ks, n) in ss.most_common():
        printf("{ks} [{n} solutions]", ks=fmt(ks))
      

      Solution: The rota for the first week is (Mon – Fri): Ann, Ben, Dave, Celia, Ben.

      The complete rota is:

      Week 1: A B D C B
      Week 2: E C A B C
      Week 3: A E D C B; or: E A D C B
      Week 4: E C A E C


      The prime days (of which there are 7) are covered by B, C, D (which means all A and E’s days must be non-prime).

      Similarly the non-prime days are covered by A, C, E (which means all B and D’s days must be prime).

      So between them B (4 days) and D (2 days) must cover 6 of the 7 prime days, so C must have 1 prime (and 5 non-prime) days.

      We can use this analysis to simplify the program slightly, although the run time is already very quick.

      Like

    • Frits's avatar

      Frits 9:50 pm on 26 August 2022 Permalink | Reply

      Faster when run with PyPy.

         
      from enigma import SubstitutedExpression
      
      # days  (1 - 5, 8 - 12, 15 - 19, 22, 26)
      days = list(n for n in range(1, 27) if n % 7 not in {0, 6})
      
      # prime days up to 26
      P = {3, 5, 7}
      P = {2, 3, 5} | {x for x in range(9, 26, 2) if x in days and all(x % p for p in P)}
      
      # daynum: day --> day index
      d_ind = {d: i for i, d in enumerate(days)}
      
      # invalid digit / symbol assignments
      d2i = dict()
      for d in range(0, 27):
        vs = set()
        if d == 0 or d not in days:
          vs.update('ABCDEFGHIJKLMNOPQRST')
          
        if d in P: 
          vs.update('AFGHERST')  # Ann and Ed
        else:
          vs.update('BIJKDQ')    # Ben and Dave
        
        if d % 7 == 1: 
          vs.update('CLMNOP')    # Celia not on Monday  
        if d % 7 == 3:
          vs.update('BIJKERST')  # Ben and Ed not on Wednesday 
         
        d2i[d] = vs
      
      # check gap of at least 2 work days
      def check(seq):
        for i, s in enumerate(seq[1:], start=1):
          if d_ind[s] - d_ind[seq[i - 1]] < 3: return False
        return True  
      
      # the alphametic puzzle
      p = SubstitutedExpression(
        [
          "check([B, I, J, K])",
          "check([D, Q])",
          "check([C, L, M, N, O, P])",
          "check([A, F, G, H])",
          "check([E, R, S, T])",
        ],
        answer="(A, F, G, H, B, I, J, K, C, L, M, N, O, P, D, Q, E, R, S, T)",
        base=27,
        d2i=d2i,
        env=dict(check=check),
        reorder=0,
        denest=1,     # use denest=1 if not run with PyPy
        verbose=0,    # use 256 to see the generated code
      )
      
      order = "AAAABBBBCCCCCCDDEEEE"
      # collect answers
      sols = set()
      for (_, ans) in p.solve():
        mix = sorted(zip(ans, order))
        #print(f"{', '.join(x[1] for x in mix)}")
        sols.add(', '.join(x[1] for i, x in enumerate(mix) if i < 5))
      
      # print answers
      for s in sols:  
        print(s)  
      

      Like

    • Hugh+Casement's avatar

      Hugh+Casement 7:04 am on 27 August 2022 Permalink | Reply

      Next month’s rota?? It was the 1st of August that was a Monday this year.
      It seems the puzzles editor doesn’t read the puzzles.

      Like

    • Frits's avatar

      Frits 1:06 pm on 27 August 2022 Permalink | Reply

      Similar to my previous version but faster.
      I experienced some order problems with a set for P so I decided to use a tuple.

         
      from itertools import combinations
      
      # select items from <a> that do not occur in <b>
      diff = lambda a, b: tuple(x for x in a if x not in b)
      
      # days  (1 - 5, 8 - 12, 15 - 19, 22, 26)
      days = [n for n in range(1, 27) if n % 7 not in {0, 6}]
      
      # don't use a set for P as a set is an unordered collection in CPython
      # (results differ depending on the CPython release)
      
      # primes work days up to 26
      P = (3, 5)
      P = (2, 3, 5) + tuple(x for x in range(7, 27, 2) 
                       if x in days and all(x % p for p in P))
                       
      NP = diff(days, P)
      
      # day --> day index
      d_ind = {d: i for i, d in enumerate(days)}
      
      # check for a gap between shifts of at least 2 work days
      def check(seq):
        for i in range(len(seq) - 1):
          if d_ind[seq[i + 1]] - d_ind[seq[i]] < 3: return False
        return True  
        
      sols = set()
      for b4 in combinations(P, 4):
        if not check(b4): continue              # check gaps between shifts 
        # Ben not on Wednesday
        if any(x for x in b4 if x % 7 == 3): continue
        for d2 in combinations(diff(P, b4), 2):
          if not check(d2): continue            # check gaps between shifts 
          c1 = diff(P, b4 + d2)
          # Celia not on Monday
          if c1[0] % 7 == 1: continue
      
          for c5 in combinations(NP, 5):
            # Celia not on Monday
            if any(x for x in c5 if x % 7 == 1): continue
            c6 = tuple(sorted(c5 + c1))
            if not check(c6): continue        # check gaps between shifts 
              
            for e4 in combinations(diff(NP, c5), 4):
              if not check(e4): continue          # check gaps between shifts 
              # Ed not on Wednesday
              if any(x for x in e4 if x % 7 == 3): continue
              
              a4 = diff(NP, e4 + c5)
              if not check(a4): continue        # check gaps between shifts 
              
              mix = sorted(str(y).zfill(2) + "ABCDE"[i]
                    for i, x in enumerate([a4, b4, c6, d2, e4]) for y in x)
              sols.add(tuple(x[2] for x in mix[:5]))    
           
      # print answers
      for s in sols:  
        print("answer: ", *s) 
      

      Like

  • Unknown's avatar

    Jim Randell 4:31 pm on 19 August 2022 Permalink | Reply
    Tags:   

    Teaser 3126: Sweet success 

    From The Sunday Times, 21st August 2022 [link] [link]

    My five nieces Abby, Betty, Cathy, Dolly and Emily each had some sweets. I asked them how many they had but they refused to answer directly. Instead, in turn, each possible pair from the five stepped forward and told me the total number of sweets the two of them had. All I remember is that all ten totals were different, that Abby and Betty’s total of 8 was the lowest, and that Cathy and Dolly’s total of 18 was the second highest. I also remember one of the other totals between those two but I don’t remember whose total it was. With that limited information I have worked out the total number of sweets.

    In fact it turns out that the other total I remember was Betty and Cathy’s.

    In alphabetical order of their names, how many sweets did each girl have?

    [teaser3126]

     
    • Jim Randell's avatar

      Jim Randell 5:29 pm on 19 August 2022 Permalink | Reply

      I had to read it through a few times to get the mechanics straight.

      This Python program runs in 68ms. (Internal run time is 15.3ms).

      Run: [ @replit ]

      from enigma import (defaultdict, irange, subsets, Matrix, as_int, nCr, peek, printf)
      
      # check a collection of values
      def check(vs):
        k = len(vs)
        # all pair sums are different
        ps = set(x + y for (x, y) in subsets(vs, size=2))
        if len(ps) != nCr(k, 2): return False
        # min pair sum is 8
        m = min(ps)
        if (k == 5 and m != 8) or m < 8: return False
        # only one pair sum is > 18
        n = sum(s > 18 for s in ps)
        if (k == 5 and n != 1) or n > 1: return False
        # looks OK
        return True
      
      # generate possible (A, B, C, D, E) values
      def generate():
        # for constructing equations in A, B, C, D
        eq = Matrix.equation("ABCD")
      
        # choose a non-highest pair from AC, AD, BD
        for nhp in ['AC', 'AD', 'BD']:
          # and choose values for it and BC (between 9 and 17)
          for (v1, v2) in subsets(irange(9, 17), size=2, select='P'):
            # solve the 4 equations for integer A, B, C, D
            # A + B = 8; C + D = 18; B + C = {v1}; {nhp} = {v2}
            eqs = [ eq('AB', 8), eq('CD', 18), eq('BC', v1), eq(nhp, v2) ]
            try:
              (A, B, C, D) = Matrix.linear(eqs, valid=(lambda x: as_int(x, '+')))
            except ValueError:
              continue
      
            # check all A, B, C, D pairings
            if not check([A, B, C, D]): continue
      
            # choose a value for E
            for E in irange(1, 18):
              if not check([A, B, C, D, E]): continue
              yield (A, B, C, D, E)
      
      # collect candidate values
      ss = set(generate())
      
      # record candidate totals by pair sums
      d = defaultdict(set)
      for vs in ss:
        t = sum(vs)
        for (x, y) in subsets(vs, size=2):
          d[x + y].add(t)
      
      # look for a sum between 8 and 18 (exclusive), with only one candidate total
      for (k, vs) in d.items():
        if 8 < k < 18 and len(vs) == 1:
          t = peek(vs)
          printf("[{k} -> total {t}]")
          # find candidates values with the same total, and k = B + C
          for (A, B, C, D, E) in ss:
            if B + C == k and A + B + C + D + E == t:
              printf("A={A} B={B} C={C} D={D} E={E}")
      

      Solution: The numbers of sweets are: Abby = 5; Betty = 3; Cathy = 6; Dolly = 12; Emily = 7.

      So the pairs are:

      A+B = 8
      B+C = 9
      B+E = 10
      A+C = 11
      A+E = 12
      C+E = 13
      B+D = 15
      A+D = 17
      C+D = 18
      D+E = 19


      There are 4 possible total values: 33, 34, 35, 36. Each with a single set of individual values, and 4 corresponding distributions of sweets are made by swapping the A/B and C/D pairs. (So there are 16 possible distributions in total).

      But the pair sum of 9 only appears for the values that have a total of 33.

      So we are interested in distributions with a total of 33, where B + C = 9. And only one of the four distributions with a total of 33 qualifies.

      Like

      • Jim Randell's avatar

        Jim Randell 8:58 am on 20 August 2022 Permalink | Reply

        Or using [[ decompose ]] to calculate A, B, C, D. This Python program is shorter and faster.

        It runs in 52ms. (Internal run time is 557µs).

        Run: [ @replit ]

        from enigma import (defaultdict, Decompose, irange, subsets, cproduct, peek, printf)
        
        # decompose a pair sum into viable pairs
        decompose = Decompose(2, increasing=0, sep=1, min_v=1)
        
        # record candidates by total and candidate totals by pair sums,
        (t2vs, ps2t) = (defaultdict(list), defaultdict(set))
        
        # A + B = 8; C + D = 18
        for ((A, B), (C, D)) in cproduct(decompose(x) for x in (8, 18)):
          vs4 = (A, B, C, D)
          # check pair sums (so far)
          ps4 = sorted(set(x + y for (x, y) in subsets(vs4, size=2)))
          if len(ps4) != 6 or ps4[0] < 8 or ps4[-2] > 18: continue
          # choose value for E
          for E in irange(9 - min(vs4), 18):
            # construct the pair sums
            ps = sorted(set(ps4 + [x + E for x in vs4]))
            if ps[-2] > 18: break
            # check pair sums
            if not (len(ps) == 10 and ps[0] == 8 and ps[-2] == 18): continue
            # record the results
            t = 26 + E
            t2vs[t].append(vs4 + (E,))
            for s in ps[1:-2]:
              ps2t[s].add(t)
        
        # look for a pair sum, with only 1 candidate total
        for (s, ts) in ps2t.items():
          if len(ts) == 1:
            t = peek(ts)
            printf("[{s} -> total {t}]")
            # find candidates with the same total and s = B + C
            for (A, B, C, D, E) in t2vs[t]:
              if B + C == s:
                printf("A={A} B={B} C={C} D={D} E={E}")
        

        Like

    • NigelR's avatar

      NigelR 1:31 pm on 20 August 2022 Permalink | Reply

      On a roll this week! enigma timer gives: [timing] total time: 0.0012704s (1.27ms)
      (PS: Thanks for advice on Thonny, Jim. Removing hints on indentation fixed crashing issue)

      def ptots():
         pairs = [[a,b],[a,c],[a,d],[a,e],[b,c],[b,d],[b,e],[c,d],[c,e],[d,e]]
         return sorted([sum(x) for x in pairs])
      # values within pairs might be other way round (ie b,a not a,b etc.)
      valid=[]
      for a in [1,2,3]:
          b = 8 - a
          for c in range(b,9):
              d = 18 - c
              for e in range(c+1,15):
                  if e == d:continue
                  tots = ptots()
                  if len(set(tots)) != 10 or min(tots) != 8: continue
                  if len([x for x in tots if x > 18]) != 1: continue
                  valid.append([a,b,c,d,e])
      # now look for unique pair total between 8 and 18 in 'valid'
      x=[]
      for res in valid:
          a,b,c,d,e = res
          tots = ptots()
          x = x + tots
      # generate dictionary with count of possible pair totals across all valid values
      totcount = {y:x.count(y) for y in set(x) if y > 8 and y < 18}
      # is there a pair total that occurs only once in all valid sets for a,b,c,d,e?
      uniqtot = [x for x in totcount.keys() if totcount[x] == 1]
      if len(uniqtot) != 1:
          print('Unique solution not found')
      else:
          uniqtot = uniqtot[0]
          print('Unique pair total is:', uniqtot)
          for res in valid:
              a,b,c,d,e=res
              bc = [[x,y] for x in [a,b] for y in [b,c] if x+y == uniqtot]
              if not bc: continue
              print('Unique set of values (not necessarily in abcde order) is:',a,b,c,d,e)
              if b != [y for x in bc for y in x][0]: a,b = b,a  #swap a and b if correct value not in b
              print('A=',a,'B=',b,'C=',c,'D=',d,'E=',e)
      

      Like

    • NigelR's avatar

      NigelR 5:03 pm on 20 August 2022 Permalink | Reply

      Lines 35 on above are messy and wrong – the ‘9’ in l. 37 should have been ‘uniqtot’ (which is actually 9, but putting 9 there is cheating!).
      Lines 35 on become:

       bc=[[x,y] for x in [a,b] for y in [b,c] if x+y == uniqtot]
              if not bc:continue
              print('Unique set of values (not necessarily in abcde order) is:',a,b,c,d,e)        
              if b!=[y for x in bc for y in x][0]:a,b=b,a  #swap a and b if correct value not in b
              print('A=',a,'B=',b,'C=',c,'D=',d,'E=',e)
      

      ….

      Like

      • Frits's avatar

        Frits 8:41 pm on 20 August 2022 Permalink | Reply

        Hi Nigel,

        You didn’t put code to swap c and d (if needed).

        I took the liberty of rewriting part of your code and format it according the PEP 8 style (except using indentation of two spaces). The Wing Python IDE has an option to reformat the code to PEP 8.

           
        def ptots():
           return [a+b, a+c, a+d, a+e, b+c, b+d, b+e, c+d, c+e, d+e]
        
        # values within pairs might be other way round (ie b,a not a,b etc.)
        valid = []
        for a in [1, 2, 3]:
          b = 8 - a 
          for c in range(b + 1, 9):
            d = 18 - c
            for e in range(c + 1, d):  # choose e between c and d
              if len(set(ptots())) != 10: continue
              # we already know that the lowest total is 8 and second highest is 18
              valid.append([a, b, c, d, e])
        
        # now look for unique pair total between 8 and 18 in 'valid' 
        x = []
        for a, b, c, d, e in valid:
          x += ptots()
            
        # dictionary with count of possible pair totals across all valid values
        totcount = {y: x.count(y) for y in set(x) if y > 8 and y < 18}
        # is there a pair total that occurs only once in all valid sets
        # for a, b, c, d, e?
        uniqtot = [x for x in totcount.keys() if totcount[x] == 1] 
        
        if len(uniqtot) != 1: 
          print('Unique solution not found')
        else:
          uniqtot = uniqtot[0]
          print('Unique pair total is:', uniqtot)
          for a, b, c, d, e in valid:
            
            if uniqtot not in {a + c, a + d, b + c, b + d}: continue
            print('Unique set of values (not necessarily in abcde order) is:', 
                   a, b, c, d, e)        
            
            for betty in [a, b]:
              cathy = uniqtot - betty
              if cathy not in {c, d}: continue
              print(f"A= {8 - betty} B= {betty} C= {cathy} D= {18 - cathy} E= {e}")
        

        Like

    • Frits's avatar

      Frits 7:27 pm on 20 August 2022 Permalink | Reply

      I totally forgot the teaser last night.

       
      from itertools import combinations
      
      # Emily has the second highest number of sweets.
      # Everyone has a different numbers of sweets (otherwise duplicate totals).
      # We temporarily assume a < b < c < e < d
      # b - a  must be different from d - c (otherwise duplicate totals)
      # which leads to: c may not be equal to a + 5
      
      d_missing = dict()         # count in how many solutions a total is missing
      sols = []
      # dictionary: a --> c values 
      a_c = {a: [x for x in range(6, 9) if x not in {a + 5, 8 - a}] 
                for a in range(1, 4)}
      
      for a in a_c:
        for c in a_c[a]:
          for e in range(7, 12):
            combis = [x + y for x, y in combinations([a, 8 - a, c, 18 - c, e], 2)]
            # 10 different combinations
            if len(set(combis)) != 10: continue
            # between 8 and 18 exactly two different totals must be missing
            missing = [x for x in range(9, 18) if x not in combis]
            if len(missing) != 2: continue
            for m in missing:
              d_missing[m] = d_missing.get(m, 0) + 1
      
            sols.append([missing, [a, 8 - a, c, 18 - c, e]])
       
      # "the other total I remember was Betty and Cathy's"
      for bc, v in d_missing.items():
        # check if a total is missing for all solutions but one ...
        if v != len(sols) - 1: continue
        # ... and find this specific solution 
        for m, (a, b, c, d, e) in sols:
          if bc in m: continue
          # we don't know which number a, b is Abby or Betty or 
          # which number c, d is Cathy or Dolly
      
          # make total <bc> out of the sum of (a or b) and (c or d)
          for betty in [a, b]:
            cathy = bc - betty
            if cathy in {c, d}:
              print("answer:", [8 - betty, betty, cathy, 18 - cathy, e])
      

      Like

    • NigelR's avatar

      NigelR 9:40 pm on 20 August 2022 Permalink | Reply

      Hi Frits. Thank you for taking the time to unpick my scruffy code and come up with an improvement that is so much more elegant! I had thought about the C/D swap but concluded that, by the time we get to considering B&C, C must be <D, which is implicit in the generator.

      Like

  • Unknown's avatar

    Jim Randell 3:56 pm on 12 August 2022 Permalink | Reply
    Tags:   

    Teaser 3125: The bearings’ trait 

    From The Sunday Times, 14th August 2022 [link] [link]

    At Teaser Tor trig. point I found a geocaching box. The three-figure compass bearings (bearing 000=north, 090=east, etc.) from there to the church spires at Ayton, Beeton and Seaton were needed to decode the clue to the next location.

    Each spire lay in a different compass quadrant (eg 000 to 090 is the North-East quadrant). Curiously, each of the numerals 1 to 9 occurred in these bearings and none of the bearings were prime values.

    Given the above, if you chose one village at random to be told only its church spire’s bearing, it might be that you could not calculate the other two bearings with certainty, but it would be more likely you could.

    Give the three bearings, in ascending order.

    [teaser3125]

     
    • Jim Randell's avatar

      Jim Randell 4:28 pm on 12 August 2022 Permalink | Reply

      This Python program uses the [[ SubstitutedExpression ]] solver from the enigma.py library to find possible sets of bearings.

      We then examine each set of bearings and count how many of the bearings appear in only one set (i.e. this set). It is more likely than not that if we are given the value of one of the bearings we will be able to identify all of the bearings in the set (so there need to be more unique bearings than non-unique bearings in the set), but it is possible that we won’t be able to identify the entire set given one of the bearings (so there must be at least one non-unique bearing in the set). Hence we are looking for a set with 2 unique bearings and 1 non-unique bearing in it. (I assume this is the intended interpretation of the penultimate paragraph of the puzzle text. And it does lead to a unique answer to the puzzle).

      It runs in 86ms.

      from enigma import (SubstitutedExpression, irange, multiset, printf)
      
      # find possible bearings
      p = SubstitutedExpression(
        [
          # each of the bearings
          "0 <= ABC < 360", "0 <= DEF < 360", "0 <= GHI < 360",
          # none is prime
          "not is_prime(ABC)", "not is_prime(DEF)", "not is_prime(GHI)",
          # each in a different quadrant
          "len({ABC // 90, DEF // 90, GHI // 90}) = 3",
          # remove duplicate triples
          "A < D < G",
        ],
        digits=irange(1, 9),
        answer="(ABC, DEF, GHI)",
      )
      
      # collect the bearings
      ss = list(p.answers(verbose=0))
      
      # find bearings that only appear in one set
      unique = set(x for (x, k) in multiset.from_seq(*ss).items() if k == 1)
      
      # consider possible situations
      for bs in ss:
        # are there 2 unique bearings?
        xs = unique.intersection(bs)
        if len(xs) == 2:
          printf("bearings = {bs} -> unique = {xs}", xs=sorted(xs))
      

      Solution: The three bearings are: 159°, 267°, 348°.


      There are 8 ways to distribute the digits to make a valid set of bearings:

      159, 267, 348
      168, 249, 357
      169, 247, 358
      169, 248, 357
      176, 249, 358
      176, 259, 348
      178, 249, 356
      178, 259, 346

      The bearings in bold only occur in one of the sets, so each of them uniquely identifies the set to which it belongs.

      The first set is the only one that contains more than one unique bearing, and if we were to randomly choose a bearing from this set there is a 2/3 chance that we would get a unique bearing (and so be able to identify the entire set), and a 1/3 chance we will get a bearing that appears in more than one set. So this set gives the answer to the puzzle.

      Like

      • Jim Randell's avatar

        Jim Randell 7:28 am on 13 August 2022 Permalink | Reply

        The digit 0 is not used, which excludes all 3-digit bearings below 111°. So the values of the bearings must be 1xx, 2xx, 3xx (where the x’s are the digits 4-9) in the SE, SW, NW quadrants.

        This Python program is a little shorter and a little faster than my previous solution.

        It runs in 57ms. (Internal run time is 892µs).

        from enigma import (irange, subsets, nconcat, primes, multiset, printf)
        
        # generate possible bearings sets
        def generate():
          primes.expand(360)
        
          # allocate the digits 4-9 in X = 1xx, Y = 2xx, Z = 3xx
          for (a, b, c, d, e, f) in subsets(irange(4, 9), size=len, select="P"):
            X = nconcat(1, a, b)
            Y = nconcat(2, c, d)
            Z = nconcat(3, e, f)
            if not (X < 180 and Y < 270 and Z < 360): continue
            if any(n in primes for n in (X, Y, Z)): continue
            yield (X, Y, Z)
        
        # collect the bearings
        ss = list(generate())
        
        # find bearings that only appear in one set
        unique = set(x for (x, k) in multiset.from_seq(*ss).items() if k == 1)
        
        # consider possible situations
        for bs in ss:
          # are there 2 unique bearings?
          xs = unique.intersection(bs)
          if len(xs) == 2:
            printf("bearings = {bs} -> unique = {xs}", xs=sorted(xs))
        

        Like

    • GeoffR's avatar

      GeoffR 8:42 am on 13 August 2022 Permalink | Reply

      Similar method to Jim’s first solution.

      From the multiple output answers, unique values for ABC and DEF bearings are readily apparent,
      making the identification of the unique triple bearing more than likely.

      
      % A Solution in MiniZinc
      include "globals.mzn";
      
      predicate is_prime(var int: x) = 
      x > 1 /\ forall(i in 2..1 + ceil(sqrt(int2float(ub(x))))) 
      ((i < x) -> (x mod i > 0));
      
      set of int: INT = 1..9;
      
      var INT: A;var INT: B;var INT: C;
      var INT: D;var INT: E;var INT: F;
      var INT: G;var INT: H;var INT: I;
      
      constraint all_different([A,B,C,D,E,F,G,H,I]);
      
      % the three bearings must be in quadrants 2, 3 and 4
      % i.e 90-180, 180-270 and 270-360
      % for the digits of all bearings to be in the range 1..9
      
      var 123..179: ABC == 100*A + 10*B + C;
      var 181..269: DEF == 100*D + 10*E + F;
      var 271..359: GHI == 100*G + 10*H + I;
      
      % all bearings are not prime numbers
      constraint sum([is_prime(ABC), is_prime(DEF), is_prime(GHI)]) == 0;
      
      % output bearings in ascending order
      constraint increasing([ABC, DEF, GHI]);
      
      solve satisfy;
      output[ " [ABC, DEF, GHI ] = "  ++ show([ABC, DEF, GHI ]) ];
      
      

      Like

    • Frits's avatar

      Frits 12:17 pm on 13 August 2022 Permalink | Reply

       
      from itertools import permutations
      
      # primes between 145 and 360
      P = {3, 5, 7, 11, 13, 17, 19}
      P = {x for x in range(145, 361, 2) if all(x % p for p in P)}
      
      ns = "456789"     # numbers to use for tens and units
      
      # generate additional quadrant bearings 
      def bearings(mx, bs=['']): 
        return [{new} | set(b) for b in bs 
                for p in permutations([s for s in ns if s not in "".join(b)], 2) 
                if int(new := mx[0] + "".join(p)) not in P and new < mx]           
      
      # bearings can't reside in the first NE quadrant
      
      three_barings = bearings('180', bearings('270', bearings('360')))   
      
      unique_bearings = {b for b in set(x for s in three_barings for x in s) 
                         if sum(b in s for s in three_barings) == 1}
      
      # find solutions which have at least two unique bearings so the chance
      # of calculating the other two bearings with certainty is at least 2/3
      for b3 in three_barings:
        ln = len(b3 & unique_bearings)
        if ln > 1:
          print(f"answer : {', '.join(sorted(b3))}")
      

      Like

      • Frits's avatar

        Frits 3:49 pm on 15 August 2022 Permalink | Reply

        Inspired by Nigel.

        I remembered that to rotate a matrix M we can use zip(*M).

           
        from itertools import permutations
        
        # primes between 145 and 360
        P = {3, 5, 7, 11, 13, 17, 19}
        P = {x for x in range(145, 360, 2) if all(x % p for p in P)}
        
        ns = "456789"     # numbers to be used for tens and units
        
        # generate additional quadrant bearings 
        def bearings(mx, bs=['']): 
          return [sorted({new} | set(b)) for b in bs 
                  for p in permutations([s for s in ns if s not in "".join(b)], 2)
                  if int(new := mx[0] + "".join(p)) not in P and new <= mx]   
        
        # bearings can't reside in the first NE quadrant
        three_barings = bearings('180', bearings('270', bearings('360')))   
        
        # find solutions which have at least two unique bearings so the chance
        # of calculating the other two bearings with certainty is at least 2/3
        print("answer", *[b for b in three_barings 
                          if sum([list(zip(*three_barings))[i].count(s) == 1 
                                  for i, s in enumerate(b)]) > 1])
        

        Like

        • Jim Randell's avatar

          Jim Randell 5:14 pm on 15 August 2022 Permalink | Reply

          @Frits: You don’t need to call sorted():

          def bearings(mx, bs=[[]]):
            return [[new] + b for b in bs
              ...
          

          Like

    • Frits's avatar

      Frits 12:59 pm on 14 August 2022 Permalink | Reply

      Too hot to go outside.

      Bringing down the number of viable bearings down to 15.

         
      # pick one value from each entry of a <k>-dimensional list <ns>
      # so that all elements in the picked <k> values are different
      def pickOneFromEach(ns, k=None, s=set()):
        if k == 0:
           yield s
        else:
          if k is None:
            k = len(ns)
      
          elements = set(y for x in s for y in x)
          for n in ns[k - 1]:
            if all(x not in elements for x in n):
              yield from pickOneFromEach(ns, k - 1, s | {n})
      
      # as 1, 2 and 3 are reserved for hundreds only consider 
      # bearings with last two digits 45 and higher
      
      P = {3, 5, 7, 11, 13, 17, 19}
      P = {x for x in range(145, 360, 2) if all(x % p for p in P)}
      
      dgts = set([str(i) for i in range(1, 10)])
      mx = [180, 270, 360]
      
      # determine bearings for quadrants SE, SW and NW
      quadrants = [[str(b) for b in range(100 * i + 45, mx[i - 1] + 1) 
            if b not in P                      # bearing not a prime
            and len(set(s := str(b))) == 3     # different digits
            and "0" not in s                   # no zeroes
            # do not use the hundreds digit from other quadrants
            and all(k not in s for k in "123" if k != str(i)) 
           ] for i in range(1, 4)]
      
      prt = False
      updates = True    
      while updates:
        # try to reduce the number of bearings
        updates = False
        # dictionary: digit --> bearings
        d = {i: [b for q in quadrants for b in q if i in str(b)] for i in dgts}
       
        # does a bearing lead to another bearing that leads to no solution?
        for i, q in enumerate(quadrants):
          for b1 in q:
            # check digits (except digits in bearing <b1>)
            for dgt, vs in d.items():
              if dgt in b1: continue
              
              # determine which bearings (in different quadrants) 
              # can still can occur for digit <dgt>
              b2 = [v for v in vs if v[0] != b1[0] and 
                    len(set(b1[1:]).difference(v[1:])) == 2]
                                    
              ln = len(b2)                      
              if ln == 0:
                if prt: print("remove", b1, 
                        "as then no bearings remain for digit", str(dgt) + ":",
                        tuple(d[dgt]))
                quadrants[i].remove(b1)
                break
              elif ln == 1:
                b2 = b2[0]
                # bearing <b1> leads to bearing <b2> so determine third bearing
                b3 = "".join(sorted(dgts.difference(b2 + b1)))
                
                # check if bearings xyz (b3) and xzy exist in remaining quadrant
                if all(x not in quadrants[int(b3[0]) - 1]
                       for x in [b3, b3[0] + b3[2] + b3[1]]):
                  if prt: print("remove", b1,
                          "as bearing", b1, "leads to bearing", b2, 
                          "with no bearing for remaining digits", ", ".join(b3))
                  quadrants[i].remove(b1)
                  break
            else: 
              continue  
            
            # a break occurred so there were updates
            updates = True    
          
      three_bearings = list(pickOneFromEach(quadrants))
      
      unique_bearings = {b for b in set(x for s in three_bearings for x in s) 
                         if sum(b in s for s in three_bearings) == 1}
      
      # find solutions which have at least two unique bearings so the chance
      # of calculating the other two bearings with certainty is at least 2/3
      for b3 in three_bearings:
        ln = len(b3 & unique_bearings)
        if ln > 1:
          print(f"answer: {', '.join(sorted(b3))}")
      

      Like

    • GeoffR's avatar

      GeoffR 5:46 pm on 14 August 2022 Permalink | Reply

      from itertools import permutations
      from enigma import is_prime, multiset, printf
      
      BEARINGS = []  # list of all potential bearings
      # Bearings are ABC, DEF and GHI
      
      for p1 in permutations('123456789', 3):
          A, B, C = p1
          ABC = int(A + B + C)
          if is_prime(ABC):continue
          q1 = set('123456789').difference({A, B, C})
          for p2 in permutations(q1, 3):
              D, E, F = p2
              DEF = int(D + E + F)
              if is_prime(DEF):continue
              q2 = set(q1).difference({D, E, F})
              for p3 in permutations(q2):
                  G, H, I = p3
                  GHI = int(G + H + I)
                  if is_prime(GHI): continue
                  # three 3-digit bearings using digits 1..9
                  # ...must be in quadrants 2,3 and 4 only
                  if (123 < ABC <= 179 and 181 < DEF <= 269
                     and 271 < GHI <= 359):
                      BEARINGS.append([ABC, DEF, GHI])
      
      # Output Code Credit : Jim Randell 
      # find bearings that only appear in one set
      unique = set(x for (x, k) in multiset.from_seq(*BEARINGS).items() if k == 1)
       
      # consider possible situations
      for bs in BEARINGS:
        # are there 2 unique bearings?
        xs = unique.intersection(bs)
        if len(xs) == 2:
          printf("bearings = {bs} -> unique = {xs}", xs=sorted(xs))
      
      
      
      
      

      Like

    • NigelR's avatar

      NigelR 12:54 pm on 15 August 2022 Permalink | Reply

      Version below seems to run astonishingly quickly (2.1ms) unless I’m missing something!

      from itertools import permutations as perm
      from enigma import is_prime,timer
      timer.start()
      
      def test(n): #test set of bearings whether in the right sectors and non-prime
          if n[0]>180 or n[1]>270 or n[2]>359: return False
          if [m for m in n if is_prime(m)]:return False
          return True       
      
      #generate valid sets of bearings in format [1xx,2xx,3xx]
      digs='456789' #digits without 1, 2, or 3
      y = lambda a : [int('1'+a[0]+a[1]),int('2'+a[2]+a[3]),int('3'+a[4]+a[5])]
      brgs = [y(x) for x in perm(digs) if test(y(x))]
      #regroup sets of bearings by values for each village
      vals = [[x[0] for x in brgs],[x[1] for x in brgs], [x[2] for x in brgs]]
      #check for 2 unique values in set of bearings
      sols = [x for x in brgs if [vals[0].count(x[0]),vals[1].count(x[1]),vals[2].count(x[2])].count(1)==2]
      if len(sols)==1: print('unique solution is:',sols)
      else: print ('possible sets of values are :',sols)
      timer.stop()
      timer.report()
      

      Like

      • Frits's avatar

        Frits 2:49 pm on 15 August 2022 Permalink | Reply

        Well done.

        Using

           
        if any(is_prime(m) for m in n): return False 
        

        it is even more efficient.

        Like

        • NigelR's avatar

          NigelR 12:34 am on 17 August 2022 Permalink | Reply

          Thanks Frits. I’m still at the very rewarding end of the Python learning curve (and likely to remain so for some considerable time)!

          Like

  • Unknown's avatar

    Jim Randell 5:26 pm on 5 August 2022 Permalink | Reply
    Tags:   

    Teaser 3124: Lawn order 

    From The Sunday Times, 7th August 2022 [link] [link]

    A gardener was laying out the border of a new lawn; he had placed a set of straight lawn edging strips, of lengths 16, 8, 7, 7, 7, 5, 4, 4, 4 & 4 feet, which joined at right angles to form a simple circuit. His neighbour called over the fence, “Nice day for a bit of garden work, eh? Is that really the shape you’ve decided on? If you took that one joined to its two neighbours, and turned them together through 180°, you could have a different shape. Same with that one over there, or this one over here — oh, look, or that other one”. The gardener wished that one of his neighbours would turn through 180°.

    What is the area of the planned lawn, in square feet?

    [teaser3124]

     
    • Jim Randell's avatar

      Jim Randell 12:41 pm on 6 August 2022 Permalink | Reply

      This Python program generates possible layouts from the given strips, and then checks to see if they form a non-crossing closed circuit. If so, it counts how many adjacent groups of 3 strips can be rotated to also form a different non-crossing closed circuit. If we find 4 (or more), then the original layout is a viable solution.

      The following program runs in 399ms.

      Run: [ @replit ]

      from enigma import (multiset, tuples, irange, update, reverse, ediv, join, printf)
      
      # fit the sections together, with right-angled joins, to form a circuit
      def solve(ss, x=0, y=0, dx=1, dy=0, rs=[]):
        # last section?
        if ss.size() == 1:
          r = ss.peek()
          (x_, y_) = (x + r * dx, y + r * dy)
          if x_ == y_ == 0:
            rs_ = tuple(rs + [(r, (dx, dy))])
            if check(rs_):
              yield rs_
        else:
          # choose a section
          for r in ss.keys():
            ss_ = ss.copy().remove(r)
            (x_, y_) = (x + r * dx, y + r * dy)
            if abs(x_) + abs(y_) > ss_.sum(): continue
            rs_ = rs + [(r, (dx, dy))]
            # turn one way ...
            yield from solve(ss_, x_, y_, dy, dx, rs_)
            if not rs: break
            # ... or the other
            yield from solve(ss_, x_, y_, -dy, -dx, rs_)
      
      # check the sections form a simple circuit
      def is_circuit(rs):
        x = y = 0
        # walk the path, ensuring each step visits a new spot
        pts = set()
        for (r, (dx, dy)) in rs:
          for i in irange(1, r):
            k = (x, y) = (x + dx, y + dy)
            if k in pts: return False
            pts.add(k)
        # are we back at the start?
        return (x == y == 0)
      
      # remove rotations / reflections
      def is_duplicate(rs, seen=set()):
        # have we seen it before?
        if rs in seen: return True
        # record this shape, along with its rotation / reflection
        seen.add(rs)
        (rs1, rrs) = (rs[:1], reverse(rs[1:]))
        seen.add(rs1 + rrs)
        seen.add(rs1 + tuple((r, (dx, -dy)) for (r, (dx, dy)) in rrs))
        return False
      
      # check the sections meet the puzzle requirements
      def check(rs):
        # have we seen this shape before?
        if is_duplicate(rs): return False
        # does the initial layout form a simple circuit?
        if not is_circuit(rs): return False
      
        # count the number of rotatable groups of 3 adjacent sections
        k = 0
        n = len(rs)
        for (j, ss) in enumerate(tuples(rs, 3, circular=1)):
          ((r1, d1), _, (r3, d3)) = ss
          if r1 != r3 or d1 != d3:
            # rotate the group
            rs_ = update(rs, [j, (j + 2) % n], [(r3, d3), (r1, d1)])
            k += is_circuit(rs_)
        # we need at least 4 rotatable groups
        if k < 4: return False
        # looks good
        return True
      
      # calculate the area of a polygon with vertices <vs> [see Enigma 664]
      def area(vs, m=0.5):
        return m * sum(x1 * y2 - x2 * y1 for ((x1, y1), (x2, y2)) in tuples(vs, 2, circular=1))
      
      # calculate the area of a circuit
      def circuit_area(rs):
        vs = list()
        (x, y) = (0, 0)
        for (r, (dx, dy)) in rs:
          vs.append((x, y))
          x += r * dx
          y += r * dy
        return ediv(abs(area(vs, m=1)), 2)
      
      # format a circuit
      def _fmt(rs):
        d = { (1, 0): 'E', (0, 1): 'N', (-1, 0): 'W', (0, -1): 'S' }
        for (r, k) in rs:
          yield join((d[k], r))
      fmt = lambda rs: join(_fmt(rs), sep=" ", enc="[]")
      
      # the sections
      sections = multiset.from_seq([16, 8, 7, 7, 7, 5, 4, 4, 4, 4])
      
      # find viable circuits
      for rs in solve(sections):
        printf("area = {a} {rs}", rs=fmt(rs), a=circuit_area(rs))
      

      Solution: The area of the planned lawn is 176 square feet.


      The planned layout looks like this:

      (The code used to generate the diagram is given in my program on replit).

      Starting from the bottom left-hand corner and proceeding anti-clockwise around the shape the length of the sections are:

      16 (turn left)
      4 (turn right)
      5 (turn left)
      4 (turn left)
      7 (turn right)
      4 (turn left)
      7 (turn left)
      4 (turn right)
      7 (turn left)
      8 (back to start)

      (Of course rotations and reflections of this shape will also work).

      And the four groups of three adjacent sections that can be rotated through 180°, are shown below:

      Liked by 2 people

      • Jim Randell's avatar

        Jim Randell 4:37 pm on 9 August 2022 Permalink | Reply

        And here is a faster (but slightly longer) alternative implementation.

        Instead of representing the sections as (length, (dx, dy)) tuples, we just use their lengths, as the directions alternate horizontal and vertical, and we use the sign of the length to indicate direction.

        We select sections by dividing the available sections into 2 groups the lengths of which can be assigned positive/negative values to give a zero sum in both horizontal and vertical directions.

        We then proceed as described above.

        This program runs in 63ms. (Internal run time is 7.3ms).

        Run: [ @replit ]

        from enigma import (multiset, ediv, interleave, sign, irange, reverse, tuples, update, join, printf)
        
        # choose <n> sections from <ss>, with length sum <t>
        def choose(ss, n, t, xs=[]):
          # final section?
          if n == 1:
            if t in ss or -t in ss:
              yield xs + [t]
          else:
            # choose the next section
            for x in ss.keys():
              ss_ = ss.copy().remove(x)
              yield from choose(ss_, n - 1, t - x, xs + [x])
              yield from choose(ss_, n - 1, t + x, xs + [-x])
        
        # fit sections together, with right-angled joins, to form a circuit
        def solve(ss):
          # number of horizontal/vertical sections (which must alternate)
          n = ediv(ss.size(), 2)
          # choose an initial horizontal section
          h = ss.peek()
          # find n - 1 more to get back to the start
          for hs in choose(ss.copy().remove(h), n - 1, -h, [h]):
            ss_ = ss.difference(abs(x) for x in hs)
            # choose an initial vertical section
            for v in ss_.keys():
              # find n - 1 more to get back to the start
              for vs in choose(ss_.copy().remove(v), n - 1, -v, [v]):
                rs = tuple(interleave(hs, vs))
                if check(rs):
                  yield rs
        
        # check the sections form a simple circuit
        def is_circuit(rs):
          x = y = 0
          # walk the path, ensuring each step visits a new spot
          pts = set()
          for (i, r) in enumerate(rs):
            if i % 2 == 0:
              (r, dx, dy) = (abs(r), sign(r), 0)
            else:
              (r, dx, dy) = (abs(r), 0, sign(r))
            for i in irange(1, r):
              k = (x, y) = (x + dx, y + dy)
              if k in pts: return False
              pts.add(k)
          # are we back at the start?
          return (x == y == 0)
        
        # remove rotations / reflections
        def is_duplicate(rs, seen=set()):
          # have we seen it before?
          if rs in seen: return True
          # record this shape, along with its rotation / reflection
          seen.add(rs)
          (rs1, rrs) = (rs[:1], reverse(rs[1:]))
          seen.add(rs1 + rrs)
          seen.add(rs1 + tuple((-r if i % 2 == 1 else r) for (i, r) in enumerate(rrs, start=1)))
          return False
        
        # check the sections meet the puzzle requirements
        def check(rs):
          # have we seen this shape before?
          if is_duplicate(rs): return False
          # does the initial layout form a simple circuit?
          if not is_circuit(rs): return False
        
          # count the number of rotatable groups of 3 adjacent sections
          k = 0
          n = len(rs)
          for (j, ss) in enumerate(tuples(rs, 3, circular=1)):
            (r1, _, r3) = ss
            if r1 != r3:
              # rotate the group
              rs_ = update(rs, [j, (j + 2) % n], [r3, r1])
              k += is_circuit(rs_)
          # we need at least 4 rotatable groups
          if k < 4: return False
          # looks good
          return True
        
        # calculate the area of a polygon with vertices <vs> [see Enigma 664]
        def area(vs, m=0.5):
          return m * sum(x1 * y2 - x2 * y1 for ((x1, y1), (x2, y2)) in tuples(vs, 2, circular=1))
        
        # calculate the area of a circuit
        def circuit_area(rs):
          # calculate the co-ordinates of the vertices
          vs = list()
          (x, y) = (0, 0)
          for (i, r) in enumerate(rs):
            vs.append((x, y))
            if i % 2 == 0:
              x += r
            else:
              y += r
          return ediv(abs(area(vs, m=1)), 2)
        
        # format a circuit
        def _fmt(rs):
          d = { (0, 1): 'E', (1, 1): 'N', (0, -1): 'W', (1, -1): 'S' }
          for (i, r) in enumerate(rs):
            yield join((d[(i % 2, sign(r))], abs(r)))
        fmt = lambda rs: join(_fmt(rs), sep=" ", enc="[]")
        
        # the sections
        sections = multiset.from_seq([16, 8, 7, 7, 7, 5, 4, 4, 4, 4])
        
        # find viable circuits
        for rs in solve(sections):
          printf("area = {a} {rs}", rs=fmt(rs), a=circuit_area(rs))
        

        Like

        • Frits's avatar

          Frits 6:23 pm on 9 August 2022 Permalink | Reply

          @Jim,

          Can you tell me the scope of the “seen” parameter in is_duplicate() ?
          I expected is_duplicate() to do nothing as “seen” seems to be a local variable.

          Test:

           
          def xx(a=0):
            a += 1
            print("a =", a)
            return
            
          xx()
          xx()
          
          # a = 1
          # a = 1
          
          def yy(n, b=set()):
            b.add(n)
            print("b =", b)
            return
            
          yy(2)
          yy(3)
          
          # b = {2}
          # b = {2, 3}
          

          Do set parameters always have a global scope?

          Like

          • Jim Randell's avatar

            Jim Randell 7:33 pm on 9 August 2022 Permalink | Reply

            In Python the default parameters are attributes of the object (stored under the __defaults__ key), that are initialised when the function is created and shared between calls to the function. So if you use a mutable default the value persists between calls.

            This can be a problem. If you start with a default parameter that is the empty list, add things to it and then return it at the end. The next time the function is called the the list is still full of the values from the previous invocation. This is why pylint reports mutable defaults as “dangerous”.

            But in this case I do want the value to persist between multiple invocations of the function. (So it functions like a static variable in C).

            Originally I used a global variable, but I think this looks neater. However it may not be considered “Pythonic”. But I think as you need to be aware how mutable defaults work, you should be able to use them to your advantage.

            Like

            • Frits's avatar

              Frits 8:03 pm on 9 August 2022 Permalink | Reply

              Thanks.

              Like

            • Jim Randell's avatar

              Jim Randell 8:43 am on 10 August 2022 Permalink | Reply

              There is a [[ static() ]] decorator defined in the enigma.py library, that serves a similar purpose (and works by setting attributes on the function). Using it the code for is_duplicate() would look like this:

              @static(seen=set())
              def is_duplicate(rs, seen=None):
                if seen is None: seen = is_duplicate.seen
                ...
              

              Which would probably makes it clearer to someone who is not familiar with the behaviour of mutable default function arguments.

              This is very similar to just using a global variable:

              _is_duplicate_seen = set()
              def is_duplicate(rs, seen=_is_duplicate_seen):
                ...
              

              And removing the global reference to the set() gets us back to the start:

              def is_duplicate(rs, seen=set()):
                ...
              

              Really we are just changing which namespace the set seen is stored in.

              Like

        • Frits's avatar

          Frits 5:57 pm on 10 August 2022 Permalink | Reply

          @Jim,

          I noticed this program processes 64 circuits and my program 160 circuits.
          My program probably processes too many circuits as I didn’t bother filtering out all rotations/reflections.

          You choose an initial vertical section (v). Are you sure this is allowed?

          It seems you disregard the option that v may not have to be attached to the initial horizontal section (h) in the final solution.

          Like

          • Jim Randell's avatar

            Jim Randell 6:09 pm on 10 August 2022 Permalink | Reply

            @Frits: It’s not right. It is only supposed to be the direction of the initial vertical section that is fixed. I’ll fix that up.

            (Fixed now. As it was it seemed to always choose an initial vertical 4, and one of those must be attached to 16, so all viable possibilities were considered anyway).

            Like

            • Frits's avatar

              Frits 6:58 pm on 10 August 2022 Permalink | Reply

              Now you process the expected 80 circuits.

              Like

    • Frits's avatar

      Frits 2:20 pm on 7 August 2022 Permalink | Reply

      Based on Jim’s ground work. Working with points is probably less efficient than working with directions dx/dy. The program runs in 15ms.

      Uncomment the four plot related lines for displaying the polygon.

       
      from itertools import permutations, combinations, product
      #import matplotlib.pyplot as plt
      
      # circular list,  example [H,B,C,A]: [(H,B,C), (B,C,A), (C,A,H), (A,H,B)]
      quads = lambda lst, n: [(lst[i],
                               lst[(i + 1) % n],
                               lst[(i + 2) % n],
                               lst[(i + 3) % n]) for i in range(n)]
      
      # return entries from <a> minus the entries from <b>
      def diff(a, b):
        a_ = a.copy()
        for x in b:
          a_.remove(x)
        return a_  
      
      # calculate the area of a polygon with vertices <vs>
      def area(vs):
        return abs(sum(x1 * y2 - x2 * y1
                   for ((x1, y1), (x2, y2)) in zip(vs, vs[1:] + [vs[0]]))) // 2
      
      # return all positive and negative possibilities of numbers in <lst>
      def pos_neg_possibilities(lst):
        return list(product(*((x, -x) for x in lst)))  
      
      # return vertices list from two lists of directions  
      def build_vertices(lst):
        vs = []
        (x, y) = (0, 0)
        # weave elements from two lists
        for j, p in enumerate([y for x in list(zip(lst[0], lst[1])) for y in x]):
          (x, y) = (x + p * (j % 2 == 0), y + p * (j % 2))
          vs.append((x, y))  
       
        return vs
      
      # return all points on horizontal or vertical line between points p1 and p2
      # (excluding p2 itself)  
      def points(p1, p2):
        assert p1[0] == p2[0] or p1[1] == p2[1]
       
        ver = 1 if p1[0] == p2[0] else 0
        hor = 1 - ver
        stp = 1 if p2[0] > p1[0] or p2[1] > p1[1] else -1
        if hor:
          pts = [(i, p1[1]) for i in range(p1[0], p2[0], stp)]
        else:
          pts = [(p1[0], i) for i in range(p1[1], p2[1], stp)]
       
        return pts
         
      # check if the circuit sections overlap
      def do_overlap(vs, prt=0):
        (x, y) = (0, 0)
        pts = {(x, y)}
       
        vertices = []
        # select 2 adjacent points p and q
        for j, (p, q) in enumerate(zip(vs, vs[1:] + [vs[0]])):
          for p in points(p, q):
            # only accept overlap for origin (0, 0) on last check
            if p in pts and (p != (0, 0) or j < len(vs) - 1):
              return True
            pts.add(p)  
      
        return False
         
      # check if the circuit allows for more than three rotations
      def check_rotation(vs):
        # count the number of rotatable groups of 3 adjacent sections
        k = 0
        n = len(vs)
       
        # 3 adjacent sections so check a group of 4 points
        for (j, ss) in enumerate(quads(vs, n)):
          ((x1, y1), (x2, y2), (x3, y3), (x4, y4)) = ss
          # check on different length or different direction
          if abs(x2 - x1) + abs(y2 - y1) != abs(x4 - x3) + abs(y4 - y3) or \
             ((x2 - x1 + y2 - y1) > 0) != ((x4 - x3 + y4 - y3) > 0):
           
            # rotate the group by adjusting the middle points
            vs_ = vs.copy()
            vs_[(j + 1) % n] = (x1 + x4 - x3, y1 + y4 - y3)
            vs_[(j + 2) % n] = (x4 + x1 - x2, y4 + y1 - y2)
           
            k += not(do_overlap(vs_))
       
        # we need 4 (or more) rotatable groups
        return (k > 3)  
      
      strips = sorted([16, 8, 7, 7, 7, 5, 4, 4, 4, 4], reverse=True)
      
      opts = []  
      # definition direction: direction strip (-1 or 1) times length of the strip
      
      # we split the strips into two groups for horizontal and vertical directions
      # always place the first strip horizontally
      
      # find 4 strips besides the first strip (16)
      for c in combinations(strips[1:], 4):  
        hor = c + (strips[0], )
        vert = diff(strips, hor)  # rest of strips
       
        # do combinations of negative/positive <hor> elements sum to zero?
        hor_dir = set(tuple(sorted(x)) for x in pos_neg_possibilities(hor)
                      if sum(x) == 0 and -strips[0] not in x)
        if hor_dir:
          # do combinations of negative/positive <vert> elements sum to zero?
          vert_dir = set(tuple(sorted(x)) for x in pos_neg_possibilities(vert)
                         if sum(x) == 0)
          if not vert_dir: continue        
          opts.append([(hor, vert), (hor_dir, vert_dir)])
      
      
      # check if we can find valid circuits with enough rotations
      for (hor, vert), (hor_dir, vert_dir) in opts:
        # for all combinations of directions
        for h1, v1 in product(hor_dir, vert_dir):
          # make sure highest strip (16) is up front
          # for every possible permutation of horizontals and vertical directions
          for (h2, v2) in list(set(product([(h1[-1], ) + x
                                             for x in permutations(h1[:-1])],
                                           permutations(v1)))):
            # build vertices from two lists of directions                                
            vertices = build_vertices((h2, v2))
            # integrity check  (not needed)
            if vertices[-1] != (0, 0):
              continue
           
            if do_overlap(vertices, 0): continue
            if not check_rotation(vertices): continue
           
            print("area =", area(vertices), vertices)
            #plt.plot(*zip(*vertices), '-o')
            #plt.title(vertices)
            #plt.show()
      

      Liked by 1 person

      • Jim Randell's avatar

        Jim Randell 5:26 pm on 7 August 2022 Permalink | Reply

        @Frits: Good stuff. I was beginning to worry no-one else was going to try this one.

        Like

        • Frits's avatar

          Frits 6:56 pm on 7 August 2022 Permalink | Reply

          @Jim, it was not so easy this week. I had problems with the “turn 180 degrees “concept.

          Like

    • Frits's avatar

      Frits 2:18 pm on 14 August 2022 Permalink | Reply

      Code to print polygons with 90 degree angles.

      (I have problems installing matplotlib under PyPy)

         
      # plot polygon with print statements
      # input is a list of vertices
      def print_polygon(pts):
      
        # return all points between integer coordinates p1 and p2 
        def line_points(p1, p2):
          if any(type(x) != int for x in [p1[0], p1[1], p2[0], p2[1]]):
            # not integer coordinates
            return []
          
          # number of intermediate points
          n = max(abs(p2[0] - p1[0]), abs(p2[1] - p1[1])) - 1
          
          # calculate intervals
          x1, rx = divmod(p2[0] - p1[0], n + 1)
          y1, ry = divmod(p2[1] - p1[1], n + 1)
          
          return tuple((p1[0] + i * x1 + (i * rx / (n + 1) if rx else 0), 
                        p1[1] + i * y1 + (i * ry / (n + 1) if ry else 0))
                       for i in range(n + 2))
       
        x_lo = min(x for (x, y) in pts)
        x_hi = max(x for (x, y) in pts)
        y_lo = min(y for (x, y) in pts)
        y_hi = max(y for (x, y) in pts)
        
        # build a set of all points on the polygon
        pts_pol = set()
        for p1, p2 in zip(pts, pts[1:] + [pts[0]]):  
          pts_pol.update(line_points(p1, p2))
          
        #print(sorted(pts_pol, key = lambda x: x[::-1] , reverse=True))
        
        print()
        # draw lines from top to bottom
        for v in range(y_hi, y_lo - 1, -1):
          on_line = {x for x, y in pts_pol if y == v}
          print("    |" if v % 5 else str(v).ljust(3) + "-|", end="")
          for h in range(x_lo, x_hi + 1):
            print("*" if h in on_line else  " ", end=" ")
          print()  
        
        # print three horizontal scale lines
        print("    |", end="")
        for x in range(x_lo + 1, x_hi + 1):
          print("__", end="")
        print("_")  
        
        print("    ", end="")
        for i, x in enumerate(range(x_lo, x_hi + 1)):
          print("  " if x % 5 else " |", end="")
        print()  
        
        print("    ", end="")
        for x in range(x_lo, x_hi + 1):
          print("  " if x % 5 else str(x).rjust(2), end="")
        print()  
      
      print_polygon([(16, 0), (16, 4), (21, 4), (21, 8), (14, 8), (14, 12), (7, 12), (7, 8), (0, 8), (0, 0)])       
      
       
       
          |              * * * * * * * *
          |              *             *
      10 -|              *             *
          |              *             *
          |* * * * * * * *             * * * * * * * *
          |*                                         *
          |*                                         *
      5  -|*                                         *
          |*                               * * * * * *
          |*                               *
          |*                               *
          |*                               *
      0  -|* * * * * * * * * * * * * * * * *
          |___________________________________________
           |         |         |         |         |
           0         5        10        15        20
      

      Like

      • Jim Randell's avatar

        Jim Randell 5:14 pm on 14 August 2022 Permalink | Reply

        @Frits: You might be interested in the simple plotting library I use to generate a lot of the diagrams for puzzles (including for the solution of this puzzle – I included code to plot the shapes in the code I put on replit). It only requires the tkinter library. I use it with pypy all the time.

        I have uploaded it to GitHub. [@github]

        Like

  • Unknown's avatar

    Jim Randell 3:16 pm on 29 July 2022 Permalink | Reply
    Tags:   

    Teaser 3123: A six-pipe problem 

    From The Sunday Times, 31st July 2022 [link] [link]

     

    A factory makes six types of cylindrical pipe, A to F in decreasing size, whose diameters in centimetres are whole numbers, with type A 50 per cent wider than type B. The pipes are stacked in the yard as a touching row of As with an alternating row of touching Bs and Cs in the next layer, with each B touching two As. Type Ds fill the gap between the As and the ground; Es fill the gap between As and the Bs; and Fs fill the gap between As, Ds and the ground. Finally another row of As is put on top of the stack, giving a height of less than 5 metres.

    What is the final height of the stack in centimetres?

    [teaser3123]

     
    • Jim Randell's avatar

      Jim Randell 3:55 pm on 29 July 2022 Permalink | Reply

      The title of this puzzle is presumably an allusion to Sherlock Holmes’ “three pipe problem” in The Red-Headed League.

      Some judicious applications of Pythogoras’ theorem gets us the answer.

      This Python program runs in 54ms. (Internal runtime is 313µs).

      Run: [ @replit ]

      from enigma import (irange, div, is_square, sq, printf)
      
      # suppose the pipes have diameters a, b, c, d, e, f (in cm)
      # we know h = 2a + c < 500; b = (2/3)a
      for a in irange(6, 249, step=3):
        b = div(2 * a, 3)
        # from pipes ABC
        c = a - b
        # calculate the height of the stack (in centimetres)
        h = 2 * a + c
        if not (h < 500): break
        # from pipes AAD: (a + d)^2 = a^2 + (a - d)^2
        d = div(a, 4)
        if d is None: continue
        # from pipes ABC: (a + e)^2 = (a + c - b - e)^2 + (b + c)^2
        e = div(sq(b) + sq(c) - a * (b - c), 2 * a - b + c)
        if e is None: continue
        # from pipes ADF: (d + f)^2 = (d - f)^2 + x^2; (a + f)^2 = (a - f)^2 + y^2; x + y = a
        r = is_square(a * d)
        if r is None: continue
        f = div(sq(a), 4 * (a + d + 2 * r))
        if f is None: continue
        if a > b > c > d > e > f > 0:
          # output solution (in cm)
          printf("height = {h} cm [a={a} b={b} c={c} d={d} e={e} f={f}]")
      

      Solution: The height of the stack is 420 cm.

      Note that the derivation of d requires a to also be divisible by 4 (as well as 3), so we could consider just multiples of 12 for a for a slight increase in speed.


      Manually:

      If we suppose a = 180k, then we can simplify the expressions used in the program to give the following values:

      a = 180k
      b = 120k
      c = 60k
      d = 45k
      e = 24k
      f = 20k

      And we require all these values to be positive integers, so k takes on a positive integer value.

      The total height of the stack is h = 2a + c = 420k, and we require h < 500.

      So the only permissible value is k = 1, and the answer follows directly.

      Like

    • Frits's avatar

      Frits 2:26 pm on 2 August 2022 Permalink | Reply

      Problem is when to stop with your analysis.
      Ultimately each diameter can be expressed in terms of the smallest diameter.

         
      from enigma import SubstitutedExpression
      
      # AGH, BIJ, CK, DL, EM and FN variables are diameters
      # if (x - y)^2 = z^2 then (2x - 2y)^2 = (2z)^2
      # so we can also calculate with diameters in the Pythagorean formulae
      
      # the alphametic puzzle
      p = SubstitutedExpression(
        [
          # type A 50 per cent wider than type B
          # diameter A is equal to diameter C + two times radius B
          # a height of less than 5 metres so 2a + c = 7c < 500
          "1 < CK < 72",
          "3 * CK = AGH",
          "2 * CK = BIJ",
          
          # (a + d)^2 = a^2 + (a - d)^2 so 4ad = a^2
          "div(AGH, 4) = DL",
          
          # (a + e)^2 = (a + c - b - e)^2 + (b + c)^2 using a = 3c and b = 2c
          # 6ce + 9c^2 = 4c^2 - 4ce + 9c^2
          "div(2 * CK, 5) = EM",
          
          # (d + f)^2 = (d - f)^2 + x^2; (a + f)^2 = (a - f)^2 + (a - x)^2;
          # so 4df = x^2 and 4af = (a - x)^2
          "4.0 * AGH * FN == (AGH - 2 * (DL * FN)**.5)**2",
          
          # A to F in decreasing size
          "AGH > BIJ > CK > DL > EM > FN"
        ],
        answer="2 * AGH + CK, (AGH, BIJ, CK, DL, EM, FN)",
        d2i=dict([(k, "CF") for k in range(8, 10)]),
        distinct="",
        verbose=0,    # use 256 to see the generated code
      )
      
      # print answers
      for (_, ans) in p.solve():
        print(f"the final height of the stack in centimetres: {ans[0]}")
      

      Like

  • Unknown's avatar

    Jim Randell 4:03 pm on 22 July 2022 Permalink | Reply
    Tags:   

    Teaser 3122: Bank robbery 

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

    Five witnesses were interviewed following a robbery at the bank in the High Street. Each was asked to give a description of the robber and his actions.

    The details given were: height; hair colour; eye colour; weapon carried; escape method.

    Witness 1: short; fair; brown; cricket bat; motorbike.

    Witness 2: tall; fair; grey; gun; car.

    Witness 3: tall; dark; brown; crowbar; motorbike.

    Witness 4: short; ginger; blue; knife; car.

    Witness 5: tall; dark; blue; stick; pushbike.

    When the police caught up with the perpetrator, they found that each of the five witnesses had been correct in exactly two of these characteristics.

    What was the robber carrying, and how did he get away?

    [teaser3122]

     
    • Jim Randell's avatar

      Jim Randell 4:15 pm on 22 July 2022 Permalink | Reply

      It is straightforward to try all possible combinations. (Assuming the robber has a unique single value for each characteristic).

      I include an “other” value for each characteristic to account for possibilities where none of the witnesses have correctly described it.

      This Python program runs in 58ms. (Internal runtime is 1.9ms).

      Run: [ @replit ]

      from enigma import (cproduct, join, printf)
      
      # descriptions
      descs = [
        dict(S='short', T='tall', X='other'),
        dict(F='fair', D='dark', G='ginger', X='other'),
        dict(R='brown', G='grey', L='blue', X='other'),
        dict(B='cricket bat', G='gun', C='crowbar', K='knife', S='stick', X='other'),
        dict(M='motorbike', C='car', P='pushbike', X='other'),
      ]
      
      # statements
      statements = [ 'SFRBM', 'TFGGC', 'TDRCM', 'SGLKC', 'TDLSP' ]
      
      # how many characteristics are correct?
      correct = lambda xs, ys: sum(x == y for (x, y) in zip(xs, ys))
      
      # consider characteristics of the perp
      for cs in cproduct(d.keys() for d in descs):
        # each witness gave 2 correct statements
        if all(correct(xs, cs) == 2 for xs in statements):
          # output solution
          printf("{cs}", cs=join((d[k] for (d, k) in zip(descs, cs)), sep=", "))
      

      Solution: The robber was carrying a knife. He made his escape by motorbike.

      In fact we can determine a complete description:

      height = tall
      hair = fair
      eyes = blue
      weapon = knife
      vehicle = motorbike

      Like

      • Jim Randell's avatar

        Jim Randell 8:48 am on 26 July 2022 Permalink | Reply

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

        The following run file executes in 72ms.

        Run: [ @replit ]

        #! python3 -m enigma -rr
        
        # statements:
        #
        #  A = height is short; B = height is tall
        #  C = hair is fair; D = hair is dark; E = hair is ginger
        #  F = eyes are brown; G = eyes are grey; H = eyes are blue
        #  I = weapon is cricket bat; J = weapon is gun; K = weapon is crowbar; L = weapon is knife; M = weapon is stick
        #  N = vehicle is motorbike; P = vehicle is car; Q = vehicle is pushbike
        
        SubstitutedExpression
        
        # binary statements
        --base=2
        --distinct=""
        
        # at most one of the statements in each category is true
        "not (A + B > 1)"
        "not (C + D + E > 1)"
        "not (F + G + H > 1)"
        "not (I + J + K + L + M > 1)"
        "not (N + P + Q > 1)"
        
        # witnesses each make exactly 2 true statements
        "A + C + F + I + N == 2"
        "B + C + G + J + P == 2"
        "B + D + F + K + N == 2"
        "A + E + H + L + P == 2"
        "B + D + H + M + Q == 2"
        
        --template=""
        

        This construction also leads to a simple manual solution:

        In the matrix of statements (lines 25 – 29), each row sums to 2. So the total sum of all matrix elements is 10.

        Looking at the columns of the matrix we get the following potential column totals:

        height: 0 (X), 2 (A), 3 (B)
        hair: 0 (X), 1 (E), 2 (C, D)
        eyes: 0 (X), 1 (G), 2 (F, H)
        weapon: 0 (X), 1 (I, J, K, L, M)
        vehicle: 0 (X), 1 (Q), 2 (N, P)

        A grand total of 10 can only be achieved by taking the maximum value for each column.

        So we can eliminate all the X’s and A, E, G, Q (all of which must = 0). Hence B = 1.

        One of C, D = 1 (and the other = 0). If D = 1, then witnesses 3 and 5 have achieved their 2 correct statements so: F, H, K, M, N = 0, but one of F, H = 1. So D = 0 and C = 1.

        We can then complete the assignment of values, and determine the true statements are: B, C, H, L, N.

        Like

    • GeoffR's avatar

      GeoffR 5:51 pm on 23 July 2022 Permalink | Reply

      
      # List of possible witness statements
      statements = []
      
      # Witness statements
      W1 = ['short', 'fair', 'brown', 'cricket bat', 'motorbike']
      W2 = ['tall', 'fair', 'grey', 'gun', 'car' ]
      W3 = ['tall', 'dark', 'brown', 'crowbar', 'motorbike' ]
      W4 = ['short', 'ginger', 'blue', 'knife', 'car' ] 
      W5 = ['tall', 'dark', 'blue', 'stick', 'pushbike' ]
      
      # Form lists of all possible witness statements
      for a in ('short', 'tall'): 
        for b in ('fair', 'dark', 'ginger'):
          for c in ('brown', 'grey', 'blue'):
            for d in ('cricket bat', 'gun', 'crowbar', 'knife', 'stick'):
              for e in ('motorbike', 'car', 'pushbike'):
                statements.append([a, b, c, d, e])
      
      for st in statements:
          a, b, c, d, e = st
          # Two statements from five are true for each witness
          # test Witness No.1 statements
          if sum([a == 'short', b == 'fair', c == 'brown', \
                  d == 'cricket bat', e == 'motorbike']) == 2:
            
            #test Witness No.2 statements
            if sum([a == 'tall', b == 'fair', c == 'grey', \
                    d == 'gun', e == 'car']) == 2:
              
              # test Witness No.3 statements
              if sum([ a == 'tall', b == 'dark', c == 'brown', \
                       d == 'crowbar', e == 'motorbike']) == 2:
              
                # test Witness No.4 statements
                if sum([a == 'short', b == 'ginger', c == 'blue', \
                        d == 'knife', e == 'car']) == 2:
                  
                  # test Witness No.5 statements
                  if sum([ a == 'tall', b == 'dark', c == 'blue', \
                           d == 'stick', e == 'pushbike']) == 2:
                    print(f"The robber was {a}.")
                    print(f"He had {b} coloured hair and {c} colour eyes.")
                    print(f"He carried a {d} as a weapon, escaping on a {e}.")
                    
      
      
      

      Like

    • GeoffR's avatar

      GeoffR 9:41 pm on 23 July 2022 Permalink | Reply

      Indexing the witness statement lists is neater:

      
      # List of possible witness statements
      statements = []
      
      # Witness statements
      W1 = ['short', 'fair', 'brown', 'cricket bat', 'motorbike']
      W2 = ['tall', 'fair', 'grey', 'gun', 'car' ]
      W3 = ['tall', 'dark', 'brown', 'crowbar', 'motorbike' ]
      W4 = ['short', 'ginger', 'blue', 'knife', 'car' ] 
      W5 = ['tall', 'dark', 'blue', 'stick', 'pushbike' ]
      
      # Form lists of all possible witness statements
      for a in ('short', 'tall'): 
        for b in ('fair', 'dark', 'ginger'):
          for c in ('brown', 'grey', 'blue'):
            for d in ('cricket bat', 'gun', 'crowbar', 'knife', 'stick'):
              for e in ('motorbike', 'car', 'pushbike'):
                statements.append([a, b, c, d, e])
      
      for st in statements:
          a, b, c, d, e = st
          # Two statements from five are true for each witness
          # test Witness No.1 statements
          if sum([a == W1[0], b == W1[1], c == W1[2], \
                  d == W1[3], e == W1[4]]) == 2:
            
            # test Witness No.2 statements
            if sum([a == W2[0], b == W2[1], c == W2[2], \
                    d == W2[3], e == W2[4]]) == 2:
              
              # test Witness No.3 statements
              if sum([ a == W3[0], b == W3[1], c == W3[2], \
                       d == W3[3], e == W3[4]]) == 2:
              
                # test Witness No.4 statements
                if sum([a == W4[0], b == W4[1], c == W4[2], \
                        d == W4[3], e == W4[4]]) == 2:
                  
                  # test Witness No.5 statements
                  if sum([ a == W5[0], b == W5[1], c == W5[2], \
                           d == W5[3], e == W5[4]]) == 2:
                    print(f"The robber was {a}.")
                    print(f"He had {b} coloured hair and {c} colour eyes.")
                    print(f"He carried a {d} as a weapon, escaping on a {e}.")
                    
      
      
      
      

      Like

    • GeoffR's avatar

      GeoffR 12:07 pm on 1 August 2022 Permalink | Reply

      % A Solution in MiniZinc
      include "globals.mzn";
      
      set of int: TF = {1,0};
      
      var TF:short; var TF:tall;
      var TF:h_fair; var TF:h_dark; var TF:h_ginger;
      var TF:e_brown; var TF:e_grey; var TF:e_blue;
      var TF:c_bat; var TF:gun; var TF:c_bar; var TF:knife; var TF:stick;
      var TF:mbike; var TF:car; var TF:pbike;
      
      % Values are 0 or 1 for main variables 
      % ..i.e. for height, hair colour, eye colour, weapon, vehicle
      constraint sum([short, tall]) < 2;
      constraint sum([h_fair, h_dark, h_ginger]) < 2;
      constraint sum([e_brown, e_grey, e_blue]) < 2;
      constraint sum([c_bat, gun, c_bar, knife, stick]) < 2;
      constraint sum([mbike, car, pbike]) < 2;
      
      % 5 witness statements - 2 are true for each witness
      constraint sum([short, h_fair, e_brown,c_bat, mbike]) == 2;
      constraint sum([tall, h_fair, e_grey, gun, car]) == 2;
      constraint sum([tall, h_dark, e_brown, c_bar, mbike]) == 2;
      constraint sum([short, h_ginger, e_blue, knife, car]) == 2;
      constraint sum([tall, h_dark, e_blue, stick, pbike]) ==  2;
      
      solve satisfy;
      
      output [" [short, tall ] = " ++ show([ short, tall ]) ++ "\n"
      ++ " [h_fair, h_dark, h_ginger] = " ++ show([ h_fair, h_dark, h_ginger])  
      ++ "\n" ++ " [e_brown, e_grey, e_blue] = " ++ show([e_brown, e_grey, e_blue] )
      ++ "\n" ++ " [c_bat, gun, c_bar, knife, stick] = " 
      ++ show([c_bat, gun, c_bar, knife, stick]) 
      ++ "\n" ++ " [mbike, car, pbike] = "  ++ show([mbike, car, pbike]) ];
      
      %  [short, tall ] = [0, 1]
      %  [h_fair, h_dark, h_ginger] = [1, 0, 0]
      %  [e_brown, e_grey, e_blue] = [0, 0, 1]
      %  [c_bat, gun, c_bar, knife, stick] = [0, 0, 0, 1, 0]
      %  [mbike, car, pbike] = [1, 0, 0]
      % ----------
      % ==========
      % i.e. Robber was tall, had fair hair, blue eyes, with a knife, escaping on a motorbike.
      
      
      
      

      Like

  • Unknown's avatar

    Jim Randell 4:03 pm on 15 July 2022 Permalink | Reply
    Tags:   

    Teaser 3121: Top marks 

    From The Sunday Times, 17th July 2022 [link] [link]

    A teacher is preparing her end of term class test. After the test she will arrive at each pupil’s score by giving a fixed number of marks for each correct answer, no marks for a question that is not attempted, and deducting a mark for each incorrect answer. The computer program she uses to prepare parents’ reports can only accept tests with the number of possible test scores (including negative scores) equal to 100.

    She has worked out all possible combinations of the number of questions asked and marks awarded for a correct answer that satisfy this requirement, and has chosen the one that allows the highest possible score for a pupil.

    What is that highest possible score?

    [teaser3121]

     
    • Jim Randell's avatar

      Jim Randell 4:25 pm on 15 July 2022 Permalink | Reply

      If there are n questions asked, and there are a marks for a correct answer, then the possible scores are:

      score = ax − z; where x + z ≤ n

      And we need there to be exactly 100 possible scores.

      There are tri(n + 1) different (correct, unanswered, incorrect) ways the n questions can be answered.

      So we need n ≥ 13 in order for there to be 100 or more different combinations (some may have the same score).

      Here’s a quick program to solve the puzzle.

      It runs in 66ms.

      Run: [ @replit ]

      from enigma import (irange, inf, Accumulator, printf)
      
      # find the highest possible score (all answers correct)
      r = Accumulator(fn=max, collect=1)
      
      # consider possible marks for a correct answer
      for a in irange(1, 10):
        # consider possible numbers of questions
        for n in irange(13, inf):
          # calculate possible scores
          ss = set(a * x - z for x in irange(0, n) for z in irange(0, n - x))
          k = len(ss)
          if k > 100: break
          if k == 100:
            m = max(ss) # = a * n
            r.accumulate_data(m, (a, n))
            printf("[a={a} n={n} m={m}]")
      
      # output solutions
      printf("max score = {m}")
      for (a, n) in r.data:
        printf("-> {n} questions; {a} marks for a correct answer")
      

      Solution: The maximum possible score is 105.

      There are 15 questions, and 7 marks for each correct answer. The maximum is therefore 15×7 = 105.

      It is also possible to have 100 potential scores with 21 questions, and 4 marks for each correct answer. In this case the maximum is 21×4 = 84.

      Like

    • Frits's avatar

      Frits 7:28 pm on 15 July 2022 Permalink | Reply

         
      m = 0 
      # consider possible marks for a correct answer
      for a in range(1, 11):
        # range [-n, -n+1, ..., -1, 0, 1, ..., a*n]
        # max scores            = (a + 1) * n + 1    
        # nr impossible to make = a * (a - 1) / 2
      
        # n.a + n + 1 - 1/2 a.a + 1/2 a = 100
        # (2a + 2)n = a.a - a + 198
        n, r = divmod(a * a - a + 198, 2 * a + 2)
        if r: continue
        
        m = max(m, a * n)
      
      print("answer:", m)  
      

      Like

      • Jim Randell's avatar

        Jim Randell 8:59 am on 16 July 2022 Permalink | Reply

        I wondered how you arrived at your formula for the unreachable marks.

        I came up with this:

        If we get all the questions right the (maximum possible) score is: a.n

        If we get all but one of the questions right (and don’t attempt the remaining question) the next lowest score is: a(n − 1).

        So there are (a − 1) unreachable scores between these.

        If, however, we get the remaining question wrong, we get a score of: a(n − 1) − 1.

        The there are only (a − 2) unreachable scores in the next gap.

        If we get all but two of the questions right the possible scores are: a(n − 2), a(n − 2) − 1, a(n − 2) − 2.

        And there are (a − 3) unreachable scores in the following gap.

        So, in each gap the number of unreachable scores decreases by 1.

        Hence the total number of unreachable scores is: tri(a − 1), providing na. (All possible negative scores are reachable).

        And using this we can determine the number of possible scores without having to construct them.

        And we can proceed manually (there are only 10 values to check), or programatically:

        Run: [ @replit ]

        from enigma import (irange, inf, tri, Accumulator, printf)
        
        # find the highest possible score (all answers correct)
        r = Accumulator(fn=max, collect=1)
        
        # consider possible marks for a correct answer
        for a in irange(1, inf):
          # "unreachable" scores
          u = tri(a - 1)
          # find number of questions (100 = m + n + 1 - u)
          (n, q) = divmod(99 + u, a + 1)
          if n < 13: break
          if q != 0: continue
          assert n >= a
          # max possible score
          m = a * n
          r.accumulate_data(m, (a, n))
          printf("[a={a} n={n} m={m}]")
        
        # output solutions
        printf("max score = {m}")
        for (a, n) in r.data:
          printf("-> {n} questions; {a} marks for a correct answer")
        

        Manually, we can consider increasing a values, calculate u values (by just adding the preceding a value), and compute n = (99 + u) / (a + 1). We need n to be an integer ≥ 13. The calculations proceed as follows:

        a = 1; u = 0; n = 99/2 = 49r1
        a = 2; u = 1; n = 100/3 = 33r1
        a = 3; u = 3; n = 102/4 = 25r2
        a = 4; u = 6; n = 105/5 = 21 [candidate solution]
        a = 5; u = 10; n = 109/6 = 18r1
        a = 6; u = 16; n = 115/7 = 16r2
        a = 7; u = 21; n = 120/8 = 15 [candidate solution]
        a = 8; u = 28; n = 127/9 = 14r1
        a = 9; u = 36; n = 135/10 = 13r5
        a = 10; u = 45; n = 144/11 = 13r1
        a = 11; u = 55; n = 154/12 = 12r10 [n < 13]

        There are two candidate solutions a=4, n=21 ⇒ max=84 and a=7, n=15 ⇒ max=105, so we want the second one.

        Liked by 1 person

        • Frits's avatar

          Frits 12:48 pm on 16 July 2022 Permalink | Reply

          I printed and analyzed the sorted ss lists of your program and noticed that
          the first unreachable number always is F = (n – (a – 2)) * a – (a – 1).
          This happens if the number of correct answers is equal to n – (a – 2).

          the next two unreachable numbers always are:
          (F + a, F + a + 1)
          the next three unreachable numbers always are:
          (F + 2*a, F + 2*a + 1, F + 2*a + 2)
          etc…

          Like

      • Frits's avatar

        Frits 12:27 am on 17 July 2022 Permalink | Reply

        If a >= n then the number of possible test scores is independent of a.
        The number of unreachable scores is then tri(a – 1) – tri(a – n – 1).

        The number of possible test scores is equal to (n + 1) * (n + 2) / 2.
        The number of possible test scores equal to 100 leads to the formula
        (n + 1) * (n + 2) = 200

        A positive solution for n is (3 * sqrt(89) – 3) / 2 = 12.65 so
        not an integer solution.

        Like

    • Frits's avatar

      Frits 6:09 pm on 21 July 2022 Permalink | Reply

      Formula (2a + 2).n = a.a – a + 198 is valid if marks for a correct answer (a) is not higher than the number of questions (n). If a is higher or equal to n then n must be a non-integer solution (approx 12.65).

      Another option would have been to use divisor_pairs().

         
      #! python3 -m enigma -r
      
      # rearrangement idea from John Crabtree:
      
      # formula (2a + 2).n = a.a - a + 198
      # can be rearranged to 2.n + 3 = 200 / (a + 1) + (a + 1) 
      # so both terms of RHS are divisors of 200
       
      SubstitutedExpression
      
      # find divisors of 200
      "div(200, AB) = DEF"
      "1 < AB <= DEF"
      
      # make sure exactly one of them is odd
      "(AB + DEF) % 2"
      
      # marks for a correct answer: AB - 1
      # number of question: (AB + DEF - 3) / 2
      --answer="(AB - 1) * (AB + DEF - 3) // 2"
      --distinct=""
      --accumulate=max
      --invalid="2-9,A"
      --verbose=16
      

      Like

      • Jim Randell's avatar

        Jim Randell 3:04 pm on 22 July 2022 Permalink | Reply

        I also did a solution based on John Crabtree’s analysis:

        Run: [ @replit ]

        from enigma import (Accumulator, divisors_pairs, div, printf)
        
        # based on John Crabtree's derivation: 2n + 3 = 200/(a + 1) + (a + 1)
        
        # find the highest possible score (all answers correct)
        r = Accumulator(fn=max, collect=1)
        
        # look for divisors of 200
        for (p, q) in divisors_pairs(200, every=1):
          a = p - 1
          if a < 1: continue
          # calculate n
          n = div(p + q - 3, 2)
          if n is None or n < a: continue
          m = a * n
          r.accumulate_data(m, (a, n))
          printf("[a={a}: n={n} m={m}]")
        
        # output solutions
        printf("max score = {m}")
        for (a, n) in r.data:
          printf("-> {n} questions; {a} marks for a correct answer")
        

        It is neat because we sum the divisor pairs.

        But it is more straightforward (and slightly faster) just to consider increasing a values (as in my second program).

        Like

  • Unknown's avatar

    Jim Randell 4:50 pm on 8 July 2022 Permalink | Reply
    Tags:   

    Teaser 3120: Bus stop blues 

    From The Sunday Times, 10th July 2022 [link] [link]

    While waiting for buses, I often look out for interesting number plates on passing cars. From 2001 the UK registration plate format has been 2 letters + a 2-digit number + 3 more letters, the digits being last two of the year of registration with 50 added after six months (for example in 2011, the possible numbers were 11 and 61). I spotted one recently with its five letters in alphabetical order, all different and with no vowels. Looking more closely, I saw that if their numerical positions in the alphabet (A = 1, B = 2 etc.) were substituted for the 5 letters, their sum plus 1 was the 2-digit number and the sum of their reciprocals was equal to 1.

    Find the 7-character registration.

    [teaser3120]

     
    • Jim Randell's avatar

      Jim Randell 4:59 pm on 8 July 2022 Permalink | Reply

      I used the [[ reciprocals() ]] function from the enigma.py library (originally written for Enigma 348).

      This Python program runs in 63ms. (Internal run time is 174µs).

      Run: [ @replit ]

      from enigma import (reciprocals, printf)
      
      # letters (1-indexed)
      letters = "?ABCDEFGHIJKLMNOPQRSTUVWXYZ"
      
      # vowels: A=1, E=5, I=9, O=15, U=21
      vowels = set(letters.index(x) for x in "AEIOU")
      
      # find 5 reciprocals that sum to 1
      for ns in reciprocals(5, M=26, g=1):
        # no vowels allowed
        if vowels.intersection(ns): continue
        # calculate the year
        y = sum(ns) + 1
        if not (1 < y < 23 or 50 < y < 72): continue
        # output the registration
        (A, B, X, Y, Z) = (letters[n] for n in ns)
        printf("reg = [{A}{B} {y:02d} {X}{Y}{Z}]")
      

      Solution: The registration is: BD 51 HLX.

      Note that the “vowels” restriction is not necessary if the restriction on the year number being within [02, 22] or [51, 71] is observed.

      Like

    • Frits's avatar

      Frits 6:39 pm on 8 July 2022 Permalink | Reply

         
      from enigma import SubstitutedExpression
      
      # the alphametic puzzle
      p = SubstitutedExpression(
        [
          # its five letters converted to numerical positions
          # AB, CD, EF, GH, IJ
          "1 < AB < 23",
          "AB < CD < 24",
          "CD < EF < 25",
          "EF < GH < 26",
          "GH < IJ < 27",
          
          # no vowels allowed, A=1, E=5, I=9, O=15, U=21
          "all(x not in {1, 5, 9, 15, 21} for x in [AB, CD, EF, GH, IJ])",
          # ranges for the sum
          "(AB + CD + EF + GH + IJ) < 22 or \
           49 < (AB + CD + EF + GH + IJ) < 72",
          # the sum of their reciprocals was equal to 1
          "1 / AB + 1 / CD + 1 / EF + 1 / GH + 1 / IJ == 1",
        ],
        answer="AB, CD, EF, GH, IJ",
        d2i="",
        distinct="",
        verbose=0,    # use 256 to see the generated code
      )
      
      # print answers
      for (_, ans) in p.solve():
        reg = chr(ord('A') + ans[0] - 1) + \
              chr(ord('A') + ans[1] - 1) + " "
        reg += str(sum(ans) + 1) + " "
        reg += chr(ord('A') + ans[2] - 1) + \
               chr(ord('A') + ans[3] - 1) + \
               chr(ord('A') + ans[4] - 1)
        print("registration =", reg)
      

      Like

      • Jim Randell's avatar

        Jim Randell 9:38 pm on 8 July 2022 Permalink | Reply

        Or we can use base 27 to make things a bit neater:

        Run: [ @replit ]

        #! python3 -m enigma -rr
        
        SubstitutedExpression
        
        # allow digits 1-26
        --base=27
        # but exclude vowels (1, 5, 9, 15, 21)
        --digits="1-26,!1,!5,!9,!15,!21"
        
        # numbers are in increasing order
        "A < B" "B < X" "X < Y" "Y < Z"
        
        # reciprocals sum to 1
        "fraction(1, A,  1, B,  1, X,  1, Y,  1, Z) == (1, 1)"
        
        # check year
        --code="year = lambda n: (2 <= n <= 22) or (51 <= n <= 71)"
        "year(A + B + X + Y + Z + 1)"
        
        # output the registration
        --code="l = lambda x: int2base(x, base=27, digits='?ABCDEFGHIJKLMNOPQRSTUVWXYZ')"
        --code="n = lambda x: int2base(x, width=2)"
        --code="reg = lambda A, B, X, Y, Z: sprintf('{A}{B} {y} {X}{Y}{Z}', A=l(A), B=l(B), X=l(X), Y=l(Y), Z=l(Z), y=n(A + B + X + Y + Z + 1))"
        --answer="reg(A, B, X, Y, Z)"
        --template=""
        

        Like

    • GeoffR's avatar

      GeoffR 8:27 pm on 8 July 2022 Permalink | Reply

      % A Solution in MiniZinc
      include "globals.mzn";
      
      % Omitting vowel values for A,E,I,O,U
      % Used to translate numerical letters in output(v,w,x,y,z) to letters
      int:B == 2; int:C == 3; int:D == 4; int:F == 6; int:G == 7;
      int:H == 8; int:J == 10; int:K == 11; int:L == 12; int:M == 13;
      int:N == 14; int:P == 16; int:Q == 17; int:R == 18; int:S == 19;
      int:T == 20; int:V == 22; int:W == 23; int:X == 24; int:Y == 25;
      int:Z == 26;
      
      % number plate format is v w num x y z  (num is 2 digits)
      var 2..26:v; var 2..26:w; var 2..26:x;
      var 2..26:y; var 2..26:z;
      
      % last 2 digits in number plate - range = 2001 to 2022 + 50
      var 01..72: num;
      
      set of int: letters = {B,C,D,F,G,H,J,K,L,M,N,P,Q,R,S,T,V,W,X,Y,Z};
      
      % Five number plate letters
      constraint v in letters /\ w in letters /\ x in letters
      /\ y in letters /\ z in letters;
      
      % five letters in number plate are in alphabetical order
      constraint w > v /\ x > w /\ y > x /\ z > y;
      
      % Reciprocals of letter values sum to 1
      constraint z * 1/v + z * 1/w + z * 1/x + z * 1/y + z/z == z;
      
      % Two middle digits are the sum of letter values plus 1
      constraint v + w + x + y + z + 1 == num;
      
      solve satisfy;
      
      output [ "Number plate = " ++ show( [v, w, num, x, y, z ] ) ];
      % uses translation table to convert numbers to letters
      
      
      

      Like

    • GeoffR's avatar

      GeoffR 3:39 pm on 10 July 2022 Permalink | Reply

      
      from itertools import combinations
      import string
      
      # Dictionary mapping numbers to upper case letters
      DN = dict(zip(range(1, 27), string.ascii_uppercase))
      
      # omit values for vowels A, E, I, O, U
      vowels = {1, 5, 9, 15, 21}
      
      for C1 in combinations(set(range(1, 27)).difference(vowels), 5):
        a, b, c, d, e = C1
        #  five letters are in alphabetical order
        if a < b < c < d < e:
          # the sum of their reciprocals was equal to 1
          # i.e. 1/a + 1/b + 1/c + 1/d + 1/e == 1:
          if b*c*d*e + a*c*d*e + a*b*d*e + a*b*c*e + a*b*c*d == a*b*c*d*e:
            
            # last two digits of the year
            year2d = a + b + c + d + e + 1
            if year2d <= 22 or (51 <= year2d <= 72):
              print('Reg. No. = ', DN[a], DN[b], year2d, DN[c], DN[d], DN[e])
      
      
      

      Like

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