Recent Updates Page 31 Toggle Comment Threads | Keyboard Shortcuts

  • Unknown's avatar

    Jim Randell 11:56 am on 31 July 2022 Permalink | Reply
    Tags:   

    Brain-Teaser 161: 100-yard race 

    From The Sunday Times, 10th May 1964 [link]

    Harry, Kenneth, Lionel, Martin, Nicholas and Oliver were, the competitors in the 100-yard race on Sports Day. They wore cards numbered 1, 2, 3, 4, 5, 6 but not in that order. In no case was the position of any of the competitors the same as his card number but two of the competitors had positions equal to each other’s card number.

    Nicholas was 5th and his card number was the same as Kenneth’s position. Harry’s card number was the same as Oliver’s position which was 4th. Martin’s card number was 1.

    It was found that the sum of the products of each competitor’s position and card number was 61.

    Place the competitors in the order in which they finished the race and give their card numbers.

    This puzzle is included in the book Sunday Times Brain Teasers (1974).

    [teaser161]

     
    • Jim Randell's avatar

      Jim Randell 11:57 am on 31 July 2022 Permalink | Reply

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

      Run: [ @replit ]

      from enigma import (irange, subsets, reverse, diff, singleton, printf)
      
      pos = list(irange(1, 6))
      
      # consider the cards in each position (1st, ..., 6th)
      for ps in subsets(pos, size=6, select="D"):
        # card: map pos -> card
        card = dict(enumerate(ps, start=1))
        # the sum of the position/card products is 61
        if sum(i * p for (i, p) in card.items()) != 61: continue
        # there is (at least) one 2-cycle
        if not any(card[p] == i for (i, p) in card.items()): continue
      
        # name: map pos -> name
        # "N was 5th"
        # "O was 4th"
        # "K's position is N's card number (N is 5th)"
        # "H's card number is O's position (O is 4th)"
        # "M's card number is 1"
        # so we have enough information to fill out the map
        rcard = reverse(card)
        ks = [5, 4, card[5], rcard[4], rcard[1]]
        ks.append(singleton(diff(pos, ks)))
        if ks[-1] is None: continue
        name = dict(zip(ks, "NOKHML"))
        # check disallowed order
        if all(name[i] == x for (i, x) in zip(pos, "HKLMNO")): continue
      
        # output solution
        for i in pos:
          printf("{i}: {n} = #{c}", c=card[i], n=name[i])
        printf()
      

      Solution: The finishing positions are:

      1st: Lionel (#6)
      2nd: Harry (#4)
      3rd: Kenneth (#2)
      4th: Oliver (#5)
      5th: Nicholas (#3)
      6th: Martin (#1)

      Like

    • Frits's avatar

      Frits 11:53 am on 3 August 2022 Permalink | Reply

       
      from enigma import SubstitutedExpression
      
      # get position from card number
      pos = lambda n, lst: [i for i, x in enumerate(lst, start=1) if x == n][0]
      
      l1 = "[A, B, C, D, E, F]"          # card numbers
      l2 = "[1, 2, 3, 4, 5, 6], " + l1   # positions and card numbers
      z = "zip(" + l2 + ")"
      
      exprs = []
      # position unequal to card number for all competitors
      exprs.append("all(x != y for x, y in " + z + ")")
      # sum of the products of each competitor's position and card number was 61
      exprs.append("sum(x * y for x, y in " + z + ") == 61")
      # two of the competitors had positions equal to each other's card number
      exprs.append("sum((y, x) in " + z + " for x, y in " + z + ") == 2")
      # M ( , 1)
      # N (5, x)
      # O (4,  )
      # H ( , 4)
      # K (x,  )
      # L ( ,  )
      # check on five different positions
      exprs.append("len({pos(1, " + l1 + "), pos(4, " + l1 + "), E, 4, 5}) == 5")
                    
      
      # the alphametic puzzle
      p = SubstitutedExpression(
        exprs,
        answer="((1, A), (2, B), (3, C), (4, D), (5, E), (6, F))",
        digits=range(1, 7),
        distinct=("ABCDEF"),
        env=dict(pos=pos),
        verbose=0,    # use 256 to see the generated code
      )
      
      names = ["", "", "", "Oliver", "Nicholas", ""]
      for (_, ans) in p.solve():
        # dictionary, card --> postion
        d = {y: x for x, y in ans}
        
        names[d[1] - 1] = "Martin"
        names[d[4] - 1] = "Harry"
        names[ans[4][1] - 1] = "Kenneth"
        
        for i in range(6):
          if names[i] == "":
            names[i] = "Lionel"
         
        # integrity check 
        if len(set(names)) != 6: continue
        
        for i in range(6):
          print(f"{ans[i][0]}: {names[i]} with card number {ans[i][1]}")   
      

      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 9:10 am on 28 July 2022 Permalink | Reply
    Tags:   

    Teaser 2726: Christmas star 

    From The Sunday Times, 21st December 2014 [link] [link]

    I asked Peter to place the numbers 1 to 10 at the ten intersection points of the Christmas star so that in each of the five lines the four numbers added to the same total. He found that this was impossible so instead he did it with the numbers 1 to 9 together with just one of those digits repeated. In his answer there was just one line in which that digit did not appear.

    [In ascending order] what were the four numbers in that line?

    [teaser2726]

     
    • Jim Randell's avatar

      Jim Randell 9:12 am on 28 July 2022 Permalink | Reply

      (See also: Enigma 1063).

      I assume were are talking about a layout like this:

      There are 5 lines, and each of the numbers appears on two of the lines.

      If we add the lines we will be counting each number twice, and the total will be 5 times the common sum, T.

      So, if X is the repeated digit we have:

      2 × (1 + 2 + … + 9 + X) = 5 × T
      T = 18 + (2/5)X

      Hence: X = 5, and T = 20.

      To remove duplicate solutions we can suppose the line that doesn’t include X is (B, C, D, E) and that B < E.

      The following run file executes in 98ms.

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      # suppose the line with no repeated digit is BCDE
      
      SubstitutedExpression
      
      --digits="1-9"
      --distinct="BCDE"
      
      # the 5 lines have the same sum (= 20)
      "A + C + F + I = 20"
      "A + D + G + J = 20"
      "B + C + D + E = 20"
      "B + F + H + J = 20"
      "E + G + H + I = 20"
      
      # all 9 digits are used
      "len({A, B, C, D, E, F, G, H, I, J}) = 9"
      
      # 5 is in all the lines except BCDE
      "5 not in {B, C, D, E}"
      "5 in {A, C, F, I}"
      "5 in {A, D, G, J}"
      "5 in {B, F, H, J}"
      "5 in {E, G, H, I}"
      
      # remove duplicate solutions
      "B < E"
      
      --answer="ordered(B, C, D, E)"
      --template=""
      

      Solution: The four numbers on the line without the repeated digit are: 1, 3, 7, 9.

      There are 12 essentially different layouts, here is one:

      Like

  • Unknown's avatar

    Jim Randell 9:17 am on 26 July 2022 Permalink | Reply
    Tags:   

    Brain-Teaser 727: Right hand 

    From The Sunday Times, 22nd June 1975 [link]

    When South, who was bored with being dummy, had left the card room of the Logic Club for good, West extracted four kings, three queens and two jacks from the pack, showed them round, shuffled them, and dealt three cards to each of the three.

    “Forget the suits”, he said. “Let each of us look at his own three cards and see if he can make a sure statement about any kings, queens or jacks in the next man’s hand; we will use as evidence what we see in our own hands and what we hear each other say.

    They played with the full rigours of logic. West began by announcing that he could say nothing about North’s hand; North then said that he could say nothing about East’s hand; thereupon East said that he could deduce the value of one card — and one only — in West’s hand.

    What can you say about the cards that were dealt to East?

    This puzzle is included in the book The Sunday Times Book of Brain-Teasers: Book 2 (1981).

    [teaser727]

     
    • Jim Randell's avatar

      Jim Randell 9:18 am on 26 July 2022 Permalink | Reply

      There are 9 possible hands that could be dealt to W, and in each case W would be able to say something about the cards that are dealt to N:

      KKK → N holds at most 1K
      KKQ → N holds at most 2K, at most 2Q
      KKJ → N holds at most 2K, at most 1J
      KQQ → N holds at most 1Q
      KQJ → N holds at most 2Q, at most 1J
      KJJ → N holds no J
      QQQ → N holds at least 1K, no Q
      QQJ → N holds at least 1K, at most 1Q, at most 1J
      QJJ → N holds at least 1K, at most 2Q, no J

      So let’s use a tighter definition that each must declare if they know for certain cards that are held by the next player.

      This reduces the list to:

      QQQ → N holds at least 1K
      QQJ → N holds at least 1K
      QJJ → N holds at least 1K

      And this allows us to find an answer to the puzzle.

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

      Run: [ @replit ]

      from collections import defaultdict
      from enigma import (multiset, join, printf)
      
      # format a hand
      fmt = lambda x: join(x).upper()
      
      # deal groups of <k> cards from <cards>
      def deal(cards, k, ss=[]):
        if not cards:
          yield ss
        else:
          # choose the first hand
          for hand in cards.subsets(size=3):
            yield from deal(cards.difference(hand), k, ss + [tuple(hand.sorted())])
      
      # initial cards
      cards = multiset(K=4, Q=3, j=2)
      values = sorted(cards.keys())
      
      # possible deals
      hands = list(deal(cards, 3))
      
      # look at W's hand, record the number of Ks, Qs, Js in N's
      w = defaultdict(lambda: defaultdict(set))
      for (W, N, E) in hands:
        for k in values:
          w[W][k].add(N.count(k))
      
      # W can't be sure of any cards held by N
      Ws = set(W for (W, d) in w.items() if all(0 in d[k] for k in values))
      printf("[W = {Ws}]", Ws=join(map(fmt, Ws), sep=", ", enc="{}"))
      
      # look at N's hand, record the number of Ks, Qs, Js in E's
      n = defaultdict(lambda: defaultdict(set))
      for (W, N, E) in hands:
        if W not in Ws: continue
        for k in values:
          n[N][k].add(E.count(k))
      
      # N can't be sure of any cards held by E
      Ns = set(N for (N, d) in n.items() if all(0 in d[k] for k in values))
      printf("[N = {Ns}]", Ns=join(map(fmt, Ws), sep=", ", enc="{}"))
      
      # look at E's hand, record the number of Ks, Qs, Js in W's
      e = defaultdict(lambda: defaultdict(set))
      for (W, N, E) in hands:
        if W not in Ws or N not in Ns: continue
        for k in values:
          e[E][k].add(W.count(k))
      
      # E can be sure of exactly 1 card held by W
      for (E, d) in e.items():
        if sorted(min(v) for v in d.values()) == [0, 0, 1]:
          printf("E = {E}", E=fmt(E))
      

      Solution: East held a King, a Queen, and a Jack.

      Like

      • John Crabtree's avatar

        John Crabtree 6:50 pm on 26 July 2022 Permalink | Reply

        In manually reaching the same solution, I concluded that West must hold at least one King, and that North must hold at least one King and one Queen. That gives three possible sets of hands.
        Do you agree? Thanks.

        Like

        • Jim Randell's avatar

          Jim Randell 8:20 am on 27 July 2022 Permalink | Reply

          @John: Yes, I found three possible sets of hands:

          W = KQJ; N = KKQ; E = KQJ
          W = KKJ; N = KQQ; E = KQJ
          W = KKQ; N = KQJ; E = KQJ

          Like

  • Unknown's avatar

    Jim Randell 2:04 pm on 24 July 2022 Permalink | Reply
    Tags: ,   

    Brain-Teaser 159: Fallen leaves 

    From The Sunday Times, 26th April 1964 [link]

    Three leaves fall from a book. The total of the page numbers remaining in the second half of the book is now three times the total of the page numbers remaining in the first half.

    The total of the page numbers on the fallen leaves lies between 1050 and 1070, and is the highest which could have produced this effect.

    How many numbered pages did the book contain originally?

    I found many solutions, which improve on the published answer. So I have marked the puzzle as flawed.

    This puzzle is included in the book Sunday Times Brain Teasers (1974).

    [teaser159]

     
    • Jim Randell's avatar

      Jim Randell 2:06 pm on 24 July 2022 Permalink | Reply

      I think this puzzle is flawed, as I found many solutions that seem to satisfy the puzzle text, and are different from the published solution.

      If we suppose the book consists of n leaves (= 2n pages), and that the leaves are 1|2, 3|4, …, (2n − 1)|(2n).

      And the two halves of the book are pages 1 – n and (n + 1)(2n).

      Then the initial sum of page numbers in each half of the book are:

      first half = sum(1 .. n) = n(n + 1)/2
      second half = sum(n+1 .. 2n) = n(3n + 1)/2

      Now, if the three leaves removed are (2a − 1)|(2a), (2b − 1)|(2b), (2c − 1)|(2c).

      Then the total of the page numbers removed is t = 4(a + b + c) − 3, and this is in the range 1050 – 1070.

      So (a + b + c) is in the range 264 – 268, and we want this to take on the highest possible value (i.e. 268 if possible, which corresponds to t = 1069).

      If the sum of the removed pages in the first and second halves is t1, t2 (t1 + t2 = t), then we have:

      n(3n + 1)/2 − t2 = 3(n(n + 1)/2 − t1)
      n = 3.t1 − t2

      This Python program starts looking for values of t from 268 down to 264, and stops when it finds a value that gives viable books. It runs in 74ms. And it finds many possible solutions.

      from enigma import (irange, decompose, printf)
      
      # solve with leaves removed (even page numbers given)
      def solve(*vs):
        # calculate the removed page numbers
        (x, y, z) = (2 * v for v in vs)
        ps = (x - 1, x, y - 1, y, z - 1, z)
        # split the pages at index i
        for i in irange(2, 4):
          t1 = sum(ps[:i])
          t2 = sum(ps[i:])
          # calculate the number of leaves (so there are 2n pages)
          n = 3 * t1 - t2
          if n < 3 or ps[-1] > 2 * n: continue
          # and check the split occurs between the halves
          if i > 0:
            if ps[i - 1] > n: continue
          if i < 6:
            if not (ps[i] > n): continue
          yield (n, ps, t1, t2)
      
      # record solutions (number of pages = twice the number of leaves)
      ss = set()
      
      # t = a + b + c
      for t in irange(268, 264, step=-1):
        # calculate possible a, b, c values
        for (a, b, c) in decompose(t, 3):
          for (n, ps, t1, t2) in solve(a, b, c):
            printf("[t={t}: a={a} b={b} c={c} -> {ps}; t1={t1} t2={t2}; n={n}]")
            ss.add(2 * n)
        # are we done?
        if ss:
          printf("pages = {ss}", ss=sorted(ss))
          break
      

      Solution: The book could have: 310, 318, 342, 350, 374, 406, 438, 470, 502, 534, 566, 598, 630, 662, 694, 6414 pages.


      Here is one of the solutions found:

      If the book has 203 leaves = 406 pages, numbered 1|2, 3|4, …, 203|204, …, 405|406.

      Then the sum of the page numbers in the first half of the book is: sum(1..203) = 20706.

      And the sum of the page numbers in the second half of the book is: sum(204..406) = 61915.

      (Note that if leaf 203|204 is removed, that is one page from each half of the book).

      Now, if pages 1|2, 157|158, 375|376 are removed (total = 1069, the largest possible value).

      Then the pages removed from the first half are: 1, 2, 157, 158; sum = 318, so the sum of the remaining pages in the first half is 20706 − 318 = 20388.

      And the pages removed from the second half are: 375, 376; sum = 751, so the sum of the remaining pages in the second half is 61915 − 751 = 61164.

      And: 61164 = 3 × 20388, as required.


      The published solution (and that given in the 1974 book) is that the book initially had 302 pages (i.e. 151 leaves), and the removed pages are 75|76, 151|152, 301|302. Which gives a total of 1057.

      But I have shown above that this is not the largest total possible.

      It was also noted: “Only 4 correct answers in a very small entry”. So it seems I wasn’t the only one that had issues with this puzzle.

      The solution in the book only considers 3 ways that leaves can be removed: 1 from the first half + 2 from the second half; 2 from the first half + 1 from the second half; 1.5 from each half. In terms of pages these are 2+4, 3+3, 4+2, but my program also considers 0+6, 1+5, 5+1, 6+0. However the example I give is a 4+2 combination, which should be accounted for by the published solution. But it has arrived at the conclusion that the maximum viable total of the numbers on the removed pages for a book with x leaves is 7x, so 1057 is the maximum possible (with 151 leaves), but I think this is faulty reasoning.

      (The only thing I can think of is that the two halves of the book are considered after the leaves have been removed. (Although the solution given in the book doesn’t seem to support this). But even with this interpretation there are multiple solutions with a total of 1069 – any that removes 3 pages from the first half and three from the second half will leave the boundary unchanged).

      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 9:07 am on 21 July 2022 Permalink | Reply
    Tags:   

    Teaser 2533: A lopsided number 

    From The Sunday Times, 10th April 2011 [link] [link]

    In this letters-for-digits substitution puzzle, each letter consistently represents a different digit. In the display, each letter in the top row is the sum of the two letters directly below it:

    What number is LOPSIDED?

    [teaser2533]

     
    • Jim Randell's avatar

      Jim Randell 9:08 am on 21 July 2022 Permalink | Reply

      We can use the [[ SubstitutedExpression ]] solver from the enigma.py library to solve this puzzle.

      The following run file executes in 64ms. The internal runtime of the generated program is 55µs).

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      "F + I = P"
      "I + D = O"
      "D + D = S"
      "D + L = E"
      "L + E = R"
      
      --answer="LOPSIDED"
      

      Solution: LOPSIDED = 24961353.

      And: POSER = 94657; FIDDLE = 813325.

      Like

    • GeoffR's avatar

      GeoffR 11:38 am on 21 July 2022 Permalink | Reply

      % A Solution in MiniZinc
      include "globals.mzn";
       
      var 1..9:P; var 1..9:O; var 1..9:S; var 1..9:E; var 1..9:R;
      var 1..9:F; var 1..9:I; var 1..9:D; var 1..9:L; 
      
      constraint all_different([P, O, S, E, R, F, I, D, L]);
      
      constraint F + I == P /\ I + D == O /\ D + D == S
      /\ D + L == E /\ L + E == R;
      
      solve satisfy;
      
      output ["LOPSIDED = \(L)\(O)\(P)\(S)\(I)\(D)\(E)\(D)"];
      
      % LOPSIDED = 24961353
      % ----------
      % ==========
      
      
      

      Like

  • Unknown's avatar

    Jim Randell 6:31 am on 19 July 2022 Permalink | Reply
    Tags:   

    Teaser 2721: Milliner’s hat-trick 

    From The Sunday Times, 16th November 2014 [link] [link]

    A football tournament has four groups each of four teams, with the teams in the same group playing each other once. So far the teams have each played two games and in each group the distribution of points is different. Also, in each group just one pair of teams are level on points and their positions have been determined by their “goals scored”. Milliner has scored four (including a hat-trick), Orlando two, and nine other players have scored one goal each. Despite his success, Orlando’s team is not the top of their group.

    What are the results of the two games that Milliner’s team have played? And the two games that Orlando’s team have played?

    [teaser2721]

     
    • Jim Randell's avatar

      Jim Randell 6:33 am on 19 July 2022 Permalink | Reply

      The puzzle is not explicit on the scoring system used (i.e. 2 points for a win, or 3 points for a win), although it turns out it doesn’t matter which is used, the answer to the puzzle is the same in both cases.

      In each group each team has played 2 of their 3 matches. So there have been 4 matches played (and there are 2 matches left to play). So among the 4 groups there have been 16 matches played in total.

      The minimum possible number of goals scored among the 4 matches in a group is 2. So the maximum possible number of goals scored is 15 − 3×2 = 9. (Although it may be possible to reduce this theoretical maximum value, which would speed up the program).

      For each group, this Python program considers the scores for teams A (1st), B (2nd), C (3rd), D (4th), such that each team has played 2 matches, and two of the teams are tied on points, but have different “goals for” values.

      It then chooses 4 different points distributions (one for each of the groups), and from the previously examined scores it chooses 4 sets of scores that have 15 goals scored in total, and ensures there are possible teams for both Milliner and Orlando.

      The program runs in 361ms.

      Run: [ @replit ]

      from collections import defaultdict
      from enigma import (
        Football, irange, subsets, tuples, cproduct, peek, delete,
        update, find, unzip, reverse, map2str, join, printf
      )
      
      # scoring regime (2 or 3 points for a win)
      football = Football(games='wdlx', points=dict(w=2, d=1))
      
      # labels for the teams
      teams = (A, B, C, D) = tuple("ABCD")
      
      # keys for the matches
      ks = list(x + y for (x, y) in subsets(teams, size=2))
      
      # allocate <t> goals among the matches <ms>, return scores <ss>
      def allocate(t, ms, ss=dict()):
        # are we done?
        if not ms:
          if t == 0:
            yield ss
        else:
          # calculate the number of surplus goals
          n = t - sum(v == 'w' for v in ms.values())
          if n < 0: return
          # choose the next match to allocate goals to
          (k, v) = peek(ms.items())
          if v == 'x':
            # not played
            yield from allocate(t, delete(ms, [k]), update(ss, [(k, None)]))
          elif v == 'w' or v == 'l':
            # a win (or loss), calculate scores (x, y)
            for x in irange(1, n + 1):
              for y in irange(0, min(x - 1, n - x + 1)):
                s = ((x, y) if v == 'w' else (y, x))
                yield from allocate(t - x - y, delete(ms, [k]), update(ss, [(k, s)]))
          elif v == 'd':
            # a draw, calculate scores (x, x)
            for x in irange(0, n // 2):
              yield from allocate(t - x - x, delete(ms, [k]), update(ss, [(k, (x, x))]))
      
      # check goals scored by team <t> in a match from scores <ss> is <n> or more
      def scored(t, ss, n):
        for (k, v) in ss.items():
          if v is not None:
            i = find(k, t)
            if i != -1 and v[i] >= n:
              return True
      
      # map: <pts> -> <total goals> -> (<match scores>, <milliner>, <orlando>) values
      d = defaultdict(lambda: defaultdict(list))
      
      # consider possible match outcomes
      for ms in football.games(repeat=len(ks)):
        ms = dict(zip(ks, ms))
        # compute the table for each team
        table = dict((t, football.extract_table(ms, t)) for t in teams)
        # each team has played twice
        if not all(table[t].played == 2 for t in teams): continue
        # points are increasing, with exactly 2 teams having the same points
        pts = tuple(table[t].points for t in teams)
        if len(set(pts)) != 3: continue
        if not all(x >= y for (x, y) in tuples(pts, 2)): continue
        # allocate goals to the matches
        for t in irange(2, 9):
          for ss in allocate(t, ms):
            # extract the goals (for, against) for each team
            goals = dict((t, football.extract_goals(ss, t)) for t in teams)
            # check teams with same points have different "goals for" values
            if any(p1 == p2 and not (goals[t1][0] > goals[t2][0])
              for ((t1, p1), (t2, p2)) in tuples(zip(teams, pts), 2)
            ): continue
            # find teams for Milliner: team must have a match with >= 3 goals scored
            # and have >= 4 goals scored in total
            tM = list(t for (t, (f, _)) in goals.items() if f > 3 and scored(t, ss, 3))
            # find teams for Orlando: must have >= 2 goals, and not be top
            tO = list(t for (t, (f, _)) in goals.items() if t != 'A' and f > 1)
            # record this set of scores
            d[pts][t].append((ss, tM, tO))
      
      # collect scores from <ss> for teams in <ts> (from the team's perspective)
      def collect(ss, ts):
        for t in ts:
          rs = list()
          for (s, f) in unzip(football.extract(ss, t)):
            if s is None: continue
            rs.append(reverse(s) if f else s)
          yield tuple(sorted(rs, reverse=1))
      
      # choose 4 different points distributions
      for ps in subsets(d.keys(), size=4):
        # choose the number of goals scored in each group
        for ts in cproduct(d[p].keys() for p in ps):
          # there should be 15 goals in total
          if sum(ts) != 15: continue
          # choose the actual scores in each match
          for ss in cproduct(d[p][t] for (p, t) in zip(ps, ts)):
            # check for possibilities for Milliner and Orlando
            if not any(tM for (_, tM, _) in ss): continue
            if not any(tO for (_, _, tO) in ss): continue
            # output a solution (groups = scores, pts, M, O; M's team; O's team)
            (Ms, Os) = (set(), set())
            for (i, ((s, tM, tO), p)) in enumerate(zip(ss, ps), start=1):
              printf("Group {i}: {s} {p} {tM} {tO}", s=map2str(s, enc=""), tM=join(tM, enc="[]"), tO=join(tO, enc="[]"))
              Ms.update(collect(s, tM))
              Os.update(collect(s, tO))
            for M in sorted(Ms): printf("M's team: {M}", M=join(M, sep=", "))
            for O in sorted(Os): printf("O's team: {O}", O=join(O, sep=", "))
            printf()
      

      Solution: The results for Milliner’s team are: a 3-1 win, a 1-0 win. The results for Orlando’s team are: a 2-0 win, a 0-1 loss.

      M’s team is a group leader. O’s team is second in their group.

      With “2 points for a win” (and 1 for a draw) there are three possible sets of scores:

      Group 1: A v B = 1-0; A v C = 1-0; B v D = 2-0; C v D = 1-0; points = [4, 2, 2, 0], Orlando is team B
      Group 2: A v C = 3-1; A v D = 1-0; B v C = 0-0; B v D = 0-0; points = [4, 2, 1, 1], Milliner is team A
      Group 3: A v B = 1-0; A v C = 0-0; B v D = 1-0; C v D = 0-0; points = [3, 2, 2, 1]
      Group 4: A v C = 0-0; A v D = 2-0; B v C = 0-0; B v D = 1-0; points = [3, 3, 2, 0]

      Group 1: A v B = 1-0; A v D = 1-0; B v C = 2-0; C v D = 1-0; points = [4, 2, 2, 0], Orlando is team B
      Group 2: A v C = 3-1; A v D = 1-0; B v C = 0-0; B v D = 0-0; points = [4, 2, 1, 1], Milliner is team A
      Group 3: A v B = 1-0; A v C = 0-0; B v D = 1-0; C v D = 0-0; points = [3, 2, 2, 1]
      Group 4: A v C = 0-0; A v D = 2-0; B v C = 0-0; B v D = 1-0; points = [3, 3, 2, 0]

      Group 1: A v C = 1-0; A v D = 1-0; B v C = 0-1; B v D = 2-0; points = [4, 2, 2, 0], Orlando is team B
      Group 2: A v C = 3-1; A v D = 1-0; B v C = 0-0; B v D = 0-0; points = [4, 2, 1, 1], Milliner is team A
      Group 3: A v B = 1-0; A v C = 0-0; B v D = 1-0; C v D = 0-0; points = [3, 2, 2, 1]
      Group 4: A v C = 0-0; A v D = 2-0; B v C = 0-0; B v D = 1-0; points = [3, 3, 2, 0]

      We can examine what happens with “3 points for a win” by changing the initialisation at line 8. And we find in this case there are four possible sets of scores:

      Group 1: A v B = 1-0; A v D = 0-0; B v C = 2-0; C v D = 1-0; points = [4, 3, 3, 1], Orlando is team B
      Group 2: A v B = 1-1; A v D = 1-0; B v C = 0-0; C v D = 0-0; points = [4, 2, 2, 1]
      Group 3: A v C = 3-1; A v D = 1-0; B v C = 0-0; B v D = 0-0; points = [6, 2, 1, 1], Milliner is team A
      Group 4: A v C = 0-0; A v D = 2-0; B v C = 0-0; B v D = 1-0; points = [4, 4, 2, 0]

      Group 1: A v B = 1-0; A v D = 0-0; B v C = 2-0; C v D = 1-0; points = [4, 3, 3, 1], Orlando is team B
      Group 2: A v C = 0-0; A v D = 1-0; B v C = 0-0; B v D = 1-1; points = [4, 2, 2, 1]
      Group 3: A v C = 3-1; A v D = 1-0; B v C = 0-0; B v D = 0-0; points = [6, 2, 1, 1], Milliner is team A
      Group 4: A v C = 0-0; A v D = 2-0; B v C = 0-0; B v D = 1-0; points = [4, 4, 2, 0]

      Group 1: A v C = 1-0; A v D = 0-0; B v C = 0-1; B v D = 2-0; points = [4, 3, 3, 1], Orlando is team B
      Group 2: A v B = 1-1; A v D = 1-0; B v C = 0-0; C v D = 0-0; points = [4, 2, 2, 1]
      Group 3: A v C = 3-1; A v D = 1-0; B v C = 0-0; B v D = 0-0; points = [6, 2, 1, 1], Milliner is team A
      Group 4: A v C = 0-0; A v D = 2-0; B v C = 0-0; B v D = 1-0; points = [4, 4, 2, 0]

      Group 1: A v C = 1-0; A v D = 0-0; B v C = 0-1; B v D = 2-0; points = [4, 3, 3, 1], Orlando is team B
      Group 2: A v C = 0-0; A v D = 1-0; B v C = 0-0; B v D = 1-1; points = [4, 2, 2, 1]
      Group 3: A v C = 3-1; A v D = 1-0; B v C = 0-0; B v D = 0-0; points = [6, 2, 1, 1], Milliner is team A
      Group 4: A v C = 0-0; A v D = 2-0; B v C = 0-0; B v D = 1-0; points = [4, 4, 2, 0]

      Like

  • Unknown's avatar

    Jim Randell 12:33 pm on 17 July 2022 Permalink | Reply
    Tags:   

    Teaser 2531: [Harshad numbers] 

    From The Sunday Times, 27th March 2011 [link] [link]

    A Harshad number (or H-number) is any number that is divisible by the sum of its digits. Using each non-zero digit just the once, I have written down a 9-figure H-number. Reading from left to right, it also consists of three 2-figure H-numbers followed by a 3-figure H-number. Again working from left to right through the 9-figure number, the last five digits form a 5-figure H-number. Reversing the order of the first five digits of the 9-figure number also gives a 5-figure H-number.

    What is the 9-figure number?

    This puzzle was originally published with no title.

    [teaser2531]

     
    • Jim Randell's avatar

      Jim Randell 12:34 pm on 17 July 2022 Permalink | Reply

      See: [@wikipedia].

      We can use the [[ SubstitutedExpression ]] solver from the enigma.py library to solve this puzzle.

      The following run file executes in 72ms. The internal runtime of the generated program is 3.1ms).

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      --digits="1-9"
      
      # is <n> an H-number?
      --code="H = lambda n: n % dsum(n) == 0"
      
      # the 9-digit number is: abcdefghi
      "H({abcdefghi})"
      
      # three 2-digit H-numbers and a 3-digit H-number
      "H({ab})"
      "H({cd})"
      "H({ef})"
      "H({ghi})"
      
      # 5-digit H-numbers
      "H({efghi})"
      "H({edcba})"
      
      --answer="{abcdefghi}"
      

      Solution: The 9-digit number is: 273684915.

      Like

    • GeoffR's avatar

      GeoffR 2:16 pm on 17 July 2022 Permalink | Reply

      
      % A Solution in MiniZinc 
      include "globals.mzn";
      
      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]);
      
      % 2-digit Harshad numbers
      constraint (10*a + b) mod (a + b) == 0
      /\ (10*c + d) mod (c + d) == 0
      /\ (10*e + f) mod (e + f) == 0;
      
      % 3-digit Harshad number
      constraint (100*g + 10*h + i) mod (g + h + i) == 0;
      
      % 5-digit Harshad numbers
      constraint (10000*e + 1000*f + 100*g + 10*h + i) mod(e + f + g + h + i) == 0;
      constraint (10000*e + 1000*d + 100*c + 10*b + a) mod(e + d + c + b + a) == 0;
      
      % 9-digit Harshad number
      constraint (a * pow(10,8) + b * pow(10,7) + c * pow(10,6)
      + d * pow(10,5) + e * pow(10,4) + f * pow(10,3)
      + g * pow(10,2) + h * 10 + i) mod (a+b+c+d+e+f+g+h+i) == 0;
      
      solve satisfy;
      
      output ["9-figure number = " ++ " \(a)\(b)\(c)\(d)\(e)\(f)\(g)\(h)\(i)" ];
      
      % 9-figure number = 273684915
      % ----------
      % ==========
      
      

      Like

    • Frits's avatar

      Frits 3:40 pm on 17 July 2022 Permalink | Reply

         
      from itertools import permutations
      from functools import reduce
      
      # convert digits sequence to number
      d2n = lambda s: reduce(lambda x, y: 10 * x + y, s)
      
      # calculate H-number
      H = lambda s: d2n(s) % sum(s) == 0
      
      i = 5  # as abcdefghi is divisible by 45
      
      # ghi:   sum(g,h) must be even as sum(g,h,i) must be odd
      # efghi: sum(e,f) must be even as sum(e,f,g,h,i) must be odd
      # ef:    e and f not both odd otherwise 10*d + e is not divisible by e + f
      #        so e and f are both even
      # ab:    a and b not both odd
      # cd:    c and d not both odd
      # g an h must be both odd otherwise a, b, c and d must all be odd
      
      # ....eeoo5
      # edcba : a must be even as sum(e,d,c,b,a) is even
      # e...eeoo5
      # as c and d are not both odd b must be odd
      # eo..eeoo5
      
      for (b, g, h) in permutations([1, 3, 7, 9], 3):
        if not H((g, h, i)): continue
        for (a, e, f) in permutations([2, 4, 6, 8], 3):
          if not H((a, b)): continue
          if not H((e, f)): continue
          if not H((e, f, g, h, i)): continue
          rest = set(range(1, 10)).difference({a, b, e, f, g, h, i})
          for (c, d) in permutations(rest):
            if not H((c, d)): continue
            if not H((e, d, c, b, a)): continue
            if not H((a, b, c, d, e, f, g, h, i)): continue
            print("the 9-figure number:", d2n((a, b, c, d, e, f, g, h, i)))
      

      Like

    • GeoffR's avatar

      GeoffR 6:43 pm on 17 July 2022 Permalink | Reply

      I carried out some further analysis to see how the number of Harshad numbers reduced with different arrangements of numbers.

      1) For numbers ab, cd, ef, ghi and abcdefghi, this gave 96 possible 9-digit Harshad numbers.

      2) Adding abcde to (1) above, I found 16 possible Harshad numbers for abcdefghi:

       a b c d e f g h i    a b c d e f g h i    a b c d e f g h i
      ------------------------------------------------------------
       2 7 3 6 4 8 1 9 5,   2 7 3 6 8 4 9 1 5,   2 7 6 3 4 8 1 9 5,   
       2 7 6 3 8 4 9 1 5,   3 6 2 7 4 8 1 9 5,   3 6 2 7 8 4 9 1 5,   
       3 6 7 2 4 8 1 9 5,   3 6 7 2 8 4 9 1 5,   6 3 2 7 4 8 1 9 5,   
       6 3 2 7 8 4 9 1 5,   6 3 7 2 4 8 1 9 5,   6 3 7 2 8 4 9 1 5,   
       7 2 3 6 4 8 1 9 5,   7 2 3 6 8 4 9 1 5,   7 2 6 3 4 8 1 9 5,   
       7 2 6 3 8 4 9 1 5.
      

      3) Finally, adding number edcba to (2) above, this gave the single 9-digit answer of 273684915.
      (top row, middle value)

      Interesting to note how the 16 solutions illustrate aspects of analysis in Frits code.

      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 9:01 am on 14 July 2022 Permalink | Reply
    Tags:   

    Teaser 2532: In hot pursuit 

    From The Sunday Times, 3rd April 2011 [link] [link]

    George and Martha are jogging around a circular track. Martha starts at the most westerly point, George starts at the most southerly point, and they both jog clockwise at their own steady speeds. After a short while Martha is due north of George for the first time. Five minutes later she is due south of him for the first time. Then George catches up with her during their second laps at the most northeasterly point of the track.

    What is Martha’s speed (in degrees turned per minute)?

    [teaser2532]

     
    • Jim Randell's avatar

      Jim Randell 9:02 am on 14 July 2022 Permalink | Reply

      If George starts at 0° and goes at g degrees per minute, and Martha starts at 90° and goes at m degrees per minute.

      Then at time x (shortly after they set off) M is due north of G. So the angle G has gone is the same angular distance that M has to go to reach the north (180°) marker.

      So we have:

      xg = 90° − xm
      x(g + m) = 90°

      And 5 minutes later M is due south of G for the first time. The distance G has gone beyond the north (180°) marker is the same as the distance that M has to go to reach the south (360°/0°) marker.

      (x + 5)g − 180° = 270° − (x + 5)m
      (x + 5)(g + m) = 450°

      5x = x + 5
      4x = 5
      x = 5/4
      g + m = 72°/min

      Later, at some time t during lap 2, G catches up with M at the north-east (225°) marker. So G has gone 585° and M has gone 495°.

      Hence:

      585 = tg
      495 = tm

      t(g + m) = 1080
      t = 1080 / 72 = 15 min
      g = 39°/min
      m = 33°/min

      Solution: Martha’s speed is 33°/min.

      Like

  • Unknown's avatar

    Jim Randell 7:29 am on 12 July 2022 Permalink | Reply
    Tags:   

    Teaser 2724: Headcount 

    From The Sunday Times, 7th December 2014 [link] [link]

    My grandson and I play a coin game. First we toss seven coins and I have to predict in advance the number of heads whilst he has to predict the number of tails. I then get a number of points equal to the number of heads, he gets a number of points equal to the number of tails, and anyone whose prediction was correct gets a fixed bonus number of points (less than 40). We repeat this with six coins in the second round, then five, and so on down to two. In a recent game we noticed that, after each round, the total of all the points so far awarded was equal to a prime number.

    What is the “fixed bonus” number of points? And what was the total of all the points at the end of the game?

    [teaser2724]

     
    • Jim Randell's avatar

      Jim Randell 7:30 am on 12 July 2022 Permalink | Reply

      (See also: Teaser 3009).

      In a round with n coins the points awarded are, the number of heads (+ bonus if guessed correctly) and the number of tails (+ bonus if guessed correctly). So n points are always awarded, along with 0, 1, or 2 bonuses.

      We don’t need to worry about the points won by each player, just the total number of points gained in each round.

      This Python program keeps a set of (total, bonus) pairs, and then progresses through the rounds keeping viable values.

      It runs in 57ms. (Internal runtime is 664µs).

      Run: [ @replit ]

      from enigma import (irange, is_prime, printf)
      
      # start with a total of 0, and all possible bonus values
      ss = set((0, x) for x in irange(0, 40))
      
      # start with <k> coins
      k = 7
      
      # consider subsequent rounds
      while k > 1:
        ss_ = set()
        # consider (total, bonus) in the previous round
        for (t, b) in ss:
          # consider number of bonuses awarded in this round
          for n in (0, 1, 2):
            t_ = t + k + n * b
            if is_prime(t_):
              ss_.add((t_, b))
        # move on to the next round
        (ss, k) = (ss_, k - 1)
      
      # output solutions
      for (t, b) in ss:
        printf("total = {t}, bonus={b}")
      

      Solution: The number of bonus points is 19. The total number of points at the end of the game is 103.

      The progression of the game is:

      7 coins: total = 7 (0 bonus points)
      6 coins: total = 13 (0 bonus points)
      5 coins: total = 37 (19 bonus points)
      4 coins: total = 79 (38 bonus points)
      3 coins: total = 101 (19 bonus points)
      2 coins: total = 103 (0 bonus points)

      Like

  • Unknown's avatar

    Jim Randell 8:02 am on 10 July 2022 Permalink | Reply
    Tags:   

    Brain-Teaser 150: Paving the way 

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

    I have eight paving stones whose dimensions are an exact number of inches. Their areas are 504, 420, 324, 306, 270, 130, 117 and 112 square inches. Four of these are red and four green. There should be a ninth stone coloured yellow and square so that all nine stones can be fitted together to form a square in such a way that the red stones completely enclose the other five and the green stones completely enclose the yellow one.

    What are the dimensions of the red stones?

    [teaser150]

     
    • Jim Randell's avatar

      Jim Randell 8:03 am on 10 July 2022 Permalink | Reply

      The tiles that are mentioned have a total area of 2183.

      If the missing central tile has an area of x², and the entire collection has an area of y² then we have:

      y² = x² + 2183
      y² − x² = 2183
      (y − x)(y + x) = 2183

      So we can calculate possible values for x and y by examining the divisors of 2183.

      The Python program find values for x and y, and then chooses 4 green tiles, along with the x by x yellow square, which can fit together to form a square. And then checks that this square, along with the the remaining (green) tiles can fit together to form a y by y square.

      We use the rectpack.py module to fit the tiles into squares.

      The program runs in 169ms.

      Run: [ @replit ]

      from enigma import (divisors_pairs, div, subsets, is_square, cproduct, printf)
      import rectpack
      
      # areas of tiles we are given
      areas = {504, 420, 324, 306, 270, 130, 117, 112}
      
      # record possible tiles
      dps = dict((x, list(divisors_pairs(x))) for x in areas)
      
      # select tiles by area <x>, with max dimension not exceeding <m>
      tiles = lambda x, m: ((a, b) for (a, b) in dps[x] if not (b > m))
      
      # pack rectangles <rs> and an <s> by <s> square into a <t> by <t> square
      def pack(rs, s, t):
        # make the list of rectangles
        rs_ = [(s, s)]
        rs_.extend(rs)
        # pack them into a t by t square
        for ps in rectpack.pack(t, t, rs_):
          # check the square is not on the edge
          if not any(
            w == h == s and x > 0 and y > 0 and x + w < t and y + h < t
            for (x, y, w, h) in ps
          ): continue
          return ps  # we only need one packing
      
      # consider divisors of the total area
      for (a, b) in divisors_pairs(sum(areas)):
        # calculate x and y
        (x, y) = (div(b - a, 2), div(a + b, 2))
        if x is None or y is None or y < x + 4: continue
        # choose 4 green tiles to go around a central x by x square
        for green in subsets(areas, size=4):
          z = is_square(x * x + sum(green))
          if z is None: continue
          # consider dimensions of green tiles
          for gs in cproduct(tiles(x, z) for x in green):
            # pack them into a z by z square
            ps1 = pack(gs, x, z)
            if ps1 is None: continue
            # consider dimensions of red tiles
            red = areas.difference(green)
            for rs in cproduct(tiles(x, y) for x in red):
              # pack them into a y by y square
              ps2 = pack(rs, z, y)
              if ps2 is None: continue
              # output packings
              printf("red {red} around {z} x {z} green in {y} x {y}\n-> {ps2}")
              printf("green {green} around {x} x {x} yellow in {z} x {z}\n-> {ps1}")
              printf()
      

      Solution: The dimensions of the red stones (in inches) are: 3×39, 6×45, 9×36, 12×42.

      Here is a diagram of one possible layout:

      Like

      • Frits's avatar

        Frits 1:01 pm on 10 July 2022 Permalink | Reply

        @Jim, it is not stated that the yellow and green tiles form a square.

        Like

        • Jim Randell's avatar

          Jim Randell 1:11 pm on 10 July 2022 Permalink | Reply

          @Frits: Is there a way the red stones can enclose the green ones without forming a square?

          Like

        • Jim Randell's avatar

          Jim Randell 1:14 pm on 10 July 2022 Permalink | Reply

          I suppose it could be a non-square rectangle. But luckily it is possible to do it with a square.

          Like

          • Frits's avatar

            Frits 1:18 pm on 10 July 2022 Permalink | Reply

            So there might be another solution…

            Like

          • Jim Randell's avatar

            Jim Randell 1:26 pm on 10 July 2022 Permalink | Reply

            I made some tweaks to my program, and it didn’t find any solutions with a non-square rectangle. But I’ll have a closer look.

            Like

          • Jim Randell's avatar

            Jim Randell 2:53 pm on 10 July 2022 Permalink | Reply

            This is my program adapted to consider packing the green and yellow tiles into a (possibly non-square) rectangle.

            It doesn’t find any additional solutions.

            Run: [ @replit ]

            from enigma import (divisors_pairs, div, subsets, cproduct, printf)
            import rectpack
            
            # areas of tiles we are given
            areas = {504, 420, 324, 306, 270, 130, 117, 112}
            
            # record possible tiles
            dps = dict((x, list(divisors_pairs(x))) for x in areas)
            
            # select tiles by area <x>, with max dimension not exceeding <m>
            tiles = lambda x, m: ((a, b) for (a, b) in dps[x] if not(b > m))
            
            # pack rectangles <rs> and rectangle <s> into a bounding rectangle <t>
            def pack(rs, s, t):
              (a, b) = t
              # make the list of rectangles
              rs_ = [s]
              rs_.extend(rs)
              # pack them into a the rectangle
              for ps in rectpack.pack(t[0], t[1], rs_):
                # check that s is not on the edge
                if not any(
                  x > 0 and y > 0 and x + w < a and y + h < b and {w, h} == set(s)
                  for (x, y, w, h) in ps
                ): continue
                return ps  # we only need one packing
            
            # consider divisors of the total area
            for (a, b) in divisors_pairs(sum(areas)):
              # calculate x and y
              (x, y) = (div(b - a, 2), div(a + b, 2))
              if x is None or y is None or y < x + 4: continue
              # choose 4 green tiles to go around the central x by x square
              for green in subsets(areas, size=4):
                for (a, b) in divisors_pairs(x * x + sum(green)):
                  if a < x + 2 or y < b + 2: continue
                  # consider dimensions of green tiles
                  for gs in cproduct(tiles(x, b) for x in green):
                    # pack them into an a by b rectangle
                    ps1 = pack(gs, (x, x), (a, b))
                    if ps1 is None: continue
                    # consider dimensions of red tiles
                    red = areas.difference(green)
                    for rs in cproduct(tiles(x, y) for x in red):
                      # pack them into a y by y square
                      ps2 = pack(rs, (a, b), (y, y))
                      if ps2:
                        # output packings
                        printf("red {red} around {a} x {b} green in {y} x {y}\n-> {ps2}")
                        printf("green {green} around {x} x {x} yellow in {a} x {b}\n-> {ps1}")
                        printf()
            

            Like

    • Frits's avatar

      Frits 5:02 pm on 10 July 2022 Permalink | Reply

      One piece of logic is not coded, it turns out it is not needed.

         
      from enigma import divisors_pairs
      from itertools import product
       
      # find three other red pieces which can form the outer ring
      def check(a, b, d):
        # combinations of length and width of fourth red piece
        lst = [[[big_square_side - x, (big_square_side - y) * x] 
                for x in d[big_square_side - y] if (big_square_side - x) in d] 
               for y in [a, b]]
        
        # check every combination of possible length and width
        for c, d in product(lst[0], lst[1]):
          # has the fourth piece a known area
          if c[0] * d[0] not in d_red: continue
      
          # do all four pieces have a different area
          if len(set([c[0] * d[0], a * b, c[1], d[1]])) != 4: continue
          # return all four pieces
          return tuple(sorted([tuple(sorted([a, b])), 
                 tuple(sorted([big_square_side - a, big_square_side - c[0]])), 
                 tuple(sorted([big_square_side - b, big_square_side - d[0]])), 
                 tuple(sorted([c[0], d[0]]))]))
         
        return tuple()          
            
      # areas of tiles we are given
      areas = sorted([504, 420, 324, 306, 270, 130, 117, 112])
       
      # record possible tiles
      d_tiles = dict((x, list(divisors_pairs(x))) for x in areas)
      
      # possible areas for the big square
      sqs = {n * n for n in range(1, areas[-4] + 1)}
      
      area_greenred = sum(areas)
      
      # candidates for yellow area
      area_yellow_cands = [sq for sq in sqs if (sq + area_greenred) in sqs]
      
      # check all candidates for yellow areas 
      for area in area_yellow_cands:
        big_square_side = int((area + area_greenred)**.5)
        yellow = int(area**.5)
        area_yellow = area
        
        # adjust possible red tiles to drop tiles with a big length
        d_red = {k: [x for x in vs if x[0] <= big_square_side and 
                                      x[1] <= big_square_side] 
                    for k, vs in d_tiles.items()}
        
        d_side = dict()     # dictionary per side
        for k, vs in d_red.items():
          for x in vs:
            d_side[x[0]] = d_side.get(x[0], set()) | {x[1]}
            d_side[x[1]] = d_side.get(x[1], set()) | {x[0]}
      
        if big_square_side in d_side:
          # valid situation but not coded!
          continue
        
        # as each piece now is shorter than the side of the big square
        # we need to have a form so that 2 sides of different pieces are equal to
        # the side of the big square, forms should be similar to this one:
        #
        #   +----------+--+
        #   |          |  |
        #   +----+-----+  |
        #   |    |     |  |
        #   |    +-----+--+
        #   |    |        |
        #   +----+--------+
        
        # adjust dictionary to keep only sides <x> for which complementary side 
        # <big_square_side> - <k> also exists
        d_side = {k: vs for k, vs in d_side.items() 
                        if big_square_side - k in d_side}
        # also adjust possible red tiles 
        d_red = {k: [x for x in vs if big_square_side - x[0] in d_side and 
                                      big_square_side - x[1] in d_side] 
                    for k, vs in d_red.items()}
       
        sol = set()
        for k, vs in d_red.items():
          # pick a first red piece
          for (le, wi) in vs:
            # find three other red pieces which can form the outer ring
            chk = check(le, wi, d_side)
            if chk:
              if chk not in sol:
                print("answer:", *chk)
              sol.add(chk)
      

      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

  • Unknown's avatar

    Jim Randell 9:53 am on 7 July 2022 Permalink | Reply
    Tags:   

    Teaser 2723: St Andrew’s day 

    From The Sunday Times, 30th November 2014 [link] [link]

    Bearing in mind today’s date, I have written down two numbers in code, with different letters being used consistently to replace different digits. The addition of the two numbers is shown below, appropriately leading to today’s date as a six- figure number.

    What is the value of SEND?

    [teaser2723]

     
    • Jim Randell's avatar

      Jim Randell 9:55 am on 7 July 2022 Permalink | Reply

      We can use the general alphabetic solver [[ SubstitutedExpression ]] from the enigma.py library to solve this puzzle.

      The following run file is straightforward, but not especially quick. It runs in 710ms.

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      "{SAINT} + {ANDREW} = 301114"
      
      --answer="{SEND}"
      

      One way we can improve the execution time is to split the sum into multiple partial sums consisting of columns from the original sum.

      This process is automated by the [[ SubstitutedExpression.split_sum ]] helper method, which can now be called from a run file.

      The following run file executes in 65ms. (The internal runtime of the generated program is 277µs).

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      SubstitutedExpression.split_sum
      
      "{SAINT} + {ANDREW} = {301114}"
      
      --literal="0134"
      --distinct="ADEINRSTW"
      --answer="{SEND}"
      

      Solution: SEND = 3568.

      The solver finds 4 solutions to the sum as (I, R) = (1, 9) and (T, W) = (0, 4), in some order.

      Like

    • GeoffR's avatar

      GeoffR 11:18 am on 7 July 2022 Permalink | Reply

      
      % A Solution in MiniZinc
      include "globals.mzn";
      
      var 1..9:S; var 1..9:A; var 0..9:I;
      var 0..9:N; var 0..9:T; var 0..9:D;
      var 0..9:R; var 0..9:E; var 0..9:W;
      
      constraint all_different([S,A,I,N,T,D,R,E,W]);
      
      var 10000..99999: SAINT = 10000*S + 1000*A + 100*I + 10*N + T;
      var 100000..999999: ANDREW = 100000*A + 10000*N + 1000*D+ 100*R + 10*E + W;
      
      var 1000..9999:SEND = 1000*S + 100*E + 10*N + D;
      
      constraint SAINT + ANDREW == 301114;
      
      solve satisfy;
      output["SEND = " ++ show(SEND) ];
      
      % SEND = 3568
      % ----------
      % ==========
      
      
      

      Like

    • GeoffR's avatar

      GeoffR 2:37 pm on 7 July 2022 Permalink | Reply

      A solution simulating the normal column addition process, with carry digits.

      
      % A Solution in MiniZinc
      include "globals.mzn";
      
      %    S A I N T
      %  A N D R E W
      %-------------
      %  3 0 1 1 1 4
      %-------------
      
      var 1..9:S; var 1..9:A; var 0..9:I;
      var 0..9:N; var 0..9:T; var 0..9:D;
      var 0..9:R; var 0..9:E; var 0..9:W;
      
      constraint all_different([S,A,I,N,T,D,R,E,W]);
      
      % column carry digits for adding columns
      var 0..1: c1; var 0..1: c2; var 0..1: c3; var 0..1: c4; var 0..1: c5; 
      
      % carry digits start from RH end of sum
      constraint (T + W) mod 10 == 4 /\ (T + W) div 10 == c1;
      
      constraint (N + E + c1) mod 10 == 1 /\ (N + E + c1) div 10 == c2;
      
      constraint (I + R + c2) mod 10 == 1 /\ (I + R + c2) div 10 == c3;
      
      constraint (A + D + c3) mod 10 == 1 /\ (A + D + c3) div 10 == c4;
      
      constraint (S + N + c4) mod 10 == 0 /\ (S + N + c4) div 10 == c5;
      
       constraint A + c5 == 3;
       
      solve satisfy;
      
      output ["SEND = \(S)\(E)\(N)\(D)" 
      ++  ", SAINT = \(S)\(A)\(I)\(N)\(T)"
      ++  ", ANDREW = \(A)\(N)\(D)\(R)\(E)\(W)" ];
      
      % SEND = 3568, SAINT = 32964, ANDREW = 268150
      % ----------
      % SEND = 3568, SAINT = 32960, ANDREW = 268154
      % ----------
      % SEND = 3568, SAINT = 32164, ANDREW = 268950
      % ----------
      % SEND = 3568, SAINT = 32160, ANDREW = 268954
      % ----------
      % ==========
      
      
      

      Like

  • Unknown's avatar

    Jim Randell 10:32 am on 5 July 2022 Permalink | Reply
    Tags:   

    Brain-Teaser 731: A little list 

    From The Sunday Times, 20th July 1975 [link]

    (1) In this list there is a true statement and a false statement whose numbers add up to give the number of a false statement.

    (2) Either statement 4 is false or there are three  consecutive true statements.

    (3) The number of the last false statement subtracted from the product of the numbers of the first and last true statements gives the number of a statement which is false.

    (4) The number of even-numbered true statements equals the number of false statements.

    (5) One of the first and last statements is true and the other is false.

    (6) When I first sent this problem to the editor, thanks to a typing error no sixth statement was included. However the answer to the following question was the same then as it is now:

    Which statements are false?

    This puzzle is included in the book The Sunday Times Book of Brain-Teasers: Book 2 (1981). The puzzle text above is taken from the book.

    [teaser724]

     
    • Jim Randell's avatar

      Jim Randell 10:33 am on 5 July 2022 Permalink | Reply

      This Python program runs in 62ms. (Internal runtime is 680µs).

      Run: [ @replit ]

      from enigma import (subsets, filter2, irange, first, product, tuples, icount, printf)
      
      # solve the puzzle with k (= 5 or 6) statements, considering only the first 5 statements
      def solve(k):
      
        # consider the puzzle with k statements
        for ss in subsets((True, False), size=k, select="M"):
          # check the first 5 statements
          (s1, s2, s3, s4, s5) = first(ss, 5)
          (ts, fs) = filter2((lambda n: ss[n - 1]), irange(1, k))
      
          # 1: "in this list there is a true statement and a false statement
          # whose numbers add up to give the number of a false statement"
          if not (s1 == any(x + y in fs for (x, y) in product(ts, fs))): continue
      
          # 2: "either statement 4 is false, or there are three consecutive
          # true statements"
          if not (s2 == ((s4 is False) != (any(all(xs) for xs in tuples(ss, 3))))): continue
      
          # 3: "the number of the last false statement subtracted from the
          # product of the numbers of the first and last true statements gives
          # the number of a statement which is false"
          if not (s3 == (ts and fs and ts[0] * ts[-1] - fs[-1] in fs)): continue
      
          # 4: "the number of even numbered true statements equals the number
          # of false statements"
          if not (s4 == (icount(n for n in ts if n % 2 == 0) == len(fs))): continue
      
          # 5: "one of the first and last statements is true and the other is false"
          if not (s5 == (s1 != ss[-1])): continue
      
          # return the numbers of the false statements
          yield fs
      
      # look for solutions for the first 5 statements of the 6 statement
      # version of the puzzle, grouped by the value of statement 6
      (fs6t, fs6f) = filter2((lambda fs: 6 not in fs), solve(6))
      
      # if statement 6 is false, we don't have to worry about the 5 statement version
      printf("false statements = {fs6f}")
      
      # if statement 6 is true, the solution(s) must be the same as the 5 statement version
      fs5 = list(solve(5))
      if fs5 == fs6t:
        printf("false statements = {fs6t}")
      

      Solution: Statements 3, 4, 6 are false.

      So statements 1, 2, 5 are true.

      1. (true) “in this list there is a true statement and a false statement whose numbers add up to give the number of a false statement”.
      There are two: 1 + 3 = 4, 2 + 4 = 6.

      2. (true) “either statement 4 is false, or there are three consecutive true statements”.
      4 is false; there are not three consecutive true statements.

      3. (false) “the number of the last false statement subtracted from the product of the numbers of the first and last true statements gives the number of a statement which is false”.
      1×5 − 6 = −1, is not the number of a statement.

      4. (false) “the number of even numbered true statements equals the number of false statements”.
      There is 1 even numbered true statement, and there are 3 false statements.

      5. (true) “one of the first and last statements is true and the other is false”.
      1 is true; 6 is false.

      6. (false) “When I first sent this problem to the editor, thanks to a typing error no sixth statement was included. However the answer to the following question was the same then as it is now: Which statements are false”.
      The first part could be false (there was no error). But if it was true there are no solutions given only the first 5 statements, so the answer to the puzzle could not be the same if the 6th statement was included.

      Like

  • Unknown's avatar

    Jim Randell 12:49 pm on 3 July 2022 Permalink | Reply
    Tags:   

    Brain-Teaser 120: Rugby results 

    From The Sunday Times, 14th July 1963 [link]

    At a certain stage in our all-against-all Rugby football competition the table of results read as follows. There had been no matches in which neither side had scored at all:

    What was the result of the match between A and B?

    Note: This problem was set when a try was worth 3 points, a penalty goal 3 points and a converted try 5 points. Match points are on the usual basis of 2 for a win and 1 for a draw.

    This puzzle is included in the book Sunday Times Brain Teasers (1974).

    [teaser120]

     
    • Jim Randell's avatar

      Jim Randell 12:50 pm on 3 July 2022 Permalink | Reply

      There are certain scores which are not possible to make from combinations of 3 and 5 points.

      This Python program uses the [[ express() ]] function from the enigma.py library to determine what they are. We then use the [[ Football() ]] helper class to determine possible match outcomes and scorelines.

      It runs in 89ms.

      Run: [ @replit ]

      from enigma import (express, irange, peek, Football, digit_map, cache)
      
      # find scores that cannot be made from multiples of 3 and 5
      invalid = set(n for n in irange(0, 18) if not peek(express(n, [3, 5]), default=None))
      
      # scoring system (2 points for a win, 1 for a draw)
      football = Football(points=dict(w=2, d=1))
      
      # digits used in the table (10=A, 13=D, 15=F, 16=G, 18=I)
      d = digit_map(0, 18)
      
      # the table, goals for/against
      (table, gf, ga) = (dict(played="34432", points="55420"), "DGF53", "8AI88")
      
      # check for a valid score (and cache as we go along)
      @cache
      def valid(s):
        # unplayed?
        if s is None: return True
        # otherwise:
        # (0, 0) is not allowed
        if s == (0, 0): return False
        # reject invalid scores
        if invalid.intersection(s): return False
        # looks OK
        return True
      
      # find possible match outcomes
      for (ms, _) in football.substituted_table(table, d=d):
        # find possible scorelines
        for ss in football.substituted_table_goals(gf, ga, ms, d=d, valid=valid):
          # output solution
          football.output_matches(ms, ss, teams="ABCDE")
      

      Solution: The result of the A vs B match was a 5-3 win for A.

      The full results are:

      A vs B = 5-3
      A vs C = 5-5
      A vs D = 3-0
      A vs E = (unplayed)
      B vs C = 5-5
      B vs D = 5-0
      B vs E = 3-0
      C vs D = 0-5
      C vs E = 5-3
      D vs E = (unplayed)

      Like

  • Unknown's avatar

    Jim Randell 4:40 pm on 1 July 2022 Permalink | Reply
    Tags:   

    Teaser 3119: Hidden powers 

    From The Sunday Times, 3rd July 2022 [link] [link]

    My grandson is studying “History since the Battle of Hastings”. I made him a game, which consisted of a row of nine cards, each with a different non-zero digit on it. Throw a standard die, note the number of spots displayed, count that number of places along the row and pause there. Throw the die again, move the corresponding number of places further along and pause again. Repeat this until you come off the end of the row, noting the digit or digits you paused on and put these together in the same order, to produce a number.

    Keeping the cards in the same order I asked my grandson to try to produce a square or cube or higher power. He eventually discovered that the lowest possible such number was equal to the number of one of the years that he had been studying.

    What is the order of the nine digits along the row?

    [teaser3119]

     
    • Jim Randell's avatar

      Jim Randell 5:54 pm on 1 July 2022 Permalink | Reply

      It took me a little while to understand how this puzzle is supposed to work.

      And despite my initial misgivings there is only one possible arrangement of digits that can lead to the situation described.

      The following Python program isn’t very quick (it tries all possible arrangements of digits), but it does find the required arrangement.

      The code to generate powers is adapted from Teaser 683 and Enigma 1777.

      It runs in 669ms.

      Run: [ @replit ]

      from enigma import (Primes, nsplit, subsets, irange, tuples, printf)
      
      # generate powers (x^y) in numerical order, x >= 0, y >= 2,
      def powers():
        # powers less than 2^2
        yield 0
        yield 1
        # powers from 2^2 upwards
        base = { 2: 2 }
        power = { 2: 4 }
        maxp = 2
        primes = Primes(35, expandable=1).generate(maxp + 1)
        while True:
          # find the next power
          n = min(power.values())
          yield n
          # what powers are involved
          ps = list(p for (p, v) in power.items() if v == n)
          # increase the powers
          for p in ps:
            base[p] += 1
            power[p] = pow(base[p], p)
          # do we need to add in a new power?
          if maxp in ps:
            maxp = next(primes)
            base[maxp] = 2
            power[maxp] = pow(2, maxp)
      
      # collect candidate powers
      pows = list()
      for n in powers():
        if n > 2022: break
        # split the power into digits
        ds = nsplit(n)
        # no 0 digits, or repeated digits
        if 0 in ds or len(set(ds)) != len(ds): continue
        pows.append((n, ds))
      
      # check powers that can be made with an arrangement of digits
      def play(ss):
        # find powers that can be made
        ns = list()
        for (n, ds) in pows:
          # find indices of the digits
          js = list(ss.index(d) for d in ds)
          # check entry and exit
          if js[0] > 5 or js[-1] < 3: continue
          js.insert(0, -1)
          # and corresponding dice throws
          ts = list(y - x for (x, y) in tuples(js, 2))
          if min(ts) < 1 or max(ts) > 6: continue
          # check the power is permissible
          if n < 1066: return
          ns.append(n)
        # return the list of powers
        return ns
      
      # consider possible arrangements of digits
      for ss in subsets(irange(1, 9), size=len, select="P"):
        ns = play(ss)
        if ns:
          printf("{ss} -> {ns}")
      

      Solution: The order of the digits is: 1, 9, 8, 7, 5, 2, 3, 4, 6.

      And the smallest (and onliest) power which can be made by rolling the die is 1936, with rolls of (1, 1, 5, 2, *).


      Although the puzzle talks about the lowest power that can be generated, it turns out it is the only (non-trivial) power that can be generated with the required arrangement of cards.

      The puzzle will continue to work in the future, until the year 4913 (= 17³) is incorporated into the history book.

      Like

      • Jim Randell's avatar

        Jim Randell 10:13 am on 2 July 2022 Permalink | Reply

        Here’s a faster (but slightly longer) program. Instead of just trying all possible arrangements of digits, it recursively constructs arrangements that cannot give powers less than 1066, and then finds which greater powers can be made.

        It runs in 174ms.

        Run: [ @replit ]

        from enigma import (Primes, nsplit, subsets, tuples, diff, update, printf)
        
        # generate powers (x^y) in numerical order, x >= 0, y >= 2,
        def powers():
          # powers less than 2^2
          yield 0
          yield 1
          # powers from 2^2 upwards
          base = { 2: 2 }
          power = { 2: 4 }
          maxp = 2
          primes = Primes(35, expandable=1).generate(maxp + 1)
          while True:
            # find the next power
            n = min(power.values())
            yield n
            # what powers are involved
            ps = list(p for (p, v) in power.items() if v == n)
            # increase the powers
            for p in ps:
              base[p] += 1
              power[p] = pow(base[p], p)
            # do we need to add in a new power?
            if maxp in ps:
              maxp = next(primes)
              base[maxp] = 2
              power[maxp] = pow(2, maxp)
        
        # collect candidate powers (disallowed and allowed)
        (xpows, pows) = (list(), list())
        for n in powers():
          if n > 2022: break
          # split the power into digits
          ds = nsplit(n)
          # no 0 digits, or repeated digits
          if 0 in ds or len(set(ds)) != len(ds): continue
          (xpows if n < 1066 else pows).append((n, ds))
        
        # check if digit sequence <ds> can be made from arrangement <ns>
        def play(ns, ds):
          # find indices of the digits
          js = list(ns.index(d) for d in ds)
          # check entry and exit
          if js[0] > 5 or js[-1] < 3: return False
          # check corresponding dice throws (1-6)
          js.insert(0, -1)
          return all(0 < y - x < 7 for (x, y) in tuples(js, 2))
        
        # generate arrangements <ns> that don't allow sequences <xs> to be made
        # ns = digit arrangement
        # xs = disallowed sequences
        # ss = allocated digits
        # js = indices of empty slots
        def solve(ns, xs, ss=set(), js=None):
          if not xs:
            yield ns
          else:
            # find the shortest sequences with the fewest missing digits
            fn = lambda ds: (len(set(ds).difference(ss)), len(ds))
            ds = min(xs, key=fn)
            # choose placements for the missing digits
            ms = diff(ds, ss)
            if not js: js = set(j for (j, n) in enumerate(ns) if n is None)
            for ks in subsets(js, size=len(ms), select="P"):
              ns_ = update(ns, ks, ms)
              if not play(ns_, ds):
                # solve the rest
                yield from solve(ns_, xs.difference([ds]), ss.union(ms), js.difference(ks))
        
        # find layouts that do not permit sequences from xpows
        for ns in solve([None] * 9, set(ds for (_, ds) in xpows)):
          # look for permissible powers
          ps = list(n for (n, ds) in pows if play(ns, ds))
          if ps:
            printf("{ns} -> {ps}")
        

        Liked by 1 person

    • Frits's avatar

      Frits 11:28 pm on 1 July 2022 Permalink | Reply

      Reusing my code of Teaser 683 and using Jim’s early performance enhancement (also considering number 1 as a power).

         
      from itertools import permutations
      
      def prime_factors(n):
        i = 2
        factors = []
        while i * i <= n:
          if n % i:
            i = 3 if i == 2 else i + 2
          else:
            n //= i
            factors.append(i)
        if n > 1:
          factors.append(n)
           
        return factors
          
      def is_a_power(n):
        pf = prime_factors(n)
        if len(pf) < 2: 
          return False
           
        # occurences of digits  
        oc = {pf.count(x) for x in set(pf)}
        if mult_gcd(list(oc)) == 1:
          return False
        return True
       
      # calculate greatest common divisor of multiple numbers  
      def mult_gcd(li):
        if len(li) == 1:
          return li[0]
       
        for i in range(len(li) - 1):
          a = li[i]
          b = li[i+1]
          while b:
            (a, b) = (b, a % b)
           
          if a == 1: return a
           
          li[i+1] = a
        return a  
       
      # generate powers, stop if you encounter one less than 1067    
      def solve(k, s, pw=0, ns=[], rs=set()):
        if k > 0:
          # year must be before 2023
          if k == 1 and pw > 202:
            return []
          ls = len(s)
          for i in range(6):
            # check if you come off the end of the row
            if i > ls - 1: break
            p = 10 * pw + s[i]
            if p in pows and ls - i < 7:
              return [p]
            
            # recurse if worthwhile
            if p in powstart:  
              res = solve(k - 1, s[i + 1:], p, ns + [i + 1], rs)  
              if len(res) == 1:
                rs.add(res[0])
                if res[0] < 1067:
                  return []  # no solution
          
          # if we didn't encounter a low power return list of powers
          return list(rs)    
          
      # collect candidate powers
      pows = {i for i in range(1, 2023) if is_a_power(i) and \
              len(s:= str(i)) == len(set(s)) and "0" not in s}  
      
      powstart = {int(str(p)[:i]) for p in pows for i in range(1, 4)}
      
      # consider possible arrangements of digits
      for perm in permutations(range(1, 10)):
        # [performance] single digit powers cannot be in slots 3, 4, 5
        if {1, 4, 8, 9}.intersection(perm[3:6]): continue
      
        r = sorted(solve(4, perm, rs=set()))
        if r and r[0] > 1066:
          print(perm, "number", r[0])
      

      Like

      • Frits's avatar

        Frits 9:29 am on 2 July 2022 Permalink | Reply

        Python 3.9 introduced multiple arguments version of math.gcd so math.gcd() can be used instead of mult_gcd().

        Like

    • Frits's avatar

      Frits 2:50 pm on 2 July 2022 Permalink | Reply

      Just as with Jim ‘s third program only calling solve() for 49 permutations.

      Doing the checks for 2-digit and 3-digit powers in a general way is a bit slower.
      Also storing digit positions in a dictionary to minimize index() calls is a bit slower.

         
      from itertools import permutations
      from math import gcd
      
      def prime_factors(n):
        i = 2
        factors = []
        while i * i <= n:
          if n % i:
            i = 3 if i == 2 else i + 2
          else:
            n //= i
            factors.append(i)
        if n > 1:
          factors.append(n)
           
        return factors
          
      def is_a_power(n):
        pf = prime_factors(n)
        if len(pf) < 2: 
          return False
           
        # occurences of digits  
        oc = {pf.count(x) for x in set(pf)}
        if gcd(*oc) == 1:
          return False
        return True
       
      # generate powers, stop if you encounter one less than 1067    
      def solve(k, s, pw=0, ns=[], rs=set()):
        if k > 0:
          # year must be before 2023
          if k == 1 and pw > 202:
            return []
          ls = len(s)
          for i in range(6):
            # check if you come off the end of the row
            if i > ls - 1: break
            p = 10 * pw + s[i]
            if p in pows and ls - i < 7:
              return [p]
            
            # recurse if worthwhile
            if p in powstart:  
              res = solve(k - 1, s[i + 1:], p, ns + [i + 1], rs)  
              if len(res) == 1:
                rs.add(res[0])
                if res[0] < 1067:
                  return []  # no solution
          
          # if we didn't encounter a low power return list of powers
          return list(rs)    
          
      # collect candidate powers
      pows = {i for i in range(1, 2023) if is_a_power(i) and \
              len(s:= str(i)) == len(set(s)) and "0" not in s}  
      
      powstart = {int(str(p)[:i]) for p in pows for i in range(1, 4)}
      
      # 2-digit powers
      pows2 = [(x // 10, x % 10) for x in pows if 9 < x < 100]
      
      # 3-digit powers
      pows3 = [(x // 100, (x // 10) % 10, x % 10) for x in pows if 99 < x < 1000]
      
      # consider possible arrangements of digits
      for perm in permutations(range(1, 10)):
        # [performance] single digit powers cannot be in slots 3, 4, 5
        if {1, 4, 8, 9}.intersection(perm[3:6]): continue
        
        # check 2-digit powers
        for a, b in pows2:
          posa = perm.index(a)
          posb = perm.index(b)
          # can we make the power in 2 jumps and then come off the end of the row?
          if 0 < (posb - posa) < 7 and posa < 6 and posb > 2:
            break
        else:  # no break   
          # check 3-digit powers
          for a, b, c in pows3:
            posa = perm.index(a)
            posb = perm.index(b)
            posc = perm.index(c)
            # can we make the power in 3 jumps and 
            # then come off the end of the row?
            if not (0 < (posb - posa) < 7 and 0 < (posc - posb) < 7): continue
            if posa < 6 or posc > 2:
              break    
          else:  # no break   
            # no 2- or 3-digit power can be formed
            r = sorted(solve(4, perm, rs=set()))
            if r and r[0] > 1066:
              print(perm, "number", r[0])
      

      Like

    • Frits's avatar

      Frits 4:04 pm on 2 July 2022 Permalink | Reply

      Without recursion.

         
      from itertools import permutations
      from enigma import mgcd
      
      def prime_factors(n):
        i = 2
        factors = []
        while i * i <= n:
          if n % i:
            i = 3 if i == 2 else i + 2
          else:
            n //= i
            factors.append(i)
        if n > 1:
          factors.append(n)
           
        return factors
          
      def is_a_power(n):
        pf = prime_factors(n)
        if len(pf) < 2: 
          return False
           
        # occurences of digits  
        oc = {pf.count(x) for x in set(pf)}
        if mgcd(*oc) == 1:
          return False
        return True
       
      # collect candidate powers
      pows = {i for i in range(1, 2023) if is_a_power(i) and \
              len(s:= str(i)) == len(set(s)) and "0" not in s}  
      
      # powers grouped by number of digits
      pws = [[[int(x) for x in str(p)] for p in pows 
               if 10**i <= p < 10**(i + 1)] for i in range(1, 4)]
      
      # do powers exists >= 2000 ?
      exists2xxx = any(x for x in pws[2] if x[0] == 2)
      
      # consider possible arrangements of digits
      for perm in permutations(range(1, 10)):
        # [performance] single digit powers cannot be in slots 3, 4, 5
        if {1, 4, 8, 9}.intersection(perm[3:6]): continue
        
        for ps in pws[:-1]:
          for p in ps:
            pos = [perm.index(i) for i in p]
            # first digit reachable and then come off the end of the row?
            if not(pos[0] < 6 and pos[-1] > 2): continue
            # can we make the power in <len(pos)> jumps ...
            if any(y - x not in {1, 2, 3, 4, 5, 6} for x, y in zip(pos, pos[1:])):
              continue
              
            # we have found a valid 2- or 3-digit power
            break
          else: # no break  
            continue
            
          break   # break if break occurred    
        else:  # no break
          if not exists2xxx:
            # check position of 1
            if perm.index(1) > 5: continue
          
          # check 4-digit powers
          for p in sorted(pws[2]):
            pos = [perm.index(i) for i in p]
            # it is enough to only check for increasing numbers
            if pos != sorted(pos): continue
            
            print(perm, "number", "".join(str(x) for x in p))
            break
      

      Like

    • Frits's avatar

      Frits 5:48 pm on 8 July 2022 Permalink | Reply

         
      from itertools import permutations
      from functools import reduce
      from math import gcd
      
      def prime_factors(n):
        i = 2
        factors = []
        while i * i <= n:
          if n % i:
            i = 3 if i == 2 else i + 2
          else:
            n //= i
            factors.append(i)
        if n > 1:
          factors.append(n)
           
        return factors
          
      def is_a_power(n):
        if n == 1: return [1]
        pf = prime_factors(n)
        if len(pf) < 2: 
          return False
           
        # occurences of digits  
        oc = {pf.count(x) for x in set(pf)}
        if gcd(*oc) == 1:
          return False
        return True
      
      # group type
      gtype = lambda i: "first" if i == 0 else "middle" if i == 1 else "last"
              
      # collect all candidate powers
      pows = {i for i in range(1, 2023) if is_a_power(i) and 
              len(s:= str(i)) == len(set(s)) and "0" not in s} 
      
      # powers grouped by number of digits
      pws = [set(str(p) for p in pows 
               if 10**i <= p < 10**(i + 1)) for i in range(4)]
               
      # return possible arrangements of digits in group <grp> for 2-digit powers
      def arrangements(grp):
        return [p for p in permutations(grp) if all(fdgt + sdgt not in pws[1] 
                for fdgt, sdgt in [(p[0], p[1]), (p[0], p[2]), (p[1], p[2])])
               ]
      
      # return all numbers per group that don't occur in other groups
      def calc_known(grp):
        return [[x for x in grp[i] 
                if all(x not in t for t in 
                      [y for j, y in enumerate(grp) if j != i])] 
                for i in range(3)]
       
      # for all combinations in <lst> check if a 3-digit power can be made
      # with a digit in the last group
      def check_last_group(grp, lst):
        forbidden = [set() for _ in range(len(lst))]
        for i, n in enumerate(lst):
          # for all combinations in <lst> 
          for x in [(n[0] + n[1]), (n[0] + n[2]), (n[1] + n[2])]:
            for p in pws[2]:
              if x in {p[:-1]}:
                # x + p[-1] is 3-digit power so p[-1] is forbidden in last group
                forbidden[i].add(p[-1])
              
        # return numbers in the last group which are forbidden for all <lst> entries
        return reduce((lambda x, y: x & y), map(set, forbidden))
      
      # maintain groups if all numbers in a group are known
      def maintain_groups(grp, fixed):
        for i, fix in enumerate(fixed):
          # all numbers in a group are known?
          if len(fix) == 3:
            # remove other numbers from this group
            for n in [x for x in grp[i] if x not in fix]:
              print(f"{gtype(i)} group must be {','.join(fix)}:"
                    f" remove {n} from {gtype(i)} group")
            grp[i] = set(fix)
                  
      def print_groups(g):
        print("----- groups:", ",".join(sorted(g[0])), "--", 
                               ",".join(sorted(g[1])), "--", 
                               ",".join(sorted(g[2])), "-----")
      
      # maintain and print groups  
      def update(g):
        known = calc_known(g)
        maintain_groups(g, known)
        print_groups(g)
        return known
      
      
      print("-------- START -----------")
      
      print("split the nine digits in three groups of three digits")
      # list <g> contains candidate digits for each group
      g = [set("123456789"), set("123456789"), set("123456789")]
      print_groups(g)
      
      print(f"removing 1 from last group"
            f" as we need a 4-digit power 1xxx as an answer")
      g[2] -= {'1'}             # we need a 4-digit power
      
      print("removing", ", ".join(sorted(pws[0])), 
            "from middle group to prevent single digit powers")
      g[1] -= set(pws[0])       
      
      for x in calc_known(g)[0]:
        # can we can make a 2-digit power with <x>?
        for p in [p2 for p2 in pws[1] if p2[0] == x]:
          print("removing", p[1], "from middle group to prevent power", p)
          g[1].remove(p[1])         
      
      print_groups(g)
      
      # check middle group
      if len(g[1]) == 4:
        # see what happens if we remove <m> from middle group 
        for m in g[1]:
          middle = [x for x in g[1] if x != m]
          for m2 in middle:
            # if combination is a 2-digit power <m> must occur in middle group
            if m2 + m in pws[1]: 
              g[2] -= {m}
            if m + m2 in pws[1]: 
              g[0] -= {m}
         
          # get arrangements from <middle> without a 2-digit power
          lst = arrangements(middle)
          
          if len(lst) == 1:
            # we have only one arrangement for slots 3, 4 and 5 
            # can we can make a 3-digit power with this arrangement?
            arr = lst[0]
            for a in [(arr[0] + arr[1]), (arr[0] + arr[2]), (arr[1] + arr[2])]:
              # 3-digit powers that can be made from <a>
              tdp = [p3 for p3 in pws[2] if a in p3[:-1]]
              for p in tdp:
                # last digit of 3-digit power may not be used yet
                if p[-1] in (arr + ('1', )): continue
                # removing m has lead to a valid 3-digit power 
                print(f"removing {m} from the middle group can lead to"
                      f" power {p}, remove {m} from first and last group")
                g[0] -= {m}
                g[2] -= {m}
               
        # maintain and print groups
        known = update(g)
        
        # for every 3-digit power check if two of it's digits have to occur in two 
        # groups, if so we can remove the other digit from the other group
        first = 1
        for p in pws[2]:
          unknown = [i for i in range(3) if p[i] not in known[i]]  
          if len(unknown) != 1: continue
          
          i = unknown[0]
          if first: 
            print("known digits per group:", known)
            first = 0
          
          print(f"digits {' and '.join(p[j] for j in range(3) if j != i)}"
                f" of power {p} each have to occur in a different specific group:"
                f" remove {p[i]} from {gtype(i)} group")
          g[i] -= {p[i]}
        
        update(g)
        
        # get numbers in the last group which make a 3-digit power for all
        # valid combinations in the middle group
        for s in check_last_group(g, arrangements(g[1])):
          print(f"all valid combinations of middle group combined with the number"
                f" {s} lead to 3-digit power: remove {s} from last group")
          g[2] -= {s}
               
        update(g)
        print()
        
        # consider possible arrangements of digits
        for p0 in permutations(g[0]):
          for p1 in arrangements(g[1]):
            for p2 in permutations(g[2]):
              perm = p0 + p1 + p2
              # check 2-digit and 3-digit powers
              for ps in pws[1:-1]:
                for p in ps:
                  # the index positions of its digits in the row of cards
                  pos = [perm.index(i) for i in p]
                  # ensure that its first digit is reachable and that its
                  # last digit can be the final digit (with a throw of six)
                  if not(pos[0] < 6 and pos[-1] > 2): 
                    continue
                  # can we make the power in intervals of  1 - 6 jumps?
                  if any(y - x not in set(range(1, 7))
                        for x, y in zip(pos, pos[1:])):
                    continue
      
                  # we have found a valid 2-digit or 3-digit power
                  break
                else: 
                  continue
      
                break   # break again if break occurred    
              else:  
                # check 4-digit powers
                for p in sorted(pws[3]):
                  pos = [perm.index(i) for i in p]
                  
                  # checking for the correct digit order is sufficient because
                  # the highest gap between digits is five empty slots
                  if pos != sorted(pos): continue
                  
                  print(perm, "number", p)
                  break  # only print the lowest 4-digit power
      

      Like

  • Unknown's avatar

    Jim Randell 9:59 am on 30 June 2022 Permalink | Reply
    Tags:   

    Teaser 2720: Better tetra 

    From The Sunday Times, 9th November 2014 [link] [link]

    I have four tetrahedral dice. Each has four identical triangular faces and on each face of each die is one of the numbers 1, 2, 3 or 4 (repeats being allowed on a die). No die uses the same four numbers and, for each die, the sum of its four numbers is ten.

    I play a game with my friend. My friend chooses a die first and then I choose one of the remaining three dice. We each throw our chosen die and score the face-down number: sometimes it’s a draw, otherwise the higher number wins. I can choose my die so that I am always more likely to win.

    So my friend changes the rules. We now throw our chosen die twice, add the two numbers, and the higher total wins.

    Now he knows that there is one die he can choose that makes him more likely to win the game.

    What are the four numbers on the winning dice?

    The wording for this puzzle has been modified from the originally published version.

    [teaser2720]

     
    • Jim Randell's avatar

      Jim Randell 10:00 am on 30 June 2022 Permalink | Reply

      (See: [@youtube]).

      We assume that the dice are chosen in such a way that each player is aware of which die has been chosen. (So, for example, they may be different colours and arranged on the table, but not be hidden a bag and chosen by putting your hand in and selecting one at random).

      When we compare 2 dice, we say that one is “better” than the other, if it is more like to win a game against the other die.

      There are 5 possible dice (i.e. 5 possible decompositions of 10 into the numbers 1, 2, 3, 4), but the set only contains 4 of these.

      It turns out there is only one possible set of 4, where whatever die A chooses B can always choose a better die.

      And from this set we can look to see if there is die which is better when played against each of the other dice.

      This Python program runs in 61ms. (Internal runtime is 593µs).

      Run: [ @replit ]

      from enigma import (decompose, subsets, multiset, product, printf)
      
      # return wins for X and Y in a game with k throws?
      def play(X, Y, k):
        # generate scores for X and Y
        (sX, sY) = (multiset.from_seq(sum(ss) for ss in subsets(ns, size=k, select="M")) for ns in (X, Y))
        # count wins for X, wins for Y
        wX = wY = 0
        for ((x, nx), (y, ny)) in product(sX.items(), sY.items()):
          if x > y:
            wX += nx * ny
          elif y > x:
            wY += nx * ny
        return (wX, wY)
      
      # is X better than Y in a game with k throws?
      def better(X, Y, k):
        (wX, wY) = play(X, Y, k)
        return wX > wY
      
      # generate possible dice
      ns = list(decompose(10, 4, increasing=1, sep=0, min_v=1, max_v=4))
      
      # choose a set of 4 dice, and solve the puzzle
      for ds in subsets(ns, size=4):
      
        # with 1 throw, whatever die A chooses, B can always choose a better die
        if not all(any(better(B, A, 1) for B in ds if B != A) for A in ds): continue
        printf("dice = {ds}")
      
        # with 2 throws, A can choose a die that is better than the others
        for A in ds:
          if not all(better(A, B, 2) for B in ds if B != A): continue
          printf("-> A = {A}")
      

      Solution: The best die in the second game is: (1, 3, 3, 3).


      The four dice are:

      (1, 1, 4, 4)
      (1, 3, 3, 3)
      (2, 2, 2, 4)
      (2, 2, 3, 3)

      And we have:

      (1, 1, 4, 4) is beaten by (2, 2, 2, 4)
      (2, 2, 2, 4) is beaten by (2, 2, 3, 3) and (1, 3, 3, 3)
      (2, 2, 3, 3) is beaten by (1, 3, 3, 3)
      (1, 3, 3, 3) is beaten by (1, 4, 4, 4)

      So whichever die is chosen first, there is always a choice from the other three that is better.

      Like

    • Frits's avatar

      Frits 5:54 pm on 30 June 2022 Permalink | Reply

      Apparently we can also compare lists with the less than operator in Python.

         
      from itertools import product
      from enigma import SubstitutedExpression
      
      # number of times x wins from y if die is thrown once
      play1 = lambda x, y: sum([p > q for p, q in product(x, y)])
      
      # number of times x wins from y if die is thrown twice
      def play2(x, y):
        # add the score of two throws
        x2 = [p + q for p, q in product(x, x)]
        y2 = [p + q for p, q in product(y, y)]
        
        return sum([p > q for p, q in product(x2, y2)])
      
      
      # the alphametic puzzle
      p = SubstitutedExpression(
        [
         "A <= B <= C",
         "10 - A - B - C = D",
         "C <= D",
         
         "E <= F <= G",
         "10 - E - F - G = H",
         "G <= H",
         "(A, B, C, D) < (E, F, G, H)",
         
         "I <= J <= K",
         "10 - I - J - K = L",
         "K <= L",
         "(E, F, G, H) < (I, J, K, L)",
         
         "M <= N <= O",
         "10 - M - N - O = P",
         "O <= P",
         "(I, J, K, L) < (M, N, O, P)",
         
         # with 1 throw, whatever die my friend chooses (x), 
         # I can always choose a better die (y), resulting in 4 wins
         "sum([any(play1(y, x) > play1(x, y) \
               for y in [(A,B,C,D), (E,F,G,H), (I,J,K,L), (M,N,O,P)] if y != x) \
               for x in [(A,B,C,D), (E,F,G,H), (I,J,K,L), (M,N,O,P)]]) == 4",
        ],
        answer="(A,B,C,D), (E,F,G,H), (I,J,K,L), (M,N,O,P)",
        env=dict(play1=play1),
        distinct="",
        digits=range(1, 5),
        reorder=0,
        verbose=0,    # use 256 to see the generated code
      )
      
      # check if my friend can win with the die thrown twice
      for _, s in p.solve():
        for s1 in s:
          if all(play2(s1, s2) > play2(s2, s1) for s2 in s if s1 != s2):
            print("answer:", s1)
      

      Like

  • Unknown's avatar

    Jim Randell 9:54 am on 28 June 2022 Permalink | Reply
    Tags: by: M C Breach   

    Brain-Teaser 724: Counting sheep … 

    From The Sunday Times, 1st June 1975 [link]

    In a primitive eastern country a shepherd was counting his
    sheep into four separate pens. In the first were 75 sheep, so he wrote in his records:

    In the second were 255 and he wrote:

    In the third were 183 and he wrote:

    After counting the sheep in the fourth pen he wrote:

    How many sheep were in the fourth pen?

    This puzzle is included in the book The Sunday Times Book of Brain-Teasers: Book 2 (1981). The puzzle text above is taken from the book.

    [teaser724]

     
    • Jim Randell's avatar

      Jim Randell 9:55 am on 28 June 2022 Permalink | Reply

      I think this puzzle is a bit open ended.

      I called the symbols A (for the triangle), D (for the box), and X (for the loop).

      For all we know DXA, ADX, XAD could be words that happen to mean 75, 255, 183.

      Or it could be some system with strange rules (like roman numerals, for instance).

      So, I made some assumptions:

      I assumed each symbol stands for a different non-negative integer value (but the same value whenever it appears). And as the same symbols appear in a different order for each of the given values I assumed position is important (as it is in our decimal system).

      My first thought was maybe it was just a substituted base system where each digit is represented by one of the symbols.

      For 255 to be represented by 3-digits the base must be 7 – 15.

      For 75 to be represented by 3-digits the base must be: 5 – 8.

      So we can look at values 75, 255, 183 in base 7 and 8:

      base 7: 75 → “135”; 255 → “513”; 183 → “351”
      base 8: 75 → “113”; 255 → “377”; 183 → “267”

      Only for the case of base 7 do the same 3 digits appear in different orders.

      And in this case the substitution: A=5, D=1, X=3, gives us:

      DAX = 153 [base 7] = 87 [base 10]

      So: 87 is certainly a possible answer (and it is the published answer).


      But there are other solutions where the positions represent different values, but not necessarily increasing powers of some base.

      For instance instead of the positions standing for (49, 7, 1) and the symbols being A=5, D=1, X=3, we swap them over to give:

      positions = (1, 5, 3); A=7, D=49, X=1
      DXA = 49×1 + 1×5 + 7×3 = 75
      ADX = 7×1 + 49×5 + 1×3 = 255
      XAD = 1×1 + 7×5 + 49×3 = 183
      DAX = 49×1 + 7×5 + 1×3 = 87

      But we can also use decreasing position values:

      positions = (39, 11, 7); A=6, D=0, X=3
      DXA = 0×39 + 3×11 + 6×7 = 75
      ADX = 6×39 + 0×11 + 3×7 = 255
      XAD = 3×39 + 6×11 + 0×7 = 183
      DAX = 0×39 + 6×11 + 3×7 = 87

      Note: this combination of positions is able to express all numbers greater than 59.

      positions = (117, 33, 21); A=2, D=0, X=1 (Note: symbols are 0, 1, 2):
      DXA = 0×117 + 1×33 + 2×21 = 75
      ADX = 2×117 + 0×33 + 1×21 = 255
      XAD = 1×117 + 2×33 + 0×21 = 183
      DAX = 0×117 + 2×33 + 1×21 = 87

      Note: this combination of positions can only be used to express multiples of 3.

      Or mixed positions:

      positions = (7, 31, 19); A=1, D=8, X=0
      DXA = 8×7 + 0×31 + 1×19 = 75
      ADX = 1×7 + 8×31 + 0×19 = 255
      XAD = 0×7 + 1×31 + 8×19 = 183
      DAX = 8×7 + 1×31 + 0×19 = 87

      Note: this combination of positions is able to express all numbers greater than 86.

      And we can also find viable answers of 159 or 267.

      positions = (31, 19, 7); A=8, D=0, X=1
      DXA = 0×31 + 1×19 + 8×7 = 75
      ADX = 8×31 + 0×19 + 1×7 = 255
      XAD = 1×31 + 8×19 + 0×7 = 183
      DAX = 0×31 + 8×19 + 1×7 = 159

      positions = (33, 21, 117); A=0, D=1, X=2 (Note: symbols are 0, 1, 2)
      DXA = 1×33 + 2×21 + 0×117 = 75
      ADX = 0×33 + 1×21 + 2×117 = 255
      XAD = 2×33 + 0×21 + 1×117 = 183
      DAX = 1×33 + 0×21 + 2×117 = 267

      These values can also be found by permuting the (49, 7, 1) positions of the base 7 representation.

      Like

      • Jim Randell's avatar

        Jim Randell 11:13 pm on 28 June 2022 Permalink | Reply

        Here is a simple program using the [[ SubstitutedExpression ]] solver from the enigma.py library to solve the puzzle as a standard positional base system (which is how the puzzle is intended).

        It runs in 65ms.

        from enigma import (SubstitutedExpression, map2str, printf)
        
        # consider bases 7 and 8
        for b in [7, 8]:
          # create the alphametic puzzle
          p = SubstitutedExpression(
            [ "DXA == 75", "ADX == 255", "XAD == 183" ],
            base=b,
            answer="DAX",
            verbose=0,
          )
          # solve it
          for (s, ans) in p.solve():
            # output solution
            printf("DAX={ans} [b={b}; {s}]", s=map2str(s, enc=""))
        

        Like

    • GeoffR's avatar

      GeoffR 10:06 am on 29 June 2022 Permalink | Reply

      % A Solution in MiniZinc
      include "globals.mzn";
      % test for numerical bases 7 and 8 only, as Jim's analysis
      % i.e. symbols A (for the triangle), D (for the box), and X (for the loop)
      
      var 1..7:D; var 1..7:A; var 1..7:X;
      
      constraint  % base 7
      (D < 7 /\ A < 7 /\ X < 7 
      /\ (7 * 7 * D + 7 * X + A == 75) 
      /\ (7 * 7 * A + 7 * D + X == 255) 
      /\ (7 * 7 * X + 7 * A + D == 183))
      \/  % base 8
      (  (8 * 8 * D + 8 * X + A == 75) 
      /\ (8 * 8 * A + 8 * D + X == 255)
      /\ (8 * 8 * X + 8 * A + D == 183));
      
      solve satisfy;
      
      output ["[D, A, X ] = " ++ show([D, A, X ]) ];
      
      % [D, A, X ] = [1, 5, 3]  
      % ----------
      % ==========
      % DXA = 135, ADX = 513, XAD = 351, DAX = 153 (base 7)
      % DAX (base 7) = 1*7*7 + 5*7 + 3 = 87 ( base 10)
      
      
      

      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