Tagged: by: Stephen Hogg Toggle Comment Threads | Keyboard Shortcuts

  • Unknown's avatar

    Jim Randell 7:04 am on 30 November 2025 Permalink | Reply
    Tags: by: Stephen Hogg   

    Teaser 3297: 6oker 

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

    6oker is a 6-card version of poker. Like 5-card poker the rank of a hand is based on the number of distinct variants of that type possible from a 52-card pack. Fewer variants gives a higher (winning) rank. For example, a running flush ({A, 2, 3, 4, 5} to {10, J, Q, K, A} in one suit) has 40 variants, beating four-of-a-kind (e.g., four aces with another card) which has 624 variants.

    Playing 6oker, Al and Di held hands of different rank. Each comprised only two card values and no aces, jacks, queens or kings (e.g., four 3s and two 6s). These four values had no common prime factors.

    Ignoring suits, if you were told just Al’s hand you couldn’t be sure of Di’s, but if you were told just Di’s you could be sure of Al’s.

    Who won? Ignoring suits, give Al’s hand.

    [teaser3297]

     
    • Jim Randell's avatar

      Jim Randell 7:41 am on 30 November 2025 Permalink | Reply

      With 6 cards of two different values you must be holding either 2 of one value and 4 of the other (936 variants), or holding 3 of each value (1248 variants). (I wrote some code to check all possible 6-card hands to ensure I got these numbers correct).

      So the first type of hand is a higher rank than the second. And one type is Al’s and the other is Di’s (and whoever has the higher rank wins).

      This Python program generates the possible values in each hand, and uses the [[ filter_unique() ]] function from the enigma.py library to determine possible distributions of cards in each hand.

      It runs in 71ms. (Internal runtime is 788µs).

      from enigma import (
        irange, subsets, is_coprime, diff, filter_unique,
        item, intersect, ulambda, sprintf, printf
      )
      
      # generate possible values in the hands
      def generate():
        # find 4 card values with that are pairwise coprime
        for vs in subsets(irange(2, 10), size=4):
          if not is_coprime(*vs): continue
      
          # choose 2 values for A
          for A in subsets(vs, size=2):
            D = diff(vs, A)
            yield (A, D)
      
      # possible distribution of values
      hs = list()
      for ((a1, a2), (d1, d2)) in generate():
        # record possible hands (value, quantity)
        hs.extend([
          (((a1, 3), (a2, 3)), ((d1, 2), (d2, 4))),
          (((a1, 3), (a2, 3)), ((d1, 4), (d2, 2))),
          (((a1, 2), (a2, 4)), ((d1, 3), (d2, 3))),
          (((a1, 4), (a2, 2)), ((d1, 3), (d2, 3))),
        ])
      
      # format a hand
      fmt = ulambda('((v1, q1), (v2, q2)): sprintf("{q1}x {v1}s + {q2}x {v2}s")')
      
      # if we were just told A we couldn't deduce D
      hs1 = filter_unique(hs, f=item(0), g=item(1)).non_unique
      
      # if we were just told D we could deduce A
      hs2 = filter_unique(hs, f=item(1), g=item(0)).unique
      
      # the answer must be in the intersection
      for (A, D) in intersect([hs1, hs2]):
        win = ("A" if D[0][1] == 3 else "D") # whoever has "3 and 3" is the loser
        # output scenario
        printf("A=({A}) D=({D}); win = {win}", A=fmt(A), D=fmt(D))
      

      Solution: Di won. Al was holding three 5s and three 7s.

      Di is holding two cards of one value, and four cards of another value. The values are one of:

      2s and 3s
      2s and 9s
      3s and 4s
      3s and 8s
      4s and 9s
      8s and 9s

      Like

      • Frits's avatar

        Frits 7:55 pm on 2 December 2025 Permalink | Reply

        @Jim,

        Is there a special reason why you determine the intersection?
        I assume list “hs2” can be built directly from list “hs1”.

        Like

        • Jim Randell's avatar

          Jim Randell 8:43 am on 3 December 2025 Permalink | Reply

          @Frits: hs1 is the set of possible hands (ignoring suits) where if we knew A’s hand we could not deduce D’s hand. hs2 is the set of possible hands where if we knew D’s hand we could deduce A’s hand. For a valid solution to the puzzle the hands must satisfy both these conditions, so we look at the intersection of these two sets (i.e. the elements that are in both sets).

          Like

    • Pete Good's avatar

      Pete Good 7:55 pm on 30 November 2025 Permalink | Reply

      Jim, I agree that 3 + 3 has a higher rank than 4 + 2 so the player with 3 + 3 wins, but the comment in your program says that whoever has 3 and 3 is the loser?!

      Like

      • Jim Randell's avatar

        Jim Randell 9:48 pm on 30 November 2025 Permalink | Reply

        @Pete: I calculated that “4 and 2” has fewer variants than “3 and 3”, so “4 and 2” is the better hand.

        Like

    • Pete Good's avatar

      Pete Good 11:13 am on 1 December 2025 Permalink | Reply

      Thanks Jim. I misunderstood the ranking process described in the teaser.

      Like

  • Unknown's avatar

    Jim Randell 6:44 am on 26 October 2025 Permalink | Reply
    Tags: by: Stephen Hogg   

    Teaser 3292: Doctor Hoo’s TARDSI 

    From The Sunday Times, 26th October 2025 [link] [link]

    My blood pressure was 160 systolic over 100 diastolic. I knew 120 over 80 is “normal”, so Doctor Hoo was concerned. She loaned me a TARDSI (Test And Record Diastolic Systolic Indicator) for a few days. I logged 12 blood pressure readings, comprising 24 different values (12 systolic between 120 and 160, and 12 diastolic between 80 and 100). I noticed some curious things about these values. Each systolic:diastolic pair had no repeated digits nor common prime factors. The systolic and diastolic sets each had exactly six odd values, but the lowest and highest values in each set were the only primes. No value was a digit rearrangement of any other and the systolic set had no consecutive values.

    Give the systolic:diastolic pair you can be sure I measured (as SSS:DD e.g. 123:74).

    [teaser3292]

     
    • Jim Randell's avatar

      Jim Randell 8:38 am on 26 October 2025 Permalink | Reply

      I recently added some functionality to the [[ choose() ]] function in the enigma.py library. And we can use that functionality in solving this puzzle.

      There are a fairly limited number of possible sets of values for both the systolic and diastolic readings.

      This Python program finds possible values for each and tries to match up each of the pairs of sets of individual readings to give candidate sets of pairs of readings, and then looks for values that are common to all possible candidate sets.

      It runs in 271ms. (Internal runtime is 196ms).

      from enigma import (
        Accumulator, primes, irange, seq_all_different, flatten, choose, nsplit, cproduct,
        disjoint_cproduct, union, ordered, call, gcd, cache, join, sprintf as f, printf
      )
      
      primes.expand(160)  # primes up to 160
      
      # digit content of numbers
      digits = cache(lambda n: call(ordered, nsplit(n)))
      
      # are there duplicated digits in <ns>?
      def is_duplicate(*ns):
        return not seq_all_different(flatten((digits(n) for n in ns), fn=iter))
      
      # find values with no repeated digits
      def values(a, b, step=1):
        ns = list()
        for n in irange(a, b, step=step):
          if is_duplicate(n): continue
          ns.append(n)
        # lowest and highest values must be prime
        while ns and ns[0] not in primes: ns.pop(0)
        while ns and ns[-1] not in primes: ns.pop(-1)
        return ns
      
      # possible systolic and diastolic values
      sys = values(120, 160)
      dia = values(80, 100)
      
      # check sets of values <vs>
      def check(*vs):
        k = len(vs)
        # only the lowest and highest values are primes
        if (k == 1 or k == 12) != (vs[-1] in primes): return
        # there are [exactly] 6 odd numbers
        n = sum(v % 2 for v in vs)
        if (k == 12 and n != 6) or n > 6: return
        # no value is an anagram of any other
        if digits(vs[-1]) in (digits(vs[i]) for i in irange(0, k - 2)): return
        # looks OK
        return 1
      
      # find possible systolic 12-sets (no consecutive values)
      sss = list(choose(sys, check, 12, increasing=1, gap=2, multi_fns=0))
      printf("[{n} systolic sets]", n=len(sss))
      
      # find possible diastolic 12-sets
      dss = list(choose(dia, check, 12, increasing=1, gap=1, multi_fns=0))
      printf("[{n} diastolic sets]", n=len(dss))
      
      # pair up <ss> values with <ds> values (in some order)
      def pairs(ss, ds):
        # for each value in <ss> find acceptable values in <ds>
        vss = list()
        for s in ss:
          # find values with no repeated digits; no common divisors
          vss.append(set(d for d in ds if not (is_duplicate(s, d) or gcd(s, d) > 1)))
      
        # check all <ds> values are possible
        if not union(vss).issuperset(ds): return
      
        # find viable sequences
        for vs in disjoint_cproduct(vss):
          # return (s, d) pairs
          yield list(zip(ss, vs))
      
      # format a collection of readings
      fmt = lambda vs: join((f("{s}:{d}") for (s, d) in vs), sep=", ", enc="()")
      
      # find common values in solutions
      r = Accumulator(fn=set.intersection)
      
      # match a systolic set with a diastolic set
      for (ss, ds) in cproduct([sss, dss]):
        for ps in pairs(ss, ds):
          printf("pairs = {ps}", ps=fmt(ps))
          r.accumulate(set(ps))
      
      # output solution
      printf("[{n} sequences found]", n=r.count)
      if r.value:
        printf("SOLUTION = {vs}", vs=fmt(r.value))
      

      Solution: The value that must have been recorded was 132:85.

      There 171 possible sets of readings that satisfy the conditions. These are derived from the following sets:

      systolic = (127, 130, 132, 135, 138, 140, 143, 145, 147, 150, 152, 157)
      systolic = (127, 130, 132, 136, 138, 140, 143, 145, 147, 150, 153, 157)
      diastolic = (83, 84, 85, 86, 87, 90, 92, 93, 94, 95, 96, 97)

      Odd numbers are underlined (6 in each set). Primes are in bold (smallest and largest of each set). The systolic values have no consecutive pairs.

      An example combination of readings is as follows (ordered by systolic value):

      (127:84, 130:87, 132:85, 135:86, 138:95, 140:83, 143:90, 145:96, 147:92, 150:97, 152:93, 157:94)

      Each pair has no shared digits, and also no common divisor (> 1).

      The value 132:85 appears in all possible reading sets. The next most common reading is 138:95 (which appears in 152 of the 171 candidate sets).

      Like

      • Jim Randell's avatar

        Jim Randell 3:50 pm on 28 October 2025 Permalink | Reply

        It occurred to me that the systolic and diastolic values form the independent sets of a bipartite graph [ @wikipedia ], where two values are connected iff they have no duplicated digits and are co-prime.

        The candidate sets of readings are then subsets of this graph with 12 systolic and 12 diastolic values, that have a perfect matching between them.

        I added code to graph.py [ @github ] to find perfect matchings of a bipartite graph:

        # bipartite graphs
        
        # subgraph of (x, y) edges that connect X and Y
        def bipartite_subgraph(edges, X, Y):
          (X, Y) = (set(X), set(Y))
          return set((x, y) for (x, y) in edges if x in X and y in Y)
        
        # matchings
        
        # (k, vs) -> len(vs)
        _key_fn = (lambda k_v: len(k_v[1]))
        
        # remove s -> t from adj
        _adj_remove = lambda adj, s, t: dict((k, vs.difference({t})) for (k, vs) in adj.items() if k != s)
        
        def _matching(adj_xy, adj_yx, d):
          # are we done?
          if adj_xy and adj_yx:
            # find minimal x->y and y->x sets
            ((x, ys), (y, xs)) = (min(adj.items(), key=_key_fn) for adj in (adj_xy, adj_yx))
            if not (xs and ys): return
            # process the smallest choice
            if len(xs) < len(ys):
              ys = {y}
            else:
              xs = {x}
            for (x, y) in cproduct([xs, ys]):
              adj_xy_ = _adj_remove(adj_xy, x, y)
              adj_yx_ = _adj_remove(adj_yx, y, x)
              yield from _matching(adj_xy_, adj_yx_, update(d, [(x, y)]))
          elif not (adj_xy or adj_yx):
            yield d
        
        # find (perfect) matchings in the bipartite graph specified by (x, y) edges
        def find_bipartite_matching(edges, X=None, Y=None):
          # construct x -> y, y -> x adjacency matrices
          adj_xy = (dict((k, set()) for k in X) if X else defaultdict(set))
          adj_yx = (dict((k, set()) for k in Y) if Y else defaultdict(set))
          for (x, y) in edges:
            adj_xy[x].add(y)
            adj_yx[y].add(x)
          fail(not is_disjoint([adj_xy.keys(), adj_yx.keys()]), "matching: graph is not bipartite")
          return _matching(adj_xy, adj_yx, dict())
        

        And then I updated my solution for this puzzle to use graph.py to find candidate pairs of values (and also to use a faster method of finding candidate systolic and diastolic sets).

        The following code runs in 124ms. (Internal runtime is 56ms).

        from enigma import (
          Accumulator, primes, irange, seq_all_different, flatten, subsets, choose,
          nsplit, union, cproduct, ordered, call, gcd, cache, join, sprintf as f, printf
        )
        import graph
        
        primes.expand(160)  # primes up to 160
        
        # digit content of numbers
        digits = cache(lambda n: call(ordered, nsplit(n)))
        
        # are there duplicated digits in <ns>?
        def is_duplicate(*ns):
          return not seq_all_different(flatten((digits(n) for n in ns), fn=iter))
        
        # find values with no repeated digits
        def values(a, b, step=1):
          ns = list()
          for n in irange(a, b, step=step):
            if is_duplicate(n): continue
            ns.append(n)
          # lowest and highest values must be prime
          while ns and ns[0] not in primes: ns.pop(0)
          while ns and ns[-1] not in primes: ns.pop(-1)
          return ns
        
        # possible systolic and diastolic values
        sys = values(120, 160)
        dia = values(80, 100)
        
        # check sets of values (4 odd; 6 even)
        def check(*ns):
          k = len(ns)
          # there are exactly 4 odd and 6 even numbers
          odd = sum(n % 2 for n in ns)
          if (k == 10 and odd != 4) or odd > 4 or k - odd > 6: return
          # looks OK
          return 1
        
        # find 12-sets of values
        def find_sets(vs, gap=1):
          # find primes in vs
          prs = list(v for v in vs if v in primes)
          # choose two primes
          for (p, q) in subsets(prs, size=2):
            # that we can fit another 10 numbers between
            if q - p < 11 * gap: continue
            # remaining numbers to choose from
            rs = list(v for v in vs if p + gap <= v <= q - gap and v not in prs)
        
            # choose 10 more numbers (4 odd (non-primes), 6 even)
            for ns in choose(rs, check, 10, increasing=1, gap=gap, multi_fns=0):
              # construct the complete list
              ns.insert(0, p)
              ns.insert(-1, q)
              # no value is an anagram of any other
              if not seq_all_different(digits(n) for n in ns): continue
              yield ns
        
        # find possible systolic 12-sets (no consecutive values)
        sss = list(find_sets(sys, gap=2))
        printf("[{n} systolic sets]", n=len(sss))
        
        # find possible diastolic 12-sets
        dss = list(find_sets(dia))
        printf("[{n} diastolic sets]", n=len(dss))
        
        # find viable value pairs (no repeated digits; no common factors)
        viable = lambda s, d: not (is_duplicate(s, d) or gcd(s, d) > 1)
        edges = set((s, d) for (s, d) in cproduct(union(xs) for xs in (sss, dss)) if viable(s, d))
        
        # pair up <ss> values with <ds> values (in some order)
        def pairs(ss, ds):
          # find perfect matchings in the bipartite graph
          subg = graph.bipartite_subgraph(edges, ss, ds)
          for m in graph.find_bipartite_matching(subg, ss, ds):
            # return (s, d) pairs
            yield list((s, m.get(s)) for s in ss)
        
        # format a collection of readings
        fmt = lambda vs: join((f("{s}:{d}") for (s, d) in vs), sep=", ", enc="()")
        
        # find common values in solutions
        r = Accumulator(fn=set.intersection)
        
        # match a systolic set with a diastolic set
        for (ss, ds) in cproduct([sss, dss]):
          for ps in pairs(ss, ds):
            printf("pairs = {ps}", ps=fmt(ps))
            r.accumulate(set(ps))
        
        # output solution
        printf("[{n} sequences found]", n=r.count)
        if r.value:
          printf("SOLUTION = {vs}", vs=fmt(r.value))
        

        The parent bipartite graph linking readings is as shown:

        There are a lot of potential links, but we can simplify the graph as follows:

        We see 91 cannot be linked with any systolic value (as it contains a 1 digit, and all systolic values start with 1). So any candidate set of diastolic values containing 91 can be removed, as it cannot participate in any matching. And this leaves us with a single candidate set of 12 diastolic readings.

        We also see in any two sets of 12 readings that consist of 6 even and 6 odd values, the even values of one set must be linked with the odd values of the other set. So we can also remove any odd-odd links in the parent graph.

        And we can then remove any further candidate sets that include a disconnected vertex (as we did with 91). This brings us down to the two candidate sets of 12 systolic values given above.

        This leaves us with the following simplified graph:

        We see there are only 12 candidate diastolic values, so each of these must occur in any solution set. And the only systolic value that 85 can be linked with is 132 (shown in red), so this pair must appear in any viable set of readings. (Although this does not show that there are viable sets of readings).

        Like

    • Frits's avatar

      Frits 2:27 am on 27 October 2025 Permalink | Reply

      from itertools import combinations as comb, product
      from math import gcd
      from collections import defaultdict
      
      # set of prime numbers between 80 and 160
      P = [3, 5, 7, 11]
      P = [x for x in range(81, 161, 2) if all(x % p for p in P)]
      
      sysF, sysT = (120, 160)
      diaF, diaT = (80, 99) # not including 100 (no repeated digits)
      # prime numbers
      sysP = [p for p in P if sysF <= p <= sysT] 
      diaP = [p for p in P if diaF <= p <= diaT] 
      
      # possible values for sets
      sys = [n for n in range(sysF, sysT + 1) if len(set(str(n))) == len(str(n))]
      dia = [n for n in range(diaF, diaT + 1) if "1" not in (s := str(n)) and 
                                                    len(set(s)) == len(s)]
      
      # combine each systolic value with a diastolic value
      def solve(s, d, ss=[]):
        if not s:
          yield ss
        else:
          for y in d:
            # had no repeated digits nor common prime factors
            if set(str(s[0])).isdisjoint(str(y)) and gcd(s[0], y) < 2:
              yield from solve(s[1:], d.difference([y]), ss + [(s[0], y)])
              
      # generate systolic or a diastolic sets
      def gen_sets(s, p, check=0):
        # choose the 2 primes
        for p1, p2 in comb(p, 2):
          if p2 - p1 < 10: continue   # allow for 4 odd numbers in between
          evens = [x for x in s if x % 2 == 0 and p1 + check < x < p2 + check] 
          # choose remaining odd numbers
          for odds in comb([x for x in s if x not in p and x % 2 and p1 < x < p2], 4):
            o = set(odds) | {p1, p2}
            # a specific set had no consecutive values
            if check:
              evens_ = [x for x in evens if all(x + i not in o for i in [-1, 1])]
            # no value was a digit rearrangement of any other
            if len({tuple(sorted(str(x))) for x in o}) != 6: continue   
            o = tuple(o)  
            # choose 6 even numbers  
            for e in comb(evens if not check else evens_, 6):
              vs12 = e + o
              # no value was a digit rearrangement of any other
              if len({tuple(sorted(str(x))) for x in vs12}) != 12: continue 
              yield vs12              
      
      # generate all sets
      systo = list(gen_sets(sys, sysP, 1))
      diasto = list(gen_sets(dia, diaP))
      
      cnt = 0
      dct = defaultdict(int)
      for s, d in product(systo, diasto):
        # combine each systolic value with a diastolic value
        for ps in solve(s, set(d)):
          cnt += 1
          for tup in ps:
            dct[tup] += 1
      
      print("answer:", ' or '.join([str(x) + ":" + str(y) 
                                   for (x, y), v in dct.items() if v == cnt]))
      

      Like

      • Frits's avatar

        Frits 2:54 am on 27 October 2025 Permalink | Reply

        @Jim, please change line 47 to “vs = e + o”.

        I wondered why I had used sorted() in line 47. Interestingly it turns out that putting the even numbers up front is efficient (only 8 ms and 1718 recursions). Putting the odd numbers up front is a lot less efficient (697 ms and 450557 recursions).

        Like

  • Unknown's avatar

    Jim Randell 1:56 am on 27 July 2025 Permalink | Reply
    Tags: by: Stephen Hogg   

    Teaser 3279: It’s a doddle! 

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

    The Holmes’s new maths tutor, Dr Moriarty, wrote several two-digit by three-digit whole number multiplications on the board (with no answer repeated). Young Sherlock whispered to his brother, “That first one’s a doddle and the three-digit number is prime.”

    Mycroft replied, “Yes, it is a doddle, but the answer is also a DODDLE”. Sherlock looked puzzled. Mycroft explained that a a DODDLE is a number with “Descending-Order Digits left to right — all different — DivisibLe Exactly by each digit”.

    Mycroft continued, “All of the above applies to the second multiplication as well”. Sherlock noticed that just one other answer, to the final problem, was a DODDLE, with the three-digit value being odd, but not prime.

    What is the answer to the final problem?

    [teaser3279]

     
    • Jim Randell's avatar

      Jim Randell 1:57 am on 27 July 2025 Permalink | Reply

      This Python program collects numbers that are DODDLEs resulting from a multiplication with a 3-digit prime (in n1s) and those that result from a multiplication with a 3-digit odd non-prime (in n2s).

      We then look for a pair of results from n1s, and a different result from n2s to give a viable solution.

      It runs in 138ms. (Internal runtime is 59ms).

      from enigma import (irange, primes, cproduct, nsplit, is_decreasing, subsets, printf)
      
      # check if n is DODDLE
      def is_doddle(n):
        ds = nsplit(n)
        if 0 in ds: return False
        return all(n % d == 0 for d in ds) and is_decreasing(ds, strict=1)
      
      # collect results where the 3-digit number is prime or non-prime
      (n1s, n2s) = (set(), set())
      for (a, b) in cproduct([irange(10, 99), irange(101, 999, step=2)]):
        n = a * b
        if not is_doddle(n): continue
        if b in primes:
          printf("[1] {a} * {b} = {n}")
          n1s.add(n)
        else:
          printf("[2] {a} * {b} = {n}")
          n2s.add(n)
      
      # consider possible results for the first 2 problems
      for r12 in subsets(n1s, size=2):
        # find a final problem with a different result
        for rf in n2s.difference(r12):
          printf("r12={r12} -> rf={rf}", r12=sorted(r12))
      

      Solution: The answer to the final problem is: 6432.

      There are only 2 possible multiplications where the 3-digit number is prime that give a DODDLE.

      72 × 131 = 9432
      72 × 137 = 9864

      And these give the first two problems (in some order).

      There are only 3 possible multiplications where the 3-digit number is odd but not prime that give a DODDLE:

      24 × 393 = 9432
      24 × 411 = 9864
      32 × 201 = 6432

      But as no results are repeated the final multiplication on this list gives the required answer to the puzzle.


      In fact the only multi-digit DODDLEs are:

      3-digit: 432, 864
      4-digit: 6432, 9432, 9864

      Like

      • Jim Randell's avatar

        Jim Randell 3:44 pm on 27 July 2025 Permalink | Reply

        A faster approach is to generate possible DODDLEs (as there are not very many), and then factorise them into 2- and 3-digit numbers.

        The following Python program runs in 61ms. (Internal runtime is 535µs).

        from enigma import (irange, subsets, nconcat, chain, divisors_pairs, is_prime, printf)
        
        # generate k-digit doddles
        def doddle(k):
          for ds in subsets(irange(9, 1, step=-1), size=k):
            n = nconcat(ds)
            if all(n % d == 0 for d in ds):
              yield n
        
        # record results using 3-digit numbers that are prime and non-prime
        (n1s, n2s) = (set(), set())
        # consider 4- and 5-digit doddles
        for n in chain(doddle(4), doddle(5)):
          # look for 2-digit, 3-digit divisor pairs
          for (a, b) in divisors_pairs(n):
            # a is a 2-digit number
            # b is a 3-digit odd number
            if a > 99 or b < 100: break
            if a < 10 or b > 999 or b % 2 == 0: continue
            if is_prime(b):
              printf("[1] {a} * {b} = {n}")
              n1s.add(n)
            else:
              printf("[2] {a} * {b} = {n}")
              n2s.add(n)
        
        # consider possible results for the first 2 problems
        for r12 in subsets(n1s, size=2):
          # find a final problem with a different result
          for rf in n2s.difference(r12):
            printf("r12={r12} -> rf={rf}", r12=sorted(r12))
        

        Like

    • GeoffR's avatar

      GeoffR 12:31 pm on 28 July 2025 Permalink | Reply

      The teaser description states that there are three Doddle type numbers, which my posting is based upon.

      from enigma import is_prime, nsplit
      
      cnt = 0
      DL = []
      
      def is_doddle(num):
        # 4-digit Doddle numbers
        if 1000 < num < 10000:
          a, b, c, d = nsplit(num)
          # cannot divide by zero
          if 0 not in (b, c, d):
            if len(str(num)) == len(set(str(num))):
              if a > b > c > d and all(num % n == 0 \
              for n in (a, b, c, d)):
                return True
              
          # 5-digit Doddle numbers
          if 10000 < num < 99999:
            a, b, c, d, e = nsplit(num)
            if 0 not in (b, c, d, e):
              if len(str(num)) == len(set(str(num))) :
                if a > b > c > d > e and all(num % n == 0 \
                for n in (a, b, c, d, e)):
                  return True
                                       
          return False
      
      # find the first two DODDLE numbers
      # ... where the three digit number(b) is prime
      for a in range(10, 99):
        for b in range(101, 999):
          if not is_prime(b): continue
          if not is_doddle(a * b): continue
          cnt += 1
          if a * b not in DL:
            DL.append(a * b)
          if cnt == 2: 
            # we are specifically told there are
            # ... only 2 Doddle numbers with b as prime
            print(f"First two Doddle numbers are {DL[0]} and {DL[1]}")
                 
      # find the unique third DODDLE number
      # this three digit number (b) is not prime, but is of odd parity
      for a in range(10, 99):
        for b in range(101, 999):
          if not is_prime(b) and b % 2 == 1:
            if is_doddle(a * b) and a * b not in DL:
              print("The answer to the final problem is", a * b)
      
      
      
      

      Like

  • Unknown's avatar

    Jim Randell 6:36 am on 11 May 2025 Permalink | Reply
    Tags: by: Stephen Hogg   

    Teaser 3268: W-hoops! 

    From The Sunday Times, 11th May 2025 [link] [link]

    The PE game “W-hoops!” involves throwing inflexible hoops to hook onto a W-shaped frame. The torus-shaped hoops all have the same cross-section but different diameters and fit snugly, in contact, into a storage box. The illustration (not to scale) shows side- and top-views with three hoops, but the box actually contains the maximum possible number of hoops, allowing for a hole at the centre. The internal width of the box is a multiple of its internal depth.

    Di preferred maths to PE and after her throws she calculated, using 22/7 for 𝛑, that the total volume of the hoops was over 60% of the internal volume of their box. Knowing this would overstate the value, she then used 3.14 for 𝛑, and calculated a value under 60%.

    How many hoops does the box hold?

    [teaser3268]

     
    • Jim Randell's avatar

      Jim Randell 7:39 am on 11 May 2025 Permalink | Reply

      See [ @wikipedia ].

      In the side view we see that it must be possible to exactly fit a whole number of discs in a line (as the width of the box is an exact multiple of the depth).

      If there is an odd number of discs in the line, then the smallest hoop must have a 1 disc wide hole (so it has a major radius of 2, where the discs are unit radius). And if there is an even number of discs, then the smallest hoop must have a 2 disc wide hole (and have a major radius of 3). The smallest hoop cannot be any larger, as it would then be possible to fit a smaller hoop inside. So these are the only two starting hoops we need to consider (R=2, R=3).

      Python floats are sufficient to find the answer to the puzzle, but for exact computations we can use [[ import rdiv as div ]] instead.

      This Python program runs in 57ms. (Internal runtime is 39µs).

      from enigma import (irange, inf, fdiv as div, sq, printf)
      
      # approximations for pi; (a, b) represents a/b
      pis = [(22, 7), (314, 100)]
      
      # solve starting with a smallest hoop with major radius = R (minor radius = 1)
      def solve(R):
        H = 0
        for n in irange(1, inf):
          H += 2 * R  # vol = pi^2 2R r^2
          w = 2 * (R + 1)  # w = box width
          V = 2 * sq(w)  # V = vol of box (depth = 2)
      
          # calculate percentage volumes using pi = 22/7 and pi = 3.14
          (p1, p2) = (div(100 * H * sq(a), V * sq(b)) for (a, b) in pis)
          # are we done?
          if not (p2 < 60): break
          if p1 > 60:
            # output solution
            printf("{n} hoops [maxR={R} w={w}; V={V} H={H} -> p1={p1:.2f}% p2={p2:.2f}%]", p1=float(p1), p2=float(p2))
          # the next hoop is larger
          R += 2
      
      # start with smallest hoops R=2 and R=3
      solve(2)
      solve(3)
      

      Solution: The box holds 5 hoops.

      I calculated the fraction of the box occupied by the hoops as:

      f = 2 × (3 + 5 + 7 + 9 + 11) × 𝛑^2 / (24 × 24 × 2)
      f = (35/1152) 𝛑^2 ≈ 59.97%

      The two approximations for 𝛑 give:

      𝛑 = 22/7 → f ≈ 60.02%
      𝛑 = 3.14 → f ≈ 59.91%


      Using analysis:

      For n hoops, the major radii R of the hoops follow one of the following sequences:

      R[] = 2, 4, 6, 8, 10, …, 2n → sum(R) = n(n + 1); box width = 4n + 2
      R[] = 3, 5, 7, 9, 11, …, 2n + 1 → sum(R) = n(n + 2); box width = 4n + 4

      In the first case the volumes of the box and the hoops are:

      V = 2(4n + 2)^2
      H = 𝛑^2 . 2n(n + 1)
      H/V = (1/4) 𝛑^2 n(n + 1) / (2n + 1)^2

      And the ratio H/V is 60% at n ≈ 2.52.

      In the second case:

      V = 2(4n + 4)^2
      H = 𝛑^2 . 2n(n + 2)
      H/V = (1/16) 𝛑^2 n(n + 2) / (n + 1)^2

      And the ratio H/V is 60% at n ≈ 5.05.

      And so the second case with n = 5 gives ratios just above and just below 60% for the approximations to 𝛑 given in the puzzle.

      Like

  • Unknown's avatar

    Jim Randell 1:43 am on 23 March 2025 Permalink | Reply
    Tags: by: Stephen Hogg   

    Teaser 3261: The evidence of weight 

    From The Sunday Times, 23rd March 2025 [link] [link]

    The sunbathing pontoon’s four identical, cubic, sealed floats had widths a whole number of centimetres (less than 100). Arkim, Edie’s son, knew that the pontoon’s labelled weight of 20,000 grams would equal the combined submerged volume of the floats in cubic centimetres, if the pool water’s density was one gram per cubic centimetre.

    Later, with Edie alone on the pontoon (once again level in still water), the floats were a whole number of centimetres further partially submerged. Assuming the above, Arkim calculated Edie’s weight to be a whole number of kilograms (under her 90 kg peak, but above her 50 kg “wedding” weight).

    If I were a cad and revealed Edie’s weight, you would be unsure of a float’s width.

    Give Edie’s weight in kilograms.

    [teaser3261]

     
    • Jim Randell's avatar

      Jim Randell 2:12 am on 23 March 2025 Permalink | Reply

      If the floats are cubes with dimension f (cm), and have submerged depth d (cm), then the total submerged volume is: 4.f^2.d (cm^3). And this is equal to the total weight (in grams) supported by the floats.

      The following Python program runs in 76ms. (Internal runtime is 1.9ms).

      from enigma import (Rational, irange, group, unpack, printf)
      
      Q = Rational()
      
      # generate candidate solutions
      def generate():
        # consider possible dimensions of the floats (f: cm)
        for f in irange(1, 99):
          # calculate initial displacement (d: cm)
          A = 4 * f * f
          d = Q(20000, A)  # platform weight = 20 kg
          # consider possible additional displacement (x: cm)
          for x in irange(1, int(f - d)):
            # calculate E's weight (E: kg)
            E = Q(A * x, 1000)
            if E.denominator == 1 and 50 < E < 90:
              printf("[f={f} d={d:.2f}, x={x} -> E={E}]", d=float(d))
              yield (f, d, x, int(E))
      
      # group candidate solutions by E
      g = group(generate(), by=unpack(lambda f, d, x, E: E))
      
      # look for solution with multiple float widths
      for (E, vs) in g.items():
        fs = set(f for (f, d, x, E) in vs)
        if len(fs) > 1:
          printf("E={E} -> fs={fs}", fs=sorted(fs))
      

      Solution: Edie weighs 72 kg.

      Without further information we cannot determine if the floats are 30 cm cubes (and the additional displacement was 20 cm), or they are 60 cm cubes (and the additional displacement was 5 cm).

      There are 8 candidate solutions. Of these there are 6 weights which do give a unique solution, and 1 that has multiple solutions:

      54 kg → 30 cm cubes; 15 cm additional displacement
      60 kg → 50 cm cubes; 6 cm additional displacement
      64 kg → 40 cm cubes; 10 cm additional displacement
      70 kg → 50 cm cubes; 7 cm additional displacement
      80 kg → 50 cm cubes; 8 cm additional displacement
      81 kg → 45 cm cubes; 10 cm additional displacement

      72 kg → 30 cm cubes; 20 cm additional displacement
      72 kg → 60 cm cubes; 5 cm additional displacement

      Eureka!

      Like

    • Frits's avatar

      Frits 3:40 am on 23 March 2025 Permalink | Reply

      # the floats should support at least Edie (>= 51kg) and the pontoon (20 kg)
      
      seen = set()
      
      # Edie weighs <wght> kilograms or 
      # <wght>.8.5^3 grams = 4.h.w^2 so w must be a multiple of 5
      min_w = next(w for w in range(5, 100, 5) if w > (71000 / 4)**(1 / 3))
      
      # cubic width of a float 
      for w in range(min_w, 100, 5):
        # make sure <v> will be a multiple of 1000
        mult = 1 + (w % 2)
        # check if h must also be a multiple of 5
        min_h, step = (5 * mult, 5 * mult) if w % 25 else (mult, mult) 
        # extra submersion in centimeters with Edie on the pontoon
        for h in range(min_h, w, step):
          # extra volume submersed 
          if (v := 4 * h * w**2) > 89000: break
          if v < 51000: continue  
          if v in seen:
            print(f"answer: {v // 1000} kilograms")
          seen.add(v)
      

      Like

    • Frits's avatar

      Frits 5:36 am on 24 March 2025 Permalink | Reply

      A different approach.

      from collections import defaultdict
      
      # the floats should support at least Edie (>= 51kg) and the pontoon (20 kg)
      
      # Edie weighs <wght> kilograms or 
      # <wght>.8.5^3 grams = 4.h.w^2 so w must be a multiple of 5
      min_w = next(w for w in range(5, 100, 5) if w >= (71000 / 4)**(1 / 3))
      
      # if there is a solution with w1, v and h1 and h1 is a multiple of a
      # square (h2 * a^2) there might be another solution v = 4 * h2 * w2^2
      # where w2 = a * w1 and w2 within boundaries
      
      # collect multiples of squares (except 1^2)
      multsq = defaultdict(list)
      for i in range(2, 9):
        for j in range(1, 25):
          if (m := j * i * i) > 99 - min_w: break
          multsq[m] += [i]
      
      sols = set()
      # cubic width of the smaller float 
      for w in range(min_w, 50, 5):
        w25 = w % 25
        # extra submersion in centimeters with Edie on the pontoon
        for h, vs in multsq.items():
          # we need enough factors of 5
          if w25 and h % 5: continue
          # extra volume submerged 
          if (v := 4 * h * w**2) > 89000: break
          if v < 51000: continue  
          if v % 1000: continue
          
          # check if there is a solution with same volume but a larger cubic width
          for i in vs:
            if i * w < 100:
              sols.add(str(v // 1000))
      
      print(f"answer: {' or '.join(sols)} kilograms")
      

      Like

    • GeoffR's avatar

      GeoffR 10:38 am on 26 March 2025 Permalink | Reply

      Multiple output solutions gives two solutions for one particular weight, which is the answer. Other solutions are single solutions for different weights.

      
      % A Solution in MiniZinc
      include "globals.mzn";
      
      var 5..100: w;      % Float width of four floats
      var 0.1..99.9: d1;  % Submerged depth due to self weight
      var 1..99: d2;      % Submerged depth due to Edie's weight
      var 51..89: E;      % Edie's weight in kilograms
      
      % Re-use d2 is a multiple of 5
      constraint d2 mod 5 == 0;
      
      % Find d1
      constraint 4.0 * int2float(w) * int2float(w) * d1 == 20000.0;
      
      % Ensure some freeboard on floats  with Edie's weight
      constraint int2float(d2) < int2float(w) - d1;
      
      % Find d2
      constraint 4 * w * w * d2 / 1000 == E;
      
      solve satisfy;
      
      output ["[ w, d1, d2, E] = " ++ show([w, d1, d2, E])];
      
      
      

      Like

      • Jim Randell's avatar

        Jim Randell 1:10 pm on 29 March 2025 Permalink | Reply

        @Geoff: This doesn’t find the candidate solutions where the floats are 50 cm cubes. This seems to be due to the constraint at line 10, which can be removed.

        However, it then finds an additional candidate where the floats are 100 cm, and this gives a second solution to the puzzle.

        But we can reduce the range at line 4, as the float dimension has to be less than 100 cm. Then we find all 8 candidate solutions, and there is only one case of a duplicate weight for Edie.

        Like

    • GeoffR's avatar

      GeoffR 5:19 pm on 29 March 2025 Permalink | Reply

      @Jim: Yes, fair comment to upgrade ny code.

      Like

    • GeoffR's avatar

      GeoffR 12:10 pm on 30 July 2025 Permalink | Reply

      Here is my updated solution.

      
      % A Solution in MiniZinc
      include "globals.mzn";
       
      var 5..99: w;       % Float width of four floats
      var 0.1..99.9: d1;  % Submerged depth due to self weight
      var 1..99: d2;      % Submerged depth due to Edie's weight
      var 51..89: E;      % Edie's weight in kilograms
       
      % Find d1
      constraint 4.0 * int2float(w) * int2float(w) * d1 == 20000.0;
       
      % Ensure some freeboard on floats  with Edie's weight
      constraint int2float(d2) < int2float(w) - d1;
       
      % Find d2
      constraint 4 * w * w * d2 / 1000 == E;
       
      solve satisfy;
       
      output ["[ w, d1, d2, E] = " ++ show([w, d1, d2, E])];
      
      % [ w, d1, d2, E] = [30.0, 5.55555555555556, 15.0, 54.0]
      % ----------
      % [ w, d1, d2, E] = [30.0, 5.55555555555556, 20.0, 72.0]
      % ----------
      % [ w, d1, d2, E] = [40.0, 3.125, 10.0, 64.0]
      % ----------
      % [ w, d1, d2, E] = [45.0, 2.46913580246914, 10.0, 81.0]
      % ----------
      % [ w, d1, d2, E] = [60.0, 1.38888888888889, 5.0, 72.0]
      % ----------
      % [ w, d1, d2, E] = [50.0, 2.0, 6.0, 60.0]
      % ----------
      % [ w, d1, d2, E] = [50.0, 2.0, 7.0, 70.0]
      % ----------
      % [ w, d1, d2, E] = [50.0, 2.0, 8.0, 80.0]
      % ----------
      % ==========
      %  Edie = 72 kg is the only weight with multiple solutions (2 No.)
      
      
      
      
      

      Like

  • Unknown's avatar

    Jim Randell 9:03 am on 8 December 2024 Permalink | Reply
    Tags: by: Stephen Hogg   

    Teaser 3246: Power Rearrangers – “Morphing times” 

    From The Sunday Times, 8th December 2024 [link] [link]

    Superhero classmates Ann, Ben and Col were Power Rearrangers with birthdays on three consecutive days in the same month. Using “morphing times” super-fast mental arithmetic, each raised that month number to the power of their own birthday number (e.g., June 2nd gives 36). Correctly calculated, the three values’ rightmost digits differed. In descending order, as a three-figure value, these digits made a multiple of the month number, but in ascending order, did not.

    A new Power Rearranger recruit, Dee, joined the class. Her birthday was earlier, but the aforementioned statements also applied to Dee, Ann and Ben. Dee also raised her birthday number to the power of the month number. Curiously, this gave the same rightmost digit as when she did the former calculation.

    Give Dee’s birthday as dd/mm (e.g. 31/01).

    Apologies for the delay in posting this puzzle – we’ve just had a 19 hour power outage.

    [teaser3246]

     
    • Jim Randell's avatar

      Jim Randell 9:43 am on 8 December 2024 Permalink | Reply

      This Python program runs in 67ms. (Internal runtime is 378µs).

      from enigma import (irange, tuples, seq_all_different, nconcat, rev, printf)
      
      # check a sequence of digits
      def check(m, ds):
        # they are all different
        if not seq_all_different(ds): return False
        # in descending order give a multiple of the month
        ds = sorted(ds, reverse=1)
        if nconcat(ds) % m > 0: return False
        # but not in ascending order
        if nconcat(rev(ds)) % m == 0: return False
        # looks good
        return True
      
      # number of days in each month
      mlen = dict(enumerate([31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31], start=1))
      
      # consider possible months
      for (m, n) in mlen.items():
        # (day, digit) pairs for the month
        vs = ((d, pow(m, d, 10)) for d in irange(1, n))
        # consider four consecutive (day, digit) in some month
        for ((D, dD), (A, dA), (B, dB), (C, dC)) in tuples(vs, 4):
          # check D's digit is the same as day^month
          if not (dD == pow(D, m, 10)): continue
          # check (A, B, C) and (D, A, B)
          if not (check(m, [dA, dB, dC]) and check(m, [dD, dA, dB])): continue
          # output solution
          printf("A={A} B={B} C={C} D={D} m={m} -> ds={ds}", ds=[dA, dB, dC, dD])
      

      Solution: Dee’s birthday is: 13/07.

      So:

      D = 13/07; 7^13 mod 10 = 7; 13^7 mod 10 = 7
      A = 14/07; 7^14 mod 10 = 9
      B = 15/07; 7^15 mod 10 = 3
      C = 16/07; 7^16 mod 10 = 1

      And:

      931 ÷ 7 = 133
      139 ÷ 7 = 19.86…

      973 ÷ 7 = 139
      379 ÷ 7 = 54.14…

      Like

    • ruudvanderham's avatar

      ruudvanderham 2:54 pm on 9 December 2024 Permalink | Reply

      from calendar import monthrange
      import istr
      
      for month in range(1, 13):
          for d in range(1, monthrange(2024, month)[1] - 3 + 1):
              for dis in (0, 1):
                  last_three = {str(month ** (d + dis + i))[-1] for i in range(3)}
                  ascending = "".join(sorted(last_three))
                  descending = "".join(sorted(last_three, reverse=True))
                  if not (len(last_three) == 3 and istr.is_divisible_by(descending, month) and not istr.is_divisible_by(ascending, month)):
                      break
              else:
                  if str(month**d)[-1] == str(d**month)[-1]:
                      print(f"{d:02}/{month:02}")
      

      Like

  • Unknown's avatar

    Jim Randell 8:07 am on 13 October 2024 Permalink | Reply
    Tags: by: Stephen Hogg   

    Teaser 3238: Any colour, as long as it’s not black 

    From The Sunday Times, 13th October 2024 [link] [link]

    Henry learnt snooker scoring basics: pot a red (= 1 pt), not counting as a colour, then a colour (yellow = 2 pt, green = 3 pt, brown = 4 pt, blue = 5 pt, pink = 6 pt, black = 7 pt), alternately. Potted reds stay out of play, while a colour potted immediately after a red returns to play. Once the last red and accompanying colour are potted, then pot the colours in the aforementioned sequence. A miss or foul lets your opponent play.

    With fifteen reds available, Henry scored the first points (a prime number) as described, then missed a red with a foul, yielding four points to Ford. Only with these four points could Ford possibly win. Potting all the remaining reds, each followed by colours other than black, then the colours to pink, Ford needed the black to win by one point. He missed with a foul, which ended the game.

    Give Ford’s score.

    [teaser3238]

     
    • Jim Randell's avatar

      Jim Randell 8:40 am on 13 October 2024 Permalink | Reply

      This Python program runs in 140ms. (Internal runtime is 70ms).

      from enigma import (multiset, subsets, is_prime, printf)
      
      # available balls
      colours = { 2, 3, 4, 5, 6, 7 }
      
      # record results
      rs = multiset()
      
      # H potted some (red + colour) to give a prime number
      for hs in subsets(colours, min_size=1, max_size=14, select='R'):
        H = sum(1 + x for x in hs)
        if not is_prime(H): continue
      
        # calculate max possible remaining score
        r = 15 - len(hs)  # remaining reds
        R = (1 + 7) * r + sum(colours)
        # F can only win with 4 extra points from a foul
        if not (R <= H < R + 4): continue
      
        # F then plays the remaining balls
        coloursF = colours.difference({7})  # F does not pot the black
        for fs in subsets(coloursF, size=r, select='R'):
          F = sum(1 + x for x in fs) + sum(coloursF) + 4
          # F needs black to win by one point
          if not (F + 7 == H + 1): continue
          # output solution
          printf("[H = {H} {hs}; F = {F} {fs}]")
          rs.add((H, F))
      
      for (k, n) in rs.most_common():
        printf("(H, F) = {k} [{n} solutions]")
      

      Solution: Ford’s score was 37.

      And Henry’s score, just before Ford committed the foul, was 43.

      In Henry’s turn he had potted 13 reds along with one of the following sets of colours:

      12 yellow (@ 2 points), 1 pink (@ 6 points)
      11 yellow (@ 2 points), 1 green (@ 3 points), 1 blue (@ 5 points)
      11 yellow (@ 2 points), 2 brown (@ 4 points)
      10 yellow (@ 2 points), 2 green (@ 3 points), 1 brown (@ 4 points)
      9 yellow (@ 2 points), 4 green (@ 3 points)

      In Ford’s turn he had potted 2 reds, along with:

      1 blue (@ 5 points), 1 pink (@ 6 points)

      and then the colours from yellow to pink.

      Like

  • Unknown's avatar

    Jim Randell 7:04 am on 11 August 2024 Permalink | Reply
    Tags: by: Stephen Hogg   

    Teaser 3229: Fishy scales 

    From The Sunday Times, 11th August 2024 [link] [link]

    For loads under 99.5 kg, my scales have three 7-segment digits. The left and centre digits show the weight; the right digit shows “r” for rounded to whole kg or “E” for exact kg. The examples show 69 kg rounded, and 0 kg exactly. Overload displays “888”.

    The scales are accurate, but one of the segments in one of the digits is faulty and is always illuminated. For example, two fish boxes were recently loaded together and “68E” appeared. A fish slid off and “62E” appeared. A third box was added and “99E” appeared. The fallen fish was then put back in its box. Curiously, the display didn’t change. Then the first two boxes were removed. A new reading appeared, with rightmost digit “E”.

    Find the reading displayed for the third box alone.

    [teaser3229]

     
    • Jim Randell's avatar

      Jim Randell 7:21 am on 11 August 2024 Permalink | Reply

      The faulty segment must be lit in all the readings given, and the first likely scenario I tried gave a viable solution, so the puzzle was solved manually in a few minutes. However the following Python program performs an exhaustive exploration of the problem space.

      The final digit of the display reads “E”, “r” or “8”, and it is not possible for a single incorrect segment to change one of these displays to a different viable digit. So all the readings must end in “E”, and so we are dealing with exact integer weights, less than 100 kg. This is enough to explore the problem space without requiring any additional analysis.

      The following Python program runs in 72ms. (Internal runtime is 4.1ms).

      Run: [ @replit ]

      from enigma import (irange, intersect, append, join, printf)
      
      # segments on a 7-segment display
      segs = {
        0: {0, 1, 2, 4, 5, 6},
        1: {2, 5},
        2: {0, 2, 3, 4, 6},
        3: {0, 2, 3, 5, 6},
        4: {1, 2, 3, 5},
        5: {0, 1, 3, 5, 6},
        6: {0, 1, 3, 4, 5, 6},
        7: {0, 2, 5},
        8: {0, 1, 2, 3, 4, 5, 6},
        9: {0, 1, 2, 3, 5, 6},
        'E': {0, 1, 3, 4, 6},
        'r': {3, 4},
      }
      
      # segment display for integer weight <w>, with digit <d> segment <s> always lit
      def display(w, d=None, s=None):
        if w > 99: return [segs[8]] * 3
        ds = list(segs[k] for k in divmod(w, 10))
        ds.append(segs['E'])
        # adjust the faulty segment
        if d is not None:
          ds[d] = append(ds[d], s)
        return ds
      
      # output a display (? for a mangled digit)
      def output(ds):
        r = dict((frozenset(v), k) for (k, v) in segs.items())  # reverse the segment map
        return join(r.get(frozenset(x), '?') for x in ds)
      
      # the given displays
      (d68E, d62E, d99E) = (display(x) for x in (68, 62, 99))
      
      # choose the faulty segment
      for (d, xs) in enumerate(zip(d68E, d62E, d99E)):
        for s in intersect(xs):
          # consider the weight of box 1 and 2 (including the fish)
          for b12 in irange(2, 98):
            # display for (box 1 + box 2) is "68E"
            if display(b12, d, s) != d68E: continue
      
            # consider the weight of the fish
            for f in irange(1, b12 - 1):
              # display for (box 1 + box 2 - fish) is "62E"
              if display(b12 - f, d, s) != d62E: continue
      
              # consider the weight of box 3
              for b3 in irange(1, 99 - b12):
                # display of (box 1 + box 2 - fish + box 3) is "99E"
                if display(b12 - f + b3, d, s) != d99E: continue
                # but display does not change if the fish is returned to its box
                if display(b12 + b3, d, s) != d99E: continue
      
                # final display is just b3
                fsegs = display(b3, d, s)
      
                # output solution
                printf("{disp} = {fsegs} [d={d} s={s}; b12={b12} f={f} b3={b3}]", disp=output(fsegs))
      

      Solution: The reading for the third box alone was: “33E”.

      The faulty segment is segment 2 (top right) of the second (middle) digit of the display.

      The first two boxes together (including the fish) weigh 66 kg. The display (incorrectly) reads “68E”.

      The fish weighs 4 kg, so when it is removed the total weight is 62 kg. The display (correctly) reads “62E”.

      The third box weighs 33 kg, so when this is added the total weight is now 95 kg. The display (incorrectly) reads “99E”.

      The fish is then returned to its box, so the total weight is now 99 kg. The display (correctly) reads “99E” (again).

      The first 2 boxes (including the fish) are now removed, leaving only the third box. The display (correctly) reads “33E”.

      Like

  • Unknown's avatar

    Jim Randell 4:31 pm on 24 May 2024 Permalink | Reply
    Tags: by: Stephen Hogg   

    Teaser 3218: Algorithm Al 

    From The Sunday Times, 26th May 2024 [link] [link]

    On his 35th birthday, maths teacher Al’s three younger half-sisters bought him “The Book of Numbers for Nerds” as a tease. It showed how to find right-angle triangles with whole-number sides using any two unequal odd square numbers. You take half their sum; half their difference; and the square root of their product to get the three sides. Any multiple of such a triplet would also work. He told his sisters this and that their ages were the sides of such a triangle. “Algorithm Al!” they yelled.

    Knowing the age of any one sister would not allow you to work out the other ages with certainty, but in one case you could be sure of her place chronologically (youngest, middle or oldest).

    Give the three sisters’ ages (youngest to oldest).

    [teaser3218]

     
    • Jim Randell's avatar

      Jim Randell 5:33 pm on 24 May 2024 Permalink | Reply

      (See also: Teaser 3017).

      The construction of primitive Pythagorean triples in this way is known as Euclid’s formula [@wikipedia].

      This program collects Pythagorean triples with sides less than 35, and looks for triangles where all sides appear in more than one candidate triangle.

      Of the selected triangles we look for a side that can only appear in one specific position in the ordering (again, considering all candidate triangles). And this identifies the required answer.

      It runs in 63ms. (Internal runtime is 193µs).

      Run: [ @replit ]

      from enigma import (
        defaultdict, pythagorean_triples, multiset, singleton, icount, printf
      )
      
      # collect possible triangles
      tris = list(pythagorean_triples(34))
      
      # record positions of each side
      d = defaultdict(multiset)
      for tri in tris:
        for (i, x) in enumerate(tri):
          d[x].add(i)
      
      # check a candidate triangle
      def check_tri(tri):
        # each side must appear in multiple triangles
        if not all(d[x].size() > 1 for x in tri): return
        printf("tri = {tri}; all sides occur in multiple triangles")
        # look for sides that can only appear in a single position
        for x in tri:
          k = singleton(d[x].distinct_elements())
          if k is not None:
            printf("-> {x} only appears in pos {k}")
            yield (x, k)
      
      # check triangles, exactly one side appears in a single position
      for tri in tris:
        if icount(check_tri(tri)) == 1:
          printf("=> SOLUTION: ages = {tri}")
      

      Solution: The three ages are: 15, 20, 25.

      There are just five primitive triangles we are interested in:

      euclid(1, 3) = (3, 4, 5)
      euclid(1, 5) = (5, 12, 13)
      euclid(1, 7) = (7, 24, 25)
      euclid(3, 5) = (8, 15, 17)
      euclid(3, 7) = (20, 21, 29)

      Candidate triangles are formed from these, along with multiples such that the largest side does not exceed 34.

      And the only candidate triangles where every side length occurs in another triangle are:

      (12, 16, 20) = (3, 4, 5) × 4
      (15, 20, 25) = (3, 4, 5) × 5

      And of these sides only 25 always appears as the largest side length:

      (7, 24, 25)
      (15, 20, 25)

      Like

    • Frits's avatar

      Frits 5:55 pm on 24 May 2024 Permalink | Reply

      Following the instructions.

       
      sols = set()
      # odd squares
      osqs = [i * i for i in range(1, 68, 2)]
      for i, a in enumerate(osqs):
        if a > 33: break
        for b in osqs[i + 1:]:
          # calculate sister's ages
          p = (a + b) // 2
          if p > 34: break
          q = abs(a - b) // 2
          r = int((a * b) ** .5)
          # allow for multiples
          for k in range(1, 35):
            if any(x > 34 for x in [k * p, k * q, k * r]): break
            sols.add(tuple(sorted([k * p, k * q, k * r])))
      
      sides = set(y for x in sols for y in x)
      # dictionary of age appearing as y/m/o
      d = {s: [sol.index(s) for sol in sols if s in sol] for s in sides}
      
      # group of sisters that qualify
      gs = [sol for sol in sols if all(len(d[s]) > 1 for s in sol)]
      for sisters in gs:
        if sum([len(set(d[s])) == 1 for s in sisters]) == 1:  # in one case ...
          print("answer:", sisters)   
      

      Like

  • Unknown's avatar

    Jim Randell 4:46 pm on 23 February 2024 Permalink | Reply
    Tags: by: Stephen Hogg   

    Teaser 3205: Colum’s columns of Bob’s bobs 

    From The Sunday Times, 25th February 2024 [link] [link]

    Colum played with Grandpa Bob’s collection of shilling coins. He used them to make minted-year columns for all years from 1900 to 1940 inclusive (in order, in line and touching). Exactly twelve columns had just one coin. This arrangement resembled a bar chart. Pleasingly, it was symmetric about the 1920 column (the tallest of all) and each column’s coin tally was a factor of its year. The total tally was a prime number.

    Later, Bob found eight more shilling coins in an old tin. Colum found that these added one to each of eight columns. Curiously, everything stated above about the arrangement still applied. If I told you the year and final tally for one of those eight columns you could be sure of the total final tally.

    Give this final total.

    [teaser3205]

     
    • Jim Randell's avatar

      Jim Randell 5:23 pm on 23 February 2024 Permalink | Reply

      My original brute force solution was quick to write, but slow to run. It ran in 4.6s. But with a few tweaks we can modify the approach to give a program that runs in a much more acceptable time.

      This Python program runs in 210ms.

      Run: [ @replit ]

      from enigma import (
        defaultdict, irange, divisors, update,  is_prime,
        group, item, singleton, subsets, printf
      )
      
      # fill slots 0-20 with possible values
      vs = [None] * 21
      # slot 20 must be an odd divisor of 1920, and be the largest value
      vs[20] = set(x for x in divisors(1920) if x > 1 and x % 2 == 1)
      m = max(vs[20])
      # remaining slots are divisors of 1900 + i and 1940 - i
      for i in irange(20):
        vs[i] = set(x for x in divisors(1900 + i) if x < m and (1940 - i) % x == 0)
      
      # find possible (left half) sequences
      def solve(k, ss):
        # are we done?
        if k == 21:
          # final value must be the largest
          (final, rest) = (ss[-1], ss[:-1])
          if not (final > max(rest)): return
          # total tally must be prime
          t = 2 * sum(rest) + final
          if not is_prime(t): return
          # there must be 12 1's (in the full sequence)
          if rest.count(1) != 6: return
          # return (<tally>, <sequence>)
          yield (t, ss)
        elif ss[k] is not None:
          # move on to the next index
          yield from solve(k + 1, ss)
        else:
          # choose a value for this index
          for v in vs[k]:
            if v == 1 and ss.count(1) > 5: continue
            yield from solve(k + 1, update(ss, [(k, v)]))
      
      # fill out indices with only one value
      seqs = list(singleton(xs) for xs in vs)
      
      # group complete sequences by tally
      g = group(solve(0, seqs), by=item(0), f=item(1))
      
      # record (<pos>, <value2>) -> <total2>
      ss = defaultdict(set)
      # choose the initial number of coins
      for k1 in sorted(g.keys()):
        k2 = k1 + 8
        if not (k2 in g): continue
      
        # look for first sequence
        for vs1 in g[k1]:
          # there must be (at least) 4 indices with a possible +1
          incs = list(j for (j, x) in enumerate(vs1[:-1]) if x + 1 in vs[j])
          if len(incs) < 4: continue
      
          # look for second sequence
          for js in subsets(incs, size=4):
            # find (<index>, <value2>) values for +1 indices
            rs = list((j, vs1[j] + 1) for j in js)
            # record (<index>, <value2>) -> <total2>
            for x in rs: ss[x].add(k2)
      
            # output scenario
            printf("{k1} -> {vs1}")
            printf("{k2} -> {vs2}", vs2=update(vs1, rs))
            printf("{rs}")
            printf()
      
      # look for keys that give a unique final total
      rs = set()
      for k in sorted(ss.keys()):
        printf("{k} -> {vs}", vs=ss[k])
        t = singleton(ss[k])
        if t is not None:
          rs.add(t)
      printf()
      
      # output solution
      printf("final tally = {rs}", rs=sorted(rs))
      

      Solution: The final total was 139 coins.


      There are 13 possible scenarios that fit the before/after requirements. 11 of these have totals of (131, 139), one has totals of (101, 109), and one has totals of (149, 157).

      And we are told the 1908 (or 1932) column was increased from 2 coins to 3 coins, then that narrows down the possibilities to just 8 of the (131, 139) totals, and so the required answer is 139.

      Here is an example scenario:

      1900, 1940: 4→5
      1901, 1939: 1
      1902, 1938: 2→3
      1903, 1937: 1
      1904, 1936: 8
      1905, 1935: 5
      1906, 1934: 2
      1907, 1933: 1
      1908, 1932: 2→3
      1909, 1931: 1
      1910, 1930: 10
      1911, 1929: 3
      1912, 1928: 2
      1913, 1927: 1
      1914, 1926: 2→3
      1915, 1925: 5
      1916, 1924: 2
      1917, 1923: 3
      1918, 1922: 2
      1919, 1921: 1
      1920: 15
      total: 131→139


      Although my program is a generic solution, there are some handy shortcuts that make a manual solution easier:

      We only need to consider the leftmost 21 values (as the arrangement is symmetric), and there are 6 values that can only have the value 1 (i.e. the two years in question are co-prime), and this gives the 12 columns containing just one coin, and so none of the other columns can have the value 1.

      And this leaves gives a further 5 columns that now only have a single candidate value, and one of them must have the value 5, which means column for 1920 (which must be the largest value) can only be 15.

      Of the remaining 8 columns with multiple candidate values, only 4 of them contain consecutive values, and these give the columns that must differ between the two sequences, so all non-consecutive values can be removed from them.

      Of the four columns with consecutive values, only one of them has a choice of values:

      1908/1932 = 2→3 or 3→4

      So this must be the column we are told that gives us a unique final tally.

      We can consider the two cases:

      [A] = [4→5; 1; 2→3; 1; {2, 4, 8}; {3, 5}; 2; 1; 2→3; 1; {2, 5, 10}; 3; {2, 4, 8}; 1; 2→3; 5; {2, 4}; 3; 2; 1; 15]

      [B] = [4→5; 1; 2→3; 1; {2, 4, 8}; {3, 5}; 2; 1; 3→4; 1; {2, 5, 10}; 3; {2, 4, 8}; 1; 2→3; 5; {2, 4}; 3; 2; 1; 15]

      In the first case the minimum tally for the first sequence is 99 and the maximum is 147.

      So the only prime values in this range where (n + 8) is also prime are: 101 and 131.

      101 is the minimum tally +2, so cannot be made from this case, so it seems likely that being told the 1908/1932 column goes from 2→3 means the final tally goes from 131→139, and this provides the answer.

      We just need to show that there is an initial sequence that gives 131 and that the second case gives rise to more than one tally.

      It is easy enough to find an initial sequence that give 131 (the minimum tally +16), here are all 8:

      [4, 1, 2, 1, 2, 3, 2, 1, 2, 1, 10, 3, 8, 1, 2, 5, 4, 3, 2, 1, 15] → 131
      [4, 1, 2, 1, 2, 5, 2, 1, 2, 1, 10, 3, 8, 1, 2, 5, 2, 3, 2, 1, 15] → 131
      [4, 1, 2, 1, 4, 3, 2, 1, 2, 1, 10, 3, 8, 1, 2, 5, 2, 3, 2, 1, 15] → 131
      [4, 1, 2, 1, 4, 5, 2, 1, 2, 1, 10, 3, 4, 1, 2, 5, 4, 3, 2, 1, 15] → 131
      [4, 1, 2, 1, 8, 3, 2, 1, 2, 1, 10, 3, 2, 1, 2, 5, 4, 3, 2, 1, 15] → 131
      [4, 1, 2, 1, 8, 3, 2, 1, 2, 1, 10, 3, 4, 1, 2, 5, 2, 3, 2, 1, 15] → 131
      [4, 1, 2, 1, 8, 5, 2, 1, 2, 1, 2, 3, 8, 1, 2, 5, 4, 3, 2, 1, 15] → 131
      [4, 1, 2, 1, 8, 5, 2, 1, 2, 1, 10, 3, 2, 1, 2, 5, 2, 3, 2, 1, 15] → 131

      In the second case the range is 101 .. 149, which gives primes of: 101, 131, 149.

      So immediately we see a sequence that gives a tally of 101, as this is the minimum possible:

      [4, 1, 2, 1, 2, 3, 2, 1, 3, 1, 2, 3, 2, 1, 2, 5, 2, 3, 2, 1, 15] → 101

      And a tally of 149 is also straightforward, as it is the maximum possible:

      [4, 1, 2, 1, 8, 5, 2, 1, 3, 1, 10, 3, 8, 1, 2, 5, 4, 3, 2, 1, 15] → 149

      And this is enough to solve the puzzle, but for completeness here are the 3 sequences that give 131 (min +15):

      [4, 1, 2, 1, 4, 5, 2, 1, 3, 1, 5, 3, 8, 1, 2, 5, 4, 3, 2, 1, 15] → 131
      [4, 1, 2, 1, 8, 3, 2, 1, 3, 1, 5, 3, 8, 1, 2, 5, 2, 3, 2, 1, 15] → 131
      [4, 1, 2, 1, 8, 5, 2, 1, 3, 1, 5, 3, 4, 1, 2, 5, 4, 3, 2, 1, 15] → 131

      And together these are the 13 sequences found by my program.

      Like

  • Unknown's avatar

    Jim Randell 4:42 pm on 5 January 2024 Permalink | Reply
    Tags: by: Stephen Hogg   

    Teaser 3198: The sixth element 

    From The Sunday Times, 7th January 2024 [link] [link]

    Agent J discovered that S.P.A.M.’s IQ booster drug comprises five elements with atomic numbers Z under 92. Some information had been coded as follows: Z1[4;6], Z2[1;4], Z3[4;6], Z4[0;6] and Z5[7;2], where Z5 is lowest and Z3 is highest. Code key: [Remainder after dividing Z by 8; Total number of factors of Z (including 1 and Z). The drug’s name is the elements’ chemical symbols concatenated in encoded list order.

    J subsequently discovered the Z3 and Z5 values. Now MI6 had just fifteen possible sets of atomic numbers to consider. Finally, J sent a sixth element’s prime Z value (a catalyst in the drug’s production, not part of it). She’d heard it was below Z1 and above Z2, without knowing these. MI6 boffins were now sure of the drug’s name.

    Give the catalyst’s atomic number.

    [teaser3198]

     
    • Jim Randell's avatar

      Jim Randell 5:05 pm on 5 January 2024 Permalink | Reply

      This Python program checks that the six Z numbers are all different (although this is not explicitly stated in the puzzle text, but it seems like a reasonable additional requirement).

      It runs in 55ms. (Internal runtime is 711µs).

      Run: [ @replit ]

      from enigma import (
        defaultdict, irange, tau, group, cproduct, primes,
        singleton, printf
      )
      
      # map Z numbers to code
      code = lambda z: (z % 8, tau(z))
      
      # group Z numbers by code
      g = group(irange(1, 91), by=code)
      
      # choose a sequence from the given codes
      codes = [(4, 6), (1, 4), (4, 6), (0, 6), (7, 2)]
      
      # collect possible sequences by (z3, z5) values
      d = defaultdict(list)
      for (z1, z2, z3, z4, z5) in cproduct(g[k] for k in codes):
        if len({z1, z2, z3, z4, z5}) != 5: continue
        # z5 is the lowest
        if not (z5 < min(z1, z2, z3, z4)): continue
        # z3 is the highest
        if not (z3 > max(z1, z2, z4, z5)): continue
        d[(z3, z5)].append((z1, z2, z3, z4, z5))
      
      # knowing z3 and z5 gives 15 possible sequences
      for (k, vs) in d.items():
        if len(vs) != 15: continue
        # consider possible prime z6 values
        for z6 in primes.between(1, 91):
          # look for matching sequences
          check = lambda z1, z2, z3, z4, z5: z2 < z6 < z1 and z6 != z4
          zs = singleton(zs for zs in vs if check(*zs))
          if zs is None: continue
          # output solution
          printf("z6={z6} -> zs={zs}")
      

      Solution: The catalyst has atomic number 47.

      The constituent elements (Z1..Z5) have atomic numbers: 52, 33, 68, 32, 7.

      So, the name of the drug is: TEASERGEN (= Te As Er Ge N).

      And the catalyst (Z6) has atomic number 47 (= Ag (Silver)).

      Like

    • NigelR's avatar

      NigelR 12:48 pm on 7 January 2024 Permalink | Reply

      A bit convoluted but it seems to work!!

      from itertools import product
      
      def is_prime(n):
          if (n % 2 == 0 and n > 2) or n < 2: return False
          for i in range(3, int(n**0.5) + 1, 2):
              if n % i == 0: return False
          return True
      
      factno = lambda n: len([i for i in range(1, n + 1) if n % i == 0])
      zvals = lambda a,b: [i for i in range(1,92) if i%8 == a and factno(i) == b] 
      #Code values for z1 - z5
      Z = [[4,6], [1,4], [4,6], [0,6], [7,2]]
      #create dictionary d with lists of possible values for each element
      d = {i:zvals(x[0],x[1]) for i,x in enumerate(Z,1)}
      #create cand as list with valid sets of element values
      cand = []
      for x in (dict(zip(d.keys(), values)) for values in product(*d.values())):
          #z3 is highest, z5 is lowest and 5 different elements
          if x[3] != max(vals:= x.values()) or len(set(vals)) != 5 or x[5] != min(vals): continue
          else: cand.append(x)
      #remove duplicates and create list of valid x3 & x5
      z3z5 = [list(tupl) for tupl in {tuple(item) for item in [[x[3],x[5]] for x in cand]}]
      #Find z3z5 entry that has 15 possible sets
      res = [x for x in z3z5 if len([y for y in cand if y[3]==x[0] and y[5]==x[1]])==15 ]
      if len(res)>1 :
          print('unique solution not found')
          exit()
      else:
          res = res[0] #flatten res to list of selected x3 & x5
      #strip invalid sets from cand
      cand = [x for x in cand if x[3]==res[0] and x[5]==res[1]]
      z6lst=[]
      #look for possible prime values of z6 between z2 and z1
      for x in cand:
          if x[2]>x[1]:continue
          for y in range(x[2]+1,x[1]): 
              if is_prime(y):
                  z6lst.append([y,x])
      z6set = [x[0] for x in z6lst]
      #look for singleton value of z6
      z6 = [x for x in z6set if z6set.count(x)==1]
      if len(z6)==1:
          z6 = z6[0]
      else:
          print('unique solution not found')
          exit()
      soltn = [x for x in z6lst if x[0]==z6][0]
      print(f'Catalyst atomic number was {soltn[0]}.')
      outstr = 'Z' + '  Z'.join([str(i)+' = '+ str(soltn[1][x]) for i,x in enumerate (soltn[1],1)])
      print('Element values were: ', outstr,'.')
      

      Like

  • Unknown's avatar

    Jim Randell 4:37 pm on 10 November 2023 Permalink | Reply
    Tags: by: Stephen Hogg   

    Teaser 3190: Mods-In-Suits 

    From The Sunday Times, 12th November 2023 [link] [link]

    In single-pack card game Mods-In-Suits, players are dealt two cards. Each card’s face value (Ace=1 to K=13) is modified by its suit, thus:

    “Spade” squares it;
    “Heart” halves it;
    “Club” changes its sign;
    “Diamond” divides it by the other card’s face value.

    Players score their modified values’ sum (or zero if there is a matching pair of face values). Players may exchange their second dealt card for a fresh card from the pack.

    Stuck in the jam in the Smoke at rush hour, John’s children were missing Andy’s party. Playing Mods-In-Suits for amusement led to one unusual game. Jo’s initial score was a positive whole number, Bo’s its negative. Four different suits and face values were dealt. Both exchanged their second card, but each score was unchanged. Four different suits were still showing.

    Give Jo’s final hand.

    [teaser3190]

     
    • Jim Randell's avatar

      Jim Randell 5:21 pm on 10 November 2023 Permalink | Reply

      This Python program runs in 80ms. (Internal runtime is 12ms).

      Run: [ @replit ]

      from enigma import (
        irange, cproduct, sq, rdiv, group, subsets, unpack,
        as_int, intersect, singleton, diff, join, printf
      )
      
      # a pack of cards (<suit>, <value>)
      pack = list(cproduct(['SHCD', irange(1, 13)]))
      
      # calculate modified value for card <X> paired with <Y>
      def value(X, Y):
        ((sx, vx), (sy, vy)) = (X, Y)
        if sx == 'S': return sq(vx)
        if sx == 'H': return rdiv(vx, 2)
        if sx == 'C': return -vx
        if sx == 'D': return rdiv(vx, vy)
      
      # score a pair of cards (we are only interested in non-zero integer
      # values, from pairs of cards with different suits)
      def score(X, Y):
        ((sx, vx), (sy, vy)) = (X, Y)
        if sx == sy or vx == vy: return None
        return as_int(value(X, Y) + value(Y, X), include='+-', default=None)
      
      # deal 3 cards from the pairs <ps>
      # return cards <x1> and <x2> which is replaced by <x3>
      def deal(ps):
        for (p1, p2) in subsets(ps, size=2, select='P'):
          x1 = singleton(intersect([p1, p2]))
          if x1 is None: continue
          x2 = singleton(diff(p1, p2))
          x3 = singleton(diff(p2, p1))
          yield (x1, x2, x3)
      
      # group pairs by integer value
      g = group(subsets(pack, size=2), by=unpack(score))
      del g[None]  # remove scores we are not interested in
      
      # consider possible scores for J
      for (k, psJ) in g.items():
        if k < 0 or len(psJ) < 2: continue
        # B's score is -k
        psB = g.get(-k)
        if psB is None or len(psB) < 2: continue
      
        # consider cards for J (J1, J2 -> J3)
        for (J1, J2, J3) in deal(psJ):
          # consider cards for B (B1, B2 -> B3)
          for (B1, B2, B3) in deal(psB):
            # check deals are possible
            if len({J1, J2, J3, B1, B2, B3}) < 6: continue
            # check 4 different suits and values initially
            if any(len({J1[i], J2[i], B1[i], B2[i]}) < 4 for i in [0, 1]): continue
            # check 4 different suits finally
            if len({J1[0], J3[0], B1[0], B3[0]}) < 4: continue
            # output solution
            (J1, J2, J3, B1, B2, B3) = map(join, (J1, J2, J3, B1, B2, B3))
            printf("J: {J1}, {J2} -> {J3} [+{k}]; B: {B1}, {B2} -> {B3} [-{k}]")
      

      Solution: Jo’s final hand is: Ace of Spades, 6 of Hearts.

      The initial cards dealt were:

      Jo: Ace of Spades, 3 of Diamonds; score = (+1, +3) = +4
      Bo: 6 of Clubs, 4 of Hearts; score = (−6, +2) = −4

      The suits are: S, D, C, H and the values: 1, 3, 4, 6.

      Jo then chooses to discard 3 of Diamonds, and is dealt 6 of Hearts:

      Jo: Ace of Spades, 6 of Hearts; score = (+1, +3) = +4

      Bo then chooses to discard 4 of Hearts, and is dealt Queen (12) of Diamonds:

      Bo: 6 of Clubs, 12 of Diamonds; score = (−6, +2) = −4

      The suits are: S, H, C, D and the values: 1, 6, 6, 12.


      If Jo’s discarded card is returned to the pack, and then it can be Bo’s exchange card (rather than a “fresh” card from the pack) and this leads to a further solution, where the final values are also all different (I initially was solving the puzzle with this extra condition). So presumably that does not happen.

      The initial cards dealt were:

      Jo: Ace of Spades, 4 of Hearts; score = (+1, +2) = 3
      Bo: 5 of Clubs, 10 of Diamonds; score = (−5, +2) = −3

      The suits are: S, H, C, D and the values 1, 4, 5, 10.

      Jo then chooses to discard the 4 of Hearts (which is returned to the pack), and is dealt 2 of Diamonds:

      Jo: Ace of Spades, 2 of Diamonds; score = (+1, +2) = 3

      Bo then chooses to discard the 10 of Diamonds, and is dealt 4 of Hearts (the card discarded by Jo):

      Bo: 5 of Clubs, 4 of Hearts; score = (−5, +2) = −3

      The suits are: S, D, C, H and the values: 1, 2, 4, 5.

      Like

  • Unknown's avatar

    Jim Randell 4:51 pm on 8 September 2023 Permalink | Reply
    Tags: by: Stephen Hogg   

    Teaser 3181: “No slack, Alice!” says Grace 

    From The Sunday Times, 10th September 2023 [link] [link]

    Grace mimicked Mr Dodgson: “Take two different positive odd numbers with no shared prime factor. Multiply the larger by their sum, giving the perimeter of a distinct right-angled triangle with whole-number sides. Only such values and their multiples work”.

    Alice created a loop from part of a 1m thread. She was able to pull it tightly on a pinboard into a right-angled triangle with whole-number cm sides in two ways (with different-shaped triangles).

    Alice then pulled the same loop tight over the thin polar spike of the classroom globe, down two meridians and along the equator. Thinking “Logic’s dead!”, she saw 90° equatorial angles and a non-zero polar angle, which obviously didn’t add to 180°.

    Grace calculated the polar angle to the nearest degree. Curiously, transposing its two digits gave the globe’s whole-number centimetre radius.

    Find this radius.

    [teaser3181]

     
    • Jim Randell's avatar

      Jim Randell 5:09 pm on 8 September 2023 Permalink | Reply

      If the string has length p and angle (in radians) made by the loop at the pole is 𝛂 on a globe of radius r, then we have:

      p = r(𝛑 + 𝛂)

      This Python program finds perimeters (that are whole numbers of centimetres, less than 100), such that (at least) two dissimilar Pythagorean triangles can be constructed, and then looks for a corresponding radius for a globe that fits the remaining conditions.

      It runs in 58ms. (Internal runtime is 296µs).

      from math import (pi, degrees)
      from enigma import (pythagorean_triples, group, item, fdiv, irange, intr, nrev, printf)
      
      # generate possible pythagorean triangles, return (<perimeter>, <triangles>)
      def generate(M):
        for ss in pythagorean_triples(M // 2):
          p = sum(ss)
          if p < M:
            yield (p, ss)
      
      # group pythagorean triangles by their perimeter
      g = group(generate(100), by=item(0), f=item(1))
      
      # consider possible perimeters
      for (p, ts) in g.items():
        if len(ts) < 2: continue
        printf("[p={p} -> {ts}]")
      
        # consider possible globe radii
        for r in irange(10, 99):
          # calculate the polar angle (to the nearest degree)
          a = intr(degrees(fdiv(p, r) - pi))
          if a > 99: continue
          if a < 10: break
          if a == nrev(r):
            printf("-> r={r} a={a}")
      

      Solution: The globe has a radius of 19 cm.

      The loop used 90 cm of thread.

      Which allows (planar) right-angled triangles with sides of (15, 36, 39) cm and (9, 40, 41) cm to be formed.

      It can also be used on a 19cm radius globe to form a triangle on the globe with approx. 30.31 cm running along the equator and 29.85 cm running up meridians to the north pole, meeting at an angle of approx. 91.4°. So the three sides are each close to 30 cm, and the angles are all about 90°.

      Like

  • Unknown's avatar

    Jim Randell 4:28 pm on 16 June 2023 Permalink | Reply
    Tags: by: Stephen Hogg   

    Teaser 3169: Cancel culture — amp it up! 

    From The Sunday Times, 18th June 2023 [link] [link]

    A heckled lecturer used her megaphone. The power reaching its amplifier’s input was multiplied by a two-figure whole number (the “intrinsic gain”). In each of three stages a fraction of the power was lost but, by good design, each of the three fractions was under one-twentieth. Curiously, each happened to have a different two-figure prime denominator. It turned out that the effective gain in the process (the ratio of the output power to the input power) was a whole number. In addition, the intrinsic gain and the effective gain both started with the same digit.

    In ascending order, what were the three fractions?

    [teaser3169]

     
    • Jim Randell's avatar

      Jim Randell 4:58 pm on 16 June 2023 Permalink | Reply

      If I understand this correctly the mechanics of it are:

      The input signal is initially multiplied by a 2-digit value (= I, the “intrinsic gain”), and then goes through 3 stages where it is reduced by fractions a/p, b/q, c/r, where each of the fractions is less than 1/20, and p, q and r are prime numbers. This gives the “effective gain” E, which is a whole number, and which has the same first digit as I.

      So:

      E = I × (1 − a/p) × (1 − b/q) × (1 − c/r)

      Here’s my initial stab at this.

      It runs in 137ms. (Internal runtime is 38ms).

      Run: [ @replit ]

      from enigma import (
        primes, irange, first, subsets, cproduct, div,
        nsplit, fdiv, unpack, seq2str, sprintf, printf
      )
      
      # map prime denominators to numerators that give fractions < 1/20
      nums = lambda p: first(irange(1, p), count=(lambda n: 20 * n < p))
      d = dict((p, nums(p)) for p in primes.between(20, 99))
      
      # choose the prime denominators (p, q, r)
      for (p, q, r) in subsets(d.keys(), size=3):
        Rd = p * q * r
        # and choose the numerators (a, b, c)
        for (a, b, c) in cproduct(d[k] for k in (p, q, r)):
          # calculate the ratio E/I (= Rn / Rd)
          Rn = (p - a) * (q - b) * (r - c)
          # choose a 2-digit number for I (= intrinsic gain)
          for I in irange(10, 99):
            # calculate E (= effective gain) (a whole number)
            E = div(I * Rn, Rd)
            if E is None: continue
            # check E and I have the same first digit
            if nsplit(E)[0] != nsplit(I)[0]: continue
            # order the fractions
            fs = sorted([(a, p), (b, q), (c, r)], key=unpack(fdiv))
            # output solution
            printf("{fs} [I={I} E={E}]", fs=seq2str(sprintf("{x}/{y}") for (x, y) in fs))
      

      Solution: The three fractions are: 1/41, 3/89, 2/43.

      The intrinsic gain (I) is 89, and so the effective gain (E) is:

      E = 89 × (1 − 1/41) × (1 − 3/89) × (1 − 2/43)
      E = (89 × 40 × 86 × 41) / (41 × 89 × 43)
      E = 80


      Without the requirement that the first digits of E and I are the same we find there are 5 potential solutions:

      88/97 = (46/47) × (94/97) × (22/23)
      66/73 = (71/73) × (69/71) × (22/23)
      56/61 = (58/59) × (59/61) × (28/29)
      80/89 = (40/41) × (86/89) × (41/43)
      78/89 = (86/89) × (41/43) × (39/41)

      Like

      • Hugo's avatar

        Hugo 6:03 pm on 16 June 2023 Permalink | Reply

        A more realistic way of looking at it is that each stage is designed to have a certain gain, the product of the three gains being I. In practice the circuitry is imperfect and each gain is reduced by a certain fraction, so that the overall gain E is somewhat lower. The overall effect is the same as in the situation described by Jim, but the losses occur during amplification, not after it.

        Like

      • Jim Randell's avatar

        Jim Randell 7:56 am on 17 June 2023 Permalink | Reply

        Here is a faster version. Once the ratio E:I is determined we can look for equivalent fractions with a 2-digit denominator to give values for E and I.

        It runs in 77ms. (Internal runtime is 17ms).

        Run: [ @replit ]

        from enigma import (
          primes, irange, divf, subsets, cproduct, fraction,
          nsplit, fdiv, unpack, seq2str, format_fraction, printf
        )
        
        # map denominators to numerators that give fractions < 1/20
        nums = lambda n: irange(1, divf(n - 1, 20))
        
        # choose the prime denominators (p, q, r)
        for (p, q, r) in subsets(primes.between(20, 99), size=3):
          Rd = p * q * r
          # and choose the numerators (a, b, c)
          for (a, b, c) in cproduct(nums(k) for k in (p, q, r)):
            # calculate the ratio E/I (= Rn / Rd)
            Rn = (p - a) * (q - b) * (r - c)
            # consider fractions E/I = Rn/Rd where I is 2-digits
            (n, d) = fraction(Rn, Rd)
            for k in irange(1, divf(99, d)):
              (E, I) = (k * n, k * d)
              if I < 10: continue
              # check E and I have the same first digit
              if nsplit(E)[0] != nsplit(I)[0]: continue
              # order the fractions
              fs = sorted([(a, p), (b, q), (c, r)], key=unpack(fdiv))
              # output solution
              printf("{fs} [I={I} E={E}]", fs=seq2str(format_fraction(x, y) for (x, y) in fs))
        

        Like

    • GeoffR's avatar

      GeoffR 11:15 pm on 16 June 2023 Permalink | Reply

      A slow solution – about 8.5 sec.
      It finds the three fractions, but not in ascending order.

      % A Solution in MiniZinc
      include "globals.mzn";
      
      % Same variables as Jim's solution.
      var 1..9:A; var 1..9:B; var 1..9:C;
      var 10..99:P; var 10..99:Q; var 10..99:R;
      var 10..99:E; var 10..99:I;
      
      constraint I > E /\ E div 10 == I div 10;
      
      predicate is_prime(var int: x) = 
       x > 1 /\ forall(i in 2..1 + ceil(sqrt(int2float(ub(x))))) 
       ((i < x) -> (x mod i > 0));   
       
      % The three denominators of the fractions are all 2-digit primes
      constraint is_prime(P) /\ P > 10  /\ P < 98;
      constraint is_prime(Q) /\ Q > 10  /\ Q < 98;
      constraint is_prime(R) /\ R > 10  /\ R < 98;
      
      % The three fractions are all less than 1/20
      constraint 20 * A < P /\ 20 * B < Q /\ 20 * C < R;
      
      % Jim's equation simplifies to:
      % E * P * Q * R  = I *(P - A) * (Q - B) * (R - C)
      constraint E * P * Q * R == I * (P - A) * (Q - B) * (R - C);
      
      solve satisfy;
      
      output[ "Three fractions are " ++ show(A) ++ "/" ++ show(P) ++", "
      ++ show(B) ++ "/" ++ show(Q) ++" and "  
      ++ show(C) ++ "/" ++ show(R) ];
      
      
      

      Like

      • Jim Randell's avatar

        Jim Randell 11:31 pm on 16 June 2023 Permalink | Reply

        You can add the following to get the fractions in order:

        constraint A * Q < B * P /\ B * R < C * Q;
        

        Like

        • GeoffR's avatar

          GeoffR 11:08 am on 17 June 2023 Permalink | Reply

          @Jim:
          Yes, that extra line of code works well.
          it also reduces the run-time for me to just over 2sec.

          Like

    • Frits's avatar

      Frits 12:31 am on 17 June 2023 Permalink | Reply

       
      from enigma import SubstitutedExpression
      from fractions import Fraction as rf
      
      # invalid digit / symbol assignments
      d2i = dict()
      for d in range(0, 10):
        vs = set()
        if d == 0: vs.update('ACEPXYZ')
        if d == 1: vs.update('ACE')
        if d > 4: vs.update('XYZ')
        if d % 2 == 0 or d == 5: vs.update('BDF')
      
        d2i[d] = vs
      
      # remaining checks for prime   
      check = lambda n: all(n % x for x in [3, 7])
        
      # PQ intrinsic gain, PR = effective gain
      # primes AB, CD and EF
      
      # the alphametic puzzle
      p = SubstitutedExpression(
        [
          "20 * (AB - X) > 19 * AB",
          # prime check
          "check(AB)",
          
          "CD > AB",
          # prime check
          "check(CD)",
          "20 * (CD - Y) > 19 * CD",
          
          "EF > CD",
          # prime check
          "check(EF)",
          
          # if (AB - X) * (CD - Y) is not divisible by either prime 
          # then (EF - Z) * PQ must be a multiple of AB * CD
          "any(((AB - X) * (CD - Y)) % x == 0 for x in [AB, CD]) or \
               ((AB - X) * (CD - Y)) % EF == 0",
          
          "20 * (EF - Z) > 19 * EF",
          
          "PQ * (AB - X) * (CD - Y) * (EF - Z) == PR * (AB * CD * EF)"
        ],
        answer="ordered(rf(X, AB), rf(Y, CD), rf(Z, EF))",
        d2i=d2i,
        env=dict(check=check, rf=rf),
        distinct="",
        verbose=0,    # use 256 to see the generated code
      )
      
      # print answers
      for (_, ans) in p.solve():
        print(f"{ans[0]}, {ans[1]} and {ans[2]}")  
      

      Like

    • GeoffR's avatar

      GeoffR 12:31 pm on 17 June 2023 Permalink | Reply

      
      from enigma import is_prime
      from itertools import permutations
      
      PR = [ x for x in range(11, 98) if is_prime(x)]
      
      for A, B, C in permutations(range(1, 5), 3):
        for P in PR:
          if not(20 * A < P):continue
          for Q in PR:
            if Q == P:continue
            if not (20 * B < Q):continue
            if not(A * Q < B * P):continue
            for R in PR:
              if R in (P, Q):continue
              if not (20 * C < R):continue
              if not(B * R < C * Q):continue
              for E in range(10, 99):
                for I in range(10, 99):
                  if E // 10 != I // 10:continue
                  # Same equation as the MiniZinc solution
                  if E * P * Q * R == I * (P - A) * (Q - B) * (R - C):
                    print(f"Three fractions: {A}/{P}, {B}/{Q}, {C}/{R}.")
      

      Like

  • Unknown's avatar

    Jim Randell 4:27 pm on 28 April 2023 Permalink | Reply
    Tags: by: Stephen Hogg   

    Teaser 3162: Flash Cordon and Ming 

    From The Sunday Times, 30th April 2023 [link] [link]

    Housing its priceless Ming collection, the museum’s main hall had eight flashing, motion-sensor alarms. Each alarm’s flash ON and OFF times were settable from 1s to 4s in 0.1s steps. This allowed the flash “duty cycle” (the percentage of one ON+OFF period spent ON) to be altered. To conserve power, all duty cycles were set under 50%. All eight duty cycles were whole numbers, not necessarily all different.

    Four alarms were set with consecutive incremental ON times (e.g. 2.0, 2.1, 2.2, 2.3). One pair of these had equal OFF times, as did the other pair (but a different OFF time from the first pair). Curiously, these two statements also apply to the other four alarms if the words ON and OFF are exchanged.

    Give the lowest and highest duty cycles for each set of four alarms.

    [teaser3162]

     
    • Jim Randell's avatar

      Jim Randell 5:27 pm on 28 April 2023 Permalink | Reply

      At first I misunderstand how this puzzle worked. But each sensor has an ON duration, and an OFF duration, each chosen from values between 1.0s and 4.0s (inclusive). The “duty cycle” is then calculated as the fraction ON / (ON + OFF), when expressed as a percentage.

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

      Run: [ @replit ]

      from enigma import (irange, subsets, div, group, item, tuples, cproduct, multiset, printf)
       
      # collect possible duty cycles (ON, OFF) -> duty%
      ds = dict()
      # consider possible ON/OFF pairs (deci-seconds)
      for (ON, OFF) in subsets(irange(10, 40), size=2):
        # calculate the duty cycle
        d = div(100 * ON, ON + OFF)
        if not d: continue
        ds[(ON, OFF)] = d
      
      # does a collection of values form pairs?
      pairs = lambda vs: multiset.from_seq(vs).all_same(2)
      
      # choose consecutive item <i> times, and corresponding pairs of <j> times
      def choose(ds, i, j):
        # group item j by item i
        g = group(ds.keys(), by=item(i), f=item(j))
       
        # choose 4 consecutive keys
        for ks in tuples(sorted(g.keys()), 4):
          if ks[-1] - ks[0] != 3: continue
          # choose possible values ...
          for vs in cproduct(g[k] for k in ks):
            # ... that form pairs
            if not pairs(vs): continue
            # return (keys, values)
            yield (ks, vs)
       
      # the first set of alarms (consecutive ON times)
      for (ks, vs) in choose(ds, 0, 1):
        duty = list(ds[k] for k in zip(ks, vs))
        printf("[1] ON = {ks} -> OFF = {vs}; duty% = {duty} => min={min} max={max}", min=min(duty), max=max(duty))
       
      # the second set of alarms (consecutive OFF times)
      for (ks, vs) in choose(ds, 1, 0):
        duty = list(ds[k] for k in zip(vs, ks))
        printf("[2] ON = {vs} -> OFF = {ks}; duty% = {duty} => min={min} max={max}", min=min(duty), max=max(duty))
      

      Solution: The minimum and maximum duty cycles for the first set of alarms are: 22% and 28%. For the second set of alarms they are: 24% and 26%.

      The first set is:

      ON = 1.1s, OFF = 3.9s; duty cycle = 22% (min)
      ON = 1.2s, OFF = 3.6s; duty cycle = 25%
      ON = 1.3s, OFF = 3.9s; duty cycle = 25%
      ON = 1.4s, OFF = 3.6s; duty cycle = 28% (max)

      And the second set:

      ON = 1.2s, OFF = 3.6s; duty cycle = 25%
      ON = 1.3s, OFF = 3.7s; duty cycle = 26% (max)
      ON = 1.2s, OFF = 3.8s; duty cycle = 24% (min)
      ON = 1.3s, OFF = 3.9s; duty cycle = 25%

      Like

    • Frits's avatar

      Frits 7:53 pm on 28 April 2023 Permalink | Reply

      Basically trying to get the same output as Jim’s program.
      Performance could have been improved by initially collecting possible duty cycles.
      This program runs in 210ms.

       
      from enigma import SubstitutedExpression
      
      # invalid digit / symbol assignments
      d2i = dict()
      for d in range(0, 10):
        vs = set()
        tens = 'ACEGIKMOQS'
        if d == 0: vs.update(tens)
        if d > 4:  vs.update(tens)
      
        d2i[d] = vs
      
      # all duty cycles are whole numbers  
      check = lambda s, t: all((100 * x) % (x + y) == 0 for x, y in zip(s, t))    
      
      # [1] ON = (AB, AB+1, AB+2, AB+3) -> OFF = {CD, EF, GH, IJ} 
      # [2] ON = {KL, MN, OP, QR}       -> OFF = (ST, ST+1, ST+2, ST+3)
      
      # the alphametic puzzle
      p = SubstitutedExpression(
        [
          # consecutive ON times: CD, EF, GH, IJ contain two pairs
          "len({CD, EF, GH}) == 2",
          "all(x < 41 for x in [CD, EF, GH])",
          "[x for x in [CD, EF, GH] if [CD, EF, GH].count(x) == 1][0] = IJ",
          
          # all duty cycles are set under 50% ...
          "AB < min(CD, EF - 1, GH - 2, IJ - 3)",
          # ... and are whole numbers
          "check((AB, AB+1, AB+2, AB+3), (CD, EF, GH, IJ))",
          
          # consecutive OFF times: KL, MN, OP, QR contain two pairs
          "len({KL, MN, OP}) == 2",
          "all(x < 41 for x in [KL, MN, OP])",
          "[x for x in [KL, MN, OP] if [KL, MN, OP].count(x) == 1][0] = QR",
          
          # all duty cycles are set under 50% ...
          "max(KL, MN - 1, OP - 2, QR - 3) < ST < 38",
          # ... and are whole numbers
          "check((KL, MN, OP, QR), (ST, ST+1, ST+2, ST+3))",
        ],
        answer="(AB, AB+1, AB+2, AB+3), (CD, EF, GH, IJ), \
                (KL, MN, OP, QR), (ST, ST+1, ST+2, ST+3)",
        d2i=d2i,
        distinct="",
        env=dict(check=check),
        reorder=0,
        verbose=0,    # use 256 to see the generated code
      )
      
      # print answers
      for (_, (n1, f1, n2, f2)) in p.solve():
        print("1st set:", min(dt := [(100 * x) // (x + y) for x, y in zip(n1, f1)]),
              "and", max(dt))
        print("2nd set:", min(dt := [(100 * x) // (x + y) for x, y in zip(n2, f2)]),
              "and", max(dt))   
      

      Like

      • Jim Randell's avatar

        Jim Randell 9:37 am on 29 April 2023 Permalink | Reply

        @Frits: You can use a [[ SubstitutedExpression ]] recipe like this to find the first set of values, and a similar recipe will find the second set:

        #! python3 -m enigma -rr
        
        SubstitutedExpression
        
        # suppose the on/off durations are:
        #  X + 0 -> P
        #  X + 1 -> Q
        #  X + 2 -> R
        #  X + 3 -> S
        # where each is a number between 10 and 40
        # so we use base 41
        --base=41
        --digits="10-40"
        --distinct=""
        
        # check values come in pairs
        --code="check = lambda vs: multiset.from_seq(vs).all_same(2)"
        "check([P, Q, R, S])"
        
        # check duty cycles
        --code="duty = lambda ON, OFF: (div(100 * ON, ON + OFF) if ON < OFF else None)"
        "duty(X, P)"
        "duty(X + 1, Q)"
        "duty(X + 2, R)"
        "duty(X + 3, S)"
        
        --template=""
        --answer="((X, P), (X + 1, Q), (X + 2, R), (X + 3, S))"
        

        Internal runtimes are 16.8ms and 7.0ms.

        I’ve put a complete solution to the puzzle using this technique up on [@replit].

        Like

        • Frits's avatar

          Frits 11:58 am on 30 April 2023 Permalink | Reply

          Thanks, I didn’t think of the combination of base and digits.

          The following MiniZinc problem runs fastest with the Chuffed solver (330ms).

           
          % A Solution in MiniZinc
          include "globals.mzn";
          
          %  ------- first 4 alarms ----------
          
          % suppose the on/off durations are:
          %  X1 + 0 -> Y1[1]
          %  X1 + 1 -> Y1[2]
          %  X1 + 2 -> Y1[3]
          %  X1 + 3 -> Y1[4]
            
          var 10..36:X1;  
          array [1..4] of var 11..40: Y1;
          array [1..4] of var 20..49: D1;  % duty cycles
          
          % Y1 contains 2 pairs 
          constraint forall (y in Y1) (count(Y1, y) == 2);
          
          % check duty cycles
          constraint forall (i in 1..4) ( X1 + i - 1 < Y1[i] /\ 
                            (100 * (X1 + i - 1)) mod (X1 + i - 1 + Y1[i]) == 0);
                            
          constraint forall (i in 1..4) ((100 * (X1 + i - 1)) div (X1 + i - 1 + Y1[i]) == D1[i]);
          
          output ["X1 = " ++ show (X1) ++ ", Y1 = " ++ show(Y1) ++ 
                  ", min = " ++ show(min(D1)) ++ ", max = " ++ show(max(D1)) ++ "\n" ];
          
          %  ------- other 4 alarms ----------
          
          % suppose the on/off durations are:
          %  Y2[1] -> X2 + 0
          %  Y2[2] -> X2 + 1
          %  Y2[3] -> X2 + 2
          %  Y2[4] -> X2 + 3
           
          var 11..37:X2;  
          array [1..4] of var 10..39: Y2;
          array [1..4] of var 20..49: D2;  % duty cycles
          
          % Y2 contains 2 pairs 
          constraint forall (y in Y2) (count(Y2, y) == 2);
          
          % check duty cycles
          constraint forall (i in 1..4) ( Y2[i] < X2 + i - 1 /\ 
                            (100 * Y2[i]) mod (X2 + i - 1 + Y2[i]) == 0);
                            
          constraint forall (i in 1..4) ((100 * (Y2[i])) div (X2 + i - 1 + Y2[i]) == D2[i]);
           
          output ["X2 = " ++ show (X2) ++ ", Y2 = " ++ show(Y2) ++ 
                  ", min = " ++ show(min(D2)) ++ ", max = " ++ show(max(D2)) ];
          
          solve satisfy;
          

          Like

  • Unknown's avatar

    Jim Randell 4:37 pm on 24 February 2023 Permalink | Reply
    Tags: by: Stephen Hogg   

    Teaser 3153: Pole position 

    From The Sunday Times, 26th February 2023 [link] [link]

    Jeb’s 25×25km square ranch had his house’s flagpole at the ranch’s pole of inaccessibility (the point whence the shortest distance to a boundary fence was maximised).

    At 50, Jeb gave each of his four sons a triangular tract of his land, with corners whole numbers of km along boundary fences from each corner post (as illustrated, not to scale). Each tract’s area (in square km) equalled that son’s age last birthday (all over 19, but under 30). All the tracts’ perimeters differed, and each son set his hacienda’s flagpole at his own tract’s pole of inaccessibility.

    Curiously, for Jeb’s new octagonal ranch the pole of inaccessibility and the shortest distance from this to a boundary fence were unchanged.

    Give the eldest son’s shortest distance from his flagpole to a boundary fence.

    This puzzle marks the 62nd birthday of the Sunday Times Brain Teaser puzzle. The first numbered puzzle, Brain-Teaser 1: Tall story, was published on 26th February 1961.

    [teaser3153]

     
    • Jim Randell's avatar

      Jim Randell 5:09 pm on 24 February 2023 Permalink | Reply

      We can calculate which of the possible triangles do not change the pole of inaccessibility, and it turns out there are exactly 4 of them (and they do have different perimeters). We can then calculate the inradius of the one with the largest area to find the required answer.

      This Python program runs in 51ms. (Internal runtimes is 259µs).

      Run: [ @replit ]

      from enigma import (irange, subsets, div, hypot, fdiv, line_distance, Accumulator, printf)
      
      # distance to boundary from pole
      R = 12.5  # = 0.5 * 25 km
      
      # record inradius for maximum age
      m = Accumulator(fn=max)
      
      # consider (a, b) plots for the sons
      for (a, b) in subsets(irange(1, int(R)), size=2, select="R"):
        # calculate area
        A = div(a * b, 2)
        # check for viable area, and age
        if A is None or not (19 < A < 30): continue
        # check boundary does not change the pole of accessibility
        if line_distance((0, a), (b, 0), (R, R)) < R: continue
        # calculate the perimeter
        p = a + b + hypot(a, b)
        # calculate inradius; area = inradius * semiperimeter
        r = fdiv(2 * A, p)
        printf("({a}, {b}) -> A={A}; p={p:.3f}; r={r:.3f}")
        m.accumulate_data(A, r)
      
      # output solution (inradius for maximum age)
      printf("answer = {m.data} km")
      assert m.count == 4  # check that 4 plots were found
      

      Solution: The shortest distance from the eldest son’s flagpole to a boundary is 2 km.


      Given a plot of land a “pole of inaccessibility” is the centre of a maximal radius circle that can be placed inside that plot without exceeding the boundaries.

      So for the square plot of land the flagpole is erected at the centre of the square. (For a non-square rectangle there would a line of possible positions where the flagpole could be erected). And for a triangle the flagpole is erected at the incentre (i.e. the centre of the incircle) of the triangle. (See [@wikipedia]).

      The plots of land, and positions of the flagpoles, are illustrated in the diagram below:

      Jeb’s flagpole is at the centre of the square, and the large red circle shows the shortest distance to a boundary. When the corner plots are made they cannot cross the large red circle, or they would reduce the shortest distance from Jeb’s flagpole to a boundary. But they could touch the circle.

      There are 4 different triangular plots that can be made, and they are also shown on the diagram, along with their incentres, which correspond to the positions of the sons’ flagpoles.

      The boundary of the plot on the bottom left (with an area of 20) comes closest to Jeb’s flagpole (it is 12.534 km away from it). The plot on the bottom right (with an area of 24) is the largest. The other two plots have areas of 20 and 21.

      Like

    • Hugo's avatar

      Hugo 9:41 am on 5 March 2023 Permalink | Reply

      Sides of the triangular plots (with area and distance of third side from the centre of the square):
      4, 10 (20, 12.534) nearly touched the big circle
      5, 8 (20, 12.985)
      6, 7 (21, 13.07)
      6, 8 (24, 12.7) third side 10
      There would be a couple more with two integer sides but half-integer areas.

      Like

  • Unknown's avatar

    Jim Randell 4:34 pm on 23 December 2022 Permalink | Reply
    Tags: by: Stephen Hogg   

    Teaser 3144: Beware the jabbers’ clock, my son! 

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

    On the NHS vaccine booking website, my queue position updated every minute. My initial position was an odd value in the 9000s. This value had four different digits, and its leftmost digit and just two others in it were factors of it. Curiously, these properties applied to each of the decreasing four-figure values after the next eight updates. I also noticed that no value had a digit in common with its predecessor, no zeroes occurred before the fourth update and no nines after this.

    My son, told just the above, found all the possible values from which a complete set could be selected. From these he was certain of more than one of the nine queue positions.

    Give these certain positions in displayed order.

    This completes the archive of puzzles from 2022.

    [teaser3144]

     
    • Jim Randell's avatar

      Jim Randell 5:14 pm on 23 December 2022 Permalink | Reply

      I took “no zeroes occurred before the fourth update …” to mean there are no zeros in the first 4 numbers in the sequence (but it doesn’t imply that there necessarily is a zero digit in the 5th number in the sequence). And “… no nines after this” to mean that there are no nines in last 5 numbers of the sequence (and again, not implying that there is a 9 digit in 4th number in the sequence). Although I think that you can use a slightly different interpretation of these conditions and still come up with the same answer.

      This Python program runs in 307ms.

      Run: [ @replit ]

      from enigma import (irange, subsets, nconcat, intersect, printf)
      
      # does <x> divide <n>?
      divides = lambda n, x: x != 0 and n % x == 0
      
      # generate candidate numbers
      def generate():
        for (A, B, C, D) in subsets(irange(0, 9), size=4, select="P"):
          if A == 0: continue
          n = nconcat(A, B, C, D)
          # n is divisible by A and exactly 2 of B, C, D
          if divides(n, A) and sum(divides(n, d) for d in (B, C, D)) == 2:
            yield (A, B, C, D)
      
      # find viable sequences from <ns>
      def solve(ns, ss=[]):
        k = len(ss)
        # are we done?
        if k == 9:
          yield ss
        else:
          # choose the next number
          for (i, n) in enumerate(ns, start=1):
            # there are no 0s before the 4th update
            if k < 4 and 0 in n: continue
            # there are no 9s after the 3rd update
            if k > 3 and 9 in n: continue
            if k == 0:
              # the first number is odd, and starts with 9
              if n[0] < 9: break
              if n[-1] % 2 != 1: continue
            else:
              # subsequent numbers
              # have no digits in common with the previous number
              if len(set(n + ss[-1])) != 8: continue
            # looks good
            yield from solve(ns[i:], ss + [n])
      
      # collect candidate numbers in decreasing order
      ns = list(generate())
      ns.reverse()
      
      # what numbers occur in all possible sequences?
      ss = list(nconcat(x) for x in intersect(solve(ns)))
      
      # output solution
      printf("{ss}", ss=sorted(ss, reverse=1))
      

      Solution: The positions that are certain are: 7315 and 4728.

      7315 must occur as the 3rd value in the sequence, and 4728 must occur as the 6th value in the sequence.


      There are 5 positions where only one set of digits can occur. But for some of these it is possible to construct more than one valid number from the digits:

      9 + {1, 3, 5} → 9153, 9351, 9513, 9531
      8 + {2, 4, 6} → 8264, 8624
      7 + {1, 3, 5} → 7315
      5 + {0, 1, 3} → 5130, 5310
      4 + {2, 7, 8} → 4728

      Only 7315 and 4728 are unique in their respective positions.

      In total there are 8832 possible sequences of 9 numbers.

      Like

      • Jim Randell's avatar

        Jim Randell 7:38 am on 24 December 2022 Permalink | Reply

        With a little analysis we can get a much faster run time (and the program is only a little longer).

        First we note that the numbers must be of the form:

        9xxx, 8xxx, 7xxx, 6xxx, 5xxx, 4xxx, 3xxx, 2xxx, 1xxx

        And as long as there is valid number that can be constructed using the non-leading digits we don’t really care what order they appear in when we look for the next number. So we can just consider the collection of digits.

        Also, as successive numbers share no digits, as we construct numbers we can exclude the leading digit of the next number (as we already know what it will be).

        Then at the end we take the collections of digits we have found that we know must appear in each position, we can look for those that give just one valid number, and we have found an answer.

        This Python program runs in 56ms. (Internal run time is 963µs).

        Run: [ @replit ]

        from enigma import (irange, subsets, nconcat, diff, intersect, singleton, printf)
        
        # does <x> divide <n>?
        divides = lambda n, x: x != 0 and n % x == 0
        
        digits = list(irange(0, 9))
        
        # is the number formed from (A, B, C, D) valid?
        def valid(A, B, C, D):
          n = nconcat(A, B, C, D)
          if A == 9 and n % 2 != 1:
            return
          if divides(n, A) and sum(divides(n, x) for x in (B, C, D)) == 2:
            return n
        
        # generate candidate digit sets
        # return digits (A, B, C, D) - A is leading digit; B, C, D are ordered
        def candidates(A, ds):
          # choose the three final digits
          for xs in subsets(ds, size=3):
            # can we find a valid number?
            if any(valid(A, B, C, D) for (B, C, D) in subsets(xs, size=len, select="P")):
              yield (A,) + xs
        
        # k = leading digit
        def solve(k, ss=[]):
          # are we done?
          if k == 0:
            yield ss
          else:
            if k == 9:
              # the first number is odd, and has no 0 digit
              cs = candidates(9, diff(digits, {0, 8, 9}))
            elif k > 5:
              # subsequent number with no 0 digit
              cs = candidates(k, diff(digits, ss[-1], {0, k - 1, k}))
            else:
              # subsequent number with no 9 digit
              xs = {k, 9}
              if k > 1: xs.add(k - 1)
              cs = candidates(k, diff(digits, ss[-1], xs))
            for s in cs:
              yield from solve(k - 1, ss + [s])
        
        # look for positions that must occur in all solutions
        for ss in sorted(intersect(solve(9)), reverse=1):
          # look for candidates with only a single valid number
          ns = (valid(ss[0], B, C, D) for (B, C, D) in subsets(ss[1:], size=len, select="P"))
          n = singleton(filter(None, ns))
          if n:
            printf("{n}")
        

        Like

    • Frits's avatar

      Frits 12:48 am on 24 December 2022 Permalink | Reply

      A different approach. At the end only 39 candidate numbers remain.

         
      from itertools import permutations
      from functools import reduce
      
      # convert digit sequences to number
      d2n = lambda s: reduce(lambda x,  y: 10 * x + y, s)
      
      # return entry and column value where column <col> is unique
      def unique_col(seq, col=0):
        d = dict()
        for s in seq:
          d[s[col]] = s[col] in d
      
        return [(s[col], s) for s in seq if not d[s[col]]]
       
      # generate candidate numbers
      def generate():
        for A, B, C, D in permutations(range(9, -1, -1), 4):
          if A == 0: break
          if (A == 9 and D % 2 == 0): continue
          
          n = d2n([A, B, C, D])
          # n is divisible by A and exactly 2 of B, C, D
          if n % A or sum(n % d == 0 for d in (B, C, D) if d) != 2:
            continue
          # A has to connect with A - 1 and A + 1 so B, C and D 
          # may not be equal to A - 1 or A + 1
          if (A < 9 and (A + 1) in {B, C, D}) or \
             (A > 1 and (A - 1) in {B, C, D}): continue
          # no zeroes occurred before the fourth update and no nines after this  
          if (A < 6 or 0 not in {B, C, D}) and \
             (A > 4 or 9 not in {B, C, D}):
            yield (A, B, C, D)
       
      # collect candidate numbers in decreasing order
      ns = list(generate())
      
      # keep maintaining the list of candidate numbers
      while True:
        # determine which numbers always exist for a specific leftmost digit ...
        d = dict()
        for A, B, C, D in ns:
          if A not in d:
            d[A] = {B, C, D}
          else:
            d[A] &= {B, C, D}
       
        # we need nine queue positions (each with a unique leftmost digit)
        if len(d) != 9: 
          print("no solution")
          exit(0)
      
        # ... and delete specific candidate numbers
        for k, vs in d.items():
          for v in vs:
            if k < 9:
              ns = [n for n in ns if n[0] != (k + 1) or v not in n[1:]]
            if k > 1:
              ns = [n for n in ns if n[0] != (k - 1) or v not in n[1:]]  
        
        
        delete = set()  
        # for each candidate number check if valid adjacent queue positions exist
        for A, B, C, D in ns:
          bef, aft = 0, 0
          for A2, B2, C2, D2 in ns:
            # skip if invalid 
            if len(set([A, B, C, D, A2, B2, C2, D2])) != 8: continue
            if A == 1 or A2 == A - 1:
              bef = 1  
            if A == 9 or A2 == A + 1:
              aft = 1
            # break if we find valid adjacent queue positions 
            if bef == aft == 1: break
          else:  
            # A, B, C, D has no (two) valid adjacent queue positions
            delete.add((A, B, C, D))
          
        if not delete: break
       
        # delete specific candidate numbers
        ns = [n for n in ns if n not in delete]
      
      print("certain positions:", [d2n(x[1]) for x in unique_col(ns)])
      

      Like

    • Frits's avatar

      Frits 5:47 pm on 24 December 2022 Permalink | Reply

      Borrowing Jim’s “intersect” call.

       
      from enigma import SubstitutedExpression, intersect
      from functools import reduce
      
      # we have nine 4-digit numbers = 36 variables
      # as the leftmost digits are known and the middle entry has to end on zero
      # we are left with 26 variables, so A-Z
      
      # 9ABC, 8DEF, 7GHI, 6JKL, 5MN0, 4OPQ, 3RST, 2UVW, 1XYZ
      nums = ['ABC', 'DEF', 'GHI', 'JKL', 'MN', 'OPQ', 'RST', 'UVW', 'XYZ']
      
      # convert digit sequences to number
      d2n = lambda s: reduce(lambda x,  y: 10 * x + y, s)
      
      # check validity 2nd argument and possible connection with 1st argument
      def check(s1, s2):
        n = d2n(s2)
        # it's leftmost digit and just two others in it were factors of it
        if n % s2[0] or sum(n % d == 0 for d in s2[1:] if d) != 2:
          return False
      
        if s1 and len(set(s1 + s2)) != 8: return False
        
        return True
      
      # invalid digit / symbol assignments
      d2i = dict()
      for d in range(0, 10):
        vs = set()
        if d:
          vs.update(nums[9 - d])
          if d < 9:
            vs.update(nums[8 - d])
          if d > 1:
            vs.update(nums[10 - d])  
        # no zeroes occurred before the fourth update and no nines after this     
        if d == 0:
          vs.update('ABCDEFGHIJKLMN') 
          # as 5MN0 also delete 0 from 4OPQ entries
          vs.update('OPQ') 
        if d == 9:
          vs.update('OPQRSTUVWXYZ')  
        if d % 2 == 0:
          vs.update('C')  
        d2i[d] = vs
      
      # the alphametic puzzle
      p = SubstitutedExpression(
        [
          # only check validity of 9ABC
          "check([], [9, A, B, C])",
          
          # check validity 2nd argument and connection with 1st argument
          "check([9, A, B, C], [8, D, E, F])",
          "check([8, D, E, F], [7, G, H, I])",
          "check([7, G, H, I], [6, J, K, L])",
          "check([6, J, K, L], [5, M, N, 0])",
          "check([5, M, N, 0], [4, O, P, Q])",
          "check([4, O, P, Q], [3, R, S, T])",
          "check([3, R, S, T], [2, U, V, W])",
          "check([2, U, V, W], [1, X, Y, Z])",
        ],
        answer="(9000+ABC, 8000+DEF, 7000+GHI, 6000+JKL, 5000+10*MN, 4000+OPQ, \
                 3000+RST, 2000+UVW, 1000+XYZ)",
        d2i=d2i,
        distinct=('ABC', 'DEF', 'GHI', 'JKL', 'MN', 'OPQ', 'RST', 'UVW', 'XYZ'),
        env=dict(check=check),
        denest=32,      # [CPython] work around statically nested block limit
        verbose=0,    # use 256 to see the generated code
      )
      
      answs = [y for _, y in p.solve()]
        
      print("certain positions:", list(d2n([x]) for x in intersect(answs)))
      

      Like

      • Jim Randell's avatar

        Jim Randell 4:41 pm on 26 December 2022 Permalink | Reply

        @Frits: That’s neat. I hadn’t thought of using the [[ SubstitutedExpression ]] solver.

        Here’s a run file that solves the puzzle (it does generate all possible sequences though, so it is not as fast as my second program).

        This run file executes in 98ms.

        Run: [ @replit ]

        #! python3 -m enigma -rr
        
        SubstitutedExpression
        
        # the positions are:
        # 9xxx = ABCD
        # 8xxx = EFGH
        # 7xxx = IJKL
        # 6xxx = MNPQ
        # 5xxx = RSTU
        # 4xxx = VWXY
        # 3xxx = Zabc
        # 2xxx = defg
        # 1xxx = hijk
        
        # leading digits are known
        --assign="A,9"
        --assign="E,8"
        --assign="I,7"
        --assign="M,6"
        --assign="R,5"
        --assign="V,4"
        --assign="Z,3"
        --assign="d,2"
        --assign="h,1"
        
        # no number shares digits with its neighbour
        --distinct="ABCDEFGH,EFGHIJKL,IJKLMNPQ,MNPQRSTU,RSTUVWXY,VWXYZabc,Zabcdefg,defghijk"
        
        # each number is divisible by its leading digit and exactly 2 of the others
        --code="divides = lambda n, d: d > 0 and n % d == 0"
        --code="check = lambda n, A, B, C, D: divides(n, A) and sum(divides(n, x) for x in (B, C, D)) == 2"
        
        "check({ABCD}, {A}, {B}, {C}, {D})"
        "check({EFGH}, {E}, {F}, {G}, {H})"
        "check({IJKL}, {I}, {J}, {K}, {L})"
        "check({MNPQ}, {M}, {N}, {P}, {Q})"
        "check({RSTU}, {R}, {S}, {T}, {U})"
        "check({VWXY}, {V}, {W}, {X}, {Y})"
        "check({Zabc}, {Z}, {a}, {b}, {c})"
        "check({defg}, {d}, {e}, {f}, {g})"
        "check({hijk}, {h}, {i}, {j}, {k})"
        
        # 9xxx is odd
        "{D} % 2 == 1"
        
        # no 0's in 9xxx, 8xxx, 7xxx, 6xxx
        --invalid="0,ABCDEFGHIJKLMNPQ"
        # no 9's in 5xxx, 4xxx, 3xxx, 2xxx, 1xxx
        --invalid="9,RSTUVWXYZabcdefghijk"
        
        # collect numbers that occur in all sequences
        --answer="({ABCD}, {EFGH}, {IJKL}, {MNPQ}, {RSTU}, {VWXY}, {Zabc}, {defg}, {hijk})"
        --code="fn = lambda xs: sorted(intersect(xs), reverse=1)"
        --accumulate="fn"
        
        --template="({ABCD} {EFGH} {IJKL} {MNPQ} {RSTU} {VWXY} {Zabc} {defg} {hijk})"
        --solution=""
        
        --denest=-1  # CPython workaround
        

        Like

    • Hugh Casement's avatar

      Hugh Casement 11:49 am on 1 January 2023 Permalink | Reply

      I found, in addition to those mentioned by Jim,
      6492, 6924, 6948
      eight possible numbers starting with 3 and also containing 015 or 156 in some order
      seven possible numbers starting with 2 and all containing 4 and 8
      twelve possible numbers starting with 1 and all containing 5.

      Like

  • Unknown's avatar

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

    Teaser 3136: Fill cells mentally, my dear Watson 

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

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

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

    In ascending order, which values did Watson know certainly?

    [teaser3136]

     
    • Jim Randell's avatar

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

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

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

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

      Run: [ @replit ]

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

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

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

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

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

      Together these 6 values use all the digits except 7.

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

      The candidates are:

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

      So there are 220 possible grids.

      Here is one possibility:

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

      Like

  • Unknown's avatar

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

    Teaser 3125: The bearings’ trait 

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

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

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

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

    Give the three bearings, in ascending order.

    [teaser3125]

     
    • Jim Randell's avatar

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

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

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

      It runs in 86ms.

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

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


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

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

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

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

      Like

      • Jim Randell's avatar

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

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

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

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

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

        Like

    • GeoffR's avatar

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

      Similar method to Jim’s first solution.

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

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

      Like

    • Frits's avatar

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

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

      Like

      • Frits's avatar

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

        Inspired by Nigel.

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

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

        Like

        • Jim Randell's avatar

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

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

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

          Like

    • Frits's avatar

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

      Too hot to go outside.

      Bringing down the number of viable bearings down to 15.

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

      Like

    • GeoffR's avatar

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

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

      Like

    • NigelR's avatar

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

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

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

      Like

      • Frits's avatar

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

        Well done.

        Using

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

        it is even more efficient.

        Like

        • NigelR's avatar

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

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

          Like

  • Unknown's avatar

    Jim Randell 6:14 pm on 1 June 2022 Permalink | Reply
    Tags: by: Stephen Hogg   

    Teaser 3115: Germometric mean 

    From The Sunday Times, 5th June 2022 [link] [link]

    On Whit Monday, Zak began self-isolating upstairs. At lunchtime Kaz shouted up: “What’s a Geometric Mean?”

    “It’s the Nth root of the product of N values”, Zak replied.

    On TV, Teaseside hospital’s “geovid” admissions for the seven days prior were listed alongside their Geometric Mean. Kaz stated that chronologically the numbers comprised a decreasing set of two-figure values, Friday’s value equalling the Geometric Mean. She added that, curiously, there was a value double the Geometric Mean, but not triple, whereas the Geometric Mean was triple a data value, but not double a data value. She then told Zak just the Geometric Mean.

    Zak worked out the unique data set.

    Give the seven numbers in chronological order.

    [teaser3115]

     
    • Jim Randell's avatar

      Jim Randell 6:41 pm on 1 June 2022 Permalink | Reply

      The following Python program runs in 70ms. (Internal run time is 9.7ms).

      Run: [ @replit ]

      from enigma import (irange, divisors, filter2, div, reverse, singleton, printf)
      
      # generate <k> increasing values from <vs> with product <v>
      def decompose(v, k, ds, ss=[]):
        if k == 1:
          if v in ds:
            yield ss + [v]
        elif not (len(ds) < k):
          for (i, d) in enumerate(ds):
            v_ = div(v, d)
            if v_ is not None:
              yield from decompose(v_, k - 1, ds[i + 1:], ss + [d])
      
      # generate candidate values for geometric mean <gm>
      def generate(gm):
        # find the remaining values
        ds = list(d for d in divisors(gm, 6) if 9 < d < 100 and d != gm)
        for vs in decompose(gm**6, 6, ds):
          # there are 2 values less than gm (and 4 values greater)
          (lt, gt) = filter2((lambda x: x < gm), vs)
          if len(lt) != 2: continue
          # there is a gm/3 value, but not a gm/2 value
          if (gm // 3 not in lt) or (div(gm, 2) in lt): continue
          # there is a gm*2 value, but not a gm*3 value
          if (gm * 2 not in gt) or (gm * 3 in gt): continue
          # return the sequence, Mo - Su
          yield reverse(gt) + [gm] + reverse(lt)
      
      # consider values for the geometric mean: gm/3 and gm*2 are 2-digit values
      for gm in irange(30, 49, step=3):
        # is there only a single candidate solution?
        ns = singleton(generate(gm))
        if ns:
          # output solutions
          printf("{ns}")
      

      Solution: The numbers are (Mon – Sun): 96, 72, 64, 54, 48, 32, 16.

      The geometric mean is 48, and there is a value twice the geometric mean (98), but not three times (144). There is also a value one third of the geometric mean (16), but not one half (24).


      This is the only possible sequence with a geometric mean of 48.

      However there are other possible sequences with a geometric mean of 42:

      (98, 84, 81, 49, 42, 14, 12)
      (98, 84, 54, 49, 42, 18, 14)
      (84, 63, 56, 49, 42, 27, 14)
      (84, 63, 54, 49, 42, 28, 14)

      Like

      • Jim Randell's avatar

        Jim Randell 6:49 am on 2 June 2022 Permalink | Reply

        And here is an even faster version:

        If g is the geometric mean we have a decreasing sequence of values (Mon – Sun):

        (a, b, c, d, g, e, f)

        Such that:

        g^7 = a × b × c × d × g × e × f

        And we know one of the values greater than g (i.e. one of (a, b, c, d)) has a value of 2g, and one of the values less than g (i.e. one of (e, f)) has a value of (g / 3) (which tells us g is a multiple of 3).

        Hence:

        (3/2)(g^4) = [3 of (a, b, c, d)] × [1 of (e, f)]

        Which tells us g is also a multiple of 2.

        Furthermore (g / 3) is a 2-digit value, so g ≥ 30, and 2g is also a 2-digit value, so g < 50.

        This Python program considers possible values for the geometric mean, and finds the three remaining values higher than the mean, and the value lower than the mean. If a mean leads to a single possible set of values, then we have found the solution.

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

        Run: [ @replit ]

        from enigma import (irange, div, diff, singleton, printf)
        
        # reversed sort
        rsort = lambda x: sorted(x, reverse=1)
        
        # find <k> increasing values from <ds> whose product divide into <v>
        # return: (<ns>, <r>) where <r> is the remainder
        def decompose(v, k, ds, ns=[]):
          if k == 0:
            yield (ns, v)
          elif not (len(ds) < k):
            for (i, d) in enumerate(ds):
              v_ = div(v, d)
              if v_ is not None:
                yield from decompose(v_, k - 1, ds[i + 1:], ns + [d])
        
        # generate candidate sequences for geometric mean <gm>
        def generate(gm):
          # find 2-digit divisors of (3/2)gm^4 greater than gm
          v = 3 * div(gm**4, 2)
          ds = diff((d for d in irange(gm + 1, 99) if v % d == 0), {2 * gm, 3 * gm})
          for (gt, r) in decompose(v, 3, ds):
            # there is no gm/2 value
            if (not 9 < r < gm) or 2 * r == gm or 3 * r == gm: continue
            # return the sequence (in decreasing order)
            yield rsort(gt + [2 * gm]) + [gm] + rsort([r, gm // 3])
        
        # consider values for the geometric mean, gm is a multiple of 6
        for gm in irange(30, 49, step=6):
          # is there only a single candidate solution?
          ns = singleton(generate(gm))
          if ns:
            # output solutions
            printf("[Mon - Sun] = {ns}")
        

        Like

        • Frits's avatar

          Frits 1:30 pm on 2 June 2022 Permalink | Reply

          Using divisors_tuples() is slower than the decompose() method.

             
          from enigma import divisors_tuples
          
          # return entry where column <col> is unique
          def unique_col(seq, col=0):
            d = dict()
            for s in seq:
                 d[s[col]] = s[col] in d
             
            return [s for s in seq if not d[s[col]]]
          
          # there is a gm/3 value and there is a 2*gm value
          # gm must also be even as gm^7 is even 
          
          sol = []
          # consider values for the geometric mean: gm/3 and gm*2 are 2-digit values
          for gm in range(30, 50, 6):
            # generate seven 2-digit numbers that multiply to gm^7
            nums = [sorted(x + (gm // 3, gm, 2 * gm), reverse = True) 
                   for x in divisors_tuples(3 * (gm**4 // 2), 4) 
                   if all(9 < y < 100 for y in x) 
                      and gm // 2 not in x 
                      and 3 * gm not in x]
           
            # store valid solutions
            sol += [x for x in nums if len(set(x)) == 7 and x[4] == gm]
          
          print("answer =", *unique_col(sol, 4))
          

          A basic SubstitutedExpression program takes about 5 seconds (with PyPy).

          Like

    • Frits's avatar

      Frits 2:58 pm on 2 June 2022 Permalink | Reply

      Program runs in 75ms (with PyPy).

         
      from enigma import SubstitutedExpression
      
      # the alphametic puzzle
      p = SubstitutedExpression(
        [
         # there is a gm/3 value and there is a gm*2 value
         # gm must also be even as gm^7 is even so E is even
         "(G + M) % 3 == 0",
         
         # 1.5 * GM**4 is the product 4 unknown sorted numbers AB, CD, EF and PQ
        
         "AB < GM",
         "CD > GM",
         "EF > GM",
         "CD < EF",
         
         "div(3 * GM**4 // 2, AB * CD * EF) = PQ",
         "PQ > EF",
         
         # seven different numbers
         "len(set([AB, CD, EF, PQ, GM, 2 * GM, GM // 3])) == 7",
         # friday's value equalling the geometric mean
         "sorted([AB, CD, EF, PQ, GM, 2 * GM, GM // 3])[2] == GM",
         
         # there is no 3*gm or gm/2 value
         "3 * GM not in {AB, CD, EF, PQ, GM, 2 * GM, GM // 3}",
         "GM // 2 not in {AB, CD, EF, PQ, GM, 2 * GM, GM // 3}",
         
        ],
        answer="sorted([AB, CD, EF, PQ, GM, 2 * GM, GM // 3], reverse=1)",
        d2i=dict([(0, "GACEP")] + 
                 [(1, "GCEM")] +                       
                 [(2, "GCE")] +
                 [(3, "M")] +
                 [(5, "AGM")] +
                 [(6, "AG")] +
                 [(7, "AGM")] +
                 [(8, "AG")] +
                 [(9, "AGM")]),
        distinct="",
        reorder=0,
        verbose=0,    # use 256 to see the generated code
      )
      
      # store answers
      d = dict()
      for (_, ans) in p.solve():
        d[ans[4]] = d.get(ans[4], []) + [ans]
        
      for v in d.values():
        if len(v) == 1:
          print(*v) 
      

      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