Updates from Jim Randell Toggle Comment Threads | Keyboard Shortcuts

  • Unknown's avatar

    Jim Randell 9:14 am on 18 July 2023 Permalink | Reply
    Tags:   

    Brainteaser 1746: Party time 

    From The Sunday Times, 3rd March 1996 [link]

    In the volatile world of Ruritanian politics, each of the three parties can safely expect to lose a fixed proportion of its supporters to each of the other parties from one election to the next. Thus, the Story party would retain a fixed proportion of its supporters, and lose fixed proportions (which may differ) to the Laboured party and to the Mauve Shirts, and so on. Three elections ago, the Story party and the Laboured party each won 40,000 votes and the Mauve Shirts won 20,000. Two elections ago, the Story party and the Mauve Shirts each won 35,000 votes, while the Laboured party won 30,000 votes. In the last election the Story party and the Laboured party each won 35,000 votes and the Mauve Shirts won 30,000. The total electorate in the current election remains at 100,000.

    What is the minimum number of votes the Mauve Shirts should expect to win in the current election?

    [teaser1746]

     
    • Jim Randell's avatar

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

      See also: Enigma 662.

      Given election 1 and election 2 we have 9 variables, of the form XY, that denote the fraction of voters for party X in election 1 that voted for party Y in the election 2.

      All votes for each party are redistributed among the three parties, so:

      SS + SL + SM = 1
      LS + LL + LM = 1
      MS + ML + MM = 1

      and (working in units of 1000 votes) for election 2:

      35 = 40 SS + 40 LS + 20 MS
      30 = 40 SL + 40 LL + 20 ML
      35 = 40 SM + 40 LM + 20 MM

      and for election 3:

      35 = 35 SS + 30 LS + 35 MS
      35 = 35 SL + 30 LL + 35 ML
      30 = 35 SM + 30 LM + 35 MM

      and for election 4 we have:

      S = 35 SS + 35 LS + 30 MS
      L = 35 SL + 35 LL + 30 ML
      M = 35 SM + 35 LM + 30 MM

      where S, L, M must be whole numbers of votes.

      The first 3 blocks give us 9 equations in 9 variables, so it looks like we could solve this system and get the answers for the final block.

      However if we try to do this, we find that the 9 equations are not enough to solve the system (i.e. we do not have 9 independent equations, so the system of equations is incomplete). And this makes sense, as the puzzle asks us for the minimum number of votes M can expect to win, so the equations will give a range of values for S, L, M.

      But we can solve the system by choosing values for some of the variables until the system becomes complete. It turns out that if we give the values for 2 of the variables the rest can be determined.

      By dividing the 20k votes for M in the first election into votes that subsequently go to S, L, M, we can determine values for MS, ML, MM and so the remaining values are determined.

      This program looks at different ways to allocate the 20k votes in blocks of 1000 votes, and calculates the results of the 4th election, discarding situations where we do not end up with a whole number of votes.

      The program runs in 129ms. (Internal runtime is 60ms).

      Run: [ @replit ]

      from enigma import (
        Rational, Matrix, Accumulator, decompose, as_int, seq2str, printf
      )
      
      Q = Rational()
      
      eqs = [
        # SS  LS  MS  SL  LL  ML  SM  LM  MM
        (( 1,  0,  0,  1,  0,  0,  1,  0,  0),  1),
        (( 0,  1,  0,  0,  1,  0,  0,  1,  0),  1),
        (( 0,  0,  1,  0,  0,  1,  0,  0,  1),  1),
        ((40, 40, 20,  0,  0,  0,  0,  0,  0), 35),
        (( 0,  0,  0, 40, 40, 20,  0,  0,  0), 30),
        (( 0,  0,  0,  0,  0,  0, 40, 40, 20), 35),
        ((35, 30, 35,  0,  0,  0,  0,  0,  0), 35),
        (( 0,  0,  0, 35, 30, 35,  0,  0,  0), 35),
        (( 0,  0,  0,  0,  0,  0, 35, 30, 35), 30),
      ]
      
      # validate a fraction in the interval 0 .. 1
      def valid(x):
        if x < 0 or x > 1: raise ValueError()
        return x
      
      # record minimum value for M is 4th election
      r = Accumulator(fn=min, collect=1)
      
      # choose MS, ML, MM as fractions of N
      N = 20
      for (a, b, c) in decompose(N, 3, increasing=0, sep=0, min_v=0):
      
        # add these into the mix
        eqs_ = [
          # SS LS MS SL LL ML SM LM MM
          (( 0, 0, 1, 0, 0, 0, 0, 0, 0), Q(a, N)),
          (( 0, 0, 0, 0, 0, 1, 0, 0, 0), Q(b, N)),
          (( 0, 0, 0, 0, 0, 0, 0, 0, 1), Q(c, N)),
        ]
      
        try:
          vs = Matrix.linear(eqs + eqs_, field=Q, valid=valid)
        except ValueError:
          continue
      
        # calculate total votes in the 4th election
        (SS, LS, MS, SL, LL, ML, SM, LM, MM) = vs
      
        S = 35*SS + 35*LS + 30*MS
        L = 35*SL + 35*LL + 30*ML
        M = 35*SM + 35*LM + 30*MM
        # check the values are integers
        try:
          (S, L, M) = (as_int(1000 * x) for x in (S, L, M))
        except ValueError:
          continue
        printf("[S={S} L={L} M={M} {vs}]", vs=seq2str(vs))
        r.accumulate_data(M, vs)
      
      # output solutions
      M = r.value
      for vs in r.data:
        printf("M={M} {vs}", vs=seq2str(vs))
      

      Solution: The Mauve Shirts should expect to win at least 30625 votes.

      Here is one set of fractions that give the required values:

      Note that no-one who votes for M in one election, votes for M in the next election.


      But this is not the only set that gives us the minimum value for M, by increasing the value of N at line 29, we can find further viable sets of values, for example:

      However, the fractions in the bottom row are always the same:

      SM = 3/4, LM = 1/8, MM = 0

      Giving the total votes for M in the 4th election:

      M = 35000 × 3/4 + 35000 × 1/8
      M = 26250 + 4375
      M = 30625

      Like

      • Jim Randell's avatar

        Jim Randell 10:22 pm on 19 July 2023 Permalink | Reply

        The equations can be simplified to give:

        The values of x and y can be restricted by noting that the value of the fraction in each box is between 0 and 1.

        And we can calculate the number of votes for M in the 4th election:

        M = 35000 (S→M) + 35000 (L→M) + 30000 (M→M)
        M = 43125 − 12500z

        And the maximum value for z (= x + y) is 1, which gives a minimum value for M = 30625.

        All that remains is to show that the minimum value is achievable, which we can do by considering an example, and checking that all the numbers of votes remain whole numbers.

        With x = 2/5 and y = 3/5 (as mentioned above) we have:

        1: S=40000 [→S 6000, →L 4000, →M 30000]
        1: L=40000 [→S 21000, →L 14000, →M 5000]
        1: M=20000 [→S 8000, →L 12000, →M 0]

        2: S=35000 [→S 5250, →L 3500, →M 26250]
        2: L=30000 [→S 15750, →L 10500, →M 3750]
        2: M=35000 [→S 14000, →L 21000, →M 0]

        3: S=35000 [→S 5250, →L 3500, →M 26250]
        3: L=35000 [→S 18375, →L 12250, →M 4375]
        3: M=30000 [→S 12000, →L 18000, →M 0]

        4: S=35625
        4: L=33750
        4: M=30625

        With these values it is not possible for another election to follow the same pattern keeping the votes as integers.

        But, if we allow fractional votes the situation settles into a steady state with (to 2dp):

        S = 35384.62
        L = 33846.15
        M = 30769.23

        Like

    • Hugo's avatar

      Hugo 10:18 am on 18 July 2023 Permalink | Reply

      According to my calculations the fewest and most votes that each party can expect are
      L 32500 – 34060, M 30625 – 32965, S 33750 – 36090.
      I’ve forgotten how I got there!

      Like

      • Jim Randell's avatar

        Jim Randell 10:16 pm on 19 July 2023 Permalink | Reply

        I agree with these values.

        I found there are 122,304 splits of votes from the 1st election that allow whole votes in the 4th election.

        Like

  • Unknown's avatar

    Jim Randell 12:31 pm on 16 July 2023 Permalink | Reply
    Tags:   

    Brain-Teaser 207: Prime flats 

    From The Sunday Times, 11th April 1965 [link]

    The number of families living on one floor in a block of flats is prime. Each family consists of a pair of parents and at least one child; the number of people in each family and the total number of people in all families are prime numbers. Every one had recently had a birthday anniversary, and each person’s age is a prime number. No one is older than 40, and every parent was at least 23 before their first child was born. Each of the prime numbers described above occurs exactly twice.

    The sum of the ages of each family is a prime number, and a different one in each case. The sum of the ages of  everyone is a prime number.

    What are the ages of the people in each family?

    [Note: In this puzzle the number 1 is counted as a prime number].

    This puzzle is included in the book Sunday Times Brain Teasers (1974). The puzzle text above is taken from the book.

    [teaser207]

     
    • Jim Randell's avatar

      Jim Randell 12:32 pm on 16 July 2023 Permalink | Reply

      Parent’s ages are primes in the interval [24, 40], so there are only 3 possibilities: 29, 31, 37.

      And children’s ages can be 1 or a prime, in the interval [1, 14], so there are only 7 possibilities: 1, 2, 3, 5, 7, 11, 13.

      As each prime can only appear twice there cannot be more than 6 parents, i.e. 3 families, and as the number of families is prime it must be 2 or 3. However, if there were 2 families each with an age sum that was prime, then combining their ages would give an even (non-prime) number. So there must be 3 families.

      (For a unique solution it is necessary to exclude the possibility of the number of families being 1. This can be done by noting that the families are referred to in the puzzle text as plural, although I don’t really consider this to be strong enough. So it may have been better to specifically note that there is more than one family, or to specifically allow 1 to be used only as a child’s age).

      This Python program generates possible family groups, where the parents ages are chosen from those given above, and the number of children is chosen such that the total number if the family group is prime. Ages are then chosen for the children to give a prime value for total sum of the ages in the family. And the viable family groups are collected together by total age.

      We then choose a collection of different total ages for the family groups, such that the combined age of all groups is prime, and the populate the groups with actual ages, such that when all the specified primes are collected together each is used exactly twice.

      The program runs in 665ms.

      Run: [ @replit ]

      from enigma import (multiset, subsets, is_prime, group, item, printf)
      
      # available ages for parents and children
      aps = multiset.from_seq([29, 31, 37], count=2)
      aks = multiset.from_seq([1, 2, 3, 5, 7, 11, 13], count=2)
      
      # generate possible family groups
      def generate():
        # choose the ages for the parents
        for ps in subsets(aps, size=2, select='mC'):
          tp = sum(ps)
          # children are at least 23 years younger than the youngest parent
          m = min(ps) - 22
          aks_ = aks.restrict(x for x in aks.keys() if x < m)
          # choose k = the number of children (such that k + 2 is prime)
          for k in (1, 3, 5, 9, 11):
            # choose the ages of the children
            for ks in subsets(aks_, size=k, select='mC'):
              # total of ages in the family
              t = tp + sum(ks)
              if is_prime(t):
                yield (t, ps + ks)
      
      # group families by total age
      fams = group(generate(), by=item(0), f=item(1))
      
      # choose family groups with total ages in <ts>
      # fs = families chosen so far
      # vs = ages used so far
      def choose(ts, fs=[], vs=multiset()):
        if not ts:
          # total number of people is prime
          P = vs.size()
          if not is_prime(P): return
          # check primes in the required set appear twice
          # number of families, total number of people, size of each family
          m = vs.combine((len(fs), P), map(len, fs))
          if not m.all_same(2): return
          # return the families
          yield fs
        else:
          # choose a family with total age <t>
          for f in fams[ts[0]]:
            vs_ = vs.combine(f)
            if not vs_.is_duplicate(2):
              yield from choose(ts[1:], fs + [f], vs_)
      
      # the number of families (must be 3)
      n = 3
      # choose the total ages for each family
      for ts in subsets(fams.keys(), size=n, fn=list):
        # total sum of all ages is prime
        T = sum(ts)
        if not is_prime(T): continue
        # choose the family groups from the ages
        ts.sort(reverse=1)
        for fs in choose(ts):
          # output solution
          printf("{n}: {ts}; T={T} -> {fs}")
      

      Solution: The ages in the family groups are: (37, 37; 13, 7, 7), (31, 31; 2, 2, 1), (29, 29; 1).

      Like

      • Frits's avatar

        Frits 5:45 pm on 16 July 2023 Permalink | Reply

        You can argue that there is no solution.

        “Each of the prime numbers described above occurs exactly twice”.

        The total number of people in all families only occurs once.

        Like

        • Jim Randell's avatar

          Jim Randell 5:52 pm on 16 July 2023 Permalink | Reply

          @Frits: There are 13 people in total, and one of the children is aged 13, so the prime number 13 occurs twice.

          Like

          • Frits's avatar

            Frits 6:13 pm on 16 July 2023 Permalink | Reply

            @Jim, Thanks, I had read it as total number of all ages (227).

            I wonder if I can use SubstitutedExpression() as it is not immediately clear what limit to use for the number of children per family (at least <= 12).

            Like

          • Frits's avatar

            Frits 1:41 pm on 17 July 2023 Permalink | Reply

            A SubstitutedExpression() solution using all letters A-Z takes 1 second with PyPy. As I now tend to use assignment statements with the reorder=0 option the denest option often can’t be used anymore as assignments are not transfered to another nested level.

            A quicker version could be made by further analysis.
            The total number of children must be 7 as 5 fails ((1, 1, 3) results in three 3’s) and 11 fails as well (out of 14 candidates we already lose 4 (number of families and three number of family members). The seven children must occur in a (1, 3, 3) distribution.

            Like

  • Unknown's avatar

    Jim Randell 4:52 pm on 14 July 2023 Permalink | Reply
    Tags:   

    Teaser 3173: Dots and boxes 

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

    In this game, two players take turns drawing a single horizontal or vertical line between two unjoined adjacent dots in a rectangular grid. A player who completes the fourth side of a 1×1 box earns one point and takes another turn. The winner is the one with the most points at the end. A recent game with 4×4 dots (as above) between two beginners, reached a critical position when no box had more than two sides already drawn and the next player was forced to complete the third side of some box. It turned out that this was the shortest possible 4×4 game to reach a critical position. Their next 4×4 game reached a critical position in the largest possible number of turns.

    How many turns [did it take to reach the critical position] in each game?

    [teaser3173]

     
    • Jim Randell's avatar

      Jim Randell 6:26 pm on 14 July 2023 Permalink | Reply

      Here is a Numberphile video about strategy in the game [@youtube].

      Not very fast (it takes 17.4s), but here is a brute force breadth first search for critical positions. It uses bitmasks to speed things up, but takes no account of symmetry. I’m not convinced this is the best approach to the problem though.

      from enigma import (Accumulator, irange, bit_from_positions as bits, dsum2, cache, printf)
      
      # the boxes
      boxes = [
        bits({0, 3, 12, 15}),
        bits({1, 4, 15, 18}),
        bits({2, 5, 18, 21}),
        bits({3, 6, 13, 16}),
        bits({4, 7, 16, 19}),
        bits({5, 8, 19, 22}),
        bits({6, 9, 14, 17}),
        bits({7, 10, 17, 20}),
        bits({8, 11, 20, 23}),
      ]
      
      # the lines
      lines = tuple(1 << v for v in irange(24))
      
      # check a position is subcritical
      check = cache(lambda ss: all(dsum2(ss & box) < 3 for box in boxes))
      
      # accumulate min/mix critical positions
      acc = Accumulator.multi(fns=[min, max])
      
      # initial position
      ps = { (0, lines) }
      
      # perform a breadth first search
      while ps:
        ps_ = set()
        # consider positions
        for (ss, rs) in ps:
          # for each remaining line try to add it
          for r in rs:
            ss_ = ss | r
            if check(ss_):
              # this is a viable new position, find remaining lines
              rs_ = tuple(x for x in rs if x != r and check(ss_ | x))
              if not rs_:
                # this is a critical position
                acc.accumulate_data(dsum2(ss_), ss_)
              else:
                ps_.add((ss_, rs_))
        ps = ps_
      
      # output solution
      for r in acc:
        printf("{fn} critical = {v} [{ss:024b}]", fn=r.fn.__name__, v=r.value, ss=r.data)
      

      Solution: The first game took 8 turns to reach the critical position. The second game took 14 turns to reach the critical position.

      There is one size 8 critical position:

      There are 138 size 14 critical positions (although this will include rotations/reflections). Here is one of them:

      Like

      • Frits's avatar

        Frits 7:14 pm on 14 July 2023 Permalink | Reply

        @Jim, I got the same results with SubstitutedExpression(), 9 seconds with PyPy.
        Somehow the denest option doesn’t work (still saying “too many statically nested blocks”).

        Your version ran for 46 seconds on my computer with CPython (17 seconds with PyPy).

        Like

      • Frits's avatar

        Frits 7:55 pm on 14 July 2023 Permalink | Reply

        Without analysis, to be run with PyPy (takes 9 seconds).

         
        from enigma import SubstitutedExpression
        
        # . A . B . C . 
        # D   E   F   G 
        # . H . I . J . 
        # K   L   M   N 
        # . O . P . Q . 
        # R   S   T   U 
        # . V . W . X . 
        
        # do we reach a critical position for any drawn line?
        def check(vs):
          for i, v in enumerate(vs):
            if v: continue
            # update v from 0 to 1
            if all(sum(b) < 3 for b in make_boxes(vs[:i] + [1] + vs[i+1:])):
              return False
          # each update reached a critical position 
          return True
              
        # return a list of all boxes    
        def make_boxes(vs):
          assert len(vs) == 24 
          A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X = vs
          return [(A, D, E, H), (B, E, I, F), (C, F, G, J), \
                  (H, K, L, O), (I, L, M, P), (J, M, N, Q), \
                  (O, R, S, V), (P, S, T, W), (Q, T, U, X)]
        
        # the alphametic puzzle
        p = SubstitutedExpression(
          [ # not yet a critical conditions
            "not any(sum(b) > 2 for b in @boxes)",
            #"not any(sum(b) > 2 for b in make_boxes(@vars))",
            
            # do we reach a critical position for any drawn line?
            "check(@vars)",
          ],
          answer="((A, B, C), (D, E, F, G), (H, I, J), (K, L, M, N), \
                   (O, P, Q), (R, S, T, U), (V, W, X))",
          distinct="",
          macro=dict([("boxes", "[(A, D, E, H), (B, E, I, F), (C, F, G, J), \
                                  (H, K, L, O), (I, L, M, P), (J, M, N, Q), \
                                  (O, R, S, V), (P, S, T, W), (Q, T, U, X)]")] +
                     [("vars", "[A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X]")]),
          env=dict(check=check, make_boxes=make_boxes),
          digits=range(0, 2),
          reorder=0,
          denest=1,
          verbose=0,    # use 256 to see the generated code
        )
        
        # used for printing a grid
        #f = lambda x, m: "  " if not x and m == "h" else " " if not x and m == "v" \
        #                 else "--" if m == "h" else "|" 
        
        sols = set()
        # print answers
        for ans in p.answers():
          
          sols.add(turns := sum(y for x in ans for y in x))
          '''
          # print grid
          print("\nturns", turns)
          for a in ans:
            if len(a) == 3: 
              m = 'h'
              print(f".{f(a[0], m)}.{f(a[1], m)}.{f(a[2], m)}.")
            else:
              m = 'v'
              print(f"{f(a[0], m)}  {f(a[1], m)}  {f(a[2], m)}  {f(a[3], m)}")
          '''
        
        if len(sols) > 1:
          print(f"answer: {min(sols)} and {max(sols)}")
        

        Like

      • Jim Randell's avatar

        Jim Randell 11:31 pm on 14 July 2023 Permalink | Reply

        And here is a depth first search of critical positions that runs in 837ms (without using bitmasks).

        Run: [ @replit ]

        from enigma import (Accumulator, irange, printf)
        
        # map lines (0..23) to boxes (0..8)
        box = [
          [0], [1], [2],
          [0, 3], [1, 4], [2, 5],
          [3, 6], [4, 7], [5, 8],
          [6], [7], [8],
          [0], [3], [6],
          [0, 1], [3, 4], [6, 7],
          [1, 2], [4, 5], [7, 8],
          [2], [5], [8],
        ]
        
        # indices for lines
        n = len(box)
        
        # check all boxes have less than 2 lines
        check = lambda bs, js: all(bs[j] < 2 for j in js)
        
        # find critical positions
        # k = current line
        # bs = count of lines for each box
        # ss = lines used
        def solve(k, bs, ss=[]):
          # are we done?
          if k == n:
            # check all remaining lines put >2 lines in a box
            for i in irange(n):
              if i not in ss and check(bs, box[i]): return
            # this is a critical position
            yield ss
          else:
            # try to include the next line
            js = box[k]
            if check(bs, js):
              bs_ = list(bs)
              for j in js: bs_[j] += 1
              yield from solve(k + 1, bs_, ss + [k])
            # or not
            yield from solve(k + 1, bs, ss)
        
        # collect min/max size critical positions
        acc = Accumulator.multi(fns=[min, max])
        for ss in solve(0, [0] * 9):
          acc.accumulate_data(len(ss), ss)
        
        # output solution
        for r in acc:
          printf("{fn} critical = {v} {ss}", fn=r.fn.__name__, v=r.value, ss=r.data)
        

        I can formulate the maximum critical position as a minimum hitting set problem, but I don’t see how to do something similar for the minimum critical position.

        Like

    • Frits's avatar

      Frits 1:18 pm on 16 July 2023 Permalink | Reply

      Jim’s 2nd program still took 1.7 seconds on my computer (with PyPy 7.3.11) so I wanted to have a faster program (Cpython is faster than PyPY this time). Fasted time (they do vary) I have seen is 140 ms.
      Somehow I found it easier to work with non-lines (or gaps or lines to erase).

      I avoided to hardcode some analysis (like code for the 4 corner boxes) which resulted in more code.

       
      from itertools import combinations
      
      # map lines (0..23) to boxes (0..8)
      ln2bxs = [
        [0], [1], [2],
        [0, 3], [1, 4], [2, 5],
        [3, 6], [4, 7], [5, 8],
        [6], [7], [8],
        [0], [3], [6],
        [0, 1], [3, 4], [6, 7],
        [1, 2], [4, 5], [7, 8],
        [2], [5], [8],
      ]
      
      N = len(ln2bxs)
      
      boxes = [[i for i, b in enumerate(ln2bxs) if n in b] for n in range(9)]
      
      # pick a minimum of <m> elements from <s>
      def pickAtLeast(s, m, ns=()):
        if s:
          for i, x in enumerate(s):
            if len(ns) > m - 2:
              yield(ns + (x, ))
            yield from pickAtLeast(s[i + 1:], m, ns + (x, ))  
      
      # fill boxes from <bxs> so that each box has at least <m> non-lines
      def processBox(bxs, M, box_ns=[], m=2, ns=set(), skip=set()):
        if not box_ns: 
          if len(ns) == M:
            # check if drawing these non-lines always put >2 lines in a box
            for i in ns:
              # return if all boxes for a non-line have at most one line
              if all(len(ns & set(boxes[b])) > 2 for b in ln2bxs[i]): return
            yield ns 
        else: # still boxes to check/fill?
          
          # selection of elements not yet picked 
          s = [x for x in bxs[box_ns[0]] if x not in ns]
          # we need to pick at least <m> - 4 + len(s) elements from s
          if (num := m - 4 + len(s)) <= 0:
            # try next box
            yield from processBox(bxs, M, box_ns[1:], m, ns, skip)
          else:  
            # pick a minimum of <num> elements from <s>
            for p in pickAtLeast(s, num):
              # skip if a side hasn't been selected before
              # check with maximum number of non-lines per box and don't exceed M
              if any(x in skip for x in p) or \
                 len(p) + 4 - len(s) > d[tuple(boxes[box_ns[0]])] or \
                 len(p) + len(ns) > M: 
                continue
            
              skip_ = skip | set(s).difference(p)
              yield from processBox(bxs, M, box_ns[1:], m, ns | set(p), skip_)
      
      # assume every line is drawn and try to put in non-lines (ie erase lines)
      
      # if a box has <n> non-shared sides then it can't have more than 4 - <n>
      # non-lines otherwise no critical position is reached when drawing a 
      # non-shared side in this box  
      
      # determine the maximum number of non-lines per box
      d = {tuple(ln): 4 - sum(1 for x in ln if len(ln2bxs[x]) == 1) for ln in boxes}  
      
      box_order = [y for x, y in sorted((x, i) for i, x in enumerate(d.values()))]
      
      # determine the minimum number of lines for the whole grid
      min_lines = 0
      for k in range(5, 0, -1): 
        # for all <k> box combinations 
        for c in combinations(boxes, k):
          # boxes with no shared sides?
          if len(set(y for x in c for y in x)) == k * 4: 
            # count the guaranteed number of lines per box
            if (n := sum(4 - d[tuple(x)] for x in c)) >= min_lines:
              min_lines = n
        
        # break if no improvement at a lower level
        if (k - 1) * 2 <= min_lines: break
      
      # for each game find the requested number of turns
      for g in range(1, 3):
        print("game", g, end = ": ")
        rng = range(N - min_lines, 0, -1) if g == 1 else range(1, N)
        for M in rng: 
          # fill boxes with <M> non-lines so that each box has at least 2 non-lines
          # start with corner boxes 
          for _ in processBox(boxes, M, box_order):
            print(N - M)
            break
          else: # no break  
            continue
      
          # we have found a critical position
          break
        else:  
          print()  
      

      Like

  • Unknown's avatar

    Jim Randell 9:10 am on 13 July 2023 Permalink | Reply
    Tags:   

    Teaser 2192: Digital shuffle 

    From The Sunday Times, 19th September 2004 [link]

    I have tried rearranging the nine digits from one to nine into various expressions of the following form:

    What is the largest whole number answer that I can get?

    [teaser2192]

     
    • Jim Randell's avatar

      Jim Randell 9:11 am on 13 July 2023 Permalink | Reply

      This Python program runs in 94ms. Internal runtime is 30ms.

      Run: [ @replit ]

      from enigma import (Accumulator, irange, subsets, fraction, printf)
      
      # available digits
      digits = set(irange(1, 9))
      
      # find maximum configurations
      r = Accumulator(fn=max, collect=1)
      
      # choose the denominators R, T, V, X in increasing order
      for (R, T, V, X) in subsets(digits, size=4, select='C'):
        # allocate the numerators
        for (Q, S, U, W, Y) in subsets(digits.difference((R, T, V, X)), size=5, select='P'):
          # evaluate the expression
          (a, b) = fraction(Q, R,  S, T,  U, V,  W, X,  -Y, 1)
          if b == 1:
            r.accumulate_data(a, (Q, R, S, T, U, V, W, X, Y))
      
      # output solution(s)
      for (Q, R, S, T, U, V, W, X, Y) in r.data:
        printf("{Q}/{R} + {S}/{T} + {U}/{V} + {W}/{X} - {Y} = {r.value}")
      

      Solution: The largest possible answer is 12.

      9/1 + 7/2 + 8/3 + 5/6 − 4 = 12

      Like

    • Frits's avatar

      Frits 1:11 pm on 13 July 2023 Permalink | Reply

      #! python3 -m enigma -r
      
      SubstitutedExpression
      
      # Q/R + S/T + U/V + W/X - Y = AB
      "div(Q*T*V*X + R*S*V*X + R*T*U*X + R*T*V*W - R*T*V*X*Y, R*T*V*X) = AB"
      
      --answer="AB"
      --distinct="QRSTUVWXY"
      --accumulate=max
      --invalid="0,QRSTUVWXY"
      --verbose=16   
      

      Like

      • Frits's avatar

        Frits 1:20 pm on 13 July 2023 Permalink | Reply

        @Jim, is it possible to get a button like crayon on PuzzlingInPython?
        It makes it less error prone to post replies.

        Like

      • Jim Randell's avatar

        Jim Randell 2:02 pm on 13 July 2023 Permalink | Reply

        Requiring the denominators be ordered makes this approach run about 10× faster:

        Run: [ @replit ]

        #! python3 -m enigma -rr
        
        SubstitutedExpression
        
        --distinct="QRSTUVWXY"
        --invalid="0,QRSTUVWXY"
        --code="q = Rational()"
        
        "as_int(q(Q, R) + q(S, T) + q(U, V) + q(W, X) - Y) = AB"
        
        "R < T" "T < V" "V < X"
        
        --answer="AB"
        --accumulate="max"
        

        (Is “crayon” a WordPress plugin? I don’t think it is possible to install plugins on a free WordPress site. It looks like you need to pay £20/month to be able to do that).

        Like

      • Frits's avatar

        Frits 5:48 pm on 13 July 2023 Permalink | Reply

        Not using the fact that 5 and 7 can’t be used as denominators.

           
        from fractions import Fraction as RF
        
        # Q/R + S/T + U/V + W/X - Y = mx
        
        # available digits
        digits = set(range(1, 10))
        
        # maximum sum for <k> fractions choosing numbers from a sorted sequence <s> 
        def maxleft(s, k=4):
          assert len(s) >= 2 * k
          return int(sum([RF(s[-1 - x], s[x]) for x in range(k)]))
        
        # check if we can make total m of <k> fractions from digits in set <s> 
        def check(s, m, k=4, ns=[]):
          if k == 1:
            ls = list(s)
        
            if m in {RF(ls[0], ls[1]), RF(ls[1], ls[0])}:
              print("answer:", sum(RF(x[0], x[1]) for x in ns) + m - Y)
              print(f"we can make remaining fraction {m} with digits {s}, other "
                    f"fractions = {', '.join(str(x) + '/' + str(y) for x, y in ns)}")
              exit(0)
          else:
            # start with lowest denominators
            for d in (ss := sorted(s)):
              # ascending denominators
              if ns and d < ns[-1][1]: continue
              # start with highest numerators
              for n in ss[::-1]:
                if n != d:
                  yield from check(s.difference([n, d]), m - RF(n, d),
                                   k - 1, ns + [(n, d)])
        
        # build a descending list of maxima per Y
        M = sorted([(maxleft(sorted(digits.difference([Y]))) - Y, Y) 
                    for Y in range(1, 10)], reverse=1)
        
        inc = 0
        # keep checking until solution gets too low (-9)
        while True:
          cnt = 0
          # check Y's with the highest maximum first
          for (mx, Y) in M:
            if mx + Y - inc >= -8:
              cnt += 1
              # check if we can make total from eight remaining digits
              for c in check(digits.difference([Y]), mx + Y - inc):
                pass
                
          if not cnt: break     
          inc += 1  
        

        Like

    • GeoffR's avatar

      GeoffR 2:05 pm on 14 July 2023 Permalink | Reply

      I found two integer solutions for the equation for the largest whole number. The largest of the two integer solutions was 12.

      
      % A Solution in MiniZinc
      include "globals.mzn";
      
      var 1..9:Q; var 1..9:R; var 1..9:S; var 1..9:T;
      var 1..9:U; var 1..9:V; var 1..9:W; var 1..9:X;
      var 1..9:Y; 
      
      % Assumed LB for the sum is 1/9 + 2/8 + 3/7 + 4/6 - 5 = -3.54 (i.e  -4)
      % Assumed UB for the sum is 9/1 + 8/2 + 7/3 + 6/4 - 5 = 11.83 (i.e. 12)
      var -4..12:Z;   % Z = Q/R + S/T + U/V + W/X - Y
      
      constraint all_different( [Q, R, S, T, U, V, W, X, Y] );
      
      % Equation in integers only (for Z == Q/R + S/T + U/V + W/X - Y) is:
      constraint Z*R*T*V*X == Q*T*V*X + S*R*V*X + U*R*T*X + W*R*T*V - Y*R*T*V*X;
      
      solve maximize(Z);
      
      output [" Z = " ++ show(Z) ++ "\n" 
      ++ "[Q,R,S,T,U,V,W,X,Y,Z] = " ++ show( [Q,R,S,T,U,V,W,X,Y,Z] ) ];
      
      % Z = 11
      % [Q,R,S,T,U,V,W,X,Y,Z] = [6, 4, 9, 3, 7, 2, 8, 1, 5, 11]
      % ----------
      %  Z = 12
      % [Q,R,S,T,U,V,W,X,Y,Z] = [5, 6, 8, 3, 7, 2, 9, 1, 4, 12]
      % ----------
      % ==========
      
      

      Like

      • Frits's avatar

        Frits 3:10 pm on 14 July 2023 Permalink | Reply

        Highest rational number seems to be 12.95 (9/1 + 8/2 + 7/4 + 6/5 – 3) and
        -7.5381 (1/5 + 2/6 + 3/7 + 4/8 – 9) for the lowest.

        Like

  • Unknown's avatar

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

    Teaser 2629: Random road 

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

    George and Martha moved into Random Road, which has 99 houses along just one side. But instead of being numbered 1 to 99 in the usual way, the man given the job of numbering them gave the first house a number equal to his age and then kept adding his age again for each subsequent house, ignoring the hundreds digits and above. (So, had he been 37 then the numbers would have been 37, 74, 11, 48, 85, 22, etc). Luckily each house got a different number. George’s house number was “correct” so he did not immediately notice. He only saw that something was amiss when Martha pointed out that a house next door had a number with the same two digits as theirs, but in reverse order.

    What is George’s and Martha’s house number? And how old was the numberer?

    [teaser2629]

     
    • Jim Randell's avatar

      Jim Randell 9:01 am on 11 July 2023 Permalink | Reply

      The house numbers in positions k = 1 .. 99 are defined as:

      a[1] = n
      a[k + 1] = (a[k] + n) mod 100

      or:

      a[k] = (k.n) mod 100

      So numberers with the same age mod 100 will give the same sequence. We can suppose that the numberers age lies in the range 16 – 115, so we can identify the ages by the residue mod 100.

      We are looking for a 2-digit house number XY that appears in the “correct” position, i.e.:

      a[XY] = XY

      And has a neighbouring house whose number is YX:

      a[XY ± 1] = YX

      I am assuming that 1-digit numbers are represented with a single digit.

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

      Run: [ @replit ]

      from enigma import (irange, nreverse, printf)
      
      # find solutions for age <n>
      def solve(n):
        # map: position -> house number
        (d, seen) = (dict(), set())
        (p, k) = (1, n)
        while p < 100:
          if k == 0 or k in seen: break
          d[p] = k
          seen.add(k)
          k = (k + n) % 100
          p += 1
        # did we allocate all the houses?
        if p == 100:
          # look for a house with a 2-digit number the same as position
          for (p, k) in d.items():
            if not (k > 9 and k == p): continue
            # look for neighbouring houses with the reverse of the number
            r = nreverse(k)
            if not (r > 9): continue
            if d.get(p - 1, 0) == r: yield (n, (p, k), (p - 1, r))
            if d.get(p + 1, 0) == r: yield (n, (p, k), (p + 1, r))
      
      # consider possible ages (mod 100)
      for n in irange(0, 99):
        for (n, (p0, k0), (p1, k1)) in solve(n):
          printf("age = {n} -> pos {p0} = {k0}, pos {p1} = {k1}")
      

      Solution: George and Martha’s house number is 25. The numberer’s age was 73.

      We have:

      a[25] = (25×73) mod 100 = 1825 mod 100 = 25
      a[24] = (24×73) mod 100 = 1752 mod 100 = 52


      The complete mapping is:

      (0 → 0), 1 → 73, 2 → 46, 3 → 19, 4 → 92, 5 → 65, 6 → 38, 7 → 11, 8 → 84, 9 → 57, 10 → 30,
      11 → 3, 12 → 76, 13 → 49, 14 → 22, 15 → 95, 16 → 68, 17 → 41, 18 → 14, 19 → 87, 20 → 60,
      21 → 33, 22 → 6, 23 → 79, 24 → 52, 25 → 25, 26 → 98, 27 → 71, 28 → 44, 29 → 17, 30 → 90,
      31 → 63, 32 → 36, 33 → 9, 34 → 82, 35 → 55, 36 → 28, 37 → 1, 38 → 74, 39 → 47, 40 → 20,
      41 → 93, 42 → 66, 43 → 39, 44 → 12, 45 → 85, 46 → 58, 47 → 31, 48 → 4, 49 → 77, 50 → 50,
      51 → 23, 52 → 96, 53 → 69, 54 → 42, 55 → 15, 56 → 88, 57 → 61, 58 → 34, 59 → 7, 60 → 80,
      61 → 53, 62 → 26, 63 → 99, 64 → 72, 65 → 45, 66 → 18, 67 → 91, 68 → 64, 69 → 37, 70 → 10,
      71 → 83, 72 → 56, 73 → 29, 74 → 2, 75 → 75, 76 → 48, 77 → 21, 78 → 94, 79 → 67, 80 → 40,
      81 → 13, 82 → 86, 83 → 59, 84 → 32, 85 → 5, 86 → 78, 87 → 51, 88 → 24, 89 → 97, 90 → 70,
      91 → 43, 92 → 16, 93 → 89, 94 → 62, 95 → 35, 96 → 8, 97 → 81, 98 → 54, 99 → 27

      So 25 is in the correct position, and 24 has the number that is the reverse of 25.

      50 and 75 are also in the correct position, but are not neighbours of 5 or 57 (respectively).

      62 is given the number 26, the reverse of its position.

      Like

    • Jim Randell's avatar

      Jim Randell 10:22 am on 11 July 2023 Permalink | Reply

      Without checking that each house is assigned a different number there is only one candidate solution (but we can provide an additional check to ensure the answer is viable).

      This run file executes in 62ms. (Internal runtime of the generated program is 1.3ms).

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      # AB = age of the numberer
      # XY = George and Martha's house number
      
      SubstitutedExpression
      
      --distinct="XY"
      --code="num = lambda n, k: (n * k) % 100"
      
      # G&M's house number is the same as its position
      "num(AB, XY) == XY"
      
      # a neighbouring house is numbered YX
      "YX in { num(AB, XY - 1), num(AB, XY + 1) }"
      
      # answer is: G&M's number, numberers age
      --answer="(XY, AB)"
      
      # check each house is assigned different number
      --reorder=0
      "seq_all_different(num(AB, k) for k in irange(1, 99))"
      

      Like

  • Unknown's avatar

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

    Brain-Teaser 923: That way I lose 

    From The Sunday Times, 30th March 1980 [link]

    My son and I play a good game on my calculator. I put in a whole number and do various operations on the calculator (always chosen to give whole number answers).

    Every now and then I stop. With me looking at the calculator the right way up, and my son looking at it upside-down, we compare what we see. If he sees a number and it is larger than mine, then he wins that round; but if he sees a number and it is smaller than mine, then he loses that round.

    So, for example, if 298 is displayed on the machine, then my son sees 862 and wins. On the other hand, if I display a number using a 3, 4 or 7, then my son does not see a number (and neither of us wins).

    Recently I displayed a three-figure number on the machine, and my son and I compared what we saw:

    “I win” he said.

    Then I subtracted 1 and we looked again.

    “I win” he said.

    Then I halved the number, and again we compared.

    “I win” he said.

    So I then added slightly over 2000 to that number and multiplied the total by 17, and we looked once more.

    I lose!” he said, laughing.

    What number did I initially display on the calculator, and what was the number which I saw finally displayed?

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

    [teaser923]

     
    • Jim Randell's avatar

      Jim Randell 11:29 am on 9 July 2023 Permalink | Reply

      See also: Enigma 212.

      If you’ve looked at Enigma 212 you might expect the exclamation “I lose!” to indicate that the inverted display of the calculator looked like “ILOSE”, i.e. the final number displayed was 35071.

      So it was 2063 before it was multiplied by 17. And some number less than 63 when “slightly over” 2000 was added to it.

      This Python program looks for numbers slightly over 2000 that give a viable sequence that starts with a 3-digit number.

      It runs in 52ms. (Internal runtime is 112µs).

      Run: [ @replit ]

      from enigma import (irange, nsplit, nconcat, catch, div, printf)
      
      # numbers which read as numbers when inverted on a 7-segment display
      inverted = { 0: 0, 1: 1, 2: 2, 5: 5, 6: 9, 8: 8, 9: 6 }
      
      # invert a number
      _invert = lambda n: nconcat(inverted[x] for x in nsplit(n, reverse=1))
      invert = lambda n: catch(_invert, n)
      
      # start from 35071 (~ invert("ILOSE"))
      n4 = 35071
      n4_ = div(n4, 17)  # reverse "multiply by 17"
      assert n4_ is not None
      # look for a value "a little over 2000"
      for x in irange(2001, 2099):
        n3 = n4_ - x  # reverse "add x"
        if n3 < 1: break
        r3 = invert(n3)
        if r3 is None or not (r3 > n3): continue
      
        n2 = n3 * 2  # reverse "divide by 2"
        r2 = invert(n2)
        if r2 is None or not (r2 > n2): continue
      
        n1 = n2 + 1  # reverse "subtract 1"
        r1 = invert(n1)
        if r1 is None or not (r1 > n1) or not (99 < n1 < 1000): continue
      
        # output solution
        printf("{n1} ({r1}) -> {n2} ({r2}) -> {n3} ({r3}) -> {n4} (ILOSE); x={x}")
        break
      

      Solution: The initial number was: 119. And the final number was: 35071.

      So we have:

      start → 119; inverted = 611 (son wins)
      −1 → 118; inverted = 811 (son wins)
      ÷2 → 59; inverted = 65 (son wins)
      +2004; ×17 → 35071; inverted = “ILOSE” (nobody wins)

      Like

  • Unknown's avatar

    Jim Randell 4:28 pm on 7 July 2023 Permalink | Reply
    Tags:   

    Teaser 3172: Light show 

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

    My bedside clock displays the time and date using eight digits; for example at 9:43am on 28th June the display would be:

    Each digit in the electronic display lights up some (or all) of seven light segments, the above display lighting up a total of 45 segments. [Not counting the dots].

    On one occasion recently the displayed digits were all different and the total number of lit segments was prime. The same was true exactly one day later. Then, just one minute after the second occasion, the number of lit segments was the average of the numbers of lit segments on those two previous occasions.

    What was that third display?

    [teaser3172]

     
    • Jim Randell's avatar

      Jim Randell 5:05 pm on 7 July 2023 Permalink | Reply

      I assumed that 7 uses 3 segments, and 6 and 9 use 6 segments each.

      This Python program runs in 69ms. (Internal runtime is 12.7ms).

      Run: [ @replit ]

      from datetime import (date, datetime, timedelta)
      from enigma import (
        irange, nsplit, flatten, repeat, inc, is_prime, nconcat,
        seq_all_different as all_different, printf
      )
      
      # segment count for each digit
      #       0  1  2  3  4  5  6  7  8  9
      segs = [6, 2, 5, 5, 4, 5, 6, 3, 7, 6]
      
      # segment sum
      ssum = lambda ds: sum(segs[d] for d in ds)
      
      # split each number into a pair of digits
      display = lambda *ns: flatten(nsplit(n, 2) for n in ns)
      
      # display pairs for hours and minutes with different digits
      hours = list(display(n) for n in irange(0, 23) if n % 11 != 0)
      mins  = list(display(n) for n in irange(0, 59) if n % 11 != 0)
      
      # date for the first time is between 2023-01-01 and 2023-07-08
      for d1 in repeat(inc(timedelta(days=1)), date(2023, 1, 1)):
        if d1.day == 8 and d1.month == 7: break
        # find the digits for the date
        d1d = display(d1.day, d1.month)
        if not all_different(d1d): continue
        # segment sum
        d1s = ssum(d1d)
        # find hour and minute values, using different digits
        for h1 in hours:
          if not all_different(d1d + h1): continue
          for m1 in mins:
            if not all_different(d1d + h1 + m1): continue
            # calculate the digit sum for the first time
            n1 = d1s + ssum(h1 + m1)
            if not is_prime(n1): continue
      
            # now check the segment sum exactly 1 day later is prime
            t1 = datetime(d1.year, d1.month, d1.day, nconcat(h1), nconcat(m1))
            t2 = t1 + timedelta(days=1)
            n2 = ssum(display(t2.hour, t2.minute, t2.day, t2.month))
            if not is_prime(n2): continue
      
            # and the time 1 minute later gives the mean of the segment sums
            t3 = t2 + timedelta(minutes=1)
            n3 = ssum(display(t3.hour, t3.minute, t3.day, t3.month))
            if n3 * 2 != n1 + n2: continue
      
            # output solution
            printf("{t1} ({n1}) -> {t2} ({n2}) -> {t3} ({n3})")
      

      Solution: The third time was: 17:00 28.04.

      The displays are:

      Time 1: 16:59 27.04 = 4:59pm on 27th April, uses 37 segments
      Time 2: 16:59 28.04 = 4:59pm on 28th April, uses 41 segments
      Time 3: 17:00 28.04 = 5:00pm on 28th April, uses 39 segments

      There is only one viable set of times (per year) even if the choice of dates is unrestricted.

      Like

    • Frits's avatar

      Frits 3:28 pm on 8 July 2023 Permalink | Reply

         
      from enigma import SubstitutedExpression, catch
      from datetime import timedelta, datetime
      
      # number of segments per digit
      seq = [('0', 6), ('1', 2), ('2', 5), ('3', 5), ('4', 4), 
             ('5', 5), ('6', 6), ('7', 3), ('8', 7), ('9', 6)]
      
      # dictionary
      d = dict(seq)
      
      total = lambda s: sum(d[y] for x in s for y in str(x).zfill(2))
      
      # check valid datetime (mm, dd, hh, tt)
      check_dt = lambda s: catch(datetime, 2023, s[0], s[1], s[2], s[3]) is not None
      
      # list external functions
      fns = ("total", "check_dt", "datetime", "timedelta")
      env = eval("dict(" + ','.join([x + "=" + x for x in fns]) + ")")
      
      # invalid digit / symbol assignments
      d2i = dict()
      for dg in range(0, 10):
        vs = set()
        if dg > 2: vs.update('A')
        if dg > 3: vs.update('E')
        if dg > 5: vs.update('C')
        if dg > 6: vs.update('H') # until and including June
        d2i[dg] = vs
      
      #         hh.tt dd.mm
      # display AB.CD EF.GH
      
      # the alphametic puzzle
      p = SubstitutedExpression(
        [
          "AB < 24",                                      # hours
          "EF < 32",                                      # days
          
          "check_dt([GH, EF, AB, CD])",                   # check for valid date
          "(d1 := datetime(2023, GH, EF, AB, CD)) != 1",  # set day 1
          "is_prime(t1 := total((AB, CD, EF, GH)))",      # total must be prime
          
          "(d2 := d1 + timedelta(days=1)) != 1",          # set day 2
          "is_prime(t2 := total((d2.month, d2.day, d2.hour, d2.minute)))",
          
          "(d21 := d2 + timedelta(minutes=1)) != 1",      # set day 2 plus 1 minute
          # t3 is average of t1 and t2
          "2 * total((d21.month, d21.day, d21.hour, d21.minute)) == t1 + t2",
        ],
        answer="(d21.hour, d21.minute, d21.day, d21.month)",
        d2i=d2i,
        s2d=dict(G=0),
        env=env,
        reorder=0,
        verbose=0,    # use 256 to see the generated code
      )
      
      fm = lambda n: str(n).zfill(2)
      # print answers
      for h, t, d, m in p.answers():
        print(f"{fm(h)}.{fm(t)} {fm(d)}.{fm(m)}")
      

      @Jim, is there a better way to set the env variable in a compact way without mentioning the function name twice?

      Like

      • Jim Randell's avatar

        Jim Randell 9:22 am on 9 July 2023 Permalink | Reply

        @Frits: You can always pass the entire local environment:

        env = locals()
        

        Or to include just the necessary values you could use:

        # construct the restriction of dict <d> to keys <ks>
        restrict = lambda d, ks: dict((k, d[k]) for k in ks)
        env = restrict(locals(), fns)
        

        Perhaps I will add [[ restrict() ]] to enigma.py.

        Like

  • Unknown's avatar

    Jim Randell 10:32 am on 6 July 2023 Permalink | Reply
    Tags:   

    Brain-Teaser 900: World Cup goal scorers 

    From The Sunday Times, 5th November 1978 [link]

    The Soccotia World Cup soccer squad consists of two goalkeepers and sufficient outfield players to ensure that each of these latter play in exactly two games, of the three scheduled. Each player is also allocated a consecutive number starting with one, including the goalkeepers. Each outfield player is equally capable of playing in defence and attack, but will not change from one role to another during the same game.

    The manager selects the teams for each match such that the sum of the numbers of the outfield players remains constant for each game, and such that the sum of the numbers of the four defenders equals one half of the sum of the six attackers.

    In each game Soccotia scored one goal, each of which was scored by an attacking player, three different players  scoring one goal each.

    In the first game, the goal resulted from a five man move involving four passes. The move was started by a defender, who was the only defender in the move. Each pass was made to a player whose number was a fixed integer multiple of the passer.

    In the second game, the same four players were chosen to play in defence as in the first game, and the same defender started the five man/four pass move which led to the goal scored in this game. This time however the number of each player receiving the ball exceeded the number of the passer by a fixed integer.

    In the third game the five man/four pass move leading to the goal followed the same pattern as that scored in the second game.

    What were the numbers of the goalscorers in match order?

    News

    There are now 900 Teaser puzzles available on the site, so I thought it appropriate to post Teaser 900.

    [teaser900]

     
    • Jim Randell's avatar

      Jim Randell 10:32 am on 6 July 2023 Permalink | Reply

      In each match the team consists of 11 players (1 goalkeeper, 4 defenders, 6 attackers).

      In the 3 games with 30 slots for outfield (= non-goalkeeper) players, each member of the squad fills 2 slots, so there are 15 outfield members, and 17 members of the squad in total. So they are numbered from 1-17.

      This Python program starts by forming possible geometric and arithmetic sequences from the available numbers, and then selects attackers and defenders for each match based on the specified requirements. Once we have 3 matches we check there are 2 unused numbers for the goalkeepers, and then determine the scorers in each match.

      The program runs in 296ms.

      from enigma import (irange, subsets, repeat, div, multiset, printf)
      
      # numbers 1 .. 17
      ns = set(irange(1, 17))
      
      # form possible geometric and arithmetic sequences
      gseqs = list()
      aseqs = list()
      # consider the first 2 terms
      for (a, b) in subsets(sorted(ns), size=2):
        # geometric?
        n = div(b, a)
        if n:
          ss = tuple(repeat((lambda x: x * n), a, 4))
          if ns.issuperset(ss): gseqs.append(ss)
        # arithmetic?
        n = b - a
        ss = tuple(repeat((lambda x: x + n), a, 4))
        if ns.issuperset(ss): aseqs.append(ss)
      
      # choose a geometric sequence for the first game
      for seq1 in gseqs:
        # the sequence consists of 1d + 4a
        (d1, a1s) = (seq1[0], seq1[1:])
        # choose 2 more attackers to give an even sum (= 2t)
        for atts1 in subsets(ns.difference(seq1), size=2, fn=set):
          atts1.update(a1s)
          t = div(sum(atts1), 2)
          if t is None: continue
          # choose 3 defenders to bring their total to t
          for defs1 in subsets(ns.difference(atts1, {d1}), size=3, fn=set):
            defs1.add(d1)
            if sum(defs1) != t: continue
      
            # in the second game we have the same 4 defenders
            defs2 = defs1
            h2 = sum(defs2)
            # find an arithmetic sequence that starts with d1 ...
            for seq2 in aseqs:
              if seq2[0] != d1: continue
              # and ends in an attacker
              a2 = seq2[-1]
              if a2 == seq1[-1] or a2 in defs2: continue
              # any player in the sequence that is not a defender is an attacker
              a2s = set(seq2).difference(defs2)
              # find more attackers to give a sum of 2 * t
              for atts2 in subsets(ns.difference(defs2, a2s), size=6 - len(a2s), fn=set):
                atts2.update(a2s)
                if sum(atts2) != 2 * t: continue
      
                # find out who has already played twice
                m = multiset.from_seq(defs1, atts1, defs2, atts2)
                p2s = set(k for (k, v) in m.items() if v == 2)
                # find an arithmetic sequence for game 3 that doesn't involve p2s
                for seq3 in aseqs:
                  if not p2s.isdisjoint(seq3): continue
                  # the sequence starts with a defender and ends with an attacker
                  a3 = seq3[-1]
                  if a3 == seq1[-1] or a3 == seq2[-1]: continue
                  d3 = seq3[0]
                  # choose 3 more defenders to bring the sum to t
                  for defs3 in subsets(ns.difference(p2s, {d3, a3}), size=3, fn=set):
                    defs3.add(d3)
                    if sum(defs3) != t: continue
                    # any player in the sequence that is not a defender is an attacker
                    a3s = set(seq3).difference(defs3)
                    # find more attackers to give a sum of 2 * h3
                    for atts3 in subsets(ns.difference(p2s, defs3, a3s), size=6 - len(a3s), fn=set):
                      atts3.update(a3s)
                      if sum(atts3) != 2 * t: continue
      
                      # find the goalkeepers (not involved in any match so far)
                      gs = ns.difference(defs1, atts1, defs2, atts2, defs3, atts3)
                      if len(gs) != 2: continue
                      # find the scorers
                      ss = (seq1[-1], seq2[-1], seq3[-1])
      
                      # output solution
                      fmt = sorted
                      printf("1: defs={defs1} atts={atts1} {seq1}", defs1=fmt(defs1), atts1=fmt(atts1))
                      printf("2: defs={defs2} atts={atts2} {seq2}", defs2=fmt(defs2), atts2=fmt(atts2))
                      printf("3: defs={defs3} atts={atts3} {seq3}", defs3=fmt(defs3), atts3=fmt(atts3))
                      printf("goalies = {gs}", gs=fmt(gs))
                      printf("scorers = {ss}")
                      printf()
      

      Solution: The goalscorers were: 16, 9, 8.


      The squad for the first match was:

      1: defenders = (1, 3, 11, 13); attackers = (2, 4, 8, 12); sequence = 1 → 2 → 4 → 8 → 16

      This is the only possible geometric sequence using the allowed numbers.

      The numbers of the defenders sum to 28, and the numbers of the attackers sum to 56.

      The squad for the second match was:

      2: defenders = (1, 3, 11, 13); attackers = (5, 6, 7, 9, 14, 15); sequence = 1 → 3 → 5 → 7 → 9

      And the squad for the third match was one of the following:

      3: defenders = (2, 4, 6, 16); attackers = (5, 7, 8, 9, 13, 15); sequence = 4 → 5 → 6 → 7 → 8
      3: defenders = (2, 4, 7, 15); attackers = (5, 6, 8, 9, 12, 16); sequence = 4 → 5 → 6 → 7 → 8
      3: defenders = (4, 5, 7, 12); attackers = (2, 6, 8, 9, 15, 16); sequence = 4 → 5 → 6 → 7 → 8

      Each of the outfield player has played in 3 matches, and the remaining members of the squad, 10 and 17, are the goalkeepers.

      The goal scorers are the final players in the three sequences: 16, 9, 8.

      Like

    • Frits's avatar

      Frits 8:22 pm on 6 July 2023 Permalink | Reply

      from itertools import combinations
      
      N = 17
      
      # return all arithmetic sequences in sorted seq, len=5 [x, k + x, ... , 4k + x]
      def ametric(seq): 
        for x in seq[:-4]:
          for k in range(1, 5):
            if 4 * k + x > seq[-1]: break 
            if all(x + i * k in seq for i in range(5)):
              yield (x, 4 * k + x)                             # yield first and last
              
      # decompose <t> into <n> different numbers, min/max, excluding <xs> elements 
      def decompose(t, n, xs, m=1, M=17, s=[]):
        if n == 1:
          if m <= t <= M and t not in xs:
            yield s + [t]
        else:
          # still n - 1 numbers to follow with minimal sum m+1 + m+2 + .. + m+n-1
          # or (n - 1)m + n(n-1)/2  
          for x in range(m, min(M + 1, t - (n - 1) * m + n * (n - 1 ) // 2 + 1)):
            if x not in xs:
              yield from decompose(t - x, n - 1, xs, x + 1, M, s + [x])
              
         
      # round 1: defs = [fd, x, y, z], atts = {k*fd, k**2fd, k**3fd, k**4fd, a, b}
      # geometric sequences
      for fd, geo in [(fd, {fd * m**i for i in range(5)}) 
                      for fd in range(1, int(N**.25) + 1) 
                      for m in range(2, int((N / fd)**.25) + 1)]: 
        s1 = max(geo)                                             # scorer in round 1
        # choose 2 remaining attackers in round 1           
        for a, b in combinations(set(range(1, N + 1)).difference(geo), 2):
          if (a + b) % 2: continue                                    # a + b is even
          atts_1 = geo.difference([fd]) | {a, b}
          sum_atts = sum(atts_1)
          # can we make defender sum (minus fd) from choosing 3 from leftovers?
          for d in decompose(sum_atts // 2 - fd, 3, {fd} | atts_1):
            d = {fd} | set(d)
            sumd = sum(d)
            
            # make an arithmetic sequence [fd, k + fd, ... , 4k + fd]
            for k in range(1, 5):
              # different scorer must be an attacker
              if (s2 := 4 * k + fd) == s1 or s2 in d: continue 
              # already known attackers in round 2  
              atts_2_1 = set(fd + i * k for i in range(5)).difference(d)
              # only one attacker played in both round 1 and round 2
              # so exactly 10 outfield players remain for round 3
              if len(both := atts_1 & atts_2_1) > 1: continue
      
              # choose remaining attackers in round 2
              for d2 in decompose(sum_atts - sum(atts_2_1), 6 - len(atts_2_1), 
                                  d | atts_2_1 | (atts_1 if both else set())):
                atts_2 = atts_2_1 | set(d2)
                # only one attacker played in both round 1 and round 2
                if len(p11 := atts_1 | atts_2) != 11: continue
            
                # pick players who played only one game in round 1 and 2
                p3 = sorted(p for p in p11 if p not in atts_1 or p not in atts_2)
                # sum of the numbers of the outfield players remains constant 
                if sum(p3) != 3 * sumd: continue
      
                # build list of possible defenders
                defss = list(decompose(sumd, 4, set(range(1, N + 1)).difference(p3)))
               
                # can we make an arithmetic sequence [x, k + x, ... , 4k + x]?
                for (f, s3) in ametric(p3):
                  # three different players scoring one goal each
                  if s3 in {s1, s2}: continue
                  # the sequence starts with a defender and ends with an attacker
                  if any(s3 not in defs and f in defs for defs in defss): 
                    print(f"answer: {s1}, {s2} and {s3}")    
      

      Like

  • Unknown's avatar

    Jim Randell 10:43 am on 4 July 2023 Permalink | Reply
    Tags:   

    Teaser 2625: Mind the gap 

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

    If you list in increasing order all ten-figure numbers with no repeat digit, then the first is 1023456789 and the last is 9876543210. The differences between consecutive numbers in the list vary wildly but no difference is ever equal to the average of all the differences between the consecutive numbers.

    What difference between consecutive numbers comes closest to the average?

    [teaser2625]

     
    • Jim Randell's avatar

      Jim Randell 10:44 am on 4 July 2023 Permalink | Reply

      The way [[ subsets(..., select='P') ]] works the numbers will be produced in numerical order, so we don’t need to order them.

      The distances between consecutive numbers are recorded in a [[ multiset() ]], so we don’t have to bother processing duplicate values.

      But there are a lot of numbers to process, so the program is quite slow.

      This Python program runs in 871ms.

      Run: [ @replit ]

      from enigma import (irange, subsets, nconcat, multiset, tuples, rdiv, Accumulator, printf)
      
      # construct pandigitals in numerical order
      def generate():
        for ds in subsets(irange(0, 9), size=10, select='P'):
          if ds[0] != 0:
            yield nconcat(ds)
      
      # record the collection of differences between consecutive pandigitals
      ds = multiset.from_seq(b - a for (a, b) in tuples(generate(), 2))
      # calculate the mean difference
      m = ds.avg(div=rdiv)
      printf("mean difference = {m}", m=float(m))
      
      # find the closest difference
      r = Accumulator(fn=min, collect=1)
      for d in ds.keys():
        r.accumulate_data(abs(m - d), d)
      # output solution(s)
      for d in r.data:
        printf("closest difference = {d} [{n} times]", n=ds[d])
      

      Solution: The difference that comes closest to the mean is 2727.

      Which occurs 1872 times, for example in the following pairs:

      (1034679852, 1034682579)

      (9865317420, 9865320147)

      There are 500 different difference values.

      The smallest difference is 9 (which happens 322560 times):

      (1023456789, 1023456798)

      (9876543201, 9876543210)

      And the largest difference is 104691357 (which happens 2 times):

      (1098765432, 1203456789)
      (8796543210, 8901234567)


      With some analysis we can get a faster program.

      We can calculate the mean difference fairly easily:

      In an ordered sequence (a, b, c, …, x, y, z) the sequence of consecutive distances is:

      (b − a, c − b, …, y − x, z − y)

      and this has one fewer element than the original sequence.

      And the sum of this sequence is simply:

      sum = z − a

      i.e. the difference between the largest pandigital and the smallest pandigital.

      So, if we knew the number of pandigitals in the original sequence we can calculate the mean.

      There are factorial(10) possible digit sequences, but factorial(9) of them will have a leading zero, so are rejected.

      Hence, there are 3265920 pandigitals (and the number of distances is 1 less than this).

      And so the mean distance is:

      mean distance = (9876543210 − 1023456789) / 3265919 = 2710.7489…

      So we see that no individual difference is equal to the mean because the mean is not an integer and all individual differences are. (And in fact they all must be multiples of 9).

      Now, if we consider two consecutive pandigitals that are at a distance apart that is close to the mean, then they are going to look something like:

      abcde|vwxyz
      abcde|xzyvw

      Where there is some shared prefix, and then the remaining digits appear in a different arrangement for the two numbers. And when calculating the difference between them the common prefix disappears.

      So the difference for the numbers in the example is then:

      xzyvw − vwxyz

      And we can calculate the differences by just looking at sets of numbers that are not part of the common prefix, and calculating the closest difference using those set.

      We are looking for a difference close to 2711, so we can examine sets of digits that have 5 elements.

      This Python program runs in 123ms.

      Run: [ @replit ]

      from enigma import (
        factorial, rdiv, ndigits, Accumulator,
        irange, subsets, nconcat, tuples, printf
      )
      
      # calculate the mean distance
      m = rdiv(9876543210 - 1023456789, 9 * factorial(9) - 1)
      printf("mean difference = {m}", m=float(m))
      
      # record the closest difference to the mean
      r = Accumulator(fn=min)
      
      # consider possible sets of digits
      k = ndigits(int(m)) + 1
      for ds in subsets(irange(0, 9), size=k):
        # find consecutive numbers using this set of digits
        ns = subsets(ds, size=len, select='P', fn=nconcat)
        # find differences between consecutive numbers closest to the mean
        for (a, b) in tuples(ns, 2):
          d = b - a
          r.accumulate_data(abs(m - d), d)
      
      # output solution
      printf("closest difference = {r.data}")
      

      Like

    • Frits's avatar

      Frits 5:22 pm on 4 July 2023 Permalink | Reply

        
      from enigma import factorial
      from itertools import permutations, combinations
      
      # calculate the mean distance
      m = (9876543210 - 1023456789) / (factorial(10) - factorial(9) - 1)
      print(f"mean difference = {m}")
      
      mn, md = 9999999, 9999999
      
      # abcde with c > d > e 
      # choose last three descending digits
      for c in combinations('9876543210', 3):
        # choose first 2 digits
        for p in permutations(set('0123456789').difference(c), 2):
          s = ''.join(p + c)
      
          # jump within same 10000:  like 32510 to 35012
          if s[1] < '7':
            # jump 3 higher for a difference of 2xxx
            s2 = str(int(s[1]) + 3)
            if s2 not in s[1:]: continue
            new = int(s[0] + s2 + ''.join(sorted(set(s[1:]).difference(s2))))
          else:  
            ints = [int(x) for x in s]
            # we must be able to jump to a higher 10000
            a1 = str(ints[0] + 1)
            if s[0] == '9' or a1 not in s: continue
            # digit 3 higher then 2nd digit must be present
            if str(ints[1] - 7) not in s: continue
            # digits higher than 2nd digit may not occur in last three positions
            if any(str(j) in s[2:] for j in range(ints[1] + 1, 10)): continue
            
            # jump to higher 10000:   like 17420 to 20147
            new = int(a1 + ''.join(sorted(set(s).difference(a1))))
        
          i = int(s)
          # check for closeness to target
          if abs(m - new + i) <= mn:
            mn, md = abs(m - new + i), new - i
      
      print("answer:", md)
      

      Like

      • Frits's avatar

        Frits 1:27 pm on 5 July 2023 Permalink | Reply

        With the abcde|vwxyz method we might miss other combinations that can come relatively close to 2710.

        like 2345698710, 2345701689 with difference 2979

        Like

        • Jim Randell's avatar

          Jim Randell 1:37 pm on 5 July 2023 Permalink | Reply

          We can reduce the size of the prefix considered at line 14:

          k = ndigits(int(m)) + 2
          

          Although it turns out the common prefix is size 5 in all 1872 cases, so we are OK.

          Originally I allowed 1 digit more than the length of the mean for the suffix, but this does assume that the closest value is going to be reasonably close to the mean value.

          Like

  • Unknown's avatar

    Jim Randell 9:17 am on 2 July 2023 Permalink | Reply
    Tags:   

    Brain-Teaser 438: Electric clock 

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

    My watch keeps perfect time and so does my electric wall clock until the battery starts to run down. I check the clock every noon, and when I find that it has lost a minute in 24 hours I know that it will slow down by an additional minute each day until the battery is exhausted.

    At noon on a day towards the end of August, when I was preparing for a fortnight’s holiday, I noticed that the clock was one minute slow but. I forgot to do anything about it. As I hurried off to catch my train, at noon on 1st September, I saw that the clock was exactly 21 minutes slow. I put it right and left it at that.

    Family matters forced me to return earlier than I had expected and on my arrival home I found that the clock was still going. I then noticed that the hands of my watch were exactly overlapping, while the hands of the clock were almost so.

    It took me only a minute to fix a new battery, and I at once put the clock on the correct time. In doing so the hands crossed each other three times.

    On what date did I return from my holiday?

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

    [teaser438]

     
    • Jim Randell's avatar

      Jim Randell 9:18 am on 2 July 2023 Permalink | Reply

      The clock is 21 minutes slow on 1st Sep, and as 21 is the 6th triangular number it must be progressing as follows (minutes slow at noon on the specified date):

      27th Aug: 1 min (+1)
      28th Aug: 3 min (+2)
      29th Aug: 6 min (+3)
      30th Aug: 10 min (+4)
      31st Aug: 15 min (+5)
      1st Sep: 21 min (+6), corrected to 0 min
      2nd Sep: 7 min (+7)
      3rd Sep: 15 min (+8)
      4th Sep: 24 min (+9)
      5th Sep: 34 min (+10)
      6th Sep: 45 min (+11)
      7th Sep: 57 min (+12)
      8th Sep: 70 min (+13)
      9th Sep: 85 min (+14)
      10th Sep: 99 min (+15)
      11th Sep: 115 min (+16)
      12th Sep: 132 min (+17)
      13th Sep: 150 min (+18)
      14th Sep: 169 min (+19), expected return

      A 12 hour clock has overlapping hands at 12:00, and then every 12/11 hours later.

      gap = 12/11 hours = 65m27.27s

      So we can check times that are multiples of this gap (up to 14 days), and look for times when the hands on the wall clock are separated by a small angle, and it would require 3 crossings of the hands to set to clock to the correct time.

      As the clock is set to a time just after the hands have crossed the amount the clock has lost must be greater than 131 minutes. So the earliest possible date of return is 12th Sep. And a return on 14th Sep wouldn’t be an early return, so the answer must be 12th Sep or 13th Sep.

      This Python program starts by constructing a continuous function to give the amount the clock is slow at any particular time, and it does this by using polynomial interpolation of the times given above at noon on the 1st to 14th Sep. (And as we might expect, that function turns out to be a quadratic polynomial).

      It then looks at times when the hands of a correct clock are overlapping, and calculates the angular distance between the hands on the slow clock. If the hand are less than 10° apart, and setting the clock right requires crossing the hands exactly 3 times, then we have found a solution.

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

      Run: [ @replit ]

      from enigma import (Rational, Polynomial, irange, mdivmod, divc, sprintf, printf)
      
      Q = Rational()
      
      # after h hours, loss (in minutes) is t
      h = t = 0
      ps = [(h, t)]
      for i in irange(7, 19):
        h += 24
        t += i
        # remove (<time>, <loss>) (both in hours)
        ps.append((h, Q(t, 60)))
      
      # p: hour -> hours lost
      p = Polynomial.interpolate(ps)
      printf("[p(x) = {p}]")
      
      # calculate hand positions from time in hours (between 0 and 1)
      def pos(t):
        (z, h, m) = mdivmod(t, 12, 1)
        return (Q(h + m, 12), m)
      
      # angular distance between hand positions (between 0 and 1)
      def ang(t):
        (h, m) = pos(t)
        d = abs(h - m)
        return min(d, 1.0 - d)
      
      # format a time (in hours)
      def fmt(t):
        (d, h, m) = mdivmod(t, 24, 1)
        return sprintf("{d:d}+{h:02d}:{m:05.2f}", m=float(m * 60))
      
      # gap between times of successive overlapping hands
      g = Q(12, 11)
      
      # continue forward from noon on 1st Sep in 12/11 hour increments
      for i in irange(1, 11 * 28):
        # actual elapsed time (hours)
        h = i * g
        # time lost (hours)
        x = p(h)
        # calculate the number of crossings in setting the clock to the correct time
        k = divc(x, g)
        if k != 3: continue
        # clock elapsed time (hours)
        t = h - x
        # calculate the angular distance between the hands
        d = ang(t)
        # look for distances less than 10 degrees
        if not (0 < 360 * d < 10): continue
        # output solution
        printf("[i={i}] {h} -> {t}; {d:.2f} deg; {k} crossings", h=fmt(h), t=fmt(t), d=float(360 * d))
      

      Solution: You returned home on 12th September.

      The polynomial that gives the time lost (in hours) from the time of departure (noon on 1st Sep) is:

      p(x) = (1/69120)x^2 + (13/2880)x

      If the setter returned home at noon on 12th Sep, then the clock would be 132 minutes slow, so it would be showing 9:48am. And the hands are 6° apart, and the minute hand is slightly behind the hour hand.

      The battery is replaced and the clock is set to 12:01pm, which involves the hands crossing at: just before 10, just before 11, 12. So the hands cross 3 times.

      And this is the solution given in the book.

      However the program finds a “better” solution.

      If the setter were to return at 10:54am on 12th Sep (i.e. the previous time the hands on the watch coincide), then the clock would be displaying 8:43am, and the hands are just 1.6° apart, with the minute hand slightly behind the hour hand.

      The battery is replaced and the clock set to 10:55am. So the hands cross at: just before 9, just before 10, just before 11. So the hands cross 3 times, and they started out much closer together than the given solution.

      I also think that the wording of the puzzle implies that setter returned home at an overlapping time other than noon.

      However it doesn’t change the date that the setter returned, so the answer is the same in both cases.

      Like

  • Unknown's avatar

    Jim Randell 3:37 pm on 30 June 2023 Permalink | Reply
    Tags:   

    Teaser 3171: What’s happened to our chocolate bar? 

    From The Sunday Times, 2nd July 2023 [link] [link]

    Father Christmas was rewarding his helpers (sprites, elves and fairies). He had 333 helpers, of whom 111 were elves.

    He had 28 chocolate bars to give away in proportion to the size of the groups. If the groups’ shares were 9.5, 9.3 and 9.2 then they would actually receive 10, 9 and 9, because the bars can’t be split and the extra bar goes to the group with the highest remainder (for two extra bars, one each would go to the two groups with the highest remainders). In fact the sprites, elves and fairies received 7, 9 and 12 bars respectively.

    “I’ve found another chocolate bar”, said Father Christmas, “so now the sprites, elves and fairies will receive 6, 10 and 13 bars respectively.”

    “What’s happened to our chocolate bar?” asked the sprites.

    How many sprites are there?

    [teaser3171]

     
    • Jim Randell's avatar

      Jim Randell 3:58 pm on 30 June 2023 Permalink | Reply

      What better time to run a Christmas themed puzzle?

      See also: this Matt Parker video [@youtube]

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

      from enigma import (decompose, item, first, printf)
      
      # distribute <k> bars among <ns>
      def distribute(k, ns):
        t = sum(ns)
        ds = list()  # allocation
        rs = list()  # (index, remainder)
        # initial allocation
        for (i, n) in enumerate(ns):
          (d, r) = divmod(n * k, t)
          ds.append(d)
          rs.append((i, r))
        # distribute remaining bars
        k -= sum(ds)
        for (i, r) in first(sorted(rs, key=item(1), reverse=1), k):
          ds[i] += 1
        return ds
      
      # total number of helpers
      T = 333
      # number of elves
      E = 111
      # choose the number of sprites and fairies
      for (S, F) in decompose(T - E, 2, increasing=1, sep=2, min_v=1):
      
        # calculate the distributions of 28 and 29 bars
        if distribute(28, [S, E, F]) != [7, 9, 12]: continue
        if distribute(29, [S, E, F]) != [6, 10, 13]: continue
      
        # output solution
        printf("S={S} E={E} F={F}")
      

      Solution: There are 76 sprites.

      There are 76 sprites, 111 elves, 146 fairies (= 333 helpers in total).

      With 28 chocolate bars the distribution is:

      S → 6.39 bars
      E → 9.33 bars
      F → 12.27 bars

      After allocating the 6, 9, 12 bars there is 1 bar left over, which goes to the sprites, as they have the largest remainder, giving a final allocation of 7, 9, 12 bars (28 in total).

      With 29 the distribution is:

      S → 6.62 bars
      E → 9.67 bars
      F → 12.71 bars

      After allocating the 6, 9, 12 bars there are 2 bars left over, which go to the fairies and the elves, as they have the largest remainders, giving a final allocation of 6, 10, 13.


      We can look at the “fairness” of the allocations by considering the amount of chocolate per helper.

      If each bar weighs 100g, then in the first case there is 2800g of chocolate, and 333 helpers, so a completely fair allocation would be 8.41g per helper.

      The actual allocations are:

      S → 7 bars = 9.21g / sprite (= avg + 0.80g)
      E → 9 bars = 8.11g / elf (= avg − 0.30g)
      F → 12 bars = 8.22g / fairy (= avg − 0.19g)

      So the sprites get an above average share this allocation, and the elves and fairies get below average.

      In the second allocation there is 2900g of chocolate, giving a fair allocation of 8.71g per helper.

      And the actual allocations are:

      S → 6 bars = 7.89g / sprite (= avg − 0.82g)
      E → 10 bars = 9.01g / elf (= avg + 0.30g)
      F → 13 bars = 8.90g / fairy (= avg + 0.19g)

      In this allocation the sprites get a below average share, and the elves and fairies get above average.


      Manually:

      Adding 1 bar to the total causes S’s proportion of the chocolate to go down and E’s and F’s proportions to go up. So in the first case there was 1 remaining bar which went to S, and in the second case there were 2 remaining bars which went to E and F.

      In the first case the sum of the remainders come to 1 bar, so the largest remainder (S’s) must be more than 1/3:

      (S/333)×28 > 6 + 1/3
      S > (19/3)×(333/28) ≈ 75.32
      S ≥ 76

      In the second case the sum of the remainders come to 2 bars, so the smallest remainder (S’s) must be less than 2/3:

      (S/333)×29 < 6 + 2/3
      S < (20/3)×(333/29) ≈ 76.55

      And the only value in these two ranges is: S = 76.

      Like

      • Jim Randell's avatar

        Jim Randell 11:21 am on 1 July 2023 Permalink | Reply

        Here is a different approach that calculates bounds on the population sizes of the different groups using the distributions of chocolate bars given in the puzzle text. It then tries population sizes within the calculated bounds to see which give the required distributions.

        It also has an internal runtime of <1ms.

        from enigma import (irange, inf, divc, divf, cproduct, item, first, printf)
        
        # distribute <k> bars among <ns>
        def distribute(k, ns):
          t = sum(ns)
          ds = list()  # allocation
          rs = list()  # (index, remainder)
          # initial allocation
          for (i, n) in enumerate(ns):
            (d, r) = divmod(n * k, t)
            ds.append(d)
            rs.append((i, r))
          # distribute remaining bars
          k -= sum(ds)
          for (i, r) in first(sorted(rs, key=item(1), reverse=1), k):
            ds[i] += 1
          return ds
        
        # find divisions of population <t> that give distributions <dss>
        def solve(t, dss):
          # calculate bounds for each group
          (mn, mx) = (dict(), dict())
          for ds in dss:
            k = sum(ds)
            for (i, d) in enumerate(ds):
              # if this distribution was rounded up
              x = divc((d - 1) * t, k)
              if x > mn.get(i, 0): mn[i] = x
              # if this distribution was rounded down
              x = divf((d + 1) * t - 1, k)
              if x < mx.get(i, inf): mx[i] = x
          # choose populations
          for ns in cproduct(irange(mn[i], mx[i]) for i in sorted(mn.keys())):
            if sum(ns) != t: continue
            # check distributions
            if all(distribute(sum(ds), ns) == ds for ds in dss):
              yield ns
        
        # solve for the specified distributions
        for (S, E, F) in solve(333, [[7, 9, 12], [6, 10, 13]]):
          if E != 111: continue
          printf("S={S} E={E} F={F}")
        

        It turns out that the condition that E = 111 is not needed, as there is only one set of population sizes that gives the required distributions with the given total population size. (i.e. line 41 is optional).

        Although knowing E = 111 allows a straightforward manual solution.

        Like

        • Frits's avatar

          Frits 1:43 pm on 1 July 2023 Permalink | Reply

          Without using the mn and mx boundaries and E = 111 this program runs in 100ms.

          I wonder if manual solvers would have an elegant way of solving if E = 111 was omitted.

             
          from enigma import SubstitutedExpression
          
          # calculate distribution with population numbers <s>
          def dist(n, s, t):
            guaranteed = [(n * x) // t for x in s]
            n_leftovers = n - sum(guaranteed)
          
            remainders = sorted([((n * x) % t, i) for i, x in enumerate(s)])
          
            # add leftovers to the people with the highest remainders
            for i in range(n_leftovers):
              guaranteed[remainders[2 - i][1]] += 1
            return guaranteed
          
          # the alphametic puzzle
          p = SubstitutedExpression(
            [ "(total := 333)",
              "(sprites := ABC) < (elves := DEF)",
              "total - sprites - DEF = GHI",
              "elves < (fairies := GHI)",
              "dist(28, [sprites, elves, fairies], total) == [7, 9, 12]",
              "dist(29, [sprites, elves, fairies], total) == [6, 10, 13]",
            ],
            answer="sprites",
            d2i=dict([(k, "ADG") for k in range(4, 10)]),
            distinct="",
            env=dict(dist=dist),
            reorder=0,
            verbose=0,    # use 256 to see the generated code
          )
          
          # print answers
          for ans in p.answers():
            print(f"answer: {ans}")
          

          @Jim, notice I had to postpone the assignment of fairies and that in the calculation of GHI I could use variables sprites or elves but not both of them.

          Like

          • Frits's avatar

            Frits 2:54 pm on 1 July 2023 Permalink | Reply

            or without GHI

               
            # the alphametic puzzle
            p = SubstitutedExpression(
              [ "total := 333",
                "(sprites := ABC) < (elves := DEF)",
                "fairies := total - sprites - elves",
                "elves < fairies",
                "dist(28, [sprites, elves, fairies], total) == [7, 9, 12]",
                "dist(29, [sprites, elves, fairies], total) == [6, 10, 13]",
              ],
              answer="sprites, elves, fairies",
              d2i=dict([(k, "AD") for k in range(2, 10)]),
              distinct="",
              env=dict(dist=dist),
              reorder=0,
              verbose=0,    # use 256 to see the generated code
            )
            

            Like

          • Tony Brooke-Taylor's avatar

            Tony Brooke-Taylor 11:25 am on 2 July 2023 Permalink | Reply

            I think it is possible to infer bounds for the number of sprites without knowing 111, simply by recognising that with 28 bars, the sprites have the largest remainder (implying > 1/3) while with 29 bars, the sprites have the smallest remainder (implying < 2/3). Only 1 integer value for the number of sprites fits between these bounds.

            Like

    • Frits's avatar

      Frits 6:51 pm on 30 June 2023 Permalink | Reply

      Not many iterations.

       
      T, E, N = 333, 111, 28
      RS = [[7, 9, 12], [6, 10, 13]]
      
      # in 1st round the sprites win against the elves 
      # N.S / T > RS[1][0] + 1/3
      # in 2nd round the sprites lose against the elves 
      # (N + 1).S / T < RS[1][0] + 2/3
      
      for s in range((RS[1][0] * T + T // 3) // N + 1, 
                     (RS[1][0] * T + 2 * T // 3) // (N + 1) + 1):
        f = T - E - s
       
        # play 2 rounds
        for rnd in range(2):
          n = N + rnd
          guaranteed = [(n * x) // T for x in (s, E, f)]
          n_leftovers = n - sum(guaranteed)
      
          remainders = sorted([((n * x) % T, i) for i, x in enumerate([s, E, f])])
          
          # add leftovers to the people with the highest remainders
          for i in range(n_leftovers):
            guaranteed[remainders[2 - i][1]] += 1
          # check with actual distribution  
          if guaranteed != RS[rnd]: break
        else:  # no break  
          print("answer:", s)  
      

      Like

  • Unknown's avatar

    Jim Randell 10:37 am on 29 June 2023 Permalink | Reply
    Tags:   

    Brainteaser 1093: Bumper’s final ball 

    From The Sunday Times, 17th July 1983 [link]

    It is a lovely evening in August, and the final Test of the 1990 series has reached its climax. The rubber stands at two games each, the last ball of the last over of the match is now to come. Australia are batting and need six runs for victory. Whacker, their wicket-keeper, the last man in, has taken his stand. with his captain’s words ringing in his ears, “Six or bust!”.

    Bumper, the England bowler, has just taken the previous two wickets in succession, With a dim opinion of Whacker’s batting, he feels sure that a fast straight one will not only give England the Ashes, but will give him his hat-trick and his seventh wicket of the match.

    In a breathless hush he delivers a  fast straight one. Striding forward like a Titan, Whacker mows …

    The records of this match are scanty. The Australian total in two innings was 490. Except for one man run out in the first innings, their casualties fell to bowlers. The five England bowlers had averages of 14, 20, 25 (Bumper), 33, 43, each with a differing number of wickets.

    How many wickets did each take and who won the Ashes?

    This puzzle was originally published as Brain-Teaser 15 on 11th June 1961 (although when originally published it was credited to R. Skeffington Quinn), and was re-published as Brainteaser 1093 to celebrate Mr Skeffington Quin’s 100th birthday.

    [teaser1093]

     
    • Jim Randell's avatar

      Jim Randell 10:38 am on 29 June 2023 Permalink | Reply

      See my solution at Teaser 15.


      In the same issue an interview with Mr Skeffington Quin was published. [link]

      The article notes that Mr Skeffington Quin’s “very first” puzzle (Brain-Teaser 15, 11th June 1961) is being re-published (as Brainteaser 1093) in celebration of his 100th birthday.

      However in 1960, the year before Brain-Teaser became a weekly feature (and before the puzzles were numbered), he had contributed a puzzle on 31st July 1960 (Brain-Teaser: Silver collection), which was also included in the book Sunday Times Brain Teasers (1974).

      The article also mentions a puzzle set on 5th November 1972 as his most “impenetrable” puzzle, and that only two correct answers were received.

      However when I searched for this puzzle in The Sunday Times archive, I found a note saying that The Sunday Times was not published on that date due to industrial action. (However the accompanying magazine, which was prepared in advance, is present in the archive, but the puzzle does not appear in it).

      However there is a gap in the puzzle numbers at that point (the missing puzzle is Brain-Teaser 591), and a solution was given with Brain-Teaser 593, where it is noted that only 2 (out of 50) entrants got the correct answer, suggesting the puzzle was published.

      So it is indeed something of a puzzle.

      Like

    • GeoffR's avatar

      GeoffR 6:52 pm on 29 June 2023 Permalink | Reply

      
      % A Solution in MiniZinc
      include "globals.mzn";
       
      % Number of wickets taken by each bowler is different
      var 1..9:w1; var 1..9:w2; var 1..9:w3;  
      var 1..9:w4; var 1..9:w5; 
      constraint all_different([w1, w2, w3, w4, w5]);
      
      % Average runs per wicket for each bowler
      int: a1 == 14;
      int: a2 == 20;
      int: a3 == 25;  % Bumper's average runs per wicket 
      int: a4 == 33;
      int: a5 == 43;
      
      % total runs for each bowler ( say, UB half total runs scored)
      var 1..245:t1; var 1..245:t2; var 1..245:t3; var 1..245:t4; var 1..245:t5;
      
      % The Australian total in two innings was 490
      constraint  t1 + t2 + t3 + t4 + t5 == 490;
      
      constraint a1 * w1 == t1 /\ a2 * w2 == t2 /\ a3 * w3 == t3 /\ 
      a4 * w4 == t4 /\ a5 * w5 == t5;
      
      % Bumper (w3) takes 6 or 7 wickets (Jim's analysis)
      % .. and total wickets are 18 or 19
      constraint ((w1 + w2 + w3 + w4 + w5 == 18) /\ w3 == 6)
      \/  ((w1 + w2 + w3 + w4 + w5 == 19) /\ w3 == 7);
      
      solve satisfy; 
      
      output [ "[w1, w2, w3, w4, w5 ] = " ++ show( [w1, w2, w3, w4, w5 ]) ++
      "\n" ++ "[t1, t2, t3, t4, t5 ] = " ++ show( [t1, t2, t3, t4, t5 ]) ++
      "\n" ++ "[a1, a2, a3, a4, a5 ] = " ++ show( [a1, a2, a3, a4, a5 ])];
      
      % [w1, w2, w3, w4, w5 ] = [5, 2, 7, 1, 4]
      % [t1, t2, t3, t4, t5 ] = [70, 40, 175, 33, 172]
      % [a1, a2, a3, a4, a5 ] = [14, 20, 25, 33, 43]
      % ----------
      % ==========
      % so Bumper takes 7 wickets and England wins the series.
      
      % Not sure of relevance of condition below
      % Bumper, the England bowler, has just taken the previous two wickets in succession,
      
      

      Like

    • Pete Good's avatar

      Pete Good 1:23 pm on 19 September 2023 Permalink | Reply

      Jim,

      I may be able to shed some light on the missing Skeffington-Quin teaser. I first started doing Brain Teasers when I was at school and one puzzle that I particularly remember was “Here Comes the Bride” by this author. I remember it because the Sunday Times printed a note in the magazine a few weeks later stating that there were only 2 correct entries out of 50. I kicked myself at the time for not having sent in a postcard because I had obtained the correct solution!

      I never kept copies of those early newspapers so I was delighted when I recently obtained a second-hand copy of Ronnie Postill’s 1974 book of Brain Teasers and had the pleasure of solving it again. I thought it was originally published in 1968 or 1969, not in 1972 as stated in the article you read. If you have access to the Sunday Times archives then you may find it in an earlier year.

      Regards

      Pete

      Like

      • Jim Randell's avatar

        Jim Randell 6:43 pm on 19 September 2023 Permalink | Reply

        @Pete: Thanks for the info.

        I also have a copy of the 1974 book, and I have gradually been adding puzzles from it to the site – [teaser-book-1974]. There are currently 44 (of 101) puzzles posted from that book, but I haven’t yet got to the one you mention, which is Teaser 429 (27th July 1969). When the answer was published (10th August 1969) it was noted that there were 21 correct entries.

        I searched the Sunday Times Digital Archive for puzzles involving Ara and Chne, and found these:

        Brain-Teaser 250: Spider’s Wedding March / 13th February 1966
        Brain-Teaser 346: Antics / 24th December 1967 (also in the 1974 book)
        Brain-Teaser 429: Here Comes the Bride / 27th July 1969 (also in the 1974 book)
        Brain-Teaser 843: Stratagem / 11th September 1977

        but it didn’t find the missing puzzle, Brain-Teaser 591 (5th November 1972), so the search continues.

        Like

  • Unknown's avatar

    Jim Randell 9:20 am on 27 June 2023 Permalink | Reply
    Tags: ,   

    Teaser 2530: [A day at the races] 

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

    At the St Patrick’s Day races, Pat backed losers in each of the first three. Starting with £100, he bet a whole-number percentage of his money in the first race and a whole-number percentage of the remainder (but not a whole number of pounds) in the second. In the third, he bet a whole-number percentage of what was left, leaving a whole-number percentage of his £100. This final percentage was the sum of the three percentages he had bet and lost.

    How much was lost on the third race?

    This puzzle was originally published with no title.

    [teaser2530]

     
    • Jim Randell's avatar

      Jim Randell 9:21 am on 27 June 2023 Permalink | Reply

      If the percentages bet are p1, p2, p3, and the remaining amounts after each bet it lost are r1, r2, r3, then:

      p1 + p2 + p3 = r3
      r1 = 100 − p1
      r2 = r1 − p2 . r1 / 100 = r1(100 − p2)/100 [and r2 is not a whole number]
      r3 = r2 − p3 . r2 / 100 = r2(100 − p3)/100

      So we can choose a value for r3, and split that into the three percentages (p1, p2, p3) and check the bets turn out as required.

      And this approach does find the answer in a reasonable time, but it is not hugely fast.

      This Python program runs in 347ms. (Internal runtime is 237ms).

      Run: [ @replit ]

      from enigma import (irange, decompose, printf)
      
      # generate possible (p1, p2, p3) percentage values
      def generate():
        # consider the number of pounds remaining after the 3 bets
        for r in irange(3, 98):
          # this is equal to p1 + p2 + p3
          yield from decompose(r, 3, increasing=0, sep=0, min_v=1)
      
      # consider values for p1, p2, p3
      for (p1, p2, p3) in generate():
        # bet 1, we lose p1 pounds
        r1 = 100 - p1
        # bet 2, we lose not a whole number of pounds
        b2 = r1 * p2  # = (bet 2) * 100
        if b2 % 100 == 0: continue
        r2 = 100 * r1 - b2  # = (remaining 2) * 100
        # bet 3, we are left with (p1 + p2 + p3) pounds
        b3 = r2 * p3  # = (bet 3) * 100^2
        r3 = 100 * r2 - b3  # = (remaining 3) * 100^2
        if r3 != (p1 + p2 + p3) * 10000: continue
        # output solution
        printf(
          "p1={p1}% p2={p2}% p3={p3}% -> b1={b1:.2f} b2={b2:.2f} b3={b3:.2f}; r1={r1:.2f} r2={r2:.2f} r3={r3:.2f}",
          b1=float(p1), b2=(b2 * 0.01), b3=(b3 * 0.0001), r1=float(r1), r2=(r2 * 0.01), r3=(r3 * 0.0001),
        )
      

      Solution: £2.25 was bet (and lost) on the third race.

      The scenario is:

      start = £100.00
      bet 1 = 25% of £100.00 = £25.00
      bet 1 lost, remaining = £75.00
      bet 2 = 25% of £75.00 = £18.75
      bet 2 lost, remaining = £56.25
      bet 3 = 4% of £56.25 = £2.25
      bet 3 lost, remaining = £54.00
      sum of percentage values = 25 + 25 + 4 = 54.


      For a faster program we can do a bit of analysis:

      We can write (r1, r2, r3) in terms of (p1, p2, p3) to get:

      r1 = 100 − p1
      r2 = (100 − p1)(100 − p2)/100
      r3 = (100 − p1)(100 − p2)(100 − p3)/10000
      r3 = p1 + p2 + p3

      10000(p1 + p2 + p3) = (100 − p1)(100 − p2)(100 − p3)

      Note that 5^4 divides the LHS, so it must divide the terms of the RHS, so one of them must be a multiple of 25 (i.e. 25, 50, 75), and another must also be a multiple of 5.

      And this gives us a faster way to generate possible (p1, p2, p3) values.

      This Python 3 program uses a different [[ generate() ]] function, and runs in 61ms. (Internal runtime is 89µs).

      Run: [ @replit ]

      from enigma import (irange, div, subsets, printf)
      
      # generate possible percentage values
      def generate():
        # one of the values is a multiple of 25
        for a in (25, 50, 75):
          # an another is a multiple of 5
          for b in irange(5, 95 - a, step=5):
            c_ = div(10000 * (a + b + 100), 10000 + (100 - a) * (100 - b))
            if c_ is None or not (c_ < 100): continue
            # return the values (in some order)
            yield from subsets((a, b, 100 - c_), size=len, select='mP')
      
      # consider values for p1, p2, p3
      for (p1, p2, p3) in generate():
        # bet 1, we lose p1 pounds
        r1 = 100 - p1
        # bet 2, we lose not a whole number of pounds
        b2 = r1 * p2  # = (bet 2) * 100
        if b2 % 100 == 0: continue
        r2 = 100 * r1 - b2  # = (remaining 2) * 100
        # bet 3, we are left with (p1 + p2 + p3) pounds
        b3 = r2 * p3  # = (bet 3) * 100^2
        r3 = 100 * r2 - b3  # = (remaining 3) * 100^2
        if r3 != (p1 + p2 + p3) * 10000: continue
        # output solution
        printf(
          "p1={p1}% p2={p2}% p3={p3}% -> b1={b1:.2f} b2={b2:.2f} b3={b3:.2f}; r1={r1:.2f} r2={r2:.2f} r3={r3:.2f}",
          b1=float(p1), b2=(b2 * 0.01), b3=(b3 * 0.0001), r1=float(r1), r2=(r2 * 0.01), r3=(r3 * 0.0001),
        )
      

      In fact only one set of values (25, 25, 4) satisfies the analysis. And only with the 4% value for the final bet is the condition that the second amount bet is not a whole number of pounds satisfied.

      Like

    • Frits's avatar

      Frits 7:08 pm on 27 June 2023 Permalink | Reply

      Manual solution:

      Let the percentages NOT bet at each stage be p1, p2 and p3 and
      let the remainders after each stage be r1, r2 and r3 with the
      initial amount r0. Then r1 = r0.p1 / 100, r2 = r1.p2 / 100 and
      r3 = r2.p3 / 100 giving:
      
      p1 = 100.r1/r0 = 10000.r2/p2.r0 = 1000000.r3/p2.p3.r0
      
         p1.p2.p3 = 10^6.r3/r0
      
      Also:
      
        100.r3/r0 = (100 - p1) + (100 - p2) + (100 - p3)
      
      Eliminating r3/r0 now allows p3 to be derived from p1 and p2:
      
        p3 = (300 - p1 - p2).10^4 / (10^4 + p1.p2)
      
        p1 * p2 * p3 - 10^4 * (300 - p1 - p2 - p3) = 0
      
        p1 * p2 * p3 = (300 - p1 - p2 - p3) * 16 * 5^4
      
      if p1, p2 and p3 are all multiples of 5 then
      (300 - p1 - p2 - p3) is also a multiple of 5 so
      p1 * p2 * p3 must be a multiple of 5^5
      
      so we can conclude at least 2 out (p1, p2, p3) must be equal to 75,
      remaining percentage p* must be a multiple of 16
      and (300 - p1 - p2 - p3) must be a multiple of 9 (3 * 3)
          (150 - p*) must be a multiple of 9
          so p* mod 9 = 6
      the only multiple of 16 and mod 9 = 6 out of 64, 80 and 96 is 96
      so (p1, p2, p3) = (75, 75, 96) in any order
      
      p1 can't be 96 as 0.75 * 96 is a whole number
      p2 can't be 96 as 75 * 0.96 is the same whole number
      so (p1, p2, p3) = (75, 75, 96) in this order
      
      on the third race was lost: (3/4)^2 * 100 * (100 - 96) / 100 = 2.25 pounds
      

      Like

  • Unknown's avatar

    Jim Randell 10:08 am on 25 June 2023 Permalink | Reply
    Tags:   

    Brain-Teaser 346: Antics 

    From The Sunday Times, 24th December 1967 [link]

    All distances and dimensions are exact feet; all times, exact seconds; all the spiders run at 5 feet per second; and drop with a maximum fall of 30 feet.

    Ara and Chne sat hungry in the top north-west corner of the palace corridor (no flies).

    “Could be and ant or two in that bottom south-east corner by the garden door”, said Chne.

    “True”, said Ara. She dropped to the floor and headed straight for the prospective meal, while Chne (she never drops) instantly set off down the shortest route via the north wall and floor.

    Farther along the corridor, Taran and Tula sat hungry together at the top of the south wall.

    “Hey, look!”, cried Taran, as the ant-hunters approached.

    “Must be something in that corner”, said Tula, dropping to the floor and speeding straight toward it.

    Taran at the same moment ran direct for the corner. As she started, Ara, clocking 39 seconds, passed by.

    Tangle and wrangle! Dead heat all! No ants!

    How wide is the corridor?

    This puzzle is included in the book Sunday Times Brain Teasers (1974). The puzzle text is taken from the book.

    [teaser346]

     
    • Jim Randell's avatar

      Jim Randell 10:09 am on 25 June 2023 Permalink | Reply

      By folding the walls of the corridor flat we can turn all of the paths into straight lines in the same plane.

      Measuring all distances in feet, and times in “ticks” (= 1/5 second), then each spider travels 1 ft per tick.

      We don’t know how long it takes a spider to drop the height of the corridor, but this is calculated as part of the solution.

      The following Python program runs in 58ms. (Internal runtime is 859µs).

      Run: [ @replit ]

      from enigma import (pythagorean_triples, sum_of_squares, is_square, printf)
      
      # consider the path Taran takes, it is the hypotenuse of a Pythagorean triple
      for (a, b, t2) in pythagorean_triples(999):
        # but must be divisible by 5
        if t2 % 5 > 0: continue
        # and the height of the corridor must be no more than 30
        for (h, t1) in [(a, b), (b, a)]:
          # and Tula's path must also be a multiple of 5
          if h > 30 or t1 % 5 > 0: continue
          # and their times are equal, so we can calculate drop time (in 1/5 s)
          d = t2 - t1
      
          # total time for Ara and Chne is 195 ticks more than for t2
          t = t2 + 195
          # and this is Chne's distance, the hypotenuse of a (w + h, l, t) triangle
          for (x, y) in sum_of_squares(t * t, min_v=1):
            for (wh, l) in [(x, y), (y, x)]:
              w = wh - h
              if w < 0 or not (l > t1): continue
              # Ara's time is the same as Chne's
              z = is_square(w * w + l * l)
              if z is None or z + d != t: continue
      
              # output solution
              printf("h={h} t1={t1} t2={t2}; d={d} t={t}; w={w} l={l} z={z}")
      

      Solution: The corridor is 39 ft wide.

      And 25 ft high, and 252 ft long.

      Chne takes a direct route (across the N wall and floor) so travels hypot(39 + 25, 252) = 260 ft (in 52s).

      Ara drops to the floor (which takes 1s) and travels hypot(39, 252) = 255 ft (in 51s).

      At the 39s mark Taran and Tula set off on their journeys.

      Taran crosses the S wall diagonally for 13s, so travels a distance of 65 ft.

      Tula drops to the floor (1s) and travels for 12s a distance of 60 ft.

      And so they all arrive in the SE bottom corner at exactly the same time.

      Like

  • Unknown's avatar

    Jim Randell 4:34 pm on 23 June 2023 Permalink | Reply
    Tags:   

    Teaser 3170: Time to admire the adverts 

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

    George and Martha use a local underground station, which has up and down escalators and an adjacent 168-step staircase. Martha always uses the escalator. George, being the fitness fanatic, always uses the stairs. In olden times, he could keep up with her while climbing or descending at seven steps per second. But now he can only ascend at N steps per second and descend at N + 1 steps per second, N being a whole number. They start together at the bottom and go up and down continually, losing negligible time in turning. Interestingly, at some point after they have both been all the way up and down, but before ten minutes have elapsed, Martha overtakes George precisely half-way up or half-way down.

    After how many seconds does this happen?

    [teaser3170]

     
    • Jim Randell's avatar

      Jim Randell 4:55 pm on 23 June 2023 Permalink | Reply

      Presumably N is between 1 and 6.

      This Python program considers possible values, generating the times for Martha and George at the halfway mark, and then looking for times when Martha is overtaking George.

      We use rational numbers to avoid loss of precision in floating point comparisons.

      It runs in 64ms. (Internal runtime is 140µs).

      Run: [ @replit ]

      from enigma import (Rational, irange, printf)
      
      Q = Rational()
      
      # generate times at halfway (less than T) -> t
      def times(up, dn, H=84, T=600):
        # go halfway up
        t = Q(H, up)
        # go half up/dn (or half dn/up) to return to halfway
        inc = t + Q(H, dn)
        while t < T:
          yield t
          t += inc
      
      # generate M's times
      M = dict((t, m) for (m, t) in enumerate(times(7, 7)))
      
      # consider possible N values
      for N in irange(1, 6):
        # generate G's times
        for (g, t) in enumerate(times(N, N + 1)):
          # look for times when they meet
          if g < 2: continue
          m = M.get(t)
          if m is None: continue
          # we are interested in when they are going the same direction
          if m % 2 != g % 2: continue
          # output solution
          printf("N={N}: t={t}s [g={g} m={m}]")
      

      Solution: After 588 seconds.

      i.e. after 9 minutes, 48 seconds.

      George can only manage 1 step/s ascending and 2 steps/s descending.

      At that time George has been up the stairs 2.5 times (each ascent taking 168s) and down 2 times (each descent taking 84s, so the total time is 2.5×168 + 2×84 = 588s).

      Martha has been up the stairs 12.5 times and down 12 times (each ascent and descent takes 24s, so the total time is 12.5×24 + 12×24 = 588s).


      Manually:

      Martha travels at a constant 7 steps per second, so she is at halfway at the following times (in seconds):

      [12, 36, 60, 84, 108, …, 588]

      i.e. the odd multiples of 12, less than 600 (= 10 minutes).

      The first two elements are discarded (as M must have completed one full up and down).

      So she is going up on [60, 108, 156, …, 588] and down on [84, 132, 180, …, 564].

      As multiples of 12 these are:

      up = [5, 9, 13, …, 49]
      down = [7, 11, 15, …, 47]

      George takes N steps/s going up and (N + 1) steps/s going down so, he is halfway at:

      g[1] = 84/N
      g[k + 1] = g[k] + (84/N + 84/(N + 1))

      When N = 1, G’s halfway points are separated by 84 + 42 = 126:

      g = [84, 210, 336, 462, 588]

      And as multiples of 12 these are:

      g = [7, 17.5, 28, 38.5, 49]

      Only the first and fifth are odd multiples of 12, and the first two elements are discounted as G must have completed one full up and down.

      So only the fifth element for G is permissible, and as it is in an odd position, G is on his way up.

      And 49 also appears on M’s up list, so this gives an answer to the puzzle.

      And there are no further solutions for N = 2 .. 6.

      Like

    • Frits's avatar

      Frits 6:15 pm on 23 June 2023 Permalink | Reply

         
      NS = 168  # number of staircase steps
      
      # one cycle: going up and down
      
      # walk for less than 10 minutes and check halfway points for Martha
      for t in range(12, 600, 24):
        # determine the direction Martha was going
        directionM = "u" if t % 48 == 12 else "d"
         
        for N in range(1, 7):
          # times for George to walk up and down
          u, d = NS / N, NS / (N + 1)
          
          r = t % (u + d)     # time left after cycles have been completed
          # George must have been halfway
          if r not in {u / 2, u + d / 2}: continue
          
          # determine the direction George was going
          directionG = "u" if r < u else "d"
          if directionG != directionM: continue
          
          print("answer: after", t, "seconds")
      

      Like

    • Frits's avatar

      Frits 2:25 pm on 26 June 2023 Permalink | Reply

      71 iterations.

       
      # work with tenths of seconds to avoid fractional inaccuracies
      # when dividing by 5
      
      # T = time in tenths of seconds to walk all staircase steps 
      #     if you take 1 step a second
      T = 10 * 168
      
      # possible answers, Martha must have gone up and down at least twice
      answers = set(range(1080, 6000, 240))
      
      # consider possible N values
      for N in range(1, 7):
        # times in tenths of second for George to walk up and down
        u, d = T // N, T // (N + 1)
        ud, half_ud = u + d, (u + d) // 2
        
        up = 1       # toggle for up/down
        t = ud + u // 2
        # check George's times at halfway points
        while t < 6000:
          # for Martha to overtake George also check their directions
          if t in answers and ((t % 480) == 120) == up:
            print("answer: after", t // 10, "seconds")
          t += half_ud
          up = 1 - up
      

      Like

      • Jim Randell's avatar

        Jim Randell 10:10 am on 28 June 2023 Permalink | Reply

        @Frits: Yes, calculating G’s halfway times and then checking to see when one matches a halfway time for M only considers 71 cases (because when G is slow, he doesn’t reach halfway many times in the 10 minutes).

        And I think in general it is also a good idea not to rely on strict equality of floats.

        Like

  • Unknown's avatar

    Jim Randell 10:24 am on 22 June 2023 Permalink | Reply
    Tags: by: John Sharples   

    Teaser 2088: Black and white 

    From The Sunday Times, 22nd September 2002 [link]

    Throughout the following calculations, each digit has consistently been replaced by a letter, with different letters for different digits:

    Here the fractions are not necessarily in their simplest form — indeed, each of the three fractions in the above subtraction is in fact a whole number. Furthermore (though you might not need to know all these):

    What number is BLACK?

    [teaser2088]

     
    • Jim Randell's avatar

      Jim Randell 10:25 am on 22 June 2023 Permalink | Reply

      Here is a solution using the [[ SubstitutedExpression ]] solver from the enigma.py library.

      It runs in 78ms. (Internal runtime of the generated program is 9ms).

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      # fraction() representation of 0
      --code="zero = fraction()"
      
      # "BL/WH - AC/IT = K/E" ...
      "fraction(BL, WH, -AC, IT,  -K, E) == zero"
      
      # ... but each fraction is a whole number
      "BL % WH = 0" "AC % IT = 0" "K % E = 0"
      
      # "IT/WB + CA/KL = H/E"
      "fraction(IT, WB,  CA, KL,  -H, E) == zero"
      
      # "WB/TI * KL/CA = E/H"
      "fraction(WB * KL, TI * CA,  -E, H) == zero"
      
      # "BT/IK / EL/WA = H/C"
      "fraction(BT * WA, IK * EL,  -H, C) == zero"
      
      --answer="BLACK"
      --template="(BL/WH - AC/IT = K/E) (IT/WB + CA/KL = H/E) (WB/TI * KL/CA = E/H) (BT/IK / EL/WA = H/C)"
      --solution=""
      

      Solution: BLACK = 80549.

      And the sums are:

      [BL/WH − AC/IT = K/E] (80/16 − 54/27 = 9/3) → (5 − 2 = 3)
      [IT/WB + CA/KL = H/E] (27/18 + 45/90 = 6/3) → (3/2 + 1/2 = 2)
      [WB/TI × KL/CA = E/H] (18/72 × 90/45 = 3/6) → (1/4 × 2 = 1/2)
      [BT/IK ÷ EL/WA = H/C] (87/29 ÷ 30/15 = 6/4) → (3 ÷ 2 = 3/2)

      Like

      • Frits's avatar

        Frits 3:31 pm on 22 June 2023 Permalink | Reply

        @Jim, I noticed the generated code allows zero for variable H (which is used as a denominator).

        A further optimisation of SubstitutedExpression() could be to detect that we have used the maximum of variables (10) and that only one (L) can have a specific value (zero) which then can be assigned.

        Like

        • Frits's avatar

          Frits 3:45 pm on 22 June 2023 Permalink | Reply

          I have done some sudoku solving lately.

          If variables are distinct and values p, q, r are the only values used by variables X, Y, Z then we can remove p,q,r from the candidates of other variables.

          Like

        • Jim Randell's avatar

          Jim Randell 10:30 am on 23 June 2023 Permalink | Reply

          @Frits: I think both these cases are currently handled reasonably by the [[ SubstitutedExpression ]] solver.

          The solver doesn’t know how you are going to use a symbol, so it can’t remove values that are inappropriate to use as a denominator of a fraction. But it does skip over values that cause a runtime exception (although in some cases this can also mask flaws in the supplied expressions). If you set the warn (or -W) flag these failures are reported as warnings. Invoking my code with this flag set shows that it does not actually try the H=0 assignments.

          Also symbols that are part of a distinct grouping have values that have already been used by other symbols in that group removed, so if there is only a single value remaining, that is what will be used. And if the solver is allowed to choose the order of the expressions it will start with those that have to fewest possible assignments.

          Like

    • GeoffR's avatar

      GeoffR 7:33 pm on 22 June 2023 Permalink | Reply

      
      % A Solution in MiniZinc
      include "globals.mzn";
      
      var 1..9:B; var 0..9:L; var 1..9:W; var 0..9:H;
      var 1..9:A; var 0..9:C; var 1..9:I; var 0..9:T;
      var 1..9:K; var 1..9:E;
      
      constraint all_different ([B, L, W, H, A, C, I, T, K, E]);
      
      % Another form of first sum: BL/WH - AC/IT = K/E
      constraint (10*B + L) * (10*I  + T) * E - (10*A + C) *(10*W +  H) * E
      == K * (10*I  + T)* (10*W +  H);
      
      % Each of the three fractions in the first sum gives an exact division
      constraint (10*B + L) mod (10*W + H) == 0;
      constraint (10*A + C) mod (10*I + T) == 0;
      constraint K mod E == 0;
      
      % The second equation ; IT/WB + CA/KL = H/E 
      constraint E * ((10*I + T)* (10*K + L) + (10*C + A) * (10*W + B)) 
      == H * (10*W +  B) * (10*K + L);
      
      % 3rd equation: WB/TI X LL/CA = E/H
      constraint H * (10*W + B) * (10*K + L) == E * (10*T + I) * (10*C + A);
      
      % 4th equation: BT/IK / EL/WA =  H/C
      constraint (10*B + T) * (10*W + A) * C == (10*E + L) *(10*I + K) * H;
      
      solve satisfy;
      
      output ["BLACK = " ++ "\(B)\(L)\(A)\(C)\(K)" ];
      
      % BLACK = 80549
      % ----------
      % ==========
      
      
      

      I found using the 1st and 2nd sums, together with the the three exact divisions of the components of the 1st sum, was sufficient in practice to get a single answer for BLACK.

      Like

    • Frits's avatar

      Frits 2:42 pm on 23 June 2023 Permalink | Reply

      With a bit more analysis.

      from enigma import SubstitutedExpression
      
      vars = "ABCEKLITWH"
      distinct = set(vars)
      # invalid digit / symbol assignments
      d2i, s2d = dict(), dict()
      
      for d in range(0, 10):
        vs = set()
        # starting digit of 2-digit numbers may not be zero
        if d == 0: vs.update('BWAICKTE')
        # IT/WB + CA/KL = H/E
        if d == 0: vs.update('H')
        # BL/WH >= 4 (sum of two true multiples) so WH < 25 and BL > 40
        if d > 2: vs.update('W')
        if d < 4: vs.update('B')
        
        d2i[d] = vs
      
      # check if a value can only belong to one variable
      for k, vs in d2i.items():
        if len(vs) == 9:
          s2d[c := set(vars).difference(vs).pop()] = k
          distinct = distinct.difference(c)
      
      # more analysis, L = 0 and 
      # BT * WA * C == H * IK * EL so either T, A, or C is equal to 5  
      # WB * KL * H == E * TI * CA so either E, I, or A is equal to 5  
      s2d["A"] = 5
      d2i[5] = set(vars).difference("A")
      distinct = distinct.difference("A")
      
      # the alphametic puzzle
      p = SubstitutedExpression(
        [
          # each fraction is a whole number
          "K % E = 0",
          "BL % WH = 0",
          # BL/WH - AC/IT = K/E
          "AC // (BL // WH - K // E) = IT",
          "AC % IT = 0",
          
          # maybe not all three equations are necessary
          
          # IT/WB + CA/KL = H/E 
          "E * (IT * KL + CA * WB) == H * WB * KL", 
          
          # WB/TI × KL/CA = E/H
          "WB * KL * H == E * TI * CA",
          
          # BT/IK ÷ EL/WA = H/C
          "BT * WA * C == H * IK * EL",
        ],
        answer="BLACK",
        d2i=d2i,
        s2d=s2d,
        distinct=str(distinct),
        #reorder=0,
        verbose=0,    # use 256 to see the generated code
      )
      
      # print answers
      for (_, ans) in p.solve():
        print(f"BLACK={ans}")   
      

      Like

  • Unknown's avatar

    Jim Randell 9:31 am on 20 June 2023 Permalink | Reply
    Tags:   

    Teaser 2156: Some cubes 

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

    In what follows, digits have consistently been replaced by letters, with different letters used for different digits:

    THREE
    CAN
    BE
    CUBES

    As stated, three of those numbers are cubes (and the other one is less than five away from a cube).

    What is the value of CUBE?

    [teaser2156]

     
    • Jim Randell's avatar

      Jim Randell 9:32 am on 20 June 2023 Permalink | Reply

      This Python program uses the [[ SubstitutedExpression() ]] solver from the enigma.py library to find candidate values that are each individually within 5 of a cube number. We then check the answers for cases where three of the distances are 0.

      It runs in 68ms. (Internal runtime is 11ms).

      Run: [ @replit ]

      from enigma import (SubstitutedExpression, iroot, printf)
      
      # distance to nearest kth root
      def dist(n, k=3):
        x = iroot(n, k)
        return min(n - x**k, (x + 1)**k - n)
      
      # make an alphametic puzzle
      p = SubstitutedExpression(
        [
          # each must be less than 5 away from a cube
          "dist(THREE) < 5",
          "dist(CAN) < 5",
          "dist(BE) < 5",
          "dist(CUBES) < 5",
        ],
        answer="(THREE, CAN, BE, CUBES)",
        env=dict(dist=dist),
        verbose=0
      )
      # solve the puzzle
      for ans in p.answers():
        # count the distances
        ds = list(dist(x) for x in ans)
        # three must have distance 0
        if ds.count(0) != 3: continue
        # output solution
        CUBE = ans[-1] // 10
        printf("CUBE = {CUBE} {ans}")
      

      Solution: CUBE = 1968.

      The numbers are:

      THREE = 74088 = 42³
      CAN = 125 = 5³
      BE = 68 = 4³ + 4
      CUBES = 19683 = 27³

      Like

    • Frits's avatar

      Frits 1:57 pm on 20 June 2023 Permalink | Reply

      More messy (the reorder=0 isn’t even necessary).

         
      #!/usr/bin/env pypy -m enigma -r
       
      SubstitutedExpression
      
      # each must be a cube or less than 5 away from a cube
      "(c1 := is_cube_p(BE)) or \
        BE - (r := int(BE**(1/3)))**3 < 5        or (r + 1)**3 - BE < 5"
      "(c2 := is_cube_p(CUBES)) or (c1 and \
        (CUBES - (r := int(CUBES**(1/3)))**3 < 5 or (r + 1)**3 - CUBES < 5))"
      "(c3 := is_cube_p(CAN)) or (c1 and c2 and \
        (CAN - (r := int(CAN**(1/3)))**3 < 5     or (r + 1)**3 - CAN < 5))"
      "is_cube_p(THREE) or (c1 and c2 and c3 and \
        (THREE - (r := int(THREE**(1/3)))**3 < 5 or (r + 1)**3 - THREE < 5))"
      
      --answer="(THREE, CAN, BE, CUBES)"
      
      # [optional] less verbose output
      --template=""
      
      --reorder=0
      

      Like

      • Frits's avatar

        Frits 7:02 pm on 20 June 2023 Permalink | Reply

        The 2 missing ” and ” don’t cause a syntax error.

        Like

      • Jim Randell's avatar

        Jim Randell 8:26 am on 21 June 2023 Permalink | Reply

        @Frits: I think I see what this is doing. (I removed the extraneous trailing commas, and fixed up some missing and keywords).

        But I think it would allow the case where all four of the words were cubes.


        This is what my [[ SubstitutedExpression ]] run file looks like:

        Run: [ @replit ]

        #! python3 -m enigma -rr
        
        SubstitutedExpression
        
        # distance to nearest kth root
        --code="def _dist(n, k=3): x = iroot(n, k); return min(n - x**k, (x + 1)**k - n)"
        --code="dist = cache(_dist)"
        
        # check cube distance is less than 5 (if flag) or 0
        --code="def check(n, f): x = dist(n); return (0 < x < 5 if f else x == 0)"
        
        # n = 1..4 denotes which statement is not a cube
        --invalid="0|5|6|7|8|9,n"
        
        # each is less than 5 away from a cube
        "check({THREE}, {n} ==  1)"
        "check({CAN}, {n} == 2)"
        "check({BE}, {n} == 3)"
        "check({CUBES}, {n} == 4)"
        
        --distinct="ABCEHNRSTU"
        --answer="CUBE"
        --template="(THREE, CAN, BE, CUBES)"
        --solution="n"
        

        Like

    • GeoffR's avatar

      GeoffR 5:43 pm on 20 June 2023 Permalink | Reply

      Using Frits method of solution i.e.each number must be a cube or less than 5 away from a cube.

      % A Solution in MiniZinc
      include "globals.mzn";
      
      var 1..9:T; var 0..9:H; var 0..9:R; var 0..9:E;
      var 1..9:C; var 0..9:A; var 0..9:N; var 1..9:B;
      var 0..9:U; var 0..9:S;
      
      constraint all_different ([T, H, R, E, C, A, N, B, U, S]);
      
      % Four numbers are BE, CAN, CUBES, THREE
      var 10..99:BE == 10*B + E;
      var 100..999:CAN == 100*C + 10*A + N;
      var 10000..99999:CUBES == 10000*C + 1000*U + 100*B + 10*E + S;
      var 10000..99999:THREE == 10000*T + 1000*H + 100*R + 11*E;
      
      % Required answer is CUBE
      var 1000..9999:CUBE == 1000*C + 100*U + 10*B + E; 
      
      % Sets of cubes for 2-digit, 3-digit and 5-digit numbers
      set of int: cb2 = {27, 64};
      set of int: cb3 = {n * n * n | n in 5..9};
      set of int: cb5 = {n * n * n | n in 22..46};
      
      % Three of the numbers are cubes
      constraint sum([THREE in cb5, CAN in cb3, BE in cb2, CUBES in cb5]) == 3;
      
      % Allow for the fourth number to be less than five away from a cube
      % Number BE
      constraint BE in cb2 \/ (BE - 1) in cb2  \/ (BE - 2) in cb2 
      \/ (BE - 3) in cb2   \/ (BE - 4) in cb2  \/ (BE + 1) in cb2 
      \/ (BE + 2) in cb2   \/ (BE + 3) in cb2  \/ (BE + 4) in cb2; 
      % Number CAN
      constraint CAN in cb3 \/ (CAN - 1) in cb3 \/ (CAN - 2) in cb3
       \/ (CAN - 3) in cb3  \/ (CAN - 4) in cb3 \/ (CAN + 1) in cb3 
       \/ (CAN + 2) in cb3  \/ (CAN + 3) in cb3 \/ (CAN + 4) in cb3;
      % Number THREE
      constraint THREE in cb5 \/ (THREE - 1) in cb5  \/ (THREE - 2) in cb5
       \/ (THREE - 3) in cb5  \/ (THREE - 4) in cb5  \/ (THREE + 1) in cb5 
       \/ (THREE + 2) in cb5  \/ (THREE + 3) in cb5  \/ (THREE + 4) in cb5;
      % Number CUBES
      constraint CUBES in cb5 \/ (CUBES - 1) in cb5 \/ (CUBES - 2) in cb5
       \/ (CUBES - 3) in cb5  \/ (CUBES - 4) in cb5 \/ (CUBES + 1) in cb5 
       \/ (CUBES + 2) in cb5  \/ (CUBES + 3) in cb5 \/ (CUBES + 4) in cb5;
                                                                                        
      solve satisfy;
      
      output["CUBE = " ++ show(CUBE)  ++ "\n" ++   
      "[THREE, CAN, BE, CUBES] = " ++ show ([THREE, CAN, BE, CUBES]) ];
      
      % CUBE = 1968
      % [THREE, CAN, BE, CUBES] = [74088, 125, 68, 19683]
      % ----------
      % ==========
      
      
      

      Like

  • Unknown's avatar

    Jim Randell 2:38 pm on 18 June 2023 Permalink | Reply
    Tags:   

    Brain-Teaser 433: Hymn numbers 

    From The Sunday Times, 24th August 1969 [link]

    The hymn-board in church was electrically operated and made provision for any four hymn numbers of up to three digits in each (from 0 to 9), but did not provide blanks so that Hymn No. 7 for example would appear on the board as 007, and No. 77 as 077.

    One Sunday a parishioner noticed with surprise that not only was each of the four hymn numbers for that service a perfect square, but moreover each of the three numbers produced in the vertical columns was a 4-digit perfect square.

    When, back home, he told his son Tom of this unique occurrence, the latter exclaimed: “I can’t believe it! What were the numbers then?”, to which the father replied: “You already know enough to work that out for yourself, but it will be of help to you to learn that I also noticed that the totals of the digits of each hymn number were perfect squares too, and that, coincidentally, one of these squares would result from dividing the final hymn number into the number formed by the middle column”.

    But Tom still couldn’t puzzle it out and demanded to be told at any rate one of the hymn numbers, to which the father responded, “All I can remember now is that the third hymn number ended in two noughts”.

    What was the opening hymn number?

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

    [teaser433]

     
    • Jim Randell's avatar

      Jim Randell 2:39 pm on 18 June 2023 Permalink | Reply

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

      The following run file executes in 80ms. (Internal runtime of the generated code is 8ms).

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      #  A B C
      #  D E F
      #  G H I
      #  J K L
      
      --distinct=""
      --invalid="0,ABC" # columns form 4-digit squares
      
      # hymns are different
      "all_different(ABC, DEF, GHI, JKL)"
      
      # rows form squares
      "is_square(ABC)"
      "is_square(DEF)"
      "is_square(GHI)"
      "is_square(JKL)"
      
      # columns form squares
      "is_square(ADGJ)"
      "is_square(BEHK)"
      "is_square(CFIL)"
      
      # row sums form squares
      "is_square(A + B + C)"
      "is_square(D + E + F)"
      "is_square(G + H + I)"
      "is_square(J + K + L)"
      
      # 3rd row is x00
      --assign="H,0"
      --assign="I,0"
      
      # 4th row divides into middle column, to give one of the row sums
      "div(BEHK, JKL) in {A + B + C, D + E + F, G + H + I, J + K + L}"
      
      # [optional] just show the hymn numbers
      --template="ABC DEF GHI JKL"
      --solution=""
      

      Solution: The opening hymn number was: 529.

      The hymns are: 529, 36, 400, 144.

      We have:

      rows
      529 = 23²; 5 + 2 + 9 = 16 = 4²
      036 = 6²; 0 + 3 + 6 = 9 = 3²
      400 = 20²; 4 + 0 + 0 = 4 = 2²
      144 = 12² ; 1 + 4 + 4 = 9 = 3²

      columns
      5041 = 71²
      2304 = 48²
      9604 = 98²

      2304 / 144 = 16 = 4² = 5 + 2 + 9

      And we do indeed get a single solution if we just keep the requirements that the rows and columns form squares (lines 16 – 25).

      Like

  • Unknown's avatar

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

    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 9:33 am on 15 June 2023 Permalink | Reply
    Tags:   

    Teaser 2016: T for two 

    From The Sunday Times, 6th May 2001 [link]

    I have in mind a whole number less than 100. If I were to tell you the first letter in the spelling of that number, you would not be able to determine the number. But if I were to tell you both the first and last letters in the spelling of the number, then you would be able to determine it. Now, If I were to tell you just the last letter in the spelling of the number, you would be able to determine it.

    What is the number?

    [teaser2016]

     
    • Jim Randell's avatar

      Jim Randell 9:33 am on 15 June 2023 Permalink | Reply

      I supposed the numbers were from 1 to 99 (although you can also use 0 if you like).

      This Python program combines the [[ int2words() ]] and [[ filter_unique() ]] functions from the enigma.py library.

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

      Run: [ @replit ]

      from enigma import (irange, int2words, filter_unique, intersect, join, printf)
      
      # numbers from 1 to 99 as words
      ns = dict((n, int2words(n)) for n in irange(1, 99))
      
      # return a function to select letters at indices <js> from the spelling of a number
      select = lambda *js: (lambda k: join(ns[k][j] for j in js))
      
      # the number is ambiguous by first letter
      ss1 = filter_unique(ns.keys(), f=select(0)).non_unique
      
      # but unique by first + last letter
      ss2 = filter_unique(ns.keys(), f=select(0, -1)).unique
      
      # from these it is unique by last letter
      ss = filter_unique(intersect([ss1, ss2]), f=select(-1)).unique
      
      # output solution
      printf("{ss}", ss=join(ss, sep=", "))
      

      Solution: The number is 98.

      Like

    • Frits's avatar

      Frits 2:06 pm on 15 June 2023 Permalink | Reply

      Maintaining a dictionary.

         
      from enigma import int2words
      
      # lowercase letters
      letters = ''.join(chr(ord('a') + i) for i in range(26))
       
      # numbers from 1 to 99 as words
      nums = [int2words(n) for n in range(1, 100)]
      
      # the number is ambiguous by first letter
      d = {l: [x for x in nums if x[0] == l] for l in letters}
      d = {k: vs for k, vs in d.items() if len(vs) > 1}
      
      # but unique by first + last letter
      d = {l1 + l2: [x for x in nums if x[0] == l1 and x[-1] == l2 and x[0] in d] 
                     for l1 in letters for l2 in letters}
      d = {k: vs for k, vs in d.items() if len(vs) == 1}       
      
      # from these it is unique by last letter
      d = {k[-1]: [vs2 for k2, vs2 in d.items() if k[-1] == k2[-1]] for k in d}   
      d = {k: vs for k, vs in d.items() if len(vs) == 1}
      
      for k, v in d.items():
        print("answer:", *v[0])
      

      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