Updates from Jim Randell Toggle Comment Threads | Keyboard Shortcuts

  • Unknown's avatar

    Jim Randell 9:48 am on 18 February 2021 Permalink | Reply
    Tags:   

    Teaser 2776: Winning months 

    From The Sunday Times, 6th December 2015 [link] [link]

    I have won three Premium Bond prizes and noted the number of non-winning months between my first and second wins, and also the number between my second and third wins. Looking at the letters in the spelling of the months, I have also noted the difference between the numbers of letters in the months of my first and second wins, and also the difference between those of the months of my second and third wins. All four numbers noted were the same, and if you knew that number then it would be possible to work out the months of my wins.

    What (in order of the wins) were those three months?

    [teaser2776]

     
    • Jim Randell's avatar

      Jim Randell 9:48 am on 18 February 2021 Permalink | Reply

      The largest difference in number of letters is 6 (“May” to “September”), so we don’t need to worry about time spans longer than this.

      This Python program runs in 52ms.

      Run: [ @repl.it ]

      from enigma import filter_unique, unpack, irange, join, printf
      
      # month names (in English)
      months = (
        'January', 'February', 'March', 'April', 'May', 'June',
        'July', 'August', 'September', 'October', 'November', 'December',
      )
      
      # month lengths
      ls = set(len(x) for x in months)
      M = max(ls) - min(ls)
      
      # map (<n>, <month1>) -> <month2>
      # where month1 and month2 are separated by n months
      # and month2 length is different from month1 by n
      d = dict()
      for (m1, name) in enumerate(months):
        l1 = len(name)
        for n in irange(0, M):
          m2 = (m1 + n + 1) % 12
          if abs(l1 - len(months[m2])) == n:
            d[(n, m1)] = m2
      
      # now look for elements (n, m1) -> m2 where (n, m2) -> m3
      rs = list()
      for ((n, m1), m2) in d.items():
        m3 = d.get((n, m2), None)
        if m3 is not None:
          ms = tuple(months[i] for i in (m1, m2, m3))
          rs.append((n, ms))
          printf("[n={n}: {ms}]", ms=join(ms, sep=' -> '))
      
      # find unambiguous solutions by n
      rs = filter_unique(rs, unpack(lambda n, ms: n)).unique
      
      # output solutions
      for (n, ms) in rs:
        printf("SOLUTION: n={n}, {ms}", ms=join(ms, sep=' -> '))
      

      Solution: The winning months are: February, July, December.

      This is the only set of months such that each adjacent pair has 4 months between them, and also differ by 4 letters.

      There are ambiguous solutions for values 1 and 5:

      1: August → October → December
      1: September → November → January

      5: May → November → May
      5: November → May → November

      Like

  • Unknown's avatar

    Jim Randell 9:22 am on 16 February 2021 Permalink | Reply
    Tags:   

    Brain-Teaser 692: First-class post 

    From The Sunday Times, 20th October 1974 [link]

    On Trafalgar Day each of the five Sea Lords will take over a post now occupied by one of the others. Each predicts what will happen; those whose predictions are right will get more senior posts, and those whose predictions are wrong will get more junior posts.

    The most junior speaks first:

    Fifth Sea Lord: “Two Sea Lords will make a direct exchange.”
    Fourth Sea Lord: “The Third Sea Lord will become the Second Sea Lord.”
    Third Seal Lord: “A man who makes a true prediction will take over my job.”

    Of the First and Second Sea Lords each predicts the same future new post for himself.

    Which post is that?

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

    [teaser692]

     
    • Jim Randell's avatar

      Jim Randell 9:23 am on 16 February 2021 Permalink | Reply

      If the First Sea Lord and the Second Sea Lord both predict taking on the same position, I am assuming that the position cannot be that of the First or Second Sea Lord, as then one of them would have made a prediction that was impossible to fulfil (because no-one keeps their position).

      This Python program runs in 53ms.

      Run: [ @repl.it ]

      from enigma import subsets, peek, map2str, printf
      
      # positions
      pos = (1, 2, 3, 4, 5)
      
      # check <n> makes statement <s> is valid in map <m>
      check = lambda m, n, s: (m[n] < n) == bool(s)
      
      # find an index that maps to x
      find = lambda m, x: peek(k for (k, v) in m.items() if v == x)
      
      # consider derangements
      for s in subsets(pos, size=len, select="D"):
        m = dict(zip(pos, s))
      
        # check the statements:
      
        # 5: "two will make a direct exchange"
        if not check(m, 5, (any(m[v] == k for (k, v) in m.items()))): continue
      
        # 4: "3 -> 2"
        if not check(m, 4, (m[3] == 2)): continue
      
        # 3: "a man who makes a true prediction (so is promoted) will take
        # over my job (= 3)"
        if not check(m, 3, (find(m, 3) > 3)): continue
      
        # 1: "1 -> x", 2: "2 -> x"
        for x in pos:
          if x == 1 or x == 2: continue
          if not(check(m, 1, (m[1] == x)) and check(m, 2, (m[2] == x))): continue
      
          printf("x={x}; {m}", m=map2str(m, arr="->"))
      

      Solution: The First and Second Sea Lords both predicted they would take on the role of Third Sea Lord.

      There are two possible maps:

      1→4; 2→5; 3→2; 4→1; 5→3
      1→5; 2→4; 3→2; 4→3; 5→1

      In both scenarios the First and Second Sea Lords are demoted (so made false predictions), and the Third, Fourth and Fifth Sea Lords are promoted (and so made true predictions).

      Like

    • Frits's avatar

      Frits 10:54 am on 19 February 2021 Permalink | Reply

      from enigma import SubstitutedExpression
      
      def twoswap(a, b):
        z = [tuple(sorted((x, y))) for (x, y) in zip(a, b)]
        return len(set(z)) < len(z)
      
      # the alphametic puzzle
      p = SubstitutedExpression(
        [
        # (1, 2, 3, 4, 5) --> (V, W, X, Y, Z) 
        
        "sorted([V, W, X, Y, Z]) == [1, 2, 3, 4, 5]",
        
        # 5: "two will make a direct exchange
        "twoswap([1, 2, 3, 4, 5], [V, W, X, Y, Z])",
        
        # 4: "3 -> 2"
        "(X == 2 and Y < 4) or (Y == 5 and X != 2)",
         
        # 3: "a man who makes a true prediction (so is promoted) will take
        # over my job (= 3)"
        "[V, W, X, Y, Z].index(3) > 2 if X < 3 else [V, W, X, Y, Z].index(3) < 3",
        ],
        digits=range(1, 6),
        answer="{3, 4, 5}.difference({V, W})",
        env=dict(twoswap=twoswap),
        # derangement
        # 1: "1 -> x", 2: "2 -> x",  x > 2
        # we only know both statements have to be false
        d2i={1 : "VW", 2 : "VW", 3 : "X", 4 : "Y", 5 : "Z"},
        distinct="VWXYZ",
        verbose=0)   
      
      # print answers 
      answs = set([list(y)[0] for _, y in p.solve()])
      
      for ans in answs:
        print(f"post = {ans}")
      

      Like

      • Frits's avatar

        Frits 11:15 am on 19 February 2021 Permalink | Reply

        @Jim, do you agree with V > 2 and W > 2 for 1: “1 -> x”, 2: “2 -> x”?
        We only know both statements to be false.

        Like

        • Jim Randell's avatar

          Jim Randell 11:39 am on 19 February 2021 Permalink | Reply

          @Frits: I don’t see how to determine the required answer from the output of your program.

          Like

    • John Crabtree's avatar

      John Crabtree 4:37 pm on 19 February 2021 Permalink | Reply

      Here are a couple of ways to tackle it manually.
      The 5th SL must be correct. There are three cases to consider:
      3rd wrong, and so the 4th is wrong: 4 -> 5, 3 -> 4, 1/2 -> 3 with no possible exchange
      3rd right, 4th wrong: 3 -> 1, 4 -> 5, 5 -> 3, with no possible exchange
      3rd right, 4th right: 3 -> 2, 4/5 -> 3, 2 -> 4/5 (to allow an exchange), 5/4 -> 1, 1 -> 5/4

      1st and 2nd become 4th and 5th or vice versa. Assuming that a SL could not predict his current position, the 1st and 2nd SLs predicted that they would be 3rd Sea Lord.

      Using Boolean algebra, where 12 means 1 moves to 2 is true,
      5th SL: the statement is true
      4th SL: (41+43).32 + 45.(31+34) = 1
      3rd SL: (31+32).(43+53) + (34+35).(13+23) = 1
      Multiplying the two and removing impossible terms gives:
      41.32.53 + 43.32 + 45.31.53 + 45.34.(13+23) = 1
      These are the same four possibilities shown in the first solution. The last two do not allow an exchange. From the 5th SL:
      14.41.32.53 + 15.51.43.32 = 1, ie
      14.41.32.53.25 + 15.51.43.32.24 = 1, with the rest as before.

      Like

  • Unknown's avatar

    Jim Randell 9:16 am on 14 February 2021 Permalink | Reply
    Tags: by: C. W. Mitford   

    Brain-Teaser 21: [Crossnumber] 

    From The Sunday Times, 13th August 1961 [link]

    Across:

    1a. Square of a multiple of 11.
    5a. A fourth power.
    8a. A multiple of 2d.
    9a. A cube reversed.
    11a. If added to 4d it gives a square.
    12a. 3 times a prime number.
    14a. A multiple of 11.
    16a. A prime number.
    17a. A cube, which is also the sum of three cubes.
    18a. An even number.
    20a. A multiple of 12.
    21a. A prime number.
    22a. The square of a palindromic number.

    Down:

    1d. The product of two consecutive numbers (neither a prime).
    2d. See 8a.
    3d. A cube.
    4d. See 11a.
    6d. The sum of the digits of 3d.
    7d. The square of (6d reversed).
    10d. The product of two consecutive odd numbers.
    13d. The product of two consecutive even numbers.
    15d. A cube.
    16d. A square.
    19d. An even multiple of (18a + 21a).

    This puzzle was originally published with no title.

    [teaser21]

     
    • Jim Randell's avatar

      Jim Randell 9:18 am on 14 February 2021 Permalink | Reply

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

      The following run file executes in 333ms.

      (Using the standard Python interpreter we need to provide the additional [[ --denest ]] parameter to work around the limit on statically nested blocks. This is not required if using the PyPy interpreter).

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      #  A B C D E F G
      #  H I J K L M N
      #  P # Q R S # T
      #  U V W # X Y Z
      #  a b c d e f g
      #  h i # # # j k
      #  m n p q r s t
      
      SubstitutedExpression
      
      --distinct=""
      --reorder=0
      
      # 1a. = ABCD "Square of a multiple of 11"
      "is_square({ABCD}) % 11 = 0"
      
      # 11a. = QR "If added to 4d. it gives a square"
      # 4d. = DKR "See 11 across"
      "is_square({DKR} + {QR})"
      
      # 8a. = HIJ "A multiple of 2d."
      # 2d. = BI "See 8 across"
      "{HIJ} % {BI} = 0"
      
      # 3d. = CJQWc "A cube"
      "is_cube({CJQWc})"
      
      # 6d. = FM "The sum of the digits of 3d."
      "dsum({CJQWc}) = {FM}"
      
      # 7d. = GNTZ "The square of (6d. reversed)"
      "{MF} ** 2 = {GNTZ}"
      
      # 5a. = EFG "A fourth power"
      "is_power({EFG}, 4)"
      
      # 12a. = UVW "Three times a prime number"
      "is_prime(div({UVW}, 3))"
      
      # 1d. = AHPU "The product of two consecutive numbers (neither a prime)"
      --code="
      def check_1d(n):
        k = isqrt(n)
        return n == k * (k + 1) and not(is_prime(k) or is_prime(k + 1))
      "
      "check_1d({AHPU})"
      
      # 13d. = Vbin "The product of 2 consecutive even numbers"
      --code="
      def check_13d(n):
        k = isqrt(div(n, 4))
        return n == 4 * k * (k + 1)
      "
      "check_13d({Vbin})"
      
      # 16a. = ab "A prime number"
      "is_prime({ab})"
      
      # 20a. = hi "A multiple of 12"
      "{hi} % 12 = 0"
      
      # 16d. = ahm "A square"
      "is_square({ahm})"
      
      # 17a. = cde "A cube, which is also the sum of three cubes"
      --code="
      def check_17a(n, k=3):
        if k == 1:
          return is_cube(n)
        else:
          for x in powers(1, inf, 3):
            if k * x > n: break
            if check_17a(n - x, k - 1): return True
          return False
      "
      "is_cube({cde}) and check_17a({cde})"
      
      # 10d. = LSXe "The product of 2 consecutive odd numbers"
      "is_square(div({LSXe} + 1, 4))"
      
      # 14a. = XYZ "A multiple of 11"
      "{XYZ} % 11 = 0"
      # 9a. = KLMN "A cube reversed"
      "is_cube({NMLK})"
      
      # 21a. = jk "A prime number"
      "is_prime({jk})"
      
      # 15d. = Yfjs "A cube"
      "is_cube({Yfjs})"
      
      # 19d. = gkt "An even multiple of (18a. + 21a.)"
      "div({gkt}, {fg} + {jk}) % 2 = 0"
      
      # 18a. = fg "An even number"
      "{fg} % 2 = 0"
      
      # 22a. = npqrs "The square of a palindrome"
      "is_palindrome(nsplit(is_square({npqrs})))"
      
      # [optional]
      --template="{ABCD} {EFG} | {HIJ} {KLMN} | {P} {QR} {S} {T} | {UVW} {XYZ} | {ab} {cde} {fg} | {hi} {jk} | {m} {npqrs} {t}"
      --solution=""
      --header=""
      

      Solution: The completed grid looks like this:

      Like

  • Unknown's avatar

    Jim Randell 4:56 pm on 12 February 2021 Permalink | Reply
    Tags:   

    Teaser 3047: Some permutations 

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

    I gave Robbie three different, non-zero digits and asked him to add up all the different three-digit permutations he could make from them. As a check for him, I said that there should be three 3’s in his total. I then added two more [non-zero] digits to the [original three digits] to make [a set of] five digits, all being different, and asked Robbie’s mother to add up all the possible five-digit permutations of these digits. Again, as a check, I told her that the total should include five 6’s.

    Given the above, the product of the five digits was as small as possible.

    What, in ascending order, are the five digits?

    I have changed the text of this puzzle slightly to make it clearer.

    [teaser3047]

     
    • Jim Randell's avatar

      Jim Randell 5:10 pm on 12 February 2021 Permalink | Reply

      (See also: Teaser 2863).

      This Python program runs in 64ms.

      Run: [ @repl.it ]

      from enigma import irange, subsets, nconcat, nsplit, diff, multiply, group, printf
      
      # total all permutations of a set of distinct digits
      psum = lambda ds: sum(nconcat(*s) for s in subsets(ds, size=len, select="P"))
      
      # count occurrences of digit d in number n
      dcount = lambda n, d: nsplit(n).count(d)
      
      # generate solutions
      def solve():
        # permissible digits
        digits = irange(1, 9)
      
        # choose the initial set of 3 digits
        for ds3 in subsets(digits, size=3):
          t3 = psum(ds3)
          # there should be three 3 digits in the total
          if not(dcount(t3, 3) == 3): continue
      
          # now add 2 more digits to the mix
          for ds in subsets(diff(digits, ds3), size=2):
            ds5 = ds3 + ds
            t5 = psum(ds5)
            # there should be five 6 digits in the total
            if not(dcount(t5, 6) == 5): continue
      
            printf("[{ds3} -> {t3}; {ds5} -> {t5}]")
            yield ds5
      
      # group solutions by product
      d = group(solve(), by=multiply)
      
      # output solutions
      if d:
        k = min(d.keys())
        for ds5 in d[k]:
          printf("{ds3} + {ds2}; product = {k}", ds3=ds5[:3], ds2=ds5[3:])
      

      Solution: The five digits are: 1, 2, 5, 8, 9.

      There are two scenarios with a minimal product of 720:

      (1) Robbie was given: 1, 5, 9; Additional digits: 2, 8.
      (2) Robbie was given: 2, 5, 8; Additional digits: 1, 9.

      The sum of all 3-digit permutations is 3330 (= 222 × 15).

      The sum of all 5-digit permutations is 6666600 = (266664 × 25).

      In total there are 16 candidate solutions, each with the same sums for the 3-digit and 5-digit permutations.


      In general, if we have a set of k different digits, then they can be rearranged in factorial(k) different ways, to form different numbers.

      And when these numbers are summed, each digit will appear factorial(k) / k = factorial(k − 1) times in each column.

      So we can calculate the total of all permutations without having to generate them all:

      from enigma import factorial, repdigit
      
      def psum(ds):
        k = len(ds)
        return factorial(k - 1) * repdigit(k) * sum(ds)
      

      In particular, for 3-digit numbers this simplifies to [[ 222 * sum(ds) ]] and for 5-digit numbers it simplifies to [[ 266664 * sum(ds) ]].

      The program above can be adapted using this analysis to give a solution with a run time of 51ms:

      from enigma import irange, subsets, nsplit, diff, multiply, Accumulator, inf, factorial, repdigit, printf
      
      # return multiplier for total sum of all permutations of a set of distinct digits
      psum = lambda k: factorial(k - 1) * repdigit(k)
      psum3 = psum(3)
      psum5 = psum(5)
      
      # count occurrences of digit d in number n
      dcount = lambda n, d: nsplit(n).count(d)
      
      # collect minimal products
      r = Accumulator(fn=min, value=inf, collect=1)
      
      # choose the set of 5 digits
      for ds5 in subsets(irange(1, 9), size=5):
        p = multiply(ds5)
        if p > r.value: continue
      
        # there should be five 6 digits in the total
        t5 = psum5 * sum(ds5)
        if not(dcount(t5, 6) == 5): continue
      
        # choose a subset of 3 digits
        for ds3 in subsets(ds5, size=3):
          t3 = psum3 * sum(ds3)
          # there should be three 3 digits in the total
          if not(dcount(t3, 3) == 3): continue
      
          # record the initial set of 3 digits, and the additional 2 digits
          r.accumulate_data(p, (ds3, diff(ds5, ds3)))
      
      # output solutions
      if r.count:
        for (ds3, ds2) in r.data:
          printf("{ds3} + {ds2}; product = {r.value}")
      

      Like

      • Tony Brooke-Taylor's avatar

        Tony Brooke-Taylor 3:11 pm on 13 February 2021 Permalink | Reply

        I think it is possible to simplify the problem to a point where the solution can be found manually. I admit I only spotted the simplifications once I had written an algorithm similar to yours Jim.

        In the 3-digit problem we can work out manually what the total must be: there is only one multiple of 222 that has three 3’s in it. Eight sets of three digits sum to the correct cofactor, of which four are symmetrical around the value 5.

        In the 5-digit problem we can also work out what the total must be: there is only one multiple of 266664 that has five 6’s in it, and I believe this is more obvious if we look at multiples of 11111. Thus we have a unique cofactor in this problem too, and the sum of the extra two digits must be the difference between the two cofactors. Only four pairs of digits make up this sum.

        If we combine the four pairs with the eight trios and deduplicate, we get ten possible sets of 5, of which six are symmetrical around the value 5.

        By trigonometry, the solution with the minimum product has the greatest distances between the central number and the others. My leap of faith is that we could also have applied this at the 3-digit stage to reduce the number of trios we needed to test.

        Like

        • Jim Randell's avatar

          Jim Randell 12:07 pm on 17 February 2021 Permalink | Reply

          @Tony: Once we’ve got the 8 pairs of triples that sum to 15, and we are extending them with a pair that sums to 10 (= (1,9) (2,8) (3, 7) (4,6)), then we only need to consider the first viable pair, as later ones will give a larger product.

          So we can exhaustively check the solution space by calculating only 7 products.


          Here’s some code that uses this analysis:

          from enigma import irange, Accumulator, printf
          
          # collect minimal product
          r = Accumulator(fn=min, collect=1)
          
          # look for sets of 3 digits (a, b, c) that sum to 15
          for a in irange(1, 4):
            for b in irange(a + 1, 6):
              c = 15 - (a + b)
              if not(b < c < 10): continue
          
              # and now add (d, e) that sum to 10
              for d in irange(1, 4):
                e = 10 - d
                if len({a, b, c, d, e}) != 5: continue
          
                # collect minimal product
                r.accumulate_data(a * b * c * d * e, ((a, b, c), (d, e)))
                break # only need to check the first viable (d, e) pair
          
          # output solutions
          if r.count:
            for (ds3, ds2) in r.data:
              printf("{ds3} + {ds2}; product = {r.value}")
          

          Like

      • Frits's avatar

        Frits 10:31 am on 15 February 2021 Permalink | Reply

        @Jim,

        If you replace line 23 “t5 = psum(ds5) ” with

        t5 = 0
        for v, w, x, y, z in permutations(ds5):
          t5 += v * 10000 + w * 1000 + x * 100 + y * 10 + z
        

        your code is approx. 4.5 times faster (with Python 3.9.0).

        Maybe nconcat is not efficient if called multiple times?
        Subsets only seems to have minor overhead.

        Like

        • Jim Randell's avatar

          Jim Randell 12:40 pm on 15 February 2021 Permalink | Reply

          @Frits: In a program that runs pretty much instantaneously I am less concerned about coding for execution speed as I am that the program itself should be concise, clear and easy to modify (and, above all, correct).

          So using a general utility function like [[ nconcat() ]] makes it clear that I am turning a sequence of digits into a number, even if it is not the fastest way to achieve this.

          Although with a bit of analysis we find that we don’t have to construct numbers corresponding to all the permutations of the digits in order to arrive at the sum (and gives a general principle for attacking similar problems), which lets us write a program that runs even faster.

          But that is not to say I’m not at all interested in execution speed. I’ll have a look at improving [[ nconcat() ]], and another advantage of using utility functions is that if an improvement is made, every program that uses them gets the benefit.

          Like

    • Frits's avatar

      Frits 11:23 am on 13 February 2021 Permalink | Reply

      Basic version, not really optimized for speed.

      from itertools import permutations, combinations
      
      sol = set()
      for a, b, c in combinations(range(1, 10), 3):
        t1 = 0
        for x, y, z in permutations([a, b, c]):
          t1 += x * 100 + y * 10 + z
       
        # there should be three 3's in the total
        if str(t1).count("3") == 3:
          for d in [q for q in range(1, 9) if q not in {a, b, c}]:
            for e in [q for q in range(d + 1, 10) if q not in {a, b, c}]:
              t2 = 0
              for v, w, x, y, z in permutations([a, b, c, d, e]):
                t2 += v * 10000 + w * 1000 + x * 100 + y * 10 + z
              
              # there should be five 6's in the total
              if str(t2).count("6") == 5:
                # put product up front so we can use if for sorting
                sol.add(tuple([a * b * c * d * e] + sorted([a, b, c, d, e])))
        
      if len(sol):  
        print(f"answer = {sorted(sol)[0][1:]}")   
      

      Like

    • Frits's avatar

      Frits 2:07 pm on 18 February 2021 Permalink | Reply

      Partly based on Tony’s comments.

      from math import prod
      
      # Suppose a < b < c < d < e and (a + b + c + d + e) is a constant
      #
      # a * b * c * d * e is minimal 
      # if sum of distances with respect to c is maximal.
      # 
      # Proof:
      # 
      # sum of distances = (c - a) + (c - b) + (d - c) + (e - c) = d + e - a - b
      # 
      # Suppose we allow d or e to decrease with one and a or b to increase with 
      # one, so the sum stays the same. Say d' = d - 1 and b' = b + 1
      # 
      # b' * d' = (b + 1) * (d - 1) = b * d + (d - b) - 1
      # as c is between b and d we can say d - b > 1, thus d' * b' > d * b
      # so new product a * b' * c * d' * e  >  old product a * b * c * d * e
      
      # decompose number <t> into <k> different numbers from sequence <ns> 
      # so that  sum(<k> numbers) equals <t>
      # the result is already sorted on product !!
      def decompose(t, k, ns, s=[]):
        if k == 1:
          if t in ns:
            yield s + [t]
        else:
          for (i, n) in enumerate(ns):
            if not(n < t): break
            yield from decompose(t - n, k - 1, ns[i + 1:], s + [n])
      
      digits = [1, 2, 3, 4, 5, 6, 7, 8, 9]
      
      # sum permutations of 3 different digits is 222 * sum(digits)
      # last digit of sum can never be a 3 
      # so first 3 digits of sum must be all 3's
      s3 = 3340 // 222                   # sum of 3 digits
      if (s3 * 222) // 10 != 333: 
        exit()                           # no solution
      
      # sum permutations of 5 different digits is 266664 * sum(digits)
      sum5 = [x for x in range(s3 + 3, min(s3 + 18, 36)) 
              if str(266664 * x).count("6") == 5] 
              
      sol = []        
      
      for s5 in sum5:
        # we look for 5 digits a, b, c, d, e which sum to <s5>
        # and where three of them sum to <s3>
        # (a * b * c * d * e must be minimal)
      
        # choose <a> as low as possible and <e> as high as possible
        a = max(1, s5 - 30)
        e = min(9, s5 - 10)
        # d - b must be maximal
        b = max(2, s5 - 25 - a + 1)
        d = min(8, max(4, s5 - 15))
        c = s5 - (a + b + d + e)
        
        # check optimal solution 
        if len(list(decompose(s3, 3, [a, b, c, d, e]))) > 0:
          sol.append([prod([a, b, c, d, e]), (a, b, c, d, e)])
        else:
          for abcde in decompose(s5, 5, digits): 
            if len(list(decompose(s3, 3, abcde))) > 0:
              sol.append([prod(abcde), abcde])
              break  # decompose returns entries sorted on product!
        
      if len(sol) > 0:  
        sol.sort()   # sort on product
        print("answer = ", sol[0][1])
      

      Like

  • Unknown's avatar

    Jim Randell 9:01 am on 11 February 2021 Permalink | Reply
    Tags:   

    Brain-Teaser 691: That’s torn it … 

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

    It was typical of Uncle Bungle that he should have torn up the sheet of paper which gave particulars of the numbers of matches played, won, lost, drawn, etc. of four local football teams who were eventually going to play each other once. The only piece left was as shown (as usual there are 2 points for a win and 1 for a draw):

    It will not surprise those who know my Uncle to hear that one of the figures was wrong, but fortunately it was only one out (i.e. one more or less than the correct figure).

    Each side had played at least one game, and not more than seven goals were scored in any match.

    Calling the teams A, B, C and D (in that order), find the score in each match.

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

    [teaser691]

     
    • Jim Randell's avatar

      Jim Randell 9:02 am on 11 February 2021 Permalink | Reply

      (See also: Puzzle 81).

      This Python program uses the [[ Football ]] helper class from the enigma.py library. It runs in 198ms.

      from enigma import (Football, subsets, cproduct, printf)
      
      # scoring system
      football = Football(points=dict(w=2, d=1))
      
      # possible scores in the matches (no match has more than 7 goals scored)
      scores = {
        'w': [
          (1, 0), (2, 0), (3, 0), (4, 0), (5, 0), (6, 0), (7, 0),
          (2, 1), (3, 1), (4, 1), (5, 1), (6, 1),
          (3, 2), (4, 2), (5, 2),
          (4, 3),
        ],
        'd': [(0, 0), (1, 1), (2, 2), (3, 3)],
        'x': [None],
      }
      scores['l'] = list((b, a) for (a, b) in scores['w'])
      
      # choose outcomes for each of the matches
      for (AB, AC, AD, BC, BD, CD) in subsets('wdlx', size=6, select="M"):
        # calculate the points
        A = football.table([AB, AC, AD], [0, 0, 0])
        B = football.table([AB, BC, BD], [1, 0, 0])
        C = football.table([AC, BC, CD], [1, 1, 0])
        D = football.table([AD, BD, CD], [1, 1, 1])
      
        # each team has played at least one game
        if not all(x.played > 0 for x in [A, B, C, D]): continue
      
        # count the discrepancies in the points
        dp = abs(A.points - 3) + abs(B.points - 5) + C.points
        if dp > 1: continue
      
        # choose the scores in C's games
        for (sAC, sBC, sCD) in cproduct([scores[AC], scores[BC], scores[CD]]):
          (fC, aC) = football.goals([sAC, sBC, sCD], [1, 1, 0])
          dC = aC
          if dp + dC > 1: continue
      
          # choose the scores in A's games
          for (sAB, sAD) in cproduct([scores[AB], scores[AD]]):
            (fA, aA) = football.goals([sAB, sAC, sAD], [0, 0, 0])
            dA = abs(aA - 5)
            if dp + dA + dC > 1: continue
      
            # remaining game
            for sBD in scores[BD]:
              (fB, aB) = football.goals([sAB, sBC, sBD], [1, 0, 0])
              dB = abs(aB - 6)
              if dp + dA + dB + dC > 1: continue
      
              (fD, aD) = football.goals([sAD, sBD, sCD], [1, 1, 1])
              dD = abs(aD - 7)
              if dp + dA + dB + dC + dD != 1: continue
      
              printf("A=({aA} {A.points}) B=({aB} {B.points}) C=({aC} {C.points}) D=({aD} {D.points}) / \\")
              printf("AB={AB}:{sAB} AC={AC}:{sAC} AD={AD}:{sAD} BC={BC}:{sBC} BD={BD}:{sBD} CD={CD}:{sCD}")
      

      Solution: The scores in the played matches are: A vs B = 3-3; A vs D = 3-2; B vs C = 1-0; B vs D = 4-3.

      The A vs C and C vs D matches are not yet played.

      The incorrect value is C’s “goals against”. It should be 1, not 0.

      It is clear that the incorrect value must be one of C’s, as if they have played at least one game they cannot have both 0 points and 0 goals against.

      Like

  • Unknown's avatar

    Jim Randell 8:50 am on 9 February 2021 Permalink | Reply
    Tags:   

    Teaser 2775: Strictly not 

    From The Sunday Times, 29th November 2015 [link] [link]

    Four celebrities entered a dance competition. Five judges each shared out their eight marks among the four dancers, with each getting a non-zero whole number. Each judge split the eight marks in a different way and then allocated them as follows. Amanda’s marks to Lana and Natasha added to the same total as Barry’s marks to Madge and Paula. Barry gave more marks to Madge than to any other dancer, Charles gave more to Paula than to any other, and Doris gave more to Natasha than to any other. Lana scored more from Edna than from Amanda. All dancers had the same total so the head judge’s scores were used, giving a winner and runner-up.

    Who was the head judge, who was the winner and who was the runner-up?

    [teaser2775]

     
    • Jim Randell's avatar

      Jim Randell 8:51 am on 9 February 2021 Permalink | Reply

      Each of the 5 judges gives out 8 points, so 40 points are awarded in total. And there are 4 dancers, and they all receive the same total number of points. So each dancer receives 10 points in total.

      Each judge awards a non-zero number of points to each dancer, so the points awarded are between 1 and 5.

      I used the [[ SubstitutedExpression ]] solver from the enigma.py library to find possible collections of points awarded by the judges, and the scores are then examined to determine which judges scores identify a clear winner and runner up.

      The following run file executes in 80ms.

      Run: [ @replit ]

      from enigma import (SubstitutedExpression, irange, seq_all_different, ordered, printf)
      
      # consider the points given:
      #
      #             Lana
      #             |  Madge
      #             |  |  Natasha
      #             |  |  |  Paula
      #   Amanda:   A  B  C  D = 8
      #   Barry:    E  F  G  H = 8
      #   Charles:  I  J  K  L = 8
      #   Doris:    M  N  P  Q = 8
      #   Edna:     R  S  T  U = 8
      #             =  =  =  =
      #            10 10 10 10
      
      # check the splits are all different
      check_splits = lambda *xs: seq_all_different(ordered(*x) for x in xs)
      
      # construct the alphametic problem
      p = SubstitutedExpression([
          # each judge gave out 8 points
          "A + B + C + D = 8",
          "E + F + G + H = 8",
          "I + J + K + L = 8",
          "M + N + P + Q = 8",
          "R + S + T + U = 8",
      
          # points were split in a different way by each judge
          "check_splits((A, B, C, D), (E, F, G, H), (I, J, K, L), (N, M, P, Q), (R, S, T, U))",
      
          # "A's marks to L and N had the same sum as B's marks to M and P"
          "A + C == F + H",
      
          # "B gave more marks to M than to any other"
          "max(E, G, H) < F",
      
          # "C gave more to P than to any other"
          "max(I, J, K) < L",
      
          # "D gave more to N than to any other"
          "max(M, N, Q) < P",
      
          # "L scored more from E than from A"
          "R > A",
      
          # all dancers had the same total (= 10)
          "A + E + I + M + R = 10",
          "B + F + J + N + S = 10",
          "C + G + K + P + T = 10",
          "D + H + L + Q + U = 10",
        ],
        distinct=(),
        digits=irange(1, 5), # points are in the range 1-5
        env=dict(check_splits=check_splits),
        # answer is the scores from each judge
        answer="((A, B, C, D), (E, F, G, H), (I, J, K, L), (M, N, P, Q), (R, S, T, U))",
        denest=-1, # [CPython] work around statically nested block limit
      )
      
      # solve the puzzle, and output solutions
      (judges, dancers) = ("ABCDE", "LMNP")
      for (_, scores) in p.solve(verbose=0):
        # look for scores that differentiate 1st and 2nd place
        for (j, ss) in zip(judges, scores):
          printf("{j}: {ss}\\")
          (p1, p2, p3, p4) = sorted(ss, reverse=1)
          if p1 > p2 > p3:
            (d1, d2) = (dancers[ss.index(p)] for p in (p1, p2))
            printf(" -> head judge; 1st = {d1}, 2nd = {d2}\\")
          printf()
        printf()
      

      Solution: Doris was the head judge. Natasha was the winner. Lana was the runner-up.

      The scores were allocated as follows (L, M, N, P):

      A: (2, 2, 2, 2)
      B: (2, 3, 2, 1)
      C: (1, 1, 1, 5)
      D: (2, 1, 4, 1)
      E: (3, 3, 1, 1)

      There are only 5 different (unordered) ways to divide a score of 8 between 4 dancers, and only one of them (4, 2, 1, 1) gives a clear 1st and 2nd place.

      Like

    • Frits's avatar

      Frits 9:26 pm on 9 February 2021 Permalink | Reply

      from collections import defaultdict
       
      # consider the points given:
      #
      #             Lana
      #             |  Madge
      #             |  |  Natasha
      #             |  |  |  Paula
      #   Amanda:   A  B  C  D = 8
      #   Barry:    E  F  G  H = 8
      #   Charles:  I  J  K  L = 8
      #   Doris:    M  N  P  Q = 8
      #   Edna:     R  S  T  U = 8
      #             =  =  =  =
      #            10 10 10 10
      
      M = 5  # number of rows
      N = 4  # number of columns 
      
      # decompose number <t> into <k> positive numbers from <ns>
      # so that sum(<k> numbers) equals <t>
      def decompose(t, k, ns, s=[]):
        if k == 1:
          if t in ns:
            yield s + [t]
        else:
          for (i, n) in enumerate(ns):
            if not(n + k - 2 < t): break
            #yield from decompose(t - n, k - 1, ns[i:], s + [n])
            yield from decompose(t - n, k - 1, ns, s + [n])   
            
      # combine rows of dictionary <d>
      # so that every column total equals <t>
      def combineRows(t, k, d, s=[]):
        if k == 1:
          remaining = [t - sum(x) for x in zip(*s)]
          if remaining in d[0]:
            yield s + [remaining]
        else:
          for row in d[k - 1]:
            # for each column ...
            for j in range(N):
              # .. sum already processed numbers
              already = sum([0 if len(s) == 0 else x[j] for x in s]) 
              # and check if there is room for remaining rows
              if not(row[j] + already  + k - 2 < t): 
                already = -1  # skip this row
                break
            if already > -1:
              yield from combineRows(t, k - 1, d, s + [row])         
      
      
      types = []
      d_rows = defaultdict(list)
      
      # create dictionary of possible rows for each type of row
      # the points awarded are between 1 and 5
      for x in decompose(8, N, range(1, 6)): 
        sortx = sorted(x) 
        if sortx in types:
          type = types.index(sortx)
        else:
          type = len(types)
          types.append(x)
        # append possible row  
        d_rows[type] += [x]
        
      
      # combine rows so that column total always equals 10  
      for rows in combineRows(10, M, d_rows): 
        testB, testC, testD = -1, -1, -1
        
        # validate rows
        for i, r in enumerate(rows):
          sr = sorted(r)
          # check if maximum value is unique
          if sr[-1] > sr[-2]:
            
            if sr[-1] == r[1]:    # testB : "max(E, G, H) < F",
              testB = i  
            elif sr[-1] == r[3]:  # testC : "max(I, J, K) < L",
              testC = i
            elif sr[-1] == r[2]:  # testD : "max(M, N, Q) < P",
              testD = i
        
        if -1 in {testB, testC, testD}: continue
        
        # get the other rows
        ix_oth = [x for x in range(M) if x not in {testB, testC, testD}]
        
        # "R > A"
        if rows[ix_oth[0]][0] < rows[ix_oth[1]][0]:
          testA = ix_oth[0]; testE = ix_oth[1]
        elif rows[ix_oth[0]][0] > rows[ix_oth[1]][0]:
          testE = ix_oth[0]; testA = ix_oth[1]
        else:
          continue
        
        # "A + C == F + H"
        if rows[testA][0] + rows[testA][2] != rows[testB][1] + rows[testB][3]:
          continue
        
        # the head judge's scores were used, giving a winner and runner-up
        judges, dancers = "ABCDE", "LMNP"
        for i, r in enumerate(rows):
          sr = sorted(r)
          if sr[1] < sr[2] < sr[3]:
            print(f"head judge: {judges[i]} ")
            print(f"winner    : {dancers[r.index(sr[3])]}")
            print(f"runner up : {dancers[r.index(sr[2])]}")
            print()
           
            for r in rows:
              print(r)
      

      Like

  • Unknown's avatar

    Jim Randell 9:26 am on 7 February 2021 Permalink | Reply
    Tags:   

    Brain-Teaser 20: The kite 

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

    Some problems that look simple enough at first can prove to be remarkably tricky. Consider, for instance, the kite pictured above. The shaded areas are squares of equal size, the sides of each square being 15 inches. The width of the kite from A to B is exactly 42 inches.

    What is the length of the kite from C to D?

    [teaser20]

     
    • Jim Randell's avatar

      Jim Randell 9:29 am on 7 February 2021 Permalink | Reply

      The figure is a right kite [ @wikipedia ], so is composed of 2 identical right-angled triangles joined along their common hypotenuse CD (which is the length we wish to determine).

      Writing: AC = BC = a, and AD = BD = b, then the value we wish to determine, CD, is:

      CD = √(a² + b²)

      We are told the other diagonal, AB, has length 42, hence:

      42 = 2ab / √(a² + b²)
      21√(a² + b²) = ab

      And the sides of the squares that run from the centre to the edges of the kite are radii of the incircle, so:

      15 = ab / (a + b)

      Writing: x = ab, y = a + b, we get:

      15 = x / y
      x = 15y
      x² = 225y²

      Also:

      a² + b² = (a + b)² − 2ab = y² − 2x

      So:

      441(y² − 2x) = x²
      441(y² − 30y) = 225y²
      216y² = 13230y
      4y = 245
      y = 245 / 4
      x = 3675 / 4

      So the value of CD is:

      CD = √(a² + b²)
      = √(y² − 2x)
      = √(30625 / 16)
      = 175 / 4

      Solution: CD = 43.75 inches.

      And the lengths of the sides of the kite are 26.25 in and 35 in.

      Like

    • John Crabtree's avatar

      John Crabtree 10:22 pm on 8 February 2021 Permalink | Reply

      Let the mid-point of AB be F, and the point where the squares meet be G
      By Pythagoras, FG = √(2 * 225 – 441) = 3
      Let z be the angle formed by GAF.
      Tan(z) = t = 3 / 21 = 1 / 7
      CD = 21 [tan(45 + z) + tan(45 – z)] = 21 [(1+t)/(1-t) + (1-t)/(1+t))
      = 21 [4/3 + 3/4] = 21 * 25/12 = 43.75 inches.
      AC = 21 * 5/4 = 26.25 in. AD = 21 * 5/3 = 35 in.

      Like

  • Unknown's avatar

    Jim Randell 4:52 pm on 5 February 2021 Permalink | Reply
    Tags:   

    Teaser 3046: Square phone numbers 

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

    George and Martha run a business in which there are 22 departments. Each department has a perfect-square three-digit extension number, i.e., everything from 100 (10²) to 961 (31²), and all five of their daughters are employees in separate departments.

    Andrea, Bertha, Caroline, Dorothy and Elizabeth have extensions in numerical order, with Andrea having the lowest number and Elizabeth the highest. George commented that, if you look at those of Andrea, Bertha and Dorothy, all nine positive digits appear once. Martha added that the total of the five extensions was also a perfect square.

    What is Caroline’s extension?

    [teaser3046]

     
    • Jim Randell's avatar

      Jim Randell 5:03 pm on 5 February 2021 Permalink | Reply

      A simple Python program finds the solution in 67ms.

      Run: [ @repl.it ]

      from enigma import powers, subsets, concat, is_square, printf
      
      # possible square numbers
      squares = powers(10, 31, 2)
      
      # choose 5 of the squares (in order)
      for (A, B, C, D, E) in subsets(squares, size=5):
      
        # the sum of the numbers is also a square
        if not is_square(A + B + C + D + E): continue
      
        # A, B, D use the digits 1-9
        ds = concat(A, B, D)
        if '0' in ds or len(set(ds)) != 9: continue
      
        # output solution
        printf("A={A} B={B} C={C} D={D} E={E}")
      

      Solution: Caroline’s extension is 729.

      Manually, you can write out the squares, and then working through the possible candidates for A, B, D, the solution is discovered quite quickly.

      Like

    • GeoffR's avatar

      GeoffR 7:51 pm on 5 February 2021 Permalink | Reply

      
      % A Solution in MiniZinc
      include "globals.mzn";
       
      % Andrea, Bertha, Caroline, Dorothy and Elizabeth's numbers
      var 100..961:A; var 100..961:B; var 100..961:C;
      var 100..961:D; var 100..961:E;
      
      constraint all_different([A, B, C, D, E]);
      constraint A < B /\ B < C /\ C < D /\ D < E;
      
      % digits for squares A, B and D
      var 1..9:a1; var 1..9:a2; var 1..9:a3;
      var 1..9:b1; var 1..9:b2; var 1..9:b3;
      var 1..9:d1; var 1..9:d2; var 1..9:d3;
      
      % all 3-digit squares
      set of int: sq3 = {n*n | n in 10..31};
      
      predicate is_sq(var int: y) =
      exists(z in 1..ceil(sqrt(int2float(ub(y)))))
      (z*z = y);
              
      % the total of the five extensions was also a perfect square        
      constraint is_sq(A + B + C + D + E);
      
       % all five telephone extensions are 3-digit squares       
      constraint A in sq3 /\ B in sq3 /\ C in sq3 /\ D in sq3 /\ E in sq3;
      
      % digit components in squares A, B and D
      constraint a1 == A div 100 /\ a2 == A div 10 mod 10 /\ a3 == A mod 10;
      constraint b1 == B div 100 /\ b2 == B div 10 mod 10 /\ b3 == B mod 10;
      constraint d1 == D div 100 /\ d2 == D div 10 mod 10 /\ d3 == D mod 10;
      
      set of int: all_dig ={1, 2, 3 ,4, 5, 6, 7, 8, 9};
      var set of int: S1 == {a1, a2, a3, b1, b2, b3, d1, d2, d3};
      constraint all_dig == S1;
      
      solve satisfy;
      
      output ["Caroline's extension is " ++ show(C)
      ++"\nOther extensions are " ++ show(A) ++ ", " ++ show(B)
      ++ ", " ++ show(D) ++ ", " ++ show(E) ];
      
      

      Like

    • GeoffR's avatar

      GeoffR 8:44 am on 6 February 2021 Permalink | Reply

      from itertools import combinations
      from enigma import nsplit, is_square
      
      squares =[x * x for x in range(10, 32)]
      
      for A, B, C, D, E in combinations(squares, 5):
        # five extensions are in numerical order
        if A < B < C < D < E:
          sq_total = A + B + C + D + E 
          if is_square(sq_total):
            # find individual digits of A, B and D
            a1, a2, a3 = nsplit(A)
            b1, b2, b3 = nsplit(B)
            d1, d2, d3 = nsplit(D)
            S1 = set((a1, a2, a3, b1, b2, b3, d1, d2, d3))
            # check this set contains nine positive digits
            if len(S1) == 9 and 0 not in S1:
              print(f"Five extensions are {A}, {B}, {C}, {D}, {E}")
      
      
      

      Like

    • Frits's avatar

      Frits 7:44 pm on 6 February 2021 Permalink | Reply

      squares = [x * x for x in range(15, 66)]
      ABD_squares = [x * x for x in range(13, 31) 
                     if len(set(str(x * x) + "0")) == 4]
      
      for i, a in enumerate(ABD_squares[:-2]):
        for j, b in enumerate(ABD_squares[i + 1:-1]):
          if len(set(str(a) + str(b))) != 6: continue
          for d in ABD_squares[i + j + 2:]:
            if len(set(str(a) + str(b) + str(d))) != 9: continue
            p = squares.index(b)
            q = squares.index(d)
            
            for c in squares[p + 1:q]:
              for e in squares[q + 1:17]:
                if not (a + b + c + d + e) in squares: continue
                print(f"Caroline's extension is {c}.")
      

      Like

    • Tony Brooke-Taylor's avatar

      Tony Brooke-Taylor 10:21 am on 7 February 2021 Permalink | Reply

      import itertools
      
      digits = set([str(j) for j in range(1, 10)])
      
      for M in itertools.combinations([i**2 for i in range(10, 32)], 5):
        if (sum(M)**(1/2))%1 == 0:
          if set(str(M[0])+str(M[1])+str(M[3])) == digits: print("Caroline's extension is", M[2])
      

      Like

    • Frits's avatar

      Frits 4:21 pm on 7 February 2021 Permalink | Reply

      Logic can reduce a,b and d to specific squares.
      The version without reduction logic is faster.

      from collections import defaultdict
      
      squares = [x * x for x in range(15, 66)]
      ABD_squares = [x * x for x in range(13, 31) 
                     if len(set(str(x * x) + "0")) == 4]
          
      # return first column    
      col0 = lambda li: [x[0] for x in li]               
      
      fixed = set()
      str_fixed = ""
      mut_excl = defaultdict(set)
      
      # reduce candidates in list <ABD_squares>
      # by looking for digits occurring once or twice in list <ABD_squares>
      for loop in range(9):
        occ = defaultdict(list)
        
        for x in "123456789":
          # add entries to occurence dictionary <occ>
          for i, s in enumerate(ABD_squares):
            s1 = str(s)
            if x not in s1 or s1 in fixed: continue
            # skip if a square digit already occurs in <fixed>
            if any(y in str_fixed for y in s1): continue
            occ[x].append([i, s1])
      
          if len(occ[x]) == 1:  
            # a square has to occur in <ABD_squares>
            if not occ[x][0][1] in fixed:
              fixed.add(occ[x][0][1])
              str_fixed = "".join(fixed)
              # also update occurrence for other 2 related digits to same square
              for c in occ[x][0][1]:
                if c == x: continue
                occ[c] = occ[x]
          elif len(occ[x]) == 2:
            # we have a square with digits x,e,f and a square with digits x,g,h
            # so e, e, f, f may not occur in another square with resp. g, h ,g, h
            for y in occ[x][0][1]: 
              if y == x: continue
              for z in occ[x][1][1]:
                if z == x: continue
                mut_excl[y].add(z)  
                mut_excl[z].add(y) 
      
        # check if 2 digits always occur in different squares
        for x1, y1 in occ.items():
          for x2, y2 in occ.items():
            if x1 >= x2: continue
            if len(set(col0(y1) + col0(y2))) != \
               len(col0(y1)) + len(col0(y2)): continue
            # digit x1 and x2 occur in different squares
            # so maintain dictionary <mut_excl>
            mut_excl[x1].add(x2)
            mut_excl[x2].add(x1)
      
        # reduce squares if a square contains mutually exclusive digits
        ABD_squares = [s for s in ABD_squares 
                       if not any(m in str(s) for y in str(s) 
                                  for m in mut_excl[y])
                      ]
         
        if len(ABD_squares) == 3: break
        
        
      # start with a,b,d and then check c and e
      for i, a in enumerate(ABD_squares[:-2]):
        for j, b in enumerate(ABD_squares[i + 1:-1]):
          if len(set(str(a) + str(b))) != 6: continue
          for d in ABD_squares[i + j + 2:]:
            if len(set(str(a) + str(b) + str(d))) != 9: continue
            p = squares.index(b)
            q = squares.index(d)
            
            for c in squares[p + 1:q]:
              for e in squares[q + 1:17]:
                if not (a + b + c + d + e) in squares: continue
                print(f"Caroline's extension is {c}.")
      

      Like

    • Frits's avatar

      Frits 1:25 pm on 8 February 2021 Permalink | Reply

      Not very efficient.

      # decompose a number into <k> increasing 3-digit squares a, b, c, d, e
      # (a, b, d) has to contain all digits 1-9
      def decompose(t, k, m=1, ss=[]):
        if k == 1:
          # check if last number is higher and a square
          if t > ss[-1] and int(t ** 0.5) ** 2 == t:
            yield ss + [t]
        else:
          for i in range(m, 32 - k + 1):
            s = i * i
            if s > t: break
            
            if k in {3, 1} or len(set(str(s) + "0")) == 4:
              yield from decompose(t - s, k - 1, i + 1, ss + [s])
              
      # sum first five 3-digit squares is 750 and last five 3-digit squares is 4215
      for r in range(28, 65):
        # get five 3-digit squares which add up to square <r * r>
        for x in decompose(r * r, 5, 10): 
          sabd = set(str(1000000 * x[0] + 1000 * x[1] + x[3]))
          if len(sabd) != 9: continue
      
          print(f"Caroline's extension is {x[2]}.")
      

      Like

  • Unknown's avatar

    Jim Randell 8:47 am on 4 February 2021 Permalink | Reply
    Tags: by: D C Pusinelli   

    Brain-Teaser 688: Job allocation 

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

    Ashley, Bill, Charles, David, and Edward are (not necessarily in that order), a dustman, a grocer, a miner, a blacksmith, and an artist, and all live on the right hand side of Strife Lane, in even numbered houses. All five are of different ages and no man has reached the age of retirement (65). All of course are upright and honest citizens, and never tell lies. However, I had forgotten what job each man did, where he lived, and how old he was, and so, to help me, each man volunteered the following statements:

    Ashley:
    (1) The artist lives at No. 10, next to Charles;
    (2) Nobody lives next to the grocer, although Bill is only two doors away.

    Bill:
    (3) I am the only man whose age is indivisible by 9;
    (4) I am 4 years older than Ashley;

    Charles:
    (5) The blacksmith’s age is 5 times his house number;

    David:
    (6) The miner lives 4 houses higher up the road from me;
    (7) The miner’s age is 3 times the dustman’s house number, but he is two-thirds the dustman’s age;

    Edward:
    (8) The dustman is twice as old as David;
    (9) I am the oldest man in the street.

    At what number does Ashley live?
    How old is the grocer?
    Who is the artist?

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

    [teaser688]

     
    • Jim Randell's avatar

      Jim Randell 8:48 am on 4 February 2021 Permalink | Reply

      I wrote a MiniZinc model to solve this puzzle, and then used the minizinc.py wrapper to format the output.

      This Python program runs in 178ms.

      from enigma import irange, printf
      from minizinc import MiniZinc
      
      model = """
      
        include "globals.mzn";
      
        % indices for A, B, C, D, E
        int: A = 1;
        int: B = 2;
        int: C = 3;
        int: D = 4;
        int: E = 5;
      
        % house numbers
        array [1..5] of var int: n;
      
        % even numbers
        constraint forall (i in 1..5) (n[i] > 0 /\ n[i] mod 2 = 0);
        % all different
        constraint all_different(n);
      
        % ages
        array [1..5] of var 16..64: a;
      
        % all different
        constraint all_different(a);
      
        % jobs
        var 1..5: Du;
        var 1..5: Gr;
        var 1..5: Mi;
        var 1..5: Bl;
        var 1..5: Ar;
      
        % all different
        constraint all_different([Du, Gr, Mi, Bl, Ar]);
      
        % (1) "the artist lives at number 10, next to C"
        constraint n[Ar] = 10;
        constraint n[C] = 8 \/ n[C] = 12;
      
        % (2) "nobody lives next to the grocer, although B is only 2 doors away"
        constraint forall (i in 1..5 where i != Gr) (abs(n[Gr] - n[i]) != 2);
        constraint abs(n[Gr] - n[B]) = 4;
      
        % (3) "B is the only one whose age is not divisible by 9"
        constraint forall (i in 1..5) (a[i] mod 9 != 0 <-> i = B);
      
        % (4) "B is 4 years older than A"
        constraint a[B] = a[A] + 4;
      
        % (5) "Bl age is 5x his house number"
        constraint a[Bl] = 5 * n[Bl];
      
        % (6) "Mi lives 4 houses further up the road than D"
        constraint n[Mi] = n[D] + 8;
      
        % (7) "Mi's age is 3x Du's house number; but Mi is 2/3 Du's age"
        constraint a[Mi] = 3 * n[Du];
        constraint 3 * a[Mi] = 2 * a[Du];
      
        % (8) "Du is 2x D's age"
        constraint a[Du] = 2 * a[D];
      
        % (9) "E is the oldest"
        constraint forall (i in 1..5 where i != E) (a[i] < a[E]);
      
        solve satisfy;
      
      """
      
      p = MiniZinc(model)
      
      name = [ "", "Ashley", "Bill", "Charles", "David", "Edward" ]
      for (n, a, Du, Gr, Mi, Bl, Ar) in p.solve(result="n a Du Gr Mi Bl Ar"):
        j = { Du: "Dustman", Gr: "Grocer", Mi: "Miner", Bl: "Blacksmith", Ar: "Artist" }
        for i in irange(1, 5):
          printf("{name}: house = {n}; age = {a}; job = {j}", name=name[i], n=n[i], a=a[i], j=j[i])
      

      Solution: Ashley lives at number 18. The grocer is 63. David is the artist.

      The complete solution:

      Ashley: house = 18; age = 36; job = Miner
      Bill: house = 8; age = 40; job = Blacksmith
      Charles: house = 12; age = 54; job = Dustman
      David: house = 10; age = 27; job = Artist
      Edward: house = 4; age = 63; job = Grocer

      Like

      • Frits's avatar

        Frits 11:52 am on 11 February 2021 Permalink | Reply

        @Jim, you didn’t code that B’s age may not be not divisible by 9.

        Like

        • Jim Randell's avatar

          Jim Randell 11:59 am on 11 February 2021 Permalink | Reply

          @Frits: Oh yes.

          I’ll update line 48 to:

            % (3) "B is the only one whose age is not divisible by 9"
            constraint forall (i in 1..5) (a[i] mod 9 != 0 <-> i = B);
          

          Like

      • Frits's avatar

        Frits 9:38 pm on 11 February 2021 Permalink | Reply

        “if stop: continue”

        My first version started looping over house numbers (where a maximum house number had to be specified). Looping over ages first eliminates this problem.

        from itertools import permutations
        
        # indices Ashley ... Edward
        (A, B, C, D, E) = (0, 1, 2, 3, 4)
        
        #  "B is 4 years older than A (which is a multiple of 9)"
        b_ages = [x for x in range(22, 65) if x % 9 != 0 and (x - 4) % 9 == 0]
        
        acde_ages = tuple(permutations(range(18, 65, 9), 4))    
        
        jobs_indices = list(permutations(range(5)))
        
        # update list hn for key <k> if value <v> doesn't exist yet in hn
        def update(k, v):
          if v in hn: 
            return False
          else:  
            hn[k] = v
            return True
          
        # start with ages divisble by 9
        for p4 in acde_ages:
          #  "Bl age is 5x his house number"
          if all(x % 5 != 0 for x in p4): 
            # at least one age must be a multiple of 5
            b_ages = [x for x in b_ages if x % 5 == 0]
        
          for b in b_ages:
            #  "B is 4 years older than A"
            if b != p4[0] + 4: continue
            
            # b must be placed at 2nd spot
            ag = (p4[0], b) + p4[1:]
            
            #  "E is the oldest"
            if ag[E] != max(ag): continue
            
            #  "Du is 2x D's age"
            if (2 * ag[D]) not in ag: continue
          
            # indices for Du, Gr, Mi, Bl, Ar
            for (Du, Gr, Mi, Bl, Ar) in jobs_indices:
              #  "Mi is 2/3 Du's age"
              if 3 * ag[Mi] != 2 * ag[Du]: continue
              
              #  "Bl age is 5x his house number"
              if ag[Bl] % 5 != 0: continue
              
              #  "Mi's age is 3x Du's house number"
              if ag[Mi] % 3 != 0: continue
             
              #  "Du is 2x D's age"
              if ag[Du] != 2 * ag[D]: continue
              
              # initialize house numbers
              hn = [0, 0, 0, 0, 0]
              
              #  "the artist lives at number 10, next to C"
              hn[Ar] = 10
              if not update(Du, ag[Mi] // 3): continue
              if not update(Bl, ag[Bl] // 5): continue
              
              if hn[C] and hn[C] not in {8, 12} : continue
              
              for loop in range(2):
                #  "Mi lives 4 houses further up the road than D"
                if hn[Mi] == 0 and hn[D]:
                  if not update(Mi, hn[D] + 8): break
                elif hn[Mi] and hn[D] == 0:
                  hn[D] = hn[Mi] - 8
                  if hn[Mi] < 10 or not update(D, hn[Mi] - 8): 
                    break
                elif hn[Mi] and hn[D]:  
                  if hn[Mi] != hn[D] + 8: break
        
                # B is only 2 doors away from grocer"
                if hn[B]:
                  if hn[Gr]:
                    if abs(hn[B] - hn[Gr]) != 4: break
                  else:
                    Gr_opts = []
                    if hn[B] + 4 not in hn: 
                      Gr_opts.append(hn[B] + 4)
                    if hn[B] > 5 and hn[B] - 4 not in hn:
                      Gr_opts.append(hn[B] - 4)
                    if len(Gr_opts) == 1:
                      if not update(Gr, Gr_opts[0]): break
                    elif len(Gr_opts) == 0: break
                else:
                  if hn[Gr]:
                    B_opts = []
                    if hn[Gr] + 4 not in hn: 
                      B_opts.append(hn[Gr] + 4)
                    if hn[Gr] > 5 and hn[Gr] - 4 not in hn:
                      B_opts.append(hn[Gr] - 4)
                    if len(B_opts) == 1:
                      if not update(B, B_opts[0]): break
                    elif len(B_opts) == 0: break
                
                #  "nobody lives next to the grocer"
                if hn[Gr] and any(abs(x - hn[Gr]) == 2 and x != 0 for x in hn): break
                
                #  "the artist lives at number 10, next to C"
                if hn[C] and hn[C] not in {8, 12} :  break
                
              else: # no break
                
                # all house numbers determined? 
                if 0 in hn: continue
        
                jobs = {Du:  "Dustman", Gr: "Grocer", Mi: "Miner", 
                        Bl: "Blacksmith", Ar: "Artist"}
        
                print("              Ashley", ) 
                print("              |   Bill") 
                print("              |   |   Charles") 
                print("              |   |   |   David") 
                print("              |   |   |   |   Edward") 
                print("house number", tuple(hn))
                print("age         ", ag)
                print("              |   |   |   |  ", jobs[4]) 
                print("              |   |   |  ", jobs[3]) 
                print("              |   |  ", jobs[2]) 
                print("              |  ", jobs[1]) 
                print("             ", jobs[0]) 
        

        Like

      • Frits's avatar

        Frits 12:58 pm on 10 August 2021 Permalink | Reply

        @Jim,

        Did you choose for MiniZinc because it allows for an unrestrained house number range?

        Like

        • Jim Randell's avatar

          Jim Randell 3:35 pm on 10 August 2021 Permalink | Reply

          @Frits: That was probably the main reason. Also using a declarative solver like MiniZinc meant I didn’t have to worry about what order to attack the problem in.

          Like

      • Frits's avatar

        Frits 2:13 pm on 10 August 2021 Permalink | Reply

        @Jim,

        If I add

        var 2..6: q = Ar + 1;

        then variable q is not in the result set.

        Is this a MiniZinc issue or a wrapper issue?

        Like

        • Jim Randell's avatar

          Jim Randell 3:29 pm on 10 August 2021 Permalink | Reply

          @Frits:

          The wrapper just parses the MiniZinc output and turns it into Python objects. So if MiniZinc doesn’t output a value it is not available in Python.

          But you should be able to use a constraint to get the value output by default:

          var 2..6: q;
          constraint q = Ar + 1;
          

          Like

  • Unknown's avatar

    Jim Randell 8:33 am on 2 February 2021 Permalink | Reply
    Tags:   

    Teaser 2774: Loaded dice 

    From The Sunday Times, 22nd November 2015 [link] [link]

     I have two traditional-looking dice, but only one of them is fair. In the other there is an equal chance of getting a 1, 2, 3, 4 or 5, but the die is loaded so that a 6 is thrown more than half the time. I threw the two dice and noted the total. It turned out that with my dice the chance of getting that total was double what it would have been with two fair dice.

    What (as a simple fraction) is the chance of getting a 6 with the loaded die?

    [teaser2774]

     
    • Jim Randell's avatar

      Jim Randell 8:35 am on 2 February 2021 Permalink | Reply

      The fair die has a probability of 1/6 of showing any particular face (scores 1-6).

      The unfair die has a probability of p of showing 6, and (1 − p) / 5 of showing the other faces (scores 1-5).

      If we consider the probability of throwing a total of t (scores 2-12) using both dice:

      There are { n = (t − 1) if t ≤ 6; otherwise n = (13 − t) } different ways of achieving the total.

      And with two fair dice (which have 36 equally like outcomes) the probability of throwing t is n / 36.

      And we want to find when the probability of throwing t with one fair and one unfair dice is twice this value: i.e. when the probability is n / 18.

      If t ≤ 6, then neither die can show a 6, so the probability of throwing t is:

      P = n ((1 − p) / 5) (1/6)

      which we can solve for P = n / 18:

      n ((1 − p) / 5) (1/6) = n / 18
      3(1 − p) = 5
      p = −2/3 (not possible)

      If t > 6, then 6 can show on the unfair dice, so we have two ways to make the total:

      P = [(n − 1) ((1 − p) / 5) (1/6)] + [p (1/6)]

      and we can solve this for P = n / 18:

      (n − 1) ((1 − p) / 5) (1/6) + p / 6 = n / 18
      3(n − 1)(1 − p) + 15p = 5n
      18p − 3np − 3 = 2n
      p = (2n + 3) / (18 − 3n)

      Also for t > 6 we have: n = 13 − t, so:

      p = (2(13 − t) + 3) / (18 − 3(13 − t))
      p = (29 − 2t) / (3t − 21)

      And we are interested in cases where t = 7 .. 12 and 0.5 < p ≤ 1:

      t = 7 ⇒ p = undefined (not possible)
      t = 8 ⇒ p = 13/3 (not possible)
      t = 9 ⇒ p = 11/6 (not possible)
      t = 10 ⇒ p = 1
      t = 11 ⇒ p = 7/12
      t = 12 ⇒ p = 1/3 (not permitted)

      So we find there are 2 possible situations:

      (1) The loaded die always shows 6, and the setter threw 10.
      The probability of throwing 10 with two fair dice is 1/12.
      The probability of throwing 10 with one fair die and the loaded die is 1/6.

      (2) The loaded die has a probability 7/12 of showing 6, and the setter threw 11.
      The probability of throwing 11 with two fair dice is 1/18.
      The probability of throwing 11 with one fair die and the loaded die is 1/9.

      I suspect we are to ignore that case where the loaded die always shows 6, as this would be quite obvious. (Although I would have thought a die that shows 6 more than half the time would be fairly obvious anyway). The setter could have explicitly excluded this case by specifying “an equal non-zero chance of scoring 1 – 5″ for the loaded die.

      Solution: The probability of scoring 6 with loaded die is 7/12.

      Like

    • John Crabtree's avatar

      John Crabtree 2:16 am on 4 February 2021 Permalink | Reply

      I was fortunate and took the probability of the throwing 6 with the biased die as n times that of any other number ie n>5 and p = n/(n+5).
      I reached t = 10 + 9/(n+2) when Jim’s two situations are immediately apparent.

      Like

  • Unknown's avatar

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

    Teaser 3045: Let Tel play BEDMAS hold ’em! 

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

    Awaiting guests on poker night, Tel placed (using only clubs, face-up, in order left-to-right) the Ace (=1) to 9 (representing numerals), then interspersed the Ten, Jack, Queen and King (representing –, +, × and ÷ respectively) in some order, but none together, among these.

    This “arithmetic expression” included a value over 1000 and more even than odd numbers. Applying BEDMAS rules, as follows, gave a whole-number answer. “No Brackets or powErs, so traverse the expression, left-to-right, doing each Division or Multiplication, as encountered, then, again left-to-right, doing each Addition or Subtraction, as encountered.”

    Tel’s auntie switched the King and Queen. A higher whole-number answer arose. Tel displayed the higher answer as a five-card “flush” in spades (the Jack for + followed by four numeral cards).

    Give each answer.

    [teaser3045]

     
    • Jim Randell's avatar

      Jim Randell 5:51 pm on 29 January 2021 Permalink | Reply

      I took “whole number” to be any integer, although you get the same answer if you just use positive integers.

      It was fun writing the [[ evaluate() ]] routine to perform the calculations in the described fashion (although it is just standard arithmetic precedence rules).

      The following Python program runs in 59ms.

      Run: [ @replit ]

      from enigma import (Rational, irange, subsets, tuples, peek, as_int, catch, update, multiset, join, printf)
      
      # implementation of rationals
      Q = Rational()
      
      # map operators to implementation
      fn = {
        '+': (lambda a, b: a + b),
        '-': (lambda a, b: a - b),
        '*': (lambda a, b: a * b),
        '/': (lambda a, b: Q(a, b)),
      }
      
      # evaluate the expression <ss>
      def evaluate(ss):
        ss = list(ss)
        # precedence order
        for ops in [set('*/'), set('+-')]:
          while True:
            # find the leftmost occurrence of an operator
            i = peek((i for (i, op) in enumerate(ss) if op in ops), default=-1)
            if i == -1: break
            # apply the operation
            assert 0 < i < len(ss) - 1
            n = fn[ss[i]](ss[i - 1], ss[i + 1])
            ss[i - 1:i + 2] = [n]
        assert len(ss) == 1
        return ss[0]
      
      # interleave 2 sequences (until one runs out)
      def interleave(s1, s2):
        (i1, i2) = (iter(s1), iter(s2))
        while True:
          try:
            yield next(i1)
            yield next(i2)
          except StopIteration:
            break
      
      # the string of digits
      digits = "123456789"
      ds = multiset.from_seq(digits)
      
      # slice up the string of digits
      n = len(digits)
      for ps in subsets(irange(1, n - 1), size=4, fn=list):
        ns = tuple(int(digits[i:j]) for (i, j) in tuples([0] + ps + [n]))
        # at least one number is greater than 1000
        if not any(n > 1000 for n in ns): continue
        # there are more even numbers than odd numbers
        if not (len(ns) > 2 * sum(n % 2 for n in ns)): continue
      
        # insert the operators
        for ops in subsets("+-*/", size=4, select="P"):
          # make the first expression
          ss1 = list(interleave(ns, ops))
          # evaluate it
          r1 = catch(as_int, evaluate(ss1))
          if r1 is None: continue
      
          # swap * and /
          ss2 = update(ss1, [(ss1.index('*'), '/'), (ss1.index('/'), '*')])
          r2 = catch(as_int, evaluate(ss2))
          if r2 is None or not(r1 < r2): continue    
      
          # the result is a positive 4-digit number
          if r2 < 1000 or r2 > 9999: continue
          # that can be represented using the available digits
          if not ds.issuperset(str(r2)): continue
      
          # output solution
          printf("{ss1} = {r1}; {ss2} = {r2}", ss1=join(ss1, sep=" "), ss2=join(ss2, sep=" "))
      

      Solution: Tel’s answer was 1274. Auntie’s answer was 1289.

      The expressions are:

      Tel: 1234 + 56 × 7 ÷ 8 − 9 = 1274
      Auntie: 1234 + 56 ÷ 7 × 8 − 9 = 1289

      There are only 6 ways to split up the digits in the required fashion:

      1 | 2 | 34 | 5678 | 9
      1 | 2 | 3456 | 78 | 9
      12 | 3 | 4 | 5678 | 9
      12 | 3456 | 7 | 8 | 9
      1234 | 5 | 6 | 78 | 9
      1234 | 56 | 7 | 8 | 9

      Which lead to 22 expressions that evaluate to a positive integer.

      There are 2 further candidate solutions resulting in 4-digit positive numbers, but Tel would not be able to display the result as described:

      Tel: 12 × 3 ÷ 4 + 5678 − 9 = 5678
      Auntie: 12 ÷ 3 × 4 + 5678 − 9 = 5685

      Tel: 1234 − 56 ÷ 7 × 8 + 9 = 1179
      Auntie: 1234 − 56 × 7 ÷ 8 + 9 = 1194

      Like

      • Tony Brooke-Taylor's avatar

        Tony Brooke-Taylor 11:02 am on 31 January 2021 Permalink | Reply

        I was going mad trying to find my error but when I compared my solution to yours I get the same. I guess the puzzle allows for an Ace in the flush.

        Like

        • Jim Randell's avatar

          Jim Randell 2:05 pm on 31 January 2021 Permalink | Reply

          @Tony: There is only one candidate solution that is positive, and is composed of 4 distinct positive digits. And I was happy enough that this could be represented using the described cards.

          Fortunately I don’t know enough about poker to know if this counts as a “5 card flush” or not.

          Like

    • Frits's avatar

      Frits 9:23 pm on 30 January 2021 Permalink | Reply

      from enigma import SubstitutedExpression
      from itertools import  permutations
      
      def check(li):
        # credit: B. Gladman
        # there must be more even than odd numbers
        if sum([x & 1 for x in li]) > 2:
          return ""
          
        li2 = [str(x) for x in li]  
        for p in permutations("*/+-", 4):
          exp = ''.join(a + b for a, b in zip(li2, p + ('',)))
          ev = eval(exp) 
          if ev % 1 == 0 and ev >= 0:
            exp2 = ''.join(chr(ord(c) ^ 5) if c in '/*' else c for c in exp)
            ev2 = eval(exp2) 
            if ev2 > ev and ev2 % 1 == 0:
              ev2 = int(ev2)
              # larger answer has distinct non-zero digits
              if "0" not in str(ev2) and len(set(str(ev2))) == len(str(ev2)):
                return exp + " ; " + exp2
            
        return ""
      
      
      # the alphametic puzzle
      p = SubstitutedExpression(
        [
        "T + 1 == U or T == 0",
        "S + 1 == T or S == 0",
        "R + 1 == S or R == 0",
        "[x for x in [R, S, T, U] if x > 0][0] == Q + 1",
      
        "P + 1 == Q or P == 0",
        "O + 1 == P or O == 0",
        "N + 1 == O or N == 0",
        "[x for x in [N, O, P, Q] if x > 0][0] == M + 1",
      
        "L + 1 == M or L == 0",
        "K + 1 == L or K == 0",
        "J + 1 == K or J == 0",
        "[x for x in [J, K, L, M] if x > 0][0] == H + 1",
      
        "G + 1 == H or G == 0",
        "F + 1 == G or F == 0",
        "E + 1 == F or E == 0",
        "[x for x in [E, F, G, H] if x > 0][0] == D + 1",
      
        "C + 1 == D or C == 0",
        "B + 1 == C or B == 0",
        "A + 1 == B or A == 0",
        "[x for x in [A, B, C, D] if x > 0][0] == 1",
        
        # included a value over 1000
        "sum([A > 0, E > 0, J > 0, N > 0, R > 0]) = 1",
        
        "len(check([ABCD, EFGH, JKLM, NOPQ, RSTU])) > 0",
        
        ],
        answer="check([ABCD, EFGH, JKLM, NOPQ, RSTU])", 
        distinct="",
        env=dict(check=check),
        d2i=dict([(0, "DHMQU")] + 
                 [(1, "EFGHJKLMNOPQRSTU")] + 
                 [(2, "JKLMNOPQRSTU")] + 
                 [(3, "NOPQRSTU")] +
                 [(4, "RSTU")] +
                 [(5, "RSTU")] +
                 [(k, "U") for k in range(6, 9)]
                 ),
      
        #reorder=0,
        verbose=0
        )   
        
      # Print answer
      for (_, ans) in p.solve():
        print(f"{ans}")
      

      Like

  • Unknown's avatar

    Jim Randell 9:19 am on 28 January 2021 Permalink | Reply
    Tags:   

    Brain-Teaser 685: Overseas mail 

    From The Sunday Times, 1st September 1974 [link]

    We were visiting the island state of Kimbu and had come to the post-office to send off some parcels to friends at home. The island’s currency is the pim, and the postmaster told us that he had only stamps of five different face-values, as these had to be used up before a new issue of stamps was introduced.

    These stamps were black, red, green, violet, and yellow, in descending order of values, the black being the highest denomination and the yellow the lowest.

    One parcel required stamps to the value of 100 pims and we were handed 9 stamps; 5 black, one green, and 3 violet. The other two parcels required 50 pims’ worth each, and for these we were given two different sets of 9 stamps.

    One consisted of 1 black and 2 of each of the other colours, and the other set contained 5 green and 1 of each of the others.

    What would be been the smallest number of stamps needed for a 50-pim parcel, and of which colours?

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

    [teaser685]

     
    • Jim Randell's avatar

      Jim Randell 9:20 am on 28 January 2021 Permalink | Reply

      I assumed the denominations of the stamps were all positive integer values.

      We can place lower and upper limits on the values of the stamps.

      The stamps are ordered: B > R > G > V > Y, which gives minimum values of: 5, 4, 3, 2, 1.

      And 5 black stamps are used in making a value of 100, so black must have a value of less than (100 − (3 + 3×2)) / 5 = 18.2.

      Similarly, 5 green stamps are used in making a value of 50, so green must have a value of less than (50 − (5 + 4 + 2 + 1)) / 5 = 7.6.

      So we can write down the following ranges for each denomination:

      B: 5..18
      R: 4..17
      G: 3..7
      V: 2..6
      Y: 1..5

      And we have three equations relating the values. So choosing values for V and Y will give us a set of 5 simultaneous equations that can be solved to find the other values.

      This Python program use the [[ matrix.linear() ]] solver for systems of linear equations from the enigma.py library. It runs in 63ms.

      Run: [ @repl.it ]

      from enigma import (irange, subsets, matrix, as_int, express, group, printf)
      
      # choose values for Y and V
      for (Y, V) in subsets(irange(1, 6), size=2):
      
        # make the equations
        eqs = [
          # B  R  G  V  Y = ???
          ((5, 0, 1, 3, 0), 100), # 100-pim parcel
          ((1, 2, 2, 2, 2),  50), # 1st 50-pim parcel
          ((1, 1, 5, 1, 1),  50), # 2nd 50-pim parcel
          ((0, 0, 0, 0, 1),   Y), # Y value
          ((0, 0, 0, 1, 0),   V), # V value
        ]
      
        # solve the equations for integer values
        # (either matrix.linear() or as_int() may raise a ValueError)
        try:
          (B, R, G, V, Y) = (as_int(x) for x in matrix.linear(*zip(*eqs)))
        except ValueError:
          continue
      
        # check ordering
        if not (B > R > G > V > Y > 0): continue
      
        # find combinations that make 50, grouped by the total number of stamps
        d = group(express(50, [Y, V, G, R, B]), by=sum)
        # output minimal configurations
        k = min(d.keys())
        for (y, v, g, r, b) in d[k]:
          printf("{k} stamps; 50 = {b}x {B} (black), {r}x {R} (red), {g}x {G} (green), {v}x {V} (violet), {y}x {Y} (yellow)")
      

      Solution: A 50-pim parcel can be sent using 5 stamps: 2 black, 1 red, 1 green, 1 yellow.

      The values of the stamps are:

      black = 18 pim
      red = 9 pim
      green = 4 pim
      violet = 2 pim
      yellow = 1 pim

      So each stamp is half (rounded down) the next highest value.

      There are 621 different ways to make 50 using the above denominations.


      Here’s a manual derivation of the denominations:

      [1] B + 2R + 2G + 2V + 2Y = 50
      [2] B + R + 5G + V + Y = 50

      Taking 2×[2][1] gives:

      B + 8G = 50

      And we know G is in the range 3 .. 7, and B is in the range 5 .. 18 and G < B:

      G = 3 ⇒ B = 26 [not possible]
      G = 4 ⇒ B = 18
      G = 5 ⇒ B = 10
      G = 6 ⇒ B = 2 [not possible]
      G = 7 ⇒ B = −6 [not possible]

      And:

      5B + G + 3V = 100

      Where V is in the range 2 .. 6 and V < G:

      G = 4, B = 18 ⇒ V = 2
      G = 5, B = 10 ⇒ V = 15 [not possible]

      So we have: V = 2, G = 4, B = 18.

      From which we see Y = 1, and R = 9.

      Like

      • Jim Randell's avatar

        Jim Randell 5:55 pm on 28 January 2021 Permalink | Reply

        Here’s a solution using the [[ SubstitutedExpression ]] solver from the enigma.py library. It runs in 122ms.

        It operates in base 19 as B ≤ 18.

        The only other restriction I used was that we already know that 50 is possible with 9 stamps, so I limit the numbers used for any single denomination to 8. (We can also do this in my Python program above, by passing [[ qs=irange(0, 8) ]] to the call to [[ express() ]] and that saves a little bit of time).

        We could restrict the other denominations in line with the ranges given above, but it is not necessary for a decent run time.

        Run: [ @repl.it ]

        #! python -m enigma -rr
        
        SubstitutedExpression
        
        --base=19
        --symbols="BRGVYbrgvy"
        --distinct="BRGVY"
        
        # work out the denominations
        "Y > 0"
        "V > Y"
        "G > V"
        "R > G"
        "B > R"
        
        "5 * B + G + 3 * V == 100" # 100 pim parcel
        "B + 2 * (R + G + V + Y) == 50" # 1st 50 pim parcel
        "5 * G + (B + R + V + Y) == 50" # 2nd 50 pim parcel
        
        # find minimal amount to make 50
        "b * B + r * R + g * G + v * V + y * Y == 50"
        
        --answer="((b, r, g, v, y), (B, R, G, V, Y))"
        --code="fmin = lambda ss: min(ss, key=unpack(lambda ns, ds: sum(ns)))"
        --accumulate="fmin"
        
        --verbose=16
        
        # [optional] we already know 50 is possible with 9 stamps
        --invalid="9-18,brgvy"
        

        Like

    • Frits's avatar

      Frits 12:24 pm on 28 January 2021 Permalink | Reply

      from enigma import SubstitutedExpression
      
      # the alphametic puzzle
      p = SubstitutedExpression(
        [
        # try to avoid looping over BL and RE
        "G > V",
        "V > Y",
        # (5, 0, 1, 3, 0)  100-pim parcel
        "(100 - G - 3*V) // 5 = BL",
        
        # (1, 2, 2, 2, 2)  1st 50-pim parcel
        "(50 - BL) // 2  - (G + V + Y) = RE",
        "RE > G",
        "BL > RE",
        # (1, 1, 5, 1, 1)  2nd 50-pim parcel
        "BL + RE + 5*G + V + Y = 50",
        
        # Pick M blacks, N reds, O greens, P violets and Q yellows
        # we already know a 50-pim parcel can be formed with 9 stamps
        "M + N < 10",
        "M + N + O < 10",
        "M + N + O + P < 10",
        "M + N + O + P + Q < 10",
        
        "M*BL + N*RE + O*G + P*V + Q*Y = 50", 
        ],
        answer="M + N + O + P + Q, M, N, O, P, Q",
        distinct="",
        accumulate=min,
        # BL: 5..18  RE: 4..12  G: 3..7  V: 2..6  Y: 1..5 
        d2i=dict([(0, "GVY")] + 
                 [(1, "VG")] + 
                 [(2, "RBG")] + 
                 [(k, "RB") for k in range(3, 6)] +
                 [(6, "YRB")] +
                 [(7, "YVRB")] +
                 [(k, "YVGRBMNOPQ") for k in range(8, 10)]),
      
        reorder=0,
        verbose=0
        )   
        
        
      # solve the puzzle
      r = p.run()
      
      colors = ["black", "red", "green", "violet", "yellow"]
      for (x, y) in zip(list(r.accumulate)[1:], colors):
        if x > 0:
          print(x, y)
      

      Like

    • GeoffR's avatar

      GeoffR 3:04 pm on 28 January 2021 Permalink | Reply

      % A Solution in MiniZinc
      include "globals.mzn";
      
      % black, red, green, violet, yellow stamps
      % ... using Jim's analysis values for all stamp ranges
      var 5..18:B; var 4..17:R; var 3..7: G;
      var 2..6: V; var 1..5:Y;
      
      % stamp values all different and in colour descending values
      constraint all_different([B, R, G, V, Y]);
      constraint B > R /\ R > G /\ G > V /\ V > Y;
      
      % numbers of each colour of stamp to make 50 pim
      var 0..9:NumB; var 0..9:NumG; var 0..9:NumV;
      var 0..9:NumR; var 0..9:NumY;
      
      % three stamp equations stated in teaser description
      % 1. 5 black, one green, and 3 violet stamps for 100 pim
      constraint 5*B + G + 3*V == 100;
      
      % two different equations for stamps of 50 pim
      % 2. one set consisted of 1 black and 2 of each of the other colours
      constraint B + 2*R + 2*G + 2*V + 2*Y == 50;
      
      % 3. other set contained 5 green and 1 of each of other colours 
      constraint B + R + 5*G + V + Y == 50;
      
      % General equation to make 50 pims value
      constraint NumB*B + NumR*R +  NumG*G + NumV*V + NumY*Y == 50;
      
      % minimise total number of coins used to make 50 units value
      solve minimize(NumB + NumR + NumG + NumV + NumY);
      
      output [ "Black stamp value = " ++ show(B) ++ " pim"
      ++ ", Red stamp value = " ++ show(R) ++ " pim"
      ++ "\n" ++ "Green stamp value = " ++ show(G) ++ " pim"
      ++ ", Violet stamp value = " ++ show(V) ++ " pim"
      ++ "\n" ++ "Yellow stamp value = " ++ show(Y) ++ " pim"
      ++ "\n"
      ++ "\n" ++ "Smallest number of stamps needed for a 50-pim parcel" 
      ++ "\n" ++ "[B, R, G, V, Y] =  "
      ++ show (NumB),", ",show(NumR),", ",show(NumG),", ",show(NumV),", ",show(NumY) ];
      
      % Black stamp value = 18 pim, Red stamp value = 9 pim
      % Green stamp value = 4 pim, Violet stamp value = 2 pim
      % Yellow stamp value = 1 pim
      
      % Smallest number of stamps needed for a 50-pim parcel
      % [B, R, G, V, Y] =  2, 1, 1, 0, 1
      % ----------
      % ==========
      
      
      
      

      Like

    • John Crabtree's avatar

      John Crabtree 12:07 am on 29 January 2021 Permalink | Reply

      Eq. 1: 5.B + 0.R + 1.G +3.V + 0.Y = 100
      Eq. 2: 1.B + 2.R + 2.G +2.V + 2.Y = 50
      Eq. 3 : 1.B + 1.R + 5.G +1.V + 1.Y = 50
      10 * Eq. 3 – 5 * Eq. 2 – Eq. 1 gives 39.G – 3.V = 150, or 13.G – V = 50
      Assuming integer solutions, G = 4, V = 2 and then B = 18, R + Y = 10 etc

      Like

  • Unknown's avatar

    Jim Randell 8:16 am on 26 January 2021 Permalink | Reply
    Tags: ,   

    Teaser 2772: Madam I’m Adam 

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

    Adam noticed that today’s Teaser number was a four-figure palindrome. He told me that he had written down [a] four-figure palindrome and he asked me to guess what it was. I asked for some clues regarding its prime factors and so he told me, in turn, whether his number was divisible by 2, 3, 5, 7, 11, 13, … Only after he had told me whether it was divisible by 13 was it possible to work out his number.

    What was his number?

    When this was originally published “another” was used where I have placed “[a]”, but this makes the puzzle unsolvable. However, as presented above, there is a unique solution.

    [teaser2772]

     
    • Jim Randell's avatar

      Jim Randell 8:17 am on 26 January 2021 Permalink | Reply

      The use of “another” in the puzzle text implies that the palindrome 2772 is excluded from the set of possibilities, but if this is the case the puzzle is not solvable. So instead we just consider that Adam has told the setter that he has written down some 4-digit palindrome (which may be 2772), and the setter has to guess it.

      This Python program considers increasing prime numbers p, and looks for palindromes that can be uniquely identified by knowing if it is divisible by primes up to (and including) p.

      To solve this puzzle we look up to p = 13, but the maximum prime can be specified on the command line. (We have to go to 859 to be sure of determining the palindrome).

      The program runs in 45ms.

      Run: [ @repl.it ]

      from enigma import Primes, irange, filter_unique, diff, join, arg, printf
      
      # max prime
      N = arg(13, 0, int)
      
      # 4-digit palindromes
      palindromes = sorted(1001 * a + 110 * b for a in irange(1, 9) for b in irange(0, 9))
      #palindromes.remove(2772) # puzzle text says 2772 is excluded, but that makes it impossible
      
      # palindromes unique by divisibility of primes, but not unique by shorter sequences
      r = set()
      # consider sequences of primes by increasing length
      ps = list()
      for p in Primes(N + 1):
        ps.append(p)
        # find unique palindromes by this sequence of primes
        s = filter_unique(palindromes, lambda x: tuple(x % p == 0 for p in ps)).unique
        # unique palindromes we haven't seen before
        s = diff(s, r)
        if s:
          printf("{p} -> {s}", s=join(s, sep=", ", enc="()"))
        # update the list of unique palindromes
        r.update(s)
      

      Solution: The number was 6006.


      If 2772 is removed from the set of possible palindromes (by removing the comment from line 8), we see that 8778 would also be uniquely identified by the divisibility values for primes up to 13.

      So although the setter would know the answer (because he knows if the number is divisible by 13 or not), we can’t work it out:

      6006 → 2=Y, 3=Y, 5=N, 7=Y, 11=Y, 13=Y
      8778 → 2=Y, 3=Y, 5=N, 7=Y, 11=Y, 13=N, [17=N, 19=Y]
      2772 → 2=Y, 3=Y, 5=N, 7=Y, 11=Y, 13=N, [17=N, 19=N]

      But, if 2772 is included in the set of possible palindromes, then 8778 and 2772 cannot be distinguished until we are told the answer for divisibility by 19, so the fact that the setter can work out the number at p = 13 means that it must be 6006.

      There are two palindromes that can be distinguished with a shorter sequence of answers:

      5005 → 2=N, 3=N, 5=Y, 7=Y
      5775 → 2=N, 3=Y, 5=Y, 7=Y

      Note that all palindromes are divisible by 11.

      Like

    • Frits's avatar

      Frits 8:08 pm on 26 January 2021 Permalink | Reply

      With fewer modulo calculations.

      from collections import Counter
       
      # max prime
      N = 13
      
      # list of prime numbers
      P = [2, 3, 5, 7, 11, 13]
       
      # 4-digit palindromes
      palindromes = sorted(1001 * a + 110 * b for a in range(1, 10) 
                                              for b in range(0, 10))
      
      # initialize dictionary (divisibility by prime)                                    
      d = {key: [] for key in palindromes}
       
      singles = []
      # consider sequences of primes by increasing length
      for p in P:
        # maintain dictionary (divisibility by prime)   
        for x in palindromes:
          d[x].append(x % p == 0)
         
        c = Counter(map(tuple, d.values()))
        # get combinations unique by divisibility of primes
        # but not unique by shorter sequences
        c2 = [k for k, v in c.items() if v == 1 and 
              all(k[:-i] not in singles for i in range(1, len(k)))]
        
        if len(c2) == 1:
          for x in palindromes:
            if d[x] == list(c2[0]):
              print(f"{p} -> {x}")
              if p != N:
                print("Error: palindrome already known before prime", N)
              exit()  
              
        # add "unique by shorter sequences" to singles
        for x in [k for k, v in c.items() if v == 1]:
          singles.append(x)
      

      Like

  • Unknown's avatar

    Jim Randell 10:07 am on 24 January 2021 Permalink | Reply
    Tags: by: A. Bowie   

    Brain-Teaser 19: Hillside Farm 

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

    My friend, Mr. Little, is a farmer whose chief interest is in sheep, but keeps a number of Shorthorn and Redpoll (polled) cattle too.

    Mr. Little is six years older than his wife and 33 years older than his son, George. George is twice as old as Mary, the daughter of the house, whose birthday happens to be today.

    There is no tractor at Hillside Farm and Mr. Little does the ploughing behind his two horses leaving a furrow nine inches wide. The number of acres ploughed this year happens to be the same as the number of cats on the farm.

    Readers are invited to determine the information required to complete the following “cross-number” puzzle:

    Across:

    1a: Number of Mr. Little’s Redpoll cows.
    3a: Square of the number of Redpoll cows.
    5a: Total number of cows.
    6a: Mr. Little’s age.
    7a: Number of ewes per ram in Mr. Little’s flock. This also happens to be the number of miles walked by Mr. Little in doing this year’s ploughing.
    9a: Acreage of rough grass land. (1.5 acres per ewe).
    11a: Age of Mrs. Little.
    12a: Number of ewes in flock (in scores).
    13a: Cube of the number of collies on the farm.

    Down:

    1d: Area of rectangular farmyard in square yards.
    2d: Length of farmyard in yards.
    3d: Number of ewes on the farm.
    4d: Age at which Mr. Little intends to retire.
    7d: Seven times Mr. Little’s age.
    8d: Total number of sheep.
    10d: Number of rams on the farm.
    12d: Total number of horns possessed by all the cattle.

    [teaser19]

     
    • Jim Randell's avatar

      Jim Randell 10:12 am on 24 January 2021 Permalink | Reply

      I didn’t need to use all the information given to solve the puzzle. But I did need to look up what “polled” meant – it means “lacking horns”.

      I used the [[ SubstitutedExpression ]] solver from the enigma.py library to solve the puzzle as a collection of alphametic expressions.

      The following run file executes in 131ms.

      Run: [ @repl.it ]

      #! python -m enigma -rr
      
      # consider the grid:
      #
      #  A B # C D E
      #  F G # H I #
      #  J # K L # M
      #  N P Q R # S
      #  # T U # V W
      #  # # # X Y Z
      
      SubstitutedExpression
      
      --distinct=""
      
      # 1a. AB = "Number of redpoll cows"
      # 3a. CDE = "Square of the number of redpoll cows"
      # 5a. FG = "Total number of cows"
      # 12d. VY = "Total number of horns possessed by all the cattle"
      "AB * AB = CDE"
      "AB < FG"
      "2 * (FG - AB) = VY"
      
      # 6a. HI = "Mr Little's age" (= wife + 6)
      # 11a. TU = "Age of Mrs. Little"
      # 7d. KQU = "Seven times Mr Little's age"
      # 4d. DI = "Age at which Mr Little intends to retire"
      "TU + 6 = HI"
      "HI * 7 = KQU"
      "HI < DI"
      
      # 7a. KL = "Number of ewes per ram."
      # 9a. NPQR = "Acreage of rough grassland. 1.5 acres per ewe"
      # 12a. VW = "Number of ewes in flock (in scores)
      # 3d. CHLR = "Number of ewes on the farm"
      # 10d. PT = "Number of rams on the farm"
      # 8d. MSWZ = "Total number of sheep"
      "VW * 20 = CHLR"
      "2 * NPQR == 3 * CHLR"
      "CHLR + PT = MSWZ"
      "PT * KL = CHLR"
      
      # 13a. XYZ = "Cube of the number of collies on the farm"
      "is_cube(XYZ)"
      
      # 1d. AFJN = "Area of farmyard in square yards"
      # 2d. BG = "Length of farmyard in yards"
      "AFJN % BG = 0"
      
      # across / down
      --template="(AB CDE FG HI KL NPQR TU VW XYZ) (AFJN BG CHLR DI KQU MSWZ PT VY)"
      --solution=""
      

      Solution: The completed grid looks like this:

      From which we get the following information

      Number of redpoll cows = 13
      Number of shorthorn cows = 36
      (Total number of cows = 49)
      (Total number of horns = 72)

      Number of ewes = 1540 (= 77× 20)
      Number of rams = 35
      (Ewes/ram = 44)
      (Total number of sheep = 1575)
      Area of rough grassland = 2310

      Mr. Little’s age = 59
      Retirement age = 69
      (Mrs. Little’s age = 53)

      Area of farmyard = 1482
      Length of farmyard = 39
      (Width of farmyard = 38)

      Number of collies = 5

      And from the text we can deduce the following further information:

      (George’s age = 26)
      (Mary’s age = 13)

      (Number of cats = 4)

      Like

    • Frits's avatar

      Frits 2:06 pm on 24 January 2021 Permalink | Reply

       
       consider the grid:
      
        A B # C D E
        F G # H I #
        J # K L # M
        N P Q R # S
        # T U # V W
        # # # X Y Z
      
      Rules:
      
      AB * AB = CDE
      AB < FG
      2 * (FG - AB) = VY
      TU + 6 = HI
      HI * 7 = KQU
      HI < DI
      VW * 20 = CHLR
      2 * NPQR = 3 * CHLR
      CHLR + PT = MSWZ
      PT * KL = CHLR
      is_cube(XYZ)
      AFJN % BG = 0
      
      
      VW * 20 = CHLR -> R=0 C=1 L=even
      AB * AB = 1DE --> A=1 
      HI>33 and DI>HI ->  CDE in (144, 169, 196)
      PT * KL = CHL0 -> T=5 or L=0
      1 mile=63360 inches, 1 acre=6272640 square inches
      no cats = KL * 63360 * 9 / 6272640 = KL / 11 
      KL multiple of 11 -> K=L, K!=0 so T=5
      CHL0 + PT = MSWZ -> Z=T so Z=5 -> XYZ=125 
      Age George is even so TU and HI are odd
      TU HI KQU
      51 57 399 U's inconsistent 
      53 59 413 OK
      55 61 427 U's inconsistent 
      57 53 441 U's inconsistent 
      59 65 455 U's inconsistent 
      So TU=53, HI=59, KQU=413 -> L=4
      VW * 20 = 1540 -> VW=77
      2 * NPQR = 3 * 1540 -> NPQR=2310
      CHLR + PT = MSWZ  -> MSWZ=1575
      2 * (FG - AB) = 72 -> FG = AB + 36
      HI=59 -> DI>59 and CDE is 169 or 196
      so AB/FG is 13/49 or 14/50
      1FJ2 % BG = 0 -> AB=13 FG=49 CDE=169
      14J2 % 39 = 0 -> J=8
      

      Like

    • Frits's avatar

      Frits 6:12 pm on 24 January 2021 Permalink | Reply

      Similar.

      from functools import reduce
       
      # convert numbers to digit sequences and vice versa
      # Credit: B. Gladman
      d2n = lambda s: reduce(lambda x,  y: 10 * x + y, s) 
      n2d = lambda n: [n] if n < 10 else n2d(n // 10) + [n % 10] 
      
      # consider the grid:
      #
      #  A B # C D E
      #  F G # H I #
      #  J # K L # M
      #  N P Q R # S
      #  # T U # V W
      #  # # # X Y Z
      
      # "VW * 20 = CHLR"
      for V in range(1, 10):
        for W in range(10):
          CHLR = d2n([V, W]) * 20
          if len(str(CHLR)) != 4: continue
          (C, H, L, R) = n2d(CHLR)
          
          # "AB * AB = CDE"
          for A in range(1, 10):
            for B in range(1, 10):
              AB = d2n([A, B])
              CDE = AB * AB
              if len(str(CDE)) != 3: continue
              (c, D, E) = n2d(CDE)
              if c != C: continue
              
              # "2 * NPQR == 3 * CHLR"
              NPQR = int(1.5 * (CHLR))
              if len(str(NPQR)) != 4: continue
              (N, P, Q, r) = n2d(NPQR)
              if r != R: continue
              
              # "PT * KL = CHLR"
              for K in range(1, 10):
                PT = CHLR // d2n([K, L])
                if len(str(PT)) != 2: continue
                (p, T) = n2d(PT)
                if p != P: continue
                
                # "CHLR + PT = MSWZ"
                MSWZ = CHLR + PT
                if len(str(MSWZ)) != 4: continue
                (M, S, W, Z) = n2d(MSWZ)
                
                # "2 * (FG - AB) = VY"
                for Y in range(10):
                  FG = AB + d2n([V, Y]) // 2
                  if len(str(FG)) != 2: continue
                  (F, G) = n2d(FG)
                  
                  # "TU + 6 = HI"
                  for U in range(10):
                    HI = d2n([T, U]) + 6
                    if len(str(HI)) != 2: continue
                    (h, I) = n2d(HI)
                    if h != H: continue
                    
                    # "HI < DI"
                    if not HI < d2n([D, I]): continue
                    
                    # "HI * 7 = KQU"
                    KQU = HI * 7
                    if len(str(KQU)) != 3: continue
                    (k, q, u) = n2d(KQU)
                    if k != K or q != Q or u != U: continue
                    
                    # "is_cube(XYZ)"
                    for X in [1, 2, 3, 5, 7]:
                      XYZ = d2n([X, Y, Z])
                      if not XYZ in {125, 216, 343, 512, 729}: continue
                      
                      # "AFJN % BG = 0"
                      for J in range(10):
                        AFJN = d2n([A, F, J, N])
                        if not AFJN % d2n([B, G]) == 0: continue
                        
                        print("CHLR CDE AB PT FG HI KQU MSWZ XYZ")
                        print(CHLR, CDE, AB, PT, FG, HI, KQU, MSWZ, XYZ)
                    
                        print("A B C D E F G H I J K L")
                        print(A, B, C, D, E, F, G, H, I, J, K, L)
                        print("M N P Q R S T U V W X Z")
                        print(M, N, P, Q, R, S, T, U, V, W, X, Z)
      

      Like

  • Unknown's avatar

    Jim Randell 5:02 pm on 22 January 2021 Permalink | Reply
    Tags:   

    Teaser 3044: Building blocks 

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

    The kids had used all the blocks each of them owned (fewer than 100 each) to build triangular peaks — one block on the top row, two on the next row, and so on.

    “My red one’s nicer!”

    “My blue one’s taller!”

    “Why don’t you both work together to make one bigger still?” I said.

    I could see they could use all these blocks to make another triangle.

    This kept them quiet until I heard, “I bet Dad could buy some yellow blocks to build a triangle bigger than either of ours was, or a red and yellow triangle, or a yellow and blue triangle, with no blocks ever left over.”

    This kept me quiet, until I worked out that I could.

    How many red, blue and yellow blocks would there be?

    [teaser3044]

     
    • Jim Randell's avatar

      Jim Randell 5:18 pm on 22 January 2021 Permalink | Reply

      The following Python program runs in 43ms.

      Run: [ @repl.it ]

      from enigma import tri, irange, inf, first, subsets, is_triangular, printf
      
      # generate triangular numbers from tri(a) to tri(b)
      tris = lambda a=1, b=inf: (tri(i) for i in irange(a, b))
      
      def solve():
        # collect triangular numbers less than 100
        ts = first(tris(), (lambda x: x < 100))
      
        # choose (R, B) pairs that sum to another triangular number
        RBs = list(s for s in subsets(ts, size=2) if is_triangular(sum(s)))
      
        # consider values for Y
        for Y in tris(is_triangular(min(B for (R, B) in RBs)) + 1):
          # look for R, B pairs that work with Y
          for (R, B) in RBs:
            if B < Y and is_triangular(R + Y) and is_triangular(B + Y):
              yield (R, B, Y)
      
      # look for the first solution
      for (R, B, Y) in solve():
        printf("R={R} B={B} Y={Y}")
        break
      

      Solution: There were 45 red blocks; 91 blue blocks; 990 yellow blocks.

      We have:

      R = 45 = T(9)
      B = 91 = T(13)
      R + B = 136 = T(16)

      Y = 990 = T(44)
      R + Y = 1035 = T(45)
      B + Y = 1081 = T(46)


      It makes me wish Python allowed something like a [[ while ... ]] clause in list comprehensions, so they could be terminated early:

      ts = (t for t in tris() while t < 100)
      

      This exact syntax was actually proposed [ PEP 3142 ], but not implemented.

      I’ve folded the functionality into the [[ first() ]] function in the enigma.py library, so it can take a function and collection of items will stop when the function returns a false value for an item.

      Like

      • Frits's avatar

        Frits 11:11 pm on 22 January 2021 Permalink | Reply

        @Jim, I can imagine a similar puzzle in 3D. Didn’t we have such a puzzle before?

        Like

      • Jim Randell's avatar

        Jim Randell 10:58 am on 23 January 2021 Permalink | Reply

        Here’s an exhaustive version, based on the observation that if Y = tri(y) then R ≥ y + 1, otherwise it will not be possible to complete an additional row of the Y triangle using R blocks.

        It still runs in 46ms.

        Run: [ @repl.it ]

        from enigma import tri, irange, inf, first, subsets, is_triangular, printf
        
        # generate triangular numbers from tri(a) to tri(b)
        tris = lambda a=1, b=inf: (tri(i) for i in irange(a, b))
        
        # collect triangular numbers less than 100
        ts = first(tris(), (lambda x: x < 100))
        
        # choose (R, B) pairs that sum to another triangular number
        for (R, B) in subsets(ts, size=2):
          if not is_triangular(R + B): continue
        
          # consider values for Y
          for Y in tris(is_triangular(B) + 1, R - 1):
            if is_triangular(R + Y) and is_triangular(B + Y):
              printf("R={R} B={B} Y={Y}")
        

        This code can also be used to explore larger solutions by increasing the limit at line 7.

        There is 1 further solution below 1275:

        R = T(23) = 276
        B = T(30) = 465
        Y = T(90) = 4095

        Like

    • Frits's avatar

      Frits 10:55 pm on 22 January 2021 Permalink | Reply

      Nothing has been implemented regarding upper limits of RE and BL, the program runs fast enough (between 31 ms and 46 ms).

      from enigma import SubstitutedExpression 
      
      # the alphametic puzzle
      p = SubstitutedExpression(
        [
        # number of reds    = RE * (RE + 1)/2
        # number of blues   = BL * (BL + 1)/2
        # number of yellows = YW * (YW + 1)/2
        "RE > 0",
        "BL > 0", 
        
        # My blue one's taller
        "BL > RE",
        
        # fewer than 100 each
        "BL * (BL + 1) // 2 < 100",
        
        # they could use all these blocks to make another triangle
        "RE * (RE + 1) // 2 + BL * (BL + 1) // 2 == JK * (JK + 1) // 2",
        
        # Dad could buy some yellow blocks to build a triangle bigger 
        # than either of ours was, or a red and yellow triangle, 
        # or a yellow and blue triangle, with no blocks ever left over
        "YW > BL",
        
        "YW * (YW + 1) // 2 + RE * (RE + 1) // 2 == CD * (CD + 1) // 2",
        "YW * (YW + 1) // 2 + BL * (BL + 1) // 2 == FG * (FG + 1) // 2",
        
        ],
        answer="RE * (RE + 1) // 2, BL * (BL + 1) // 2, YW * (YW + 1) // 2",
        distinct="",
        reorder=0,
        d2i={},
        verbose=0)   
      
      for (_, ans) in p.solve():
        print(f"{ans}")
      

      Like

      • Frits's avatar

        Frits 8:52 am on 23 January 2021 Permalink | Reply

        Similar.

        from enigma import SubstitutedExpression, is_square 
        
        # the alphametic puzzle
        p = SubstitutedExpression(
          [
          # If n is the mth triangular number, then n = m*(m+1)/2
          # and m = (sqrt(8n+1) - 1) / 2
        
          # number of reds    = RE * (RE + 1)/2
          # number of blues   = BL * (BL + 1)/2
          # number of yellows = YW * (YW + 1)/2
          "RE > 0",
          "BL > 0", 
          
          # My blue one's taller
          "BL > RE",
          
          # fewer than 100 each
          "BL * (BL + 1) // 2 < 100",
          
          # they could use all these blocks to make another triangle
          "is_square(4 * (BL * (BL + 1) + RE * (RE + 1)) + 1)",
          
          # Dad could buy some yellow blocks to build a triangle bigger 
          # than either of ours was, or a red and yellow triangle, 
          # or a yellow and blue triangle, with no blocks ever left over
          "YW > BL",
          
          "is_square(4 * (YW * (YW + 1) + RE * (RE + 1)) + 1)",
          "is_square(4 * (YW * (YW + 1) + BL * (BL + 1)) + 1)",
          
          ],
          answer="RE * (RE + 1) // 2, BL * (BL + 1) // 2, YW * (YW + 1) // 2",
          distinct="",
          reorder=0,
          d2i={},
          verbose=0)   
        
        for (_, ans) in p.solve():
          print(f"{ans}")
        

        Like

      • Frits's avatar

        Frits 10:53 am on 23 January 2021 Permalink | Reply

        One (ugly) statement, no imports.

        The “is_square” method “n**.5 % 1 == 0” will fail for large numbers of n (like 152415789666209426002111556165263283035677490).

        print([(r * (r + 1) // 2, b * (b + 1) // 2, y * (y + 1) // 2) 
          # Triangular root <m> of n = m*(m+1)/2  is (sqrt(8n+1) - 1) / 2
          # combinations of red and blue triangular roots and number of blocks < 100
          for (r, b) in [(c1, c2) 
            for c1 in range(1, [m for m in range(1, 50) if m * (m + 1)> 198][0] - 1) 
            for c2 in range(c1 + 1, [m for m in range(1, 50) if m * (m + 1)> 198][0]) 
            # they could use all these blocks to make another triangle
            if (4 * (c1 * (c1 + 1) + c2 * (c2 + 1)) + 1)**.5 % 1 == 0] 
          for y in range(b + 1, 99) 
          # red and yellow triangle, with no blocks ever left over
          # blue and yellow triangle, with no blocks ever left over
          if (4 * (r * (r + 1) + y * (y + 1)) + 1)**.5 % 1 == 0 and
             (4 * (b * (b + 1) + y * (y + 1)) + 1)**.5 % 1 == 0])  
        

        Like

        • Frits's avatar

          Frits 12:44 pm on 23 January 2021 Permalink | Reply

          [m for m in range(1, 50) if m * (m + 1)> 198][0]
          

          can be achieved more efficiently (probably) by:

          next(filter(lambda m: m * (m + 1)> 198, range(1, 50)))
          

          Like

  • Unknown's avatar

    Jim Randell 8:11 am on 21 January 2021 Permalink | Reply
    Tags: by: Richard Furner   

    Brain-Teaser 683: Powerless 

    From The Sunday Times, 18th August 1974 [link]

    I have here two positive single figure numbers, each less than 9. Neither is a factor of the other. I add the larger number to the smaller.

    Then, to that total I again add the original larger number, and to the new total I again add the original larger number and may, if I like, continue this process indefinitely, but never shall I obtain a total which is a “power” of any whole number whatsoever.

    What are my two numbers?

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

    [teaser683]

     
    • Jim Randell's avatar

      Jim Randell 8:11 am on 21 January 2021 Permalink | Reply

      This Python program narrows down the candidate pairs until there is only one left, so if there is a solution this must be it.

      It runs in 49ms.

      Run: [ @replit ]

      from enigma import (subsets, irange, prime_factor, mgcd, unpack, printf)
      
      # check a number is _not_ an exact power
      check = lambda n, gcd=unpack(mgcd): gcd(e for (p, e) in prime_factor(n)) == 1
      
      # record totals and pairs of numbers
      ss = list((A + B, A, B) for (A, B) in subsets(irange(2, 8), size=2) if B % A != 0)
      
      # for each total that is not an exact power, add in B to the total
      while len(ss) > 1:
        ss = list((t + B, A, B) for (t, A, B) in ss if check(t))
      
      # output candidate solutions
      for (t, A, B) in ss:
        printf("A={A} B={B}")
      

      Solution: The two numbers are 6 and 8.

      To show that this is indeed a solution we need to show that (6 + 8k) can never be a non trivial exact power.

      To find a counter example, we are looking for for some positive integers k, a, n, where n ≥ 2, such that:

      6 + 8k = a^n
      2(3 + 4k) = a^n

      Clearly the LHS is a multiple of 2. So the RHS must also be a multiple of 2, so let: a = 2b.

      2(3 + 4k) = (2b)^n
      2(3 + 4k) = (2^n)(b^n)
      3 + 4k = 2^(n − 1)(b^n)

      Clearly the RHS is divisible by 2, but the LHS is not divisible by 2.

      So (6 + 8k) can never be an exact power of an integer.


      Most of the pairs drop out fairly quickly, but (3, 7) hangs in until we get to 2187 = 3 + 312×7 = 3^7.

      Leaving (6, 8) as the only remaining candidate.

      Like

      • Jim Randell's avatar

        Jim Randell 11:06 am on 21 January 2021 Permalink | Reply

        Here is a more efficient program that reuses the code I wrote for Enigma 1777 to generate powers in numerical order.

        Then for each power we remove pairs of numbers that can be used to make that power, until there is only a single pair remaining.

        Run: [ @replit ]

        from enigma import (Primes, irange, subsets, div, printf)
        
        # generate powers in numerical order (starting from 2 ** 2)
        def powers():
          base = { 2: 2 }
          power = { 2: 4 }
          maxp = 2
          primes = Primes(32, 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)
        
        # possible pairs of numbers
        ss = list((A, B) for (A, B) in subsets(irange(2, 8), size=2) if B % A != 0)
        
        # consider increasing powers
        for n in powers():
          # remove any pairs that can make n
          ss = list((A, B) for (A, B) in ss if div(n - A, B) is None)
          if len(ss) < 2:
            printf("solution: {ss}")
            break
        

        Like

      • Frits's avatar

        Frits 8:15 pm on 21 January 2021 Permalink | Reply

        Python 3.9 introduced multiple arguments version of math.gcd.

        Without imports.

        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  
        
        # record totals and pairs of numbers
        ss = [(A + B, A, B) for A in range(2, 8) for B in range(A, 9) if B % A != 0]
         
        # for each total that is not an exact power, add in B to the total
        while len(ss) > 1:
          ss = list((t + B, A, B) for (t, A, B) in ss if not is_a_power(t))
          
        # output candidate solutions
        for (t, A, B) in ss:
          print(f"A={A} B={B}")  
        

        Like

  • Unknown's avatar

    Jim Randell 8:10 am on 19 January 2021 Permalink | Reply
    Tags:   

    Teaser 2771: All Saints Day 

    From The Sunday Times, 1st November 2015 [link] [link]

    I have written down three even numbers and then consistently replaced digits by letters with different letters used for different digits. In this way I get:

    ALL
    THE
    SAINTS

    In fact multiplying together the first two of these numbers gives the third.

    What number is my SAINT?

    [teaser2771]

     
    • Jim Randell's avatar

      Jim Randell 8:11 am on 19 January 2021 Permalink | Reply

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

      The following run file executes in

      Run: [ @repl.it ]

      #! python -m enigma -rr
      
      SubstitutedExpression
      
      # all numbers are even
      --invalid="1|3|5|7|9,LES"
      
      "ALL * THE = SAINTS"
      
      --answer="SAINT"
      

      Solution: SAINT = 69357.

      Like

    • GeoffR's avatar

      GeoffR 10:40 am on 19 January 2021 Permalink | Reply

      % A Solution in MiniZinc
      include "globals.mzn";
      
      var 0..9:A; var 0..9:L; var 0..9:T; var 0..9:H; 
      var 0..9:E; var 0..9:S; var 0..9:I; var 0..9:N; 
        
      constraint A != 0 /\ T != 0 /\ S != 0;
      constraint all_different ([A,L,T,H,E,S,I,N]);
      
      var 100..999: ALL = 100*A + 11*L;
      var 100..999: THE = 100*T + 10*H + E;
      var 100000..999999: SAINTS = 100001*S + 10000*A + 1000*I + 100*N + 10*T;
      var 10000..99999: SAINT == 10000*S + 1000*A + 100*I + 10*N + T;
      
      constraint ALL mod 2 == 0 /\ THE mod 2 == 0 /\ SAINTS mod 2 == 0;
      constraint ALL * THE == SAINTS;
      
      solve satisfy;
      
      output ["SAINT = " ++ show(SAINT)
      ++"\nSum is " ++ show(ALL) ++ " x " ++ show(THE) ++ " = " ++ show(SAINTS)];
      
      % SAINT = 69357
      % ----------
      % Sum is 988 x 702 = 693576
      % ==========
      
      
      

      Like

  • Unknown's avatar

    Jim Randell 4:31 pm on 15 January 2021 Permalink | Reply
    Tags:   

    Teaser 3043: An odd selection 

    From The Sunday Times, 17th January 2021 [link] [link]

    I wrote an odd digit in each of the sixteen cells of a four-by-four grid, with no repeated digit in any row or column, and with each odd digit appearing three or more times overall. Then I could read four four-figure numbers across the grid and four four-figure numbers down. I calculated the average of the four across numbers and the larger average of the four down numbers. Each was a whole number consisting entirely of odd digits, and each used an odd number of different odd digits.

    What were those two averages?

    [teaser3043]

     
    • Jim Randell's avatar

      Jim Randell 4:54 pm on 15 January 2021 Permalink | Reply

      Here’s a quick (to write) constructive solution using the [[ SubstitutedExpression ]] solver from the enigma.py library.

      It runs in 687ms.

      Run: [ @repl.it ]

      #! python3 -m enigma -rr
      
      # suppose the layout is:
      #
      #  A B C D
      #  E F G H
      #  J K L M
      #  N P Q R
      
      SubstitutedExpression
      
      --digits="1,3,5,7,9"
      
      # no repeated digit in any row or column
      --distinct="ABCD,EFGH,JKLM,NPQR,AEJN,BFKP,CGLQ,DHMR"
      
      # don't reorder the expressions (they all use all 16 symbols)
      --reorder=0
      
      # each average uses an odd number of different odd digits
      --code="avg = lambda *ns: ediv(sum(ns), len(ns))"
      --code="odds = {1, 3, 5, 7, 9}"
      --code="check = lambda s: len(s) in odds and odds.issuperset(s)"
      
      # row average = WXYZ
      "avg(ABCD, EFGH, JKLM, NPQR) = WXYZ"
      "check({W, X, Y, Z})"
      
      # col average = STUV, is greater than row average
      "avg(AEJN, BFKP, CGLQ, DHMR) = STUV"
      "STUV > WXYZ"
      "check({S, T, U, V})"
      
      # each digit appears at least 3 times
      --code="count = lambda s: all(s.count(d) > 2 for d in odds)"
      "count([A, B, C, D, E, F, G, H, J, K, L, M, N, P, Q, R])"
      
      # required answer (row avg, col avg)
      --answer="(WXYZ, STUV)"
      
      # [optional] less verbose output
      --template="({ABCD}, {EFGH}, {JKLM}, {NPQR}) -> {WXYZ}, {STUV}"
      --solution=""
      

      Solution: The row average is 5159. The column average is 5951.

      There are 56 possible arrangements of digits in the grid. Here is one:

      1 9 7 5
      3 5 1 7
      5 3 9 1
      9 7 5 3

      Row average = (1975 + 3517 + 5391 + 9753) / 4 = 5159.

      Column average = (1359 + 9537 + 7195 + 5713) / 4 = 5951.

      Like

    • Frits's avatar

      Frits 12:31 pm on 16 January 2021 Permalink | Reply

      @Jim,

      First time I see a nested lambda.

      I would have used

      “check({W, X, Y, Z})” etc

      and

      –code=”check = lambda s: len(s) in odds and odds.issuperset(s)”

      Like

      • Jim Randell's avatar

        Jim Randell 1:48 pm on 16 January 2021 Permalink | Reply

        @Frits: Yes, that would probably be clearer. I’ll change it.

        I wrote that bit of code before I was using STUV and WXYZ for the averages, so I didn’t already have them broken out into separate digits.


        You can use a lambda expression to implement a sort of let clause in Python.

        So a hypothetical:

        let x=<expr1>, y=<expr2>: <expr3>
        

        construct can be implemented as:

        (lambda x, y: <expr3>)(<expr1>, <expr2>)
        

        or:

        (lambda x=<expr1>, y=<expr2>: <expr3>)()
        

        This last form is almost exactly the same as the let form, but with a few extra brackets.

        Like

    • Jim Randell's avatar

      Jim Randell 8:18 am on 17 January 2021 Permalink | Reply

      And here is a much faster solution that uses some analysis:

      If we start with the four 4-digit numbers that form the rows, we can construct a fifth 4-digit number using the missing digit from each column.

      When these five numbers are added together, each possible digit now appears in each position, so we get:

      T = 1111 × (1 + 3 + 5 + 7 + 9) = 27775

      We can then make possible actual totals by subtracting a 4-digit number composed of different odd digits from T.

      (Each possible digit appears 4 times in our collection of five 4-digit numbers, and in the actual grid we want each digit to appear at least 3 times, so we can only remove at most 1 copy of each digit).

      A viable total must be divisible by 4 to give the average, which itself must consist of an odd number of different odd digits.

      And for any pair of averages the row average must be less than the column average.

      The following program uses this technique to find possible viable pairs of averages. It turns out there is only one viable pair, so this gives us our solution.

      It runs in 44ms.

      Run: [ @repl.it ]

      from enigma import subsets, nconcat, div, nsplit, printf
      
      # allowable digits
      digits = {1, 3, 5, 7, 9}
      
      # generate possible averages for 4x 4-digit numbers
      # with no digit repeated in the same position
      def avgs():
        # if all digits appeared in each position then 4 copies
        # of each digit would be used, and the sum would be ...
        T = 1111 * sum(digits)
      
        # now delete a digit from each position
        # at least 3 copies of each digit must remain, so we can only
        # delete at most one instance of any digit
        for xs in subsets(digits, size=4, select="P"):
          # calculate the average
          avg = div(T - nconcat(xs), 4)
          if avg is None: continue
          # check it uses an odd number of different odd digits
          s = nsplit(avg, fn=set)
          if not(len(s) in digits and digits.issuperset(s)): continue
          # return the average
          yield avg
      
      # choose possible row and column averages
      for (ravg, cavg) in subsets(sorted(avgs()), size=2):
        printf("row avg = {ravg}, col avg = {cavg}")
      

      All that remains is to show that a grid can be constructed using the required averages. Fortunately my first program (above) finds many possible ways to do this.

      Like

      • Frits's avatar

        Frits 3:39 pm on 17 January 2021 Permalink | Reply

        @Jim, Very clever. I also was thinking to start with the missing digits but would probably not have come up with this solution.

        nconcat(xs) modulo 4 must be 3 but this will not help an already very fast program.

        Like

    • GeoffR's avatar

      GeoffR 12:36 pm on 17 January 2021 Permalink | Reply

      This programme is a bit verbose and an array type solution might well be shorter.
      Hovever, it gets the same answer as Jim’s first solution and runs in about the same time as that solution – about 0.7 sec.

      % A Solution in MiniZinc
      include "globals.mzn";
      
      % four-by-four grid
      %  A B C D
      %  E F G H
      %  J K L M
      %  N P Q R
      
      set of int: odds = {1,3,5,7,9};
      
      var 1..9: A; var 1..9: B; var 1..9: C; var 1..9: D; 
      var 1..9: E; var 1..9: F; var 1..9: G; var 1..9: H; 
      var 1..9: J; var 1..9: K; var 1..9: L; var 1..9: M; 
      var 1..9: N; var 1..9: P; var 1..9: Q; var 1..9: R; 
      
      % Row 1 digits
      constraint A in odds /\ B in odds /\ C in odds /\ D in odds;
      constraint all_different ([A,B,C,D]);
      % Row 2 digits
      constraint E in odds /\ F in odds /\ G in odds /\ H in odds;
      constraint all_different ([E,F,G,H]);
      % Row 3 digits
      constraint J in odds /\ K in odds /\ L in odds /\ M in odds;
      constraint all_different ([J,K,L,M]);
      % Row 4 digits
      constraint N in odds /\ P in odds /\ Q in odds /\ R in odds;
      constraint all_different ([N,P,Q,R]);
      
      % Each odd digit appears three or four times in the grid
      % Row 1 digit counts
      constraint count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], A, 3) \/ count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], A, 4) ;
      
      constraint count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], B, 3) \/ count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], B, 4) ;
      
      constraint count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], C, 3) \/ count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], C, 4) ;
      
      constraint count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], D, 3) \/ count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], D, 4) ;
      
      % Row 2 digit counts
      constraint count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], E, 3) \/ count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], E, 4) ;
      
      constraint count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], F, 3) \/ count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], F, 4) ;
      
      constraint count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], G, 3) \/ count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], G, 4) ;
      
      constraint count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], H, 3) \/ count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], H, 4) ;
      
      % Row 3 digit counts
      constraint count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], J, 3) \/ count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], J, 4) ;
      
      constraint count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], K, 3) \/ count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], K, 4) ;
      
      constraint count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], L, 3) \/ count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], L, 4) ;
      
      constraint count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], M, 3) \/ count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], M, 4) ;
      
      % Row 4 digit counts
      constraint count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], N, 3) \/ count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], N, 4) ;
      
      constraint count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], P, 3) \/ count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], P, 4) ;
      
      constraint count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], Q, 3) \/ count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], Q, 4) ;
      
      constraint count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], R, 3) \/ count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
      N,P,Q,R], R, 4) ;
      
      % Down column digits are all different
      constraint all_different([A,E,J,N]);
      constraint all_different([B,F,K,P]);
      constraint all_different([C,G,L,Q]);
      constraint all_different([D,H,M,R]);
      
      % Across numbers in grid
      var 1000..9999:ABCD = 1000*A + 100*B + 10*C + D;
      var 1000..9999:EFGH = 1000*E + 100*F + 10*G + H;
      var 1000..9999:JKLM = 1000*J + 100*K + 10*L + M;
      var 1000..9999:NPQR = 1000*N + 100*P + 10*Q + R;
      
      % Down numbers in grid
      var 1000..9999:AEJN = 1000*A + 100*E + 10*J + N;
      var 1000..9999:BFKP = 1000*B + 100*F + 10*K + P;
      var 1000..9999:CGLQ = 1000*C + 100*G + 10*L + Q;
      var 1000..9999:DHMR = 1000*D + 100*H + 10*M + R;
      
      % Each average (across and down) is a whole number(see description)
      % Average of across numbers 
      var 1000..9999: Av_Across;
      Av_Across = (ABCD + EFGH + JKLM + NPQR) div 4;
      
      % Across average digits (A1, A2, A3, A4)
      var 1..9:A1; var 1..9:A2; var 1..9:A3; var 1..9:A4;
      
      constraint A1 == Av_Across div 1000 /\ A1 mod 2 == 1;
      
      constraint A2 = Av_Across div 100 mod 10 /\ A2 mod 2 == 1;
      
      constraint A3 = Av_Across div 10 mod 10 /\ A3 mod 2 == 1;
      
      constraint A4 = Av_Across mod 10 /\ A4 mod 2 == 1;
      
      % Average of down numbers
      var 1000..9999: Av_Down;
      Av_Down = (AEJN + BFKP + CGLQ + DHMR) div 4;
      
      % Down average digits (D1, D2, D3, D4)
      var 1..9:D1; var 1..9:D2; var 1..9:D3; var 1..9:D4;
      
      constraint D1 == Av_Down div 1000 /\ D1 mod 2 == 1;
      
      constraint D2 = Av_Down div 100 mod 10 /\ D2 mod 2 == 1;
      
      constraint D3 = Av_Down div 10 mod 10 /\ D3 mod 2 == 1;
      
      constraint D4 = Av_Down mod 10 /\ D4 mod 2 == 1;
      
      % Larger average is down average
      constraint Av_Down > Av_Across;
      
      % Each average used an odd number of digits
      constraint card({A1,A2,A3,A4}) mod 2 == 1 
      /\ card({D1,D2,D3,D4}) mod 2 == 1;
      
      solve satisfy;
      
      output ["Average across = " ++ show(Av_Across)
      ++ "\nAverage down = " ++ show(Av_Down) 
      ++ "\nTypical grid:"
      ++ "\n" ++ show(ABCD) ++ "\n" ++ show(EFGH)
      ++ "\n" ++ show(JKLM) ++ "\n" ++ show(NPQR)];
      
      
      

      Like

      • Frits's avatar

        Frits 1:54 pm on 17 January 2021 Permalink | Reply

        @GeoffR,

        You don’t have to the count for each variable A-R, only for 1,3,5,7,9.
        However, the program doesn’t seem to run faster.

        % Each odd digit appears three or four times in the grid
        array[1..5] of int: nbrs = [1,3,5,7,9];
        constraint forall (i in 1..5) (count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
        N,P,Q,R], nbrs[i], 3) \/ count_eq ( [A,B,C,D,E,F,G,H,J,K,L,M,
        N,P,Q,R], nbrs[i], 4));
        

        Like

    • GeoffR's avatar

      GeoffR 3:26 pm on 17 January 2021 Permalink | Reply

      @Frits: Good point. That will shorten the code.
      The original code took 240 ms to print the answer, and 667 msec to complete the search on my I7 laptop.

      Like

  • Unknown's avatar

    Jim Randell 8:28 am on 14 January 2021 Permalink | Reply
    Tags:   

    Teaser 2770: Football fans 

    From The Sunday Times, 25th October 2015 [link] [link]

    The clubs Barnet, Exeter, Gillingham, Plymouth, Southend and Walsall need to attract more fans. So each has persuaded one of the players Aguero, Ibrahimovic, Lampard, Neymar, Schweinsteiger and Suarez to join them. Also, each club has persuaded one of the managers Conte, Mourinho, Pellegrini, Terim, Van Gaal and Wenger to take control. For each club, if you look at the club, player and manager, then for any two of the three there are just two different letters of the alphabet that occur in both (with the letters possibly occurring more than once).

    In alphabetical order of the teams, list their new players.

    [teaser2770]

     
    • Jim Randell's avatar

      Jim Randell 8:29 am on 14 January 2021 Permalink | Reply

      The [[ grouping ]] functionality was added to the enigma.py library to solve this kind of puzzle.

      This Python program runs in 45ms.

      Run: [ @repl.it ]

      from enigma import grouping
      
      clubs = ('Barnet', 'Exeter', 'Gillingham', 'Plymouth', 'Southend', 'Walsall')
      players = ('Aguero',  'Ibrahimovic', 'Lampard', 'Neymar', 'Schweinsteiger', 'Suarez')
      managers = ('Conte', 'Mourinho', 'Pellegrini', 'Terim', 'Van Gaal', 'Wenger')
      
      grouping.solve([clubs, players, managers], grouping.share_letters(2))
      

      Solution: The new players are Lampard (Barnet), Suarez (Exeter), Aguero (Gillingham), Neymar (Plymouth), Ibrahimovic (Southend) and Schweinsteiger (Walsall).

      The groups are:

      Barnet, Lampard, Mourinho
      Exeter, Suarez, Wenger
      Gillingham, Aguero, Terim
      Plymouth, Neymar, Conte
      Southend, Ibrahimovic, Pellegrini
      Walsall, Schweinsteiger, Van Gaal

      Like

  • Unknown's avatar

    Jim Randell 9:04 am on 12 January 2021 Permalink | Reply
    Tags:   

    Brain-Teaser 660: An efficient type 

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

    My typewriter had the standard keyboard:

    row 1: QWERTYUIOP
    row 2: ASDFGHJKL
    row 3: ZXCVBNM

    until I was persuaded by a time-and-motion expert to have it “improved”. When it came back I found that none of the letters was in its original row, though the number of letters per row remained unchanged. The expert assured me that, once I got used to the new system, it would save hours.

    I tested it on various words connected with my occupation — I am a licensed victualler — with the following results. The figures in parentheses indicate how many rows I had to use to produce the word:

    BEER (1)
    STOUT (1)
    SHERRY (2)
    WHISKY (3)
    HOCK (2)
    LAGER (2)
    VODKA (2)
    CAMPARI (2)
    CIDER (3)
    FLAGON (2)
    SQUASH (2, but would have been 1 but for a single letter)

    Despite feeling a trifle MUZZY (a word which I was able to type using two rows) I persevered, next essaying CHAMBERTIN.

    Which rows, in order, did I use?

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

    [teaser660]

     
    • Jim Randell's avatar

      Jim Randell 9:05 am on 12 January 2021 Permalink | Reply

      We can use the [[ SubstitutedExpression ]] solver from the enigma.py library to solve this puzzle. Although the program generated does not run under the standard CPython interpreter, as it exceeds the limit on statically nested blocks, but it runs fine with the PyPy interpreter, which does not have this limitation, or we can use the recently added [[ --denest ]] option, which generates code that is not as deeply nested, and allows the program to run under the standard Python interpreter.

      The following run file executes in 76ms. (Internal runtime of the generated code is 468µs).

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      --distinct=""
      --digits="1,2,3"
      
      # no letter is in its original row
      --invalid="1,QWERTYUIOP"
      --invalid="2,ASDFGHJKL"
      --invalid="3,ZXCVBNM"
      
      # number of letters per row remained unchanged
      --code="count = multiset.from_seq"
      "count([A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z]) == {1: 10, 2: 9, 3: 7}"
      
      # rows used for each word
      "len({B, E, E, R}) = 1"
      "len({S, T, O, U, T}) = 1"
      "len({S, H, E, R, R, Y}) = 2"
      "len({W, H, I, S, K, Y}) = 3"
      "len({H, O, C, K}) = 2"
      "len({L, A, G, E, R}) = 2"
      "len({V, O, D, K, A}) = 2"
      "len({C, A, M, P, A, R, I}) = 2"
      "len({C, I, D, E, R}) = 3"
      "len({F, L, A, G, O, N}) = 2"
      "len({S, Q, U, A, S, H}) = 2"
      "len({M, U, Z, Z, Y}) = 2"
      
      # SQUASH is typed on one row, except for a single letter
      "sorted(count([S, Q, U, A, S, H]).values()) == [1, 5]"
      
      # answer is the rows involved in typing CHAMBERTIN
      --answer="(C, H, A, M, B, E, R, T, I, N)"
      
      # [optional] less verbose output
      --template=""
      
      # [experimental] work around static block limit
      --denest=-1  # apply fix for CPython
      

      Solution: The rows involved in typing CHAMBERTIN are: 1, 3, 1, 2, 2, 2, 2, 3, 2, 1.

      The new assignment of rows to keys is:

      Row 1: A C F G J K L N V X
      Row 2: B E I M P R W Y Z
      Row 3: D H O Q S T U

      Although we can’t tell what order the keys are in on a row.

      Liked by 1 person

      • Frits's avatar

        Frits 9:54 am on 12 January 2021 Permalink | Reply

        @Jim, repl.it isn’t working for this puzzle

        Like

        • Jim Randell's avatar

          Jim Randell 10:07 am on 12 January 2021 Permalink | Reply

          @Frits: Odd. It works for me if I’m logged in, but not if I try to run the repl anonymously. You have to jump through some hoops to get PyPy running in repl.it, so it is not surprising the end result is a bit fragile.

          Like

          • Frits's avatar

            Frits 10:21 am on 12 January 2021 Permalink | Reply

            Yesterday I updated PyPy. I didn’t realize repl.it depended on it.

            (error: Makefile:52: recipe for target ‘exec_pip’ failed). I’ll try to reinstall Pip under PyPy.

            Have you already used the 64 bit PyPy nightly build?

            Liked by 1 person

            • Jim Randell's avatar

              Jim Randell 10:49 am on 12 January 2021 Permalink | Reply

              @Frits: I don’t think your local installation of PyPy will affect repl.it. I think the problem is running PyPy under repl.it (which is not directly supported). Maybe the template I used to allow you to do this only works for the owner of the repl. (You could try forking it and see if that works for you).

              Like

            • Frits's avatar

              Frits 10:57 am on 12 January 2021 Permalink | Reply

              I have now installed the latest nightly build of PyPy ([PyPy 7.3.4-alpha0 with MSC v.1927 64 bit (AMD64)] on win32).

              It seems to be a recent error in PyPy.

               
              https://foss.heptapod.net/pypy/pypy/-/issues/3342 
              

              “pypy3 -m ensurepip” doesn’t give errors anymore but repl.it still fails.

              Like

        • Jim Randell's avatar

          Jim Randell 2:09 am on 13 January 2021 Permalink | Reply

          @Frits: It should be working now.

          Like

          • Frits's avatar

            Frits 10:53 am on 13 January 2021 Permalink | Reply

            Thanks, it works but it takes approx 30 seconds.
            The program itself takes 39ms.

            Like

      • Frits's avatar

        Frits 10:14 am on 12 January 2021 Permalink | Reply

        @Jim,

        If the S of SQUASH was the single letter in a different row the multiset line would not recognize it (it depends how you interpret “a single letter”).

        Do you know an easy way how I can execute the run file like a normal py file in a Command Prompt (without creating a main each and every time)?

        Like

        • Jim Randell's avatar

          Jim Randell 10:41 am on 12 January 2021 Permalink | Reply

          @Frits: I would have thought if the S was on a different line then typing SQUASH would involve typing 2 letters that were on a different row. But the puzzle text is open to interpretation there. To try the other interpretation you can just remove one of the S‘s from the list on line 32 and check for a [1, 4] allocation of letters. Fortunately it doesn’t change the answer.


          On Unix like operating systems the #! line tells the OS how to execute a file, so the file just needs to be executable. (And a while ago I made a command called run, to make it even easier to run such files).

          Otherwise you can just run the command manually. So for this file:

          % pypy -m enigma -rr teaser660.run
          

          Or if you haven’t put enigma.py in your $PYTHONPATH, just put them in the same directory (or specify full paths):

          % pypy enigma.py -rr teaser660.run
          

          Alternatively you can run it from the Python shell, using the [[ enigma.run() ]] function:

          % pypy
          >>>> from enigma import run
          >>>> run("teaser660.run")
          A=1 B=2 C=1 D=3 E=2 F=1 G=1 H=3 I=2 J=1 K=1 L=1 M=2 N=1 O=3 P=2 Q=3 R=2 S=3 T=3 U=3 V=1 W=2 X=1 Y=2 Z=2
          (C, H, A, M, B, E, R, T, I, N) = (1, 3, 1, 2, 2, 2, 2, 3, 2, 1) [1 solution]
          

          (I have [[ from enigma import * ]] in my .pythonrc file, so code from enigma.py is always available in interactive Python shells, and I don’t need to import them separately).

          Like

          • Frits's avatar

            Frits 11:08 am on 12 January 2021 Permalink | Reply

            @Jim, thanks.

            It doesn’t work if the filetype is py but that’s OK.

            Like

    • Frits's avatar

      Frits 11:05 am on 13 January 2021 Permalink | Reply

      @Jim, 4th run still takes 15 second, it doesn’t matter.

      I am busy with a program to solve the puzzle without using brute force but am kind of stuck….

      Like

    • Jim Randell's avatar

      Jim Randell 6:18 pm on 13 January 2021 Permalink | Reply

      People who use the [[ SubstitutedExpression ]] solver from the enigma.py library might be interested in the experimental version of enigma.py in this repl [ @repl.it ].

      It adds the denest parameter to [[ SubstitutedExpression]] (which available as --denest or -X from the command line or in run files), that changes the way the code is generated to reduce the amount of nesting in the generated code.

      The upshot of this is, if you are using [[ SubstitutedExpression ]] and you run foul of the "SyntaxError: too many statically nested blocks" issue, you can just use [[ SubstitutedExpression(..., denest=1) ]] (or add the --denest or -X parameter to a run file or on the command line).

      The run file given above, with the additional --denest parameter runs in 93ms using CPython 2.7.

      Assuming no problems show up in further testing, this will be rolled out in the next enigma.py update.

      Update: I have rolled this out in the 2021-01-14 version of enigma.py.

      Like

      • Frits's avatar

        Frits 7:05 pm on 13 January 2021 Permalink | Reply

        @Jim Thanks, it works.

        However, –verbose=256 doesn’t seem to work properly anymore.

        Like

        • Frits's avatar

          Frits 10:55 pm on 16 January 2021 Permalink | Reply

          @Jim, I don’t know if this is caused by your latest update.

          from enigma import SubstitutedExpression
          
          # the alphametic puzzle
          p = SubstitutedExpression(
            [
            "WXYZ = 3713",
            ],
            distinct="",
            digits="1,3,5,7,9",
            answer="W, XYZ",
            d2i={},
            verbose=256
            )   
          
          for (_, ans) in p.solve():  
            print(f"{ans}")
          
          

          No solution is given, see generated code:

           
          [SubstitutedExpression: replacing ({WXYZ} = 3713) -> (3713 = {WXYZ})]
          -- [code language="python"] --
          def _substituted_expression_solver1():
            try:
              _x_ = int(3713)
            except NameError:
              raise
            except:
              raise
            if _x_ >= 0:
              _Z = _x_ % 10
              if _Z != 0 and _Z != 1 and _Z != 2 and _Z != 3 and _Z != 4 and _Z != 5 and _Z != 6 and _Z != 7 and _Z != 8 and _Z != 9:
                _x_ //= 10
                _Y = _x_ % 10
                if _Y != 0 and _Y != 1 and _Y != 2 and _Y != 3 and _Y != 4 and _Y != 5 and _Y != 6 and _Y != 7 and _Y != 8 and _Y != 9:
                  _x_ //= 10
                  _X = _x_ % 10
                  if _X != 0 and _X != 1 and _X != 2 and _X != 3 and _X != 4 and _X != 5 and _X != 6 and _X != 7 and _X != 8 and _X != 9:
                    _x_ //= 10
                    _XYZ = _Z + _Y*10 + _X*100
                    _W = _x_ % 10
                    if _W != 0 and _W != 1 and _W != 2 and _W != 3 and _W != 4 and _W != 5 and _W != 6 and _W != 7 and _W != 8 and _W != 9:
                      _x_ //= 10
                      if _x_ == 0:
                        _WXYZ = _Z + _Y*10 + _X*100 + _W*1000
                        _r_ = (_W), (_XYZ)
                        yield ({ 'W': _W, 'X': _X, 'Y': _Y, 'Z': _Z }, _r_)
          

          Like

          • Jim Randell's avatar

            Jim Randell 11:08 pm on 16 January 2021 Permalink | Reply

            @Frits

            The digits parameter should be a collection of permissible digits (not a string), e.g.:

            digits=(1,3,5,7,9)
            

            Like

            • Frits's avatar

              Frits 11:40 pm on 16 January 2021 Permalink | Reply

              Thanks, I copied it from the run version (something which I normally don’t do).

              Like

    • Frits's avatar

      Frits 6:17 pm on 14 January 2021 Permalink | Reply

      Value 0b110 means letter can reside in row 1 or 2.

      from itertools import combinations as comb
      
      letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
      
      orig = ["QWERTYUIOP", "ASDFGHJKL", "ZXCVBNM"]
      sol  = ["ACFGJKLNVX", "BEIMPRWYZ", "DHOQSTU"]     # only used for printing
      
      common1 = [set("BEER"), set("STOUT")]
      common2 = [set("LAGER"), set("CAMPARI"), set("SHERRY"), set("HOCK"), 
                 set("VODKA"), set("FLAGON"), set("SQUASH"), set("MUZZY")]
      common3 = [set("WHISKY"), set("CIDER")]           
      
      r = {4 : "1", 2 : "2", 1 : "3"}   # row number
      
      # return 0 if all letters have been uniquely placed in a row
      def unsolved():
        c = 0
        for let in letters:
          c += d[let]  
          if d[let] not in {1, 2, 4}:
            return c
        return 0
        
      # remove <val> from letter <let> 
      def remove(let, val, text = ""):
        if d[let] & val == 0: return   # already removed
        
        d[let] = d[let] & (7 - val)
        if text != "":
          print(f"remove letter {let} from row {r[val]} {text}")
      
      # one row in common
      def cond1(s):
        # skip if all letters in <s> are known 
        if all(d[x] in {1, 2, 4} for x in s): return
        
        # check if letters in <s> have only one common bit
        common = 7
        for x in s:
          common &= d[x]
        
        if common in {1, 2, 4}:
          print(f"place letters {','.join(s)} in row {r[common]} ", end = "")
          print("(as they have to share 1 row)")
          for x in s:
            d[x] = common   # set all letters to the same value
            
      # two rows in common
      def cond2(s):
        known = [x for x in s if d[x] in {1, 2, 4}]
        rows_used = set(d[x] for x in known)
        
        if len(rows_used) == 2:
          # we have solved letters in 2 rows
          # so all letters in <s> have to be in these 2 rows
          twoRows = sum(rows_used)
          missing_row = 7 - twoRows
          
          for x in s:
            if x in known: continue
            # letter can be in either one of the 2 rows?
            if d[x] == twoRows: continue
            
            rows_used = sorted(list(rows_used), reverse=1)
            text = "(letters "+ ",".join(s) +" must occur in rows " 
            text += ", ".join(r[x] for x in rows_used) + ")"
            remove(x, missing_row, text)
       
      # three rows in common
      def cond3(s):
        # for each row check candidates
        for i in [1, 2, 4]:
          li = [x for x in s if d[x] & i]
           
          if len(li) == 1 and d[li[0]] != i:   # one candidate for row i
            print(f"place letter {li[0]} in row {r[i]} (other ", end = "")
            print(f"letters in {','.join(s)} are not possible in row {r[i]})")
            d[li[0]] = i
            return
      
      # check if all letters in a row are known
      def row_full():
        for i, n in enumerate([4, 2, 1]):
          c = 0
          for let in letters:
            if d[let] == n:
              c += 1
          if c == len(orig[i]):
            # row <i> is full so remove <i> from other candidates
            for let in letters:
              if d[let] == n: continue
              remove(let, n, "(already " + str(len(orig[i])) + 
                     " letters have to be in row " + r[n] +")")
      
      # check whether the number of letters in bottom 2 rows exceeds 16        
      def bottom2(c1 ,c2):  
        common_letters = c1 & c2  
        if len(common_letters) < 2: return False
        
        known = [x for x in common_letters if d[x] in {1, 2, 4}]
        rows_used = list(set(d[x] for x in known))
        if len(rows_used) != 1: return False
        
        bot2 = [x for x in common_letters if x != known[0] and 
                x not in orig[0] and   
                d[x] & d[known[0]] == 0] # different row from known
                
        # known[0] in one bottom row and <bot2> in the other bottom row?       
        if len(bot2) > 0:
          # suppose <bot2> is not in top row, so <bot2> and <known> encompass 
          # 2 rows, this means that all letters in c1 and c2 are in bottom 2 rows
          # check if letters in bottom 2 rows exceeds 16 (9 + 7)
          
          # count entries in 2 bottom rows if <bot2> is not in top row
          li = [x for x in letters 
                if x in orig[0] or        # the 10 QWERTYUIOP entries
                ((d[x] & 4) == 0 or       # not in top row
                  x in (c1 | c2) )]       # in c1 or in c2
                  
          max = len(orig[1]) + len(orig[2])
          if len(li) > max:      
            # put QWERTY... up front
            notTop = {x for x in orig[0]} | {x for x in letters if d[x] & 4 == 0}
            # letter <bot2[0]> can not be in bottom 2 rows
            text = "(assuming letter " + bot2[0] + " in bottom 2 rows leads to\n"
            text += "letters " + ",".join(notTop | c1 | c2) 
            text += " in bottom 2 rows (exceeding total of " + str(max) + "))"
            remove(bot2[0], 1, text)
            remove(bot2[0], 2, text)
            return True
            
        return False   
           
      # print rows  
      def report(li):
        print()
        for i, x in enumerate(li): 
          for y in x: 
            if d[y] == (1 << (2 - i)):  # letter is known
              print(f"{y} = {d[y]:<5}", end=" ")
            else:  
              print(f"{y} = {d[y]:#05b}", end=" ")   
          print()
        print()  
      
      
      d = dict()
      # initialize letters 
      for x in letters:
        d[x] = 7
        
      # remove original row number for each letter
      for i, y in enumerate(orig):
        for x in y:
          remove(x, (1 << (2-i)))
       
      prev = 0
      for loop in range(99):
        for x in common1:
          cond1(x)
        for x in common2:
          cond2(x)
        for x in common3:
          cond3(x)
         
        row_full()         # check if all letters in a row are known
         
        nr_unsolved = unsolved()
        if nr_unsolved == 0:   # are we done?
          report(sol)
          
          squash_rows = sorted(d[x] for x in "SQUASH")
          if squash_rows[0] != squash_rows[1] or \
             squash_rows[-1] != squash_rows[-2]:
            print("letters in SQUASH are all in one row but for a single letter")
          break
        
        report(sol)
       
        if nr_unsolved == prev:    # nothing has been solved in this loop 
          # check all combinations of words which reside on 2 rows
          for c1, c2 in comb(common2, 2):
            # check whether the number of letters in bottom 2 rows exceeds 16
            if bottom2(c1, c2): break
        prev = nr_unsolved
      

      Like

    • John Crabtree's avatar

      John Crabtree 1:04 am on 15 January 2021 Permalink | Reply

      Q, W, E, R, T, Y U, I, O, P (10)
      A, S, D, F, G, H, J, K, L (9)
      Z, X, C, V, B, N, M (7)

      BEER (1) B, E, R -> row 2
      STOUT (1) S, T, O, U -> row 3
      only 3 more letters can go to row 3, and from CIDER, at least one must be D or I
      LAGER (2) A, G, L -> row 1
      SQUASH (2*) Q, H -> row 3 (full except for D or I)
      first row P, W, Y – > row 2
      second row F, J, K -> row 1
      HOCK (2) C -> row 1
      CIDER (3) D -> row 3 (full), I -> row 2
      MUZZY (2) M, Z -> row 2 (full)
      third row N, X, V -> row 1

      CHAMBERTIN comes from rows 1, 3, 1, 2, 2, 2, 2, 3, 2, 1

      In this method WHISKY (3), VODKA (2), CAMPARI (2) and FLAGON (2) are redundant

      Like

      • Jim Randell's avatar

        Jim Randell 5:22 am on 15 January 2021 Permalink | Reply

        If I remove the WHISKY, VODKA, CAMPARI, FLAGON conditions in my program it finds 4 possible solutions. So they can’t all be redundant.

        But adding just CAMPARI back in gets us back to a unique solution.

        Like

        • Frits's avatar

          Frits 9:52 am on 15 January 2021 Permalink | Reply

          True, the problem lies with the CIDER (3) D …. deduction.

          Like

          • John Crabtree's avatar

            John Crabtree 3:33 pm on 15 January 2021 Permalink | Reply

            Jim and Frits, thank you. originally I used CAMPARI. In my incorrect CIDER (3) deduction I thought that E and R were in row 1. 🙂

            Like

    • GeoffR's avatar

      GeoffR 4:13 pm on 15 January 2021 Permalink | Reply

      
      % A Solution in MiniZinc
      include "globals.mzn";
      
      set of int: rows = 1..3;
      
      var rows: A; var rows: B; var rows: C; var rows: D;
      var rows: E; var rows: F; var rows: G; var rows: H;
      var rows: I; var rows: J; var rows: K; var rows: L;
      var rows: M; var rows: N; var rows: O; var rows: P;
      var rows: Q; var rows: R; var rows: S; var rows: T;
      var rows: U; var rows: V; var rows: W; var rows: X;
      var rows: Y; var rows: Z; 
      
      % Standard keyboard layout
      % row1 = Q, W, E, R, T, Y, U, I, O, P (10 letters)
      % row2 = A, S, D, F, G, H, J, K, L  (9 letters)
      % row3 = Z, X, C, V, B, N, M  (7 letters)
      
      % 1st row letters must be in a different row
      constraint Q != 1 /\ W != 1 /\ E != 1 /\ R != 1
      /\ T != 1 /\ Y != 1 /\ U != 1 /\ I != 1
      /\ O != 1 /\ P != 1;
      
      % 2nd row letters must be in a different row
      constraint A != 2 /\ S != 2 /\ D != 2 /\ F != 2
      /\ G != 2 /\ H != 2 /\ J != 2 /\ K != 2 /\ L != 2;
      
      % 3rd row letters must be in a different row
      constraint Z != 3 /\ X != 3 /\ C != 3 /\ V != 3
      /\ B != 3 /\ N != 3 /\ M != 3;
      
      % Letter constraints for three new rows
      % New 1st row
      constraint count_eq ( [A,B,C,D,E,F,G,H,I,J,K,L,M,N,
      O,P,Q,R,S,T,U,V,W,X,Y,Z], 1, 10); 
      
      % New 2nd row
      constraint count_eq ( [A,B,C,D,E,F,G,H,I,J,K,L,M,N,
      O,P,Q,R,S,T,U,V,W,X,Y,Z], 2, 9);
      
      % New 3rd row
      constraint count_eq ( [A,B,C,D,E,F,G,H,I,J,K,L,M,N,
      O,P,Q,R,S,T,U,V,W,X,Y,Z], 3, 7); 
      
      % (Rows) I had to use to produce the words
      % BEER(1)
      constraint card({B,E,E,R}) == 1;
      % STOUT (1)
      constraint card ({S,T,O,U,T}) == 1;
      % SHERRY (2)
      constraint card({S,H,E,R,R,Y}) == 2;
      % WHISKY (3)
      constraint card({W,H,I,S,K,Y}) == 3;
      % HOCK (2)
      constraint card({H,O,C,K}) == 2;
      % LAGER (2)
      constraint card( {L,A,G,E,R}) == 2;
      % VODKA (2)
      constraint card({V,O,D,K,A}) == 2;
      % CAMPARI (2)
      constraint card ({C,A,M,P,A,R,I}) == 2;
      % CIDER (3)
      constraint card ({C,I,D,E,R}) == 3;
      % FLAGON (2)
      constraint card({F,L,A,G,O,N}) == 2;
      
      % SQUASH (2, but would have been 1 but for a single letter)
      
      % There are two rows for these letters. 
      % If four letters in 'QUASH' (i.e. missing first S out)
      % have a cardinality of 1, the other letter one must be
      % in a different row on its own. 
      
      constraint 
           (card ({U,A,S,H}) == 1 \/ card ({Q,A,S,H}) == 1
           \/ card ({Q,U,S,H}) == 1 \/ card ({Q,U,A,H}) == 1
           \/ card ({Q,U,A,S}) == 1);
      
      % MUZZY(2 rows)
      constraint card({M,U,Z,Z,Y}) == 2;
      
      output [ "CHAMBERTIN rows are " ++ 
      show([C,H,A,M,B,E,R,T,I,N]) ++ "\n"
      ++ "\nLETTERS : [A, B, C, D, E, F, G, H, I, J, K, L, M]"
      ++ "\nROWS :    " ++ show( [A, B, C, D, E, F, G, H, I, J, K, L, M])
      
      ++ "\nLETTERS : [N, O, P, Q, R, S, T, U, V, W, X, Y, Z]"
      ++ "\nROWS :    " ++ show([N, O, P, Q, R, S, T, U, V, W, X, Y, Z])
      ++ "\n"
      ++ "\n" ++ "New Row 1 is [A,C,F,G,J,K,L,N,V,X] " 
      ++ "\n" ++ "New Row 2 is [B,E,I,M,P,R,W,Y,Z] " 
      ++ "\n" ++ "New Row 3 is [D,H,O,Q,S,T,U]" ];
      
      % CHAMBERTIN rows are [1, 3, 1, 2, 2, 2, 2, 3, 2, 1]
      
      % LETTERS : [A, B, C, D, E, F, G, H, I, J, K, L, M]
      % ROWS      [1, 2, 1, 3, 2, 1, 1, 3, 2, 1, 1, 1, 2]
      % LETTERS : [N, O, P, Q, R, S, T, U, V, W, X, Y, Z]
      % ROWS      [1, 3, 2, 3, 2, 3, 3, 3, 1, 2, 1, 2, 2]
      % New rows follow from above values
      % New Row 1 is [A,C,F,G,J,K,L,N,V,X] 
      % New Row 2 is [B,E,I,M,P,R,W,Y,Z] 
      % New Row 3 is [D,H,O,Q,S,T,U]
      % ----------
      % ==========
      

      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