Updates from Jim Randell Toggle Comment Threads | Keyboard Shortcuts

  • Unknown's avatar

    Jim Randell 2:29 pm on 26 November 2021 Permalink | Reply
    Tags:   

    Teaser 3088: Bog standard deviation 

    From The Sunday Times, 28th November 2021 [link] [link]

     

    My four toilet rolls had zigzag-cut remnants (not unlike that shown). Curiously, each polygonal remnant had a different two-figure prime number of sides, each with a digit sum itself prime.

    Calculating the average number of sides for the four remnants and the average of their squares, I found that the average of the squares minus the square of the average had a value whose square root (the “standard deviation”) was a whole number.

    I also noticed that a regular convex polygon with a number of sides equal to the average number of sides would have an odd whole-number internal angle (in degrees).

    Give the “standard deviation”.

    [teaser3088]

     
    • Jim Randell's avatar

      Jim Randell 2:45 pm on 26 November 2021 Permalink | Reply

      A bit of a convoluted scenario, but the calculations are fairly straightforward.

      The average number of sides must be an integer to construct the polygon, and so the average of the squares must also be an integer, and so must the internal angle. So we can keep everything in the integers.

      This Python program runs in 46ms.

      Run: [ @replit ]

      from enigma import Primes, dsum, subsets, div, sq, is_square, printf
      
      primes = Primes(100)
      
      # candidate primes
      ps = list(p for p in primes.irange(10, 99) if dsum(p) in primes)
      
      # choose 4 of them
      for ns in subsets(ps, size=4):
        # calculate the mean, and mean square
        m = div(sum(ns), 4)
        m2 = div(sum(sq(n) for n in ns), 4)
        if m is None or m2 is None: continue
        # standard deviation
        sd = is_square(m2 - sq(m))
        if sd is None: continue
        # internal angle
        a = div(360, m)
        if a is None or a % 2 == 0: continue
      
        # output solution
        printf("{ns}; m={m} m2={m2} sd={sd} a={a}", a=180 - a)
      

      Solution: The “standard deviation” is 15.

      The prime numbers are: 23, 29, 47, 61.

      Which give a mean of 40, and a mean square of 1825. And a regular 40-sided polygon has internal angles of 171°.

      Without the regular polygon condition there are three potential integer solutions:

      (11, 29, 61, 67); m=42 sd=23
      (23, 29, 47, 61); m=40 sd=15
      (29, 43, 61, 67); m=50 sd=15

      A 42-gon and 50-gon don’t have whole number internal angles, so the requirement for the angle to be odd isn’t necessary (although it may help in a manual solution).

      Like

    • Frits's avatar

      Frits 5:44 pm on 26 November 2021 Permalink | Reply

      Starting from the (odd, even) divisor pairs of number 360.

        
      from enigma import divisor_pairs, dsum, is_square
       
      P = {3, 5, 7}
      P |= {2} | {x for x in range(11, 100, 2) if all(x % p for p in P)}
       
      # candidate primes
      P = sorted(p for p in P if p > 9 and dsum(p) in P)
      
      # get_standard_deviation by decomposing number <t> into <k> increasing 
      # numbers from <ns> so that sum(<k> numbers) equals <t>
      def get_standard_deviation(t, k, ns, sq_avg, s=[]):
        if k == 1:
          if t in ns and t > s[-1]:
            # average of the squares minus the square of the average is square
            sd = is_square(sum(x**2 for x in s + [t]) // 4 - sq_avg)
            if sd is not None:
              yield sd
        else:
          for n in ns:
            if s and n <= s[-1]: continue
            yield from get_standard_deviation(t - n, k - 1, ns, sq_avg, s + [n])
      
      min_avg = sum(P[:4]) / 4
      max_avg = sum(P[-4:]) / 4
      penmax_avg = sum([P[-5]] + P[-3:]) / 4   # penultimate maximum
      
      # determine number of sides for all possible angles
      lst_nsides = [nsides for a, nsides in divisor_pairs(360) 
                    if a % 2 and min_avg <= nsides <= max_avg]
      
      # skip smallest angle if number of sides is too close to max_avg
      if penmax_avg < lst_nsides[0] < max_avg: 
        lst_nsides = lst_nsides[1:]
      
      # solve for all possible angles
      for avg in lst_nsides:
        for sd in get_standard_deviation(4 * avg, 4, P, avg**2):
          print("answer:", sd)
      

      Like

  • Unknown's avatar

    Jim Randell 10:22 am on 25 November 2021 Permalink | Reply
    Tags: ,   

    Teaser 2839: Unwholesome 

    From The Sunday Times, 19th February 2017 [link] [link]

    I have written down three positive numbers, the highest of which is an even two-figure number. Also, one of the numbers is the average of the other two. I have calculated the product of the three numbers and the answer is a prime number.

    You might be surprised that the product of three numbers is a prime but, of course, they are not all whole numbers — at least one of them is a fraction.

    What is the largest of the three numbers, and what is the product of the three?

    As presented there are 2 solutions to this puzzle.

    [teaser2839]

     
    • Jim Randell's avatar

      Jim Randell 10:23 am on 25 November 2021 Permalink | Reply

      The largest number is a 2-digit even integer, say 2n.

      And say the smallest number is the fraction p/q (where p and q are co-prime).

      The middle number is the arithmetic mean of these = (p + 2nq)/2q.

      The product is then: p (p + 2nq) n / q².

      And this is an integer. No non-trivial divisor of q can divide into p or (p + 2nq), so n must be divisible by q².

      Furthermore for the product to a prime, we must have: q² = n, and p = 1.

      Leaving the prime product as: (2q³ + 1).

      And the numbers as: a = 1/q; b = (2q³ + 1)/2q; c = 2q².

      We can solve this manually (remembering c is a 2 digit number):

      q=2: c = 8 [too small]
      q=3: c = 18; product = 55 [not prime]
      q=4: c = 32; product = 129 [not prime]
      q=5: c = 50; product = 251 [*** SOLUTION ***]
      q=6: c = 72; product = 433 [*** SOLUTION ***]
      q=7: c = 98; product = 687 [not prime]
      q=8: c = 128 [too big]

      or programmatically:

      Run: [ @replit ]

      from enigma import (irange, inf, is_prime, printf)
      
      for q in irange(2, inf):
        # check c is 2 digits
        c = 2 * q * q
        if c < 10: continue
        if c > 99: break
        # check product is prime
        m = 1 + c * q
        if not is_prime(m): continue
        # output solution
        printf("q={q}; a=1/{q} b={m}/{d} c={c}; product={m}", d=2 * q)
      

      Either way we find there are two solutions:

      Solution: (1) The largest number is 50, and the product of the three numbers is 251; or:
      (2) The largest number is 72, and the product of the three numbers is 433.

      The published solution is: “largest = 50; product = 251”. Apparently the setter was unaware of the second solution.

      Like

  • Unknown's avatar

    Jim Randell 9:06 am on 23 November 2021 Permalink | Reply
    Tags:   

    Brain-Teaser 887: The valetudinarians 

    From The Sunday Times, 6th August 1978 [link]

    “We have just been discussing our health”, said Alf, “and we have discovered that between us we share the same five complaints, and the same prescribed tablets for them. Each of us has two complaints, but no two have both the same, and no more than two of us take each sort of tablets. For instance two take red tablets, two blue, and so on. I do not have green ones. I take one lot the same as Ed, but they are not yellow, and I do not take kidney tablets”.

    Bob said: “I do not have green tablets. One heart patient also has tablets for sleeplessness”.

    Cyril said: “I do not have kidney tablets. I take one lot the same as Ed which are not for the heart. I do not take blue ones”.

    Don said: “I do not have heart trouble. My tablets are not yellow. Those who take green tablets do not also take blue ones”.

    Ed said: “I take white tablets which are not for the heart. The ones with nerves do not have indigestion, and nerve tablets are not yellow”.

    What colour were the heart tablets? Who took those for nerves?

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

    [teaser887]

     
    • Jim Randell's avatar

      Jim Randell 9:08 am on 23 November 2021 Permalink | Reply

      I assumed each colour tablet is prescribed for the same condition in each patient.

      This Python program uses the [[ choose() ]] function from enigma.py, which I thought I would use more when I implemented it. It runs in 65ms.

      Run: [ @replit ]

      from enigma import (subsets, ordered, choose, intersect, singleton, multiset, join, printf)
      
      # tablet colours
      tablets = "BGRWY"
      
      # map complaints to tablets
      for (K, H, N, I, S) in subsets(tablets, size=len, select="P"):
      
        # "white tablets are not for the heart"
        if H == 'W': continue
      
        # "nerve tablets are not yellow"
        if N == 'Y': continue
      
        # there are 2 invalid combinations: nerves + indigestion, green + blue
        invalid = { ordered(N, I), ('B', 'G') }
        values = set(s for s in subsets(tablets, size=2)).difference(invalid)
      
        # choose tablets for each person:
        def fE(Es):
          # E has white
          if 'W' not in Es: return False
          return True
      
        def fA(Es, As):
          # A does not have green or kidney
          if 'G' in As or K in As: return False
          # A has one tablet the same as E, and it isn't yellow
          AE = singleton(intersect([As, Es]))
          if AE is None or AE == 'Y': return False
          return True
      
        def fC(Es, As, Cs):
          # C does not have kidney or blue
          if K in Cs or 'B' in Cs: return False
          # C has one tablet the same as E, and it isn't heart
          CE = singleton(intersect([Cs, Es]))
          if CE is None or CE == H: return False
          return True
      
        def fD(Es, As, Cs, Ds):
          # D does not have heart or yellow
          if H in Ds or 'Y' in Ds: return False
          return True
      
        def fB(Es, As, Cs, Ds, Bs):
          # B does not have green
          if 'G' in Bs: return False
          return True
      
        # make the choices
        for vs in choose(values, [fE, fA, fC, fD, fB], distinct=1):
          # one of them has heart + sleeplessness
          if not (ordered(H, S) in vs): continue
          # each tablet is used by two people
          if not multiset.from_seq(*vs).all_same(2): continue
      
          # output solution
          (Es, As, Cs, Ds, Bs) = (join(v, sep="+") for v in vs)
          printf("A={As} B={Bs} C={Cs} D={Ds} E={Es} [K={K} H={H} N={N} I={I} S={S}]")
      

      Solution: Heart tablets are blue. Cyril and Don took tablets for nerves.

      The complete situation is:

      Alf: blue (heart); yellow (indigestion)
      Bob: red (kidney); yellow (indigestion)
      Cyril: green (nerves); white (sleeplessness)
      Don: green (nerves); red (kidney)
      Ed: blue (heart); white (sleeplessness)

      Like

    • Frits's avatar

      Frits 1:32 pm on 30 November 2021 Permalink | Reply

        
      from itertools import combinations as comb, permutations as perm
      
      # filter on tablet, complaint and shared tablets (with, type, exclusion) 
      def filter(lst, exc_tabl, exc_cmpl=99, shr_with=[], shr_t=0, shr_e={}):
        if shr_with:
          shr_set = {shr_with[0][shr_t], shr_with[1][shr_t]}
          frz_set = {frozenset(), frozenset(shr_e)}
          
        return [(x, y) for x, y in lst if all(e not in {x[i], y[i]} 
               for i, e in enumerate([exc_tabl, exc_cmpl])) and (not shr_with or 
               shr_set.intersection({x[shr_t], y[shr_t]}) not in frz_set)]
      
      guys = ["Alf", "Bob", "Cyril", "Don", "Ed"]
      tablets = ["red", "blue", "green", "white", "yellow"]
      # [0, 0, 1, 1, 2, 2, 3, 3, 4, 4]
      sorted_tablets = [i for i in range(5) for _ in range(2)]
      
      # red    0       indigestion   0
      # blue   1       heart         1
      # green  2       kidney        2
      # white  3       sleeplessness 3 
      # yellow 4       nerves        4
      
      # loop over complaints per tablet
      for p in perm(range(5)):
        # "white tablets are not for the heart"
        # "nerve tablets are not yellow"
        if p[3] == 1 or p[4] == 4: continue
        
        # combine two out of (tablets, complaints) list
        # there are 2 invalid combinations: nerves + indigestion, green + blue
        cmbs = [(x, y) for x, y in comb([(i, z) for i, z in enumerate(p)], 2) 
               if sorted((x[1], y[1])) != [0, 4] and
                  sorted((x[0], y[0])) != [1, 2]]
        
        # one of them has heart + sleeplessness          
        if all(sorted([x[1], y[1]]) != [1, 3] for x, y in cmbs): continue
        
        # E has white
        for E in [(x, y) for x, y in cmbs if 3 in {x[0], y[0]}]:
          notE = set(cmbs).difference({E})
          # A does not have green or kidney
          # A has one tablet the same as E, and it isn't yellow
          for A in filter(notE, 2, 2, E, 0, [4]):
            notAE = set(notE).difference({A})
            # C does not have kidney or blue
            # C has one tablet the same as E, and it isn't heart
            for C in filter(notAE, 1, 2, E, 1, [1]):
              notAEC = set(notAE).difference({C})
              # D does not have heart or yellow
              for D in filter(notAEC, 4, 1):
                notAECD = set(notAEC).difference({D})
                # B does not have green
                for B in filter(notAECD, 2):
                  ABCDE = [A, B, C, D, E]
                  
                  # each tablet is sorted by two people
                  if sorted(z for x, y in ABCDE for z in (x[0], y[0])) != \
                     sorted_tablets: continue
                  
                  # one of them has heart + sleeplessness     
                  if all(sorted([x[1], y[1]]) != [1, 3] for x, y in ABCDE): 
                    continue
                  
                  heart = set(x[0] if x[1] == 1 else y[0] 
                              for x, y in ABCDE if 1 in {x[1], y[1]})
                  if len(heart) != 1: continue 
                  print(f"Heart tablets are {tablets[heart.pop()]}.")
                  
                  nerves = [i for i, (x, y) in enumerate(ABCDE) 
                            if 4 in {x[1], y[1]}]
                  print(f"{guys[nerves[0]]} and {guys[nerves[1]]} "
                        f"took tablets for nerves")
      

      Like

  • Unknown's avatar

    Jim Randell 4:36 pm on 19 November 2021 Permalink | Reply
    Tags:   

    Teaser 3087: Hexagonia 

    From The Sunday Times, 21st November 2021 [link] [link]

    Hexagonia is a hexagonal republic and is divided into 24 numbered counties, as shown. The counties are to be reorganised into four departments, each composed of six edge-connected counties. No two departments will have the same shape or be reflections of each other, and the president’s residence in Department A will be built on an axis of symmetry of the department. Every sum of the county numbers in a department will be, in the prime minister’s honour, a prime number, and her mansion will be built in the middle of a county in Department B, on an axis of symmetry of the department, and as far as possible from the president’s residence.

    In what county will the Prime Minister’s mansion be built?

    This is the next puzzle after Teaser 2401 whose number has the property described in Teaser 2700.

    [teaser3087]

     
    • Jim Randell's avatar

      Jim Randell 5:30 pm on 19 November 2021 Permalink | Reply

      It looks like I implemented my polyiamonds.py module at just the right time (see Teaser 2838).

      I added the other hexiamonds to it and then used it to generate the only possible map. From this we see there are only 2 achiral shapes involved, and only one of them has an axis of symmetry that passes through the counties, so the required answer is determined directly from the map.

      The following Python program runs in 1.16s. Although by limiting the sets of pieces tested to those containing 2 chiral shapes, this can be reduced to 668ms.

      from enigma import (subsets, join, primes, printf)
      import polyiamonds
      
      # collect the possible hexiamonds
      shapes = polyiamonds.shapes("O6 I6 C6 E6 F6 G6 H6 J6 P6 S6 V6 X6".split())
      
      # map cells of the 24-hex grid to county numbers
      cells = [
        (0, 6), (0, 7), (1, 6), (1, 7), (2, 6), # 1 - 5
        (0, 4), (0, 5), (1, 4), (1, 5), (2, 4), (2, 5), (3, 4), # 6 - 12
        (0, 3), (1, 2), (1, 3), (2, 2), (2, 3), (3, 2), (3, 3), # 13 - 19
        (1, 1), (2, 0), (2, 1), (3, 0), (3, 1), # 20 - 24
      ]
      county = dict((cell, i) for (i, cell) in enumerate(cells, start=1))
      grid = set(cells)
      
      primes.extend(130)
      
      # choose 4 of the shapes
      for ps in subsets(shapes.keys(), size=4):
      
        # attempt to fit the shapes into the grid
        ss = list(shapes[p] for p in ps)
        for g in polyiamonds.fit(ss, grid):
          # the sum of the counties in each region should be prime
          t = dict((n, 0) for n in (1, 2, 3, 4))
          for (k, v) in g.items():
            t[v] += county[k]
          if not all(v in primes for v in t.values()): continue
      
          printf("{ps}\n", ps=join(ps, sep=" ", enc="[]"))
          polyiamonds.output_grid(g)
      

      Solution: The Prime Minister’s Mansion is in county 16.

      The layout of the departments is as follows:

      The departments have the following prime totals: blue (H6) = 31; red (C6) = 61; green (E6) = 101; orange (J6) = 107.

      The two departments with axes of symmetry are the red one (C6) and the green one (E6).

      The red one’s axis is along the 6/13 border, so this gives us the location of the President’s Residence (so Department A is red), and the axis in the green department (Department B) is indicated with the dotted line. This passes through counties 15 and 16, but we want to be as far from the President’s Residence as possible, so the Prime Minister’s Mansion must be in county 16.

      In fact C6 is the only achiral piece that can give a prime total, and has an axis on a border, so C6 must be involved in the map with one of E6 or V6 (which are the remaining achiral pieces with axes that pass through cells). And the remaining 2 counties must be chosen from F6, G6, H6, I6, J6, P6, S6 (chiral).

      Like

      • Jim Randell's avatar

        Jim Randell 10:28 pm on 19 November 2021 Permalink | Reply

        Here is a faster version, where we only consider combinations of 2 achiral and 2 chiral pieces, and positions of pieces in the grid are only considered if the counties involved have a prime sum.

        It runs in 67ms.

        from enigma import (subsets, join, primes, defaultdict, intersect, exact_cover, printf)
        import polyiamonds
        
        # collect the possible hexiamonds
        achiral = "O6 C6 E6 V6 X6".split()
        chiral = "I6 F6 G6 H6 J6 P6 S6".split()
        shapes = polyiamonds.shapes(achiral + chiral)
        
        # map cells of the 24-hex grid to county numbers
        cells = [
          (0, 6), (0, 7), (1, 6), (1, 7), (2, 6), # 1 - 5
          (0, 4), (0, 5), (1, 4), (1, 5), (2, 4), (2, 5), (3, 4), # 6 - 12
          (0, 3), (1, 2), (1, 3), (2, 2), (2, 3), (3, 2), (3, 3), # 13 - 19
          (1, 1), (2, 0), (2, 1), (3, 0), (3, 1), # 20 - 24
        ]
        county = dict((cell, i) for (i, cell) in enumerate(cells, start=1))
        grid = set(cells)
        
        primes.extend(130)
        
        # look for placements that give a prime total
        ss = defaultdict(list)
        for (k, vs) in shapes.items():
          for cs in polyiamonds.placements(vs, grid):
            if sum(county[c] for c in cs) in primes:
              ss[k].append(cs)
        
        achiral = sorted(intersect([achiral, ss.keys()]))
        chiral = sorted(intersect([chiral, ss.keys()]))
        
        # choose 2 achiral shapes
        for ps1 in subsets(achiral, size=2):
          # and the rest are chiral
          for ps2 in subsets(chiral, size=2):
            ps = ps1 + ps2
            # look for an exact cover using these pieces
            for rs in exact_cover(list(ss[p] for p in ps), grid):
              # output solution
              printf("{ps}\n", ps=join(ps, sep=" ", enc="[]"))
              g = dict()
              for (i, cs) in enumerate(rs, start=1):
                g.update((c, i) for c in cs)
              polyiamonds.output_grid(g)
        

        Like

      • Frits's avatar

        Frits 5:20 pm on 20 November 2021 Permalink | Reply

        @Jim, you don’t answer the question

        Like

        • Jim Randell's avatar

          Jim Randell 5:48 pm on 20 November 2021 Permalink | Reply

          @Frits: I reveal the answers to competition puzzles after the deadline for entries is closed.

          Like

          • Frits's avatar

            Frits 6:57 pm on 20 November 2021 Permalink | Reply

            @Jim, I mean that your program doesn’t seem to print in what county will the Prime Minister’s mansion be built. You normally don’t hide the answer in your program output. It doesn’t matter.

            Like

  • Unknown's avatar

    Jim Randell 9:09 am on 18 November 2021 Permalink | Reply
    Tags:   

    Brain-Teaser 884: Mice in the works 

    From The Sunday Times, 16th July 1978 [link]

    Recently a hot-drink vending machine was installed in our office. Very nice it is too — completely up to date it was when it was bought. There are five switches, a slot for your money, and a button. The switches are labelled TEA, COFFEE, CHOCOLATE, MILK and SUGAR, and you select the combination you want, put in your money, press the button, and out comes your drink. Why, you can even have coffolatea if you want!

    At least, this is the idea. Unfortunately, during the ten years it has been in store, “awaiting approval”, mice have chewed up the wiring. Mice with soldering irons, I should think. The result is now that no switch affects its “own” ingredient at all, but instead turns on two other ingredients, each ingredient being turned on by two different switches. However, if two switches are set which turn on the same ingredient, then they cancel each other out, and that ingredient doesn’t come out at all.

    The result is somewhat chaotic, though occasionally some of the output is actually drinkable. For instance, when you ask for white sweet coffee, you get unsweetened milky tea; when you ask for sweet milky chocolate, you get sweet chocolate without milk; and when you ask for unsweetened milky tea you get a glorious gooey mocha — i.e. chocolate and coffee with milk and sugar.

    Luckily, pressing the “deliver” button reinstates the original chaos, so that setting the same switches always gives the same results.

    So, what is the easiest way to get white coffee without sugar? (i.e. Name the fewest switches that will deliver just coffee and milk).

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

    [teaser884]

     
    • Jim Randell's avatar

      Jim Randell 9:12 am on 18 November 2021 Permalink | Reply

      We’ve seen puzzles like this before. See Enigma 91, Puzzle #103 (although this puzzle was published before either of them).

      I reused the code I wrote for Puzzle #103.

      The following Python 3 program runs in 60ms.

      Run: [ @replit ]

      from enigma import (multiset, subsets, diff, update, ordered, join, map2str, printf)
      
      # buttons
      buttons = 'T Cf Ch M S'.split()
      
      # map each value in <ks> to <n> other values chosen from mutliset <vs>
      # return map d: <ks> -> <n>-tuples from <vs>
      def make_map(ks, vs, n=2, d=dict()):
        # are we done?
        if not ks:
          yield d
        else:
          # choose n values for the next key
          k = ks[0]
          for ss in subsets(diff(vs.keys(), [k]), size=n):
            yield from make_map(ks[1:], vs.difference(ss), n, update(d, [(k, ordered(*ss))]))
      
      # outcome of selecting buttons <bs> using map <d>
      def select(d, bs):
        m = multiset()
        for b in bs:
          m.update_from_seq(d[b])
        return set(k for (k, v) in m.items() if v == 1)
      
      # consider possible maps
      for d in make_map(buttons, multiset.from_seq(buttons, count=2)):
      
        # select M+S+Cf -> M+T
        if not (select(d, ['M', 'S', 'Cf']) == {'M', 'T'}): continue
      
        # select S+M+Ch -> S+Ch
        if not (select(d, ['S', 'M', 'Ch']) == {'S', 'Ch'}): continue
      
        # select M+T -> Ch+Cf+M+S
        if not (select(d, ['M', 'T']) == {'Ch', 'Cf', 'M', 'S'}): continue
      
        # answer, easiest way to get Cf+M?
        for bs in subsets(buttons, min_size=1):
          if select(d, bs) == {'Cf', 'M'}:
            fmt = lambda s: join(sorted(s), sep="+")
            printf("{bs} -> Cf+M {d}", bs=fmt(bs), d=map2str(((k, fmt(v)) for (k, v) in d.items()), arr="->", enc="[]"))
      

      Solution: The easiest way to get coffee and milk is to select “Coffee” and “Milk”.

      There is also a combination of 3 buttons that will give coffee and milk: “Chocolate”, “Tea” and “Sugar”.

      The items delivered by the buttons are:

      Coffee: Chocolate + Milk
      Chocolate: Tea + Sugar
      Milk: Coffee + Chocolate
      Sugar: Coffee + Tea
      Tea: Milk + Sugar

      Like

    • John Crabtree's avatar

      John Crabtree 5:44 pm on 25 November 2021 Permalink | Reply

      Using the same switch twice selects nothing. Using all five switches selects nothing. Call this statement S4
      Using all of the switches in statements S1, S2, S3 and S4 results in: using S gives Cf and T
      S1 reduces to: using Cf and M gives Cf and M
      This is one possible answer, but we do not know that it is the shortest.
      From S1 and S, using M gives Cf
      From S2 and S, using M gives Ch as well as Cf (from above)
      From S1, using Cf gives Ch and M
      From S2, using Ch gives S and T
      From S3, using T gives M and S

      Since no one button gives Coffee with Milk, pressing Coffee and Milk is the simplest way to get Coffee with Milk

      This solution is much simpler that the official one in the book.

      Like

  • Unknown's avatar

    Jim Randell 9:08 am on 16 November 2021 Permalink | Reply
    Tags:   

    Teaser 2838: King Lear IV 

    From The Sunday Times, 12th February 2017 [link] [link]

    King Lear IV’s realm consisted of a regular hexagon divided into 24 counties that were equal-sized equilateral triangles. In his will he wanted to share the counties among his six daughters, each daughter’s portion having the property that, if you walked in a straight line between any two points in it, then you remained in her portion. If two daughters’ portions had the same area then they had to be of different shapes (and not the mirror image of each other). He wanted Cordelia to have a single county (his favourite county on the edge of the kingdom), he wanted Goneril to have a hexagonal-shaped portion, and he knew the number of counties he wanted to allocate to each of the other daughters, with Reagan’s portion being the largest of all. It turned out that his requirements uniquely determined Goneril’s and Reagan’s counties.

    What, in increasing order, were the numbers of counties allocated to the six daughters?

    [teaser2838]

     
    • Jim Randell's avatar

      Jim Randell 9:10 am on 16 November 2021 Permalink | Reply

      I originally solved this puzzle using the Polyform Puzzler framework [link], the problem then becomes how to interface to framework, but it allowed me to quickly produce a solution using a variation on the code I had previously written for Enigma 321.

      But having subsequently packaged my polyominoes code written for Enigma 321 into a separate module, I thought I could do the same for polyiamonds.

      So I wrote a package polyiamonds.py [@github] that knows enough about the convex shapes that fit into the 24-hex grid, and used that to solve this puzzle.

      Here is a reference for polyiamonds [link]. I used the same names, but added octiamonds “d8” (= “diamond”) and “t8” (= “trapezium”) to produce a complete set of convex polyiamonds that will fit in the required hexagonal grid. In total there are 12 possible shapes.

      The program looks for 4 additional pieces to go with “T1” and “O6” (which we are told are there), and one of these pieces must be larger than “O6”. The position of the “T1” piece is fixed along the edge of grid. The program then looks for packings where the positions of Regan’s (the largest piece) and Goneril’s piece (“O6”) are uniquely determined. It runs in 67ms.

      Run: [ @replit ]

      from enigma import (defaultdict, subsets, diff, union, irange, ordered, join, printf)
      import polyiamonds
      
      # convex polyiamonds up to size 8 that fit in the hexagon
      pieces = {
        "T1": 1,
        "D2": 2,
        "I3": 3,
        "T4": 4, "I4": 4,
        "I5": 5,
        "O6": 6, "I6": 6,
        "I7": 7, "D7": 7,
        "d8": 8, "t8": 8,
      }
      
      # collect the shapes
      shapes = polyiamonds.shapes(pieces.keys())
      
      # make the hexagonal grid
      grid = union(set((x, y) for y in irange(y1, y2)) for (x, y1, y2) in [(0, 3, 7), (1, 1, 7), (2, 0, 6), (3, 0, 4)])
      # remove (1, 1) for T1
      grid.remove((1, 1))
      
      # accumulate results by the position of G (= O6) and R (= largest)
      def key(g, ps):
        cells = lambda g, k: ordered(*(c for (c, n) in g.items() if n == k))
        return (cells(g, ps.index("O6") + 1), cells(g, len(ps)))
      
      # choose 4 pieces to go with T1 and O6
      r = defaultdict(set)
      for ps in subsets(diff(pieces.keys(), ["T1", "O6"]), size=4, fn=list):
        # check the size
        ss = list(pieces[p] for p in ps)
        m = max(ss)
        if not (m > 6 and sum(ss) == 17 and ss.count(m) == 1): continue
      
        # attempt to fit the shapes into the grid
        ps.extend(["T1", "O6"])
        ps = sorted(ps, key=lambda p: pieces[p])
        ks = tuple(pieces[p] for p in ps)
        printf("{ps} -> {ks}\n", ps=join(ps, sep=" ", enc="[]"), ks=join(ks, sep=" ", enc="()"))
        n = 0
        for g in polyiamonds.fit(list(shapes[p] for p in ps if p != "T1"), grid, start=2):
          r[ks].add(key(g, ps))
          g[(1, 1)] = 1 # place T1 as 1
          polyiamonds.output_grid(g)
          n += 1
        printf("[{n} ways]\n")
      
      # look for a unique solution
      for (k, vs) in r.items():
        if len(vs) == 1:
          printf("solution = {k}")
      

      Solution: The numbers of counties allocated to the daughters are: 1, 2, 4, 4, 6, 7.

      I found three allocations that allow the grid to be packed. They are summarised in the following diagram:

      (Click to enlarge).

      The allocation that has only one way to pack the grid gives the solution.

      Like

  • Unknown's avatar

    Jim Randell 2:26 pm on 14 November 2021 Permalink | Reply
    Tags:   

    Teaser 2505: [Letter values] 

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

    I have taken 15 letters of the alphabet and given each of them a numerical value. They are not all positive, they are not all whole numbers and they are not all different. In fact, four of the letters have the same positive value.

    Using these values, I can work out the total value of some words by adding up the values of their letters.

    So, ONE has total value 1, TWO has total value 2, and so on up to SEVENTEEN, which has total value 17.

    What is the value of NOUGHT?

    This puzzle was originally published with no title.

    [teaser2505]

     
    • Jim Randell's avatar

      Jim Randell 2:27 pm on 14 November 2021 Permalink | Reply

      See also: Teaser 1908, Teaser 2756, Enigma 1602, Enigma 1278, Enigma 1292.

      It turns out that choosing 2 symbols to be the same, and then checking for other values with the same value as the 2 we chose is much faster than trying all combinations of 4 symbols.

      The following Python program runs in 105ms.

      Run: [ @replit ]

      from enigma import (irange, int2words, union, matrix, subsets, trim, join, map2str, printf)
      
      # we are interested in the values of the words "one" ... "seventeen"
      words = dict((i, int2words(i)) for i in irange(1, 17))
      
      # determine symbols used
      symbols = sorted(union(words.values()))
      n = len(symbols)
      
      # a function for making equations
      eq = matrix.equation(symbols)
      
      eqs = list(eq(w, i) for (i, w) in words.items())
      
      # choose the first k symbols to have the same (positive) value
      k = 2
      for ss in subsets(trim(symbols, tail=4 - k), size=k, fn=list):
        s0 = ss[0]
        eqs_ = list(eq ({s0: -1, s: 1}) for s in ss[1:])
      
        try:
          vs = matrix.linear(eqs + eqs_)
        except ValueError:
          continue
      
        # check the shared value is positive, and used exactly 4 times
        m = dict(zip(symbols, vs))
        v = m[s0]
        sv = sorted(s for s in symbols if m[s] == v)
        if not (v > 0 and len(sv) == 4 and sv[:k] == ss): continue
      
        # output solution
        ans = sum(m[s] for s in 'nought')
        printf("nought={ans} {vs} {sv}",
          vs=map2str(m, sep=" ", enc="[]"),
          sv=join(sv, sep="=", enc="[]"),
        )
      

      Solution: NOUGHT = 23/2 (= 11.5).

      The values are:

      E = I = S = W = 0
      F = R = U = V = 5/2
      G = 15/2
      H = −5
      L = 4
      N = 9/2
      O = −7/2
      T = 11/2
      X = 6

      So the four letters that share a positive value are F, R, U, V.

      (E, I, S, W, also share a value, but this is zero).


      We immediately see that E = 0. (As 14 = 4 + 10, 16 = 6 + 10, 17 = 7 + 10), and then I = 0 (as 13 = 3 + 10). So we could exclude these from the letters we consider when we choose some of them to be the same, to give a slight speed improvement.

      This observation can also form the start of a manual solution.

      Like

      • Frits's avatar

        Frits 3:33 pm on 15 November 2021 Permalink | Reply

        @Jim, symbols[:n + k – 4] makes more sense to me. fi, now you miss the situation e, v, w and x being the same.

        Like

        • Jim Randell's avatar

          Jim Randell 3:45 pm on 15 November 2021 Permalink | Reply

          @Frits: Yes it should be [:n + k − 4].

          I might add a [[ trim(s, head, tail) ]] function to enigma.py to make it easier to avoid these “off-by-1” errors (which are all too easy to make with Python indices).

          Like

  • Unknown's avatar

    Jim Randell 4:05 pm on 12 November 2021 Permalink | Reply
    Tags:   

    Teaser 3086: Four-sided dice game 

    From The Sunday Times, 14th November 2021 [link] [link]

    I have two identical four-sided dice (regular tetrahedra). Each die has the numbers 1 to 4 on the faces. Nia and Rhys play a game in which each of them takes turns throwing the two dice, scoring the sum of the two hidden numbers. After each has thrown both dice three times, if only one of them scores an agreed total or over, then he or she is the winner. Otherwise the game is drawn.

    After Rhys had taken two throws, before Nia took her second throw she noticed that they both had a chance of winning, but if things had gone differently, they could have reached a situation where Rhys had scored a lower total on his two throws, and Nia had scored twice her current total with a single throw, then in that situation Nia’s chance of winning would be 35 times what it was in reality.

    What are Nia’s and Rhys’ actual scores (so far) and what is the agreed winning total?

    I have modified the wording of this puzzle slightly to make it clearer (hopefully).

    [teaser3086]

     
    • Jim Randell's avatar

      Jim Randell 6:05 pm on 12 November 2021 Permalink | Reply

      After a couple of false starts with no solution found, I decided that we are looking for a hypothetical probability of N winning that is 35× what the actual probability is.

      This Python program runs in 58ms.

      Run: [ @replit ]

      from enigma import (Rational, multiset, subsets, multiply, printf)
      
      Q = Rational()
      
      # possible scores and their frequency for a single throw of the pair
      scores = multiset.from_seq(a + b for (a, b) in subsets((1, 2, 3, 4), size=2, select="M"))
      T = scores.size()
      
      # possible totals from 1, 2 throws
      total1 = set(scores.keys())
      total2 = set(sum(s) for s in subsets(total1, size=2, select="R"))
      total3 = set(sum(s) for s in subsets(total1, size=3, select="R"))
      
      # calculate probabilities of reaching target <tgt> from totals <ts> with <k> additional throws
      def probabilities(tgt, ts, k):
        d = T ** k
        ps = dict()
        for t in ts:
          n = sum(multiply(scores[x] for x in xs) for xs in subsets(total1, size=k, select="M") if t + sum(xs) >= tgt)
          ps[t] = Q(n, d)
        return ps
      
      # consider possible targets
      for tgt in total3:
      
        # probabilities of reaching target after one and two throws
        p1 = probabilities(tgt, total1, 2)
        p2 = probabilities(tgt, total2, 1)
      
        # look for possible N totals, where 2N is also possible
        for (N, pN) in p1.items():
          if not (0 < pN < 1): continue
          pN_ = p1.get(2 * N)
          if pN_ is None: continue
      
          # consider possible R, pR values
          for (R, pR) in p2.items():
            if not (0 < pR < 1): continue
      
            # calculate N's actual probability of winning
            w = pN * (1 - pR)
            w_ = 35 * w
            if w_ > 1: continue
      
            # look for lower hypothetical R_ value, that gives N a probability of w_
            for (R_, pR_) in p2.items():
              if not (R_ < R and pN_ * (1 - pR_) == w_): continue
      
              # output solution
              printf("tgt={tgt}; N={N} pN={pN} pN_={pN_}; R={R} pR={pR}; R_={R_} pR_={pR_}")
      

      Solution: Nia has scored 2 on her single throw. Rhys has scored 13 from his two throws. The target total is 17.

      N has a 5/256 (≈ 2.0%) probability of reaching the target, and R’s probability would be 13/16 (≈ 81%).

      So N’s overall probability of winning is: (5/256)(3/16) = 15/4096 (≈ 0.37%).

      However if N had scored twice as much on her throw (i.e. 4) she would have a 35/256 (≈ 14%) probability of reaching the target, and if R had scored only 9 on his two throws, his probability would be 1/16 (≈ 6.3%).

      And the overall probability of N winning in this situation would be: (35/256)(15/16) = 525/4096 (≈ 13%).

      And we see: 35 × 15/4096 = 525/4096, as required.

      Like

      • Jim Randell's avatar

        Jim Randell 11:13 am on 14 November 2021 Permalink | Reply

        Here is a faster version of my program. It avoids recalculation of probabilities, and keeps values in the integers.

        Run: [ @replit ]

        from enigma import (multiset, multiply, subsets, cached, printf)
        
        # possible scores, and their frequency for 1 throw of 2 dice
        scores = multiset.from_seq(a + b for (a, b) in subsets((1, 2, 3, 4), size=2, select="M"))
        T = scores.size()
        T2 = T * T
        T3 = T * T2
        
        # possible totals from 1, 2, 3 throws
        total = { 1: sorted(scores.keys()) }
        total.update((i, sorted(set(sum(s) for s in subsets(total[1], size=i, select="R")))) for i in (2, 3))
        
        # probability of scoring n or more in k throws
        @cached
        def prob(n, k):
          if n < 0: return 0
          return sum(multiply(scores[x] for x in xs) for xs in subsets(total[1], size=k, select="M") if sum(xs) >= n)
        
        # consider possible targets
        for tgt in total[3]:
        
          # consider actual total for N after 1 throw
          for N in total[1]:
            N_ = 2 * N
            if not (N_ in total[1]): continue
            # calculate N's actual probability of reaching the target in 2 throws
            pN = prob(tgt - N, 2) # / T2
            if not (0 < pN < T2): continue
        
            # calculate N's hypothetical probability of reaching the target in 2 throws
            pN_ = prob(tgt - N_, 2) # / T2
        
            # consider possible R, pR values    
            for R in total[2]:
              pR = prob(tgt - R, 1) # / T
              if not (0 < pR < T): continue
        
              # calculate N's actual probability of winning
              w = pN * (T - pR) # / T3
              w_ = 35 * w
              if w_ > T3: continue
        
              # look for lower hypothetical R_ value, that gives N a probability of w_
              for R_ in total[2]: 
                if not (R_ < R): break
                pR_ = prob(tgt - R_, 1)
                if not (pN_ * (T - pR_) == w_): continue
        
                # output solution
                printf("tgt={tgt}; N={N} pN={pN}/{T2} N_={N_} pN_={pN_}/{T2}; R={R} pR={pR}/{T}; R_={R_} pR_={pR_}/{T}")
        

        Like

        • Frits's avatar

          Frits 11:03 pm on 14 November 2021 Permalink | Reply

          @Jim, your Friday night version is still the fastest when run with PyPy (timeit, 20 times 8 executions). My program also benefits from the @cached function (do you know if there is a @cached variant for lambda functions?)

          Like

          • Jim Randell's avatar

            Jim Randell 9:27 am on 15 November 2021 Permalink | Reply

            I am currently targeting CPython 3.9 as my primary platform (although I am expecting to switch to CPython 3.10 before long), but I also like my code to run on older versions of Python (including Python 2.7 where possible).

            I measured the internal run time of my original program as 7.1ms and my faster version as 1.1ms. So I was happy enough with the performance improvement. And both times are dwarfed by the overhead of running Python.

            The [[ cached ]] function can be called directly as well as used as a decorator. For example, in Teaser 3081:

            recurring = cached(lambda n: enigma.recurring(1, n, recur=1))
            

            Note that if you are targeting a Python 3 environment you can use [[ functools.lru_cache ]] which comes with the Python standard library.

            I’ve added the [[ cache() ]] function to enigma.py which will use [[ functools.cache ]] if available (it was added in Python 3.9), otherwise it uses [[ enigma.cached ]].

            Like

    • Frits's avatar

      Frits 12:32 pm on 13 November 2021 Permalink | Reply

      Not storing the product structures.
      I have let the winning total start from 7, I didn’t see an easy deduction to use a better starting value.

        
      rom itertools import product
      from fractions import Fraction as RF
      
      N = 4         # number of sides
      GC = 35       # greater chance of winning
      
      # number of times out of N**rep attempts you will reach the limit
      ntimes = lambda rep, limit: sum(sum(p2) >= limit
                                  for p2 in product(range(1, N + 1), repeat=rep)) 
      
      # loop over winning totals
      for wt in range(7, 6 * N + 1):
        for nia in range(2, N + 1): 
          # number of times out of N**4 tries Nia will reach the winning total
          nt_nia = ntimes(4, wt - nia)        
          # they both have a chance of winning
          if nt_nia == 0: continue
         
          nt_nia_double = ntimes(4, wt - 2 * nia) 
         
          # GC * w1 = w2
          # nt_nia * GC / nt_nia_double = (N**2 - nt_rhys_less) / (N**2 - nt_rhys))
          if not (1 < nt_nia * GC / nt_nia_double <= N**2): continue
             
          # w1 * GC <= 1 so nt_nia * (N**2 - nt_rhys) * GC / N**6 <= 1
          # (N**2 - nt_rhys) <= N**6 / (GC * nt_nia)
          min_nt_rhys = N**2 - N**6 / (GC * nt_nia)
         
          for rhys in range(5, 4 * N + 1):
            # number of times out of N**2 attempts Rhys will reach the winning total
            nt_rhys = ntimes(2, wt - rhys)   
            # they both have a chance of winning
            if nt_rhys == 0 or nt_rhys < min_nt_rhys: continue        
       
            # calculate Nia's winning chance
            w1 = RF(nt_nia * (N**2 - nt_rhys), N**6)
            if w1 == 0: break
           
            for rhys_less in range(4, rhys):
              nt_rhys_less = ntimes(2, wt - rhys_less)    
             
              # calculate Nia's winning chance under better conditions
              w2 = RF(nt_nia_double * (N**2 - nt_rhys_less), N**6)
             
              if w2 == GC * w1:
                print(f"Nia's score: {nia}, Rhys's score: {rhys}"
                      f" and the agreed winning total: {wt}")
              elif w2 < GC * w1:
                break
      

      Like

    • Hugh Casement's avatar

      Hugh Casement 12:58 pm on 14 November 2021 Permalink | Reply

      “35 times greater” would mean 36 times as much. Just the sort of silly expression that journalists use. Altogether badly worded.

      Like

      • Jim Randell's avatar

        Jim Randell 1:28 pm on 14 November 2021 Permalink | Reply

        Yes. I suppose “35 times” will do. (We can see it must have a greater value, as N has a non-zero actual probability of winning).

        I did a quick check and there is no solution to the puzzle where N’s hypothetical probability is 36× the actual probability.

        Originally I was thrown by what were were comparing N’s hypothetical probability to. (I thought maybe it was R’s hypothetical probability, bit it turns out it is N’s actual probability).

        Like

  • Unknown's avatar

    Jim Randell 9:11 am on 11 November 2021 Permalink | Reply
    Tags: by: J P Mernagh   

    Brain-Teaser 883: Goals galore 

    From The Sunday Times, 9th July 1978 [link]

    We’ve grown tired of these low-scoring, defensive football matches in our locality, and so it was agreed last March for the annual competition between the five villages, each playing the others once, that no goalkeepers would be allowed, and each game would continue until nine goals had been scored.

    Each village won 2 matches, and scored a different number of goals in each match. In the games, each possible result occurred twice. We had to decide the tournament on the total of goals scored, and happily, all five totals were different.

    Blackton, the eventual champions, lost 2-7 to Appleton. Easton were last with 11 goals fewer than Blackton.

    The Drafton centre-forward remarkably scored a hat-trick in each match, which included a last-second winner against Blackton.

    Caxton scored in every match and could indeed have won the league if they had scored twice more in their match against Blackton. As it was they finished one goal ahead of Drafton in the final totals.

    What was the score between Easton and Appleton; and what was the score between Caxton and Drafton?

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

    [teaser883]

     
    • Jim Randell's avatar

      Jim Randell 9:12 am on 11 November 2021 Permalink | Reply

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

      The following run file executes in 357ms.

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      # let the matches be:
      #
      #  A vs B = F - G
      #  A vs C = H - I
      #  A vs D = J - K
      #  A vs E = L - M
      #  B vs C = N - P
      #  B vs D = Q - R
      #  B vs E = S - T
      #  C vs D = U - V
      #  C vs E = W - X
      #  D vs E = Y - Z
      
      SubstitutedExpression
      
      # each village scored a different number of goals in each match
      # (and so they also each conceded a different number of goals in each match)
      --distinct="FHJL,GNQS,IPUW,KRVY,MTXZ,GIKM,FPRT,HNVX,JQUZ,LSWY"
      
      # each match goes to 9-goals
      "9 - F = G"
      "9 - I = H"
      "9 - K = J"
      "9 - L = M"
      "9 - P = N"
      "9 - R = Q"
      "9 - T = S"
      "9 - U = V"
      "9 - W = X"
      "9 - Y = Z"
      
      # each team won 2 matches
      "sum([F > 4, H > 4, J > 4, L > 4]) = 2"
      "sum([G > 4, N > 4, Q > 4, S > 4]) = 2"
      "sum([I > 4, P > 4, U > 4, W > 4]) = 2"
      "sum([K > 4, R > 4, V > 4, Y > 4]) = 2"
      "sum([M > 4, T > 4, X > 4, Z > 4]) = 2"
      
      # each result occurred twice
      --code="twice = lambda *ss: multiset.from_seq(ordered(*s) for s in ss).all_same(2)"
      "twice((F, G), (H, I), (J, K), (L, M), (N, P), (Q, R), (S, T), (U, V), (W, X), (Y, Z))"
      
      # each side scored a different number of goals
      "all_different(F + H + J + L, G + N + Q + S, I + P + U + W, K + R + V + Y, M + T + X + Z)"
      
      # A vs B = 7 - 2
      --assign="F,7"
      --assign="G,2"
      
      # D beat B
      --invalid="0-4,R"
      --invalid="5-9,Q"
      
      # D had a hat-trick in each match
      --invalid="0-2,KRVY"
      --invalid="7-9,JQUZ"
      
      # E has 11 goals fewer than B"
      "(G + N + Q + S) - (M + T + X + Z) = 11"
      
      # C finished 1 goal ahead of D
      "(I + P + U + W) - (K + R + V + Y) = 1"
      
      # B were the eventual champions
      "G + N + Q + S > max(F + H + J + L, I + P + U + W)"
      
      # C scored in every match
      --invalid="0,IPUW"
      --invalid="9,HNVX"
      
      # C could have scored 2 more goals against B ...
      --invalid="8-9,P"
      --invalid="0-1,N"
      # ... and been champions
      "I + P + U + W + 2 > max(F + H + J + L, G + N + Q + S - 2, I + P + U + W)"
      
      # [optional] neater output
      --template="F-G H-I J-K L-M; N-P Q-R S-T; U-V W-X; Y-Z"
      --solution=""
      

      Solution: Appleton vs. Easton = 3 – 6; Caxton vs. Grafton = 2 – 7.

      The full results are:

      A vs B = 7 – 2
      A vs C = 0 – 9
      A vs D = 6 – 3
      A vs E = 3 – 6
      B vs C = 8 – 1
      B vs D = 4 – 5
      B vs E = 9 – 0
      C vs D = 2 – 7
      C vs E = 8 – 1
      D vs E = 4 – 5

      B = 23 for; 13 against
      C = 20 for; 16 against
      D = 19 for; 17 against
      A = 16 for; 20 against
      E = 12 for; 24 against

      Like

    • Frits's avatar

      Frits 12:28 pm on 12 November 2021 Permalink | Reply

      Similar but with fewer variables (including the hat-trick requirement).

        
      #!/usr/bin/env python3 -m enigma -r
      
      # let the matches be:
      #
      #  A vs B = F - ..
      #  A vs C = G - ..
      #  A vs D = H - ..
      #  A vs E = I - ..
      #  B vs C = J - ..
      #  B vs D = K - ..
      #  B vs E = L - ..
      #  C vs D = M - ..
      #  C vs E = N - ..
      #  D vs E = O - ..
      SubstitutedExpression
      
      # each village scored a different number of goals in each match
      # (and so they also each conceded a different number of goals in each match)
      --distinct="FGHI, JKL, MN"
      
      "9 - F not in {J, K, L}"
      "all_different(9 - G, 9 - J, M, N)"
      "all_different(9 - H, 9 - K, 9 - M, O)"
      "all_different(9 - I, 9 - L, 9 - N, 9 - O)"
      
      # each team won 2 matches
      "sum([F > 4, G > 4, H > 4, I > 4]) = 2"
      "sum([9 - F > 4, J > 4, K > 4, L > 4]) = 2"
      "sum([9 - G > 4, 9 - J > 4, M > 4, N > 4]) = 2"
      "sum([9 - H > 4, 9 - K > 4, 9 - M > 4, O > 4]) = 2"
      "sum([9 - I > 4, 9 - L > 4, 9 - N > 4, 9 - O > 4]) = 2"
      
      # each result occurred twice
      --code="twice = lambda *y: sorted(sorted(x) for x in y) == \
                      sorted([[0, 9], [1, 8], [2, 7], [3, 6], [4, 5]] * 2)"
      "twice((F, 9-F), (G, 9-G), (H, 9-H), (I, 9-I), (J, 9-J), (9-K, K), 
             (9-L, L), (9-M, M), (N, 9-N), (O, 9-O))"
      
      # each side scored a different number of goals
      "all_different(F + G + H + J, 
                     9 - F + J + K + L, 
                     18 - G - J + M + N, 
                     27 - H - K - M + O, 
                     36 - I - L - N - O)"
      
      # A vs B = 7 - 2
      --assign="F,7"
      
      # D beat B and scored at least 3 goals in each match
      --invalid="5-9,K"
      --invalid="0-2,O"
      --invalid="7-9,HM"
      
      # E has 11 goals fewer than B"
      "11 - (9 - F + J + K + L - (36 - I - L - N)) = O"
      
      # C finished 1 goal ahead of D
      "(18 - J + M + N) - (27 - H - K - M + O) - 1 = G"
      
      # B were the eventual champions
      "9 - F + J + K + L > max(F + G + H + J, 18 - G - J + M + N)"
      
      # C scored in every match
      --invalid="0,MN"
      --invalid="9,GJ"
      
      # C could have scored 2 more goals against B ...
      --invalid="0-1,J"
      # ... and been champions
      "20 - G - J + M + N > max(F + G + H + J, 7 - F + J + K + L)"
      
      # [optional] neater output
      --template="F, G, H, I, J, K, L, M, N, O"
      --solution=""
      

      Like

  • Unknown's avatar

    Jim Randell 8:46 am on 9 November 2021 Permalink | Reply
    Tags:   

    Teaser 2834: Degrees of freedom 

    From The Sunday Times, 15th January 2017 [link] [link]

    I bought an odd thermometer from an old curiosity shop. On its linear scale the freezing point and boiling point of water were higher than they are on the centigrade scale. In fact the freezing point was a prime number and, higher up the scale, the boiling point was a perfect square. There was only one number on the scale where it actually agreed with the corresponding centigrade temperature. That number was the negative of an odd prime (and not the same prime as the one mentioned earlier).

    On this new scale, what are the freezing and boiling points of water?

    There are now 2500 puzzles available between the Enigmatic Code and S2T2 sites. See [link].

    [teaser2834]

     
    • Jim Randell's avatar

      Jim Randell 8:46 am on 9 November 2021 Permalink | Reply

      A straightforward program finds the answer. The following Python program runs in 48ms.

      Run: [ @replit ]

      from enigma import (primes, irange, inf, div, printf)
      
      def solve():
      
        # consider possible squares (for the boiling point)
        for n in irange(11, inf):
          b = n * n
      
          # consider possible primes (for the freezing point)
          for f in primes.between(2, b - 100):
      
            # compute when the temperature scales coincide
            d = b - f - 100
            if not (d > 0): continue
            v = div(100 * f, d)
            if v is None or not (v > 2 and v != f and v in primes): continue
      
            yield (n, b, f, v)
      
      # find the first solution
      for (n, b, f, v) in solve():
        printf("n={n} b={b} f={f} v={v}")
        break
      

      Solution: The freezing point is at 41. The boiling point is at 961.

      The scales coincide at −5.

      To convert from Celsius you can use:

      y = (46/5)x + 41

      However, if the scales were allowed to coincide at −2, there would be further solutions:

      f = 31, b = 1681 (= 41²), y = (33/2)x + 31, coincide at −2
      f = 71, b = 3721 (= 61²), y = (73/2)x + 71, coincide at −2


      A bit of analysis gives a shorter program:

      The scales coincide at −v where:

      v = 100f / (b − f − 100)

      And v is an odd prime number different from f.

      The prime factorisation of the numerator is: 2 × 2 × 5 × 5 × f.

      So we immediately see:

      v = 5
      b = 21f + 100

      And a short program provides the answer:

      from enigma import (primes, inf, is_square, printf)
       
      for f in primes.irange(2, inf):
        b = 21 * f + 100
        if is_square(b):
          printf("f={f} b={b}")
          break
      

      Alternatively, we can further analyse the expression for b, which is a square, say n²:

      n² = 21f + 100
      n² − 100 = 21f
      (n − 10)(n + 10) = 3 × 7 × f

      Considering the factor that does not contain f:

      n − 10 = 1 ⇒ n = 11, f = 1 [not prime]
      n + 10 = 1 ⇒ n = −9, f = −19/21 [non-integral]
      n − 10 = 3 ⇒ n = 13, f = 23/7 [non-integral]
      n + 10 = 3 ⇒ n = −7, f = −17/7 [non-integral]
      n − 10 = 7 ⇒ n = 17, f = 9 [not prime]
      n + 10 = 7 ⇒ n = −3, f = −13/3 [non-integral]
      n − 10 = 21 ⇒ n = 31, f = 41 [*** SOLUTION ***]
      n + 10 = 21 ⇒ n = 11, f = 1 [not prime]

      Like

  • Unknown's avatar

    Jim Randell 9:08 am on 7 November 2021 Permalink | Reply
    Tags: by: H. G. ApSimon   

    A Brain-Teaser: [Boxes and ladders] 

    From The Sunday Times, 12th April 1957 [link]

    A schoolmaster set a problem of the following type to each of four pupils:

    “A ladder of length ___ rests squarely, and more steeply than, 45 degrees, against a (vertical) wall, with its foot on the (horizontal) ground distant ___ from the base of the wall. A cubical box fits flush into the angle of the wall and the ground, and just touches the ladder. What is the length of side of the box?”

    and he filled in the gaps in the problem as set out with Integral numbers of inches such that the answer would also be an integral number of inches.

    To each pupil, however, he gave different values for the length of ladder (all less than 150 ft.) and for the distance of its foot from the base of the wall. The answers submitted to him by the four pupils were all the same, and all were correct.

    What were the four different pairs of values he gave to his pupils, and what was the common answer implied by them?

    This is one of the occasional Holiday Brain Teasers published in The Sunday Times prior to the start of numbered Teasers in 1961. A prize of £5 was offered.

    This puzzle was originally published with no title.

    [teaser-1957-04-12] [teaser-unnumbered]

     
    • Jim Randell's avatar

      Jim Randell 9:08 am on 7 November 2021 Permalink | Reply

      The distance along the ground, x, the height up the wall, y, and the length of the ladder, z, form a Pythagorean Triple (x, y, z).

      And the side of the cube, k, is then given by:

      k = xy / (x + y)

      The following Python program runs in 48ms.

      Run: [ @replit ]

      from enigma import (pythagorean_triples, div, group, unpack, printf)
      
      # generate (x, y, z, k) solutions
      def generate(Z):
        for (x, y, z) in pythagorean_triples(Z):
          k = div(x * y, x + y)
          if k is not None:
            yield (x, y, z, k)
      
      # collect (x, y, z, k) solutions by the value of k
      # z < 150 ft = 1800 in
      d = group(generate(1799), by=unpack(lambda x, y, z, k: k))
      
      # look for k values with 4 (or more) solutions
      for (k, vs) in d.items():
        if len(vs) < 4: continue
        printf("k={k} [{n} triangles]", n=len(vs))
        for (x, y, z, k) in vs:
          printf("  x={x} y={y} z={z}")
        printf()
      

      Solution: The (length, distance) pairs given to the students were (in inches): (1189, 820), (1225, 735), (1547, 595), (1739, 564). The common answer was 420 inches.

      Like

    • GeoffR's avatar

      GeoffR 11:01 am on 10 November 2021 Permalink | Reply

      Using the pythagorean_triples function from enigma.py, this function uses x < y < z, so there is no need to check the ladder angle is greater than 45 degrees, as y is always greater than x.

      Let cube side = c and ladder angle with ground = A
      Let y = p + c and x = c + q

      Tan A = p / c = c / q, so (y – c) / c = c /(x – c)
      This simplifies to c = (y * x) / (y + x)

      
      from enigma import pythagorean_triples
      from collections import defaultdict
      
      BT1 = defaultdict(list)
      
      for x, y, z in pythagorean_triples(1799):
          # find integer value of cube side
          q, r = divmod(x * y, x + y)
          if q > 0 and r == 0:
              BT1[q] += [(x, y, z)]
      
      for k, v  in BT1.items():
          # Looking for four sets of values for (x, y, z)
          if len(v) > 3:
              print(f"Cube side = {k}")
              print(f"Triangles: {v}")
      

      Like

    • John Crabtree's avatar

      John Crabtree 3:57 pm on 11 November 2021 Permalink | Reply

      I think that this brain teaser is very hard to tackle manually. Hugh ApSimon presents a parameter method for generating the primitive solutions in his book “Mathematical Byways in Ayling, Beeling and Ceiling” which was published in 1984. See Chapter 2 on pages 7-12. Chapter 1 on pages 3-6 is a lead in.

      Like

  • Unknown's avatar

    Jim Randell 4:34 pm on 5 November 2021 Permalink | Reply
    Tags:   

    Teaser 3085: Lucky progression 

    From The Sunday Times, 7th November 2021 [link] [link]

    I wrote down a number which happened to be a multiple of my lucky number. Then I multiplied the written number by my lucky number to give a second number, which I also wrote down. Then I multiplied the second number by my lucky number again to give a third, which I also wrote down. Overall, the three numbers written down used each of the digits 0 to 9 exactly once.

    What were the three numbers?

    [teaser3085]

     
    • Jim Randell's avatar

      Jim Randell 4:44 pm on 5 November 2021 Permalink | Reply

      The following Python program runs in 51ms.

      Run: [ @replit ]

      from enigma import irange, inf, nsplit, flatten, printf
      
      # consider lucky numbers, n
      for n in irange(2, inf):
        # and the initial multiple
        for k in irange(2, inf):
          # the three numbers
          ns = list(k * n ** x for x in (1, 2, 3))
          # collect the digits in the numbers
          ds = flatten(nsplit(n) for n in ns)
          if len(ds) > 10: break
          if len(ds) == 10 and len(set(ds)) == 10:
            printf("ns = {ns} [n = {n}; k = {k}]")
        if k == 2: break
      

      Solution: The three numbers are: 306, 918, 2754.

      The lucky number is 3.

      So we have:

      102 × 3 = 306
      306 × 3 = 918
      918 × 3 = 2754

      Like

    • GeoffR's avatar

      GeoffR 5:34 pm on 5 November 2021 Permalink | Reply

      % A Solution in MiniZinc
      include "globals.mzn";
      
      var 2..10: LN; % my lucky number
      
      var 1..9: A; var 0..9: B; var 0..9: C;
      var 1..9: D; var 0..9: E; var 0..9: F;
      var 1..9: G; var 0..9: H; var 0..9: I; var 0..9: J;
      
      % the three numbers written down have different digits
      constraint all_different ([A, B, C, D, E, F, G, H, I, J]);
      
      % My three numbers
      var 100..999: ABC = 100*A + 10*B + C;
      var 100..999: DEF = 100*D + 10*E + F;
      var 1000..9999: GHIJ = 1000*G + 100*H + 10*I + J;
      
      % First number is a multiple of my lucky number
      constraint ABC mod LN == 0;
      
      % Second and third lucky numbers
      constraint ABC * LN == DEF /\ DEF * LN == GHIJ;
      
      solve satisfy;
      
      output ["My three numbers are " ++show(ABC) ++ ", " ++ show(DEF) ++ 
      " and " ++ show(GHIJ)
      ++ "\n" ++ "Lucky number is " ++ show(LN) ];
      
      
      

      Like

    • NigelR's avatar

      NigelR 5:51 pm on 5 November 2021 Permalink | Reply

      First number must be less than 1000 or the three numbers would have more than 10 digits between them. Code below runs in 39ms:

      for lucky in range(1,100):
          for first in range(1,1000):
              if first%lucky!=0:
                  continue
              second = first*lucky
              third = second*lucky
              res=str(first)+str(second)+str(third)
              if len(set(res)) == 10 and len(res) == 10:
                  print (‘first: ‘, first,’  second: ‘, second,’  third:’, third,’  lucky:’, lucky)
                  exit()
      

      Like

    • GeoffR's avatar

      GeoffR 8:52 am on 6 November 2021 Permalink | Reply

      # Assume a digit split of 3:3:4 for 10 digits for three numbers.
      # The multiplier is less than 10 between two increasing 3-digit numbers
      # The three numbers are ABC, DEF and GHIJ and the lucky number is LN
      
      from enigma import nsplit, all_different
      
      from enigma import Timer
      timer = Timer('ST3085')
      timer.start()
      
      for LN in range(2, 10):
        for ABC in range(102, 987):
          if ABC % LN != 0:
            continue
          A, B, C = nsplit(ABC)
          if not all_different(A, B, C):
            continue
          DEF = ABC * LN
          if DEF < 100 or DEF > 999:
            continue
          D, E, F = nsplit(DEF)
          if not all_different(A, B, C, D, E, F):
            continue
          GHIJ = DEF * LN
          if GHIJ < 1000 or GHIJ > 9999:
            continue
          G, H, I, J = nsplit(GHIJ)
          if not all_different(A, B, C, D, E, F, G, H, I, J):
            continue
          print(f"My three numbers are {ABC}, {DEF} and {GHIJ}")
          timer.stop()
          timer.report()  # 4.98ms (I9 processor)
          
      
      
      
      

      Like

      • Frits's avatar

        Frits 10:33 am on 9 November 2021 Permalink | Reply

        @GeoffR, a digit split of 2:3:5 is also possible.

        Like

    • GeoffR's avatar

      GeoffR 11:09 am on 9 November 2021 Permalink | Reply

      @Frits:
      I decided to try a 3:3:4 digit split first as this seemed a more likely candidate for a solution. As this digit split gave a single answer, I did not try a 2:3:5 digit split.

      Like

  • Unknown's avatar

    Jim Randell 11:15 am on 4 November 2021 Permalink | Reply
    Tags:   

    Teaser 2832: A New Year reminiscence 

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

    Whilst filing away last year’s diary this morning I came across an old diary from my teenage years. In it I can see that in one particular month I went to four parties, three of them being on Saturdays and the other on a Sunday. I wrote down the four dates of the parties in words (in the format “January first” etc.) and found that each of the dates used a different prime number of letters.

    What were the four dates that I wrote down?

    [teaser2832]

     
    • Jim Randell's avatar

      Jim Randell 11:16 am on 4 November 2021 Permalink | Reply

      This Python program finds the first (most recent) year and month satisfying the conditions.

      It runs in 55ms.

      Run: [ @replit ]

      from datetime import date
      from enigma import (irange, int2words, catch, is_prime, subsets, join, sprintf as f, cached, printf)
      
      # map month numbers to English names
      months = 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 (0 < n < 100)
      @cached
      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 int2words(n)[:-1] + 'ieth'
        else:
          return int2words(n - r) + ' ' + ordinal(r)
      
      # format dates
      fmt = lambda x: join((f('"{d}" {n}') for (d, n) in x), sep=", ", enc="[]")
      
      # find solutions (going back in time)
      def solve(month=12, year=2016):
      
        while year > 0:
          # find saturdays and sundays in the specified month
          sats = list()
          suns = list()
          for day in irange(1, 31):
            d = catch(date, year, month, day)
            if d is None: break
            wd = d.weekday()
            if not (wd == 5 or wd == 6): continue
            s = months[month] + " " + ordinal(day)
            n = sum(1 for x in s if x.isalpha())
            if is_prime(n):
              (sats if wd == 5 else suns).append((s, n))
      
          # choose three saturdays
          for sat in subsets(sats, size=3):
            ps = set(p for (s, p) in sat)
            if len(ps) != 3: continue
            sun = list((s, p) for (s, p) in suns if p not in ps)
            if sun:
              yield (month, year, sat, sun)
      
          # move back a month
          month -= 1
          if month == 0: (month, year) = (12, year - 1)
      
      # find the first solution
      for (month, year, sat, sun) in solve():
        printf("month={month}, year={year}, sat={sat}, sun={sun}", sat=fmt(sat), sun=fmt(sun))
        break
      

      Solution: The four dates are: August fifth, August twelfth, August twenty sixth, August twenty seventh.

      With 11, 13, 17, and 19 letters respectively.

      The first viable year (counting back from 2016) is 2006.

      If we suppose the setter was 16 when attending the parties that gives an age in 2016 of 26.

      But there are further solutions going back in time (but they all give the same answer to the puzzle):

      year = 2000; current age = 32
      year = 1995; current age = 37
      year = 1989; current age = 43
      year = 1978; current age = 54
      year = 1972; current age = 60
      year = 1967; current age = 65
      year = 1961; current age = 71
      year = 1950; current age = 82
      year = 1944; current age = 88
      year = 1939; current age = 93
      year = 1933; current age = 99
      year = 1922; current age = 110
      year = 1916; current age = 116
      year = 1911; current age = 121
      year = 1905; current age = 127

      I won’t speculate on the probable age of the setter.

      Like

    • Frits's avatar

      Frits 2:31 pm on 4 November 2021 Permalink | Reply

      Some constants have been taken from Brian Gladman’s site.
      I didn’t bother to put in code for leap years (28/29 in February, the month always has to be August anyway).

        
      import datetime
      
      MONTHS = ( "January", "February", "March", "April", "May", 
                 "June", "July", "August", "September", "October", 
                 "November", "December" )
       
      DAYS = ( "first", "second", "third", "fourth", "fifth", "sixth", 
               "seventh", "eighth", "ninth", "tenth", "eleventh", "twelfth", 
               "thirteenth", "fouteenth", "fifteenth", "sixteenth", 
               "seventeenth", "eighteenth","nineteenth", "twentieth", 
               "twentyfirst", "twentysecond", "twentythird","twentyfourth",
               "twentyfifth", "twentysixth", "twentyseventh", "twentyeighth", 
               "twentyninth", "thirtieth", "thirtyfirst" )
               
      WEEKDAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", 
                  "Saturday", "Sunday"]
               
      P = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29}
       
      # days in month (29 for February)
      DIM = ( 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 )
      
      minm = min(len(m) for m in MONTHS)
      maxm = max(len(m) for m in MONTHS)
      
      mind = min(len(d) for d in DAYS)
      maxd = max(len(d) for d in DAYS)
      
      primes = [x for x in range(minm + mind, maxm + maxd + 1) if x in P]
      
      if len(primes) < 4: 
        print("no solution")
        exit()
      
      # skip months that are too short to make the 4th prime
      ms = [m for m in MONTHS if len(m) + maxd >= primes[3]]
      
      # skip months that are too long to make the 1st prime
      ms = [m for m in ms if len(m) + mind <= primes[-4]]
      
      # pick one value from each entry of a <k>-DIMensional list <ns>
      def pickOneFromEach(k, ns, s=[]):
        if k == 0:
           s = sorted(s)
           # first entry must be either a day later than the rest or same week day
           if tuple(sorted((s[0] - x) % 7 for x in s[1:])) in \
                   {(1, 1, 1), (0, 0, 6)}:
             yield s
        else:
          for n in ns[k-1]:
            # day numbers must be equal or one apart
            if not s or all((n - x) % 7 in {0, 1, 6} for x in s):
              yield from pickOneFromEach(k - 1, ns, s + [n])
      
      # process all viable months
      for m in ms:
        lenm = len(m)
        mno = MONTHS.index(m) + 1
        
        # make list of different month+day lengths
        lst = [[] for _ in range(4)]  
        for i, d in enumerate(DAYS[:DIM[mno] - 1], start=1):
          totlen = lenm + len(d)
          if totlen not in primes: continue
          lst[primes.index(totlen)].append(i)
        
        # check whether we can find a combination of <lst> entries with
        # three same week days and one on the following day
        cands = list(pickOneFromEach(4, lst))
        for c in cands:
          for y in range(2016, 1900, -1):    
            wdays = [WEEKDAYS[datetime.date(year=y, month=mno, day=d).weekday()] 
                     for d in c]
      
            if sorted(wdays) == ['Saturday', 'Saturday', 'Saturday', 'Sunday']:
              print(f"{y}, {m} {c}")
              break # stop at first solution
      

      Like

  • Unknown's avatar

    Jim Randell 5:45 pm on 2 November 2021 Permalink | Reply
    Tags: ,   

    Brainteaser 1816: Polls apart 

    From The Sunday Times, 6th July 1997 [link]

    A TV station has commissioned a survey of a small sample of its viewers. One quarter said they watched boxing, one third said they watched tennis, and one half said they watched football. All watched at least one sport. When analysed, the results showed that the number of those questioned who watched just one of the sports equalled the square of the number watching all three. The TV people were unconvinced and asked for a second poll with the number of people questioned increased by 50%. Surprisingly, all that was said above about the first poll was still true for the second.

    How many people were questioned in the first poll?

    [teaser1816]

     
    • Jim Randell's avatar

      Jim Randell 5:46 pm on 2 November 2021 Permalink | Reply

      See also: Brain-Teaser 25.

      If the total number of participants is N, and the 7 regions of the Venn Diagram are: B, T, F, BT, BF, TF, BTF, then we have:

      N = B + T + F + BT + BF + TF + BTF
      N/4 = B + BT + BF + BTF
      N/3 = T + BT + TF + BTF
      N/2 = F + BF + TF + BTF
      B + T + F = BTF²

      Summing the middle three, and then replacing (B + T + F) using the last equation:

      (13/12)N = (B + T + F) + 2(BT + BF + TF) + 3BTF
      (13/12)N = BTF² + 2(BT + BF + TF) + 3BTF
      ⇒ 2(BT + BF + TF) = (13/12)N − 3BTF − BTF²

      And from the first equation:

      (BT + BF + TF) = N − (B + T + F) − BTF
      ⇒ (BT + BF + TF) = N − BTF − BTF²

      equating these:

      2N − 2BTF − 2BTF² = (13/12)N − 3BTF − BTF²
      (11/12)N = BTF² − BTF
      ⇒ N = (12/11)BTF(BTF − 1)

      So for a given value of BTF we can calculate the corresponding value for N.

      This Python program considers increasing (BTF, N) values, and looks for an example viable arrangement by calculating values for the remaining areas of the Venn diagram. When it finds an N value that is 50% more than a previously determined value it stops.

      It runs in 54ms.

      Run: [ @replit ]

      from enigma import (irange, inf, div, decompose, sq, Matrix, as_int, printf)
      
      # generate viable configurations
      def solve():
        # consider values for the number that watch all 3 sports
        for BTF in irange(2, inf):
          # calculate N
          N = div(12 * BTF * (BTF - 1), 11)
          if N is None: continue
          BTF2 = sq(BTF)
          if BTF + BTF2 > N: continue
          
          # choose values for BT, BF, TF
          for (BT, BF, TF) in decompose(N - BTF - BTF2, 3, increasing=0, sep=0, min_v=0):
      
            # determine the remaining variables
            eqs = [
              # B  T  F BT BF TF BTF = k
              ((1, 1, 1, 1, 1, 1, 1), N), # B + T + F + BT + BF + TF + BTF = N
              ((4, 0, 0, 4, 4, 0, 4), N), # B + BT + BF + BTF = N/4
              ((0, 3, 0, 3, 0, 3, 3), N), # T + BT + TF + BTF = N/3
              ((0, 0, 2, 0, 2, 2, 2), N), # F + BF + TF + BFT = N/2
              ((1, 1, 1, 0, 0, 0, 0), BTF2), # B + T + F = BTF^2
              ((0, 0, 0, 0, 0, 0, 1), BTF), # BTF
              ((0, 0, 0, 1, 0, 0, 0), BT), # BT
              ((0, 0, 0, 0, 1, 0, 0), BF), # BF
              ((0, 0, 0, 0, 0, 1, 0), TF), # TF
            ]
      
            try:
              (B, T, F, BT, BF, TF, BTF) = Matrix.linear(eqs, valid=(lambda x: as_int(x, "0+")))
            except ValueError:
              continue
      
            yield (N, B, T, F, BT, BF, TF, BTF)
            break # only need one example for any N
      
      # record results by N
      seen = set()
      for (N, B, T, F, BT, BF, TF, BTF) in solve():
        N_ = 2 * N // 3
        printf("N={N}; B={B} T={T} F={F} BT={BT} BF={BF} TF={TF} BTF={BTF} [(2/3)N={N_}]")
        if N_ in seen: break
        seen.add(N)
      

      Solution: The number of participants in the first poll was 2160.

      And there were 3240 in the second poll.

      Here are example arrangements:

      N=2160; B=495 T=585 F= 945 BT=0 BF=0 TF= 90 BTF=45
      N=3240; B=755 T=865 F=1405 BT=0 BF=0 TF=160 BTF=55

      (There are many possibilities for (BT, BF, TF), but for each (N, BTF) pair they sum to the same value).


      There are many possible arrangements that satisfy the conditions, but there are fewer that have an N value that is 1.5× a smaller N value.

      The program stops when it finds the first solution, but there are further solutions, although they don’t really qualify as “a small sample”:

      N=211680; B=52479 T=53361 F=88641 BT=0 BF=0 TF=16758 BTF=441
      N=317520; B=78840 T=79920 F=132840 BT=0 BF=0 TF=25380 BTF=540

      N=2032553520; B=508095215 T=508181545 F=846940465 BT=0 BF=0 TF=169293130 BTF=43165
      N=3048830280; B=762154704 T=762260436 F=1270398816 BT=0 BF=0 TF=253963458 BTF=52866

      N=199169502480; B=49791948335 T=49792802905 F=82987719985 BT=0 BF=0 TF=16596603970 BTF=427285
      N=298754253720; B=74688040115 T=74689086745 F=124481462365 BT=0 BF=0 TF=24895141180 BTF=523315

      Like

    • Hugh Casement's avatar

      Hugh Casement 6:30 pm on 2 November 2021 Permalink | Reply

      BTF must be a multiple of 11, or 1 more than that, for N to be an integer.

      For any BTF, N pair there are many possible values for BT, BF, and TF.
      But the sum of those three is determined, as shown in the third grey block of Jim’s analysis,
      and it also equals N/12 – 2BTF.

      It turns out to be negative is BTF is too small.

      Like

    • GeoffR's avatar

      GeoffR 8:02 pm on 2 November 2021 Permalink | Reply

      It is easy just to use Jim’s five basic equations in a MiniZinc solution as constraints, although fixing the ranges of variables was not easy to pre-determine.

      The programme took under 2 sec to find the answer, but nearly 2 minutes to exhaust all the subsequent searching with the Chuffed Solver.

      % A Solution in MiniZinc
      include "globals.mzn";
      % re-using Jim's basic equations
      % using upper case variables for the first survey
      % ..and lower case variables for the second survey
      
      var 1..1000:B; var 1..1000:b;
      var 1..1000:T; var 1..1000:t;
      var 1..2000:F; var 1..2000:f;
      var 0..500:BT; var 0..500:bt;
      var 0..500:BF; var 0..500:bf;
      var 0..1000:BTF; var 0..1000:btf;
      var 0..500:TF; var 0..500:tf;
      var 3..5000:N; var 3..5000:n;
      
      % First Survey - main equations
      constraint N == B + T + F + BT + BF + TF + BTF;
      constraint N == 4 * (B + BT + BF + BTF);
      constraint N == 3 * (T + BT + TF + BTF);
      constraint N == 2 * (F + BF + TF + BTF);
      constraint B + T + F == BTF * BTF;
      
      % Second Survey
      % For second survey,  people questioned increased by 50%
      constraint 2 * n == 3 * N;
      
      % Second survey - main equations
      constraint n == b + t + f + bt + bf + tf + btf;
      constraint n == 4 * (b + bt + bf + btf);
      constraint n == 3 * (t + bt + tf + btf);
      constraint n == 2 * (f + bf + tf + btf);
      constraint b + t + f == btf * btf;
      
      solve satisfy;
      
      output [ "People questioned in the first poll = " ++ show(N) ];
      
      % People questioned in the first poll = 2160
      % ----------
      % ==========
      
      
      

      Like

  • Unknown's avatar

    Jim Randell 9:01 am on 2 November 2021 Permalink | Reply
    Tags:   

    Brain-Teaser 914: Advancing numeracy 

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

    My mathematics class is much more reliable than formerly. I can now be certain that each pupil will always follow a completely true statement by a false one, and vice versa.

    It is clear that some difficulties still arise, but when I asked three of them to select one five-figure number between them, and to make two statements each about it, it was possible to deduce the number they had in mind.

    They used a, b, c, d and e to indicate the positions of the successive digits, which were not necessarily all different.

    Anne said:
    1. The sum of the number and 969 Is divisible by 714.
    2. The sum of c and e is one third of the sum of the five
    digits.

    Bill said:
    1. The sum of the number and 798 is divisible by 693.
    2. The sum of the number and 987 is divisible by 658.

    Carol said:
    1. The sum of b and d equals three times the first digit.
    2. The sum of the number and 988 is divisible by 741.

    What was the number?

    This puzzle is included in the book The Sunday Times Book of Brainteasers (1994). The puzzle text above is taken from the book.

    [teaser914]

     
    • Jim Randell's avatar

      Jim Randell 9:02 am on 2 November 2021 Permalink | Reply

      This Python program runs in 60ms.

      Run: [ @replit ]

      from enigma import irange, nsplit, printf
      
      # consider the 5-digit number
      for n in irange(10000, 99999):
      
        # consider Bill's statements (can't both have the same truth value)
        if ((n + 798) % 693 == 0) == ((n + 987) % 658 == 0): continue
      
        # and Anne's
        (a, b, c, d, e) = nsplit(n)
        if ((n + 969) % 714 == 0) == (3 * (c + e) == a + b + c + d + e): continue
      
        # and Carol's
        if (b + d == 3 * a) == ((n + 988) % 741 == 0): continue
      
        printf("{n}")
      

      Solution: The number was: 32571.

      Anne’s statements are: False, True.

      Bill’s statements are: False, True.

      Carol’s statements are: True, False.

      Liked by 1 person

      • Jim Randell's avatar

        Jim Randell 10:27 am on 3 November 2021 Permalink | Reply

        Or, checking fewer candidates:

        from enigma import irange, nsplit, printf
        
        # exactly one of Bill's statements must be true
        b1 = set(irange(10290, 99999, step=693))
        b2 = set(irange(10199, 99999, step=658))
        for n in b1.symmetric_difference(b2):
          (a, b, c, d, e) = nsplit(n)
          if (
            # Anne
            ((n + 969) % 714 == 0) != (3 * (c + e) == a + b + c + d + e) and
            # Carol
            (b + d == 3 * a) != ((n + 988) % 741 == 0)
          ):
            printf("{n}")
        

        Like

    • GeoffR's avatar

      GeoffR 8:52 am on 13 January 2022 Permalink | Reply

      
      % A Solution in MiniZinc
      include "globals.mzn";
      
      % the five digits - assuming no digit is zero
      var 1..9:a; var 1..9:b; var 1..9:c; var 1..9:d;  var 1..9:e;
      
      % the five digit number
      var 10000..99999:num = 10000*a + 1000*b + 100*c + 10*d + e;
      
      % Anne - one of two statements is true
      constraint ((num + 969) mod 714 == 0) 
      \/ (3 *(c + e) = (a  + b + c + d + e));
      
      % Bill - one of two statements is true
      constraint ((num + 798) mod 693 == 0) 
      \/ ((num + 987) mod 658 == 0);
      
      % Carol - one of two statements is true
      constraint ((b + d) == 3 * a) 
      \/ ((num  + 988) mod 741 == 0);
      
      solve satisfy;
      output ["Number is " ++  show(num) ];
      
      % Number is 32571
      % ----------
      % ==========
      
      
      

      Like

      • Jim Randell's avatar

        Jim Randell 12:51 pm on 13 January 2022 Permalink | Reply

        @GeoffR: This model checks that at least one of the two statements for A, B, C is true. Rather than exactly one.

        But changing \/ to xor (or !=) will do the job.

        Like

    • GeoffR's avatar

      GeoffR 2:31 pm on 13 January 2022 Permalink | Reply

      @ Jim: Ok, I see what you mean. This programme confirms it gets the same answer.

      
      % A Solution in MiniZinc
      include "globals.mzn";
       
      % the five digits - assuming no digit is zero
      var 1..9:a; var 1..9:b; var 1..9:c; var 1..9:d;  var 1..9:e;
       
      % the five digit number
      var 10000..99999:num = 10000*a + 1000*b + 100*c + 10*d + e;
       
      % Anne - one of two statements is true
      constraint ((num + 969) mod 714 == 0) 
      != (3 *(c + e) = (a  + b + c + d + e));
       
      % Bill - one of two statements is true
      constraint ((num + 798) mod 693 == 0) 
      != ((num + 987) mod 658 == 0);
       
      % Carol - one of two statements is true
      constraint ((b + d) == 3 * a) 
      != ((num  + 988) mod 741 == 0);
       
      solve satisfy;
      output ["Number is " ++  show(num) ];
       
      % Number is 32571
      % ----------
      % ==========
      
      

      Like

  • Unknown's avatar

    Jim Randell 9:35 am on 31 October 2021 Permalink | Reply
    Tags: by: R. Crosbie-Hill   

    A Brain-Teaser: [Multiples of 11] 

    From The Sunday Times, 9th June 1957 [link]

    In how many different ways can the following groups of digits be arranged so that the resultant number is a multiple of 11?

    (a) 1, 2, 3, 4, 5.
    (b) 1, 2, 3, 4, 5, 6.
    (c) 1, 2, 3, 4, 5, 6, 7.

    This is one of the occasional Holiday Brain Teasers published in The Sunday Times prior to the start of numbered Teasers in 1961. A prize of £5 was offered.

    This puzzle was originally published with no title.

    [teaser-1957-06-09] [teaser-unnumbered]

     
    • Jim Randell's avatar

      Jim Randell 9:36 am on 31 October 2021 Permalink | Reply

      Again, the advent of computers means that this problem can be tackled with a very simple program, that constructs all possible rearrangements of the digits, and counts those that are divisible by 11.

      The following Python program runs in 56ms.

      Run: [ @replit ]

      from enigma import (subsets, nconcat, icount, printf)
      
      # find numbers that are multiples of 11
      def m11s(ds):
        for ss in subsets(ds, size=len, select="P"):
          n = nconcat(ss)
          if n % 11 == 0:
            yield n
      
      for ds in [(1, 2, 3, 4, 5), (1, 2, 3, 4, 5, 6), (1, 2, 3, 4, 5, 6, 7)]:
        t = icount(m11s(ds))
        printf("{ds} -> {t}")
      

      Solution: (a) 0; (b) 0; (c) 576.

      We can be a bit cleverer though.

      One test for divisibility by 11 is to calculate the alternating sum of the digits (i.e. 1st digit − 2nd digit + 3rd digit − 4th digit + …), and if this is divisible by 11, then the original number is divisible by 11. And it also means that any number formed by rearrangement of the digits, providing the digits originally in odd positions are still in odd positions (and the digits in even positions are still in even positions), will also be divisible by 11.

      The following Python program is a longer, but more efficient method of counting the numbers. (And indeed the internal runtime decreases from 9.03ms to 151µs).

      from enigma import (subsets, factorial, printf)
      
      # count the number of multiples of 11 that can be made from digits ds
      def count_m11s(ds):
        r = 0
        t = sum(ds)
        # consider the 2nd, 4th, ... digits
        n = len(ds)
        k = n // 2
        for ss in subsets(ds, size=k):
          if (t - 2 * sum(ss)) % 11 == 0:
            r += factorial(k) * factorial(n - k)
        return r
      
      for ds in [(1, 2, 3, 4, 5), (1, 2, 3, 4, 5, 6), (1, 2, 3, 4, 5, 6, 7)]:
        t = count_m11s(ds)
        printf("{ds} -> {t}")
      

      In the case of the digits (1, 2, 3, 4, 5, 6, 7), we find that the following numbers in (odd) and (even) positions will form multiples of 11:

      (2, 3, 4, 5) + (1, 6, 7)
      (1, 3, 4, 6) + (2, 5, 7)
      (1, 2, 5, 6) + (3, 4, 7)
      (1, 2, 4, 7) + (3, 5, 6)

      Each collection of digits contributes factorial(4) × factorial(3) = 144 numbers to the total.

      So the answer is: 4 × 144 = 576.

      Like

    • GeoffR's avatar

      GeoffR 12:37 pm on 31 October 2021 Permalink | Reply

      from itertools import permutations
      
      cnt5, cnt6, cnt7 = 0, 0, 0
      
      # 5 consecutive digits - divisibility by 11
      for p1 in permutations((1,2,3,4,5)):
          a,b,c,d,e = p1
          num1 = 10000*a + 1000*b + 100*c + 10*d + e
          if num1 % 11 == 0: cnt5 += 1
      
      print('5 digits divisibility count = ', cnt5)
      
      # 6 consecutive digits - divisibility by 11
      for p2 in permutations((1,2,3,4,5,6)):
          a,b,c,d,e,f = p2
          num2 = 100000*a + 10000*b + 1000*c + 100*d + 10*e + f
          if num2 % 11 == 0: cnt6 += 1
      
      print('6 digits divisibility count = ', cnt6)
      
      # 7 consecutive digits - divisibility by 11
      for p3 in permutations((1,2,3,4,5,6,7)):
          a,b,c,d,e,f,g = p3
          num3 = 1000000*a + 100000*b + 10000*c + 1000*d + 100*e + 10*f + g
          if num3 % 11 == 0: cnt7 += 1
      
      print('7 digits divisibility count = ', cnt7)
      
      # 5 digits divisibility count =  0
      # 6 digits divisibility count =  0
      # 7 digits divisibility count =  576
      
      

      Like

  • Unknown's avatar

    Jim Randell 4:05 pm on 29 October 2021 Permalink | Reply
    Tags:   

    Teaser 3084: Face value 

    From The Sunday Times, 31st October 2021 [link] [link]

    Plato: I have written a different whole number (chosen from 1 to 9 inclusive) on each of the faces of one of my regular solids and have labelled each vertex with the product of the numbers on its adjacent faces. If I tell you the sum of those eight vertex labels, you can’t deduce my numbers, but if I rearrange the numbers on the faces and tell you the new sum, then you can deduce the numbers.

    Eudoxus: Tell me the new sum then.

    Plato: No, but I’ll tell you it’s a 10th power.

    Eudoxus: Aha! I know your numbers now.

    Plato: Yes, that’s right. But if I now told you the original sum, you couldn’t work out which numbers were originally opposite each other.

    What was the original sum?

    [teaser3084]

     
    • Jim Randell's avatar

      Jim Randell 5:01 pm on 29 October 2021 Permalink | Reply

      The cube is the only Platonic Solid [@wikipedia] with 8 vertices.

      This Python program runs in 51ms.

      Run: [ @replit ]

      from enigma import (irange, inf, subsets, partitions, group, unpack, peek, powers, printf)
      
      # generate face pairs and vertex sum for a set of numbers
      def arrange(ns):
        for fs in partitions(ns, 2, distinct=1):
          ((U, D), (L, R), (F, B)) = fs
          # calculate the values on the vertices
          vs = (
            U * L * F, U * R * F, U * L * B, U * R * B,
            D * L * F, D * R * F, D * L * B, D * R * B,
          )
          yield (ns, fs, sum(vs))
      
      # generate all possible face pairs and vertex sums
      def generate():
        for ns in subsets(irange(1, 9), size=6):
          yield from arrange(ns)
      
      # map vertex sum to sets of numbers
      d = group(generate(), by=unpack(lambda ns, fs, t: t), f=unpack(lambda ns, fs, t: ns), fn=set)
      
      # look for vertex sums that are 10th powers
      m = max(d.keys())
      for k in powers(1, inf, 10):
        if k > m: break
        vs = d.get(k, None)
        if vs is None: continue
        printf("{k} -> {vs}")
        assert len(vs) == 1
      
        # map vertex sums (for this set of numbers) to face pairs
        r = group(arrange(peek(vs)), by=unpack(lambda ns, fs, t: t), f=unpack(lambda ns, fs, t: fs))
      
        # look for sums with multiple face pairs (and multiple number sets)
        for (t, fs) in r.items():
          if not (len(fs) > 1 and len(d[t]) > 1): continue
          printf("  {t} -> {fs}")
      

      Solution: Plato’s original labelling gave a vertex sum of 1188.

      There are many ways to achieve this, so we couldn’t tell which set of numbers he used:

      (1+8)(2+9)(5+7)
      (1+8)(3+9)(4+7)
      (1+8)(3+9)(5+6)
      (2+9)(3+6)(4+8)
      (2+7)(3+9)(5+6)
      (2+9)(3+6)(5+7)
      (2+7)(4+8)(5+6)

      But he then rearranged his numbers to give a vertex sum of 1024, which can only be done in one way:

      (2+6)(3+5)(7+9)

      So we know he used the numbers (2, 3, 5, 6, 7, 9).

      He then tells us that even though we know the numbers, we still can’t tell his original arrangement.

      This is because there are two ways to achieve 1188 using this set of numbers:

      (2+7)(3+9)(5+6)
      (2+9)(3+6)(5+7)

      And this is the only set of numbers for 1188 that has more than one arrangement.


      Manually, noting:

      (U + D)(L + R)(F + B) = ULF + ULB + URF + URB + DLF + DLB + DRF + DRB

      gives us a quick way to calculate the vertex sum. It is the same as the product of the three sums of opposite face pairs.

      And this lets us simplify the arrange() function above:

      from enigma import multiply
      def arrange(ns):
        for fs in partitions(ns, 2, distinct=1):
          yield (ns, fs, multiply(x + y for (x, y) in fs))
      

      And we see that the only possible value for the 10th power is 2^10 = 1024.

      So the values on opposite faces must sum to powers of 2:

      (1, 3) = 2^2
      (1, 7) = 2^3
      (2, 6) = 2^3
      (3, 5) = 2^3
      (7, 9) = 2^4

      The only arrangement that gives 1024 is:

      (2, 6) (3, 5) (7, 9)

      We can then look for 2 different arrangements of these numbers into pairs that give the same vertex sum. And 1188 is the only possibility.

      Like

      • Frits's avatar

        Frits 11:54 am on 30 October 2021 Permalink | Reply

        @Jim, len(vs) is likely to be 1 but I am not sure if it can be used for checking if the numbers can be deduced.

        Like

        • Jim Randell's avatar

          Jim Randell 12:03 pm on 30 October 2021 Permalink | Reply

          Yes, we can check the values in vs all use the same numbers:

          vs = set(tuple(sorted(flatten(v))) for v in vs)
          assert len(vs) == 1
          

          And then ns is just this single value.

          I’ll update my code.

          (In fact, I’ve rewritten it to be a bit faster and more compact).

          Like

    • Frits's avatar

      Frits 11:19 am on 30 October 2021 Permalink | Reply

        
      from enigma import SubstitutedExpression, tri
      
      ndigits = 9
      nfaces = 6
      npairs = nfaces // 2
      # highest possible sum
      h = ((tri(ndigits) - tri(ndigits - nfaces)) / npairs) ** npairs
      powers = [i for i in range(2, int(h ** 0.1) + 1)]
      
      # based on the entries in <powers> we can easily deduct the digits used
      # in the new sum (in this case the highest pair sum must be a 4th power)
      
      # the alphametic puzzle
      p = SubstitutedExpression(
        [ # find 2 different arrangements with same sum
          "A < C < E",                    # order the pairs
          "A < B and C < D and E < F",    # order within the pairs
          "G < I < K",                    # order the pairs
          "G < H and I < J and K < L",    # order within the pairs
          
          # different arrangements
          "(A, B, C, D, E, F) < (G, H, I, J, K, L)",
          
          # same sum
          "(A + B) * (C + D) * (E + F) == (G + H) * (I + J) * (K + L)",
        ],
        answer="((A, B), (C, D), (E, F)), ((G, H), (I, J), (K, L)), \
                (A + B) * (C + D) * (E + F)",
        # from manual analysis        
        digits=(2, 3, 5, 6, 7, 9),
        distinct=("ABCDEF","GHIJKL"),
        verbose=0,
      )
      
      # print answers
      for (_, ans) in p.solve():
        print(f"the original sum was {ans[2]}")
      

      Like

      • Jim Randell's avatar

        Jim Randell 1:11 pm on 30 October 2021 Permalink | Reply

        Or you could do:

        from enigma import (partitions, multiply, filter_unique, printf)
        
        # possible face pairs
        pairs = partitions((2, 3, 5, 6, 7, 9), 2, distinct=1)
        
        # calculate vertex sum
        fn = lambda fs: multiply(x + y for (x, y) in fs)
        
        # find vertex sums with multiple face pairs
        for fs in filter_unique(pairs, fn).non_unique:
          printf("{t} -> {fs}", t=fn(fs))
        

        Like

    • GeoffR's avatar

      GeoffR 9:31 am on 5 November 2021 Permalink | Reply

      from enigma import all_different
      from itertools import combinations, permutations
      from collections import defaultdict
      
      D3084 = defaultdict(list)
      
      # If the cube sides are (L1, L2, R1, R2, U, D), the sum
      # of eight vertices = (L1 + L2) * (R1 + R2) * (U + D)
      
      # Since 2 ^ 10 = 1024 and 3 ^ 10 = 59049 and the maximum
      # vertices sum for face digits (1..9) = 2805 (17*15*11)
      # ... the 10th power must be 2 ^ 10 = 1024
      
      # find cube face values for 10th power = 1024
      for p1 in permutations(range(1, 10), 6):
        L1, L2, R1, R2, U, D = p1
        if (L1 + L2) * (R1 + R2) * (U + D) == 1024:
          set_faces = set((L1, L2, R1, R2, U, D))
      
      face_pairs = list(combinations(set_faces, 2))
      
      # map six cube face values to sum of eight vertices
      for A, B, C in combinations(face_pairs, 3):
        if all_different(A[0], A[1], B[0], B[1], C[0], C[1]):
          # digits must be same as 10th power of 1024
          if {A[0], A[1], B[0], B[1], C[0], C[1]} == set_faces:
            VSum = (A[0] + A[1]) * (B[0] + B[1]) * (C[0] + C[1])
            D3084[VSum].append(((A[0], A[1]), (B[0], B[1]), (C[0], C[1])))
      
      for k, v in D3084.items():
        # original number must have non-unique face values
        if len(v) > 1:
          print(f"The original number was {k}")
          print(f"Face Values are {v}")
      
      
      
      

      Like

    • GeoffR's avatar

      GeoffR 9:53 am on 8 November 2021 Permalink | Reply

      This programme ran in 300ms with the Chuffed Solver, but took much longer with the Geocode Solver. I had a constraint to limit the set length of 15 values to 14 (i.e. 1 No. duplicated), but it proved not necessary in practice, and increased the run time substantially to 9.5s. This constraint is commented out in the programme.

      The tenth power of two is shown in the output after the code, as is the original sum of 1188 (i.e. the answer), for two instances.

      % A Solution in MiniZinc
      include "globals.mzn";
      
      % min vertex sum = (1+2) * (3+4) * (5+6) = 231
      % max vertex sum = (9+8) * (7+6) * (5+4) = 1989
      % ..as 2^10 = 1024 and 3^10 = 59049, therefore 1024 is the only
      % possible 10th power within min and max sum limits
      
      % the cube has six faces with different digits 1..9
      var 1..9: a; var 1..9: b; var 1..9: c;
      var 1..9: d; var 1..9: e; var 1..9: f;
      
      constraint all_different([a, b, c, d, e, f]);
      
      % N1..N15 are all the cube possible vertex totals
      var 231..1989: N1; var 231..1989:N2; var 231..1989:N3;
      var 231..1989: N4; var 231..1989:N5; var 231..1989:N6;
      var 231..1989: N7; var 231..1989:N8; var 231..1989:N9;
      var 231..1989: N10; var 231..1989:N11; var 231..1989:N12;
      var 231..1989: N13; var 231..1989:N14; var 231..1989:N15;
      
      % the fifteen possible vertex totals,
      % from six of the possible nine digits 1..9
      constraint N1 == (a+b) * (c+d) * (e+f);
      constraint N2 == (a+b) * (c+e) * (d+f);
      constraint N3 == (a+b) * (c+f) * (d+e);
      
      constraint N4 == (a+c) * (b+d) * (e+f);
      constraint N5 == (a+c) * (b+e) * (d+f);
      constraint N6 == (a+c) * (b+f) * (d+e);
      
      constraint N7 == (a+d) * (b+c) * (e+f);
      constraint N8 == (a+d) * (b+e) * (c+f);
      constraint N9 == (a+d) * (b+f) * (c+e);
      
      constraint N10 == (a+e) * (b+c) * (d+f);
      constraint N11 == (a+e) * (b+d) * (c+f);
      constraint N12 == (a+e) * (b+f) * (c+d);
      
      constraint N13 == (a+f) * (b+c) * (d+e);
      constraint N14 == (a+f) * (b+d) * (c+e);
      constraint N15 == (a+f) * (b+e) * (c+d);
      
      % One of the vertex totals is a 10th power i.e.2 ^ 10 = 1024
      % ...this constraint fixes the six digits for the solution
      constraint sum([
      N1 == 1024, N2 == 1024, N3 == 1024, N4 == 1024, N5 == 1024,
      N6 == 1024, N7 == 1024, N8 == 1024, N9 == 1024, N10 == 1024,
      N11 == 1024, N12 == 1024, N13 == 1024, N14 == 1024, N15 == 1024
      ]) == 1;
                      
      % constraint card ({N1, N2, N3, N4, N5, N6, N7, N8,
      % N9, N10, N11, N12, N13, N14,  N15}) == 14;
                      
      solve satisfy;
      
      output["N1 = " ++ show(N1) ++ ", sides: " ++ show([a,b]) 
      ++ ", " ++ show([c,d]) ++ ", " ++ show([e,f]) 
      ++ "\nN2 = " ++ show(N2) ++ ", sides: " ++ show([a,b]) 
      ++ ", " ++ show([c,e]) ++ ", " ++ show([d,f])
      ++ "\nN3 = " ++ show(N3) ++ ", sides: " ++ show([a,b]) 
      ++ ", " ++ show([c,f]) ++ ", " ++ show([d,e])
      
      ++ "\nN4 = " ++ show(N4) ++ ", sides: " ++ show([a,c]) 
      ++ ", " ++ show([b,d]) ++ ", " ++ show([e,f])
      ++ "\nN5 = " ++ show(N5) ++ ", sides: " ++ show([a,c]) 
      ++ ", " ++ show([b,e]) ++ ", " ++ show([d,f])
      ++ "\nN6 = " ++ show(N6) ++ ", sides: " ++ show([a,c])
       ++ ", " ++ show([b,f]) ++ ", " ++ show([d,e])
      
      ++ "\nN7 = " ++ show(N7) ++ ", sides: " ++ show([a,d]) 
      ++ ", " ++ show([b,c]) ++ ", " ++ show([e,f])
      ++ "\nN8 = " ++ show(N8) ++ ", sides: " ++ show([a,d]) 
      ++ ", " ++ show([b,e]) ++ ", " ++ show([c,f])
      ++ "\nN9 = " ++ show(N9) ++ ", sides: " ++ show([a,d]) 
      ++ ", " ++ show([b,f]) ++ ", " ++ show([c,e])
      
      ++ "\nN10 = " ++ show(N10) ++ ", sides: " ++ show([a,e])
      ++ ", " ++ show([b,c]) ++ ", " ++ show([d,f])
      ++ "\nN11 = " ++ show(N11) ++ ", sides: " ++ show([a,e])
      ++ ", " ++ show([b,d]) ++ ", " ++ show([c,f])
      ++ "\nN12 = " ++ show(N12) ++ ", sides: " ++ show([a,e])
      ++ ", " ++ show([b,f]) ++ ", " ++ show([c,d])
      
      ++ "\nN13 = " ++ show(N13) ++ ", sides: " ++ show([a,f]) 
      ++ ", " ++ show([b,c]) ++ ", " ++ show([d,e])
      ++ "\nN14 = " ++ show(N14) ++ ", sides: " ++ show([a,f]) 
      ++ ", " ++ show([b,d]) ++ ", " ++ show([c,e])
      ++ "\nN15 = " ++ show(N15) ++ ", sides: " ++ show([a,f]) 
      ++ ", " ++ show([b,e]) ++ ", " ++ show([c,d])
      ];
      
      % 15 Possible vertex totals for 6 sides of a cube
      % N1 = 1210, sides: [3, 7], [9, 2], [5, 6]
      % N2 = 1120, sides: [3, 7], [9, 5], [2, 6]
      % N3 = 1050, sides: [3, 7], [9, 6], [2, 5]
      % N4 = 1188, sides: [3, 9], [7, 2], [5, 6]   <<< original sum = 1188 (answer)
      % N5 = 1152, sides: [3, 9], [7, 5], [2, 6]
      % N6 = 1092, sides: [3, 9], [7, 6], [2, 5]
      % N7 = 880, sides: [3, 2], [7, 9], [5, 6]
      % N8 = 900, sides: [3, 2], [7, 5], [9, 6]
      % N9 = 910, sides: [3, 2], [7, 6], [9, 5]
      % N10 = 1024, sides: [3, 5], [7, 9], [2, 6]   <<< 10th power = 1024 (2^10)
      % N11 = 1080, sides: [3, 5], [7, 2], [9, 6]
      % N12 = 1144, sides: [3, 5], [7, 6], [9, 2]
      % N13 = 1008, sides: [3, 6], [7, 9], [2, 5]
      % N14 = 1134, sides: [3, 6], [7, 2], [9, 5]
      % N15 = 1188, sides: [3, 6], [7, 5], [9, 2]   <<< original sum = 1188 (answer)
      % ----------
      
      
      
      

      Like

      • Frits's avatar

        Frits 12:47 pm on 9 November 2021 Permalink | Reply

        The all_different clause has been replaced by a < b < c < d < e < f.
        Also the card statement has been changed to less equal to 14.

        Now the Geocode solver takes less than half a second (printing all solutions) .
        Chuffed still takes 11 seconds.

         
        % A Solution in MiniZinc
        include "globals.mzn";
         
        % min vertex sum = (1+2) * (3+4) * (5+6) = 231
        % max vertex sum = (9+8) * (7+6) * (5+4) = 1989
        % ..as 2^10 = 1024 and 3^10 = 59049, therefore 1024 is the only
        % possible 10th power within min and max sum limits
         
        % the cube has six faces with different digits 1..9
        var 1..4: a; var 2..5: b; var 3..6: c;
        var 4..7: d; var 5..8: e; var 6..9: f;
         
        constraint a < b /\ b < c /\ c < d /\ d < e /\ e < f;
         
        % N1..N15 are all the cube possible vertex totals
        var 231..1989: N1; var 231..1989:N2; var 231..1989:N3;
        var 231..1989: N4; var 231..1989:N5; var 231..1989:N6;
        var 231..1989: N7; var 231..1989:N8; var 231..1989:N9;
        var 231..1989: N10; var 231..1989:N11; var 231..1989:N12;
        var 231..1989: N13; var 231..1989:N14; var 231..1989:N15;
         
        % the fifteen possible vertex totals,
        % from six of the possible nine digits 1..9
        constraint N1 == (a+b) * (c+d) * (e+f);
        constraint N2 == (a+b) * (c+e) * (d+f);
        constraint N3 == (a+b) * (c+f) * (d+e);
         
        constraint N4 == (a+c) * (b+d) * (e+f);
        constraint N5 == (a+c) * (b+e) * (d+f);
        constraint N6 == (a+c) * (b+f) * (d+e);
         
        constraint N7 == (a+d) * (b+c) * (e+f);
        constraint N8 == (a+d) * (b+e) * (c+f);
        constraint N9 == (a+d) * (b+f) * (c+e);
         
        constraint N10 == (a+e) * (b+c) * (d+f);
        constraint N11 == (a+e) * (b+d) * (c+f);
        constraint N12 == (a+e) * (b+f) * (c+d);
         
        constraint N13 == (a+f) * (b+c) * (d+e);
        constraint N14 == (a+f) * (b+d) * (c+e);
        constraint N15 == (a+f) * (b+e) * (c+d);
         
        % One of the vertex totals is a 10th power i.e.2 ^ 10 = 1024
        % ...this constraint fixes the six digits for the solution
        constraint sum([
        N1 == 1024, N2 == 1024, N3 == 1024, N4 == 1024, N5 == 1024,
        N6 == 1024, N7 == 1024, N8 == 1024, N9 == 1024, N10 == 1024,
        N11 == 1024, N12 == 1024, N13 == 1024, N14 == 1024, N15 == 1024
        ]) == 1;
                         
        constraint card ({N1, N2, N3, N4, N5, N6, N7, N8,
         N9, N10, N11, N12, N13, N14,  N15}) <= 14;
                         
        solve satisfy;
         
        output["N1 = " ++ show(N1) ++ ", sides: " ++ show([a,b]) 
        ++ ", " ++ show([c,d]) ++ ", " ++ show([e,f]) 
        ++ "\nN2 = " ++ show(N2) ++ ", sides: " ++ show([a,b]) 
        ++ ", " ++ show([c,e]) ++ ", " ++ show([d,f])
        ++ "\nN3 = " ++ show(N3) ++ ", sides: " ++ show([a,b]) 
        ++ ", " ++ show([c,f]) ++ ", " ++ show([d,e])
         
        ++ "\nN4 = " ++ show(N4) ++ ", sides: " ++ show([a,c]) 
        ++ ", " ++ show([b,d]) ++ ", " ++ show([e,f])
        ++ "\nN5 = " ++ show(N5) ++ ", sides: " ++ show([a,c]) 
        ++ ", " ++ show([b,e]) ++ ", " ++ show([d,f])
        ++ "\nN6 = " ++ show(N6) ++ ", sides: " ++ show([a,c])
         ++ ", " ++ show([b,f]) ++ ", " ++ show([d,e])
         
        ++ "\nN7 = " ++ show(N7) ++ ", sides: " ++ show([a,d]) 
        ++ ", " ++ show([b,c]) ++ ", " ++ show([e,f])
        ++ "\nN8 = " ++ show(N8) ++ ", sides: " ++ show([a,d]) 
        ++ ", " ++ show([b,e]) ++ ", " ++ show([c,f])
        ++ "\nN9 = " ++ show(N9) ++ ", sides: " ++ show([a,d]) 
        ++ ", " ++ show([b,f]) ++ ", " ++ show([c,e])
         
        ++ "\nN10 = " ++ show(N10) ++ ", sides: " ++ show([a,e])
        ++ ", " ++ show([b,c]) ++ ", " ++ show([d,f])
        ++ "\nN11 = " ++ show(N11) ++ ", sides: " ++ show([a,e])
        ++ ", " ++ show([b,d]) ++ ", " ++ show([c,f])
        ++ "\nN12 = " ++ show(N12) ++ ", sides: " ++ show([a,e])
        ++ ", " ++ show([b,f]) ++ ", " ++ show([c,d])
         
        ++ "\nN13 = " ++ show(N13) ++ ", sides: " ++ show([a,f]) 
        ++ ", " ++ show([b,c]) ++ ", " ++ show([d,e])
        ++ "\nN14 = " ++ show(N14) ++ ", sides: " ++ show([a,f]) 
        ++ ", " ++ show([b,d]) ++ ", " ++ show([c,e])
        ++ "\nN15 = " ++ show(N15) ++ ", sides: " ++ show([a,f]) 
        ++ ", " ++ show([b,e]) ++ ", " ++ show([c,d])
        ];
        

        Like

      • Frits's avatar

        Frits 12:50 pm on 9 November 2021 Permalink | Reply

        @Jim/GeoffR,

        Please let me know how you post minizinc programs.

        Like

        • Jim Randell's avatar

          Jim Randell 2:08 pm on 9 November 2021 Permalink | Reply

          @Frits: Details on using [code] ... [/code] tags are here [link].

          For languages that don’t have a supported syntax highlighter you can use [[ language="text" ]].

          Like

  • Unknown's avatar

    Jim Randell 10:16 am on 28 October 2021 Permalink | Reply
    Tags:   

    Brain-Teaser 905: Prime square 

    From The Sunday Times, 25th November 1979 [link]

    “Here”, said Uncle Henry to the twins, “is a magic square which I’ve started for you”:

    “You must complete it by putting eight different prime numbers in the eight empty squares, so that the [rows], the columns and the diagonals add up to the same total; and it must be the smallest possible total under the conditions.”

    There followed half an hour of comparative peace… after which the twins could bear it no longer.

    “Oh. Uncle”, complained Betty, “It looks so easy and yet it’s much too difficult”. And Brian fervently agreed.

    “All right”, said Uncle Henry, “I’ll tell you a couple more things: there is one such magic square where the number in the middle square is the average of the two numbers directly above and below it; the third largest number is not in the right-hand column; and each square contains one or two digits”.

    After a further ten minutes, the twins managed to produce the right solution.

    Can you complete the magic square?

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

    It is also included in the book The Sunday Times Book of Brainteasers (1994).

    [teaser905]

     
    • Jim Randell's avatar

      Jim Randell 10:17 am on 28 October 2021 Permalink | Reply

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

      The following run file executes in 64ms.

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      #  AB  CD  EF
      #  GH  IJ  KL
      #  MN  01  PQ
      
      SubstitutedExpression
      
      --distinct=""
      --invalid=""
      
      # missing numbers are different 1 or 2 digit primes
      "is_prime(AB)"
      "is_prime(CD)"
      "is_prime(EF)"
      "is_prime(GH)"
      "is_prime(IJ)"
      "is_prime(KL)"
      "is_prime(MN)"
      "is_prime(PQ)"
      "all_different(AB, CD, EF, GH, IJ, KL, MN, PQ)"
      
      # in an additive magic square the magic constant is 3 times the centre value (= 3 * IJ)
      "2 * IJ - 1 = CD"
      "2 * IJ - AB = PQ"
      "2 * IJ - EF = MN"
      "2 * IJ - GH = KL"
      "3 * IJ - AB - CD = EF"
      "3 * IJ - MN - 1 = PQ"
      "3 * IJ - AB - GH = MN"
      "3 * IJ - EF - KL = PQ"
      
      # third largest number is _not_ in the RH column
      "ordered(1, AB, CD, EF, GH, IJ, KL, MN, PQ)[-3] not in {EF, KL, PQ}"
      
      --template="(AB CD EF) (GH IJ KL) (MN 01 PQ)"
      --solution=""
      

      Solution: Here is a diagram of the completed square:

      Like

      • Mark Valentine's avatar

        Mark Valentine 1:21 pm on 28 October 2021 Permalink | Reply

        Third largest number can’t be in right-hand column. Need to flip it.

        Like

        • Mark Valentine's avatar

          Mark Valentine 1:23 pm on 28 October 2021 Permalink | Reply

          Apologies. Misread it. Yours is correct.

          Like

      • Frits's avatar

        Frits 3:41 pm on 28 October 2021 Permalink | Reply

        Using “3 * IJ – AB – MN = GH” instead of “3 * IJ – AB – GH = MN” you don’t have to internally loop over G and H.

        Like

      • Jim Randell's avatar

        Jim Randell 12:35 pm on 29 October 2021 Permalink | Reply

        And here is a Python program that can be used to generate magic squares with larger magic constants:

        from enigma import primes, ordered, arg, printf
        
        # the magic square is:
        #
        #  A B C
        #  D E F
        #  G 1 H
        #
        # so the magic constant k = 3E
        
        # generate magic squares
        def generate():
          # consider values for E
          for E in primes.range(3):
            k = 3 * E
            B = k - E - 1
            if B not in primes: continue
        
            # choose a value for A
            for A in primes.range(3, k - E):
              # calculate the remaining values
              H = k - A - E
              if H not in primes: continue
              C = k - A - B
              if C not in primes: continue
              F = k - C - H
              if F not in primes: continue
              G = k - C - E
              if G + 1 + H != k or G not in primes: continue
              D = k - E - F
              if A + D + G != k or D not in primes: continue
              # check conditions
              if len({A, B, C, D, E, F, G, H}) != 8: continue
              ns = ordered(A, B, C, D, E, F, G, 1, H)
              if ns[-3] in {C, F, H}: continue
              # valid layout
              yield (A, B, C, D, E, F, G, H)
        
        N = arg(1, 0, int)
        for (n, (A, B, C, D, E, F, G, H)) in enumerate(generate(), start=1):
          printf("[ {A} {B} {C} | {D} {E} {F} | {G} 1 {H} ] {k}", k=3 * E)
          if n == N: break
        

        Like

    • Hugh Casement's avatar

      Hugh Casement 2:47 pm on 28 October 2021 Permalink | Reply

      It’s superfluous information (though perhaps helpful) that the middle column is an arithmetic progression.
      Rather than say “the third largest number is not in the right-hand column” it would have been simpler to tell us that the smallest prime is in the left-hand column (otherwise we find a reflection in the vertical axis).
      Without the 1 we would need at least two 3-digit primes to make a magic square.

      Like

    • GeoffR's avatar

      GeoffR 4:12 pm on 28 October 2021 Permalink | Reply

      My solution shows that the 3rd highest number must not be in the right hand column is a definite requirement, as shown in the note at the end of the programme output. I initially wrote a programme without this constraint. I am not sure how this constraint would be programmed in MiniZinc.

      % A Solution in MiniZinc
      include "globals.mzn";
      
      %  A  B  C
      %  D  E  F
      %  G  H  I
      
      var 2..97:A; var 2..97:B; var 2..97:C; 
      var 2..97:D; var 2..97:E; var 2..97:F;
      var 2..97:G; var 2..97:I;
      
      int: H == 1;
      constraint all_different([A, B, C, D, E, F, G, H, I]);
      
      predicate is_prime(var int: x) = 
      x > 1 /\ forall(i in 2..1 + ceil(sqrt(int2float(ub(x))))) ((i < x) -> (x mod i > 0));
      
      % All values of the grid (except H) are primes
      constraint is_prime(A) /\ is_prime(B) /\ is_prime(C)
      /\ is_prime(D) /\ is_prime(E) /\ is_prime(F)
      /\ is_prime(G) /\ is_prime(I);
      
      % All rows, columns and diagonals add to the same total
      constraint A + B + C == D + E + F /\ A + B +  C == G + H + I
      /\  A + B + C == A + D + G /\  A + B + C == B + E + H 
      /\  A + B + C == C + F + I /\  A + B + C == A + E + I
      /\  A + B + C == C + E + G;
      
      % the middle square is the average of the two numbers directly above and below it
      constraint 2 * E == B + H \/ 2 * D == A + G \/ 2 * F == C + I;
      
      %  the smallest possible total
      solve minimize(A + B + C);
      
      output [ "[A,B,C,D,E,F,G,H,I] = " ++ show([A,B,C,D,E,F,G,H,I] ) ];
      
      % [A, B, C, D, E, F, G, H, I] = [31, 73, 7, 13, 37, 61, 67, 1, 43]
      % ----------
      % ==========
      % Analysis of this solution
      % -------------------------
      % the third highest number (61) must not be in the third column
      % it is moved to the left hand column by transposing left and right columns
      % 31  73  7   =>   7  73  31
      % 13  37  61  =>  61  37  13
      % 67   1  43  =>  43   1  67
      
      
      
      

      Like

      • Frits's avatar

        Frits 10:28 am on 29 October 2021 Permalink | Reply

        @GeoffR, you can declare an array, fill it with same elements as the original array and add an “increasing” constraint.

          
        % A Solution in MiniZinc
        include "globals.mzn";
         
        %  A  B  C
        %  D  E  F
        %  G  H  I
         
        var 2..97:A; var 2..97:B; var 2..97:C; 
        var 2..97:D; var 2..97:E; var 2..97:F;
        var 2..97:G; var 2..97:I;
         
        int: H == 1;
        constraint all_different([A, B, C, D, E, F, G, H, I]);
        
        array[1..9] of var 1..97: nbrs = [A, B, C, D, E, F, G, H, I];
        array[1..9] of var 1..97: sorted;
        
        % make sure both arrays contain same elements
        constraint forall(X in nbrs)(
                      exists(i in 1..9) ( X = sorted[i])
        );
        
        % make sure array "sorted" is sorted
        constraint increasing(sorted);
         
        predicate is_prime(var int: x) = x > 1 /\ 
        forall(i in 2..1 + ceil(sqrt(int2float(ub(x))))) ((i < x) -> (x mod i > 0));
         
        % All values of the grid (except H) are primes
        constraint is_prime(A) /\ is_prime(B) /\ is_prime(C)
        /\ is_prime(D) /\ is_prime(E) /\ is_prime(F)
        /\ is_prime(G) /\ is_prime(I);
         
        % All rows, columns and diagonals add to the same total
        constraint A + B + C == D + E + F /\ A + B + C == G + H + I
        /\  A + B + C == A + D + G /\  A + B + C == B + E + H 
        /\  A + B + C == C + F + I /\  A + B + C == A + E + I
        /\  A + B + C == C + E + G;
         
        % the middle square is the average of the two numbers
        % directly above and below it
        constraint 2 * E == B + H;
        
        % third largest number is not in the RH column
        constraint C != sorted[7] /\ F != sorted[7] /\ I != sorted[7];
         
        %  the smallest possible total
        solve minimize(A + B + C);
         
        output [show([A,B,C]) ++ "\n" ++ show([D,E,F]) ++ "\n" ++ show([G, H, I])];
        

        Like

    • GeoffR's avatar

      GeoffR 11:00 am on 29 October 2021 Permalink | Reply

      @Frits:

      Yes, that is a neat solution to ensure that the third largest number is not in the right-hand column.

      Like

  • Unknown's avatar

    Jim Randell 9:31 am on 26 October 2021 Permalink | Reply
    Tags:   

    Teaser 2833: Celebrity dogs 

    From The Sunday Times, 8th January 2017 [link] [link]

    Six celebrities appeared on television with their dogs. Each celebrity brought two dogs and between them they had twelve different breeds.

    The celebrities were Clooney, Hathaway, Jackman, Palermo, Rossum and Seyfried. The breeds of dog were Akita, Basenji, Basset, Bull Terrier, Chihuahua, Dalmation, Foxhound, Keeshond, Plott, Poodle, Rottweiler and Setter.

    For the name and breeds in each trio of celebrity plus their two dogs, if you look at any two out of the three then there are just two letters of the alphabet that occur (once or more) in both.

    In alphabetical order of the breeds, please list the initials of the owners (e.g. C, S, A, C, …)

    [teaser2833]

     
    • Jim Randell's avatar

      Jim Randell 9:32 am on 26 October 2021 Permalink | Reply

      I used the standard spelling of “Dalmatian”, but it doesn’t change the answer if you use the version given in the puzzle text.

      This is another puzzle that can be solved using the [[ grouping ]] functionality from the enigma.py library.

      The following Python program runs in 45ms.

      Run: [ @replit ]

      from enigma import (grouping, join, printf)
      
      # categories for this puzzle (using the normal spelling of "Dalmatian")
      names = ( 'Clooney', 'Hathaway', 'Jackman', 'Palermo', 'Rossum', 'Seyfried' )
      dogs = (
        'Akita', 'Basenji', 'Basset', 'Bull Terrier', 'Chihuahua', 'Dalmatian',
        'Foxhound', 'Keeshond', 'Plott', 'Poodle', 'Rottweiler', 'Setter'
      )
      
      # form the 2-gangs
      for gs in grouping.gangs(2, names, dogs, grouping.share_letters(2)):
        # output the gangs
        grouping.output_gangs(names, gs)
      
        # map breeds to their owners
        m = dict()
        for (n, ds) in zip(names, gs):
          m.update((d, n) for d in ds)
        # output owner initials by breed
        printf("-> {s}", s=join((m[d][0] for d in dogs), sep=" "))
      

      Solution: The initials of the owners are: J P H C J H S R C S R P.

      Ownership is as follows:

      Clooney: Bull Terrier, Plott
      Hathaway: Basset, Dalmatian
      Jackman: Akita, Chihuahua
      Palermo: Basenji, Setter
      Rossum: Keeshond, Rottweiler
      Seyfried: Foxhound, Poodle

      Like

  • Unknown's avatar

    Jim Randell 4:18 pm on 22 October 2021 Permalink | Reply
    Tags:   

    Teaser 3083: Village signposts 

    From The Sunday Times, 24th October 2021 [link] [link]

    Inside a shed I saw where the council was preparing signposts for seven neighbouring villages. Between any two villages there is at most one direct road, and the roads don’t meet except at village centres, where the signposts are to stand. The arms were all affixed, and labelled except for one name to be completed on each. The names in clockwise order on each were as follows:

    Barton, Aston, ?
    Barton, Grafton, ?
    Carlton, Forton, Eaton, ?
    Grafton, Aston, ?
    Dalton, Grafton, Eaton, ?
    Barton, Forton, Carlton, ?
    Dalton, Aston, ?

    Starting at Dalton, I walked along roads, taking the road furthest rightwards at each village and returning to Dalton. I chose the first road so that I visited as many other villages as possible with this method.

    In order, what were the other villages I visited?

    [teaser3083]

     
    • Jim Randell's avatar

      Jim Randell 10:18 pm on 22 October 2021 Permalink | Reply

      I’m not quite sure what “furthest rightwards” means in this context. At the moment I’m assuming it means when I arrive in a village I take the “first right” (i.e. the road that is anti-clockwise from that which I entered the village on).

      And using this interpretation I think there is a unique layout and solution.

      I determined the layout manually (but with programmatic assistance, a fully programmatic determination is given below). The following Python program finds the longest “first right” journey on this layout.

      Run: [ @replit ]

      from enigma import (Accumulator, join, printf)
      
      # the layout is determined by teaser3083c.py
      graph = dict(D='BAC', E='BGF', G='CFEB', F='GAE', B='DGEA', A='BFCD', C='DAG')
      
      # make a journey on the graph, taking the "first right" at each village
      def journey(vs):
        while True:
          (v0, v1) = vs[-2:]
          # are we done?
          if v1 == vs[0]:
            return vs
          # choose the next destination
          ds = graph[v1]
          i = ds.index(v0)
          vs.append(ds[i - 1])
      
      # format a journey
      fmt = lambda vs: join(vs, sep=" -> ")
      
      # find maximal length journeys
      r = Accumulator(fn=max, collect=1)
      
      # start at D, and choose initial destination
      for v in graph['D']:
        vs = journey(['D', v])
        printf("{vs}", vs=fmt(vs))
        r.accumulate_data(len(vs), vs)
      
      for vs in r.data:
        printf("longest: {vs}", vs=fmt(vs))
      

      Solution: The other villages visited were: Carlton, Grafton, Barton.

      Here is a diagram that shows the layout of the roads:

      The possible “first right” journeys from D are:

      D → A → C → D
      D → B → A → D
      D → C → G → B → D

      Like

      • Jim Randell's avatar

        Jim Randell 10:34 pm on 23 October 2021 Permalink | Reply

        And here is a fully programmed solution that determines the layout of the graph (as used in the program above).

        It considers possible ways to complete the signs that give 12 roads, with a signpost at each end, and then assembles them into an acceptable layout with no crossing roads. It does this by turning each town into a “molecule” with the specified roads leading out of it, and then allows one molecule to gradually absorb each of the others so that the roads form a viable layout.

        It runs in 603ms.

        Run: [ @replit ]

        from enigma import (union, multiset, ordered, unpack, irange, find, seq_all_different, Accumulator, map2str, printf)
        
        # the given signposts (with known destinations)
        signs = [ 'BA', 'BG', 'CFE', 'GA', 'DGE', 'BFC', 'DA' ]
        
        # determine the labels used
        labels = union(signs)
        assert len(labels) == len(signs)
        
        # choose a distinct element from each set
        def choose(ss, s=[]):
          # are we done?
          if not ss:
            yield s
          else:
            for x in ss[0]:
              if x not in s:
                yield from choose(ss[1:], s + [x])
        
        # look for the longest match between s1 and s2
        def align(s1, s2):
          r = Accumulator(fn=max)
          n = len(s2)
          for j in irange(1, n):
            # does the first element of s2 match s1?
            i = find(s1, s2[0])
            if i != -1:
              # bring it to the front of s1
              if i > 0: s1 = s1[i:] + s1[:i]
              k = 1
              # extend the match if possible
              while k < n and s1[-1] == s2[k]:
                s1.insert(0, s1.pop(-1))
                k += 1
              # all matches must be used
              if seq_all_different(s1[k:] + s2[k:]):
                # save the length of the match and the aligned sequences      
                r.accumulate_data(k, (list(s1), list(s2)))
              if k == n: break
            if j < n:
              # rotate s2
              s2.append(s2.pop(0))
          if r.value: return (r.value,) + (r.data)
          return (None, None, None)
        
        # can we merge the blobs into a single blob?
        def merge(blobs, blob=None):
          rev = lambda s: s[::-1]
          # initial blob?
          if blob is None:
            (i, blob) = max(enumerate(blobs), key=unpack(lambda i, x: len(x)))
            blob = list(rev(x) for x in blob)
            blobs.pop(i)
          # process the remaining blobs
          while blobs:
            # find the most matching of the remaining blobs
            r = Accumulator(fn=max)
            for (j, b) in enumerate(blobs):
              (k, blob_, b_) = align(blob, b)
              if k:
                r.accumulate_data((k, k - len(b)), (j, b_, blob_))
            if not r.value: return False
            ((k, _), (j, b, blob)) = (r.value, r.data)
            # merge blobs
            blob = blob[k:] + list(rev(x) for x in b[k:])
            blobs.pop(j)
          # did everything match up?
          return (not blob)
        
        # check the graph can be laid out (could check graph planarity)
        def check(g):
          # each blob is a list roads
          blobs = list(list(k + v for v in vs) for (k, vs) in g.items())
          # can the blobs be merged?
          return merge(blobs)
        
        # choose a location for each signpost
        for locs in choose(list(labels.difference(s) for s in signs)):
          # choose the missing destinations
          for dests in choose(list(labels.difference(s + v) for (v, s) in zip(locs, signs))):
            # make the graph
            g = dict((v, s + d) for (v, s, d) in zip(locs, signs, dests))
            # count the roads
            rs = multiset()
            for (s, ds) in g.items():
              rs.update_from_seq(ordered(s, d) for d in ds)
            # each road should have 2 ends
            if not all(v == 2 for v in rs.values()): continue
            # can the graph be laid out?
            if not check(g): continue
            # output the graph
            printf("graph = {g}", g=map2str(g))
        

        Without the [[ check() ]] function at line 90, we find there are 8 candidate layouts, which originally I drew out manually, and the “correct” one was immediately obvious. But it was quite fun to come up with an algorithm to determine which of the layouts is viable.

        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