Updates from May, 2023 Toggle Comment Threads | Keyboard Shortcuts

  • Unknown's avatar

    Jim Randell 8:37 am on 18 May 2023 Permalink | Reply
    Tags:   

    Teaser 2630: River crossing 

    From The Sunday Times, 17th February 2013 [link] [link]

    Two lads walking together had to get back to their tent which was a short distance south-east of them. However, it involved crossing a river which was running from west to east and was a constant five metres wide. Whichever route they chose, they made sure that they were in the water for the shortest distance possible.

    One lad took the obvious such route and went due south and then due east, but the other took the shortest possible such route, thus cutting his friend’s distance by a quarter.

    What was the length of that shortest route?

    [teaser2630]

     
    • Jim Randell's avatar

      Jim Randell 8:38 am on 18 May 2023 Permalink | Reply

      See also: Teaser 3103.

      They both cross the river at right angles to give a river crossing of 5m.

      We can then remove the river, and the remaining (walking) paths are the sides of a right-angled triangle. The optimal route being the hypotenuse, and the first lad traversing the other two sides.

      So if the triangle has sides (x, y, z), the total length of the first path (including river crossing) is: (x + y + 5) and the total length of the second (optimal) path is (z + 5).

      And so:

      (z + 5) / (x + y + 5) = 3/4
      4z + 5 = 3(x + y)

      In order to get a unique solution to the puzzle we need to assume that the tent is exactly SE of the lads (i.e. on a bearing of 135°).

      Then the first lad travels the same total distance on the south leg of his journey (= x + 5) as he does on the east leg (= y).

      So we have:

      y = x + 5
      z = (3(x + y) − 5)/4 = (3x + 5)/2

      and:

      x² + y² = z²
      x² + (x + 5)² = (3x + 5)²/4
      8x² + 40x + 100 = 9x² + 30x + 25
      x² − 10x − 75 = 0
      (x + 5)(x − 15) = 0
      x = 15, y = 20, z = 25

      So the optimal distance is z + 5 = 30m.

      Solution: The shortest route is 30m.

      And the first lad’s distance is 40m.


      We can solve the quadratic equation using the [[ Polynomial() ]] class from the enigma.py library:

      Run: [ @replit ]

      from enigma import (Polynomial, sq, printf)
      
      # construct the polynomial (quadratic)
      px = Polynomial([0, 1])
      py = px + 5
      pz = (3 * px + 5) * 0.5
      p = sq(px) + sq(py) - sq(pz)
      printf("[p = {p}]")
      
      # find (real, positive) roots
      for x in p.quadratic_roots(domain='F', include='+'):
        (y, z) = (py(x), pz(x))
        printf("x={x:g} y={y:g} z={z:g} -> A={A:g} B={B:g}", A=x + y + 5, B=z + 5)
      

      Or we can solve the problem numerically, using the [[ find_value() ]] solver from the enigma.py library:

      Run: [ @replit ]

      from enigma import (find_value, hypot, fdiv, printf)
      
      # A's distance
      A = lambda x: 2 * (x + 5)
      
      # B's distance
      B = lambda x: hypot(x, x + 5) + 5
      
      # find a value where B/A = 3/4
      f = lambda x: fdiv(B(x), A(x))
      r = find_value(f, 0.75, 0, 1000)
      
      x = r.v
      (a, b) = (A(x), B(x))
      printf("B={b:g} A={a:g} -> B/A={f:g}", f=r.fv)
      

      If we had been told that the total distances travelled were whole numbers of metres, then we could look for appropriate right-angled triangles:

      Run: [ @replit ]

      from enigma import (pythagorean_triples, printf)
      
      for (x, y, z) in pythagorean_triples(995, primitive=0):
        if 4 * z + 5 == 3 * (x + y) and y == x + 5:
          printf("A={A} B={B} [x={x} y={y} z={z}]", A=x + y + 5, B=z + 5)
      

      The triangle we are looking for is a (15, 20, 25) triangle = 5× (3, 4, 5).

      Removing the [[ y == x + 5 ]] condition at line 4 allows us to find further (integer) solutions, where the tent is south and east of the starting position, but not on a bearing of 135°.

      Like

  • Unknown's avatar

    Jim Randell 9:11 am on 9 May 2023 Permalink | Reply
    Tags:   

    Teaser 2628: Unnaturally quiet 

    From The Sunday Times, 3rd February 2013 [link] [link]

    I have given each letter of the alphabet a different value from 0 to 25, so some letters represent a single digit and some represent two digits. Therefore, for example, a three-letter word could represent a number of three, four, five or six digits. With my values it turns out that:

    NATURAL = NUMBER.

    What is the sum of the digits in the number MUTE?

    [teaser2628]

     
    • Jim Randell's avatar

      Jim Randell 9:15 am on 9 May 2023 Permalink | Reply

      Presumably the value of a word is the number formed by the concatenation of the digits in the values for each letter.

      So we are looking for assignments that make ATURAL = UMBER (as the value of N is immaterial).

      This Python program looks for assignments of letters to numeric values that make the translated versions of the strings equal. It does this by assigning values to the leading letters that allow the corresponding digits to match.

      It runs in 122ms. (Internal runtime is 66ms).

      Run: [ @replit ]

      from enigma import (irange, subsets, filter2, group, item, update, remove, translate, join, catch, printf)
      
      # replace letters in <X>, <Y> with numeric values from <ns>, so the numeric strings formed are equal
      # <ns> groups values by their first digit
      # return the map: letter -> value
      def _solve(X, Y, ns, d):
        # are we done?
        if X == Y:
          yield d
        elif X and Y:
          # remove any common prefix
          while X and Y and X[0] == Y[0]: (X, Y) = (X[1:], Y[1:])
          if not (X and Y): return
          # split the leading characters into numeric / non-numeric
          (nums, nons) = filter2((lambda x: x.isdigit()), [X[0], Y[0]])
          # different leading numerics = failure
          if len(nums) > 1: return
          # choose values with the same leading digit
          # if there is a numeric value use that, otherwise use the groups
          for k in (nums or ns.keys()):
            ss = ns.get(k, [])
            for vs in subsets(ss, size=len(nons), select='P'):
              # update the strings
              d_ = update(d, nons, vs)
              (X_, Y_) = (translate(x, d_, embed=0) for x in (X, Y))
              ns_ = update(ns, [(k, remove(ss, *vs))])
              # and solve for the translated strings
              yield from _solve(X_, Y_, ns_, d_)
      
      # replace letters in <X>, <Y>
      def solve(X, Y, ns):
        # group numeric values (as strings) by their leading digit
        ns = group(map(str, ns), by=item(0), fn=set)
        # and call the solver
        return _solve(X, Y, ns, dict())
      
      # possible values
      ns = irange(0, 25)
      
      # word values we are interested in
      (w1, w2, w3) = ("NATURAL", "NUMBER", "MUTE")
      
      # turn a word into a string
      fmt = lambda w, sep=':': join((d.get(x, x) for x in w), sep=sep)
      # calculate digit sum
      dsum = lambda w: sum(map(int, fmt(w, sep='')))
      
      # solve the puzzle
      for d in solve(w1, w2, ns):
        # sum the digits in w3
        s = catch(dsum, w3)
        if s is not None:
          # output solution
          printf("dsum({w3}) = {s} [{w1}={f1} {w2}={f2} {w3}={f3}]", f1=fmt(w1), f2=fmt(w2), f3=fmt(w3))
      

      Solution: The sum of the digits in MUTE is 12.

      M and U are 12 and 21, in some order. And T and E are 11 and 22, in some order.

      So MUTE is either “12:21:11:22” or “21:12:22:11”. Either way there are four copies of each of the digits “1” and “2”.

      There are 8 ways to assign the letters in ATURAL and UMBER (and N can be any of the remaining values).

      The assignments are of the form:

      NATURAL = N:x:yy:xy:z:x:xz
      NUMBER = N:xy:yx:yz:xx:z

      where: (x, y) are (1, 2), in some order; and z ∈ {0, 3, 4, 5}.


      As noted by Brian Gladman [link], it is more efficient if the words are considered starting at the end, rather than the beginning.

      I think this is because there are many values with leading characters of “1” and “2”, but when values are grouped by their final character they form into much smaller sets (max size = 3).

      The internal runtime of my code is about twice as fast if the words and values are simply reversed (and the answer is the same). But it is straightforward to adapt the program to process the strings from the end.

      This program runs in 90ms. (Internal runtime is 33ms). [@replit].

      from enigma import (irange, subsets, filter2, group, item, update, remove, translate, join, catch, printf)
      
      # replace letters in <X>, <Y> with numeric values from <ns>, so the numeric strings formed are equal
      # <ns> groups values by their final digit
      # return the map: letter -> value
      def _solve(X, Y, ns, d=dict()):
        # are we done?
        if X == Y:
          yield d
        elif X and Y:
          # remove any common suffix
          while X and Y and X[-1] == Y[-1]: (X, Y) = (X[:-1], Y[:-1])
          if not (X and Y): return
          # split the final characters into numeric / non-numeric
          (nums, nons) = filter2((lambda x: x.isdigit()), [X[-1], Y[-1]])
          # different final numerics = failure
          if len(nums) > 1: return
          # choose values with the same final digit
          # if there is a numeric value use that, otherwise use the groups
          for k in (nums or ns.keys()):
            ss = ns.get(k, [])
            for vs in subsets(ss, size=len(nons), select='P'):
              # update the strings
              d_ = update(d, nons, vs)
              (X_, Y_) = (translate(x, d_, embed=0) for x in (X, Y))
              ns_ = update(ns, [(k, remove(ss, *vs))])
              # and solve for the translated strings
              yield from _solve(X_, Y_, ns_, d_)
      
      # replace letters in <X>, <Y>
      def solve(X, Y, ns):
        # group numeric values (as strings) by their final digit
        ns = group(map(str, ns), by=item(-1), fn=set)
        # and call the solver
        return _solve(X, Y, ns, dict())
      
      # possible numeric values
      ns = irange(0, 25)
      
      # word values we are interested in
      (w1, w2, w3) = ("NATURAL", "NUMBER", "MUTE")
      
      # turn a word into a string
      fmt = lambda w, sep=':': join((d.get(x, x) for x in w), sep=sep)
      # calculate digit sum
      dsum = lambda w: sum(map(int, fmt(w, sep='')))
      
      # solve the puzzle
      for d in solve(w1, w2, ns):
        # sum the digits in w3
        s = catch(dsum, w3)
        if s is not None:
          # output solution
          printf("dsum({w3}) = {s} [{w1}={f1} {w2}={f2} {w3}={f3}]", f1=fmt(w1), f2=fmt(w2), f3=fmt(w3))
      

      Like

  • Unknown's avatar

    Jim Randell 9:10 am on 2 May 2023 Permalink | Reply
    Tags:   

    Teaser 2624: Is it 2013? 

    From The Sunday Times, 6th January 2013 [link] [link]

    I have in mind a four-digit number. Here are three clues about it:

    (1) Like 2013, it uses 4 consecutive digits in some order;
    (2) It is not divisible by any number from 2 to 11;
    (3) If I were to tell you the _____ digit, then you could work out my number.

    Those clues would have enabled you to work out my number, but unfortunately the printer has left a gap: it should have been the word “units” or “tens” or “hundreds” or “thousands”.

    What is my number?

    [teaser2624]

     
    • Jim Randell's avatar

      Jim Randell 9:13 am on 2 May 2023 Permalink | Reply

      The puzzle works equally well if the missing word is chosen from: “first”, “second”, “third”, “fourth”.

      It is straightforward to consider all possible 4-digit numbers. But we can be a bit cleverer, and there are a few sets of digits we can use, some of which can be discarded immediately, and we can then produce viable permutations from the remaining digits.

      This Python program runs in 52ms. (Internal runtime is 201µs).

      Run: [ @replit ]

      from enigma import (irange, tuples, subsets, nconcat, filter_unique, item, join, printf)
      
      # generate candidate numbers
      def generate():
        # consider possible sets of digits
        for ds in tuples(irange(0, 9), 4):
          # discard any sets where all numbers will be divisible by 3
          if sum(ds) % 3 == 0: continue
          # consider possible permutations
          for (A, B, C, D) in subsets(ds, size=4, select='P'):
            # discard final digit = 0, 2, 4, 6, 8 (divisible by 2) or 5 (divisible by 5)
            if D not in {1, 3, 7, 9}: continue
            # discard if divisible by 7 or 11
            n = nconcat(A, B, C, D)
            if n % 7 == 0 or n % 11 == 0: continue
            # return viable (<candidate>, <digits>)
            yield (A, B, C, D)
      
      # collect candidate numbers
      rs = list(generate())
      
      # consider possible missing digit positions
      for i in irange(0, 3):
        # there must be only one solution
        ss = filter_unique(rs, item(i)).unique
        if len(ss) == 1:
          printf("digit {i} => {n}", i=i + 1, n=join(ss[0]))
      

      Solution: The number is 2143.


      There are 5 candidate numbers where if we were told the digit in a certain position we would be able to work out the number:

      1st digit = 1 ⇒ 1423
      1st digit = 8 ⇒ 8567
      2nd digit = 1 ⇒ 2143
      3rd digit = 1 ⇒ 2413
      3rd digit = 3 ⇒ 4231

      But the puzzle text tells us that if we knew which of the words the printer had left out, we would be able to work out the number. (Without being told the value of the digit in that position).

      The only possibility that lets us work out the number, is if the position the printer omitted is the 2nd digit (“hundreds”) (as either of 1st (“thousands”) or 3rd (“tens”) would not allow us to work out the number).

      So the missing word must be “hundreds”, and the number is 2143.

      Like

    • Frits's avatar

      Frits 1:43 pm on 3 May 2023 Permalink | Reply

         
      from itertools import permutations as perm
      
      # sum(x, x+1, x+2, x+3) = 4.x + 6 is divisible by 3 if x is divisible by 3 
      # dgts = 1-8    (as x cannot be 0 and x+3 cannot be 9)
      mind, maxd = 1, 8
      dgts = ''.join(str(x) for x in range(mind, maxd + 1))
      
      cands = [''.join(p) for s in [x - mind for x in range(mind, maxd - 2) if x % 3]
               for p in perm(dgts[s: s + 4])
               if p[-1] in "137" and all(int(''.join(p)) % i for i in [7, 11])]
               
      # consider possible missing digit positions
      for pos in range(4):
        # check if there is a unique digit that occurs only once in this position
        occ1 = [s[0] for d in dgts if len(s := [c for c in cands if c[pos] == d]) == 1]
        if len(occ1) == 1:
          print("answer:", occ1[0]) 
      

      If we know there is exactly one solution:

         
      from itertools import permutations as perm
      
      # reverse non-empty 2-dimensional matrix (from  MxN to NxM)
      R = lambda mat: [[m[pos] for m in mat] for pos in range(len(mat[0]))]
      
      # sum(x, x+1, x+2, x+3) = 4.x + 6 is divisible by 3 if x is divisible by 3 
      # dgts = 1-8    (as x cannot be 0 and x+3 cannot be 9)
      mind, maxd = 1, 8
      dgts = ''.join(str(x) for x in range(mind, maxd + 1))
      
      cands = [''.join(p) for s in [x - mind for x in range(mind, maxd - 2) if x % 3]
               for p in perm(dgts[s: s + 4])
               if p[-1] in "137" and all(int(''.join(p)) % i for i in [7, 11])]
               
      # check reversed candidates (from  MxN to NxM)
      ind = [s[0] for r in R(cands) 
             if len(s := [r.index(d) for d in dgts if r.count(d) == 1]) == 1][0] 
      print("answer:", cands[ind])
      

      Like

  • Unknown's avatar

    Jim Randell 9:36 am on 27 April 2023 Permalink | Reply
    Tags:   

    Teaser 2626: Homophones 

    From The Sunday Times, 20th January 2013 [link] [link]

    Wo, who lives in Woe, has been studying homophones (i.e. sets of words that sound the same, but have different meanings). He has now extended his studies to sets of numbers that have a common property.

    He wrote down three numbers which were prime, and a further two numbers whose different digits formed a consecutive set. Having consistently replaced different digits by different letters, this made his primes into:

    TWO
    TOO
    TO

    and his numbers using consecutive digits into:

    STRAIGHT
    STRAIT

    What is the three-figure number WOE?

    This puzzle appears in the book The Sunday Times Brain Teasers Book 2 (2020) under the title “Our secret”.

    [teaser2626]

     
    • Jim Randell's avatar

      Jim Randell 9:36 am on 27 April 2023 Permalink | Reply

      I used the [[ SubstitutedExpression ]] solver from the enigma.py library to solve this puzzle.

      The following run file executes in 79ms. (Internal runtime is 10.4ms).

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      # primes
      "is_prime(TWO)"
      "is_prime(TOO)"
      "is_prime(TO)"
      
      # check for a consecutive set of digits
      --code="check = lambda ds: max(ds) - min(ds) == len(ds) - 1"
      "check({S, T, R, A, I, G, H, T})"
      "check({S, T, R, A, I, T})"
      
      # no leading zeros
      --invalid="0,TSW"
      
      --answer="WOE"
      --template="(TWO TOO TO) (STRAIGHT STRAIT) (WOE)"
      --solution=""
      

      Solution: WOE = 798.

      There are 84 possible solutions, although the following words have the same value in all of them:

      TO = 19
      TOO = 199
      TWO = 179
      WOE = 798

      Note that STRAIGHT and STRAIT are not required to be prime, but there are only 7 solutions where they are:

      STRAIGHT = 41203651, STRAIT = 412031
      STRAIGHT = 31402561, STRAIT = 314021
      STRAIGHT = 41325601, STRAIT = 413251
      STRAIGHT = 21435061, STRAIT = 214351
      STRAIGHT = 31245601, STRAIT = 312451
      STRAIGHT = 31245061, STRAIT = 312451
      STRAIGHT = 41352061, STRAIT = 413521

      Like

    • Frits's avatar

      Frits 1:57 pm on 27 April 2023 Permalink | Reply

         
      #! python3 -m enigma -r
       
      SubstitutedExpression
       
      # primes
      "is_prime(TWO)"
      "is_prime(TOO)"
      "is_prime(TO)"
      
      #  credit: Brian Gladman [ https://brg.me.uk/?page_id=485 ]
      
      # since the digits for letters other than WOE are consecutive,
      # we must have one of the following four situations:
      #
      # digit sequence  (W, O, E) values (in some order with W <> 0)
      #      0..6       (7, 8, 9)
      #      1..7       (0, 8, 9)
      #      2..8       (0, 1, 9)
      #      3..9       (0, 1, 2)
       
      # check for a consecutive set of digits in remaining digits
      "''.join(sorted(str(WOE))) in {'012', '019', '089', '789'}", 
      
      # no leading zeros
      --invalid="0,OTW"
      --invalid="3-6,WOE"
      --invalid="2|8,O"
       
      --answer="WOE"
      --template="(TWO TOO TO) (WOE)"
      --solution=""
      

      Like

  • Unknown's avatar

    Jim Randell 10:25 am on 25 April 2023 Permalink | Reply
    Tags: ,   

    Teaser 2627: Inversion 

    From The Sunday Times, 27th January 2013 [link] [link]

    In the woodwork class Pat cut a triangle from a sheet of plywood, leaving a hole in the sheet. The sides of the triangle were consecutive whole numbers of centimetres long. He then wanted to turn the triangle over and fit it back into the hole. To achieve this he had to cut the triangle into three pieces. He did this with two straight cuts, each cut starting at a midpoint of a side. Then each of the three pieces (two of which had the same perimeter) could be turned over and placed back in exactly its starting position in the hole.

    What are the lengths of the sides of the original triangle?

    [teaser2627]

     
    • Jim Randell's avatar

      Jim Randell 10:26 am on 25 April 2023 Permalink | Reply

      I imagined the wood was painted black on one side, and the goal is then to cut the triangle into 3 shapes, such that each shape will fit back into its original hole when turned over, to produce a completely black triangle of the same shape as the original.

      My first thought, when looking for a triangle with sides that are consecutive whole numbers, was to look at a (3, 4, 5) triangle, as these often crop up in these puzzles.

      Making two cuts between midpoints of sides divides the triangle into 3 shapes as shown:

      The quadrilateral Q, can be turned over to fit back into its vacated hole, but the triangles P and R do not, as they are not isosceles (but they are identical, and so have the same perimeter).

      But we can make these triangles isosceles, but in doing this the quadrilateral shrinks to zero size (and our two cuts become one):

      But what if we make the initial triangle such that it can be divided into two smaller isosceles triangles and a quadrilateral that can be turned over?

      We end up with something like this:

      And we can make Q and R have the same perimeter by making the base of R have length 2a.

      Now, the lengths {2a, 2b, 2(a + c)} must be consecutive integers in some order.

      So either:

      b = a + 1; c = 1/2
      b = a + 1/2; c = 1

      We can calculate the height h of triangles P and R to get:

      h² = a² − c² = b² − a²
      ⇒ 2a² − b² − c² = 0

      Substituting the possible values for b and c in terms of a, gives us quadratic equations (in a), which we can solve for viable solutions.

      This Python program runs in 59ms. (Internal runtime is 241µs).

      Run: [ @replit ]

      from enigma import (enigma, Polynomial, sq, printf)
      
      Q = enigma.Rational()
      as_int = lambda x: enigma.as_int(x, include="+", default=None)
      
      # consider values for c
      for c in (Q(1, 2), 1):
        # form the polynomial
        b = Polynomial([Q(3, 2) - c, 1])
        p = Polynomial([-sq(c), 0, 2]) - sq(b)
        # find positive rational roots for a
        for a in p.quadratic_roots(domain='Q', include="+", F=Q):
          # find integer sides
          sides = list(map(as_int, [2 * a, 2 * b(a), 2 * (a + c)]))
          if all(sides):
            # output solution
            printf("sides = {sides} [c={c} p={p} -> a={a}]", sides=sorted(sides))
      

      Solution: The sides of the original triangle were 5cm, 6cm, 7cm.


      The polynomials we form are:

      a² − 2a − 5/4 = 0
      ⇒ (2a + 1)(2a − 5) = 0
      ⇒ a = 5/2, b = 7/2, c = 1/2

      a² − a − 5/4 = 0
      ⇒ no rational roots

      The second polynomial does have a real root at: a = (1 + √6) / 2.

      Which gives a triangle with sides of (3.449…, 4.449…, 5.449…).

      So although the puzzle does not work with a (3, 4, 5) triangle, it does work with a (3, 4, 5) + (√6 − 2) triangle.

      Like

  • Unknown's avatar

    Jim Randell 10:07 am on 20 April 2023 Permalink | Reply
    Tags:   

    Teaser 2696: Digital display 

    From The Sunday Times, 25th May 2014 [link] [link]

    George and Martha have a seven-figure telephone number consisting of seven different digits in decreasing order. Martha commented that the seven digits added together gave a total number that did not use any of the digits from the telephone number.

    George agreed and added that their telephone number was in fact a multiple of that total number.

    What is their telephone number?

    This completes the archive of Teaser puzzles originally published in The Sunday Times in 2014.

    There are now 856 Teaser puzzles available on the site, including a complete archive of puzzles from December 2013 to the most recent puzzle (April 2023).

    [teaser2696]

     
    • Jim Randell's avatar

      Jim Randell 10:07 am on 20 April 2023 Permalink | Reply

      This Python program runs in 52ms. (Internal runtime is 320µs).

      Run: [ @replit ]

      from enigma import (irange, subsets, nsplit, nconcat, printf)
      
      # choose 7 digits in decreasing order
      for ds in subsets(irange(9, 0, step=-1), size=7):
        # the sum of the digits does not use any of the digits
        dsum = sum(ds)
        if any(d in ds for d in nsplit(dsum)): continue
      
        # compute the telephone number
        n = nconcat(ds)
        if n % dsum != 0: continue
      
        # output solution(s)
        printf("number = {n} [dsum = {dsum}]")
      

      Solution: Their telephone number is: 8654310.

      The digit sum is 27 and:

      8654310 = 320530 × 27

      Like

      • Jim Randell's avatar

        Jim Randell 1:39 pm on 10 May 2023 Permalink | Reply

        Here is a version that uses the [[ SubstitutedExpression ]] solver from the enigma.py library, using the newly implemented macro functionality to tidy things up a bit.

        Run: [ @replit ]

        #! python3 -m enigma -rr
        
        SubstitutedExpression
        
        # suppose the 3 unused digits are X, Y, Z
        "X < Y" "Y < Z"
        
        # digit sum of the remaining digits is: 45 - (X + Y + Z)
        --macro="@ds = (45 - X - Y - Z)"
        # and must be composed entirely of digits in {X, Y, Z}
        --macro="@xs = {X, Y, Z}"
        "@xs.issuperset(nsplit(@ds))"
        
        # phone number is a multiple of ds
        --code="number = lambda xs: nconcat(diff(irange(9, 0, step=-1), xs))"
        "number(@xs) % @ds == 0"
        
        --answer="number(@xs)"
        --template=""
        

        Like

    • Frits's avatar

      Frits 11:11 am on 20 April 2023 Permalink | Reply

         
      from enigma import SubstitutedExpression
      
      vars = "ABCDEFG"
      d2i = {d: (vars[:6 - d] if d < 6 else "") + (vars[3 - d:] if d > 3 else "")
                for d in range(0, 10)} 
      
      # the alphametic puzzle
      p = SubstitutedExpression(
        [
          "A > B",
          "B > C",
          "C > D",
          "D > E",
          "E > F",
          "F > G",
          "A + B + C + D + E + F + G = HI",
          "div(ABCDEFG, HI)",
          
        ],
        answer="ABCDEFG",
        d2i=d2i,
        reorder=0,
        verbose=0,    # use 256 to see the generated code
      )
      
      # print answers
      for (_, ans) in p.solve():
        print(f"{ans}")
      

      Like

      • Jim Randell's avatar

        Jim Randell 2:54 pm on 20 April 2023 Permalink | Reply

        @Frits: I think I would allow H and I to have the same value.

        You can allow this with the following parameter to [[ SubstitutedExpression ]]:

        distinct=["ABCDEFGH", "ABCDEFGI"]
        

        Like

        • Frits's avatar

          Frits 5:12 pm on 20 April 2023 Permalink | Reply

          @Jim, I erronuously thought that the total number also had to have different digits

          Like

        • Frits's avatar

          Frits 5:15 pm on 20 April 2023 Permalink | Reply

             
          dgts = "0123456789"
          
          # decompose <t> into <k> numbers from <ns>
          # return number with decreasing digits
          def decompose(t, k, ns, s=[]):
            if k == 1:
              if t in ns:
                yield int("".join(str(x) for x in (s + [t])[::-1]))
            else:
              for (i, n) in enumerate(ns):
                if t - n < 0: continue
                yield from decompose(t - n, k - 1, ns[i + 1:], s + [n])
          
          # try each possible total number
          for tot_nr in range(sum(range(7)), sum(range(3, 10)) + 1):
            # remaining digits
            rd = [int(x) for x in dgts if x not in str(tot_nr)]
           
            # total number must lie between sum 7 smallest and sum 7 highest digits 
            if not (sum(x for x in rd[:7]) <= tot_nr <= sum(x for x in rd[-7:])):
              continue
            
            pr(tot_nr, rd, sum(x for x in rd[-7:]))
            
            # pick 7 digits with sum to <tot_nr>
            for y in decompose(tot_nr, 7, rd):
              if y % tot_nr == 0:
                print("answer:", y)
          

          Like

    • GeoffR's avatar

      GeoffR 9:14 pm on 20 April 2023 Permalink | Reply

      % A Solution in MiniZinc
      include "globals.mzn";
      
      var 1..9:A; var 0..9:B; var 0..9:C; var 0..9:D;
      var 0..9:E; var 0..9:F; var 0..9:G;
      var 1..9:H; var 0..9:I;
      
      % 7 digits of the telephone number are in descending order
      constraint A > B /\ B > C /\ C > D /\ D > E /\ E > F /\ F > G;
      
      % Telephone number is ABCDEFG
      var 6543210..9876543: ABCDEFG == A * pow(10,6) + B * pow(10,5) + C * pow(10,4)
      + D * pow(10,3) + E *  pow(10,2) + F * pow(10,1) + G;
      
      % Sum of digits in Telephone Number
      var 21..42: digsum = A + B + C + D + E + F + G;
      
      constraint H == digsum div 10;  
      constraint I == digsum mod 10;
      
      % Divisor of telephone number uses digits H and I
      constraint ABCDEFG div digsum > 0 /\  ABCDEFG mod digsum == 0;
      
      % Digits H and I could be the same or different
      constraint card({A, B, C, D, E, F, G, H}) == 8 
      /\ card({A, B, C, D, E, F, G, I}) == 8;  
      
      solve satisfy;
      
      output["Tel. Num. = " ++ show(ABCDEFG) ++ ", Digit sum = " ++ show(digsum)];
      
      % Tel. Num. = 8654310, Digit sum = 27
      % ----------
      % ==========
      
      
      
      
      

      Like

  • Unknown's avatar

    Jim Randell 9:15 am on 18 April 2023 Permalink | Reply
    Tags:   

    Teaser 2675: Should old acquaintance … 

    From The Sunday Times, 29th December 2013 [link] [link]

    I have given each letter of the alphabet a different whole number value from 1 to 26 (for instance, A has the value 5 and B has the value 24). In this way I can work out the value of any word by adding up the values of its letters. Looking at the values of the words:

    SHOULD
    OLD
    ACQUAINTANCE

    I find that the value of OLD is the average of the other two values.

    I also find that NEW and YEAR have the same value as each other – and it is like the value of OLD but with its two digits in the reverse order. It is possible from all this information to work out the values of N, E and W, but all we want to know is…

    … what is the value of NEW?

    I replaced “for example” in the original puzzle text by “for instance” to make it clear these are the actual values that are being given. In the book The Sunday Times Brain Teasers 2 (2020), the values are simply given as “(eg, A=5 and B=24)”, which I think is more ambiguous than the original text.

    [teaser2675]

     
    • Jim Randell's avatar

      Jim Randell 9:16 am on 18 April 2023 Permalink | Reply

      From:

      (SHOULD + ACQUAINTANCE) / 2 = OLD

      we determine:

      SHU + ACQUAINTANCE = OLD
      ⇒ 3A + 2CNU + CEHIQST = OLD

      And A = 5, so the smallest possible value for OLD is: 3×5 + 2×(1 + 2 + 3) + (4 + 6 + 7 + 8 + 9 + 10) = 71.

      And this is enough to give us a way to attack the problem programatically.

      This Python program runs in 81ms. (Internal runtime is 23.5ms).

      Run: [ @replit ]

      from enigma import (irange, subsets, group, diff, nreverse, cproduct, intersect, singleton, printf)
      
      # given values
      A = 5
      B = 24
      
      # collect sums of 3 digit values for OLD, NEW, YER
      g = group(subsets(diff(irange(1, 26), {A, B}), size=3), by=sum)
      
      # find 2-digit keys with whose reverse is also a key
      for OLD in g.keys():
        if OLD < 71: continue
        NEW = nreverse(OLD)
        if NEW < 10 or NEW == OLD or NEW not in g: continue
        YER = NEW - A
        if YER not in g: continue
      
        # NEW and YER must have E in common (but nothing else)
        for (vNEW, vYER) in cproduct([g[NEW], g[YER]]):
          E = singleton(intersect([vNEW, vYER]))
          if E is None: continue
          # OLD has no letters in common with them
          vs = vNEW + vYER
          for vOLD in g[OLD]:
            if intersect([vOLD, vs]): continue
      
            # find a value for N (which also gives W)
            for N in diff(vNEW, {E}):
              # calculate remaining amount for 2CU + HIQST
              r = OLD - (3*A + E + 2*N)
              ns = diff(irange(1, 26), {A, B}, vs, vOLD)
              # can we make this from the remaining values?
              if r < sum(ns[0:2]) + sum(ns[0:7]): continue
      
              # output solution
              printf("OLD={OLD} NEW={NEW} YER={YER} -> E={E} NEW={vNEW} YER={vYER} OLD={vOLD} -> N={N} {ns}", ns=ns[0:7])
      

      Solution: NEW has a value of 37.

      And we can determine: N = 3, E = 11, W = 23

      OLD has a value of 73, so the individual letters have values 22, 25, 26 (in some order).

      And Y and R are 9 and 12 (in some order).

      To complete the given letters, U and C can be 1 and 2 (in some order).

      And H, I, Q, S, T can be 4, 6, 7, 8, 10 (in some order).

      Like

    • GeoffR's avatar

      GeoffR 1:56 pm on 18 April 2023 Permalink | Reply

      The Chuffed Solver took about 1 sec to run, but the Geocode Solver took about 40 sec.

      % A Solution in MiniZinc
      include "globals.mzn";
      
      var 1..26:A; var 1..26:C; var 1..26:E; var 1..26:D; var 1..26:H; 
      var 1..26:I; var 1..26:L; var 1..26:N; var 1..26:O; var 1..26:Q;
      var 1..26:S; var 1..26:T; var 1..26:U; var 1..26:Y; var 1..26:R;
      var 1..26:W; var 1..26:B;
      
      constraint A == 5 /\ B == 24; % given in teaser
      
      constraint all_different ([A,C,E,D,H,I,L,N,O,Q,S,T,U,Y,R,W,B]);
      
      % OLD is the average of SHOULD and ACQUAINTANCE
      constraint 2 * (O + L + D) == (S + H + O + U + L + D) 
      + (A + C + Q + U + A + I + N + T + A + N + C + E);
      
      % NEW and YEAR have the same value
      constraint (N + E + W) == (Y + E + A + R);
      
      % NEW is similar OLD, but with its two digits in the reverse order
      var 10..99:NEW == N + E + W;
      var 10..99:OLD == O + L + D;
      var 1..9:a; var 1..9:b;
      var 1..9:c; var 1..9:d;
      
      constraint a == NEW mod 10 /\ b == NEW div 10;
      constraint c == OLD mod 10 /\ d == OLD div 10;
      constraint a == d /\ b == c;
      
      solve satisfy;
      
      output ["[N, E, W] = " ++ show([N, E, W]) 
      ++ "\n" ++ "NEW = " ++ show(NEW)
      ++ "\n" ++ "OLD = " ++ show(OLD)
      ];
      
      % [N, E, W] = [3, 11, 23]
      % NEW = 37
      % OLD = 73
      
      
      

      Like

    • Frits's avatar

      Frits 1:05 pm on 19 April 2023 Permalink | Reply

      Not as fast as my version of Brian’s program.

      [https://brg.me.uk/?page_id=1756#comment-3740]

       
      from enigma import SubstitutedExpression
      
      # invalid digit / symbol assignments
      d2i = dict()
      for d in range(0, 27):
        vs = set()
        if d == 0: vs.update('ABCDEHILNOQRSTUWY')
        # 71 <= OLD <= 74
        # 3A + 2CNU + EHIQST = OLD
        # 3×5 + 2×(1 + 2 + 3) + (4 + 6 + 7 + 8 + 9 + x) <= 74 
        # so x <= 13
        if d > 13: vs.update('NECUHIQST')
        
        if d < 20: vs.update('OLD')
        if d < 21: vs.update('LD')
        if d < 22: vs.update('D')
        if d > 24: vs.update('O')
        if d > 25: vs.update('OL')
      
        d2i[d] = vs
      
      # the alphametic puzzle
      p = SubstitutedExpression(
        [
          # order not important
          "O < L < D",      # order not important
          # NEW is like the value of OLD with digits in the reverse order
          "(10 * ((old := O + L + D) % 10) + old // 10) - N - E = W",
          
          "C < U",          # order not important
          
          # 3×5 + 2×(C + N + U) + (1 + 2 + 3 + 4 + 6 + 7) <= 74 
          # thus the maximum of C + N + U is 18 
          "(cnu := C + N + U) <= 18",
          
          # 3A + 2CNU + EHIQST = OLD, determine value of HIQST 
          # check if remaining numbers are too high for making target HIQST
          "(hiqst := old - 3 * A - 2 * cnu - E) >= 16",
          
          "hiqst >= sum(sorted(set(range(1, 14)) - {A, C, E, N, U})[:5])",
          
          "H < I < Q < T",  # order not important
          
          # OLD is the average of the values SHOULD and ACQUAINTANCE 
          "old - (H+U + A+C+Q+U+A+I+N+T+A+N+C+E) = S",
          "Q < S < T",     # order not important
          
          #  NEW and YEAR have the same value
          "N + W - Y - A = R",
          "R < Y",         # order not important
          
        ],
        answer="N + E + W",
        base=27,
        d2i=d2i,
        s2d=dict(A=5, B=24),
        reorder=0,
        verbose=0,    # use 256 to see the generated code
      )
      
      # print answers
      for (_, ans) in p.solve():
        print(f"{ans}")  
      

      Like

  • Unknown's avatar

    Jim Randell 9:14 am on 13 April 2023 Permalink | Reply
    Tags:   

    Teaser 2697: Gimme five! 

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

    In the list of words below each different letter consistently represents a different digit. Each of the coded numbers has the property that the sum of its digits is divisible by (but not equal to) the number it spells out:

    THREE
    FIVE
    EIGHT
    NINE
    TEN
    ELEVEN
    THIRTEEN

    What number does FIVE represent?

    [teaser2697]

     
    • Jim Randell's avatar

      Jim Randell 9:15 am on 13 April 2023 Permalink | Reply

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

      The following run file executes in 70ms. (Internal runtime of the generated program is 3.0ms).

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      "div(T + H + R + E + E, 3) > 1"
      "div(F + I + V + E, 5) > 1"
      "div(E + I + G + H + T, 8) > 1"
      "div(N + I + N + E, 9) > 1"
      "div(T + E + N, 10) > 1"
      "div(E + L + E + V + E + N, 11) > 1"
      "div(T + H + I + R + T + E + E + N, 13) > 1"
      
      --template="THREE FIVE EIGHT NINE TEN ELEVEN THIRTEEN"
      --answer="FIVE"
      

      Solution: FIVE = 8237.

      We have:

      THREE = 45177
      FIVE = 8237
      EIGHT = 72654
      NINE = 9297
      TEN = 479
      ELEVEN = 707379
      THIRTEEN = 45214779

      Like

    • GeoffR's avatar

      GeoffR 10:24 am on 14 April 2023 Permalink | Reply

      % A Solution in MiniZinc
      include "globals.mzn";
      
      % Leading digits of numbers
      var 1..9:T; var 1..9:F; var 1..9:E; var 1..9:N;
      % Other digits in numbers
      var 0..9:I; var 0..9:H; var 0..9:R;
      var 0..9:G; var 0..9:V; var 0..9:L;
       
      constraint all_different([T, F, E, N, I, H, R, G, V, L]);
      
      % For each number, sum of its digits is divisible by (but not equal to) the number it spells out
      constraint (T + E + N) mod 10 == 0 /\ (T + E + N) div 10 > 1;
      constraint (N + I + N + E) mod 9 == 0 /\ (N + I + N + E) div 9 > 1;
      constraint (T + H + R + E + E) mod 3 == 0 /\ (T + H + R + E + E) div 3 > 1;
      constraint (F + I + V + E) mod 5 == 0 /\ (F + I + V + E) div 5 > 1;
      constraint (E + I + G + H + T) mod 8 == 0 /\ (E + I + G + H + T) div 8 > 1;
      constraint (E + L + E + V + E + N) mod 11 == 0 /\ (E + L + E + V + E + N) div 11 > 1;
      constraint (T + H + I + R + T + E + E + N) mod 13 == 0 /\ (T + H + I + R + T + E + E + N) div 13 > 1;
      
      solve satisfy;
      
      % Digits of numbers in teaser
      output ["[T,H,R,E,E ] = " ++ show([T,H,R,E,E ] )
      ++ "\n" ++ "[F,I,V,E] = " ++ show ([F,I,V,E] )
      ++ "\n" ++ "[E,I,G,H,T] = " ++ show([E,I,G,H,T] )
      ++"\n" ++ "[N,I,N,E] = " ++ show([N,I,N,E])
      ++"\n" ++ "[T,E,N] = " ++ show ([T,E,N] )
      ++ "\n" ++ "[E,L,E,V,E,N] = " ++ show ([E,L,E,V,E,N] )
      ++ "\n" ++ "[T,H,I,R,T,E,E,N] = " ++ show ([T,H,I,R,T,E,E,N] )
      ];
      
      % [T,H,R,E,E ] = [4, 5, 1, 7, 7]
      % [F,I,V,E] = [8, 2, 3, 7]   - so FIVE = 8237 (answer)
      % [E,I,G,H,T] = [7, 2, 6, 5, 4]
      % [N,I,N,E] = [9, 2, 9, 7]
      % [T,E,N] = [4, 7, 9]
      % [E,L,E,V,E,N] = [7, 0, 7, 3, 7, 9]
      % [T,H,I,R,T,E,E,N] = [4, 5, 2, 1, 4, 7, 7, 9]
      % ----------
      % ==========
      % Finished in 180msec
      
      
      
      

      Like

  • Unknown's avatar

    Jim Randell 9:41 am on 11 April 2023 Permalink | Reply
    Tags:   

    Teaser 2703: Bastille Day 

    From The Sunday Times, 13th July 2014 [link] [link]

    Three towns in France issue their own car licence plates. Argentan’s numbers consist of the digits 1 to 9 in some order, Beaurepaire’s consist of the digits 1 to 8 in some order, and Corbigny’s consist of the digits 1 to 7 in some order. All three towns only use numbers divisible by 11 and all such possible plates have been issued. Tomorrow is Bastille Day and, in order to minimise chaos, only cars with licence numbers divisible by 5 will be allowed in.

    In the form “one in …”

    (a) What proportion of Argentan’s cars will be allowed into Argentan?

    (b) What proportion of Beaurepaire’s cars will be allowed into Beaurepaire?

    (c) What proportion of Corbigny’s cars will be allowed into Corbigny?

    [teaser2703]

     
    • Jim Randell's avatar

      Jim Randell 9:42 am on 11 April 2023 Permalink | Reply

      I assumed each valid number plate is a rearrangement of all allowable digits (each appearing exactly once).

      This Python program constructs the valid plates, and counts those divisible by 5. It runs in 189ms.

      Run: [ @replit ]

      from enigma import (irange, subsets, nconcat, fraction, printf)
      
      # solve the puzzle
      qs = dict(A=irange(1, 9), B=irange(1, 8), C=irange(1, 7))
      for k in sorted(qs.keys()):
        ds = qs[k]
        # count plates divisible by 11 (t) and those divisible by 5 (n)
        t = n = 0
        for s in subsets(ds, size=len, select='P'):
          p = nconcat(s)
          if p % 11 == 0:
            t += 1
            if p % 5 == 0:
              n += 1
        # find the fraction
        (a, b) = fraction(n, t)
        # output solution
        printf("{k}: {t} plates, {n} divisible by 5 = {a}/{b}")
      

      Solution: (a) 1 in 11; (b) 1 in 8; (c) 1 in 8.

      The counts for total number of plates, and permitted plates are:

      A: total = 31680, permitted = 2880 (1 in 11)
      B: total = 4608, permitted = 576 (1 in 8)
      C: total = 576, permitted = 72 (1 in 8)

      Like

  • Unknown's avatar

    Jim Randell 9:26 am on 6 April 2023 Permalink | Reply
    Tags:   

    Teaser 2691: Hot × buns 

    From The Sunday Times, 20th April 2014 [link] [link]

    I have consistently replaced each of the digits 0 to 9 by a different letter to turn some numbers into words. It turns out that the number EASTER is exactly divisible by each of:

    (a) the number of days in a week;
    (b) the number of days in a year;
    (c) the seasonal product HOT × BUNS.

    What numbers do HOT and BUNS represent?

    This puzzle is included in the book The Sunday Times Brain Teasers 2 (2020), however the “×” sign is missing in condition (c).

    [teaser2691]

     
    • Jim Randell's avatar

      Jim Randell 9:26 am on 6 April 2023 Permalink | Reply

      I assume there are (a) 7 days a week; and (b) 365 days a year.

      We can solve the puzzle using the [[ SubstitutedExpression ]] solver from the enigma.py library.

      The following run file executes in 74ms. (Internal runtime of the generated program is 11ms).

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      "EASTER % 7 = 0"
      "EASTER % 365 = 0"
      "EASTER % (HOT * BUNS) = 0"
      

      Solution: HOT = 147; BUNS = 3285.

      And this is the only solution to the alphametic expression: EASTER % (HOT * BUNS) = 0.

      So it seems conditions (a) and (b) are just there to make things a bit easier.

      Like

    • GeoffR's avatar

      GeoffR 5:38 pm on 6 April 2023 Permalink | Reply

      from itertools import permutations
      
      for H, O, T in permutations ('1234567890', 3):
          if H == '0': continue
          HOT = int(H + O + T)
          # Find remaining seven digits
          q1 = set('1234567890').difference([H, O, T])
          for p2 in permutations(q1):
              E, A, S, R, B, U, N = p2
              if '0' in (E, B):continue
              BUNS = int(B + U + N + S)
              EASTER = int(E + A + S + T + E + R)
              if EASTER % 7 == 0 and EASTER % 365 == 0 \
                 and EASTER % (HOT * BUNS) == 0:
                 print(f"HOT = {HOT}, BUNS = {BUNS}, EASTER = {EASTER}")
          
      # HOT = 147, BUNS = 3285, EASTER = 965790
      

      Like

  • Unknown's avatar

    Jim Randell 9:13 am on 4 April 2023 Permalink | Reply
    Tags:   

    Teaser 2706: Spinners 

    From The Sunday Times, 3rd August 2014 [link] [link]

    I have three “spinners” each with five evenly-spaced numbers on. All the numbers on the spinners are positive whole numbers and the sum of the five numbers on each spinner is the same. The first spinner has numbers 3, 3, 3, 3, 3 and the second has numbers 2, 2, 2, 3, 6. My friend chooses a spinner and I choose one of the remaining pair. Then we each spin to choose a number: sometimes it’s a draw, otherwise the higher number wins – and we repeat this many times. He gets very annoyed because (whichever spinner he chooses) I am always more likely to win.

    What are the five numbers on the third spinner?

    [teaser2706]

     
    • Jim Randell's avatar

      Jim Randell 9:14 am on 4 April 2023 Permalink | Reply

      We have encountered non-transitive games before. (See: Enigma 287, Enigma 1126, Tantalizer 428).

      This Python program runs in 59ms. (Internal runtime is 3.5ms).

      Run: [ @replit ]

      from enigma import (Rational, multiset, decompose, printf)
      
      Q = Rational()
      h = Q(1, 2)
      
      # play X against Y, return chance of X beating Y
      def play(X, Y):
        (X, Y) = map(multiset.from_seq, (X, Y))
        (nX, nY) = (X.size(), Y.size())
        (xs, ys) = (sorted(X.keys()), sorted(Y.keys()))
        # accumulate wins for X
        p = 0
        for x in xs:
          pX = Q(X.count(x), nX)
          for y in ys:
            if not (x > y): break
            pY = Q(Y.count(y), nY)
            p += pX * pY
        return p
      
      # given spinners
      A = (3, 3, 3, 3, 3)
      B = (2, 2, 2, 3, 6)
      # confirm A beats B
      assert play(A, B) > h
      
      # look for a third spinner C such that B beats C and C beats A
      for C in decompose(15, 5, increasing=1, sep=0):
        if play(B, C) > h and play(C, A) > h:
          printf("{C}")
      

      Solution: The numbers on the third spinner are: 1, 1, 4, 4, 5.

      We have:

      A = (3, 3, 3, 3, 3)
      B = (2, 2, 2, 3, 6)
      C = (1, 1, 4, 4, 5)

      A beats B 60% of the time.
      B beats C 52% of the time.
      C beats A 60% of the time.

      Like

  • Unknown's avatar

    Jim Randell 9:14 am on 28 March 2023 Permalink | Reply
    Tags: ,   

    Teaser 2707: Good Times 

    From The Sunday Times, 10th August 2014 [link] [link]

    The Good Times bus company has a route from Timesboro to Teaseboro which passes through six other towns on its direct route. The distances from each town to the next are different whole numbers of miles, the largest of the seven distances being six miles more than the shortest. I worked out the average of those seven distances and my friend worked out the average distance between any two of the eight towns. Our answers used the same two digits but in reverse orders.

    How far is it from Timesboro to Teaseboro?

    [teaser2707]

     
    • Jim Randell's avatar

      Jim Randell 9:15 am on 28 March 2023 Permalink | Reply

      I am assuming all distances are measured along the road.

      The 7 distances between the towns are different whole numbers, where the largest of the values is 6 more than the smallest. So the distances, when ordered numerically, must form a sequence of 7 consecutive integers.

      We will denote them (x − 3, …, x + 3).

      The total of these distances (and the required answer) is: 7x.

      And the mean distance is: x.

      We can choose a 2-digit value for x (the mean of the distances), and then look for an arrangement of the distances that gives the mean pairwise distance to be the reverse of x.

      This Python program runs in 205ms.

      Run: [ @replit ]

      from enigma import (C, irange, multiset, subsets, tuples, printf)
      
      # count the number of pairwise distances between the 8 towns
      n = C(8, 2)
      
      # count the number of times each segment occurs in the total distance
      m = multiset()
      for k in irange(1, 7):
        for s in tuples(irange(7), k):
          m.update(s)
      
      # calculate total distance between pairs in <ss>
      pair_dist = lambda ss: sum(s * m[i] for (i, s) in enumerate(ss))
      
      # consider the digits a, b
      for (a, b) in subsets(irange(1, 9), size=2, select='P'):
        # x is the mean of the segment lengths
        # y is the reverse of x
        (x, y) = (10 * a + b, 10 * b + a)
      
        # choose an order for the segments
        for ss in subsets(irange(x - 3, x + 3), size=7, select='P'):
          if pair_dist(ss) == n * y:
            # total distance is 7x
            printf("d={d} [x={x} y={y} ss={ss}]", d=7 * x)
            break  # we only need one example
      

      Solution: The total distance is 98 miles.

      For example the distances could be: (13, 15, 12, 11, 14, 16, 17) miles, to give a total distance of 98 miles. (But there are other arrangements of these distances which also work).

      The mean distance is 98/7 = 14 miles.

      If we label the distances (a, b, c, d, e, f, g) then there are 28 ways of choosing pairs of distances, and the total of these pairwise distances is given by:

      T = 7(a + g) + 12(b + f) + 15(c + e) + 16d

      (In the program this is determined by lines 6-10).

      So in this case we get: T = 1148, and so the pairwise mean is: 1148/28 = 41 miles.


      And we can use this analysis to produce a faster program.

      If the arrangement of distances is:

      (x + a, x + b, x + c, x + d, x + e, x + f)

      where (a, …, f) are the offsets (−3, …, 3) in some order.

      Then the pairwise mean is given by:

      3x + (7(a + g) + 12(b + f) + 15(c + e) + 16d)/28

      And so we can find arrangements of (a, …, f) that are multiples of 28, and these will give integer pairwise distances.

      We can then look for instances where a 2-digit mean distance (= x) along with a viable set of offsets gives a pairwise mean that is the reverse of x.

      Run: [ @replit ]

      from enigma import (irange, subsets, dot, div, printf)
      
      # find ordering of offsets, that give an integer pairwise mean
      rs = dict()
      for vs in subsets(irange(-3, +3), size=7, select='P'):
        k = div(dot([7, 12, 15, 16, 15, 12, 7], vs), 28)
        if k is not None:
          rs[k] = vs
      
      # consider digit reversed pairs of 2-digit numbers
      for (a, b) in subsets(irange(1, 9), size=2, select='P'):
        # x is the mean of the segment lengths
        # y is the reverse of x
        (x, y) = (10 * a + b, 10 * b + a)
      
        # look for corresponding offsets
        for (k, vs) in rs.items():
          if 3 * x + k == y:
            ss = list(x + v for v in vs)
            printf("d={d} [x={x} y={y} ss={ss}]", d=7 * x)
      

      Like

  • Unknown's avatar

    Jim Randell 10:34 am on 23 March 2023 Permalink | Reply
    Tags:   

    Teaser 2710: Sterling achievement 

    From The Sunday Times, 31st August 2014 [link] [link]

    Some people were asked to donate one pound each to charity. They all used only “silver” coins (5p, 10p, 20p, 50p) to make up their pound and no two of them used the same combination. I also donated a pound using silver coins, but it was inevitable that my combination of coins would be a repeat of one already used. When all the coins were sorted into the different denominations we found that we had a whole number of pounds in each denomination.

    Which coins did I use to make my pound?

    [teaser2710]

     
    • Jim Randell's avatar

      Jim Randell 10:34 am on 23 March 2023 Permalink | Reply

      There are a couple of routines in enigma.py to calculate ways of expressing amounts using specified denominations (e.g. coins or stamps). In this case using the more sophisticated [[ Denominations() ]] algorithm gives a (slightly) faster solution than the simple [[ express() ]] function.

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

      Run: [ @replit ]

      from enigma import (Denominations, ceil, ediv, printf)
      
      # express 100 using denominations of 5, 10, 20, 50
      ds = (5, 10, 20, 50)
      qss = Denominations(ds).express(100)  # or: express(100, ds)
      
      # sum the quantities
      tqs = list(sum(qs) for qs in zip(*qss))
      
      # determine extra coins required to make the total of each
      # denomination a whole number of pounds
      for (d, q) in zip(ds, tqs):
        t = d * q
        x = ceil(t, 100) - t
        n = ediv(x, d)
        printf("{n} x {d}p = {x}p [{q} x {d}p coins = {t}p]")
      

      Solution: 6× 5p; 3× 10p; 2× 20p.

      There are 49 different ways to make 100p using 5p, 10p, 20p, 50p.

      And if we sum them all we get:

      294× 5p = 1470p
      147× 10p = 1470p
      63× 20p = 1260p
      14× 50p = 700p

      The setters donation makes the total of the 5p and 10p coins up £15, and the 20p coins up to £13, and the 50p coins were already at £7. Making a grand total of £50.

      Like

  • Unknown's avatar

    Jim Randell 9:29 am on 21 March 2023 Permalink | Reply
    Tags:   

    Teaser 2701: Flipping ages! 

    From The Sunday Times, 29th June 2014 [link] [link]

    At a recent family party the ages of the ten people present formed an arithmetic progression (i.e. the difference between each person’s age in years and the age of the next oldest person was always the same). My age was like my wife’s but with the digits in reverse order. Likewise, my sister’s age was the reverse of my mother’s, my son’s age was the reverse of my grandson’s, and my daughter’s age was the reverse of my granddaughter’s. Furthermore, the product of my brother’s age and the age of his son equalled the year of birth of one of the people at the party.

    What were the ages of my brother, my sister and myself (in that order)?

    [teaser2701]

     
    • Jim Randell's avatar

      Jim Randell 9:30 am on 21 March 2023 Permalink | Reply

      This Python program chooses the 4 pairs of ages that are the mutually reversed, and then extends this set of 8 numbers to 10 numbers in arithmetic progression (if possible). It then allocates the given relationships between the numbers (it looks for viable parent/child ages where the parent is between 16 and 50 years older than the child).

      Runtime is 282ms.

      Run: [ @replit ]

      from enigma import (irange, subsets, union, tuples, mgcd, call, divisor, div, decompose, diff, printf)
      
      # collect pairs that are the reverse of each other
      pairs = set((10 * a + b, 10 * b + a) for (a, b) in subsets(irange(1, 9), size=2))
      
      # generate arithmetic progressions based on the given numbers
      def generate(ns):
        ns = sorted(ns)
        (a, z) = (ns[0], ns[-1])
        # find potential common differences
        for d in divisor(call(mgcd, (y - x for (x, y) in tuples(ns, 2)))):
          # calculate number of terms
          k = div(z - a, d)
          if k is None or k > 9: continue
          k += 1
          # pad the sequence to bring it to 10 terms
          for (pa, pz) in decompose(10 - k, 2, increasing=0, sep=0, min_v=0):
            # construct the new sequence
            ns = list(irange(a - pa * d, z + pz * d, step=d))
            yield ns
      
      # check for viable parent, child age
      check = lambda parent, child: 15 < parent - child < 51
      
      # choose 4 pairs
      for ps in subsets(pairs, size=4):
        ss = union(ps)
        for ns in generate(ss):
          # the additional ages are brother and nephew
          (nep, bro) = diff(ns, ss)
          if not check(bro, nep): continue
          # and their product is the birth year of one of the party
          year = bro * nep
          x = 2014 - year
          if not { x, x - 1 }.intersection(ns): continue
      
          # choose the pairs
          for ((sis, mum), (gson, son), (gdtr, dtr), other) in subsets(ps, size=4, select="P"):
            # check parent/child relationships
            if not (check(mum, sis) and check(mum, bro)): continue
            if not (check(son, gson) or check(dtr, gson)): continue
            if not (check(son, gdtr) or check(dtr, gdtr)): continue
            # the remaining pair is (me, wife) (in some order)
            for (me, wife) in subsets(other, size=2, select="P"):
              # check parent/child relationships
              if not (check(mum, me) and check(me, son) and check(me, dtr)): continue
              if not (check(wife, son) and check(wife, dtr)): continue
              # output solution
              printf("{ns}")
              printf("-> me={me} wife={wife}")
              printf("-> sis={sis} mum={mum}")
              printf("-> son={son} gson={gson}")
              printf("-> dtr={dtr} gdtr={gdtr}")
              printf("-> bro={bro} nep={nep}; year={year}")
              printf()
      

      Solution: Brother = 60. Sister = 69. Self = 78.

      The arithmetic progression is (15, 24, 33, 42, 51, 60, 69, 78, 87, 96).

      And the pairs are:

      me = 78; wife = 87
      sister = 69; mother = 96
      child = 51; grandchild = 15
      child = 42; grandchild = 24
      brother = 60; nephew = 33

      And the product of the ages of the brother and nephew give 1980. And if the 33 year-old nephew has not yet celebrated his birthday in 2014, his year of birth is 1980.

      But we can’t tell how the child/grandchild pairs correspond to the son/grandson and daughter/granddaughter pairs.

      Like

  • Unknown's avatar

    Jim Randell 8:43 am on 16 March 2023 Permalink | Reply
    Tags:   

    Teaser 2711: Megan’s house 

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

    Megan’s class has been learning about primes and squares. So their teacher challenged each of them to use some of the digits 1 to 9 to write down three two-figure squares and a two-figure prime without using any digit more than once. They all succeeded in doing this and in Megan’s case the left-over digit was her house number. If you knew her house number it would be possible to work out the four two-figure numbers that she wrote down.

    What were they?

    [teaser2711]

     
    • Jim Randell's avatar

      Jim Randell 8:43 am on 16 March 2023 Permalink | Reply

      We can use the [[ SubstitutedExpression() ]] solver from the enigma.py library to find viable allocations of the 9 digits to three 2-digit squares, a 2-digit prime, and the remaining digit. We can then use the [[ filter_unique() ]] function to select those allocations that are unique by the remaining digit.

      This Python program runs in 57ms. (Internal runtime is 4.4ms).

      Run: [ @replit ]

      from enigma import (SubstitutedExpression, irange, filter_unique, item, printf)
      
      # squares: AB, CD, EF; prime: GH
      p = SubstitutedExpression(
        [
          "is_square(AB)", "is_square(CD)", "is_square(EF)", "A < C < E",
          "is_prime(GH)",
        ],
        digits=irange(1, 9),
        answer="(X, AB, CD, EF, GH)"
      )
      
      # if we knew X, we could deduce the 2-digit numbers
      rs = filter_unique(p.answers(verbose=0), item(0)).unique
      
      # output solution
      for (X, sq1, sq2, sq3, pr) in rs:
        printf("X={X} -> squares = ({sq1}, {sq2}, {sq3}), prime = {pr}")
      

      Solution: The four 2-digit numbers were: 16, 25, 49 (squares), and 83 (prime).

      So Megan’s house number is 7.

      Like

    • GeoffR's avatar

      GeoffR 11:56 am on 16 March 2023 Permalink | Reply

      from enigma import is_square, is_prime
      from itertools import permutations
      from collections import defaultdict
      nums = defaultdict(list)
      
      for p1 in permutations('123456789'):
          a, b, c, d, e, f, g, h, i = p1
          ab, cd, ef = int(a + b), int(c + d), int(e + f)
          if ab < cd < ef :
              if is_square(ab) and is_square(cd) and is_square(ef):
                  gh = int(g + h)
                  if is_prime(gh):
                      nums[i] += [(ab, cd, ef, gh)]
                  
      for k, v in nums.items():
          if len(v) == 1:
              print(f"Megan's house number was {k}.")
              print(f"Her four 2-digit numbers were {v}.")
              
      # Megan's house number was 7.
      # Her four 2-digit numbers were [(16, 25, 49, 83)].
      
      
      

      Like

    • GeoffR's avatar

      GeoffR 11:41 am on 17 March 2023 Permalink | Reply

      The basis of this teaser can be seen by printing the python dictionary in this solution, with the house number as the key. All house numbers, except 7, have multiple solutions for the four 2-digit numbers.

      8 [(16, 25, 49, 37), (16, 25, 49, 73), (25, 36, 49, 17), (25, 36, 49, 71)]
      7 [(16, 25, 49, 83)]
      9 [(25, 36, 81, 47), (25, 64, 81, 37), (25, 64, 81, 73)]
      4 [(25, 36, 81, 79), (25, 36, 81, 97)]
      6 [(25, 49, 81, 37), (25, 49, 81, 73)]
      3 [(25, 49, 81, 67), (25, 64, 81, 79), (25, 64, 81, 97)]

      Like

  • Unknown's avatar

    Jim Randell 7:47 am on 14 March 2023 Permalink | Reply
    Tags:   

    Teaser 2713: Very similar triangles 

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

    Two triangles are “similar” if they are the same shape (but not necessarily the same size). I cut out two similar (but not identical) triangles from a piece of A4 paper, all the sides of the triangles being whole numbers of centimetres in length. In fact the two triangles were extra similar in the sense that two of the sides of the first triangle were the same lengths as two of the sides of the second triangle.

    What were the lengths of the sides of the smaller triangle?

    [teaser2713]

     
    • Jim Randell's avatar

      Jim Randell 7:47 am on 14 March 2023 Permalink | Reply

      (See also: Enigma 1198).

      For a triangle with sides (a, b, c) (in order), we must have: a + b > c.

      The triangle is then scaled up to give another similar triangle (f.a, f.b, f.c) (f > 1).

      And two of the sides are the same, which must be: f.a = b, f.b = c.

      Hence:

      f = b/a = c/b
      a.c = b²

      So we can choose a value for b, and then look for a, c sides that multiply to give b².

      This Python program runs in 52ms. (Internal runtime is 288µs).

      Run: [ @replit ]

      from enigma import (irange, hypot, intf, divisors_pairs, div, sqrt, printf)
      
      # area of a triangle
      def area(a, b, c):
        return 0.5 * sqrt((a + b + c) * (a + b - c) * (c + a - b) * (b + c - a))
      
      # dimensions of an A4 sheet (cm)
      (X, Y) = (21.0, 29.7)
      A4 = X * Y
      
      # choose a value for b
      for b in irange(1, intf(hypot(X, Y))):
        # calculate possible a, c values from divisors of b^2
        for (a, c) in divisors_pairs(b, 2):
          if not (a < b < c and a + b > c): continue
          # calculate d
          d = div(b * c, a)
          if d is None: continue
          if area(a, b, c) + area(b, c, d) > A4: continue
          # output solution
          printf("({a}, {b}, {c}) -> ({b}, {c}, {d})")
      

      Solution: The smaller triangle has sides of length 8, 12, 18 cm.

      And the larger triangle has sides of length 12, 18, 27 cm.

      The dimensions of the second triangle are 1.5 times those of the first.

      Here is a diagram of the two triangles, set on a sheet of A4 paper:

      Like

  • Unknown's avatar

    Jim Randell 10:57 am on 9 March 2023 Permalink | Reply
    Tags:   

    Teaser 2712: New tricks 

    From The Sunday Times, 14th September 2014 [link] [link]

    I am currently experiencing the joy of seeing Liam, my first grandchild, developing lots of new tricks. With this in mind I have written down four odd numbers and then consistently replaced different digits by different letters to give:

    SIT
    CRAWL
    WALK
    TALK

    In fact the largest of these numbers is the sum of the other three.

    Find the numerical value of TRICK.

    [teaser2712]

     
    • Jim Randell's avatar

      Jim Randell 10:57 am on 9 March 2023 Permalink | Reply

      We can use the [[ SubstitutedExpression ]] solver from the enigma.py library to solve this puzzle. And as it is a simple addition sum we can use the [[ split_sum() ]] helper function for a faster run time.

      The following run file executes in 64ms. (Internal runtime of the generated program is 117µs).

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      SubstitutedExpression.split_sum
      
      "SIT + WALK + TALK = CRAWL"
      
      # T, K, L are odd
      --invalid="0,CKLSTW"
      --invalid="2|4|6|8,KLT"
      
      --answer="TRICK"
      

      Solution: TRICK = 50619.

      There are two possibilities for the original sum (but both give the same value for TRICK):

      SIT + WALK + TALK = CRAWL
      265 + 4739 + 5739 = 10743
      765 + 4239 + 5239 = 10243

      The values of A and S are 2 and 7 (in some order).

      Like

    • GeoffR's avatar

      GeoffR 1:05 pm on 9 March 2023 Permalink | Reply

      
      from itertools import permutations
      
      # Find numbers WALK and TALK 
      for p1 in permutations('1234567890', 5):
          W, A, L, K, T = p1
          if '0' in (W, T, K):continue
          if int(K) % 2 == 0 or int(T) % 2 == 0: continue
          WALK, TALK = int(W + A + L + K), int(T + A + L + K)
          q1 = set('1234567890').difference(p1)
          
          # Find numbers SIT, CRAWL and TRICK
          for p2 in permutations(q1, 4):
              S, I, C, R = p2
              if '0' in (C, S):continue
              if int(L) % 2 == 0:continue
              SIT = int(S + I + T)
              CRAWL = int(C + R + A + W + L)
              if SIT + WALK + TALK == CRAWL:
                  TRICK = int(T + R + I + C + K)
                  print(f"TRICK = {TRICK}")
                  print(f"{SIT} + {WALK} + {TALK} = {CRAWL}")
      
      # TRICK = 50619
      # 765 + 4239 + 5239 = 10243
      # 265 + 4739 + 5739 = 10743
      
      
      

      Like

    • GeoffR's avatar

      GeoffR 10:08 am on 10 March 2023 Permalink | Reply

      
      % A Solution in MiniZinc
      include "globals.mzn";
      
      var 1..9:S; var 0..9:I; var 1..9:T; var 1..9:W; var 0..9:A; 
      var 1..9:L; var 1..9:K; var 1..9:C; var 0..9:R;
      
      constraint all_different ([S, I, T, W, A, L, K, C, R]);
      constraint T mod 2 == 1 /\ L mod 2 == 1 /\ K mod 2 == 1;
      
      var 100..999: SIT = 100*S + 10*I + T;
      var 1000..9999:WALK = 1000*W + 100*A + 10*L + K;
      var 1000..9999:TALK = 1000*T + 100*A + 10*L + K;
      var 10000..99999:CRAWL = 10000*C + 1000*R + 100*A + 10*W + L;
      var 10000..99999:TRICK = 10000*T + 1000*R + 100*I + 10*C + K;
      
      constraint SIT + WALK + TALK == CRAWL;
      
      solve satisfy;
      output ["TRICK = " ++ show(TRICK) ];
      
      % TRICK = 50619
      % ----------
      % ==========
      
      

      Like

  • Unknown's avatar

    Jim Randell 10:17 am on 7 March 2023 Permalink | Reply
    Tags:   

    Teaser 2714: Group of death 

    From The Sunday Times, 28th September 2014 [link] [link]

    In a Champions League group four teams play each other twice with the top two qualifying for the next stage. If two teams are level on points then their head-to-head results determine their position. In one group, after five games each, the teams in order were A, B, C, D, each with a different number of points. At that stage team A was bound to qualify and teams B and C could still top the group. But the final two games were draws. All games had different results, no game had more than five goals, and team D had the same number of “goals for” as “goals against”. Team A scored fewer goals than any other team whilst D scored more than any other.

    What were the results of the two games B v C?

    [teaser2714]

     
    • Jim Randell's avatar

      Jim Randell 10:18 am on 7 March 2023 Permalink | Reply

      I am assuming the point are allocated as: “3 points for a win, 1 point for a draw”. (Although the puzzle also seems to work if “2 points for a win” is used instead).

      Each team plays their three opponents twice, so there are 12 matches in total.

      There are not more than 5 goals scored in any match, so possible scores are:

      drawn = (0, 0) (1, 1) (2, 2)
      won/list = (1, 0) (2, 0) (3, 0) (4, 0) (5, 0) (2, 1) (3, 1) (4, 1) (3, 2)

      This accounts for all 12 matches, so each score must be used.

      And the final 2 matches are drawn, so there is exactly 1 draw in the first 10 matches.

      Programming a constructive solution for this puzzle is quite involved. I hope that a manual solution is more fun. I used the [[ Football() ]] helper class from the enigma.py library to save on some of the coding, but things are complicated a bit by the fact each pair of teams meet twice, and there is nothing to distinguish the two games, so to avoid duplicated solutions I store the results for the two games in order with the “best” game (from the first team’s point of view) given first.

      This Python program runs in 253ms.

      Run: [ @replit ]

      from enigma import (Football, subsets, partitions, update, compare, ordered, rev, join, map2str, printf)
      
      football = Football(points=dict(w=3, d=1))
      
      # possible scores, no game had more than 5 goals
      scores = dict()
      scores['d'] = [(0, 0), (1, 1), (2, 2)]
      scores['w'] = [(1, 0), (2, 0), (2, 1), (3, 0), (3, 1), (3, 2), (4, 0), (4, 1), (5, 0)]
      scores['l'] = list(map(rev, scores['w']))
      
      # label the matches (each label corresponds to 2 matches)
      teams = "ABCD"
      matches = list(x + y for (x, y) in subsets(teams, size=2))
      
      # complete possible match pairs (additional win/lose outcomes)
      pairs = {
        '-': ['ww', 'wl', 'll'],
        'x': ['wx', 'lx'],
        'd': ['wd', 'dl'],
      }
      
      # swap pairs into canonical order
      _pair = { 'dw': 'wd', 'lw': 'wl', 'ld': 'dl' }
      pair = lambda x: _pair.get(x, x)
      
      # allocate win/lose outcomes to matches
      def allocate_matches(ks, ms):
        # are we done?
        if not ks:
          yield ms
        else:
          k = ks[0]
          v = ms.get(k, '-')
          if len(v) > 1:
            # skip this key
            yield from allocate_matches(ks[1:], ms)
          else:
            # allocate both outcomes
            for x in pairs[v]:
              yield from allocate_matches(ks[1:], update(ms, [(k, x)]))
      
      # allocate scores to matches
      def allocate_scores(ks, ms, used=set(), ss=dict()):
        # are we done?
        if not ks:
          yield (ss, used)
        else:
          k = ks[0]
          if k in ss:
            # skip this key
            yield from allocate_scores(ks[1:], ms, used, ss)
          else:
            # allocate both scores
            (a, b) = ms[k]
            for x in scores[a]:
              x_ = ordered(*x)
              if x_ in used: continue
              for y in scores[b]:
                y_ = ordered(*y)
                # remove duplicate solutions
                if (a == b and not x_ > y_) or y_ in used: continue
                yield from allocate_scores(ks[1:], ms, used.union({x_, y_}), update(ss, [(k, (x, y))]))
      
      # extract match/teams values from dict d
      def extract(d, t, fn):
        (gs, ts) = (list(), list())
        for (k, v) in d.items():
          if t in k:
            gs.extend(v)
            ts.extend([(0 if k[0] == t else 1)] * len(v))
        return fn(gs, ts)
      
      # construct the table for team t, using matches in ms
      table = lambda ms, t: extract(ms, t, football.table)
      
      # goals for/against team t, using scores in ss
      goals = lambda ss, t: extract(ss, t, football.goals)
      
      # output the matches and scores
      def output_matches(ms, ss, **kw):
        def _process(d, fn):
          d_ = dict()
          for (k, (a, b)) in d.items():
            d_[k] = a
            d_[rev(k)] = fn(b)
          return d_
        football.output_matches(_process(ms, football.swap), _process(ss, rev), **kw)
      
      # does team <x> beat team <y> in the table <tbl> and match outcomes <ms>?
      def beats(x, y, tbl, ms):
        # a team does not beat itself
        if x == y: return 0
        # can it be decided on points?
        r = compare(tbl[x].points, tbl[y].points)
        if r == 1: return 1
        if r == -1: return 0
        # can it be decided on the outcome of head-to-head games?
        (k, a, b) = ((x + y, 0, 1) if x < y else (y + x, 1, 0))
        gs = ms[k]
        (X, Y) = (football.table(gs, [a, a]), football.table(gs, [b, b]))
        r = compare(X.points, Y.points)
        if r == 1: return 1
        if r == -1: return 0
        # there may be other ways to order the teams (e.g. goal difference
        # or goals scored), but these are not mentioned
        return 0
      
      # check all possible final 2 games for required scenario
      def check(ms, f1, f2):
        # check conditions for teams A, B, C
        fB = fC = 0
        for (g1, g2) in football.games('wdl', repeat=2):
          ms_ = update(ms, [(f1, pair(ms[f1][0] + g1)), (f2, pair(ms[f2][0] + g2))])
          # calculate the final table
          tbl = dict((t, table(ms_, t)) for t in teams)
          # can A fail to qualify? (i.e. fail to beat at least 2 of the other teams)
          if not (sum(beats('A', t, tbl, ms_) for t in teams) >= 2): return False
          # can B come top? (i.e. beat the other 3 teams)
          if fB == 0 and sum(beats('B', t, tbl, ms_) for t in teams) == 3: fB = 1
          # can C come top? (i.e. beat the other 3 teams)
          if fC == 0 and sum(beats('C', t, tbl, ms_) for t in teams) == 3: fC = 1
        return (fB and fC)
      
      # find viable scenarios
      def generate():
        # choose the final 2 matches (which are draws)
        for ((t1, t2), (t3, t4)) in partitions(teams, 2):
          (f1, f2) = (t1 + t2, t3 + t4)
          ms0 = { f1: 'x', f2: 'x' }
          # of the remaining matches exactly 1 of them is a draw
          for d in matches:
            ms1 = update(ms0, [(d, ('dx' if d in ms0 else 'd'))])
            # choose win/lose outcomes for the remaining matches
            for ms2 in allocate_matches(matches, ms1):
              # calculate the table before the final 2 matches are played
              (A, B, C, D) = (table(ms2, t) for t in teams)
              if not (A.points > B.points > C.points > D.points): continue
              # in the final 2 games B or C _could_ gain the top spot
              # so A cannot be more than 3 points ahead of C
              if A.points > C.points + 3: continue
              # check all possible final 2 games
              if not check(ms2, f1, f2): continue
      
              # now include the final games (both are, in fact, drawn)
              ms3 = update(ms2, list((k, pair(ms2[k][0] + 'd')) for k in (f1, f2)))
              (A, B, C, D) = (table(ms3, t) for t in teams)
      
              # allocate scores for D
              (Ds, As) = (list(k for k in matches if t in k) for t in "DA")
              for (ss1, us1) in allocate_scores(Ds, ms3):
                # D has equal for and against (and scored more goals than any other team)
                (fD, aD) = goals(ss1, 'D')
                if fD != aD: continue
                # allocate remaining scores for A
                for (ss2, us2) in allocate_scores(As, ms3, us1, ss1):
                  # A scored fewer goals than any other team
                  (fA, aA) = goals(ss2, 'A')
                  if not (fA < fD): continue
                  # allocate remaining scores (B and C)
                  for (ss3, us3) in allocate_scores(matches, ms3, us2, ss2):
                    ((fB, aB), (fC, aC)) = (goals(ss3, t) for t in "BC")
                    if not (fA < fB < fD and fA < fC < fD): continue
      
                    # yield scenario (matches, scores, final2, goals for/against)
                    yield (ms3, ss3, (f1, f2), ((fA, aA), (fB, aB), (fC, aC), (fD, aD)))
      
      
      # look for viable scenarios
      for (ms, ss, fs, gfa) in generate():
        # output scenarios as we find them
        output_matches(ms, ss, end=None)
        printf("final games = {fs}", fs=join(fs, sep=", "))
        printf("goals for/against = {gfa}", gfa=map2str(zip(teams, gfa), arr=": ", enc=""))
        BCs = ss['BC']
        # answer is the scores in the B vs C matches
        printf("-> B vs C = {BCs}", BCs=join(BCs, sep=" "))
        printf()
      

      Solution: The scores in the B vs. C games were: 4-1 and 1-1.

      And the B vs C 1-1 draw was one of the two final games.


      There are two possible scenarios. Here is one of them:

      A vs B: 3-0, 2-0
      A vs C: 0-0, 0-4
      A vs D: 1-0, (2-2)
      B vs C: 4-1, (1-1)
      B vs D: 3-1, 2-1
      C vs D: 3-2, 0-5

      (with the two final draws indicated in brackets).

      Before the final 2 games (A vs D, B vs C) are the points are:

      A: 10 points
      B: 9 points
      C: 7 points
      D: 3 points

      If B wins their final game then they will have 12 points, and top the table (providing A does not win their final game).

      If C wins their final game then they will have 10 points, and if A loses their final game they will also have 10 points, so the top position comes down to the result of the A vs C games. And these are a draw and a win for C, so C comes out on top.

      A is guaranteed to finish higher than D, and unless C wins (and A loses) their final game also higher than C. But if C does win, then B must lose, and A will finish higher than B. So A is guaranteed to finish in the top 2 teams (and qualify for the next round).

      When the final 2 games are played they are both drawn, and the results are:

      A: 11 points; goals = 8 for, 6 against
      B: 10 points; goals = 10 for, 9 against
      C: 8 points; goals = 9 for, 12 against
      D: 4 points; goals = 11 for, 11 against

      So A and B go through to the next round.

      The other scenario plays out similarly, but the games won 3-1 and 3-2 are swapped with each other. Although this second scenario could be eliminated if the puzzle text stated that D were the only team with the same number of goals for as goals against. (As B ends up with 10 goals for and 10 goals against).

      We can use these 2 scenarios to construct further solutions where the order of the two matches played by pairs of teams are swapped over.

      Like

  • Unknown's avatar

    Jim Randell 9:18 am on 28 February 2023 Permalink | Reply
    Tags:   

    Teaser 2715: Colour coded 

    From The Sunday Times, 5th October 2014 [link] [link]

    Five lads cycle together daily. Each has a different number of helmets, less than a dozen, and none of them has two of the same colour. Each wears in daily rotation all the cycle helmets he possesses. On 1st September, Alan wore mauve, Bill and Charles wore red, Dave orange and Eric green. On 11th September, two were red, one green, one mauve and one white. On 19th September, Dave’s was orange, Eric’s green and the others red. Eric wore orange on the 22nd and white on the 23rd. On 1st October all five wore the same as on 1st September.

    In alphabetical order of the riders, what were the helmet colours on the 11th September?

    [teaser2715]

     
    • Jim Randell's avatar

      Jim Randell 9:18 am on 28 February 2023 Permalink | Reply

      I am assuming that each cyclist cycles(!) through their helmets in strict rotation. So each time through they wear the same colours in the same order.

      None of them have duplicate colours, so if we see one of them wearing the same colour on different days, they must have completed a whole number of cycles between those days. And they are all wearing the same colours on 1st September and 1st October, so the length of each of their cycles must be a divisor of 30, less than 12 (i.e. 1, 2, 3, 5, 6, 10).

      For any given cyclist, if we are given a set of days and colours then we can look at days when the same colour is worn. The gap between the days must be a multiple (possibly 1) of the cycle length. And so by considering all possible gaps we can can determine possible cycle lengths. Furthermore, different colours must be worn on different days in the cycle. So

      This Python program calculates possible cycle lengths from the colours given on the specified dates.

      It runs in 55ms. (Internal runtime is 2.2ms).

      Run: [ @replit ]

      from enigma import (
        subsets, group, unpack, union, tuples, mgcd, divisors,
        seq_all_different, delete, update, rev, map2str, printf
      )
      
      # find cycle lengths for values give in ms
      def solve(ms, ns=dict()):
        # are we done?
        if not ms:
          yield ns
        else:
          # choose a key to process (the one with the most entries)
          x = max(ms.keys(), key=(lambda k: len(ms[k].keys())))
          # group days by colour
          g = group(ms[x].items(), by=unpack(lambda k, v: v), f=unpack(lambda k, v: k))
          # collect multiples of cycle length
          s = union((abs(b - a) for (a, b) in tuples(vs, 2)) for vs in g.values())
          # consider possible cycle lengths (must be < 12)
          for n in divisors(mgcd(*s)):
            if n > 11: break
            # is this one already used?
            if n in ns: continue
            # check different colours give different cycle days
            if not seq_all_different(vs[0] % n for vs in g.values()): continue
            # solve for the remaining items
            yield from solve(delete(ms, [x]), update(ns, [(n, x)]))
      
      # consider possible orderings for 11th
      for (a, b, c, d, e) in subsets('RRGMW', size=len, select='mP'):
        # known positions (day 1 = 1st September)
        ms = dict(
          A={ 1: 'M', 11: a, 19: 'R', 31: 'M' },
          B={ 1: 'R', 11: b, 19: 'R', 31: 'R' },
          C={ 1: 'R', 11: c, 19: 'R', 31: 'R' },
          D={ 1: 'O', 11: d, 19: 'O', 31: 'O' },
          E={ 1: 'G', 11: e, 19: 'G', 22: 'O', 23: 'W', 31: 'G' },
        )
        # find possible cycle lengths
        for ns in solve(ms):
          # output solution
          printf("day 11: (A B C D E) = ({a} {b} {c} {d} {e}) {ns}", ns=map2str(rev(ns), sep=" ", enc="[]"))
      

      Solution: On 11th September, the colours were: Alan = mauve; Bill = red; Charles = red; Dave = green; Eric = white.

      Alan has 5 or 10 helmets. Bill has 1 or 2. Charles has 2 or 1. Dave has 3. Eric has 6.

      We can’t work out all the colours, but here is what we do know:

      A: (M ? ? ? ?) or (M ? ? ? ? ? ? ? ? ?)
      B: (R) or (R ?)
      C: (R ?) or (R)
      D: (O ? ?)
      E: (G ? ? O W ?)

      However these cycle lengths are chosen, they all give the same sequence of values for the 11th September.

      Like

  • Unknown's avatar

    Jim Randell 8:18 am on 21 February 2023 Permalink | Reply
    Tags:   

    Teaser 2716: Millennial squares 

    From The Sunday Times, 12th October 2014 [link] [link]

    I asked my class to take some pieces of card and to write a digit on each side of each card so that any year of this millennium that is a perfect square could be spelt out using four of the cards. One clever pupil achieved this with the smallest number of cards possible. He also pointed out that with his cards he could go way beyond the end of this millennium — in fact he had designed them so that, when trying to make this list of consecutive square years yet to come, he was able to go as far as possible with that number of cards.

    What is the last square he could make?

    [teaser2716]

     
    • Jim Randell's avatar

      Jim Randell 8:19 am on 21 February 2023 Permalink | Reply

      First we note that the digit 9 can use the same symbol as the digit 6, so we only need to consider the symbols 0-8. (Although it turns out that we get the same answer even if the digits 6 and 9 use different symbols).

      We can quickly find a theoretical minimum collection of symbols required to express the squares between 2001 – 3000 by considering the multiset union of each of those squares (considered as a multiset of corresponding symbols). And with two symbols per card we can determine the theoretical minimum number of cards required to accommodate these symbols. (Although this does not tell us that an appropriate set of cards can be constructed).

      We can then start adding in additional squares until we can no longer fit all the required symbols on the required number of cards, and this gives us a theoretical maximum constructible square to answer the puzzle.

      We can then check that this solution is achievable by constructing a collection of cards of the appropriate size that allows all the required squares to be made.

      This Python program runs in 56ms. (Internal runtime is 809µs).

      Run: [ @replit ]

      from enigma import (irange, inf, sq, nsplit, join, multiset, divc, delete, cache, printf)
      
      # symbols used to represent digits (6 and 9 are the same symbol)
      sym = dict(zip(irange(0, 9), "0123456786"))
      
      # words to be spelled out
      word = cache(lambda n: join(sym[d] for d in nsplit(n)))
      
      # find the minimum number of cards required (n symbols per card)
      min_cards = lambda m, n=2: divc(m.size(), n)
      
      # collect the symbols required to make the initial set of numbers
      ns = list(sq(i) for i in irange(45, 54))
      m = multiset()
      for n in ns:
        m.union_update(word(n))
      k = min_cards(m)
      printf("{k} cards = [{a} .. {b}] ({s} symbols)", a=ns[0], b=ns[-1], s=m.size())
      
      # add extra numbers until another card is required
      for i in irange(55, inf):
        n = sq(i)
        w = word(n)
        m_ = m.union(w)
        k_ = min_cards(m_)
        printf("{k_} cards = [{a} .. {b}] ({s} symbols)", a=ns[0], b=n, s=m_.size())
        if k_ > k: break
        ns.append(n)
        m = m_
      
      # now try to construct a set of <k> cards that covers the numbers <ns>
      
      # can we make word <w> with cards <cs>
      def make(w, cs):
        if not w:
          return True
        x = w[0]
        for (i, c) in enumerate(cs):
          if x in c and make(w[1:], delete(cs, [i])):
            return True
        return False
      
      # make a set of <k> cards, the can make words <ws>, symbol counts in <m>
      def cards(k, ws, m, cs=[]):
        # are we done?
        if k == 0:
          if all(make(w, cs) for w in ws):
            yield tuple(sorted(cs))
        elif k > 0 and m:
          # make the next card
          x = m.min()
          for y in m.keys():
            if x < y:
              c = x + y
              if (not cs) or (not c < cs[-1]):
                yield from cards(k - 1, ws, m.difference([x, y]), cs + [c])
      
      # try to make a <k>-set of cards that covers numbers <ns>
      if m.size() % 2 == 1: m.add('?')
      for cs in cards(k, list(word(n) for n in ns), m):
        printf("cards = {cs}", cs=join(cs, sep=" ", enc="()"))
        break
      

      Solution: The last square that he could make was 3721.


      To make the squares 2025 – 2916 requires a collection of 13 symbols, so we need 7 cards to accommodate these.

      And if we also include the squares up to 3721 then we need a collection of 14 symbols, which still requires 7 cards.

      But to also include the next square (3844) would require 15 symbols (as we need to have two 4’s), and this cannot be accommodated with 7 cards.

      There are many sets of 7 cards that allow the squares 2025 – 3721 to be made, for example:

      (02 07 13 18 26 39 45)

      (And this set works if 6 and 9 are different digits, or if they are the same digit).

      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