Updates from Jim Randell Toggle Comment Threads | Keyboard Shortcuts

  • Unknown's avatar

    Jim Randell 10:45 am on 25 August 2022 Permalink | Reply
    Tags:   

    Teaser 2745: Square cut 

    From The Sunday Times, 3rd May 2015 [link] [link]

    Jorkens, the wily old cricketer, is faced with a new type of square cut. His house has three square bedrooms, all of different sizes. He has just bought a new carpet for the largest bedroom and has cut up its old carpet into four rectangular pieces, the smallest of which has an area of four square metres. He is able to use the four pieces to carpet the other two bedrooms exactly.

    What is the area of the largest bedroom?

    [teaser2745]

     
    • Jim Randell's avatar

      Jim Randell 10:46 am on 25 August 2022 Permalink | Reply

      Suppose the floors of the rooms have sides a, b, c (smallest to largest).

      Then, if the carpet from the largest bedroom can be used to exactly carpet the other 2 bedrooms we have:

      c² = a² + b²

      So, (a, b, c) form a right-angled triangle (but this is not necessarily a Pythagorean triple, as we are not told that the sides of the rooms take on integer values).

      Or:

      b² = c² − a²
      ⇒ b² = (c − a)(c + a)

      Each side of the larger square must be reduced (otherwise we have a rectangle with side c that won’t fit in the smaller bedrooms).

      Assuming the carpet is cut into exactly 4 rectangular regions (which may be square), we must have cuts like this:

      (i.e. one cut must go across the entire length of the square, and the rectangles produced from this cut must both have cuts perpendicular to this).

      I supposed we cut off an a × a square for the small room, which leaves three rectangles remaining to assemble into a square for the middle room.

      This gives us 3 remaining pieces of size (for some value d):

      a × (c − a)
      (c − a) × d
      (c − a) × (c − d)

      And in order to be assembled into a square of side b one of the pieces must have a dimension of b.

      So either: b = (c − a) or b = d.

      If b = (c − a) we infer that b = c, which is not possible, so the 3 remaining pieces are:

      a × (c − a)
      (c − a) × b
      (c − a) × (c − b)

      Which fit together like this:

      From which we see (vertical edges):

      b = a + (c − b)
      ⇒ 2b = a + c
      ⇒ b = (a + c) / 2

      i.e. b is the mean of a and c.

      So we write:

      b = a + x
      c = a + 2x

      To give the following diagram:

      From which we see (horizontal edges):

      a + x = 4x
      ⇒ a = 3x

      So:

      a = 3x
      b = 4x
      c = 5x

      (So (a, b, c) is a Pythagorean triple, if we measure it in units of x).

      And the pieces and their areas are:

      a × a = 9x²
      a × (c − a) = 6x²
      b × (c − a) = 8x²
      (c − b) × (c − a) = 2x²

      And the smallest of these (2x²) has an area of 4 square metres hence x² = 2.

      And we want the area of the largest bedroom (c²).

      c² = (5x)²
      ⇒ c² = 25x²
      ⇒ c² = 50 square metres

      Solution: The floor area of the largest bedroom is 50 square metres.


      And we can calculate the floor areas of the other rooms as well:

      small = 18 square metres (a 3√2 metre square)
      middle = 32 square metres (a 4√2 metre square)
      large = 50 square metres (a 5√2 metre square)

      And 18 + 32 = 50.

      The 50 square metre carpet from the largest room is cut into the following rectangles:

      √2 × 2√2 (≈ 1.41 × 2.83) = 4 square metres (b.1)
      2√2 × 3√2 (≈ 2.83 × 4.24) = 12 square metres (b.2)
      2√2 × 4√2 (≈ 2.83 × 5.66) = 16 square metres (b.3)
      3√2 × 3√2 (≈ 4.24 × 4.24) = 18 square metres (a)

      Or with each square having an area of 2 square metres (i.e. of side √2 metres):

      Like

  • Unknown's avatar

    Jim Randell 10:41 am on 23 August 2022 Permalink | Reply
    Tags: ,   

    Teaser 2738: Prime day for the Irish 

    From The Sunday Times, 15th March 2015 [link] [link]

    St Patrick’s Day is March 17 and it is a prime day in many ways:

    What number month? 3;
    What number day? 17;
    How many letters in “March”? 5;
    How many days in March? 31.

    I asked Pat the same questions about his birthday this year — but I simply asked whether the four answers were prime or not. When he had told me he said: “Now, if I told you its day of the week this year, you should be able to work out my birthday”.

    Then, without me actually being told the day, I was indeed able to work out his birthday.

    What is his birthday?

    [teaser2738]

     
    • Jim Randell's avatar

      Jim Randell 10:42 am on 23 August 2022 Permalink | Reply

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

      Run: [ @replit ]

      from datetime import (date, timedelta)
      from enigma import (repeat, is_prime, filter_unique, unpack, printf)
      
      # number of letters in each month (English names)
      months = (
        "january", "february", "march", "april", "may", "june", "july",
        "august", "september", "october", "november", "december",
      )
      mlen = dict((k, len(x)) for (k, x) in enumerate(months, start=1))
      
      # number of days in each month
      mdays = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
      
      # generate dates in 2015, return (<date>, <answers-to-questions>)
      def generate():
        inc = lambda x, i=timedelta(days=1): x + i
        # consider consecutive days in 2015
        for d in repeat(inc, date(2015, 1, 1)):
          if d.year > 2015: break
          # are the following prime?
          qs = tuple(is_prime(x) for x in (
            # 1. the month number
            d.month,
            # 2. the day number
            d.day,
            # 3. the number of letters in the month
            mlen[d.month],
            # 4. the number of days in the month
            mdays[d.month],
          ))
          yield (d, qs)
      
      # candidate dates
      rs = generate()
      # selection functions
      fn_d = unpack(lambda d, qs: d)
      fn_qs = unpack(lambda d, qs: qs)
      fn_qs_dow = unpack(lambda d, qs: (qs, d.isoweekday()))
      
      # if we knew the answers to the questions _and_ the day of the week
      # then we could work out the date
      rs = filter_unique(rs, fn_qs_dow, fn_d).unique
      
      # now we can work out the date only knowing the answers to the questions
      rs = filter_unique(rs, fn_qs, fn_d).unique
      
      # output solutions
      for (d, qs) in rs:
        printf("date = {d} [qs={qs}]", d=d.strftime('%A, %d %B %Y'))
      

      Solution: Pat’s birthday is on 29th November.


      There are 9 dates that can be identified as unique knowing the answers to the questions and the day of the week (in 2015).

      If the answers to the questions are: (not prime, prime, prime, not prime) we have:

      Mon ⇒ 13th April 2015
      Tue ⇒ 7th April 2015
      Wed ⇒ 29th April 2015
      Sat ⇒ 11th April 2015

      If the answers to the questions are: (prime, prime, not prime, prime) we have:

      Mon ⇒ 13th July 2015
      Tue ⇒ 7th July 2015
      Wed ⇒ 29th July 2015
      Sat ⇒ 11th July 2015

      And if the answers to the questions are: (prime, prime, not prime, not prime) we have:

      Sun ⇒ 29th November 2015

      As the setter does not need to be told the day of the week to find the birthday from these 9 he must have been told (prime, prime, not prime, not prime) so the candidate dates are narrowed down to a single possibility.

      Like

    • Frits's avatar

      Frits 2:48 pm on 23 August 2022 Permalink | Reply

      partition_unique has recently been updated.

         
      from calendar import month_name, monthrange, day_name, weekday
      
      # https://brg.me.uk/?page_id=4800
      from partition_unique import partition_unique
      
      # primes up to 365
      P = {3, 5, 7, 11, 13, 17, 19}
      P |= {2} | {x for x in range(23, 366, 2) if all(x % p for p in P)}
      
      year = 2015
      sols = []
      # generate dates in 2015
      # store (<answers-to-questions>, <day name>, <(month, day)>)
      for month in range(1, 13):
        month_length = monthrange(year, month)[1]
        len_month_name = len(month_name[month])
        for d in range(1, month_length + 1):
          # find the day of the week
          d_name = day_name[weekday(year, month, d)]
          
          sols.append(((# 1. the month number
                       month in P, 
                       # 2. the day number
                       d in P, 
                       # 3. the number of letters in the month
                       len_month_name in P,
                       # 4. the number of days in the month
                       month_length in P),
                       d_name, (month, d) 
                     ))   
          
      f_answs = lambda answs, nm, md: answs
      f_answs_nm = lambda answs, nm, md: (answs, nm)
      f_md = lambda answs, nm, md: md
      
      # find those solutions for which the answers plus day of the week
      # lead to the date
      sols = partition_unique(sols, f_answs_nm, f_md)[0]
      
      # find those solutions for which the answers lead to the date
      sols = partition_unique(sols, f_answs, f_md)[0]
      
      for _, _, md in sols:
        print(f"answer: {month_name[md[0]]} {md[1]}")
      

      Like

    • NigelR's avatar

      NigelR 5:19 pm on 25 August 2022 Permalink | Reply

      Not, I suspect, very ‘pythonic’ but seems to run ok. enigma timer gives 3.2ms:

      prim=[2,3,5,7,11,13,17,19,23,29,31]
      mths=["january", "february", "march", "april", "may", "june", "july",
        "august", "september", "october", "november", "december"]
      days=[31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
      dow=['We','Th','Fr','Sa','Su','Mo','Tu']  # 1 Jan 2015 fell on a Thursday
      #i = month (0-11),j = day of month.  Returns string of day of week (Mo-Su)
      day = lambda i,j: dow[(j+sum([days[k] for k in range(i)]))%7]
      #generate answers to questions + day of week for each day in format 'n/p n/p n/p n/p dd'
      ans=[[''.join(['p' if x+1 in prim else 'n','p' if y in prim else 'n', 'p' if len(mths[x]) in prim else 'n',
             'p'if days[x] in prim else 'n',day(x,y)]),x,y] for x in range(12) for y in range (1,days[x]+1)]
      #create dictionary with count of each occurrence of 'n/p n/p n/p n/p dd'
      cand={x[0]:[y[0] for y in ans].count(x[0]) for x in ans }
      #remove non-unique entries
      lst=[x for x in cand.keys() if cand[x] == 1] 
      #identify whether there is a unique day of week entry:
      result = [x for x in lst if [y[4:] for y in lst].count(x[4:])==1]
      if len(result)>1:
          print('No unique solution found')
          exit()
      print([['Birthday is' + ' '+str( x[2]) + ' '+ str(mths[x[1]+1].capitalize())] for x in ans if x[0]==result[0]][0][0])
      

      Like

  • Unknown's avatar

    Jim Randell 10:37 am on 21 August 2022 Permalink | Reply
    Tags: by: R. H. Hide   

    Brain-Teaser 47: A new suit of sails 

    From The Sunday Times, 11th February 1962 [link]

    George possesses a nine-ton cruising and racing yacht, and its class rules do not allow him to exceed 500 sq. ft. of sail area. When racing George uses a tall Bermudan mainsail and a single overlapping Genoa jib.

    George has designed his own sails, and after considerable experiment he has found that the best shape for the mainsail hangs the boom at right-angles to the mast, and the best shape for the jib also takes the form of a right-angled triangle.

    On his plans for a new suit of sails George found that all the dimensions worked out as an exact number of feet (no inches or fractions). He recently approached two different sailmakers for estimates for the new sails. The first sailmaker quoted him at 4s. per square foot of sail, while the second said he always charged on the length of the perimeter edge of a sail, and he quoted at 12s. per foot run of edge.

    To George’s astonishment both quotations were identical not only in total for the pair of sails but also for each separate sail. George was keen on racing and had designed his sail area as close to the limit as possible.

    By how many square feet were George’s new sails below the limit of 500 sq. ft. allowed by the class rules?

    [teaser47]

     
    • Jim Randell's avatar

      Jim Randell 10:38 am on 21 August 2022 Permalink | Reply

      Using the [[ pythagorean_triples() ]] function from the enigma.py library.

      This Python program runs in 53ms. (Internal runtime is 158µs).

      Run: [ @replit ]

      from enigma import (pythagorean_triples, subsets, Accumulator, printf)
      
      # find dimensions of sails with equal costs by area (A) and perimeter (P)
      sails = list()
      for (x, y, z) in pythagorean_triples(100):
        A = 2 * x * y
        if A > 2000: continue
        P = 12 * (x + y + z)
        if A == P:
          sails.append((x, y, z))
      
      # find pairs with total area closest to, but not exceeding 500
      r = Accumulator(fn=max)
      for ss in subsets(sails, size=2):
        ((x1, y1, z1), (x2, y2, z2)) = ss
        # we use T = twice the total area
        T = x1 * y1 + x2 * y2
        if not (T > 1000):
          r.accumulate_data(T, ss)
      
      # output solution
      (T, ss) = (r.value, r.data)
      A = 0.5 * T
      printf("{x:g} sq ft below limit [sails = {ss}; area = {A:g}]", x=500 - A)
      

      Solution: George’s sails are 14 sq. ft. below the limit.

      i.e. together his sails have an area of 486 sq. ft.

      The smaller sail has dimensions (18, 24, 30). Area = 216; Cost by area = 864; Cost by perimeter = 864.

      The larger sail has dimensions (15, 36, 39). Area = 270; Cost by area = 1080; Cost by perimeter = 1080.

      Like

      • Frits's avatar

        Frits 11:55 am on 22 August 2022 Permalink | Reply

        You have used the “g” format before but this is the first time I notice it.

        Like

        • Jim Randell's avatar

          Jim Randell 4:58 pm on 22 August 2022 Permalink | Reply

          @Frits: I use the ‘g’ format specifier when the value is a float, but I expect it to be an integer. If it is an integer the “.0” part is suppressed, so the output looks neater.

          >>> sprintf("{x:g}", x=2.0)
          '2'
          >>> sprintf("{x:g}", x=2.5)
          '2.5'
          

          Like

    • GeoffR's avatar

      GeoffR 2:54 pm on 21 August 2022 Permalink | Reply

      % A Solution in MiniZinc
      include "globals.mzn";
      
      % Possible Two Pythagorean triangle dimensions range
      var 3..50:x1; var 3..50:y1; var 3..50:z1;
      var 3..50:x2; var 3..50:y2; var 3..50:z2;
      
      % Area of both sails less than 500 sq. ft.
      var 1..100:area_below500;
      
      % Two Pythagorean triangles of sail area
      % Main sail is larger than the genoa sail
      constraint x1 * x1 + y1 * y1 == z1 * z1;
      constraint x2 * x2 + y2 * y2 == z2 * z2 /\ z2 > z1;
      
      % Each sail triangle has area and perimeter costs the same
      constraint x1 * y1 * 2 == (x1 + y1 + z1) * 12;
      constraint x2 * y2 * 2 == (x2 + y2 + z2) * 12;
      
      % Sum of area of both sails is less than 500 sq. ft.
      constraint x1 * y1 div 2 + x2 * y2 div 2 < 500;
      
      % Maximise sum of both sail areas
      solve maximize(x1 * y1 div 2 + x2 * y2 div 2);
      
      % Find area of sum of both sails less than 500 sq. ft.
      constraint area_below500 == 500 - (x1 * y1 div 2 + x2 * y2 div 2);
      
      output ["Total sail area below 500 sq. ft. = " ++ 
      show(area_below500) ++ " sq. ft." 
      ++ "\nGenoa sail area = " ++ show(x1 * y1 div 2) ++ " sq. ft." 
      ++ "\nMain sail area = " ++ show( x2 * y2 div 2 )  ++ " sq. ft."];
      
      % Total sail area below 500 sq. ft. = 14 sq. ft.
      % Genoa sail area = 216 sq. ft.
      % Main sail area = 270 sq. ft.
      % ----------
      % ==========
      
      
      
      

      Like

    • Frits's avatar

      Frits 11:46 am on 22 August 2022 Permalink | Reply

      More complicated.

         
      # x * y = 6 * (x + y + z); x^2 + y^2 = z^2; x <= y < z
      # leads to y = (12x - 72) / (x - 12) 
      
      # assume x <= y < z
      # as z > y and z = x * y / 6 - x - y variable x must be larger than 12
      M = 500
      largest_area = M - 6   # M - area smallest Pythagorean triple (3, 4, 5)
      
      sols = []
      for x in range(13, int(largest_area**.5) + 1):
        y, r = divmod(12 * (x - 6), x - 12)
        if r or y < x: continue
        
        if x * y > 2 * largest_area: continue
        
        z, r = divmod(x * y - 6 * (x + y), 6)
        if r: continue
        sols.append([(x * y) / 2, (x, y, z)])
      
      sols.sort()  
      
      # remove entries with too high area (step is not necessary)
      while True:
        # check entry with the highest area
        if sols[-1][0] + sols[0][0] > M:
          sols.pop()
        else:
          break
      
      mx = 0
      for i, s1 in enumerate(sols):
        for s2 in sols[i + 1:]:
          tot = s1[0] + s2[0]
          if tot <= M: 
            mx = max(mx, tot)
          
      print("answer:", M - mx if mx % 1 else int(M - mx)) 
      

      Like

      • Frits's avatar

        Frits 12:11 pm on 22 August 2022 Permalink | Reply

        From the formula of z we know that x * y must be a multiple of 6 so each sail area is an integer.

        Like

    • John Crabtree's avatar

      John Crabtree 5:15 am on 23 August 2022 Permalink | Reply

      Extending Frits’ analysis, the short sides are x and y = 12 + 72/(x – 12) where 12 < x < 21
      This leads to z = x + y – 12, and the area = 6 * (z + 6).
      It then follows that the sum of the two values of z must be <= INT(500 / 6 – 12) , ie <= 71.

      Like

      • Frits's avatar

        Frits 12:15 pm on 23 August 2022 Permalink | Reply

          
        from enigma import divisors
        
        # area = 6 * x + 36 + 432 / (x - 12) and area is an integer as
        # (x * y) / 6 must be an integer so (x - 12) must be a divisor of 432
        
        # print range for x with the correct boundaries
        print([x for y in divisors(432) 
            if 134 / 3 - 2 * 2239**.5 / 3 <= (x := y + 12) <= 12 + 6 * 2**.5])
            
        # [14, 15, 16, 18, 20]
        

        Like

  • Unknown's avatar

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

    Teaser 3126: Sweet success 

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

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

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

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

    [teaser3126]

     
    • Jim Randell's avatar

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

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

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

      Run: [ @replit ]

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

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

      So the pairs are:

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


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

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

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

      Like

      • Jim Randell's avatar

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

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

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

        Run: [ @replit ]

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

        Like

    • NigelR's avatar

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

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

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

      Like

    • NigelR's avatar

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

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

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

      ….

      Like

      • Frits's avatar

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

        Hi Nigel,

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

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

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

        Like

    • Frits's avatar

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

      I totally forgot the teaser last night.

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

      Like

    • NigelR's avatar

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

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

      Like

  • Unknown's avatar

    Jim Randell 9:07 am on 18 August 2022 Permalink | Reply
    Tags:   

    Teaser 2729: Factorial fact 

    From The Sunday Times, 11th January 2015 [link] [link]

    The “factorials” of numbers (denoted by !) are defined by:

    1! = 1
    2! = 2 × 1
    3! = 3 × 2 × 1
    4! = 4 × 3 × 2 × 1
    etc.

    It is possible to take eleven of the twelve factorials 1!, 2!, 3!, 4!, 5!, 6!, 7!, 8!, 9!, 10!, 11!, 12! and to split them into groups of three, four and four, so that in each group the product of the factorials in that group is a perfect square.

    What are the factorials in the group whose product is the smallest?

    [teaser2729]

     
    • Jim Randell's avatar

      Jim Randell 9:07 am on 18 August 2022 Permalink | Reply

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

      Run: [ @replit ]

      from enigma import (
        irange, subsets, factorial, multiply, is_square_p,
        seq_all_different as all_different, printf
      )
      
      # measure function
      fn = lambda ns: multiply(factorial(n) for n in ns)
      
      # find groups of k-tuples with a measure that is a square
      def generate(k):
        for ns in subsets(irange(1, 12), size=k):
          if is_square_p(fn(ns)):
            yield ns
      
      # collect 3-, 4-tuples
      n3s = list(generate(3))
      n4s = list(generate(4))
      
      # choose two disjoint 4-tuples
      for (g1, g2) in subsets(n4s, size=2):
        if not all_different(g1 + g2): continue
        # and find a disjoint 3-tuple
        for g3 in n3s:
          if not all_different(g1 + g2 + g3): continue
          # find the group with the minimal measure
          g = min(g1, g2, g3, key=fn)
          printf("min = {g} [{g3} {g1} {g2}]")
      

      Solution: The factorials in the group with the smallest product are: 1!, 8!, 9!.

      There are 2 ways to form the groups:

      (1, 8, 9) (2, 3, 11, 12) (4, 5, 7, 10)
      (1, 8, 9) (2, 4, 11, 12) (3, 5, 7, 10)

      Like

    • GeoffR's avatar

      GeoffR 2:38 pm on 18 August 2022 Permalink | Reply

      A brute force solution produced products of 11, 14 and 18 digits long for groups of 3, 4 and 4 factorials. The solution ran in approximately 20 sec.

      The smallest solution was repeated if the break statement is excluded.

      from itertools import permutations
      from enigma import factorial as fact
      from enigma import is_square as is_sq
      
      # Precalculate the twelve factorials
      F1,F2, F3 = fact(1), fact(2), fact(3)
      F4, F5, F6 = fact(4), fact(5), fact(6)
      F7, F8, F9 = fact(7), fact(8), fact(9)
      F10, F11, F12 = fact(10), fact(11), fact(12)
      
      # Factorial dictionary for output
      ND = {F1: 'F1', F2:'F2', F3:'F3', F4:'F4', F5:'F5',F6:'F6',
            F7:'F7', F8:'F8', F9:'F9', F10:'F10', F11:'F11', F12:'F12' }
            
      for p1 in permutations((F1,F2,F3,F4,F5,F6,F7,F8,F9,F10,F11,F12),11):
        a, b, c, d, e, f, g, h, i, j, k = p1
        # split factorials into groups of three, four and four
        if not (is_sq(a * b * c)): continue
        if not (is_sq(d * e * f * g)): continue
        if not( is_sq(h * i * j * k)):continue
        # sort factorial products
        T = sorted((a * b * c , d * e * f * g, h * i * j * k))
        if a * b * c < d * e * f * g and a * b * c < h * i * j * k:
          print(f"Factorials in smallest product are {ND[a]}, {ND[b]} and {ND[c]}.")
          print(f"Smallest factorial product = {a * b * c}")
          break
      
      # Factorials in smallest product are F1, F8 and F9.
      # Smallest factorial product = 14631321600
      
      
      
      

      Like

      • GeoffR's avatar

        GeoffR 9:17 am on 19 August 2022 Permalink | Reply

        A three stage permutation seemed more compatible with a (3,4,4) group of numbers. This solution was considerably faster than my previous posting, running in 4.64ms.

        from itertools import permutations
        from enigma import factorial as fact, is_square as is_sq, timer
        
        timer.start()
        # Precalculate the twelve factorials
        F1,F2, F3 = fact(1), fact(2), fact(3)
        F4, F5, F6 = fact(4), fact(5), fact(6)
        F7, F8, F9 = fact(7), fact(8), fact(9)
        F10, F11, F12 = fact(10), fact(11), fact(12)
        
        factorials = {F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12} 
        
        # Factorial dictionary for output
        ND = {F1: 'F1', F2:'F2', F3:'F3', F4:'F4', F5:'F5',F6:'F6',
              F7:'F7', F8:'F8', F9:'F9', F10:'F10', F11:'F11', F12:'F12' }
        
        # first stage permutation
        # split factorials into groups of three, four and four
        for p1 in permutations(factorials, 3):
          a, b, c = p1
          if not (is_sq(a * b * c)):
            continue
        
          # second stage permutation
          q1 = factorials.difference({a, b, c})
          for p2 in permutations(q1, 4):
            d, e, f, g = p2
            if not (is_sq(d * e * f * g)):
              continue
        
            # third stage permutation
            q2 = q1.difference({d, e, f, g})
            for p3 in permutations(q2, 4):
              h, i, j, k = p3
              if not( is_sq(h * i * j * k)):
                continue
              
              # sort factorial products
              T = sorted((a * b * c , d * e * f * g, h * i * j * k))
              if (a * b * c) < (d * e * f * g) and (a * b * c) < (h * i * j * k):
                print(f"Factorials in smallest product are {ND[a]}, {ND[b]} and {ND[c]}.")
                print(f"Smallest factorial product = {a * b * c}")
                timer.stop()
                timer.report()
                # only one solution needed
                exit()
        
        # Factorials in smallest product are F8, F1 and F9.
        # Smallest factorial product = 14631321600
        # [timing] total time: 0.0046427s (4.64ms)
        
        
        
        
        

        Like

    • NigelR's avatar

      NigelR 11:33 pm on 18 August 2022 Permalink | Reply

      [timing] total time: 0.0030880s (3.09ms)

      from itertools import combinations as comb
      from enigma import timer
      timer.start()
      
      def fac(num): #returns num!
          return num*fac(num-1) if num > 1 else 1
      
      def is_square(num): #returns True if num is square
              return round(num ** 0.5) ** 2 == num
      
      def lprod(lst) : #returns product of elements in list
          res = 1
          for x in lst:
               res = res * x
          return res
      
      facs = {i:fac(i) for i in range(1,13)}
      # identify group of i numbers between 1 and 12 where product of their factorial is a square
      group = lambda i:[x for x in comb(facs.keys(),i) if is_square(lprod([facs[y] for y in x]))]
      threes,fours = group(3),group(4)
      #identify group of 3 and two groups of 4 where all 11 elements are different
      valid = [[x,y[0],y[1]] for x in threes for y in comb(fours,2) if len(set(list(x)+[z for sub in y for z in sub]))==11]
      valid = [y for x in valid for y in x] # flatten valid
      prods =[lprod(list(x)) for x in valid] # generate products of factorials in valid group
      res=valid[prods.index(min(prods))] #identify factorials with minimum product
      print('Factorials in group with smallest product are:',*res)
      timer.stop()
      timer.report()
      

      Factorials in group with smallest product are: 1 8 9

      Like

  • Unknown's avatar

    Jim Randell 9:29 am on 16 August 2022 Permalink | Reply
    Tags:   

    Brain-Teaser 732: … a Frenchman, an Italian, … 

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

    The staff of a certain international organisation, in their day-to-day work, use four languages — English, French, German and Italian — and every employee is required to be fluent in two of them, viz. his or her own, and one of the other three.

    The four heads of branches of the Circumlocution Division are of different nationalities; their second languages are also all different. Each has a woman secretary whose second language is the native language of her chief, but in no case is either language of the chief the native language of his secretary.

    All eight are colleagues of mine. John and Mary are English, Jules and Adèle French, Otto and Heidi German, and Nino and Gina Italian.

    The man of the same nationality as John’s secretary has German as his second language.

    The secretary of the man whose native language is Jules’s second language is of the same nationality as Heidi’s chief.

    Who is Jules’s secretary? And which man has Italian as his second language?

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

    [teaser732]

     
    • Jim Randell's avatar

      Jim Randell 9:30 am on 16 August 2022 Permalink | Reply

      This Python program runs in 58ms. (Internal run time is 649µs).

      Run: [ @replit ]

      from enigma import (subsets, update, singleton, map2str, printf)
      
      # languages (English, French, German, Italian)
      langs = ['en', 'fr', 'de', 'it']
      
      # chiefs and secretaries (in language order)
      chiefs = ['John', 'Jules', 'Otto', 'Nino']
      secs = ['Mary', 'Adele', 'Heidi', 'Gina']
      
      # lang1: map names to first languages
      lang1 = dict(zip(chiefs + secs, langs + langs))
      # nat: map first language (nationality) to chief
      nat = dict(zip(langs, chiefs))
      
      # sec: maps chief -> secretary
      # chief: maps secretary -> chief
      for ss in subsets(secs, size=len, select="P"):
        sec = dict(zip(chiefs, ss))
        chief = dict(zip(ss, chiefs))
        # chiefs and secretaries do not share first languages
        if any(lang1[k] == lang1[v] for (k, v) in sec.items()): continue
      
        # lang2: map names to second languages
        # the second language of the secretary is the first language of the chief
        lang2a = dict((v, lang1[k]) for (k, v) in sec.items())
      
        # assign second languages to the chiefs (different from first languages)
        for vs in subsets(langs, size=len, select="D"):
          lang2 = update(lang2a, chiefs, vs)
          # the second language of the chief is not the first language of the secretary
          if any(lang2[k] == lang1[v] for (k, v) in sec.items()): continue
      
          # check conditions:
          # "The man of the same nationality as John's secretary, has German as his second language"
          if not (lang2[nat[lang1[sec['John']]]] == 'de'): continue
      
          # "The secretary of the man whose native language is Jules's
          # second language is of the same nationality as Heidi's chief"
          if not (lang1[sec[nat[lang2['Jules']]]] == lang1[chief['Heidi']]): continue
      
          # output solution(s), and maps
          printf("sec[Jules] = {x}; lang2[{y}] = it", x=sec['Jules'], y=singleton(y for y in chiefs if lang2[y] == 'it'))
          printf("-> sec = {sec}", sec=map2str(sec, sort=0))
          printf("-> lang2 = {lang2}", lang2=map2str(lang2, sort=0))
          printf()
      

      Solution: Mary is Jules’ secretary. John has Italian as a second language.

      The complete assignments are:

      John (English + Italian) & Adèle (French + English)
      Jules (French + German) & Mary (English + French)
      Otto (German + French/English) & Gina (Italian + German)
      Nino (Italian + English/French) & Heidi (German + Italian)

      Otto and Nino’s second languages are English and French in some order.

      Like

    • GeoffR's avatar

      GeoffR 7:05 pm on 16 August 2022 Permalink | Reply

      Circumlocution looks like an interesting reference to Charles Dickens.

      The Circumlocution Office, a fictitious governmental department featured in the Charles Dickens novel Little Dorrit. Dickens took an existing word in the English language, circumlocution, and applied it to satirise that department.

      The word circumlocution describes the use of an unnecessarily amount of words to get to the point, where just a few would do.

      Like

    • NigelR's avatar

      NigelR 1:42 pm on 17 August 2022 Permalink | Reply

      I think I now need a lie down with a cold towel round my head after keeping track of nested dictionaries! For some reason my shell (Thonny) crashes when I run the enigma timer.

      from itertools import permutations as perm
      #timer.start()
      def test():    
          y=[z for z in Chiefs if Chiefs[z][0]==Chiefs['Jules'][1]][0] #Chief with nat of Jules 2nd lang (aka y)
         #y's secretary is [Chiefs[y][2]] and her nationality is Secs[Chiefs[y][2]][0])       
          for x in Chiefs:
              if Secs[Chiefs[x][2]][0]==Chiefs[x][0]:return False #check Sec native lang against chief nat.
              if Secs[Chiefs[x][2]][1]!=Chiefs[x][0]:return False # check Sec 2nd is chief's nat
              if Chiefs[x][0] == Secs[Chiefs[x][2]][0] or Chiefs[x][1] == Secs[Chiefs[x][2]][0]:return False # check neither sec lang is chief's nat
      #The man of the same nationality as John’s secretary has German as his second language
              if Chiefs[x][0]==Secs[Chiefs['John'][2]][0] and Chiefs[x][1]!='Ge':return False 
      #The secretary of the man whose native language is Jules’s second language is of the same nationality as Heidi’s chief
              if Chiefs[x][2]=='Heidi' and Secs[Chiefs[y][2]][0]!=Chiefs[x][0]:return False            
          return True
              
      Chief = ['John','Jules','Otto','Nino']
      Sec = ['Mary','Adele','Heidi','Gina']
      Nat = ['En','Fr','Ge','It']
      Chiefs={Chief[i]:[Nat[i],0,0] for i in range(4)}
      Secs = {Sec[i]:[Nat[i],0] for i in range(4)}
      
      for Clang in perm(Nat): #generate second languages for Chiefs
          for i, x in enumerate(Chiefs):
              Chiefs[x][1] = Clang[i]
          if any([i for i in Chiefs if Chiefs[i][0]==Chiefs[i][1]]):continue # second lang cannot be same as first
          for Slang in perm(Nat): #generate second language for secretaries
              for i, x in enumerate(Secs):
                  Secs[x][1] = Slang[i]
              if any([i for i in Secs if Secs[i][0]==Secs[i][1]]): continue  # second lang cannot be same as first     
              for Sal in perm(Secs):
                  for i , x  in enumerate(Chiefs):
                      Chiefs[x][2] = Sal[i]  #allocate secretaries to chiefs
                  if not test():continue
                  #Who is Jules’s secretary? And which man has Italian as his second language?
                  I2l = [i for i in Chiefs if Chiefs[i][1]=='It'][0]
                  print('Jules secretary is', Chiefs['Jules'][2],'.', I2l, 'has Italian as second language'   )
                  print (Chiefs)
      #timer.stop()
      #timer.report()
      

      Like

    • GeoffR's avatar

      GeoffR 3:27 pm on 17 August 2022 Permalink | Reply

      @NigelR:

      Excellent work.

      I added ‘from enigma import timer’ to your code after the permutation import. (Before timer.start() )

      This worked OK for me in the Idle interface

      [timing] total time: 0.0838237s (83.82ms)

      Like

    • Frits's avatar

      Frits 6:03 pm on 17 August 2022 Permalink | Reply

         
      from itertools import product
      
      # languages (English, French, German, Italian)
      langs = ['en', 'fr', 'de', 'it']
       
      # chiefs and secretaries (in language order)
      chiefs = ['John', 'Jules', 'Otto', 'Nino']
      secs = ['Mary', 'Adele', 'Heidi', 'Gina']
      
      # options per chief
      lst = [[] for _ in range(4)]
      
      # generate all combinations of chief, secretary and chief's second language
      for chf, sec, c_lang_2 in product(*[chiefs, secs, langs]):
        chf_i = chiefs.index(chf)
        c_lang_1 = langs[chf_i]
        s_lang_1, s_lang_2 = langs[secs.index(sec)], c_lang_1
        # in no case is either language of the chief 
        # the native language of his secretary
        if c_lang_1 == c_lang_2 or s_lang_1 == s_lang_2 or \
           s_lang_1 in {c_lang_1, c_lang_2}: continue
        
        lst[chf_i].append([c_lang_1, c_lang_2, sec, s_lang_1, s_lang_2])
      
      # check all combinations of options per chief
      for p in product(*lst):
        # chiefs have different secretaries and ...
        if len(set(chf[2] for chf in p)) != 4: continue
        
        # ... their second languages are also all different
        if len(set(chf[1] for chf in p)) != 4: continue
        
        Jo, Ju, _, _ = p
        
        # the man of the same nationality as 
        # John's secretary has German as his second language
        if [chf for chf in p[1:] if chf[0] == Jo[3]][0][1] != 'de':
          continue
            
        # The secretary of the man whose native language 
        # is Jules's second language is of the same nationality as Heidi's chief
        lang_sec = [chf[3] for i, chf in enumerate(p) 
                   if i != 1 and chf[0] == Ju[1]][0]
        lang_chf_H = [chf[0] for chf in p if chf[2] == 'Heidi'][0]
        if lang_sec != lang_chf_H: continue
        
        for z in zip(chiefs, p):
          print(z)
        print(f"Jules' secretary: {Ju[2]}, John's second language: {Jo[1]}")
        print()
      

      Like

  • Unknown's avatar

    Jim Randell 10:11 am on 14 August 2022 Permalink | Reply
    Tags: by: Richard Diamond   

    Brain-Teaser 46: [Lifts] 

    From The Sunday Times, 4th February 1962 [link]

    Don and Derek live on intermediate floors in neighbouring blocks of flats each provided with a lift. Neither lift is subject to direction change by users and they both travel continuously and steadily from the ground floor to the top floor and back, stopping at each floor, and waiting at the top and bottom twice as long as at other stops. During the year the number of floors in each block is increased, though not necessarily by the same number. Don, who lives on a higher floor than Derek, notices that if his lift is not on his floor, it is twice as likely to be going down when it first comes to him as it used to be. Derek makes a similar observation on his lift.

    The following year each moves four floors up and they each notice that if the lift is not on his floor, it has the same likelihood of going down when it first comes to him as it had originally.

    On which floors do Don and Derek live now?

    This puzzle was originally published with no title.

    [teaser46]

     
    • Jim Randell's avatar

      Jim Randell 10:13 am on 14 August 2022 Permalink | Reply

      If we consider a building with n floors, numbered from 0 to (n − 1), then this is consistent with the way building floors are numbered in the UK (i.e. 0 = ground floor, 1 = first floor, 2 = second floor, etc), assuming there are no basement levels.

      If I live on floor k (0 < k < (n − 1)) of an n storey building, then the lift has 2n states (= each floor × {up, down}), and (2n − 2) are not at my floor. Of these the lift will be travelling up when it arrives only if it is below my floor, i.e. in 2k states.

      So the likelihood of an arriving lift going up is:

      up(n, k) = 2k / (2n − 2) = k / (n − 1)

      And the likelihood of it going down is:

      down(n, k) = ((2n − 2) − 2k) / (2n − 2) = (n − k − 1) / (n − 1)

      This Python program considers building with 3 to 40 floors.

      It runs in 60ms. (Internal runtime is 5.8ms).

      Run: [ @replit ]

      from enigma import (defaultdict, Rational, irange, unpack, subsets, printf)
      
      Q = Rational()
      
      # generate (n, k, m) values
      def generate():
        # record probabilities of lift going down
        # d: down(n, k) -> (n, k)
        d = defaultdict(list)
        # n = floors of the building
        for n in irange(3, 40):
          # consider each floor
          for k in irange(1, n - 1):
            # calculate probability of lift going down
            p = Q(n - k - 1, n - 1)
            # is it the same value as the (k - 4)th floor of a lower building?
            # and half the value as the (k - 4)th floor in this building
            for (n_, k_) in d[p]:
              if k == k_ + 4 and n_ < n and (n, k_) in d[p * 2]:
                yield (n_, k_, n)
            # record this value
            d[p].append((n, k))
      
      # collect solution values sorted by k
      ss = sorted(generate(), key=unpack(lambda n, k, m: k))
      
      # choose values for Derek (lower floor) and Don (higher floor)
      for vs in subsets(ss, size=2):
        for (t, (n, k, m)) in zip(["Derek", "Don"], vs):
          printf("{t}: floor {k} of {n} -> floor {k} of {m} -> floor {k4} of {m}", k4=k + 4)
        printf()
      

      Solution: Derek lives on the eighth floor of his building. Don lives on the sixteenth floor of his building.


      Derek starts on floor 4 of 7 (numbered 0 to 6). There are 4 lift states above him and 8 lift states below him, so the probability of waiting for a down lift is 4/12 = 1/3.

      The building is then expanded to 13 floors, which adds 12 lift states above him, so the probability of waiting for a down lift is now 16/24 = 2/3.

      So the probability of a down lift is twice what it was before.

      Derek then moves up 4 floors to floor 8 of 13. There are 8 lift states above him and 16 lift states below him, so the probability of waiting for a down lift is now 8/24 = 1/3, the same as it was originally.

      And Don starts on floor 12 of 16. There are 6 lift states above him, and 24 lift states below him, so the probability of waiting for a down lift is 6/30 = 1/5.

      The building is then expanded to 21 floors, which adds 10 lift states above him, so the probability of waiting for a down lift is now 16/40 = 2/5.

      Don then moves up 4 floors to floor 16 of 21. There are 8 lift states above him and 32 lift states below him, so the probability of waiting for a down lift is 8/40 = 1/5.

      Like

      • Jim Randell's avatar

        Jim Randell 10:16 am on 14 August 2022 Permalink | Reply

        With a bit more analysis:

        When the building is extended to m floors, we are interested in situations where:

        down(m, k) = 2 × down(n, k)

        and also:

        down(m, k + 4) = down(n, k)

        which give:

        down(m, k) = 2 × down(m, k + 4)
        (m − k − 1) / (m − 1) = 2 (m − (k + 4) − 1) / (m − 1)
        ⇒ m = k + 9

        down(m, k) = 2 × down(n, k)
        ⇒ n = (k² + 9k + 4) / (k + 4)
        ⇒ n = (k + 5) − 16 / (k + 4)

        For integer values of n we need (k + 4) to be a divisor of 16.

        (k + 4) = {1, 2, 4, 8, 16}
        ⇒ k = {4, 12}

        So, Derek starts on floor 4 of (4 + 5 − 16/8) = 7, then floor 4 of (4 + 9) = 13, then floor (4 + 4) = 8 of 13.

        And, Don starts on floor 12 of (12 + 5 − 16/16) = 16, then floor 12 of (12 + 9) = 21, then floor (12 + 4) = 16 of 21.

        This Python program runs in 55ms. (Internal run time is 152µs).

        Run: [ @replit ]

        from enigma import (divisors_pairs, unpack, subsets, printf)
        
        # generate (n, k, m) values
        def generate():
          for (k4, x) in divisors_pairs(16, every=1):
            k = k4 - 4
            if k > 0:
              yield (k + 5 - x, k, k + 9)
        
        # collect solution values sorted by k
        ss = sorted(generate(), key=unpack(lambda n, k, m: k))
        
        # choose values for Derek (lower floor) and Don (higher floor)
        for vs in subsets(ss, size=2):
          for (t, (n, k, m)) in zip(["Derek", "Don"], vs):
            printf("{t}: floor {k} of {n} -> floor {k} of {m} -> floor {k4} of {m}", k4=k + 4)
          printf()
        

        Like

    • NigelR's avatar

      NigelR 5:41 pm on 16 August 2022 Permalink | Reply

      I took a slightly different approach but reached (I think) the same conclusion. I considered the time that the lift took from doors closing to doors opening going in each direction. The double wait at top and bottom effectively offsets the ‘dead’ time while it is at floor n and makes the maths a lot easier! Going down, the lift takes 2nt before the doors open again, where t is the combined transition and wait time for each floor. Going up, the lift takes 2(f-n)t, where f is the number of floors. Hence the probability that the lift when not visible is above floor k (ie it will arrive going downwards) is: (2(f-n)t)/2ft or (f-n)/f.
      Brute force:

      pdown = lambda n,f: (f-n)/f  #probability that a lift is going down on arrival at floor n of a block of height f        
      sol=[[f,n,d] for f in range(50) for n in range(1,f) for d in range(1,20) if 2*pdown(n,f) == pdown(n,f+d) and pdown(n,f) == pdown(n+4,f+d)]
      if len(sol) == 2: print('Don now lives on floor',sol[1][1]+4 ,'of his block and Derek is on floor',sol[0][1]+4,'of the other' )
      else:print('no unique solution')
      

      By analysis, for each block with d floors added we have (f-n)/f = ((f+d)-(n+4))/(f+d) ⇒ nd=4f … (1)
      We also have 2((f-n)/f) = (f+d-n)/(f+d), which ⇒ f² -fn +fd -2nd=0 … (2)
      Combining (1) and (2) we get: f+d-n=8
      Hence:

      sol=[[f,n,(8+n-f)] for f in range(50) for n in range(1,f) if (4*f)%n==0 and int((4*f)/n)==8+n-f]
      if len(sol) == 2: print('Don now lives on floor',sol[1][1]+4 ,'of his block and Derek is on floor',sol[0][1]+4,'of the other' )
      else:print('no unique solution')
      

      [timing] total time: 0.0005922s (592.20us)

      Like

  • Unknown's avatar

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

    Teaser 3125: The bearings’ trait 

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

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

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

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

    Give the three bearings, in ascending order.

    [teaser3125]

     
    • Jim Randell's avatar

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

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

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

      It runs in 86ms.

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

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


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

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

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

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

      Like

      • Jim Randell's avatar

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

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

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

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

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

        Like

    • GeoffR's avatar

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

      Similar method to Jim’s first solution.

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

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

      Like

    • Frits's avatar

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

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

      Like

      • Frits's avatar

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

        Inspired by Nigel.

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

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

        Like

        • Jim Randell's avatar

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

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

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

          Like

    • Frits's avatar

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

      Too hot to go outside.

      Bringing down the number of viable bearings down to 15.

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

      Like

    • GeoffR's avatar

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

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

      Like

    • NigelR's avatar

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

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

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

      Like

      • Frits's avatar

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

        Well done.

        Using

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

        it is even more efficient.

        Like

        • NigelR's avatar

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

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

          Like

  • Unknown's avatar

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

    Teaser 2727: Happy New Year 

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

    I have written down five positive whole numbers whose sum is a palindromic number. Furthermore, the largest of the five numbers is the sum of the other four. I have reproduced the five numbers below, but have consistently replaced digits by letters, with different letters used for different digits.

    We are about to enter an odd-numbered new year and so it is appropriate that my numbers have become:

    A
    VERY
    HAPPY
    NEW
    YEAR

    What is the odd-numbered YEAR?

    This puzzle was not included in the book The Sunday Times Brain Teasers Book 1 (2019).

    [teaser2727]

     
    • Jim Randell's avatar

      Jim Randell 7:29 am on 11 August 2022 Permalink | Reply

      The largest of the numbers is HAPPY, so we have:

      A + VERY + NEW + YEAR = HAPPY

      And also the sum of all 5 numbers (= 2 × HAPPY) is palindromic.

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

      The following run file executes in 89ms. (The internal run time of the generated program is 22.8ms).

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      # sum is palindromic
      "is_npalindrome(2 * HAPPY)"
      
      # largest number (HAPPY) is the sum of the other four
      "A + VERY + NEW + YEAR = HAPPY"
      
      # YEAR is odd
      "R % 2 = 1"
      
      --answer="YEAR"
      

      Solution: YEAR = 6925.

      Like

    • GeoffR's avatar

      GeoffR 11:01 am on 11 August 2022 Permalink | Reply

      % A Solution in MiniZinc
      include "globals.mzn";
      
      set of int: INT = 1..9;
      var INT: A; var INT: V; var INT: E;
      var INT: R; var INT: Y; var INT: N;
      var INT: W; var INT: H; var INT: P;
      
      constraint all_different([A, V, E, R, Y, N, W, H, P]);
      
      var 1000..9999:VERY = 1000*V + 100*E + 10*R + Y;
      var 1000..9999:YEAR = 1000*Y + 100*E + 10*A + R;
      var 100..999:NEW = 100*N + 10*E + W;
      var 10000..99999:HAPPY = 10000*H + 1000*A + 110*P + Y;
      var 10000..99999:HAPPY2;  % NEW + YEAR + VERY < 21000
      
      constraint YEAR mod 2 == 1;
      constraint A + VERY + NEW + YEAR == HAPPY;
      constraint HAPPY2 == 2 * HAPPY;
      
      % check 2 * HAPPY is a 5-digit palindromic number
      % 1st and 5th digits are the same
      constraint HAPPY2 div 10000 == HAPPY2 mod 10
      % 4th and 2nd digits are the same
      /\ HAPPY2 div 1000 mod 10 == HAPPY2 div 10 mod 10;
      
      solve satisfy;
      
      output ["YEAR = " ++ show(YEAR) ++ ", HAPPY2 = " ++ show(HAPPY2)
      ++"\n[A,V,E,R,Y,N,W,H,P] = " ++ show( [A,V,E,R,Y,N,W,H,P]  )];
      
      % YEAR = 6925, HAPPY2 = 25552
      % [A, V, E, R, Y, N, W, H, P] = 
      % [2, 4, 9, 5, 6, 8, 3, 1, 7]
      % ----------
      % ==========
      
      
      
      

      Like

    • Frits's avatar

      Frits 12:47 pm on 11 August 2022 Permalink | Reply

      Only loop over A and R.

         
      from enigma import SubstitutedExpression
      
      # invalid digit / symbol assignments
      d2i = dict()
      for d in range(0, 10):
        vs = set()
        # starting letters
        if d == 0: vs.update('AVN')
        
        # exclude s2d values H and Y
        if d in {1, 6}: vs.update('AENPRVW')
        
        # 2 * A < 10
        if d > 4: vs.update('A')
        # YEAR is odd
        if d % 2 == 0: vs.update('R')
        
        d2i[d] = vs
      
      
      # the alphametic puzzle
      p = SubstitutedExpression(
        [
          # 2 * HAPPY is not a 6-digit number, it's last digit must be even (not 1)
          # so 2 * HAPPY == KLMLK
          
          # L must be odd as 2 * Y > 9 and 2 * P is even
          "2 * A + 1 = L",
          
          # P > 4 as 2 * A + 1 = L (carry over from middle column)
          
          # (2 * P + 1) % 10 = L and P > 4 so 2 * P + 1 - 10 = L  (4th column)
          "div(L + 9, 2) = P", 
          # (2 * P + 1) % 10 = M and P > 4 so 2 * P + 1 - 10 = M  (middle column)
          # so M = L
          "L = M",
          "2 * HAPPY == KLMLK",
         
          # rewrite (R + A + W) % 10 = 0 so we have a formula for W
          "10 - (R + A) % 10 = W", 
       
          # 10 * HAPP - 10 * VER - 10 * NE - 10 * YEA - (R + A + W) = 0
          # tens: (P - R - E - A - (R + A + W) / 10) % 10 = 0
          "(30 + P - R - A - (R + A + W) // 10) % 10 = E",
          
          "HAPPY - A - NEW - YEAR = VERY",
        ],
        answer="YEAR",
        d2i=d2i,
        # A + VERY + NEW + YEAR = HAPPY --> H = 1 as 9xxx + 8xxx is not high enough
        # 2 * H = K and (2 * Y) % 10 = K --> Y = H + 5
        s2d=dict(H=1, Y=6, K=2),
        # exclude the s2d entries from the distinct parameter
        # they are handled more cleanly/efficiently by adding them to d2i
        distinct="AENPRVW",
        verbose=0,    # use 256 to see the generated code
      )
      
      # print answers
      for (_, ans) in p.solve():
        print(f"year = {ans}")
      

      Jim, might it be an idea to internally remove the s2d symbols from distinct and add their values internally to d2i?

      Like

    • GeoffR's avatar

      GeoffR 3:23 pm on 11 August 2022 Permalink | Reply

      A full permutation solution was quicker than expected.

      
      from itertools import permutations
      from enigma import is_palindrome as is_pal, timer
      
      timer.start() 
      for p1 in permutations ('123456789'):
          a, v, e, r, y, n, w, h, p = p1
          year = int(y + e + a + r)
          if year % 2 != 1: continue
          new = int(n + e + w)
          very = int(v + e + r + y)
          happy = int(h + a + p + p + y)
      
          # check happy * 2 is a palindrome
          if is_pal(str(2 * happy)):
              # the alphametic sum
              if int(a) + new + very + year == happy:       
                 print(f"YEAR = {year}, Palindrome = {2 * happy}")
                 timer.stop()
                 timer.report()
      
      # YEAR = 6925, Palindrome = 25552
      # [timing] total time: 0.1265317s (126.53ms)
      
      

      Like

  • Unknown's avatar

    Jim Randell 7:35 am on 9 August 2022 Permalink | Reply
    Tags:   

    Teaser 2733: Letter-writing 

    From The Sunday Times, 8th February 2015 [link] [link]

    Last year I went to calligraphy lessons. They were held weekly, on the same day each week, for nine consecutive months. Actually I only went to 15 of the lessons, and after the course was over I listed the dates of those lessons that I had attended. In order to practise my new skills I wrote the dates in words (in the format “First of January” etc.) and I found to my surprise that each date used a different number of letters.

    What were the dates of the first and last lessons that I attended?

    [teaser2733]

     
    • Jim Randell's avatar

      Jim Randell 7:36 am on 9 August 2022 Permalink | Reply

      See also: Teaser 2832.

      The lessons were for “9 months”, so let’s suppose there were 39 weekly lessons, of which the setter attended 15.

      This Python program runs in 68ms.

      Run: [ @replit ]

      from datetime import (date, timedelta)
      from enigma import (repeat, trim, int2words, group, subsets, cache, sprintf as f)
      
      # map month numbers to English names
      month = dict(enumerate([
          'january', 'february', 'march', 'april', 'may', 'june',
          'july', 'august', 'september', 'october', 'november', 'december',
        ], start=1)
      )
      
      # ordinals that aren't cardinal + "th", or cardinal - "y" + "ieth"
      _ordinal = {
        1: 'first', 2: 'second', 3: 'third', 5: 'fifth',
        8: 'eighth', 9: 'ninth', 12: 'twelfth',
      }
      
      # return the ordinal of a number
      @cache
      def ordinal(n):
        if n in _ordinal: return _ordinal[n]
        if n < 20: return int2words(n) + 'th'
        (t, r) = divmod(n, 10)
        if r == 0: return trim(int2words(n), tail=1) + 'ieth'
        return int2words(n - r) + ' ' + ordinal(r)
      
      # return the length of the inscription for a date
      @cache
      def inscribe(d):
        t = f("{d} of {m}", d=ordinal(d.day), m=month[d.month])
        return sum(x.isalpha() for x in t)
      
      # generate length <k> sequences of possible dates
      def generate(k):
        (i1, i7) = (timedelta(days=1), timedelta(days=7))
        d = date(2014, 1, 1)
        while True:
          # generate 39 weeks of dates
          ds = list(repeat((lambda d: d + i7), d, k - 1))
          # are we done?
          if ds[-1].year > 2014: break
          yield ds
          # increase start date
          d += i1
      
      # record possible date spans
      rs = set()
      
      # consider possible dates for 39 weekly lessons
      for ds in generate(39):
        # group dates by letter count
        g = group(ds, by=(lambda d: inscribe(d)))
        # choose 15 letter counts
        for ks in subsets(g.keys(), size=15):
          # find earliest and latest possible dates
          dmin = min(min(g[k] for k in ks))
          dmax = max(max(g[k] for k in ks))
          rs.add((dmin, dmax))
      
      # output solution(s)
      for (dmin, dmax) in rs:
        print(f("{dmin} -> {dmax}"))
      

      Solution: The first lesson attended was on the 5th April. The final lesson attended was on 27th December.


      In fact there is only one sequence of 39 weekly dates in 2014 that gives at least 15 different lengths:

      10 letters: “Third of May”; “Tenth of May”
      11 letters: “Fifth of July”
      12 letters: “Fifth of April”
      13 letters: “Seventh of June”; “Twelfth of July”; “Ninth of August”
      14 letters: “Twelfth of April”; “Second of August”
      15 letters: “Fourth of October”; “First of November”; “Sixth of December”
      16 letters: “Seventeenth of May”; “Thirty first of May”; “Fourteenth of June”; “Nineteenth of July”; “Sixth of September”; “Eighth of November”
      17 letters: “Nineteenth of April”; “Twenty fourth of May”; “Twenty first of June”; “Twenty sixth of July”; “Sixteenth of August”; “Thirtieth of August”; “Eleventh of October”
      18 letters: “Twenty eighth of June”
      19 letters: “Twenty third of August”; “Eighteenth of October”; “Fifteenth of November”; “Twentieth of December”
      20 letters: “Twentieth of September”; “Twenty fifth of October”; “Thirteenth of December”
      21 letters: “Thirteenth of September”; “Twenty ninth of November”
      22 letters: “Twenty second of November”
      23 letters: “Twenty seventh of December”
      24 letters: “Twenty seventh of September”

      And this gives exactly 15 different lengths of inscription. So, each length must be represented by a single date that the setter attended.

      The period covered is 5th April – 27th December, and as 5th April is the only date with an inscription of 12 letters, and 27th December is the only date with an inscription of 23 letters, the setter must have attended the actual first and last lessons, and these give the answer to the puzzle.

      Like

  • Unknown's avatar

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

    Teaser 3124: Lawn order 

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

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

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

    [teaser3124]

     
    • Jim Randell's avatar

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

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

      The following program runs in 399ms.

      Run: [ @replit ]

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

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


      The planned layout looks like this:

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

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

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

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

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

      Liked by 2 people

      • Jim Randell's avatar

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

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

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

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

        We then proceed as described above.

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

        Run: [ @replit ]

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

        Like

        • Frits's avatar

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

          @Jim,

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

          Test:

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

          Do set parameters always have a global scope?

          Like

          • Jim Randell's avatar

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

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

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

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

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

            Like

            • Frits's avatar

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

              Thanks.

              Like

            • Jim Randell's avatar

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

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

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

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

              This is very similar to just using a global variable:

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

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

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

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

              Like

        • Frits's avatar

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

          @Jim,

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

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

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

          Like

          • Jim Randell's avatar

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

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

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

            Like

            • Frits's avatar

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

              Now you process the expected 80 circuits.

              Like

    • Frits's avatar

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

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

      Uncomment the four plot related lines for displaying the polygon.

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

      Liked by 1 person

      • Jim Randell's avatar

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

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

        Like

        • Frits's avatar

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

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

          Like

    • Frits's avatar

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

      Code to print polygons with 90 degree angles.

      (I have problems installing matplotlib under PyPy)

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

      Like

      • Jim Randell's avatar

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

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

        I have uploaded it to GitHub. [@github]

        Like

  • Unknown's avatar

    Jim Randell 8:26 am on 4 August 2022 Permalink | Reply
    Tags:   

    Teaser 2725: Darts match 

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

    Andrew, Alexander, Austin, Anthony, Benjamin, Charles, Christopher, Elijah, Jacob, Jayden, Jackson, James, Jason, Mason, Michael, Nathan, Newman, Robert, Samuel and William entered a darts competition, arranged into five teams of four players. It turned out that, for any pair of members of any team, there were just two letters of the alphabet that occurred (once or more) in both their names. The names in each team were arranged alphabetically, the first name being the captain and the last name the reserve. Then the teams were numbered 1 to 5 in alphabetical order of the captains.

    In the order 1 to 5, who were the reserves?

    [teaser2725]

     
    • Jim Randell's avatar

      Jim Randell 8:27 am on 4 August 2022 Permalink | Reply

      This puzzle is slightly different from the usual kind of grouping puzzle, but we can still use some of the [[ grouping ]] functions from the enigma.py library.

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

      Run: [ @replit ]

      from enigma import (grouping, join, printf)
      
      # available players
      names = [
        'Andrew', 'Alexander', 'Austin', 'Anthony', 'Benjamin', 'Charles',
        'Christopher', 'Elijah', 'Jacob', 'Jayden', 'Jackson', 'James', 'Jason',
        'Mason', 'Michael', 'Nathan', 'Newman', 'Robert', 'Samuel', 'William',
      ]
      
      # grouping function
      fn = grouping.share_letters(2)
      
      # form the names <ns> into teams of size <k>
      def teams(ns, k, ts=[]):
        # are we done?
        if not ns:
          yield ts
        else:
          # find the next captain
          cap = min(ns)
          # and 3 team members
          for team in grouping.gang(k - 1, cap, ns.difference([cap]), fn):
            team = [cap] + sorted(team)
            # solve for the remaining names
            yield from teams(ns.difference(team), k, ts + [team])
      
      for ts in teams(set(names), 4):
        for (i, team) in enumerate(ts, start=1):
          printf("Team {i}: {team}", team=join(team, sep=", "))
        printf()
      

      Solution: The reserves are (Team 1-5): William, Samuel, Robert, Newman, Michael.

      The full teams are:

      Team 1: Alexander, Austin, James, William
      Team 2: Andrew, Christopher, Jason, Samuel
      Team 3: Anthony, Benjamin, Charles, Robert
      Team 4: Elijah, Jackson, Nathan, Newman
      Team 5: Jacob, Jayden, Mason, Michael

      Like

    • Frits's avatar

      Frits 8:04 pm on 5 August 2022 Permalink | Reply

      A lot more work but already solved before recursion (ie parameter teams will have 5 teams) .

         
      from itertools import combinations
      
      # check that each string shares exactly <k> letters
      shr_lttrs = lambda s1, s2, k: len(set(s1.lower()) & set(s2.lower())) == k
      
      # form the names <ns> into teams of size <k>
      def form_teams(ns, k, ts=[]):
        # are we done?
        if not ns:
          yield ts
        else:
          # find the name with the least matches
          key = min({k: v for k, v in d.items() if k in ns}.items(), 
                     key=lambda x: len(x[1]))
          # and 3 team members
          for rest in combinations([x for x in key[1] if x in ns], 3):
            # these 3 names should have exactly 2 letters in common
            if any(not y in d[x] for x, y in zip(rest, rest[1:])): continue
            
            team = sorted((key[0], ) + rest)
            # solve for the remaining names
            yield from form_teams(ns.difference(team), k, ts + [team])
            
      
      # available players
      vs = [
        'Andrew', 'Alexander', 'Austin', 'Anthony', 'Benjamin', 'Charles',
        'Christopher', 'Elijah', 'Jacob', 'Jayden', 'Jackson', 'James', 'Jason',
        'Mason', 'Michael', 'Nathan', 'Newman', 'Robert', 'Samuel', 'William',
      ]
      
      vs = sorted(vs)
      
      # create dictionary: name --> matches
      d = {vs[i]: {x for j, x in enumerate(vs[:i] + vs[i + 1:]) 
                       if shr_lttrs(vs[i], x, 2)} for i in range(len(vs))}
      
      teams = []
      dsize = -1
      # process until no more changes to dictionary
      while dsize != sum(len(v) for v in d.values()):
        dsize = sum(len(v) for v in d.values())
        
        # if a match doesn't share exactly 2 letters with at least 2 other matches
        # it can be removed from the dictionary as we we need 4 members in a team
        d = {k: {x for x in v if sum(x != y and y in d[x] for y in v) > 1} 
                 for k, v in d.items()}
       
        # if only three matches left we have finalized a team 
        match3 = [x for k, v in list(d.items()) 
                  for x in [k] + list(v) if len(v) == 3]
        
        # add these persons to teams (removing duplicates while preserving order)
        teams += list(dict.fromkeys(match3))
       
        # maintain dictionary for persons in teams
        d = {k: [x for x in v if x not in teams] 
                 for k, v in d.items() if k not in teams}
               
      # reduce values for persons in teams
      vs = [x for x in vs if x not in teams]
      
      # solve for remaining people
      for ts in form_teams(set(vs), 4):
        i = 0
        for (i, team) in enumerate(ts, start=1):
          print(f"Team {i}: {', '.join(team)}")
        for j in range(len(teams) // 4):
          i += 1
          print(f"Team {i}: {', '.join(sorted(teams[j * 4:j * 4 + 4]))}")
        print()
      

      Like

  • Unknown's avatar

    Jim Randell 7:58 am on 2 August 2022 Permalink | Reply
    Tags:   

    Teaser 2534: Fiftieth anniversary 

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

    We have recently celebrated the 50th anniversary of the Teaser column. At the party for the setters they gave each letter of the alphabet a different number from 1 to 26 (e.g. they made A = 7). Appropriately, this was done in such a way that, for each setter present, the values of the letters of their surname added up to 50. Angela Newing was there (so N+E+W+I+N+G = 50), as were Nick MacKinnon and Hugh Bradley. Only two of Graham Smithers, Danny Roth, Andrew Skidmore, John Owen and Victor Bryant could make it.

    Which two?

    [teaser2533]

     
    • Jim Randell's avatar

      Jim Randell 7:59 am on 2 August 2022 Permalink | Reply

      (See also: Teaser 2884).

      With a bit of analysis we can determine the required answer, and write a program to give us a viable assignment is a reasonable amount of time.

      We are given:

      A = 7
      NEWING = 50
      MACKINNON = 50
      BRADLEY = 50

      Hence:

      3N + CIKMO = 43
      BDELRY = 43
      ⇒ 3N + BCDEIKLMORY = 86

      And BCDEIKLMORY (11 letters) must be at least 71, so:

      3N ≤ 15
      N ≤ 5

      But if N is in (1, 2, 3, 4, 5), the minimal value of BCDEIKLMORY is increased to 83, so:

      3N ≤ 3
      N = 1
      BCDEIKLMORY = 83

      So the letters BCDEIKLMORY are assigned (2, 3, 4, 5, 6, 8, 9, 10, 11, 12, 13) (in some order).

      Now, all values from 1 – 13 are accounted for, leaving GHSTW to be assigned values from 14 – 26.

      SMITHERS = 2S + TH + EIMR ≥ 73

      So SMITHERS cannot be 50.

      SKIDMORE = S + IKMO + EDR = S + (40 – C) + (43 – BLY) ≥ 51

      So SKIDMORE cannot be 50.

      OWEN = 50 ⇒ O = ING = IG + 1

      But O must have a value ≤ 13 and G must have a value ≥ 14.

      So OWEN cannot be 50.

      Hence the only remaining candidates are ROTH and BRYANT.

      All that remains is to find a viable assignment of letters to values to show the puzzle has a solution.

      And we can use the [[ SubstitutedExpression ]] solver from the enigma.py library to do that.

      The following run file executes in 66ms. (The internal run time of the generated program is 48µs).

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      --base="27"
      --digits="1-26"
      
      # fixed values
      --assign="A,7"
      --assign="N,1"
      
      # limits on other values
      --invalid="1|2|3|4|5|6|7|8|9|10|11|12|13,GHSTW"
      --invalid="14|15|16|17|18|19|20|21|22|23|24|25|26,BCDEIKLMORY"
      
      # these sum to 50
      "N + E + W + I + N + G == 50"
      "M + A + C + K + I + N + N + O + N == 50"
      "B + R + A + D + L + E + Y == 50"
      "R + O + T + H == 50"
      "B + R + Y + A + N + T == 50"
      
      # these don't
      "S + M + I + T + H + E + R + S != 50"
      "S + K + I + D + M + O + R + E != 50"
      "O + W + E + N != 50"
      
      --template=""
      --first  # we only need a single solution
      

      Solution: The other two setters were: Danny ROTH and Victor BRYANT.


      Here is one possible assignment found by the run file:

      A = 7
      B = 9
      C = 12
      D = 4
      E = 3
      G = 26
      H = 25
      I = 5
      K = 13
      L = 11
      M = 8
      N = 1
      O = 2
      R = 6
      S = 15
      T = 17
      W = 14
      Y = 10
      (F J P Q U V X Z) = (16, 18, 19, 20, 21, 22, 23, 24)

      Using these values we get:

      NEWING = 1 + 3 + 14 + 5 + 1 + 26 = 50
      MACKINNON = 8 + 7 + 12 + 13 + 5 + 1 + 1 + 2 + 1 = 50
      BRADLEY = 9 + 6 + 7 + 4 + 11 + 3 + 10 = 50
      ROTH = 6 + 2 + 17 + 25 = 50
      BRYANT = 9 + 6 + 10 + 7 + 1 + 17 = 50

      SMITHERS = 15 + 8 + 5 + 17 + 25 + 3 + 6 + 15 = 94
      SKIDMORE = 15 + 13 + 5 + 4 + 8 + 2 + 6 + 3 = 56
      OWEN = 2 + 14 + 3 + 1 = 20

      Without the [[ --first ]] parameter we find there are 33,182,352 possible assignments.

      Like

    • GeoffR's avatar

      GeoffR 9:59 am on 2 August 2022 Permalink | Reply

      
      % A Solution in MiniZinc
      include "globals.mzn";
      
      var 1..26:A;   var 1..26:B;   var 1..26:C;   var 1..26:D;
      var 1..26:E;   var 1..26:F;   var 1..26:G;   var 1..26:H;   
      var 1..26:I;   var 1..26:J;   var 1..26:K;   var 1..26:L;
      var 1..26:M;   var 1..26:N;   var 1..26: O;  var 1..26:P;   
      var 1..26:Q;   var 1..26:R;   var 1..26:S;   var 1..26:T;   
      var 1..26:U;   var 1..26:V;   var 1..26:W;   var 1..26:X;   
      var 1..26:Y;   var 1..26:Z;
      
      constraint all_different ([A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,
      P,Q,R,S,T,U,V,W,X,Y,Z]);
      
      constraint A == 7;
      
      % Letters of following names sum to 50
      constraint N+E+W+I+N+G = 50;
      constraint M+A+C+K+I+N+N+O+N == 50;
      constraint B+R+A+D+L+E+Y == 50;
      
      % Letters of only two of Smithers, Roth, Skidmore,
      % Owen and Bryant, sum to 50
      constraint sum([ S+M+I+T+H+E+R+S == 50, R+O+T+H == 50,
      S+K+I+D+M+O+R+E == 50, O+W+E+N == 50, B+R+Y+A+N+T == 50]) == 2;
      
      solve satisfy;
      
      output [ "SMITHERS = " ++ show(S+M+I+T+H+E+R+S) ++ "\n"
      ++ "ROTH = " ++ show(R+O+T+H) ++ "\n"
      ++ "SKIDMORE = " ++ show(S+K+I+D+M+O+R+E) ++ "\n"
      ++ "OWEN = " ++ show(O+W+E+N) ++ "\n"
      ++ "BRYANT = " ++ show(B+R+Y+A+N+T) ];
      
      % Typical solution of multiple solutions
      % SMITHERS = 90
      % ROTH = 50  <<<
      % SKIDMORE = 56
      % OWEN = 22
      % BRYANT = 50  <<<
      % ----------
      % Only ROTH and BRYANT sum to 50 for 5 extra names.
      
      

      Like

  • 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.

      Run: [ @replit ]

      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

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