Updates from Jim Randell Toggle Comment Threads | Keyboard Shortcuts

  • Unknown's avatar

    Jim Randell 6:31 am on 19 July 2022 Permalink | Reply
    Tags:   

    Teaser 2721: Milliner’s hat-trick 

    From The Sunday Times, 16th November 2014 [link] [link]

    A football tournament has four groups each of four teams, with the teams in the same group playing each other once. So far the teams have each played two games and in each group the distribution of points is different. Also, in each group just one pair of teams are level on points and their positions have been determined by their “goals scored”. Milliner has scored four (including a hat-trick), Orlando two, and nine other players have scored one goal each. Despite his success, Orlando’s team is not the top of their group.

    What are the results of the two games that Milliner’s team have played? And the two games that Orlando’s team have played?

    [teaser2721]

     
    • Jim Randell's avatar

      Jim Randell 6:33 am on 19 July 2022 Permalink | Reply

      The puzzle is not explicit on the scoring system used (i.e. 2 points for a win, or 3 points for a win), although it turns out it doesn’t matter which is used, the answer to the puzzle is the same in both cases.

      In each group each team has played 2 of their 3 matches. So there have been 4 matches played (and there are 2 matches left to play). So among the 4 groups there have been 16 matches played in total.

      The minimum possible number of goals scored among the 4 matches in a group is 2. So the maximum possible number of goals scored is 15 − 3×2 = 9. (Although it may be possible to reduce this theoretical maximum value, which would speed up the program).

      For each group, this Python program considers the scores for teams A (1st), B (2nd), C (3rd), D (4th), such that each team has played 2 matches, and two of the teams are tied on points, but have different “goals for” values.

      It then chooses 4 different points distributions (one for each of the groups), and from the previously examined scores it chooses 4 sets of scores that have 15 goals scored in total, and ensures there are possible teams for both Milliner and Orlando.

      The program runs in 361ms.

      Run: [ @replit ]

      from collections import defaultdict
      from enigma import (
        Football, irange, subsets, tuples, cproduct, peek, delete,
        update, find, unzip, reverse, map2str, join, printf
      )
      
      # scoring regime (2 or 3 points for a win)
      football = Football(games='wdlx', points=dict(w=2, d=1))
      
      # labels for the teams
      teams = (A, B, C, D) = tuple("ABCD")
      
      # keys for the matches
      ks = list(x + y for (x, y) in subsets(teams, size=2))
      
      # allocate <t> goals among the matches <ms>, return scores <ss>
      def allocate(t, ms, ss=dict()):
        # are we done?
        if not ms:
          if t == 0:
            yield ss
        else:
          # calculate the number of surplus goals
          n = t - sum(v == 'w' for v in ms.values())
          if n < 0: return
          # choose the next match to allocate goals to
          (k, v) = peek(ms.items())
          if v == 'x':
            # not played
            yield from allocate(t, delete(ms, [k]), update(ss, [(k, None)]))
          elif v == 'w' or v == 'l':
            # a win (or loss), calculate scores (x, y)
            for x in irange(1, n + 1):
              for y in irange(0, min(x - 1, n - x + 1)):
                s = ((x, y) if v == 'w' else (y, x))
                yield from allocate(t - x - y, delete(ms, [k]), update(ss, [(k, s)]))
          elif v == 'd':
            # a draw, calculate scores (x, x)
            for x in irange(0, n // 2):
              yield from allocate(t - x - x, delete(ms, [k]), update(ss, [(k, (x, x))]))
      
      # check goals scored by team <t> in a match from scores <ss> is <n> or more
      def scored(t, ss, n):
        for (k, v) in ss.items():
          if v is not None:
            i = find(k, t)
            if i != -1 and v[i] >= n:
              return True
      
      # map: <pts> -> <total goals> -> (<match scores>, <milliner>, <orlando>) values
      d = defaultdict(lambda: defaultdict(list))
      
      # consider possible match outcomes
      for ms in football.games(repeat=len(ks)):
        ms = dict(zip(ks, ms))
        # compute the table for each team
        table = dict((t, football.extract_table(ms, t)) for t in teams)
        # each team has played twice
        if not all(table[t].played == 2 for t in teams): continue
        # points are increasing, with exactly 2 teams having the same points
        pts = tuple(table[t].points for t in teams)
        if len(set(pts)) != 3: continue
        if not all(x >= y for (x, y) in tuples(pts, 2)): continue
        # allocate goals to the matches
        for t in irange(2, 9):
          for ss in allocate(t, ms):
            # extract the goals (for, against) for each team
            goals = dict((t, football.extract_goals(ss, t)) for t in teams)
            # check teams with same points have different "goals for" values
            if any(p1 == p2 and not (goals[t1][0] > goals[t2][0])
              for ((t1, p1), (t2, p2)) in tuples(zip(teams, pts), 2)
            ): continue
            # find teams for Milliner: team must have a match with >= 3 goals scored
            # and have >= 4 goals scored in total
            tM = list(t for (t, (f, _)) in goals.items() if f > 3 and scored(t, ss, 3))
            # find teams for Orlando: must have >= 2 goals, and not be top
            tO = list(t for (t, (f, _)) in goals.items() if t != 'A' and f > 1)
            # record this set of scores
            d[pts][t].append((ss, tM, tO))
      
      # collect scores from <ss> for teams in <ts> (from the team's perspective)
      def collect(ss, ts):
        for t in ts:
          rs = list()
          for (s, f) in unzip(football.extract(ss, t)):
            if s is None: continue
            rs.append(reverse(s) if f else s)
          yield tuple(sorted(rs, reverse=1))
      
      # choose 4 different points distributions
      for ps in subsets(d.keys(), size=4):
        # choose the number of goals scored in each group
        for ts in cproduct(d[p].keys() for p in ps):
          # there should be 15 goals in total
          if sum(ts) != 15: continue
          # choose the actual scores in each match
          for ss in cproduct(d[p][t] for (p, t) in zip(ps, ts)):
            # check for possibilities for Milliner and Orlando
            if not any(tM for (_, tM, _) in ss): continue
            if not any(tO for (_, _, tO) in ss): continue
            # output a solution (groups = scores, pts, M, O; M's team; O's team)
            (Ms, Os) = (set(), set())
            for (i, ((s, tM, tO), p)) in enumerate(zip(ss, ps), start=1):
              printf("Group {i}: {s} {p} {tM} {tO}", s=map2str(s, enc=""), tM=join(tM, enc="[]"), tO=join(tO, enc="[]"))
              Ms.update(collect(s, tM))
              Os.update(collect(s, tO))
            for M in sorted(Ms): printf("M's team: {M}", M=join(M, sep=", "))
            for O in sorted(Os): printf("O's team: {O}", O=join(O, sep=", "))
            printf()
      

      Solution: The results for Milliner’s team are: a 3-1 win, a 1-0 win. The results for Orlando’s team are: a 2-0 win, a 0-1 loss.

      M’s team is a group leader. O’s team is second in their group.

      With “2 points for a win” (and 1 for a draw) there are three possible sets of scores:

      Group 1: A v B = 1-0; A v C = 1-0; B v D = 2-0; C v D = 1-0; points = [4, 2, 2, 0], Orlando is team B
      Group 2: A v C = 3-1; A v D = 1-0; B v C = 0-0; B v D = 0-0; points = [4, 2, 1, 1], Milliner is team A
      Group 3: A v B = 1-0; A v C = 0-0; B v D = 1-0; C v D = 0-0; points = [3, 2, 2, 1]
      Group 4: A v C = 0-0; A v D = 2-0; B v C = 0-0; B v D = 1-0; points = [3, 3, 2, 0]

      Group 1: A v B = 1-0; A v D = 1-0; B v C = 2-0; C v D = 1-0; points = [4, 2, 2, 0], Orlando is team B
      Group 2: A v C = 3-1; A v D = 1-0; B v C = 0-0; B v D = 0-0; points = [4, 2, 1, 1], Milliner is team A
      Group 3: A v B = 1-0; A v C = 0-0; B v D = 1-0; C v D = 0-0; points = [3, 2, 2, 1]
      Group 4: A v C = 0-0; A v D = 2-0; B v C = 0-0; B v D = 1-0; points = [3, 3, 2, 0]

      Group 1: A v C = 1-0; A v D = 1-0; B v C = 0-1; B v D = 2-0; points = [4, 2, 2, 0], Orlando is team B
      Group 2: A v C = 3-1; A v D = 1-0; B v C = 0-0; B v D = 0-0; points = [4, 2, 1, 1], Milliner is team A
      Group 3: A v B = 1-0; A v C = 0-0; B v D = 1-0; C v D = 0-0; points = [3, 2, 2, 1]
      Group 4: A v C = 0-0; A v D = 2-0; B v C = 0-0; B v D = 1-0; points = [3, 3, 2, 0]

      We can examine what happens with “3 points for a win” by changing the initialisation at line 8. And we find in this case there are four possible sets of scores:

      Group 1: A v B = 1-0; A v D = 0-0; B v C = 2-0; C v D = 1-0; points = [4, 3, 3, 1], Orlando is team B
      Group 2: A v B = 1-1; A v D = 1-0; B v C = 0-0; C v D = 0-0; points = [4, 2, 2, 1]
      Group 3: A v C = 3-1; A v D = 1-0; B v C = 0-0; B v D = 0-0; points = [6, 2, 1, 1], Milliner is team A
      Group 4: A v C = 0-0; A v D = 2-0; B v C = 0-0; B v D = 1-0; points = [4, 4, 2, 0]

      Group 1: A v B = 1-0; A v D = 0-0; B v C = 2-0; C v D = 1-0; points = [4, 3, 3, 1], Orlando is team B
      Group 2: A v C = 0-0; A v D = 1-0; B v C = 0-0; B v D = 1-1; points = [4, 2, 2, 1]
      Group 3: A v C = 3-1; A v D = 1-0; B v C = 0-0; B v D = 0-0; points = [6, 2, 1, 1], Milliner is team A
      Group 4: A v C = 0-0; A v D = 2-0; B v C = 0-0; B v D = 1-0; points = [4, 4, 2, 0]

      Group 1: A v C = 1-0; A v D = 0-0; B v C = 0-1; B v D = 2-0; points = [4, 3, 3, 1], Orlando is team B
      Group 2: A v B = 1-1; A v D = 1-0; B v C = 0-0; C v D = 0-0; points = [4, 2, 2, 1]
      Group 3: A v C = 3-1; A v D = 1-0; B v C = 0-0; B v D = 0-0; points = [6, 2, 1, 1], Milliner is team A
      Group 4: A v C = 0-0; A v D = 2-0; B v C = 0-0; B v D = 1-0; points = [4, 4, 2, 0]

      Group 1: A v C = 1-0; A v D = 0-0; B v C = 0-1; B v D = 2-0; points = [4, 3, 3, 1], Orlando is team B
      Group 2: A v C = 0-0; A v D = 1-0; B v C = 0-0; B v D = 1-1; points = [4, 2, 2, 1]
      Group 3: A v C = 3-1; A v D = 1-0; B v C = 0-0; B v D = 0-0; points = [6, 2, 1, 1], Milliner is team A
      Group 4: A v C = 0-0; A v D = 2-0; B v C = 0-0; B v D = 1-0; points = [4, 4, 2, 0]

      Like

  • Unknown's avatar

    Jim Randell 12:33 pm on 17 July 2022 Permalink | Reply
    Tags:   

    Teaser 2531: [Harshad numbers] 

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

    A Harshad number (or H-number) is any number that is divisible by the sum of its digits. Using each non-zero digit just the once, I have written down a 9-figure H-number. Reading from left to right, it also consists of three 2-figure H-numbers followed by a 3-figure H-number. Again working from left to right through the 9-figure number, the last five digits form a 5-figure H-number. Reversing the order of the first five digits of the 9-figure number also gives a 5-figure H-number.

    What is the 9-figure number?

    This puzzle was originally published with no title.

    [teaser2531]

     
    • Jim Randell's avatar

      Jim Randell 12:34 pm on 17 July 2022 Permalink | Reply

      See: [@wikipedia].

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

      The following run file executes in 72ms. The internal runtime of the generated program is 3.1ms).

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      --digits="1-9"
      
      # is <n> an H-number?
      --code="H = lambda n: n % dsum(n) == 0"
      
      # the 9-digit number is: abcdefghi
      "H({abcdefghi})"
      
      # three 2-digit H-numbers and a 3-digit H-number
      "H({ab})"
      "H({cd})"
      "H({ef})"
      "H({ghi})"
      
      # 5-digit H-numbers
      "H({efghi})"
      "H({edcba})"
      
      --answer="{abcdefghi}"
      

      Solution: The 9-digit number is: 273684915.

      Like

    • GeoffR's avatar

      GeoffR 2:16 pm on 17 July 2022 Permalink | Reply

      
      % A Solution in MiniZinc 
      include "globals.mzn";
      
      var 1..9: a; var 1..9: b; var 1..9: c;
      var 1..9: d; var 1..9: e; var 1..9: f;
      var 1..9: g; var 1..9: h; var 1..9: i;
      
      constraint all_different([a,b,c,d,e,f,g,h,i]);
      
      % 2-digit Harshad numbers
      constraint (10*a + b) mod (a + b) == 0
      /\ (10*c + d) mod (c + d) == 0
      /\ (10*e + f) mod (e + f) == 0;
      
      % 3-digit Harshad number
      constraint (100*g + 10*h + i) mod (g + h + i) == 0;
      
      % 5-digit Harshad numbers
      constraint (10000*e + 1000*f + 100*g + 10*h + i) mod(e + f + g + h + i) == 0;
      constraint (10000*e + 1000*d + 100*c + 10*b + a) mod(e + d + c + b + a) == 0;
      
      % 9-digit Harshad number
      constraint (a * pow(10,8) + b * pow(10,7) + c * pow(10,6)
      + d * pow(10,5) + e * pow(10,4) + f * pow(10,3)
      + g * pow(10,2) + h * 10 + i) mod (a+b+c+d+e+f+g+h+i) == 0;
      
      solve satisfy;
      
      output ["9-figure number = " ++ " \(a)\(b)\(c)\(d)\(e)\(f)\(g)\(h)\(i)" ];
      
      % 9-figure number = 273684915
      % ----------
      % ==========
      
      

      Like

    • Frits's avatar

      Frits 3:40 pm on 17 July 2022 Permalink | Reply

         
      from itertools import permutations
      from functools import reduce
      
      # convert digits sequence to number
      d2n = lambda s: reduce(lambda x, y: 10 * x + y, s)
      
      # calculate H-number
      H = lambda s: d2n(s) % sum(s) == 0
      
      i = 5  # as abcdefghi is divisible by 45
      
      # ghi:   sum(g,h) must be even as sum(g,h,i) must be odd
      # efghi: sum(e,f) must be even as sum(e,f,g,h,i) must be odd
      # ef:    e and f not both odd otherwise 10*d + e is not divisible by e + f
      #        so e and f are both even
      # ab:    a and b not both odd
      # cd:    c and d not both odd
      # g an h must be both odd otherwise a, b, c and d must all be odd
      
      # ....eeoo5
      # edcba : a must be even as sum(e,d,c,b,a) is even
      # e...eeoo5
      # as c and d are not both odd b must be odd
      # eo..eeoo5
      
      for (b, g, h) in permutations([1, 3, 7, 9], 3):
        if not H((g, h, i)): continue
        for (a, e, f) in permutations([2, 4, 6, 8], 3):
          if not H((a, b)): continue
          if not H((e, f)): continue
          if not H((e, f, g, h, i)): continue
          rest = set(range(1, 10)).difference({a, b, e, f, g, h, i})
          for (c, d) in permutations(rest):
            if not H((c, d)): continue
            if not H((e, d, c, b, a)): continue
            if not H((a, b, c, d, e, f, g, h, i)): continue
            print("the 9-figure number:", d2n((a, b, c, d, e, f, g, h, i)))
      

      Like

    • GeoffR's avatar

      GeoffR 6:43 pm on 17 July 2022 Permalink | Reply

      I carried out some further analysis to see how the number of Harshad numbers reduced with different arrangements of numbers.

      1) For numbers ab, cd, ef, ghi and abcdefghi, this gave 96 possible 9-digit Harshad numbers.

      2) Adding abcde to (1) above, I found 16 possible Harshad numbers for abcdefghi:

       a b c d e f g h i    a b c d e f g h i    a b c d e f g h i
      ------------------------------------------------------------
       2 7 3 6 4 8 1 9 5,   2 7 3 6 8 4 9 1 5,   2 7 6 3 4 8 1 9 5,   
       2 7 6 3 8 4 9 1 5,   3 6 2 7 4 8 1 9 5,   3 6 2 7 8 4 9 1 5,   
       3 6 7 2 4 8 1 9 5,   3 6 7 2 8 4 9 1 5,   6 3 2 7 4 8 1 9 5,   
       6 3 2 7 8 4 9 1 5,   6 3 7 2 4 8 1 9 5,   6 3 7 2 8 4 9 1 5,   
       7 2 3 6 4 8 1 9 5,   7 2 3 6 8 4 9 1 5,   7 2 6 3 4 8 1 9 5,   
       7 2 6 3 8 4 9 1 5.
      

      3) Finally, adding number edcba to (2) above, this gave the single 9-digit answer of 273684915.
      (top row, middle value)

      Interesting to note how the 16 solutions illustrate aspects of analysis in Frits code.

      Like

  • Unknown's avatar

    Jim Randell 4:03 pm on 15 July 2022 Permalink | Reply
    Tags:   

    Teaser 3121: Top marks 

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

    A teacher is preparing her end of term class test. After the test she will arrive at each pupil’s score by giving a fixed number of marks for each correct answer, no marks for a question that is not attempted, and deducting a mark for each incorrect answer. The computer program she uses to prepare parents’ reports can only accept tests with the number of possible test scores (including negative scores) equal to 100.

    She has worked out all possible combinations of the number of questions asked and marks awarded for a correct answer that satisfy this requirement, and has chosen the one that allows the highest possible score for a pupil.

    What is that highest possible score?

    [teaser3121]

     
    • Jim Randell's avatar

      Jim Randell 4:25 pm on 15 July 2022 Permalink | Reply

      If there are n questions asked, and there are a marks for a correct answer, then the possible scores are:

      score = ax − z; where x + z ≤ n

      And we need there to be exactly 100 possible scores.

      There are tri(n + 1) different (correct, unanswered, incorrect) ways the n questions can be answered.

      So we need n ≥ 13 in order for there to be 100 or more different combinations (some may have the same score).

      Here’s a quick program to solve the puzzle.

      It runs in 66ms.

      Run: [ @replit ]

      from enigma import (irange, inf, Accumulator, printf)
      
      # find the highest possible score (all answers correct)
      r = Accumulator(fn=max, collect=1)
      
      # consider possible marks for a correct answer
      for a in irange(1, 10):
        # consider possible numbers of questions
        for n in irange(13, inf):
          # calculate possible scores
          ss = set(a * x - z for x in irange(0, n) for z in irange(0, n - x))
          k = len(ss)
          if k > 100: break
          if k == 100:
            m = max(ss) # = a * n
            r.accumulate_data(m, (a, n))
            printf("[a={a} n={n} m={m}]")
      
      # output solutions
      printf("max score = {m}")
      for (a, n) in r.data:
        printf("-> {n} questions; {a} marks for a correct answer")
      

      Solution: The maximum possible score is 105.

      There are 15 questions, and 7 marks for each correct answer. The maximum is therefore 15×7 = 105.

      It is also possible to have 100 potential scores with 21 questions, and 4 marks for each correct answer. In this case the maximum is 21×4 = 84.

      Like

    • Frits's avatar

      Frits 7:28 pm on 15 July 2022 Permalink | Reply

         
      m = 0 
      # consider possible marks for a correct answer
      for a in range(1, 11):
        # range [-n, -n+1, ..., -1, 0, 1, ..., a*n]
        # max scores            = (a + 1) * n + 1    
        # nr impossible to make = a * (a - 1) / 2
      
        # n.a + n + 1 - 1/2 a.a + 1/2 a = 100
        # (2a + 2)n = a.a - a + 198
        n, r = divmod(a * a - a + 198, 2 * a + 2)
        if r: continue
        
        m = max(m, a * n)
      
      print("answer:", m)  
      

      Like

      • Jim Randell's avatar

        Jim Randell 8:59 am on 16 July 2022 Permalink | Reply

        I wondered how you arrived at your formula for the unreachable marks.

        I came up with this:

        If we get all the questions right the (maximum possible) score is: a.n

        If we get all but one of the questions right (and don’t attempt the remaining question) the next lowest score is: a(n − 1).

        So there are (a − 1) unreachable scores between these.

        If, however, we get the remaining question wrong, we get a score of: a(n − 1) − 1.

        The there are only (a − 2) unreachable scores in the next gap.

        If we get all but two of the questions right the possible scores are: a(n − 2), a(n − 2) − 1, a(n − 2) − 2.

        And there are (a − 3) unreachable scores in the following gap.

        So, in each gap the number of unreachable scores decreases by 1.

        Hence the total number of unreachable scores is: tri(a − 1), providing na. (All possible negative scores are reachable).

        And using this we can determine the number of possible scores without having to construct them.

        And we can proceed manually (there are only 10 values to check), or programatically:

        Run: [ @replit ]

        from enigma import (irange, inf, tri, Accumulator, printf)
        
        # find the highest possible score (all answers correct)
        r = Accumulator(fn=max, collect=1)
        
        # consider possible marks for a correct answer
        for a in irange(1, inf):
          # "unreachable" scores
          u = tri(a - 1)
          # find number of questions (100 = m + n + 1 - u)
          (n, q) = divmod(99 + u, a + 1)
          if n < 13: break
          if q != 0: continue
          assert n >= a
          # max possible score
          m = a * n
          r.accumulate_data(m, (a, n))
          printf("[a={a} n={n} m={m}]")
        
        # output solutions
        printf("max score = {m}")
        for (a, n) in r.data:
          printf("-> {n} questions; {a} marks for a correct answer")
        

        Manually, we can consider increasing a values, calculate u values (by just adding the preceding a value), and compute n = (99 + u) / (a + 1). We need n to be an integer ≥ 13. The calculations proceed as follows:

        a = 1; u = 0; n = 99/2 = 49r1
        a = 2; u = 1; n = 100/3 = 33r1
        a = 3; u = 3; n = 102/4 = 25r2
        a = 4; u = 6; n = 105/5 = 21 [candidate solution]
        a = 5; u = 10; n = 109/6 = 18r1
        a = 6; u = 16; n = 115/7 = 16r2
        a = 7; u = 21; n = 120/8 = 15 [candidate solution]
        a = 8; u = 28; n = 127/9 = 14r1
        a = 9; u = 36; n = 135/10 = 13r5
        a = 10; u = 45; n = 144/11 = 13r1
        a = 11; u = 55; n = 154/12 = 12r10 [n < 13]

        There are two candidate solutions a=4, n=21 ⇒ max=84 and a=7, n=15 ⇒ max=105, so we want the second one.

        Liked by 1 person

        • Frits's avatar

          Frits 12:48 pm on 16 July 2022 Permalink | Reply

          I printed and analyzed the sorted ss lists of your program and noticed that
          the first unreachable number always is F = (n – (a – 2)) * a – (a – 1).
          This happens if the number of correct answers is equal to n – (a – 2).

          the next two unreachable numbers always are:
          (F + a, F + a + 1)
          the next three unreachable numbers always are:
          (F + 2*a, F + 2*a + 1, F + 2*a + 2)
          etc…

          Like

      • Frits's avatar

        Frits 12:27 am on 17 July 2022 Permalink | Reply

        If a >= n then the number of possible test scores is independent of a.
        The number of unreachable scores is then tri(a – 1) – tri(a – n – 1).

        The number of possible test scores is equal to (n + 1) * (n + 2) / 2.
        The number of possible test scores equal to 100 leads to the formula
        (n + 1) * (n + 2) = 200

        A positive solution for n is (3 * sqrt(89) – 3) / 2 = 12.65 so
        not an integer solution.

        Like

    • Frits's avatar

      Frits 6:09 pm on 21 July 2022 Permalink | Reply

      Formula (2a + 2).n = a.a – a + 198 is valid if marks for a correct answer (a) is not higher than the number of questions (n). If a is higher or equal to n then n must be a non-integer solution (approx 12.65).

      Another option would have been to use divisor_pairs().

         
      #! python3 -m enigma -r
      
      # rearrangement idea from John Crabtree:
      
      # formula (2a + 2).n = a.a - a + 198
      # can be rearranged to 2.n + 3 = 200 / (a + 1) + (a + 1) 
      # so both terms of RHS are divisors of 200
       
      SubstitutedExpression
      
      # find divisors of 200
      "div(200, AB) = DEF"
      "1 < AB <= DEF"
      
      # make sure exactly one of them is odd
      "(AB + DEF) % 2"
      
      # marks for a correct answer: AB - 1
      # number of question: (AB + DEF - 3) / 2
      --answer="(AB - 1) * (AB + DEF - 3) // 2"
      --distinct=""
      --accumulate=max
      --invalid="2-9,A"
      --verbose=16
      

      Like

      • Jim Randell's avatar

        Jim Randell 3:04 pm on 22 July 2022 Permalink | Reply

        I also did a solution based on John Crabtree’s analysis:

        Run: [ @replit ]

        from enigma import (Accumulator, divisors_pairs, div, printf)
        
        # based on John Crabtree's derivation: 2n + 3 = 200/(a + 1) + (a + 1)
        
        # find the highest possible score (all answers correct)
        r = Accumulator(fn=max, collect=1)
        
        # look for divisors of 200
        for (p, q) in divisors_pairs(200, every=1):
          a = p - 1
          if a < 1: continue
          # calculate n
          n = div(p + q - 3, 2)
          if n is None or n < a: continue
          m = a * n
          r.accumulate_data(m, (a, n))
          printf("[a={a}: n={n} m={m}]")
        
        # output solutions
        printf("max score = {m}")
        for (a, n) in r.data:
          printf("-> {n} questions; {a} marks for a correct answer")
        

        It is neat because we sum the divisor pairs.

        But it is more straightforward (and slightly faster) just to consider increasing a values (as in my second program).

        Like

  • Unknown's avatar

    Jim Randell 9:01 am on 14 July 2022 Permalink | Reply
    Tags:   

    Teaser 2532: In hot pursuit 

    From The Sunday Times, 3rd April 2011 [link] [link]

    George and Martha are jogging around a circular track. Martha starts at the most westerly point, George starts at the most southerly point, and they both jog clockwise at their own steady speeds. After a short while Martha is due north of George for the first time. Five minutes later she is due south of him for the first time. Then George catches up with her during their second laps at the most northeasterly point of the track.

    What is Martha’s speed (in degrees turned per minute)?

    [teaser2532]

     
    • Jim Randell's avatar

      Jim Randell 9:02 am on 14 July 2022 Permalink | Reply

      If George starts at 0° and goes at g degrees per minute, and Martha starts at 90° and goes at m degrees per minute.

      Then at time x (shortly after they set off) M is due north of G. So the angle G has gone is the same angular distance that M has to go to reach the north (180°) marker.

      So we have:

      xg = 90° − xm
      x(g + m) = 90°

      And 5 minutes later M is due south of G for the first time. The distance G has gone beyond the north (180°) marker is the same as the distance that M has to go to reach the south (360°/0°) marker.

      (x + 5)g − 180° = 270° − (x + 5)m
      (x + 5)(g + m) = 450°

      5x = x + 5
      4x = 5
      x = 5/4
      g + m = 72°/min

      Later, at some time t during lap 2, G catches up with M at the north-east (225°) marker. So G has gone 585° and M has gone 495°.

      Hence:

      585 = tg
      495 = tm

      t(g + m) = 1080
      t = 1080 / 72 = 15 min
      g = 39°/min
      m = 33°/min

      Solution: Martha’s speed is 33°/min.

      Like

  • Unknown's avatar

    Jim Randell 7:29 am on 12 July 2022 Permalink | Reply
    Tags:   

    Teaser 2724: Headcount 

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

    My grandson and I play a coin game. First we toss seven coins and I have to predict in advance the number of heads whilst he has to predict the number of tails. I then get a number of points equal to the number of heads, he gets a number of points equal to the number of tails, and anyone whose prediction was correct gets a fixed bonus number of points (less than 40). We repeat this with six coins in the second round, then five, and so on down to two. In a recent game we noticed that, after each round, the total of all the points so far awarded was equal to a prime number.

    What is the “fixed bonus” number of points? And what was the total of all the points at the end of the game?

    [teaser2724]

     
    • Jim Randell's avatar

      Jim Randell 7:30 am on 12 July 2022 Permalink | Reply

      (See also: Teaser 3009).

      In a round with n coins the points awarded are, the number of heads (+ bonus if guessed correctly) and the number of tails (+ bonus if guessed correctly). So n points are always awarded, along with 0, 1, or 2 bonuses.

      We don’t need to worry about the points won by each player, just the total number of points gained in each round.

      This Python program keeps a set of (total, bonus) pairs, and then progresses through the rounds keeping viable values.

      It runs in 57ms. (Internal runtime is 664µs).

      Run: [ @replit ]

      from enigma import (irange, is_prime, printf)
      
      # start with a total of 0, and all possible bonus values
      ss = set((0, x) for x in irange(0, 40))
      
      # start with <k> coins
      k = 7
      
      # consider subsequent rounds
      while k > 1:
        ss_ = set()
        # consider (total, bonus) in the previous round
        for (t, b) in ss:
          # consider number of bonuses awarded in this round
          for n in (0, 1, 2):
            t_ = t + k + n * b
            if is_prime(t_):
              ss_.add((t_, b))
        # move on to the next round
        (ss, k) = (ss_, k - 1)
      
      # output solutions
      for (t, b) in ss:
        printf("total = {t}, bonus={b}")
      

      Solution: The number of bonus points is 19. The total number of points at the end of the game is 103.

      The progression of the game is:

      7 coins: total = 7 (0 bonus points)
      6 coins: total = 13 (0 bonus points)
      5 coins: total = 37 (19 bonus points)
      4 coins: total = 79 (38 bonus points)
      3 coins: total = 101 (19 bonus points)
      2 coins: total = 103 (0 bonus points)

      Like

  • Unknown's avatar

    Jim Randell 8:02 am on 10 July 2022 Permalink | Reply
    Tags:   

    Brain-Teaser 150: Paving the way 

    From The Sunday Times, 23rd February 1964 [link]

    I have eight paving stones whose dimensions are an exact number of inches. Their areas are 504, 420, 324, 306, 270, 130, 117 and 112 square inches. Four of these are red and four green. There should be a ninth stone coloured yellow and square so that all nine stones can be fitted together to form a square in such a way that the red stones completely enclose the other five and the green stones completely enclose the yellow one.

    What are the dimensions of the red stones?

    [teaser150]

     
    • Jim Randell's avatar

      Jim Randell 8:03 am on 10 July 2022 Permalink | Reply

      The tiles that are mentioned have a total area of 2183.

      If the missing central tile has an area of x², and the entire collection has an area of y² then we have:

      y² = x² + 2183
      y² − x² = 2183
      (y − x)(y + x) = 2183

      So we can calculate possible values for x and y by examining the divisors of 2183.

      The Python program find values for x and y, and then chooses 4 green tiles, along with the x by x yellow square, which can fit together to form a square. And then checks that this square, along with the the remaining (green) tiles can fit together to form a y by y square.

      We use the rectpack.py module to fit the tiles into squares.

      The program runs in 169ms.

      Run: [ @replit ]

      from enigma import (divisors_pairs, div, subsets, is_square, cproduct, printf)
      import rectpack
      
      # areas of tiles we are given
      areas = {504, 420, 324, 306, 270, 130, 117, 112}
      
      # record possible tiles
      dps = dict((x, list(divisors_pairs(x))) for x in areas)
      
      # select tiles by area <x>, with max dimension not exceeding <m>
      tiles = lambda x, m: ((a, b) for (a, b) in dps[x] if not (b > m))
      
      # pack rectangles <rs> and an <s> by <s> square into a <t> by <t> square
      def pack(rs, s, t):
        # make the list of rectangles
        rs_ = [(s, s)]
        rs_.extend(rs)
        # pack them into a t by t square
        for ps in rectpack.pack(t, t, rs_):
          # check the square is not on the edge
          if not any(
            w == h == s and x > 0 and y > 0 and x + w < t and y + h < t
            for (x, y, w, h) in ps
          ): continue
          return ps  # we only need one packing
      
      # consider divisors of the total area
      for (a, b) in divisors_pairs(sum(areas)):
        # calculate x and y
        (x, y) = (div(b - a, 2), div(a + b, 2))
        if x is None or y is None or y < x + 4: continue
        # choose 4 green tiles to go around a central x by x square
        for green in subsets(areas, size=4):
          z = is_square(x * x + sum(green))
          if z is None: continue
          # consider dimensions of green tiles
          for gs in cproduct(tiles(x, z) for x in green):
            # pack them into a z by z square
            ps1 = pack(gs, x, z)
            if ps1 is None: continue
            # consider dimensions of red tiles
            red = areas.difference(green)
            for rs in cproduct(tiles(x, y) for x in red):
              # pack them into a y by y square
              ps2 = pack(rs, z, y)
              if ps2 is None: continue
              # output packings
              printf("red {red} around {z} x {z} green in {y} x {y}\n-> {ps2}")
              printf("green {green} around {x} x {x} yellow in {z} x {z}\n-> {ps1}")
              printf()
      

      Solution: The dimensions of the red stones (in inches) are: 3×39, 6×45, 9×36, 12×42.

      Here is a diagram of one possible layout:

      Like

      • Frits's avatar

        Frits 1:01 pm on 10 July 2022 Permalink | Reply

        @Jim, it is not stated that the yellow and green tiles form a square.

        Like

        • Jim Randell's avatar

          Jim Randell 1:11 pm on 10 July 2022 Permalink | Reply

          @Frits: Is there a way the red stones can enclose the green ones without forming a square?

          Like

        • Jim Randell's avatar

          Jim Randell 1:14 pm on 10 July 2022 Permalink | Reply

          I suppose it could be a non-square rectangle. But luckily it is possible to do it with a square.

          Like

          • Frits's avatar

            Frits 1:18 pm on 10 July 2022 Permalink | Reply

            So there might be another solution…

            Like

          • Jim Randell's avatar

            Jim Randell 1:26 pm on 10 July 2022 Permalink | Reply

            I made some tweaks to my program, and it didn’t find any solutions with a non-square rectangle. But I’ll have a closer look.

            Like

          • Jim Randell's avatar

            Jim Randell 2:53 pm on 10 July 2022 Permalink | Reply

            This is my program adapted to consider packing the green and yellow tiles into a (possibly non-square) rectangle.

            It doesn’t find any additional solutions.

            Run: [ @replit ]

            from enigma import (divisors_pairs, div, subsets, cproduct, printf)
            import rectpack
            
            # areas of tiles we are given
            areas = {504, 420, 324, 306, 270, 130, 117, 112}
            
            # record possible tiles
            dps = dict((x, list(divisors_pairs(x))) for x in areas)
            
            # select tiles by area <x>, with max dimension not exceeding <m>
            tiles = lambda x, m: ((a, b) for (a, b) in dps[x] if not(b > m))
            
            # pack rectangles <rs> and rectangle <s> into a bounding rectangle <t>
            def pack(rs, s, t):
              (a, b) = t
              # make the list of rectangles
              rs_ = [s]
              rs_.extend(rs)
              # pack them into a the rectangle
              for ps in rectpack.pack(t[0], t[1], rs_):
                # check that s is not on the edge
                if not any(
                  x > 0 and y > 0 and x + w < a and y + h < b and {w, h} == set(s)
                  for (x, y, w, h) in ps
                ): continue
                return ps  # we only need one packing
            
            # consider divisors of the total area
            for (a, b) in divisors_pairs(sum(areas)):
              # calculate x and y
              (x, y) = (div(b - a, 2), div(a + b, 2))
              if x is None or y is None or y < x + 4: continue
              # choose 4 green tiles to go around the central x by x square
              for green in subsets(areas, size=4):
                for (a, b) in divisors_pairs(x * x + sum(green)):
                  if a < x + 2 or y < b + 2: continue
                  # consider dimensions of green tiles
                  for gs in cproduct(tiles(x, b) for x in green):
                    # pack them into an a by b rectangle
                    ps1 = pack(gs, (x, x), (a, b))
                    if ps1 is None: continue
                    # consider dimensions of red tiles
                    red = areas.difference(green)
                    for rs in cproduct(tiles(x, y) for x in red):
                      # pack them into a y by y square
                      ps2 = pack(rs, (a, b), (y, y))
                      if ps2:
                        # output packings
                        printf("red {red} around {a} x {b} green in {y} x {y}\n-> {ps2}")
                        printf("green {green} around {x} x {x} yellow in {a} x {b}\n-> {ps1}")
                        printf()
            

            Like

    • Frits's avatar

      Frits 5:02 pm on 10 July 2022 Permalink | Reply

      One piece of logic is not coded, it turns out it is not needed.

         
      from enigma import divisors_pairs
      from itertools import product
       
      # find three other red pieces which can form the outer ring
      def check(a, b, d):
        # combinations of length and width of fourth red piece
        lst = [[[big_square_side - x, (big_square_side - y) * x] 
                for x in d[big_square_side - y] if (big_square_side - x) in d] 
               for y in [a, b]]
        
        # check every combination of possible length and width
        for c, d in product(lst[0], lst[1]):
          # has the fourth piece a known area
          if c[0] * d[0] not in d_red: continue
      
          # do all four pieces have a different area
          if len(set([c[0] * d[0], a * b, c[1], d[1]])) != 4: continue
          # return all four pieces
          return tuple(sorted([tuple(sorted([a, b])), 
                 tuple(sorted([big_square_side - a, big_square_side - c[0]])), 
                 tuple(sorted([big_square_side - b, big_square_side - d[0]])), 
                 tuple(sorted([c[0], d[0]]))]))
         
        return tuple()          
            
      # areas of tiles we are given
      areas = sorted([504, 420, 324, 306, 270, 130, 117, 112])
       
      # record possible tiles
      d_tiles = dict((x, list(divisors_pairs(x))) for x in areas)
      
      # possible areas for the big square
      sqs = {n * n for n in range(1, areas[-4] + 1)}
      
      area_greenred = sum(areas)
      
      # candidates for yellow area
      area_yellow_cands = [sq for sq in sqs if (sq + area_greenred) in sqs]
      
      # check all candidates for yellow areas 
      for area in area_yellow_cands:
        big_square_side = int((area + area_greenred)**.5)
        yellow = int(area**.5)
        area_yellow = area
        
        # adjust possible red tiles to drop tiles with a big length
        d_red = {k: [x for x in vs if x[0] <= big_square_side and 
                                      x[1] <= big_square_side] 
                    for k, vs in d_tiles.items()}
        
        d_side = dict()     # dictionary per side
        for k, vs in d_red.items():
          for x in vs:
            d_side[x[0]] = d_side.get(x[0], set()) | {x[1]}
            d_side[x[1]] = d_side.get(x[1], set()) | {x[0]}
      
        if big_square_side in d_side:
          # valid situation but not coded!
          continue
        
        # as each piece now is shorter than the side of the big square
        # we need to have a form so that 2 sides of different pieces are equal to
        # the side of the big square, forms should be similar to this one:
        #
        #   +----------+--+
        #   |          |  |
        #   +----+-----+  |
        #   |    |     |  |
        #   |    +-----+--+
        #   |    |        |
        #   +----+--------+
        
        # adjust dictionary to keep only sides <x> for which complementary side 
        # <big_square_side> - <k> also exists
        d_side = {k: vs for k, vs in d_side.items() 
                        if big_square_side - k in d_side}
        # also adjust possible red tiles 
        d_red = {k: [x for x in vs if big_square_side - x[0] in d_side and 
                                      big_square_side - x[1] in d_side] 
                    for k, vs in d_red.items()}
       
        sol = set()
        for k, vs in d_red.items():
          # pick a first red piece
          for (le, wi) in vs:
            # find three other red pieces which can form the outer ring
            chk = check(le, wi, d_side)
            if chk:
              if chk not in sol:
                print("answer:", *chk)
              sol.add(chk)
      

      Like

  • Unknown's avatar

    Jim Randell 4:50 pm on 8 July 2022 Permalink | Reply
    Tags:   

    Teaser 3120: Bus stop blues 

    From The Sunday Times, 10th July 2022 [link] [link]

    While waiting for buses, I often look out for interesting number plates on passing cars. From 2001 the UK registration plate format has been 2 letters + a 2-digit number + 3 more letters, the digits being last two of the year of registration with 50 added after six months (for example in 2011, the possible numbers were 11 and 61). I spotted one recently with its five letters in alphabetical order, all different and with no vowels. Looking more closely, I saw that if their numerical positions in the alphabet (A = 1, B = 2 etc.) were substituted for the 5 letters, their sum plus 1 was the 2-digit number and the sum of their reciprocals was equal to 1.

    Find the 7-character registration.

    [teaser3120]

     
    • Jim Randell's avatar

      Jim Randell 4:59 pm on 8 July 2022 Permalink | Reply

      I used the [[ reciprocals() ]] function from the enigma.py library (originally written for Enigma 348).

      This Python program runs in 63ms. (Internal run time is 174µs).

      Run: [ @replit ]

      from enigma import (reciprocals, printf)
      
      # letters (1-indexed)
      letters = "?ABCDEFGHIJKLMNOPQRSTUVWXYZ"
      
      # vowels: A=1, E=5, I=9, O=15, U=21
      vowels = set(letters.index(x) for x in "AEIOU")
      
      # find 5 reciprocals that sum to 1
      for ns in reciprocals(5, M=26, g=1):
        # no vowels allowed
        if vowels.intersection(ns): continue
        # calculate the year
        y = sum(ns) + 1
        if not (1 < y < 23 or 50 < y < 72): continue
        # output the registration
        (A, B, X, Y, Z) = (letters[n] for n in ns)
        printf("reg = [{A}{B} {y:02d} {X}{Y}{Z}]")
      

      Solution: The registration is: BD 51 HLX.

      Note that the “vowels” restriction is not necessary if the restriction on the year number being within [02, 22] or [51, 71] is observed.

      Like

    • Frits's avatar

      Frits 6:39 pm on 8 July 2022 Permalink | Reply

         
      from enigma import SubstitutedExpression
      
      # the alphametic puzzle
      p = SubstitutedExpression(
        [
          # its five letters converted to numerical positions
          # AB, CD, EF, GH, IJ
          "1 < AB < 23",
          "AB < CD < 24",
          "CD < EF < 25",
          "EF < GH < 26",
          "GH < IJ < 27",
          
          # no vowels allowed, A=1, E=5, I=9, O=15, U=21
          "all(x not in {1, 5, 9, 15, 21} for x in [AB, CD, EF, GH, IJ])",
          # ranges for the sum
          "(AB + CD + EF + GH + IJ) < 22 or \
           49 < (AB + CD + EF + GH + IJ) < 72",
          # the sum of their reciprocals was equal to 1
          "1 / AB + 1 / CD + 1 / EF + 1 / GH + 1 / IJ == 1",
        ],
        answer="AB, CD, EF, GH, IJ",
        d2i="",
        distinct="",
        verbose=0,    # use 256 to see the generated code
      )
      
      # print answers
      for (_, ans) in p.solve():
        reg = chr(ord('A') + ans[0] - 1) + \
              chr(ord('A') + ans[1] - 1) + " "
        reg += str(sum(ans) + 1) + " "
        reg += chr(ord('A') + ans[2] - 1) + \
               chr(ord('A') + ans[3] - 1) + \
               chr(ord('A') + ans[4] - 1)
        print("registration =", reg)
      

      Like

      • Jim Randell's avatar

        Jim Randell 9:38 pm on 8 July 2022 Permalink | Reply

        Or we can use base 27 to make things a bit neater:

        Run: [ @replit ]

        #! python3 -m enigma -rr
        
        SubstitutedExpression
        
        # allow digits 1-26
        --base=27
        # but exclude vowels (1, 5, 9, 15, 21)
        --digits="1-26,!1,!5,!9,!15,!21"
        
        # numbers are in increasing order
        "A < B" "B < X" "X < Y" "Y < Z"
        
        # reciprocals sum to 1
        "fraction(1, A,  1, B,  1, X,  1, Y,  1, Z) == (1, 1)"
        
        # check year
        --code="year = lambda n: (2 <= n <= 22) or (51 <= n <= 71)"
        "year(A + B + X + Y + Z + 1)"
        
        # output the registration
        --code="l = lambda x: int2base(x, base=27, digits='?ABCDEFGHIJKLMNOPQRSTUVWXYZ')"
        --code="n = lambda x: int2base(x, width=2)"
        --code="reg = lambda A, B, X, Y, Z: sprintf('{A}{B} {y} {X}{Y}{Z}', A=l(A), B=l(B), X=l(X), Y=l(Y), Z=l(Z), y=n(A + B + X + Y + Z + 1))"
        --answer="reg(A, B, X, Y, Z)"
        --template=""
        

        Like

    • GeoffR's avatar

      GeoffR 8:27 pm on 8 July 2022 Permalink | Reply

      % A Solution in MiniZinc
      include "globals.mzn";
      
      % Omitting vowel values for A,E,I,O,U
      % Used to translate numerical letters in output(v,w,x,y,z) to letters
      int:B == 2; int:C == 3; int:D == 4; int:F == 6; int:G == 7;
      int:H == 8; int:J == 10; int:K == 11; int:L == 12; int:M == 13;
      int:N == 14; int:P == 16; int:Q == 17; int:R == 18; int:S == 19;
      int:T == 20; int:V == 22; int:W == 23; int:X == 24; int:Y == 25;
      int:Z == 26;
      
      % number plate format is v w num x y z  (num is 2 digits)
      var 2..26:v; var 2..26:w; var 2..26:x;
      var 2..26:y; var 2..26:z;
      
      % last 2 digits in number plate - range = 2001 to 2022 + 50
      var 01..72: num;
      
      set of int: letters = {B,C,D,F,G,H,J,K,L,M,N,P,Q,R,S,T,V,W,X,Y,Z};
      
      % Five number plate letters
      constraint v in letters /\ w in letters /\ x in letters
      /\ y in letters /\ z in letters;
      
      % five letters in number plate are in alphabetical order
      constraint w > v /\ x > w /\ y > x /\ z > y;
      
      % Reciprocals of letter values sum to 1
      constraint z * 1/v + z * 1/w + z * 1/x + z * 1/y + z/z == z;
      
      % Two middle digits are the sum of letter values plus 1
      constraint v + w + x + y + z + 1 == num;
      
      solve satisfy;
      
      output [ "Number plate = " ++ show( [v, w, num, x, y, z ] ) ];
      % uses translation table to convert numbers to letters
      
      
      

      Like

    • GeoffR's avatar

      GeoffR 3:39 pm on 10 July 2022 Permalink | Reply

      
      from itertools import combinations
      import string
      
      # Dictionary mapping numbers to upper case letters
      DN = dict(zip(range(1, 27), string.ascii_uppercase))
      
      # omit values for vowels A, E, I, O, U
      vowels = {1, 5, 9, 15, 21}
      
      for C1 in combinations(set(range(1, 27)).difference(vowels), 5):
        a, b, c, d, e = C1
        #  five letters are in alphabetical order
        if a < b < c < d < e:
          # the sum of their reciprocals was equal to 1
          # i.e. 1/a + 1/b + 1/c + 1/d + 1/e == 1:
          if b*c*d*e + a*c*d*e + a*b*d*e + a*b*c*e + a*b*c*d == a*b*c*d*e:
            
            # last two digits of the year
            year2d = a + b + c + d + e + 1
            if year2d <= 22 or (51 <= year2d <= 72):
              print('Reg. No. = ', DN[a], DN[b], year2d, DN[c], DN[d], DN[e])
      
      
      

      Like

  • Unknown's avatar

    Jim Randell 9:53 am on 7 July 2022 Permalink | Reply
    Tags:   

    Teaser 2723: St Andrew’s day 

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

    Bearing in mind today’s date, I have written down two numbers in code, with different letters being used consistently to replace different digits. The addition of the two numbers is shown below, appropriately leading to today’s date as a six- figure number.

    What is the value of SEND?

    [teaser2723]

     
    • Jim Randell's avatar

      Jim Randell 9:55 am on 7 July 2022 Permalink | Reply

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

      The following run file is straightforward, but not especially quick. It runs in 710ms.

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      "{SAINT} + {ANDREW} = 301114"
      
      --answer="{SEND}"
      

      One way we can improve the execution time is to split the sum into multiple partial sums consisting of columns from the original sum.

      This process is automated by the [[ SubstitutedExpression.split_sum ]] helper method, which can now be called from a run file.

      The following run file executes in 65ms. (The internal runtime of the generated program is 277µs).

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      SubstitutedExpression.split_sum
      
      "{SAINT} + {ANDREW} = {301114}"
      
      --literal="0134"
      --distinct="ADEINRSTW"
      --answer="{SEND}"
      

      Solution: SEND = 3568.

      The solver finds 4 solutions to the sum as (I, R) = (1, 9) and (T, W) = (0, 4), in some order.

      Like

    • GeoffR's avatar

      GeoffR 11:18 am on 7 July 2022 Permalink | Reply

      
      % A Solution in MiniZinc
      include "globals.mzn";
      
      var 1..9:S; var 1..9:A; var 0..9:I;
      var 0..9:N; var 0..9:T; var 0..9:D;
      var 0..9:R; var 0..9:E; var 0..9:W;
      
      constraint all_different([S,A,I,N,T,D,R,E,W]);
      
      var 10000..99999: SAINT = 10000*S + 1000*A + 100*I + 10*N + T;
      var 100000..999999: ANDREW = 100000*A + 10000*N + 1000*D+ 100*R + 10*E + W;
      
      var 1000..9999:SEND = 1000*S + 100*E + 10*N + D;
      
      constraint SAINT + ANDREW == 301114;
      
      solve satisfy;
      output["SEND = " ++ show(SEND) ];
      
      % SEND = 3568
      % ----------
      % ==========
      
      
      

      Like

    • GeoffR's avatar

      GeoffR 2:37 pm on 7 July 2022 Permalink | Reply

      A solution simulating the normal column addition process, with carry digits.

      
      % A Solution in MiniZinc
      include "globals.mzn";
      
      %    S A I N T
      %  A N D R E W
      %-------------
      %  3 0 1 1 1 4
      %-------------
      
      var 1..9:S; var 1..9:A; var 0..9:I;
      var 0..9:N; var 0..9:T; var 0..9:D;
      var 0..9:R; var 0..9:E; var 0..9:W;
      
      constraint all_different([S,A,I,N,T,D,R,E,W]);
      
      % column carry digits for adding columns
      var 0..1: c1; var 0..1: c2; var 0..1: c3; var 0..1: c4; var 0..1: c5; 
      
      % carry digits start from RH end of sum
      constraint (T + W) mod 10 == 4 /\ (T + W) div 10 == c1;
      
      constraint (N + E + c1) mod 10 == 1 /\ (N + E + c1) div 10 == c2;
      
      constraint (I + R + c2) mod 10 == 1 /\ (I + R + c2) div 10 == c3;
      
      constraint (A + D + c3) mod 10 == 1 /\ (A + D + c3) div 10 == c4;
      
      constraint (S + N + c4) mod 10 == 0 /\ (S + N + c4) div 10 == c5;
      
       constraint A + c5 == 3;
       
      solve satisfy;
      
      output ["SEND = \(S)\(E)\(N)\(D)" 
      ++  ", SAINT = \(S)\(A)\(I)\(N)\(T)"
      ++  ", ANDREW = \(A)\(N)\(D)\(R)\(E)\(W)" ];
      
      % SEND = 3568, SAINT = 32964, ANDREW = 268150
      % ----------
      % SEND = 3568, SAINT = 32960, ANDREW = 268154
      % ----------
      % SEND = 3568, SAINT = 32164, ANDREW = 268950
      % ----------
      % SEND = 3568, SAINT = 32160, ANDREW = 268954
      % ----------
      % ==========
      
      
      

      Like

  • Unknown's avatar

    Jim Randell 10:32 am on 5 July 2022 Permalink | Reply
    Tags:   

    Brain-Teaser 731: A little list 

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

    (1) In this list there is a true statement and a false statement whose numbers add up to give the number of a false statement.

    (2) Either statement 4 is false or there are three  consecutive true statements.

    (3) The number of the last false statement subtracted from the product of the numbers of the first and last true statements gives the number of a statement which is false.

    (4) The number of even-numbered true statements equals the number of false statements.

    (5) One of the first and last statements is true and the other is false.

    (6) When I first sent this problem to the editor, thanks to a typing error no sixth statement was included. However the answer to the following question was the same then as it is now:

    Which statements are false?

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

    [teaser724]

     
    • Jim Randell's avatar

      Jim Randell 10:33 am on 5 July 2022 Permalink | Reply

      This Python program runs in 62ms. (Internal runtime is 680µs).

      Run: [ @replit ]

      from enigma import (subsets, filter2, irange, first, product, tuples, icount, printf)
      
      # solve the puzzle with k (= 5 or 6) statements, considering only the first 5 statements
      def solve(k):
      
        # consider the puzzle with k statements
        for ss in subsets((True, False), size=k, select="M"):
          # check the first 5 statements
          (s1, s2, s3, s4, s5) = first(ss, 5)
          (ts, fs) = filter2((lambda n: ss[n - 1]), irange(1, k))
      
          # 1: "in this list there is a true statement and a false statement
          # whose numbers add up to give the number of a false statement"
          if not (s1 == any(x + y in fs for (x, y) in product(ts, fs))): continue
      
          # 2: "either statement 4 is false, or there are three consecutive
          # true statements"
          if not (s2 == ((s4 is False) != (any(all(xs) for xs in tuples(ss, 3))))): continue
      
          # 3: "the number of the last false statement subtracted from the
          # product of the numbers of the first and last true statements gives
          # the number of a statement which is false"
          if not (s3 == (ts and fs and ts[0] * ts[-1] - fs[-1] in fs)): continue
      
          # 4: "the number of even numbered true statements equals the number
          # of false statements"
          if not (s4 == (icount(n for n in ts if n % 2 == 0) == len(fs))): continue
      
          # 5: "one of the first and last statements is true and the other is false"
          if not (s5 == (s1 != ss[-1])): continue
      
          # return the numbers of the false statements
          yield fs
      
      # look for solutions for the first 5 statements of the 6 statement
      # version of the puzzle, grouped by the value of statement 6
      (fs6t, fs6f) = filter2((lambda fs: 6 not in fs), solve(6))
      
      # if statement 6 is false, we don't have to worry about the 5 statement version
      printf("false statements = {fs6f}")
      
      # if statement 6 is true, the solution(s) must be the same as the 5 statement version
      fs5 = list(solve(5))
      if fs5 == fs6t:
        printf("false statements = {fs6t}")
      

      Solution: Statements 3, 4, 6 are false.

      So statements 1, 2, 5 are true.

      1. (true) “in this list there is a true statement and a false statement whose numbers add up to give the number of a false statement”.
      There are two: 1 + 3 = 4, 2 + 4 = 6.

      2. (true) “either statement 4 is false, or there are three consecutive true statements”.
      4 is false; there are not three consecutive true statements.

      3. (false) “the number of the last false statement subtracted from the product of the numbers of the first and last true statements gives the number of a statement which is false”.
      1×5 − 6 = −1, is not the number of a statement.

      4. (false) “the number of even numbered true statements equals the number of false statements”.
      There is 1 even numbered true statement, and there are 3 false statements.

      5. (true) “one of the first and last statements is true and the other is false”.
      1 is true; 6 is false.

      6. (false) “When I first sent this problem to the editor, thanks to a typing error no sixth statement was included. However the answer to the following question was the same then as it is now: Which statements are false”.
      The first part could be false (there was no error). But if it was true there are no solutions given only the first 5 statements, so the answer to the puzzle could not be the same if the 6th statement was included.

      Like

  • Unknown's avatar

    Jim Randell 12:49 pm on 3 July 2022 Permalink | Reply
    Tags:   

    Brain-Teaser 120: Rugby results 

    From The Sunday Times, 14th July 1963 [link]

    At a certain stage in our all-against-all Rugby football competition the table of results read as follows. There had been no matches in which neither side had scored at all:

    What was the result of the match between A and B?

    Note: This problem was set when a try was worth 3 points, a penalty goal 3 points and a converted try 5 points. Match points are on the usual basis of 2 for a win and 1 for a draw.

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

    [teaser120]

     
    • Jim Randell's avatar

      Jim Randell 12:50 pm on 3 July 2022 Permalink | Reply

      There are certain scores which are not possible to make from combinations of 3 and 5 points.

      This Python program uses the [[ express() ]] function from the enigma.py library to determine what they are. We then use the [[ Football() ]] helper class to determine possible match outcomes and scorelines.

      It runs in 89ms.

      Run: [ @replit ]

      from enigma import (express, irange, peek, Football, digit_map, cache)
      
      # find scores that cannot be made from multiples of 3 and 5
      invalid = set(n for n in irange(0, 18) if not peek(express(n, [3, 5]), default=None))
      
      # scoring system (2 points for a win, 1 for a draw)
      football = Football(points=dict(w=2, d=1))
      
      # digits used in the table (10=A, 13=D, 15=F, 16=G, 18=I)
      d = digit_map(0, 18)
      
      # the table, goals for/against
      (table, gf, ga) = (dict(played="34432", points="55420"), "DGF53", "8AI88")
      
      # check for a valid score (and cache as we go along)
      @cache
      def valid(s):
        # unplayed?
        if s is None: return True
        # otherwise:
        # (0, 0) is not allowed
        if s == (0, 0): return False
        # reject invalid scores
        if invalid.intersection(s): return False
        # looks OK
        return True
      
      # find possible match outcomes
      for (ms, _) in football.substituted_table(table, d=d):
        # find possible scorelines
        for ss in football.substituted_table_goals(gf, ga, ms, d=d, valid=valid):
          # output solution
          football.output_matches(ms, ss, teams="ABCDE")
      

      Solution: The result of the A vs B match was a 5-3 win for A.

      The full results are:

      A vs B = 5-3
      A vs C = 5-5
      A vs D = 3-0
      A vs E = (unplayed)
      B vs C = 5-5
      B vs D = 5-0
      B vs E = 3-0
      C vs D = 0-5
      C vs E = 5-3
      D vs E = (unplayed)

      Like

  • Unknown's avatar

    Jim Randell 4:40 pm on 1 July 2022 Permalink | Reply
    Tags:   

    Teaser 3119: Hidden powers 

    From The Sunday Times, 3rd July 2022 [link] [link]

    My grandson is studying “History since the Battle of Hastings”. I made him a game, which consisted of a row of nine cards, each with a different non-zero digit on it. Throw a standard die, note the number of spots displayed, count that number of places along the row and pause there. Throw the die again, move the corresponding number of places further along and pause again. Repeat this until you come off the end of the row, noting the digit or digits you paused on and put these together in the same order, to produce a number.

    Keeping the cards in the same order I asked my grandson to try to produce a square or cube or higher power. He eventually discovered that the lowest possible such number was equal to the number of one of the years that he had been studying.

    What is the order of the nine digits along the row?

    [teaser3119]

     
    • Jim Randell's avatar

      Jim Randell 5:54 pm on 1 July 2022 Permalink | Reply

      It took me a little while to understand how this puzzle is supposed to work.

      And despite my initial misgivings there is only one possible arrangement of digits that can lead to the situation described.

      The following Python program isn’t very quick (it tries all possible arrangements of digits), but it does find the required arrangement.

      The code to generate powers is adapted from Teaser 683 and Enigma 1777.

      It runs in 669ms.

      Run: [ @replit ]

      from enigma import (Primes, nsplit, subsets, irange, tuples, printf)
      
      # generate powers (x^y) in numerical order, x >= 0, y >= 2,
      def powers():
        # powers less than 2^2
        yield 0
        yield 1
        # powers from 2^2 upwards
        base = { 2: 2 }
        power = { 2: 4 }
        maxp = 2
        primes = Primes(35, expandable=1).generate(maxp + 1)
        while True:
          # find the next power
          n = min(power.values())
          yield n
          # what powers are involved
          ps = list(p for (p, v) in power.items() if v == n)
          # increase the powers
          for p in ps:
            base[p] += 1
            power[p] = pow(base[p], p)
          # do we need to add in a new power?
          if maxp in ps:
            maxp = next(primes)
            base[maxp] = 2
            power[maxp] = pow(2, maxp)
      
      # collect candidate powers
      pows = list()
      for n in powers():
        if n > 2022: break
        # split the power into digits
        ds = nsplit(n)
        # no 0 digits, or repeated digits
        if 0 in ds or len(set(ds)) != len(ds): continue
        pows.append((n, ds))
      
      # check powers that can be made with an arrangement of digits
      def play(ss):
        # find powers that can be made
        ns = list()
        for (n, ds) in pows:
          # find indices of the digits
          js = list(ss.index(d) for d in ds)
          # check entry and exit
          if js[0] > 5 or js[-1] < 3: continue
          js.insert(0, -1)
          # and corresponding dice throws
          ts = list(y - x for (x, y) in tuples(js, 2))
          if min(ts) < 1 or max(ts) > 6: continue
          # check the power is permissible
          if n < 1066: return
          ns.append(n)
        # return the list of powers
        return ns
      
      # consider possible arrangements of digits
      for ss in subsets(irange(1, 9), size=len, select="P"):
        ns = play(ss)
        if ns:
          printf("{ss} -> {ns}")
      

      Solution: The order of the digits is: 1, 9, 8, 7, 5, 2, 3, 4, 6.

      And the smallest (and onliest) power which can be made by rolling the die is 1936, with rolls of (1, 1, 5, 2, *).


      Although the puzzle talks about the lowest power that can be generated, it turns out it is the only (non-trivial) power that can be generated with the required arrangement of cards.

      The puzzle will continue to work in the future, until the year 4913 (= 17³) is incorporated into the history book.

      Like

      • Jim Randell's avatar

        Jim Randell 10:13 am on 2 July 2022 Permalink | Reply

        Here’s a faster (but slightly longer) program. Instead of just trying all possible arrangements of digits, it recursively constructs arrangements that cannot give powers less than 1066, and then finds which greater powers can be made.

        It runs in 174ms.

        Run: [ @replit ]

        from enigma import (Primes, nsplit, subsets, tuples, diff, update, printf)
        
        # generate powers (x^y) in numerical order, x >= 0, y >= 2,
        def powers():
          # powers less than 2^2
          yield 0
          yield 1
          # powers from 2^2 upwards
          base = { 2: 2 }
          power = { 2: 4 }
          maxp = 2
          primes = Primes(35, expandable=1).generate(maxp + 1)
          while True:
            # find the next power
            n = min(power.values())
            yield n
            # what powers are involved
            ps = list(p for (p, v) in power.items() if v == n)
            # increase the powers
            for p in ps:
              base[p] += 1
              power[p] = pow(base[p], p)
            # do we need to add in a new power?
            if maxp in ps:
              maxp = next(primes)
              base[maxp] = 2
              power[maxp] = pow(2, maxp)
        
        # collect candidate powers (disallowed and allowed)
        (xpows, pows) = (list(), list())
        for n in powers():
          if n > 2022: break
          # split the power into digits
          ds = nsplit(n)
          # no 0 digits, or repeated digits
          if 0 in ds or len(set(ds)) != len(ds): continue
          (xpows if n < 1066 else pows).append((n, ds))
        
        # check if digit sequence <ds> can be made from arrangement <ns>
        def play(ns, ds):
          # find indices of the digits
          js = list(ns.index(d) for d in ds)
          # check entry and exit
          if js[0] > 5 or js[-1] < 3: return False
          # check corresponding dice throws (1-6)
          js.insert(0, -1)
          return all(0 < y - x < 7 for (x, y) in tuples(js, 2))
        
        # generate arrangements <ns> that don't allow sequences <xs> to be made
        # ns = digit arrangement
        # xs = disallowed sequences
        # ss = allocated digits
        # js = indices of empty slots
        def solve(ns, xs, ss=set(), js=None):
          if not xs:
            yield ns
          else:
            # find the shortest sequences with the fewest missing digits
            fn = lambda ds: (len(set(ds).difference(ss)), len(ds))
            ds = min(xs, key=fn)
            # choose placements for the missing digits
            ms = diff(ds, ss)
            if not js: js = set(j for (j, n) in enumerate(ns) if n is None)
            for ks in subsets(js, size=len(ms), select="P"):
              ns_ = update(ns, ks, ms)
              if not play(ns_, ds):
                # solve the rest
                yield from solve(ns_, xs.difference([ds]), ss.union(ms), js.difference(ks))
        
        # find layouts that do not permit sequences from xpows
        for ns in solve([None] * 9, set(ds for (_, ds) in xpows)):
          # look for permissible powers
          ps = list(n for (n, ds) in pows if play(ns, ds))
          if ps:
            printf("{ns} -> {ps}")
        

        Liked by 1 person

    • Frits's avatar

      Frits 11:28 pm on 1 July 2022 Permalink | Reply

      Reusing my code of Teaser 683 and using Jim’s early performance enhancement (also considering number 1 as a power).

         
      from itertools import permutations
      
      def prime_factors(n):
        i = 2
        factors = []
        while i * i <= n:
          if n % i:
            i = 3 if i == 2 else i + 2
          else:
            n //= i
            factors.append(i)
        if n > 1:
          factors.append(n)
           
        return factors
          
      def is_a_power(n):
        pf = prime_factors(n)
        if len(pf) < 2: 
          return False
           
        # occurences of digits  
        oc = {pf.count(x) for x in set(pf)}
        if mult_gcd(list(oc)) == 1:
          return False
        return True
       
      # calculate greatest common divisor of multiple numbers  
      def mult_gcd(li):
        if len(li) == 1:
          return li[0]
       
        for i in range(len(li) - 1):
          a = li[i]
          b = li[i+1]
          while b:
            (a, b) = (b, a % b)
           
          if a == 1: return a
           
          li[i+1] = a
        return a  
       
      # generate powers, stop if you encounter one less than 1067    
      def solve(k, s, pw=0, ns=[], rs=set()):
        if k > 0:
          # year must be before 2023
          if k == 1 and pw > 202:
            return []
          ls = len(s)
          for i in range(6):
            # check if you come off the end of the row
            if i > ls - 1: break
            p = 10 * pw + s[i]
            if p in pows and ls - i < 7:
              return [p]
            
            # recurse if worthwhile
            if p in powstart:  
              res = solve(k - 1, s[i + 1:], p, ns + [i + 1], rs)  
              if len(res) == 1:
                rs.add(res[0])
                if res[0] < 1067:
                  return []  # no solution
          
          # if we didn't encounter a low power return list of powers
          return list(rs)    
          
      # collect candidate powers
      pows = {i for i in range(1, 2023) if is_a_power(i) and \
              len(s:= str(i)) == len(set(s)) and "0" not in s}  
      
      powstart = {int(str(p)[:i]) for p in pows for i in range(1, 4)}
      
      # consider possible arrangements of digits
      for perm in permutations(range(1, 10)):
        # [performance] single digit powers cannot be in slots 3, 4, 5
        if {1, 4, 8, 9}.intersection(perm[3:6]): continue
      
        r = sorted(solve(4, perm, rs=set()))
        if r and r[0] > 1066:
          print(perm, "number", r[0])
      

      Like

      • Frits's avatar

        Frits 9:29 am on 2 July 2022 Permalink | Reply

        Python 3.9 introduced multiple arguments version of math.gcd so math.gcd() can be used instead of mult_gcd().

        Like

    • Frits's avatar

      Frits 2:50 pm on 2 July 2022 Permalink | Reply

      Just as with Jim ‘s third program only calling solve() for 49 permutations.

      Doing the checks for 2-digit and 3-digit powers in a general way is a bit slower.
      Also storing digit positions in a dictionary to minimize index() calls is a bit slower.

         
      from itertools import permutations
      from math import gcd
      
      def prime_factors(n):
        i = 2
        factors = []
        while i * i <= n:
          if n % i:
            i = 3 if i == 2 else i + 2
          else:
            n //= i
            factors.append(i)
        if n > 1:
          factors.append(n)
           
        return factors
          
      def is_a_power(n):
        pf = prime_factors(n)
        if len(pf) < 2: 
          return False
           
        # occurences of digits  
        oc = {pf.count(x) for x in set(pf)}
        if gcd(*oc) == 1:
          return False
        return True
       
      # generate powers, stop if you encounter one less than 1067    
      def solve(k, s, pw=0, ns=[], rs=set()):
        if k > 0:
          # year must be before 2023
          if k == 1 and pw > 202:
            return []
          ls = len(s)
          for i in range(6):
            # check if you come off the end of the row
            if i > ls - 1: break
            p = 10 * pw + s[i]
            if p in pows and ls - i < 7:
              return [p]
            
            # recurse if worthwhile
            if p in powstart:  
              res = solve(k - 1, s[i + 1:], p, ns + [i + 1], rs)  
              if len(res) == 1:
                rs.add(res[0])
                if res[0] < 1067:
                  return []  # no solution
          
          # if we didn't encounter a low power return list of powers
          return list(rs)    
          
      # collect candidate powers
      pows = {i for i in range(1, 2023) if is_a_power(i) and \
              len(s:= str(i)) == len(set(s)) and "0" not in s}  
      
      powstart = {int(str(p)[:i]) for p in pows for i in range(1, 4)}
      
      # 2-digit powers
      pows2 = [(x // 10, x % 10) for x in pows if 9 < x < 100]
      
      # 3-digit powers
      pows3 = [(x // 100, (x // 10) % 10, x % 10) for x in pows if 99 < x < 1000]
      
      # consider possible arrangements of digits
      for perm in permutations(range(1, 10)):
        # [performance] single digit powers cannot be in slots 3, 4, 5
        if {1, 4, 8, 9}.intersection(perm[3:6]): continue
        
        # check 2-digit powers
        for a, b in pows2:
          posa = perm.index(a)
          posb = perm.index(b)
          # can we make the power in 2 jumps and then come off the end of the row?
          if 0 < (posb - posa) < 7 and posa < 6 and posb > 2:
            break
        else:  # no break   
          # check 3-digit powers
          for a, b, c in pows3:
            posa = perm.index(a)
            posb = perm.index(b)
            posc = perm.index(c)
            # can we make the power in 3 jumps and 
            # then come off the end of the row?
            if not (0 < (posb - posa) < 7 and 0 < (posc - posb) < 7): continue
            if posa < 6 or posc > 2:
              break    
          else:  # no break   
            # no 2- or 3-digit power can be formed
            r = sorted(solve(4, perm, rs=set()))
            if r and r[0] > 1066:
              print(perm, "number", r[0])
      

      Like

    • Frits's avatar

      Frits 4:04 pm on 2 July 2022 Permalink | Reply

      Without recursion.

         
      from itertools import permutations
      from enigma import mgcd
      
      def prime_factors(n):
        i = 2
        factors = []
        while i * i <= n:
          if n % i:
            i = 3 if i == 2 else i + 2
          else:
            n //= i
            factors.append(i)
        if n > 1:
          factors.append(n)
           
        return factors
          
      def is_a_power(n):
        pf = prime_factors(n)
        if len(pf) < 2: 
          return False
           
        # occurences of digits  
        oc = {pf.count(x) for x in set(pf)}
        if mgcd(*oc) == 1:
          return False
        return True
       
      # collect candidate powers
      pows = {i for i in range(1, 2023) if is_a_power(i) and \
              len(s:= str(i)) == len(set(s)) and "0" not in s}  
      
      # powers grouped by number of digits
      pws = [[[int(x) for x in str(p)] for p in pows 
               if 10**i <= p < 10**(i + 1)] for i in range(1, 4)]
      
      # do powers exists >= 2000 ?
      exists2xxx = any(x for x in pws[2] if x[0] == 2)
      
      # consider possible arrangements of digits
      for perm in permutations(range(1, 10)):
        # [performance] single digit powers cannot be in slots 3, 4, 5
        if {1, 4, 8, 9}.intersection(perm[3:6]): continue
        
        for ps in pws[:-1]:
          for p in ps:
            pos = [perm.index(i) for i in p]
            # first digit reachable and then come off the end of the row?
            if not(pos[0] < 6 and pos[-1] > 2): continue
            # can we make the power in <len(pos)> jumps ...
            if any(y - x not in {1, 2, 3, 4, 5, 6} for x, y in zip(pos, pos[1:])):
              continue
              
            # we have found a valid 2- or 3-digit power
            break
          else: # no break  
            continue
            
          break   # break if break occurred    
        else:  # no break
          if not exists2xxx:
            # check position of 1
            if perm.index(1) > 5: continue
          
          # check 4-digit powers
          for p in sorted(pws[2]):
            pos = [perm.index(i) for i in p]
            # it is enough to only check for increasing numbers
            if pos != sorted(pos): continue
            
            print(perm, "number", "".join(str(x) for x in p))
            break
      

      Like

    • Frits's avatar

      Frits 5:48 pm on 8 July 2022 Permalink | Reply

         
      from itertools import permutations
      from functools import reduce
      from math import gcd
      
      def prime_factors(n):
        i = 2
        factors = []
        while i * i <= n:
          if n % i:
            i = 3 if i == 2 else i + 2
          else:
            n //= i
            factors.append(i)
        if n > 1:
          factors.append(n)
           
        return factors
          
      def is_a_power(n):
        if n == 1: return [1]
        pf = prime_factors(n)
        if len(pf) < 2: 
          return False
           
        # occurences of digits  
        oc = {pf.count(x) for x in set(pf)}
        if gcd(*oc) == 1:
          return False
        return True
      
      # group type
      gtype = lambda i: "first" if i == 0 else "middle" if i == 1 else "last"
              
      # collect all candidate powers
      pows = {i for i in range(1, 2023) if is_a_power(i) and 
              len(s:= str(i)) == len(set(s)) and "0" not in s} 
      
      # powers grouped by number of digits
      pws = [set(str(p) for p in pows 
               if 10**i <= p < 10**(i + 1)) for i in range(4)]
               
      # return possible arrangements of digits in group <grp> for 2-digit powers
      def arrangements(grp):
        return [p for p in permutations(grp) if all(fdgt + sdgt not in pws[1] 
                for fdgt, sdgt in [(p[0], p[1]), (p[0], p[2]), (p[1], p[2])])
               ]
      
      # return all numbers per group that don't occur in other groups
      def calc_known(grp):
        return [[x for x in grp[i] 
                if all(x not in t for t in 
                      [y for j, y in enumerate(grp) if j != i])] 
                for i in range(3)]
       
      # for all combinations in <lst> check if a 3-digit power can be made
      # with a digit in the last group
      def check_last_group(grp, lst):
        forbidden = [set() for _ in range(len(lst))]
        for i, n in enumerate(lst):
          # for all combinations in <lst> 
          for x in [(n[0] + n[1]), (n[0] + n[2]), (n[1] + n[2])]:
            for p in pws[2]:
              if x in {p[:-1]}:
                # x + p[-1] is 3-digit power so p[-1] is forbidden in last group
                forbidden[i].add(p[-1])
              
        # return numbers in the last group which are forbidden for all <lst> entries
        return reduce((lambda x, y: x & y), map(set, forbidden))
      
      # maintain groups if all numbers in a group are known
      def maintain_groups(grp, fixed):
        for i, fix in enumerate(fixed):
          # all numbers in a group are known?
          if len(fix) == 3:
            # remove other numbers from this group
            for n in [x for x in grp[i] if x not in fix]:
              print(f"{gtype(i)} group must be {','.join(fix)}:"
                    f" remove {n} from {gtype(i)} group")
            grp[i] = set(fix)
                  
      def print_groups(g):
        print("----- groups:", ",".join(sorted(g[0])), "--", 
                               ",".join(sorted(g[1])), "--", 
                               ",".join(sorted(g[2])), "-----")
      
      # maintain and print groups  
      def update(g):
        known = calc_known(g)
        maintain_groups(g, known)
        print_groups(g)
        return known
      
      
      print("-------- START -----------")
      
      print("split the nine digits in three groups of three digits")
      # list <g> contains candidate digits for each group
      g = [set("123456789"), set("123456789"), set("123456789")]
      print_groups(g)
      
      print(f"removing 1 from last group"
            f" as we need a 4-digit power 1xxx as an answer")
      g[2] -= {'1'}             # we need a 4-digit power
      
      print("removing", ", ".join(sorted(pws[0])), 
            "from middle group to prevent single digit powers")
      g[1] -= set(pws[0])       
      
      for x in calc_known(g)[0]:
        # can we can make a 2-digit power with <x>?
        for p in [p2 for p2 in pws[1] if p2[0] == x]:
          print("removing", p[1], "from middle group to prevent power", p)
          g[1].remove(p[1])         
      
      print_groups(g)
      
      # check middle group
      if len(g[1]) == 4:
        # see what happens if we remove <m> from middle group 
        for m in g[1]:
          middle = [x for x in g[1] if x != m]
          for m2 in middle:
            # if combination is a 2-digit power <m> must occur in middle group
            if m2 + m in pws[1]: 
              g[2] -= {m}
            if m + m2 in pws[1]: 
              g[0] -= {m}
         
          # get arrangements from <middle> without a 2-digit power
          lst = arrangements(middle)
          
          if len(lst) == 1:
            # we have only one arrangement for slots 3, 4 and 5 
            # can we can make a 3-digit power with this arrangement?
            arr = lst[0]
            for a in [(arr[0] + arr[1]), (arr[0] + arr[2]), (arr[1] + arr[2])]:
              # 3-digit powers that can be made from <a>
              tdp = [p3 for p3 in pws[2] if a in p3[:-1]]
              for p in tdp:
                # last digit of 3-digit power may not be used yet
                if p[-1] in (arr + ('1', )): continue
                # removing m has lead to a valid 3-digit power 
                print(f"removing {m} from the middle group can lead to"
                      f" power {p}, remove {m} from first and last group")
                g[0] -= {m}
                g[2] -= {m}
               
        # maintain and print groups
        known = update(g)
        
        # for every 3-digit power check if two of it's digits have to occur in two 
        # groups, if so we can remove the other digit from the other group
        first = 1
        for p in pws[2]:
          unknown = [i for i in range(3) if p[i] not in known[i]]  
          if len(unknown) != 1: continue
          
          i = unknown[0]
          if first: 
            print("known digits per group:", known)
            first = 0
          
          print(f"digits {' and '.join(p[j] for j in range(3) if j != i)}"
                f" of power {p} each have to occur in a different specific group:"
                f" remove {p[i]} from {gtype(i)} group")
          g[i] -= {p[i]}
        
        update(g)
        
        # get numbers in the last group which make a 3-digit power for all
        # valid combinations in the middle group
        for s in check_last_group(g, arrangements(g[1])):
          print(f"all valid combinations of middle group combined with the number"
                f" {s} lead to 3-digit power: remove {s} from last group")
          g[2] -= {s}
               
        update(g)
        print()
        
        # consider possible arrangements of digits
        for p0 in permutations(g[0]):
          for p1 in arrangements(g[1]):
            for p2 in permutations(g[2]):
              perm = p0 + p1 + p2
              # check 2-digit and 3-digit powers
              for ps in pws[1:-1]:
                for p in ps:
                  # the index positions of its digits in the row of cards
                  pos = [perm.index(i) for i in p]
                  # ensure that its first digit is reachable and that its
                  # last digit can be the final digit (with a throw of six)
                  if not(pos[0] < 6 and pos[-1] > 2): 
                    continue
                  # can we make the power in intervals of  1 - 6 jumps?
                  if any(y - x not in set(range(1, 7))
                        for x, y in zip(pos, pos[1:])):
                    continue
      
                  # we have found a valid 2-digit or 3-digit power
                  break
                else: 
                  continue
      
                break   # break again if break occurred    
              else:  
                # check 4-digit powers
                for p in sorted(pws[3]):
                  pos = [perm.index(i) for i in p]
                  
                  # checking for the correct digit order is sufficient because
                  # the highest gap between digits is five empty slots
                  if pos != sorted(pos): continue
                  
                  print(perm, "number", p)
                  break  # only print the lowest 4-digit power
      

      Like

  • Unknown's avatar

    Jim Randell 9:59 am on 30 June 2022 Permalink | Reply
    Tags:   

    Teaser 2720: Better tetra 

    From The Sunday Times, 9th November 2014 [link] [link]

    I have four tetrahedral dice. Each has four identical triangular faces and on each face of each die is one of the numbers 1, 2, 3 or 4 (repeats being allowed on a die). No die uses the same four numbers and, for each die, the sum of its four numbers is ten.

    I play a game with my friend. My friend chooses a die first and then I choose one of the remaining three dice. We each throw our chosen die and score the face-down number: sometimes it’s a draw, otherwise the higher number wins. I can choose my die so that I am always more likely to win.

    So my friend changes the rules. We now throw our chosen die twice, add the two numbers, and the higher total wins.

    Now he knows that there is one die he can choose that makes him more likely to win the game.

    What are the four numbers on the winning dice?

    The wording for this puzzle has been modified from the originally published version.

    [teaser2720]

     
    • Jim Randell's avatar

      Jim Randell 10:00 am on 30 June 2022 Permalink | Reply

      (See: [@youtube]).

      We assume that the dice are chosen in such a way that each player is aware of which die has been chosen. (So, for example, they may be different colours and arranged on the table, but not be hidden a bag and chosen by putting your hand in and selecting one at random).

      When we compare 2 dice, we say that one is “better” than the other, if it is more like to win a game against the other die.

      There are 5 possible dice (i.e. 5 possible decompositions of 10 into the numbers 1, 2, 3, 4), but the set only contains 4 of these.

      It turns out there is only one possible set of 4, where whatever die A chooses B can always choose a better die.

      And from this set we can look to see if there is die which is better when played against each of the other dice.

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

      Run: [ @replit ]

      from enigma import (decompose, subsets, multiset, product, printf)
      
      # return wins for X and Y in a game with k throws?
      def play(X, Y, k):
        # generate scores for X and Y
        (sX, sY) = (multiset.from_seq(sum(ss) for ss in subsets(ns, size=k, select="M")) for ns in (X, Y))
        # count wins for X, wins for Y
        wX = wY = 0
        for ((x, nx), (y, ny)) in product(sX.items(), sY.items()):
          if x > y:
            wX += nx * ny
          elif y > x:
            wY += nx * ny
        return (wX, wY)
      
      # is X better than Y in a game with k throws?
      def better(X, Y, k):
        (wX, wY) = play(X, Y, k)
        return wX > wY
      
      # generate possible dice
      ns = list(decompose(10, 4, increasing=1, sep=0, min_v=1, max_v=4))
      
      # choose a set of 4 dice, and solve the puzzle
      for ds in subsets(ns, size=4):
      
        # with 1 throw, whatever die A chooses, B can always choose a better die
        if not all(any(better(B, A, 1) for B in ds if B != A) for A in ds): continue
        printf("dice = {ds}")
      
        # with 2 throws, A can choose a die that is better than the others
        for A in ds:
          if not all(better(A, B, 2) for B in ds if B != A): continue
          printf("-> A = {A}")
      

      Solution: The best die in the second game is: (1, 3, 3, 3).


      The four dice are:

      (1, 1, 4, 4)
      (1, 3, 3, 3)
      (2, 2, 2, 4)
      (2, 2, 3, 3)

      And we have:

      (1, 1, 4, 4) is beaten by (2, 2, 2, 4)
      (2, 2, 2, 4) is beaten by (2, 2, 3, 3) and (1, 3, 3, 3)
      (2, 2, 3, 3) is beaten by (1, 3, 3, 3)
      (1, 3, 3, 3) is beaten by (1, 4, 4, 4)

      So whichever die is chosen first, there is always a choice from the other three that is better.

      Like

    • Frits's avatar

      Frits 5:54 pm on 30 June 2022 Permalink | Reply

      Apparently we can also compare lists with the less than operator in Python.

         
      from itertools import product
      from enigma import SubstitutedExpression
      
      # number of times x wins from y if die is thrown once
      play1 = lambda x, y: sum([p > q for p, q in product(x, y)])
      
      # number of times x wins from y if die is thrown twice
      def play2(x, y):
        # add the score of two throws
        x2 = [p + q for p, q in product(x, x)]
        y2 = [p + q for p, q in product(y, y)]
        
        return sum([p > q for p, q in product(x2, y2)])
      
      
      # the alphametic puzzle
      p = SubstitutedExpression(
        [
         "A <= B <= C",
         "10 - A - B - C = D",
         "C <= D",
         
         "E <= F <= G",
         "10 - E - F - G = H",
         "G <= H",
         "(A, B, C, D) < (E, F, G, H)",
         
         "I <= J <= K",
         "10 - I - J - K = L",
         "K <= L",
         "(E, F, G, H) < (I, J, K, L)",
         
         "M <= N <= O",
         "10 - M - N - O = P",
         "O <= P",
         "(I, J, K, L) < (M, N, O, P)",
         
         # with 1 throw, whatever die my friend chooses (x), 
         # I can always choose a better die (y), resulting in 4 wins
         "sum([any(play1(y, x) > play1(x, y) \
               for y in [(A,B,C,D), (E,F,G,H), (I,J,K,L), (M,N,O,P)] if y != x) \
               for x in [(A,B,C,D), (E,F,G,H), (I,J,K,L), (M,N,O,P)]]) == 4",
        ],
        answer="(A,B,C,D), (E,F,G,H), (I,J,K,L), (M,N,O,P)",
        env=dict(play1=play1),
        distinct="",
        digits=range(1, 5),
        reorder=0,
        verbose=0,    # use 256 to see the generated code
      )
      
      # check if my friend can win with the die thrown twice
      for _, s in p.solve():
        for s1 in s:
          if all(play2(s1, s2) > play2(s2, s1) for s2 in s if s1 != s2):
            print("answer:", s1)
      

      Like

  • Unknown's avatar

    Jim Randell 9:54 am on 28 June 2022 Permalink | Reply
    Tags: by: M C Breach   

    Brain-Teaser 724: Counting sheep … 

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

    In a primitive eastern country a shepherd was counting his
    sheep into four separate pens. In the first were 75 sheep, so he wrote in his records:

    In the second were 255 and he wrote:

    In the third were 183 and he wrote:

    After counting the sheep in the fourth pen he wrote:

    How many sheep were in the fourth pen?

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

    [teaser724]

     
    • Jim Randell's avatar

      Jim Randell 9:55 am on 28 June 2022 Permalink | Reply

      I think this puzzle is a bit open ended.

      I called the symbols A (for the triangle), D (for the box), and X (for the loop).

      For all we know DXA, ADX, XAD could be words that happen to mean 75, 255, 183.

      Or it could be some system with strange rules (like roman numerals, for instance).

      So, I made some assumptions:

      I assumed each symbol stands for a different non-negative integer value (but the same value whenever it appears). And as the same symbols appear in a different order for each of the given values I assumed position is important (as it is in our decimal system).

      My first thought was maybe it was just a substituted base system where each digit is represented by one of the symbols.

      For 255 to be represented by 3-digits the base must be 7 – 15.

      For 75 to be represented by 3-digits the base must be: 5 – 8.

      So we can look at values 75, 255, 183 in base 7 and 8:

      base 7: 75 → “135”; 255 → “513”; 183 → “351”
      base 8: 75 → “113”; 255 → “377”; 183 → “267”

      Only for the case of base 7 do the same 3 digits appear in different orders.

      And in this case the substitution: A=5, D=1, X=3, gives us:

      DAX = 153 [base 7] = 87 [base 10]

      So: 87 is certainly a possible answer (and it is the published answer).


      But there are other solutions where the positions represent different values, but not necessarily increasing powers of some base.

      For instance instead of the positions standing for (49, 7, 1) and the symbols being A=5, D=1, X=3, we swap them over to give:

      positions = (1, 5, 3); A=7, D=49, X=1
      DXA = 49×1 + 1×5 + 7×3 = 75
      ADX = 7×1 + 49×5 + 1×3 = 255
      XAD = 1×1 + 7×5 + 49×3 = 183
      DAX = 49×1 + 7×5 + 1×3 = 87

      But we can also use decreasing position values:

      positions = (39, 11, 7); A=6, D=0, X=3
      DXA = 0×39 + 3×11 + 6×7 = 75
      ADX = 6×39 + 0×11 + 3×7 = 255
      XAD = 3×39 + 6×11 + 0×7 = 183
      DAX = 0×39 + 6×11 + 3×7 = 87

      Note: this combination of positions is able to express all numbers greater than 59.

      positions = (117, 33, 21); A=2, D=0, X=1 (Note: symbols are 0, 1, 2):
      DXA = 0×117 + 1×33 + 2×21 = 75
      ADX = 2×117 + 0×33 + 1×21 = 255
      XAD = 1×117 + 2×33 + 0×21 = 183
      DAX = 0×117 + 2×33 + 1×21 = 87

      Note: this combination of positions can only be used to express multiples of 3.

      Or mixed positions:

      positions = (7, 31, 19); A=1, D=8, X=0
      DXA = 8×7 + 0×31 + 1×19 = 75
      ADX = 1×7 + 8×31 + 0×19 = 255
      XAD = 0×7 + 1×31 + 8×19 = 183
      DAX = 8×7 + 1×31 + 0×19 = 87

      Note: this combination of positions is able to express all numbers greater than 86.

      And we can also find viable answers of 159 or 267.

      positions = (31, 19, 7); A=8, D=0, X=1
      DXA = 0×31 + 1×19 + 8×7 = 75
      ADX = 8×31 + 0×19 + 1×7 = 255
      XAD = 1×31 + 8×19 + 0×7 = 183
      DAX = 0×31 + 8×19 + 1×7 = 159

      positions = (33, 21, 117); A=0, D=1, X=2 (Note: symbols are 0, 1, 2)
      DXA = 1×33 + 2×21 + 0×117 = 75
      ADX = 0×33 + 1×21 + 2×117 = 255
      XAD = 2×33 + 0×21 + 1×117 = 183
      DAX = 1×33 + 0×21 + 2×117 = 267

      These values can also be found by permuting the (49, 7, 1) positions of the base 7 representation.

      Like

      • Jim Randell's avatar

        Jim Randell 11:13 pm on 28 June 2022 Permalink | Reply

        Here is a simple program using the [[ SubstitutedExpression ]] solver from the enigma.py library to solve the puzzle as a standard positional base system (which is how the puzzle is intended).

        It runs in 65ms.

        from enigma import (SubstitutedExpression, map2str, printf)
        
        # consider bases 7 and 8
        for b in [7, 8]:
          # create the alphametic puzzle
          p = SubstitutedExpression(
            [ "DXA == 75", "ADX == 255", "XAD == 183" ],
            base=b,
            answer="DAX",
            verbose=0,
          )
          # solve it
          for (s, ans) in p.solve():
            # output solution
            printf("DAX={ans} [b={b}; {s}]", s=map2str(s, enc=""))
        

        Like

    • GeoffR's avatar

      GeoffR 10:06 am on 29 June 2022 Permalink | Reply

      % A Solution in MiniZinc
      include "globals.mzn";
      % test for numerical bases 7 and 8 only, as Jim's analysis
      % i.e. symbols A (for the triangle), D (for the box), and X (for the loop)
      
      var 1..7:D; var 1..7:A; var 1..7:X;
      
      constraint  % base 7
      (D < 7 /\ A < 7 /\ X < 7 
      /\ (7 * 7 * D + 7 * X + A == 75) 
      /\ (7 * 7 * A + 7 * D + X == 255) 
      /\ (7 * 7 * X + 7 * A + D == 183))
      \/  % base 8
      (  (8 * 8 * D + 8 * X + A == 75) 
      /\ (8 * 8 * A + 8 * D + X == 255)
      /\ (8 * 8 * X + 8 * A + D == 183));
      
      solve satisfy;
      
      output ["[D, A, X ] = " ++ show([D, A, X ]) ];
      
      % [D, A, X ] = [1, 5, 3]  
      % ----------
      % ==========
      % DXA = 135, ADX = 513, XAD = 351, DAX = 153 (base 7)
      % DAX (base 7) = 1*7*7 + 5*7 + 3 = 87 ( base 10)
      
      
      

      Like

  • Unknown's avatar

    Jim Randell 11:55 am on 26 June 2022 Permalink | Reply
    Tags:   

    Brain-Teaser 101: Midsummer birthday madness 

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

    Midsummer Day, 1962 (June 24) was my youngest grandson John’s first birthday, and I was then able to claim that my nine grandchildren were aged 0, 1, 2, 3, 4, 5, 6, 7, and 8 years old (neglecting, of course, the odd days). They were all born in June, and if they are arranged in birthday order through the month the following facts are true:

    John is the middle grandchild;

    The sum of the dates of the last four is an exact multiple of the sum of the dates of the first four;

    The sum of the ages of the last four is two-thirds of the sum of the ages of the first four;

    The sum of the years of birth of the first three is equal to the sum of the years of birth of the last three;

    The intervals between birthdays are 0, 1, 2, 3, 4, 5, 6, and 7 days, but not in that order;

    Also:

    My eldest son’s two daughters are exactly two years apart;

    The twins were born four hours apart;

    Two children are as old as their dates of birth.

    What was the date of birth of the grandchild born in 1954?

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

    There are now 700 Teaser puzzles available on the site.

    [teaser101]

     
    • Jim Randell's avatar

      Jim Randell 11:56 am on 26 June 2022 Permalink | Reply

      The ages are all different, so the twins must be born either side of midnight on the 24th, so John is one of the twins, and the other must have a birthday of the 25th. John is the youngest grandson, so his (younger) twin must be a girl.

      This Python program starts with the 24th in the middle of the order, and then chooses an ordering for the intervals to give the remaining dates. It then assigns the ages, and checks all the required conditions apply.

      It runs in 161ms.

      Run: [ @replit ]

      from enigma import (subsets, div, printf)
      
      # the middle birthday is on the 24th, and the separation between
      # birthdays is (0, 1, 2, 3, 4, 5, 6, 7), in some order
      for ss in subsets((0, 1, 2, 3, 4, 5, 6, 7), size=len, select="P"):
        # construct the dates
        d5 = 24
        (d4, d6) = (d5 - ss[3], d5 + ss[4])
        (d3, d7) = (d4 - ss[2], d6 + ss[5])
        (d2, d8) = (d3 - ss[1], d7 + ss[6])
        (d1, d9) = (d2 - ss[0], d8 + ss[7])
        if d1 < 1 or d9 > 30: continue
        ds = [ d1, d2, d3, d4, d5, d6, d7, d8, d9]
      
        # John's twin must be born on the 25th
        if 25 not in ds: continue
      
        # sum of the dates of the last 4 is an exact multiple of the sum of
        # the dates of the first four
        x = div(sum(ds[-4:]), sum(ds[:4]))
        if x is None or x < 2: continue
      
        # choose values for the ages (John is 1)
        for ns in subsets((0, 2, 3, 4, 5, 6, 7, 8), size=len, select="P", fn=list):
          # insert 1
          ns.insert(4, 1)
      
          # John's twin (age 0) was born on the 25th
          if ds[ns.index(0)] != 25: continue
      
          # the sum of the ages of the last four is 2/3 the sum of the ages
          # of the first four
          if 3 * sum(ns[-4:]) != 2 * sum(ns[:4]): continue
      
          # [exactly] 2 ages are the same as the date
          if sum(x == y for (x, y) in zip(ds, ns)) != 2: continue
      
          # the sum of the years of birth of the first three is the same as
          # the sum of the years of birth of the last three
          ys = list(1962 - x for x in ns[:5]) + list(1961 - x for x in ns[5:])
          if sum(ys[:3]) != sum(ys[-3:]): continue
      
          # find the 2 grandchildren that share a birthday
          i = ss.index(0)
          # neither can be John
          if i == 3 or i == 4: continue
          # the ages must differ by 2
          if abs(ns[i] - ns[i + 1]) != 2: continue
      
          # output solution
          for (d, n, y) in zip(ds, ns, ys):
            printf("{d:02d} June {y} -> age = {n}{s}", s=(' [*** SOLUTION ***]' if y == 1954 else ''))
          printf()
      

      Solution: The birthday of the grandchild born in 1954 is 11th June.

      The dates of birth and ages are:

      02 June 1960 → age = 2 (age 2 on the 2nd)
      07 June 1955 → age = 7 (age 7 on the 7th)
      11 June 1954 → age = 8 (answer)
      17 June 1958 → age = 4
      24 June 1961 → age = 1 (today, John’s birthday)
      25 June 1961 → age = 0 (John’s twin sister)
      28 June 1958 → ages = 3 & 5 (two granddaughters, exactly 2 years apart)
      30 June 1955 → age = 6

      The program prints two solutions because the granddaughters born on 28th June could appear in either order.

      Like

    • Frits's avatar

      Frits 3:09 pm on 29 June 2022 Permalink | Reply

         
      # dates: A,  B,  C,  D,  E,  F,  G,  H,  I  in order
      # ages : R,  S,  T,  U,  V,  W,  X,  Y,  Z  not necessarily in order
      
      '''
      Notation: XYZ stands for X + Y + Z
      
      The ages are all different, so the twins must be born either side of 
      midnight on the 24th, so John is one of the twins, and the other must
      have a birthday of the 25th. John is the youngest grandson, 
      so his (younger) twin must be a girl.
      
      E = 24, F = 25, V = 1, zero age is either W or X 
      
      After F (25) we can only have intervals 0, 2 and 3 otherwise I exceeds 30
      so I has to be 30 
      A has to be equal to E (24) - intervals 4, 5, 6 and 7, so A = 2
      
      Two children are as old as their dates of birth
      so R = A and S = B
      
      The sum of the ages of the last four is two-thirds of the sum of the
      ages of the first four, sum of all ages is 36, John in the middle is 1
      so we have to split the remaining 35 into a 21 and 14. 
      
      The sum of the years of birth of the first three is equal to the sum of 
      the years of birth of the last three, RST must be equal to XYZ + 3.
      
      2 * RSTU = 3 * WXYZ ==> 2 * RST + 2 * U = 3 * W + 3 * RST - 9
      ==> ST = 2 * U - 3 * W + 7  (using R = 2)
      
      RSTUVWXYZ = 36 
      
      R    [     ST      ]       V       [    XYZ      ]   
      2 +  2 * U - 3 * W + 7 + U + 1 + W + 2 * U - 3 * W + 6 = 36
      5 * (U - W) = 20 ==> U = W + 4
      (U, W) is (4, 0), (7, 3) or (8, 4)
      
      If W is not zero:
        X must be zero leaving (U, W) to be (7, 3) or (8, 4).
        Two daughters with same birthday are exactly two years apart.
        These must be Y and Z (not W as V is male and Y can't be 2)
        W + Y + Z = 14. if W is odd than Y and Z can't be two years apart.
        This leaves W = 4 and Y + Z = 10, only two years apart option for Y and Z
        is 4 and 6. This is impossible as W already is 4. So W = 0 and U = 4.
      '''
      
      # dates: 2,  B,  C,  D, 24, 25,  G,  H, 30  in order
      # ages : 2,  S,  T,  4,  1,  0,  X,  Y,  Z  not necessarily in order
      
      '''
      In order to have after F the intervals 2 and 3 either G or H must be 27 or 28
      so 109 <= FGHI <= 115.
      In order to have intervals 4, 5, 6, 7 we have 36 <= ABCD <= 46
      (using 2, 6, 11, 17 resp. 2, 9, 15, 20 for A, B, C, D). 
      
      So FGHI = 3 * ABCD (using multiplier three) leaving only values 37 and 38
      for ABCD (with FGHI resp. 111 and 114) .
      Using (2, 8, C, D) for (A, B, C, D) we get at least a sum of 39
      then (A, B, C, D) is either (2, 6, C, D) or (2, 7, C, D).
      
      As U = 4 RST must be 17, ST = 15 so R and S must use 7 and 8 
      leaving 3, 5 and 6 for X, Y and Z.
      
      As S is 7 or 8 and B is either 6 or 7 we can deduce R = 7, B = 7, S = 8.
      
      Using (2, 7, 13, 17) for (A, B, C, D) we get at a sum of 39 so 
      (A, B, C, D) is (2, 7, 11, 17). 
      '''
      
      # dates: 2,  7, 11, 17, 24, 25,  G,  H, 30  in order
      # ages : 2,  7,  8,  4,  1,  0,  X,  Y,  Z  not necessarily in order
      
      '''
      As a grandchild born in 1954 can only occur with age 8 and date before 25
      or age 7 and date after 24 we see the answer is 11th June 1954.
      '''
      

      Like

  • Unknown's avatar

    Jim Randell 3:55 pm on 24 June 2022 Permalink | Reply
    Tags:   

    Teaser 3118: Product dates 

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

    If a date during the current millennium is day D in month M during year (2000+N), it is said to be a product date if the product of D and M equals N (for example 11 February 2022).

    My daughter and I have been investigating the numbers of days from one product date to the next product date. I was able to establish the longest such interval L, while my daughter worked out the shortest such interval S. We were surprised to find that L is a whole number multiple of S.

    What is that multiple?

    [teaser3118]

     
    • Jim Randell's avatar

      Jim Randell 4:07 pm on 24 June 2022 Permalink | Reply

      This Python program runs in 61ms. (Internal run time is 862µs).

      Run: [ @replit ]

      from datetime import date
      from enigma import (Accumulator, irange, product, tuples, catch, div, printf)
      
      # generate "product dates" in the 21st century
      def generate():
        # consider each possible D, M pairing
        for (D, M) in product(irange(1, 31), irange(1, 12)):
          N = D * M
          d = catch(date, 2000 + N, M, D)
          if d is not None:
            yield d
      
      # find the maximum and minimum separations between consecutive dates
      rs = Accumulator.multi(fns=[max, min], collect=1)
      for (a, b) in tuples(sorted(generate()), 2):
        d = (b - a).days
        rs.accumulate_data(d, (a, b))
      
      # output solution
      (L, S) = (x.value for x in rs)
      d = div(L, S)
      printf("L/S = {d} [L={L} S={S}]")
      

      Solution: The multiple is 274.

      The longest interval is 4384 days (2236-12-28 → 2348-12-29 → 2360-12-30 → 2372-12-31).

      The shortest interval is 16 days (2030-01-30 → 2030-02-15).

      Like

  • Unknown's avatar

    Jim Randell 8:56 am on 23 June 2022 Permalink | Reply
    Tags: ,   

    Teaser 2753: No question about it! 

    From The Sunday Times, 28th June 2015 [link] [link]

    “Sign Works” make rectangular signs of all sizes. Pat ordered a sign for the pub with the following instructions:

    “The length must be twice the width. Furthermore, the dimensions should be such that if you take its length (in centimetres), square the sum of its digits and take away the length itself, then you get the width. On the other hand, if you take its width (in centimetres), square the sum of its digits and take away the width itself, then you get the length”.

    This was enough information for the sign-maker to calculate the dimensions of the sign.

    What were they?

    [teaser2753]

     
    • Jim Randell's avatar

      Jim Randell 8:57 am on 23 June 2022 Permalink | Reply

      This is very straightforward.

      from enigma import (irange, inf, dsum, sq, printf)
      
      for width in irange(1, inf):
        length  = 2 * width
        if width + length == sq(dsum(width)) == sq(dsum(length)):
          printf("width={width} length={length}")
          break
      

      Solution: The sign was 27 cm × 54 cm.

      Like

    • GeoffR's avatar

      GeoffR 9:30 am on 23 June 2022 Permalink | Reply

      
      % A Solution in MiniZinc
      include "globals.mzn";
      
      var 1..9:a; var 1..9:b; % length digits
      var 1..9:c; var 1..9:d; % width digits
      
      % Length and width of sign ( assumed 2 digits)
      var 10..99:L == 10*a + b; 
      var 10..99:W == 10*c + d;
      
      constraint L == 2 * W;
      
      constraint W == (a + b) * (a + b) - L;
      
      constraint L == (c + d) * (c + d) - W;
      
      solve satisfy;
      
      output [ "Length(cm) = " ++ show(L) ++ ", Width(cm) = " ++ show(W) ];
      
      % Length(cm) = 54, Width(cm) = 27
      % ----------
      % ==========
      
      
      
      

      Like

  • Unknown's avatar

    Jim Randell 11:16 am on 21 June 2022 Permalink | Reply
    Tags:   

    Brain-Teaser 719: Wotta-Woppa! 

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

    On the Island of Imperfection there are three tribes; the Pukkas who always tell the truth, the Wotta-Woppas who never tell the truth, and the Shilli-Shallas who make statements which are alternately true and false, or false and true. This story is about three inhabitants; Ugly, Stupid and Toothless, whose names give some indication of the nature of their imperfections.

    The island currency is a Hope. The weekly wage of a Pukka is between 10 and 19 Hopes, of a Shilli-Shalla between 20 and 29, and of a Wotta-Woppa between 30 and 39 (in each case a whole number of Hopes). They make three statements each, anonymously:

    A says: (1) B is a Wotta-Woppa. (2) My wages are 25% less than 20% more than one of the others. (3) I get 10 hopes more than B.

    B says: (1) The wages of one of us is different from the sum of those of the other two. (2) We all belong to the same tribe. (3) More of C‘s statements are true than mine.

    C says: (1) Ugly earns more than Toothless. (2) The wages of one of us are 15% less than the wages of another. (3) Stupid is a Shilli-Shalla.

    Find C‘s name, tribe and wages.

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

    [teaser718]

     
    • Jim Randell's avatar

      Jim Randell 11:17 am on 21 June 2022 Permalink | Reply

      This is similar to Puzzle 15 (and Puzzle 66).

      Some obvious initial analysis:

      Statement B1 (“The wages of one of us is different from the sum of those of the other two”) must always be true, as it it not possible for each of the three to have wages that are equal to the sum of the other two. (Who would have the largest value?)

      So B cannot be a Wotta-Woppa. (And so A1 is false).

      Which means B3 must be also be true. So C must make more true statements than B, so B cannot be a Pukka. Hence B is a Shilli-Shalla, and C is a Pukka. (And B2 is false).

      And so C3 must be true, and “Stupid” is a Shilli-Shalla.

      And as A1 is false, then A cannot be a Pukka, and A3 must also be false.

      I incorporated these findings into a generic solution to get a program that runs in 71ms.

      Run: [ @replit ]

      from enigma import (subsets, tuples, irange, product, div, intersect, printf)
      
      # Pukka - always tell the truth
      def P(ss):
        return all(ss)
      
      # Wotta-Woppa - never tells the truth
      def W(ss):
        return not any(ss)
      
      # Shilli-Shalla - alternates between true and false
      def S(ss):
        return all(a != b for (a, b) in tuples(ss, 2))
      
      # wages, by tribe
      wage = { P: irange(10, 19), S: irange(20, 29), W: irange(30, 39) }
      
      # assign tribes to the participants
      for ts in ((W, S, P), (S, S, P)):
        (A, B, C) = ts
      
        # B's statements
        Bs = [True, False, True]
        assert B(Bs)
      
        # assign names to the participants
        for ns in subsets("STU", size=3, select="P"):
          (nA, nB, nC) = ns
          if nC == 'S': continue  # not possible
          (iS, iT, iU) = (ns.index(x) for x in "STU")
      
          # assign wages to the participants
          for ws in product(wage[A], wage[B], wage[C]):
            (wA, wB, wC) = ws
      
            # check A's statements
            As = [False, div(10 * wA, 9) in {wB, wC}, wA == wB + 10]
            if not A(As): continue
      
            # check C's statements
            Cs = [
              ws[iU] > ws[iT],
              ts[iS] is S,
              bool(intersect([{ div(85 * x, 100) for x in ws }, ws])),
            ]
            if not C(Cs): continue
      
            # check B's 3rd statement
            assert Cs.count(True) > Bs.count(True)
      
            # output solution
            f = lambda x: x.__name__
            printf("A={A} {nA} {wA}; B={B} {nB} {wB}; C={C} {nC} {wC} [{As}; {Bs}; {Cs}]", A=f(A), B=f(B), C=f(C))
      

      Solution: C is Toothless, he is a Pukka, his wage is 17 Hopes.

      A is Ugly, he is a Wotta-Woppa, his wage is 31 – 39 Hopes.

      B is Stupid, he is a Shilli-Shalla, his wage is 20 Hopes.

      C‘s wage (17) is 15% less than that of B (20).

      Like

      • Frits's avatar

        Frits 3:04 pm on 21 June 2022 Permalink | Reply

        @Jim, You swapped the ranges of Wotta-Woppa and Shilli-Shalla.

        Like

      • Jim Randell's avatar

        Jim Randell 12:25 pm on 27 June 2022 Permalink | Reply

        Inspired by Frits’ solution (below) I wrote a solution that uses the [[ SubstitutedExpression ]] solver from the enigma.py library, and is just a restatement of the puzzle text with no analysis.

        The values assigned for the tribes make it easy to express the wages alphametically.

        This Python program runs in 73ms.

        Run: [ @replit ]

        from enigma import (SubstitutedExpression, irange, tuples, div, printf)
        
        #    statements  tribe  name  wage
        # A: a d g       p      x     ps
        # B: b e h       q      y     qt
        # C: c f i       r      z     ru
        #
        # statements: 0 (false), 1 (true)
        # tribe: 1 (P), 2 (SS), 3 (WW)
        # name: 1 (S), 2 (T), 3 (U)
        # wage (units): 0 - 9
        
        # check statements for a tribe
        def tribe(t, ss):
          if t == 1: return all(ss)
          if t == 2: return all(a != b for (a, b) in tuples(ss, 2))
          if t == 3: return not any(ss)
        
        # B1: one of the wages is different from the sum of the other two
        def B1(*ws):
          t = sum(ws)
          return any(w != t - w for w in ws)
        
        # C1: Ugly (3) earns more than Toothless (2)
        def C1(ns, ws):
          d = dict(zip(ns, ws))
          return d[3] > d[2]
        
        # C2: the wages of one of us is 85% the wages of another
        def C2(*ws):
          for w in ws:
            x = div(85 * w, 100)
            if x in ws: return True
          return False
        
        # expressions to evaluate
        exprs = [
          # tribes and statements
          "tribe({p}, [{a}, {d}, {g}])",
          "tribe({q}, [{b}, {e}, {h}])",
          "tribe({r}, [{c}, {f}, {i}])",
        
          # A1: "B is a Wotta-Woppa (3)" = {a}
          "int({q} == 3) = {a}",
          # A2: "My wages are [0.9] one of the others" = {d}
          "int(div({ps} * 10, 9) in { {qt}, {ru} }) = {d}",
          # A3: "I get 10 Hopes more than B" = {g}
          "int({ps} == {qt} + 10) = {g}",
          # B1: "The wages of one of us is different from the sum of the wages
          # of the other two" = {b}
          "int(B1({ps}, {qt}, {ru})) = {b}",
          # B2: "We all belong the same tribe" = {e}
          "int({p} == {q} == {r}) = {e}",
          # B3: "More of C's statements are true than mine" = {h}
          "int({c} + {f} + {i} > {b} + {e} + {h}) = {h}",
          # C1: "Ugly earns more than Toothless" = {c}
          "int(C1([{x}, {y}, {z}], [{ps}, {qt}, {ru}])) = {c}",
          # C2: "The wages of one of us are 0.85 the wages of another" = {f}
          "int(C2({ps}, {qt}, {ru})) == {f}",
          # C3: "Stupid (1) is a Shilli-Shalla (2)" = {i}
          "int({ {x}: {p}, {y}: {q}, {z}: {r} }[1] == 2) = {i}",
        ]
        
        # invalid digit / symbol assignments
        d2i = dict()
        for d in irange(0, 9):
          vs = set()
          if d > 1: vs.update('abcdefghi')
          if d < 1 or d > 3: vs.update('pqrxyz')
          d2i[d] = vs
        
        # create the puzzle
        p = SubstitutedExpression(
          exprs,
          distinct="xyz",
          d2i=d2i,
          env=dict(tribe=tribe, B1=B1, C1=C1, C2=C2),
          verbose=0,
        )
        
        # solve the puzzle and output solutions
        Ts = { 1: "Pukka", 2: "Shilli-Shalla", 3: "Wotta-Woppa" }
        Ns = { 1: "Stupid", 2: "Toothless", 3: "Ugly" }
        for s in p.solve():
          (a, b, c, d, e, f, g, h, i, p, q, r, s, t, u, x, y, z) = (s[k] for k in 'abcdefghipqrstuxyz')
          printf("A: {N:9s}  {T:13s}  {p}{s}  [{a} {d} {g}]", N=Ns[x], T=Ts[p])
          printf("B: {N:9s}  {T:13s}  {q}{t}  [{b} {e} {h}]", N=Ns[y], T=Ts[q])
          printf("C: {N:9s}  {T:13s}  {r}{u}  [{c} {f} {i}]", N=Ns[z], T=Ts[r])
          printf()
        

        Like

        • Frits's avatar

          Frits 1:31 pm on 29 June 2022 Permalink | Reply

          Nice idea to let tribes go from 1 to 3 so you can use the tribe value as a tens unit of the wage.

          Like

    • Frits's avatar

      Frits 3:30 pm on 21 June 2022 Permalink | Reply

      For all tribes the veracity of the first and third statement must be the same.

         
      from enigma import SubstitutedExpression
      
      # tribes 0 = Wotta-Woppa, 1 = Shilli-Shilla, 2 = Pukka 
      # names: 0 = Stupid, 1 = Toothless, 2 = Ugly
      
      #     stmnts  tribe  wages   name
      #      0/1    0/1/2         0/1/2
      # A:  D E D     P     UV      K
      # B:  F G F     Q     WX      L
      # C:  H I H     R     YZ      M
      # the alphametic puzzle
      p = SubstitutedExpression(
        [
          # if Wotta-Woppa all statements are false
          "P != 0 or D == E == 0",
          "Q != 0 or F == G == 0",
          "R != 0 or H == I == 0",
          
          # if Shilli-Shalla all statements are alternating true/false
          "P != 1 or D + E == 1",
          "Q != 1 or F + G == 1",
          "R != 1 or H + I == 1",
          
          # if Pukka all statements are true
          "P != 2 or D == E == 1",
          "Q != 2 or F == G == 1",
          "R != 2 or H == I == 1",
          
          # ranges: Pukka 10-19, Shilli-Shalla 20-29 and Wotta-Woppa 30-39
          "(P != 0 or U == 3) and (P != 1 or U == 2) and (P != 2 or U == 1)",
          "(Q != 0 or W == 3) and (Q != 1 or W == 2) and (Q != 2 or W == 1)",
          "(R != 0 or Y == 3) and (R != 1 or Y == 2) and (R != 2 or Y == 1)",
          
          # A1: B is a Wotta-Woppa
          "(F == G == 0) == D",
          # A2: My wages are 25% less than 20% more than one of the others
          "(UV in {0.9 * WX, 0.9 * YZ}) == E",
          # A3: I get 10 hopes more than B
          "(UV == WX + 10) == D",
          
          # B1: The wages of one of us is different from the sum of those 
          # of the other two
          "(UV != WX + YZ or UV + WX != YZ or UV + YZ != WX) == F",
          # B2: We all belong to the same tribe
          "(P == Q == R) == G",
          # B3: More of C's statements are true than mine
          "(2* H + I >  2 * F + G) == F",
          
          
          # C1: Ugly earns more than Toothless
          "((UV if K == 2 else WX if L == 2 else YZ) > \
            (UV if K == 1 else WX if L == 1 else YZ)) == H",
          # C2: The wages of one of us are 15% less than the wages of another
          "((20 * UV in {17 * YZ, 17 * WX}) or \
            (20 * WX in {17 * YZ, 17 * UV}) or \
            (20 * YZ in {17 * UV, 17 * WX})) == I",
          # C3: Stupid is a Shilli-Shalla.
          "((K == 0 and P == 1) or \
            (L == 0 and Q == 1) or \
            (M == 0 and R == 1)) == H",
        ],
        answer="(D, E, F, G, H, I), (P, Q, R), (UV, WX, YZ), (K, L, M)",
        d2i=dict([(0, "UWY")] +
                 [(2, "DEFGHI")] +
                 [(3, "DEFGHIKLMPQR")] +
                 [(k, "DEFGHIKLMPQRUWY") for k in range(4, 10)]),
        distinct="KLM",
        verbose=0,
      )
      
      # print answers
      for (_, ans) in p.solve():
        print(f"{ans}")
      

      Like

  • Unknown's avatar

    Jim Randell 3:11 pm on 19 June 2022 Permalink | Reply
    Tags:   

    Brain-Teaser 77: Ali’s counter move 

    From The Sunday Times, 16th September 1962 [link]

    Ali and Baba are playing the Chay game, Ali has a bag containing 12 numbered tickets, 3 each of numbers 1, 2, 3, 4 (all numbers represented by strokes). Baba has 6 double-sided counters containing the same 12 numbers, and a board of 6 squares.

    Ali draws 6 tickets from his bag one at a time, calling out the number as he does so. At each call Baba selects a counter with that number on one of its sides and places it face up in a square. If in 6 calls he fills 6 squares he wins. Once a counter is laid it stays. The counter-couplings are so arranged that 5 squares could always be filled if the numbers were known beforehand.

    But, unnoticed by Baba, Ali has slyly added 1 stroke each to 2 of his opponent’s counters. As a result, Baba frequently places no more than 3 or 4 counters, and at last comes a deal when, after Ali has called “Two”, “One”, he calls a third number and Baba cannot fill it.

    It is the last straw.

    Baba, having lost many pice and much temper, angrily examines the four remaining counters. Three of them are identical couplings!

    “Ah! wicked one”, he cries, “you have forged my counters!”. And, throwing them down, he clears for action.

    What couplings are on these 4 counters?

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

    [teaser77]

     
    • Jim Randell's avatar

      Jim Randell 3:14 pm on 19 June 2022 Permalink | Reply

      This Python program considers possible sets of counters that meet the specified conditions, and looks at ways to fake two of the counters so that there are 3 counters the same. We then match two of the fake set to the calls “1” and “2” and look to see if there is a third call that cannot be matched from the remaining counters. (It is a good opportunity to do some work with multisets).

      This Python program runs in 295ms.

      Run: [ @replit ]

      from enigma import (multiset, irange, subsets, product, union, printf)
      
      # update multiset <m>, by removing values in <rem>, and adding values in <add>
      def update(m, rem=None, add=None):
        if rem:
          m = m.difference(multiset.from_seq(rem))
        else:
          m = m.copy()
        if add:
          m.update_from_seq(add)
        return m
      
      # partition the multiset into <k>-tuples
      # return a multiset of <k>-tuples
      def mpartitions(m, k, ss=[]):
        # are we done?
        if not m:
          yield multiset.from_seq(ss)
        else:
          # choose the next <k>-tuple
          for s in subsets(m, size=k, select="mC"):
            t = tuple(sorted(s))
            if (not ss) or not (t < ss[-1]):
              yield from mpartitions(update(m, rem=s), k, ss + [t])
      
      # the tickets
      tickets = multiset.from_seq([1, 2, 3, 4], count=3)
      
      # possible sequences of tickets
      tss = list(tickets.subsets(size=6))
      
      # possible counter displays
      def display(cs, ss=multiset()):
        if not cs:
          yield ss
        else:
          # choose the next key
          (a, b) = k = cs.peek()
          n = cs[k]
          # and choose the split (how many showing a, the rest show b)
          for x in irange(0, n):
            # remaining counters
            cs_ = cs.copy().remove(k, n)
            ss_ = ss.copy().update_from_pairs([(a, x), (b, n - x)])
            yield from display(cs_, ss_)
      
      # check this set of counters
      def check(cs, ts):
        # consider each possible display
        for x in display(cs):
          # remove ticket sequences with at least 5 matches
          ts = list(t for t in ts if len(x.intersection(t)) < 5)
          if not ts: return True
        return False
      
      # fake a counter by incrementing one side
      def fake1(x, y):
        yield (x, y + 1)
        if x < y: yield (x + 1, y)
      
      # fake a set of counters
      def fake(cs):
        # choose 2 counters
        for (a, b) in subsets(cs, size=2, select="mC"):
          # fake them
          for (fa, fb) in product(fake1(*a), fake1(*b)):
            # replace the originals with the fakes
            yield update(cs, rem=(a, b), add=(fa, fb))
      
      # consider possible counters
      for cs in mpartitions(tickets, 2):
        # check these counters can match at least 5 of all tickets sequences
        if not check(cs, tss): continue
        printf("counters = {cs}", cs=sorted(cs))
      
        # make the fake set of counters
        for fcs in fake(cs):
          # there should be 3 counters the same
          if all(v < 3 for v in fcs.values()): continue
          printf("-> faked = {fcs}", fcs=sorted(fcs))
      
          # choose 2 counters to match the calls "1" and "2"
          for (x, y) in subsets(fcs, size=2, select="mP"):
            if not (1 in x and 2 in y): continue
            # remaining counters
            rs = update(fcs, rem=(x, y))
            # is there a call that cannot be matched?
            if len(union(rs.keys())) == 4: continue
      
            # output solution (rs)
            printf("-> remaining = {rs}; [1 -> {x}, 2 -> {y}]", rs=sorted(rs))
            printf()
      

      Solution: The four remaining counters are: 2|4, 2|4, 2|4, 3|4.


      There is only one set of counters that meets the specified conditions:

      1|2, 1|3, 1|4, 2|3, 2|4, 3|4

      These are then faked by incrementing the 3rd and 4th counters to give:

      1|2, 1|3, 2|4, 2|4, 2|4, 3|4

      Which has 3 copies of the 2|4 counter.

      For the call of “1” the 1|3 counter is chosen. For the call of “2” the 1|2 counter is chosen.

      This leaves: 2|4, 2|4, 2|4, 3|4, which cannot match a call of “1”.

      Like

    • Frits's avatar

      Frits 12:10 pm on 21 June 2022 Permalink | Reply

      A lot easier to solve without using Python.

         
      # Suppose numbers in Ali's bag are a, b, c and d
      
      # ------ Check Baba's original set of counters -----------------
      # Can one counter have the same numbers on it?
      
      # Then fi we have counters a/a and a/x where x is b, c or d.
      # If Ali calls 3 a's and 3 x's then both his third a call and third x call 
      # can't be filled.   ===> each counter has 2 different numbers on it
      
      # Can 2 counters be the same?
      # Then fi we have counters a/b and a/b.
      # If Ali calls 3 a's followed by 3 b's then the last two b calls can't be
      # filled.   ===> no counter combination occurs more than once.
      
      # So Baba has to have 1/2, 1/3 and 1/4. He also needs to have 2/3 and 2/4
      # and finally 3/4.
      
      # so we have 1|2, 1|3, 1|4, 2|3, 2|4 and 3|4
      # --------------------------------------------------------------
      
      
      # In two counters a number is incremented by one, resulting in at least 
      # 3 counters with the same numbers x and y. This means the x|y counter must 
      # have been present in Baba's original set of counters.
      # x|y cannot be a counter like 1|z as there is only one option 1|(z-1) to 
      # produce an additional 1|z counter.   ===> 1|2, 1|3 and 1|4 are not x|y
      
      # x|y cannot be a counter like z|(z + 1) as there is only one option 
      # (z-1)|(z+1) to produce an additional z|(z + 1) counter.
      # ==> 2|3 and 3|4 are not x|y
      
      # so x|y is 2|4 and Baba's faked set being 1|2, 1|3, 2|4, 2|4, 2|4 and 3|4.
      # As Ali has called “Two” and “One” resp. 1|2 and 1|3 must have been placed
      # in a square.   ===> the four remaining counters are 2|4, 2|4, 2|4 and 3|4
      

      Like

  • Unknown's avatar

    Jim Randell 5:00 pm on 17 June 2022 Permalink | Reply
    Tags:   

    Teaser 3117: Stop me if you’ve heard this one 

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

    A square, a triangle and a circle went into a bar.

    The barman said: “Are you numbers over 18?”

    They replied: “Yes, but we’re under a million”

    The square boasted: “I’m interesting, because I’m the square of a certain integer”

    The triangle said: “I’m more interesting — I’m a triangular number, the sum of all the integers up to that same integer”

    The circle said: “I’m most interesting — I’m the sum of you other two”

    “Well, are you actually a circular number?”

    “Certainly, in base 1501, because there my square ends in my number exactly. Now, shall we get the drinks in?”

    The square considered a while, and said: “All right, then. You(‘)r(e) round!”

    In base 10, what is the circular number?

    [teaser3117]

     
    • Jim Randell's avatar

      Jim Randell 5:27 pm on 17 June 2022 Permalink | Reply

      Circular numbers are also called automorphic numbers [@wikipedia], and we have encountered them before. See: Enigma 49, Enigma 1736.

      There is a unique value for the “certain integer” (and so S, T, C).

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

      Run: [ @replit ]

      from enigma import (irange, inf, sq, tri, nsplit, zip_eq, join, printf)
      
      # consider possible "certain" integers
      for n in irange(6, inf):
        S = sq(n)
        T = tri(n)
        C = S + T
        if not (C < 1000000): break
        # consider the digits of C and C^2 in base 1501
        Cds = nsplit(C, base=1501)
        C2ds = nsplit(sq(C), base=1501)
        # does C^2 end with C?
        if zip_eq(Cds, C2ds, reverse=1):
          # output solution
          fmt = lambda ds: join(ds, sep=":", enc="{}")
          printf("n={n}: S={S} T={T} C={C} [C={Cds} C^2={C2ds}]", Cds=fmt(Cds), C2ds=fmt(C2ds))
      

      Solution: The circular number is 1027.

      Numbers up to 999,999 can be represented in base 1501 using 1- or 2- digits.

      And there are six 1- or 2- digit automorphic numbers in base 1501, which give potential values for C:

      0
      1
      475
      1027
      368220
      1884782

      The first two are too small. The last one is too big.

      Of the remaining three only 1027 gives a viable value for n, which is n = 26.

      i.e.

      S = sq(26) = 676
      T = tri(26) = 351
      C = 676 + 351 = 1027

      And in base 1501 we have:

      1027 = {1027}
      1027² = {702:1027}

      So (in base 1501) the final digit of C² is C.


      In fact, even if S and T are square and triangular numbers of (possibly) different integers, we can still work out a unique value for C.

      Like

      • Jim Randell's avatar

        Jim Randell 8:26 pm on 17 June 2022 Permalink | Reply

        Here is a faster version that doesn’t construct the base 1501 representations.

        It has an internal run time of 852µs.

        Run: [ @replit ]

        from enigma import (irange, inf, sq, tri, printf)
        
        # consider possible "certain" integers
        M = B = 1501
        for n in irange(6, inf):
          S = sq(n)
          T = tri(n)
          C = S + T
          if not (C < 1000000): break
          # consider the digits of C and C^2 in base 1501
          while not (C < M): M *= B
          if pow(C, 2, M) == C:
            # output solution
            printf("n={n}: S={S} T={T} C={C}")
        

        Liked by 1 person

        • Jim Randell's avatar

          Jim Randell 4:11 pm on 30 June 2022 Permalink | Reply

          I read up on how to find modular roots of integer-valued polynomials using Hensel lifting and the Chinese Remainder Theorem, and then wrote some code to implement it.

          Given the “certain integer” n, the following Python code constructs a polynomial function for f(n) = sq(C(n)) − C(n), and then finds the roots of this function modulo 1501^k (for k = 1, 2). We then check these roots give viable values of S, T, C.

          The internal runtime is 564µs, so it is faster than my simple approach above, but quite a lot more complicated.

          Run: [ @replit ]

          from enigma import (irange, prime_factor, gcd, lcm, egcd, cproduct, sq, tri, ndigits, arg, printf)
          
          # simplified CRT
          # solve: x = a (mod m); x = b (mod n)
          def crt1(a, b, m, n):
            g = gcd(m, n)
            if (a - b) % g != 0: return None
            (x, y, z) = (m // g, n // g, (m * n) // g)
            (u, v, _) = egcd(x, y)
            return (b * u * x + a * v * y) % z
          
          # general Chinese Remainder Theorem
          # solve: x = rs[i] (mod ms[i])
          def crt(rs, ms):
            assert len(rs) == len(ms) # or use zip(..., strict=1)
            x = mm = None
            for (r, m) in zip(rs, ms):
              if x is None:
                (x, mm) = (r, m)
              else:
                x = crt1(r, x, m, mm)
                if x is None: return None
                mm = lcm(m, mm)
            return x
          
          # find modular roots of a polynomial
          class PolyRootsMod(object):
          
            def __init__(self, f, cache=1):
              self.f = f
              self.cache = (dict() if cache else None)
          
            # find roots of <f> mod (<b>^<k>) using Hensel's lemma
            def hensel(self, b, k):
              if k == 0: return [0]
              assert k > 0
              bk = b**k
              # check the cache
              cache = self.cache
              if cache:
                rs = cache.get(bk)
                if rs is not None:
                  return rs
              # find the roots
              f = self.f
              k_ = k - 1
              bk_ = b**k_
              rs = list()
              for r in self.hensel(b, k_):
                for i in irange(b):
                  x = i * bk_ + r
                  if f(x) % bk == 0:
                    rs.append(x)
              # cache if necessary
              if cache is not None: cache[bk] = rs
              return rs
          
            # find roots of polynomial <p> mod <n>^<k>
            def roots_mod(self, n, k=1):
          
              # find roots for each prime factor of <n>^<k>
              (rs, bs) = (list(), list())
              for (f, e) in prime_factor(n):
                rs.append(self.hensel(f, e * k))
                bs.append(pow(f, e * k))
          
              # combine roots using CRT
              for xs in cproduct(rs):
                yield crt(xs, bs)
          
          
          B = arg(1501, 0, int)
          printf("[B={B}]")
          
          # start with polynomial functions: sq, tri, circ
          circ = lambda n: sq(n) + tri(n)
          
          # find k-digit automorphic values for C in base B
          f = lambda x: x * (x - 1)
          poly = PolyRootsMod(lambda n: f(circ(n)))
          for k in irange(1, ndigits(999999, base=B)):
            for n in poly.roots_mod(B, k):
              S = sq(n)
              T = tri(n)
              C = circ(n)
              if S < 18 or T < 18 or not (C < 1000000): continue
              if ndigits(C, base=B) != k: continue
              printf("S={S} T={T} C={C} [n={n}]")
          

          Like

      • Jim Randell's avatar

        Jim Randell 9:50 am on 18 June 2022 Permalink | Reply

        Here is a version adapted from my code for Enigma 49. It proceeds by constructing possible automorphic values for C and then calculating the corresponding “certain integer” n, and from that S and T. Although this is not as fast as my previous program.

        from enigma import (irange, ndigits, uniq, quadratic, sq, tri, printf)
        
        # generate <k> digit automorphic numbers (in base <b>)
        # i.e. x where the rightmost k digits of x^n are the digits of x
        # yield (<k>, <x>) where <k> in <ks>
        def automorphic(ks, b=10, n=2):
          K = max(ks)
          (k, xs, p, q) = (1, [0], 1, b)
          while not (k > K):
            f = (k in ks)
            xs_ = list()
            for d in irange(b):
              for x in xs:
                x_ = d * p + x
                if pow(x_, n, q) == x_:
                  if f: yield (k, x_)
                  xs_.append(x_)
            (k, xs, p, q) = (k + 1, xs_, q, q * b)
        
        # consider 1-, 2- digit automorphic numbers in base 1501
        d = ndigits(999999, base=1501)
        for C in uniq(x for (k, x) in automorphic(irange(1, d), 1501)):
          if C < 18 or not (C < 1000000): continue
          # calculate n: C = n(3n + 1)/2
          for n in quadratic(3, 1, -2 * C, domain="Z"):
            if n < 6: continue
            S = sq(n)
            T = tri(n)
            printf("C={C} S={S} T={T} [n={n}]")
        

        And here is a version which proceeds by constructing possible values for C using the technique given on the Wikipedia page for automorphic numbers [@wikipedia].

        from enigma import (irange, quadratic, sq, tri, printf)
        
        # Hensel's lemma - see [ https://en.wikipedia.org/wiki/Automorphic_number ]
        def hensel(poly, base, power):
          if power == 0: return [0]
          roots = hensel(poly, base, power - 1)
          rs = list()
          for r in roots:
            for i in irange(base):
              j = i * base ** (power - 1) + r
              x = poly(j) % (base ** power)
              if x == 0:
                rs.append(j)
          return rs
        
        # generate <k> digit automorphic numbers in base <b>
        # i.e. values of x where the rightmost k digits of x^2 are the digits of x
        def automorphic(k, b=10):
          poly = lambda x: x * x - x
          return hensel(poly, b, k)
        
        # consider 1-, 2- digit automorphic numbers in base 1501
        for k in (1, 2):
          for C in automorphic(k, 1501):
            if C < 18 or not (C < 1000000): continue
            # calculate n: C = n(3n + 1)/2
            for n in quadratic(3, 1, -2 * C, domain="Z"):
              if n < 6: continue
              S = sq(n)
              T = tri(n)
              printf("C={C} S={S} T={T} [n={n}]")
        

        Like

    • Robert Brown's avatar

      Robert Brown 6:19 pm on 17 June 2022 Permalink | Reply

      Hi Jim
      Your version of the text differs from the Sunday Times site. This asks “In base 10, what is the circular number?” You seem to have “what is the certain integer”.

      Like

      • Jim Randell's avatar

        Jim Randell 6:27 pm on 17 June 2022 Permalink | Reply

        Thanks. Of course if you know the “certain integer”, you can easily calculate the square, triangular and circular numbers.

        Interestingly, there is only one possible value for C, even if S and T are square and triangular numbers of different integers.

        Like

    • GeoffR's avatar

      GeoffR 8:32 pm on 17 June 2022 Permalink | Reply

      if S and T are both less than 1,000,000, then C is less than 2,000,000.

      The last two digits in base 1501 give a max value of 2,253,000.

      i.e. 1500 *1501 + 1500, which is adequate to define C.

      
      # Square and triangular numbers are less than 1,000,000
      sqs = [x * x for x in range(5, 1000)]
      tris = [ x * (x+1)// 2 for x in range(6, 1410)]
      
      CNum = []  # list to store circular numbers
      
      # consider possible (S, T) combinations
      for S in sqs:
        for T in tris:
          C = S + T
          C2 = C * C
          # only 2 digits in base 1501 are needed
          # for the maximum value of C
          num, rem = divmod(C2, 1501)
          if rem == C:
            if C not in CNum: CNum.append(C)
      
      for n in CNum:
          print(f"Circular number = {n}")
      
      

      Like

      • Jim Randell's avatar

        Jim Randell 9:42 pm on 17 June 2022 Permalink | Reply

        @GeoffR: This is very similar to the first program I wrote. And then I realised that S = n² and T = tri(n) for the same value of n, which makes things much faster.

        It does however lead to the same answer for C, so there is a harder problem lurking inside this puzzle.

        Also I think the text implies that all of S, T, C are less than a million (and more than 17).

        But C is 1- or 2-digits in base 1501, so C² could be up to 4 digits, and we need to check the final 2 digits if C is length 2.

        Like

    • Frits's avatar

      Frits 11:11 pm on 17 June 2022 Permalink | Reply

      Approaching it from the possible C values.

      I have assumed that when the number of digits in the remainder of (C^2 base 1501) is smaller than the number of digits in C we need to also check the last digits of the divisor of (C^2 base 1501).

         
      B = 1501
      
      # C = f(n) = n(3n + 1) // 2
      # f(n+1) - f(n) = (n + 1)(3(n + 1) + 1) // 2 - n(3n + 1) // 2
      #               = 2 + 3n
      
      C = 40        # as C >= 2 * 18
      for n in range(5, 817):  
        sC = str(C)
        lC = len(sC)
          
        C2 = C**2
        sC2 = str(C2)
        
        # next C
        C += (2 + 3 * n)
        
        # calculate remainder
        R1 = C2 % B
        lR1 = len(str(R1))
        
        # can we directly check all digits of C
        if lR1 >= lC:
          if str(R1)[-lC:] != sC: continue
        else: # len(R1) < lC
          # check last digits of C
          if sC[-lR1:] != str(R1): continue
          
          # check the digits of the divisor
          R2 = ((C2 - R1) // B) % B
          lR2 = len(str(R2))
          
          # check if digits in C correspond with the remainder of the divisor
          if sC[-lR1 + lR2:-lR1] != str(R2): continue
          
          # too lazy to code what happens if lR1 + lR2 < lC
          if lR1 + lR2 < lC: continue
          
        print("answer:", sC)
      

      Like

    • Frits's avatar

      Frits 2:39 pm on 18 June 2022 Permalink | Reply

      @Jim,

      If the base is 272 is C = 57 an answer as C2ds=(11, 257) ?

      Like

      • Jim Randell's avatar

        Jim Randell 2:55 pm on 18 June 2022 Permalink | Reply

        @Frits: 57 is not automorphic in base 272.

        57 = {57}
        57² = {11:257}

        You compare the “digits” as atomic values not strings, so the final digit of 57² is 257 and this is not the same as 57.

        But 17 and 256 are both 1-digit automorphic numbers in base 272:

        17 = {17}
        17² = {1:17}

        256 = {256}
        256² = {240:256}

        Like

    • Frits's avatar

      Frits 4:48 pm on 18 June 2022 Permalink | Reply

      There is no solution if the circular number has to be a round number as well (at least not in base 10).

      Like

    • Jim Randell's avatar

      Jim Randell 9:06 am on 21 June 2022 Permalink | Reply

      For an answer with larger values for S, T, C, but a smaller base, try solving the same puzzle with a base of 105.

      Like

    • GeoffR's avatar

      GeoffR 12:08 pm on 21 June 2022 Permalink | Reply

      Easy to adapt Jim’s first program, substituting 105 for 1501 as the number base.

      This gives:

      n=458: S=209764 T=105111 C=314875 [C={28:58:85} C^2={7:80:71:28:58:85}]
      C is 3 digits long, so:
      28 * 105**2 + 58 * 105 + 85 = 314875

      Like

  • Unknown's avatar

    Jim Randell 9:22 am on 16 June 2022 Permalink | Reply
    Tags:   

    Teaser 2499: [Supermarket milk] 

    From The Sunday Times, 15th August 2010 [link] [link]

    Joe was talking to the supermarket manager about shelf stocking, and she mentioned a recent exercise with milk. Each morning, they added sufficient new stock to ensure there were 1,200 litres on the shelves. They found that 56% of the new stock was sold on the first day, half that amount the next day and half that new amount the day after. Any remaining stock was regarded as out of date and removed next morning before restocking. Hence, they calculated the number of litres to be added each morning.

    What is that number?

    This puzzle was originally published with no title.

    [teaser2499]

     
    • Jim Randell's avatar

      Jim Randell 9:23 am on 16 June 2022 Permalink | Reply

      I assumed that the numbers are fixed. So the same quantity of fresh milk is added each day, and the same quantities sold each day.

      If x litres of fresh milk are stocked at the beginning of the day (to bring the total stocks to 1200 litres).

      We can track what happens to this batch milk on the following days.

      At the start of the next day 0.56x has been sold, so there is only 0.44x of this batch of milk remaining (and a new batch of x litres of fresh milk are added).

      At the start of the next day an additional 0.28x has been sold, so there is only 0.16x of the original batch remaining (along with 0.44x litres of the previous days new batch, and x litres of fresh milk).

      At the start of the next day an additional 0.14x has been sold, so there is only 0.02x of the original batch remaining, but this is removed as it is too old.

      So the 1200 litres of milk on the shelves consists of:

      x litres of fresh milk
      0.44x litres of 1-day old milk
      0.16x litres of 2-day old milk

      Hence:

      x + 0.44x + 0.16x = 1200
      1.6x = 1200
      x = 750

      Solution: Each morning 750 litres of fresh milk are added to the shelves.

      So the milk available at the start of the day consists of:

      750 litres of fresh milk (420 litres are sold during the day, leaving …)
      330 litres of 1-day old milk (210 litres are sold during the day, leaving …)
      120 litres of 2-day old milk (105 litres are sold during the day, leaving 15 litres to dispose of)
      1200 litres in total (735 litres sold)

      Like

    • Hugh+Casement's avatar

      Hugh+Casement 8:55 am on 17 June 2022 Permalink | Reply

      That’s some supermarket! How ever many metres of shelf space do 1200 litres take up?
      I suggest that a more realistic puzzle would have all the numbers a fifteenth as much.

      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