Updates from Jim Randell Toggle Comment Threads | Keyboard Shortcuts

  • Unknown's avatar

    Jim Randell 8:37 am on 18 May 2023 Permalink | Reply
    Tags:   

    Teaser 2630: River crossing 

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

    Two lads walking together had to get back to their tent which was a short distance south-east of them. However, it involved crossing a river which was running from west to east and was a constant five metres wide. Whichever route they chose, they made sure that they were in the water for the shortest distance possible.

    One lad took the obvious such route and went due south and then due east, but the other took the shortest possible such route, thus cutting his friend’s distance by a quarter.

    What was the length of that shortest route?

    [teaser2630]

     
    • Jim Randell's avatar

      Jim Randell 8:38 am on 18 May 2023 Permalink | Reply

      See also: Teaser 3103.

      They both cross the river at right angles to give a river crossing of 5m.

      We can then remove the river, and the remaining (walking) paths are the sides of a right-angled triangle. The optimal route being the hypotenuse, and the first lad traversing the other two sides.

      So if the triangle has sides (x, y, z), the total length of the first path (including river crossing) is: (x + y + 5) and the total length of the second (optimal) path is (z + 5).

      And so:

      (z + 5) / (x + y + 5) = 3/4
      4z + 5 = 3(x + y)

      In order to get a unique solution to the puzzle we need to assume that the tent is exactly SE of the lads (i.e. on a bearing of 135°).

      Then the first lad travels the same total distance on the south leg of his journey (= x + 5) as he does on the east leg (= y).

      So we have:

      y = x + 5
      z = (3(x + y) − 5)/4 = (3x + 5)/2

      and:

      x² + y² = z²
      x² + (x + 5)² = (3x + 5)²/4
      8x² + 40x + 100 = 9x² + 30x + 25
      x² − 10x − 75 = 0
      (x + 5)(x − 15) = 0
      x = 15, y = 20, z = 25

      So the optimal distance is z + 5 = 30m.

      Solution: The shortest route is 30m.

      And the first lad’s distance is 40m.


      We can solve the quadratic equation using the [[ Polynomial() ]] class from the enigma.py library:

      Run: [ @replit ]

      from enigma import (Polynomial, sq, printf)
      
      # construct the polynomial (quadratic)
      px = Polynomial([0, 1])
      py = px + 5
      pz = (3 * px + 5) * 0.5
      p = sq(px) + sq(py) - sq(pz)
      printf("[p = {p}]")
      
      # find (real, positive) roots
      for x in p.quadratic_roots(domain='F', include='+'):
        (y, z) = (py(x), pz(x))
        printf("x={x:g} y={y:g} z={z:g} -> A={A:g} B={B:g}", A=x + y + 5, B=z + 5)
      

      Or we can solve the problem numerically, using the [[ find_value() ]] solver from the enigma.py library:

      Run: [ @replit ]

      from enigma import (find_value, hypot, fdiv, printf)
      
      # A's distance
      A = lambda x: 2 * (x + 5)
      
      # B's distance
      B = lambda x: hypot(x, x + 5) + 5
      
      # find a value where B/A = 3/4
      f = lambda x: fdiv(B(x), A(x))
      r = find_value(f, 0.75, 0, 1000)
      
      x = r.v
      (a, b) = (A(x), B(x))
      printf("B={b:g} A={a:g} -> B/A={f:g}", f=r.fv)
      

      If we had been told that the total distances travelled were whole numbers of metres, then we could look for appropriate right-angled triangles:

      Run: [ @replit ]

      from enigma import (pythagorean_triples, printf)
      
      for (x, y, z) in pythagorean_triples(995, primitive=0):
        if 4 * z + 5 == 3 * (x + y) and y == x + 5:
          printf("A={A} B={B} [x={x} y={y} z={z}]", A=x + y + 5, B=z + 5)
      

      The triangle we are looking for is a (15, 20, 25) triangle = 5× (3, 4, 5).

      Removing the [[ y == x + 5 ]] condition at line 4 allows us to find further (integer) solutions, where the tent is south and east of the starting position, but not on a bearing of 135°.

      Like

  • Unknown's avatar

    Jim Randell 9:08 am on 16 May 2023 Permalink | Reply
    Tags:   

    Teaser 2218: Speed trap 

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

    In Ireland they have changed from miles to kilometres. In European spirt and always using the rule of thumb that five miles convert to eight kilometres, I note that FIVE miles will convert into HUIT kilometres. Also UNE miles equals roughly four-fifths of TWO kilometres.

    Here, letters consistently stand for the same digit, and no two letters stand for the same digit.

    In figures, how many kilometres do NINE miles convert to?

    [teaser2218]

     
    • Jim Randell's avatar

      Jim Randell 9:09 am on 16 May 2023 Permalink | Reply

      This Python program uses the [[ SubstitutedExpression() ]] solver from the enigma.py library to find values where FIVE / HUIT = 5 / 8, and then selects a solution with the minimum distance between UNE miles and 0.8× TWO kilometres.

      It runs in 63ms.

      Run: [ @replit ]

      from enigma import (SubstitutedExpression, Accumulator, printf)
      
      # make the alphametic problem
      p = SubstitutedExpression(
        ["div(FIVE * 8, 5) = HUIT"],
        answer="(UNE, TWO, FIVE, HUIT, NINE)",
        verbose=0
      )
      # solve it and collect answers
      r = Accumulator(fn=min, collect=1)
      for (s, ans) in p.solve():
        (UNE, TWO, FIVE, HUIT, NINE) = ans
        # difference between UNE miles and 0.8 TWO km (in km)
        r.accumulate_data(abs(1.6 * UNE - 0.8 * TWO), ans)
      
      # output minimal solutions
      for (UNE, TWO, FIVE, HUIT, NINE) in r.data:
        printf("FIVE ({FIVE}) miles = HUIT ({HUIT}) km")
        printf("diff(UNE ({UNE}) miles, 0.8x TWO ({TWO}) km) = {r.value:.3f} km")
        km = 1.6 * NINE
        printf("NINE ({NINE}) miles = {km:g} km")
        printf()
      

      Solution: NINE miles = 15512 km.

      We have:

      FIVE (4605) miles = HUIT (7368) km
      diff(UNE (395) miles, 0.8× TWO (812) km) = 17.600 km
      NINE (9695) miles = 15512 km

      Like

      • Frits's avatar

        Frits 11:57 am on 17 May 2023 Permalink | Reply

        @Jim, is there a reason why you didn’t use “accumulate=min” in SubstitutedExpression?

        Like

        • Jim Randell's avatar

          Jim Randell 12:45 pm on 17 May 2023 Permalink | Reply

          @Frits: I wanted to neaten up the output, and we are minimising a function of the answer parameter, so things end up a bit messier.

          But it is perfectly possible to do this:

          Run: [ @replit ]

          #! python3 -m enigma -rr
          
          SubstitutedExpression
          
          # FIVE miles is HUIT kilometres
          "div(HUIT * 5, 8) = FIVE"
          
          # UNE miles is roughly 4/5 of TWO kilometres
          --code="diff = lambda v1, v2: abs(1.6 * v1 - 0.8 * v2)"
          # answer is NINE miles, expressed as km
          --code="ans = lambda n: 1.6 * n"
          --answer="(diff(UNE, TWO), ans(NINE), UNE, TWO, FIVE, HUIT, NINE)"
          --accumulate="min"
          

          Like

    • Frits's avatar

      Frits 11:55 am on 17 May 2023 Permalink | Reply

       
      % A Solution in MiniZinc
      include "globals.mzn";
       
      var 1..9:F; var 0..9:I ; var 0..9:V ; var 0..9: E; var 1..9: H; 
      var 1..9:U; var 1..9:T ; var 0..9:W ; var 0..9: O; var 1..9: N; 
      
      function var int: toNum(array[int] of var int: a) =
                let { int: len = length(a) }
                in
                sum(i in 1..len) (
                  ceil(pow(int2float(10), int2float(len-i))) * a[i]
                );  
      
      % the digits are all different
      constraint all_different ([F,I,V,E,H,U,T,W,O,N]);
      
      %  FIVE miles will convert into HUIT kilometres
      constraint 1.6 * toNum([F, I, V, E]) == toNum([H, U, I, T]);
      
      solve minimize(abs(1.6 * toNum([U, N, E]) - 0.8 * toNum([T, W, O])));
       
      output["NINE = " ++ show(1.6 * toNum([N, I, N, E])) ++
             " diff = " ++ show(abs(1.6 * toNum([U, N, E]) - 0.8 * toNum([T, W, O])))];
      

      Like

  • Unknown's avatar

    Jim Randell 12:12 pm on 14 May 2023 Permalink | Reply
    Tags:   

    Brain-Teaser 556: Air ways 

    From The Sunday Times, 27th February 1972 [link]

    Al, Bill, Cab, Don, Ed, Fred, Gil, Hal, and Ian were waiting at the airport. Three would take a plane going east, three plane going north and three a plane going south.

    Hal, but neither Ian nor Fred, was going south. Al would be travelling with Don or Cab. Fred would be travelling with Bill or Ed.

    Gil, unlike Ian, would be travelling east. Cab would take a seat next to Ed or Al. Ian would travel in a different plane from Bill, and in a different plane from Cab.

    If Don would still be waiting at the airport after Bill and Ed had left, who were the three going north?

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

    [teaser556]

     
    • Jim Randell's avatar

      Jim Randell 12:13 pm on 14 May 2023 Permalink | Reply

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

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

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      # allocate directions: 1=E, 2=N, 3=S
      --digits="1,2,3"
      --distinct=""
      
      # 3 go in each direction
      "multiset.from_seq([A, B, C, D, E, F, G, H, I]).all_same(3)"
      
      # H is going S; but I, F are not
      "H = 3" "I != 3" "F != 3"
      
      # A is travelling with D or C
      "A in {D, C}"
      
      # F is travelling with B or E
      "F in {B, E}"
      
      # G is travelling E; but I is not
      "G = 1" "I != 1"
      
      # C is travelling with E or A
      "C in {E, A}"
      
      # I is not travelling with B or C
      "I not in {B, C}"
      
      # D is not travelling with B or E
      "D not in {B, E}"
      
      --template=""
      

      Solution: Al, Don, Ian are travelling north.

      The full solution:

      East = B, F, G
      North = A, D, I
      South = C, E, H

      Like

      • Frits's avatar

        Frits 3:24 pm on 14 May 2023 Permalink | Reply

        Or:

        3 go in each direction

        “div(216, A * B * C * D * E * F * G * H) = I”

        Like

        • Jim Randell's avatar

          Jim Randell 3:40 pm on 14 May 2023 Permalink | Reply

          A good job I used non-zero digits ;-).

          Like

          • Frits's avatar

            Frits 5:19 pm on 14 May 2023 Permalink | Reply

            With zero you can use other non-zero digits d and 3d + 1 with check

            “(calculate 12d + 3) – sum([A, B, C, D, E, F, G, H]) = I”

            Like

  • Unknown's avatar

    Jim Randell 4:22 pm on 12 May 2023 Permalink | Reply
    Tags:   

    Teaser 3164: Touching base 

    From The Sunday Times, 14th May 2023 [link] [link]

    Liam has a pack of twenty cards; each card has a number printed on it as a word rather than figures.

    There are two cards with ONE, two cards with TWO etc., up to two cards with TEN. He has taken four of the cards to construct an addition sum. The largest value card he has used is the sum of the values of the other cards, e.g.:

    ONE + ONE + TWO = FOUR.

    If I now work in the base equal to that sum (i.e., base 4 in the example), and consistently replace letters with single digits, this gives a correct sum. All of the possible digits in that base are used.

    Leaving the answer in the working base, what is the total of my addition sum?

    [teaser3164]

     
    • Jim Randell's avatar

      Jim Randell 4:50 pm on 12 May 2023 Permalink | Reply

      This Python program constructs the viable alphametic sums and then uses the [[ SubstitutedExpression.split_sum() ]] solver from the enigma.py library to evaluate them (assuming “standard” alphametic rules, i.e. leading zeros are not allowed, and each letter stands for a different digit).

      It runs in 73ms. (Internal runtime is 16.8ms).

      Run: [ @replit ]

      from enigma import (
        SubstitutedExpression, irange, multiset, subsets, int2words,
        union, join, substitute, sprintf as f, printf
      )
      
      # the cards
      cards = multiset.from_seq(irange(1, 10), count=2)
      
      # choose 3 of the cards
      for cs in subsets(cards, size=3, select="mC", fn=list):
        # add in the the sum of the 3 cards
        t = sum(cs)
        cs.append(t)
        # check they make a valid set of 4 cards
        if not cards.issuperset(cs): continue
        # construct the corresponding words
        ws = list(int2words(x).upper() for x in cs)
        # check there are t different letters
        if len(union(ws)) != t: continue
      
        # construct the alphametic sum
        expr = f("{terms} = {result}", terms=join(ws[:-1], sep=" + "), result=ws[-1])
        p = SubstitutedExpression.split_sum(expr, base=t, verbose=0)
        # and solve it
        for s in p.solve():
          # output solution
          ss = substitute(s, expr)
          printf("{expr} / {ss} [base {t}]")
      

      Or a faster (but longer) version [ @replit ]. (Internal runtime is 6.1ms).

      Solution: The result of the sum is: 61503 (in base 8).

      There are 2 candidate alphametic sums (under “standard” alphametic rules):

      ONE + ONE + FIVE = SEVEN (7 letters)
      TWO + THREE + THREE = EIGHT (8 letters)

      Only the one with 8 letters yields solutions in the appropriate base.

      So the sum is:

      TWO + THREE + THREE = EIGHT
      327 + 30466 + 30466 = 61503 [base 8]
      215 + 12598 + 12598 = 25411 [base 10]

      In Python:

      >>> 0o327 + 0o30466 + 0o30466 == 0o61503
      True
      

      Like

    • Frits's avatar

      Frits 9:21 pm on 12 May 2023 Permalink | Reply

      The commented code seems to work but somehow it runs slower than threee separate checks.

         
      from itertools import permutations
      from math import ceil
      
      # convert a string in the specified base to an integer
      d2n = lambda s, b: int(''.join(str(x) for x in s), b)
      
      # the numbers
      numbers = ('ONE', 'TWO', 'THREE', 'FOUR', 'FIVE', 
                 'SIX', 'SEVEN', 'EIGHT', 'NINE', 'TEN')
      
      # choose 3 of the cards (below ten)
      for a in range(1, 4):
        # a + 2b <= 10
        for b in range(a, ceil((10 - a) / 2) + 1):
          for c in range(b, 11 - a - b):
            if a == b == c: continue
            t = sum([a, b, c])
            
            # get all the letters
            ltrs = [x for n in (a, b, c, t) for x in numbers[n - 1]]
            if len(set(ltrs)) != t: continue
            
            # get words
            ws = [numbers[n - 1] for n in (a, b, c, t)]
            
            # skip if RHS word is too short
            if any(len(ws[i]) > len(ws[-1]) for i in range(3)):
              continue
              
            ltrs = list(set(ltrs))
            # determine non-zero positions
            nz_ltrs = set(x[0] for x in ws)
            nz_pos = [i for i, x in enumerate(ltrs) if x in nz_ltrs]
            (u_pos, t_pos, h_pos) = ([ltrs.index(x) for x in [x[-i] for x in ws]]
                                      for i in range(1, 4))
                
            # check all possible letter values
            for p in permutations(range(t)):
              # skip for leading zeroes
              if any(not p[x] for x in nz_pos): continue
              
              '''
              sm = 0
              # already check unit/ten/hundred sums
              if any((sm := (sm // t + sum(p[x] for x in chk[:-1]))) % t != p[chk[-1]]
                     for chk in (u_pos, t_pos, h_pos)
                    ): continue
              '''
              # check unit sum
              if (usum := sum(p[x] for x in u_pos[:-1])) % t != p[u_pos[-1]]: 
                continue
              
              # check ten sum
              if (tsum := (usum // t + sum(p[x] for x in t_pos[:-1]))) % t != \
                           p[t_pos[-1]]: continue
              
              # check hundred sum
              if (hsum := (tsum // t + sum(p[x] for x in h_pos[:-1]))) % t != \
                           p[h_pos[-1]]: continue
              
              # perform the complete check via dictionary lookup
              d = {ltr: v for ltr, v in zip(ltrs, p)}
              # get the terms in base t
              ns = [[d[x] for x in ws[i]] for i in range(4)]
              # check the sum in base t
              if sum(d2n(n, t) for n in ns[:-1]) != d2n(ns[-1], t): continue
              
              print(ws)
            
              print("answer:", ''.join(str(x) for x in ns[-1]))
      

      Like

    • Frits's avatar

      Frits 11:04 am on 13 May 2023 Permalink | Reply

      from enigma import SubstitutedExpression
      from math import ceil
      
      # the numbers
      numbers = ('ONE', 'TWO', 'THREE', 'FOUR', 'FIVE', 
                 'SIX', 'SEVEN', 'EIGHT', 'NINE', 'TEN')
      
      carry_ltrs = "ABCD" # maximum word length is 5 so 4 free letters needed             
      
      # choose 3 of the cards (below ten)
      for a in range(1, 4):
        # a + 2b <= 10
        for b in range(a, ceil((10 - a) / 2) + 1):
          for c in range(b, 11 - a - b):
            if a == b == c: continue
            t = sum([a, b, c])
            
            # get string of used letters 
            ltrs = ''.join(set(x for n in (a, b, c, t) for x in numbers[n - 1]))
            if len(set(ltrs)) != t: continue
            
            # get words
            ws = [numbers[n - 1] for n in (a, b, c, t)]
            lenRHS = len(ws[-1])
            
            # skip if RHS word is too short
            if any(len(ws[i]) > lenRHS for i in range(3)):
              continue
            
            # letters per column
            c_ltrs = [[ws[j][-i] if i <= len(ws[j]) else '0' for j in range(4)]
                     for i in range(1, lenRHS + 1)]
            carries = ['0'] + list(carry_ltrs[:lenRHS - 1]) + ['0']
          
            # build expressions per column
            exprs = []
            for i, s in enumerate(c_ltrs):
              exprs.append("(" + '+'.join(s[:-1]) + "+" + carries[i] + ") % " + 
                           str(t) + " = " + s[-1])
              exprs.append("(" + '+'.join(s[:-1]) + "+" + carries[i] + ") // " + 
                           str(t) + " = " + carries[i + 1])
            
            # the alphametic puzzle
            p = SubstitutedExpression(
              exprs,
              base=t,
              answer='(' + '), ('.join(list(','.join(w) for w in ws)) + ')',
              d2i=dict([(0, set(w[0] for w in ws))] + 
                       [(k, carries[1:-1]) for k in range(3, t)]),
              distinct=ltrs,   
              verbose=0,
            )
            
            # print answer
            for (_, ans) in p.solve():
              print(f"{' + '.join(ws[:-1])} = {ws[-1]}")
              print("answer:", ''.join(str(x) for x in ans[-1]))   
      

      Like

    • Frits's avatar

      Frits 1:19 pm on 13 May 2023 Permalink | Reply

      Another variation. This program sometimes runs under 10ms.

       
      from itertools import permutations
      from math import ceil
      
      # the numbers
      numbers = ('ONE', 'TWO', 'THREE', 'FOUR', 'FIVE',
                 'SIX', 'SEVEN', 'EIGHT', 'NINE', 'TEN')
                 
      # process sum per column for each column in <cols>
      def solve_column(base, rng, cols, carry, d=dict()):
        if not cols:
          yield d
        else:
          col = cols[0]
          novalue = set(x for x in col if x not in d and x != '0')
          for p in permutations(rng, len(novalue)):
            d_ = d.copy()  
            d_['0'] = 0        # for dummy letter
           
            i = 0
            # assign letters without value
            for c1 in set(col):
              if c1 not in d_:
                d_[c1] = p[i]
                i += 1
           
            # substitute letters by value
            lst = [d_[c2] for c2 in col]
         
            # check column sum
            if (csum := (carry + sum(lst[:-1]))) % base == lst[-1]:
              yield from solve_column(base, set(rng).difference(p), cols[1:],
                                      csum // base, d_)
      
      # choose 3 of the cards (below ten)
      for a in range(1, 4):
        # a + 2b <= 10
        for b in range(a, ceil((10 - a) / 2) + 1):
          for c in range(b, 11 - a - b):
            if a == b == c: continue
            t = sum([a, b, c])
           
            # get all the letters
            ltrs = [x for n in (a, b, c, t) for x in numbers[n - 1]]
            if len(set(ltrs)) != t: continue
           
            # get words
            ws = [numbers[n - 1] for n in (a, b, c, t)]
            lenRHS = len(ws[-1])
                 
            # skip if RHS word is too short
            if any(len(ws[i]) > lenRHS for i in range(3)):
              continue
             
            # determine non-zero letters
            nz_ltrs = set(x[0] for x in ws if len(x) > 1)
           
            # letters per column
            c_ltrs = [[ws[j][-i] if i <= len(ws[j]) else '0' for j in range(4)]
                      for i in range(1, lenRHS + 1)]
           
            # solve sums for all columns
            for d1 in solve_column(t, range(t), c_ltrs, 0):
              # check for leading zeroes
              if any(d1[x] == 0 for x in nz_ltrs): continue
              print("answer:", ''.join(str(d1[x]) for x in ws[-1]))
      

      Like

    • Frits's avatar

      Frits 3:23 pm on 17 May 2023 Permalink | Reply

      With range based logic (maybe domain is a better word).

       
      from itertools import permutations
      from math import ceil
      
      # the numbers
      numbers = ('ONE', 'TWO', 'THREE', 'FOUR', 'FIVE', 
                 'SIX', 'SEVEN', 'EIGHT', 'NINE', 'TEN')
      
      # pick one value from each entry of a <ns> 
      # so that all <k> values are different
      def pickOneFromEach(k, ns, s=[]):
        if k == 0:
           yield s
        else:
          for n in ns[k - 1]:
            if n not in s:
              yield from pickOneFromEach(k - 1, ns, [n] + s)
      
      # try to reduce ranges with analysis
      def reduce_ranges(base, words, cols, rngs):
        # check first column 
        chars = [x for x in cols[-1][:-1] if x != '0']
      
        # no letters to add?
        if not chars:
          # total digit in first column must be 1
          rngs = {k: [1] if k == cols[-1][-1] else set(v).difference([1]) 
                     for k, v in rngs.items()}
      
        # only one letter has to be added more than once 
        elif len(set(chars)) == 1 and len(chars) > 1:
          mx = (base - 1) // len(chars)
          rngs[chars[0]] = [x for x in rngs[chars[0]] if x <= mx]
      
          mn = rngs[chars[0]][0]
          # total digit may have a new minimum
          rngs[cols[-1][-1]] = [x for x in rngs[cols[-1][-1]] if x >= mn * len(chars)]  
      
        # check last column 
        chars = [x for x in cols[0][:-1] if x != '0']
      
        # only one letter has to be added 
        if len(set(chars)) == 1 and chars[0] != cols[0][-1]:
          # a multiple of zero is zero
          rngs[chars[0]] = [x for x in rngs[chars[0]] if x != 0]
      
        # stop with the analysis
        return rngs  
      
      # process sum per column for each column in <cols>
      def solve_column(base, used, cols, carry, d={'0': 0}):
        if not cols:
          yield d
        else:
          col = cols[0]
          unused_ltrs = sorted(set(x for x in col if x not in d and x != '0'))
          # get ranges for unused letters (without used values)
          rs = [[y for y in ranges[x] if y not in used] for x in unused_ltrs]
          for p in pickOneFromEach(len(unused_ltrs), rs):
            d_ = d.copy()  
            d_.update(zip(unused_ltrs, p))    
      
            # substitute letters by value
            vs = [d_[c2] for c2 in col]
      
            # check column sum
            if (csum := (carry + sum(vs[:-1]))) % base == vs[-1]: 
              yield from solve_column(base, used | set(p), cols[1:], 
                                      csum // base, d_)
      
      # choose 3 of the cards (below ten)
      for a in range(1, 4):
        # a + 2b <= 10
        for b in range(a, ceil((10 - a) / 2) + 1):
          for c in range(b if b > a else b + 1, 11 - a - b):
            t = sum([a, b, c])
      
            # get all the letters
            ltrs = [x for n in (a, b, c, t) for x in numbers[n - 1]]
            if len(set(ltrs)) != t: continue
      
            # get words
            ws = [numbers[n - 1] for n in (a, b, c, t)]
            lenRHS = len(ws[-1])
      
            # skip if RHS word is too short
            if any(len(ws[i]) > lenRHS for i in range(3)):
              continue
      
            # determine non-zero letters
            nz_ltrs = set(x[0] for x in ws if len(x) > 1)
      
            # letters per column
            c_ltrs = [[ws[j][-i] if i <= len(ws[j]) else '0' for j in range(4)]
                      for i in range(1, lenRHS + 1)]
      
            # determine initial range per letter
            ranges = {ltr: range(0 if ltr not in nz_ltrs else 1, t) for ltr in ltrs}
            ranges = reduce_ranges(t, ws, c_ltrs, ranges)          
      
            # solve sums for all columns
            for d1 in solve_column(t, set(), c_ltrs, 0):
              print("answer:", ''.join(str(d1[x]) for x in ws[-1]))  
      

      Like

  • Unknown's avatar

    Jim Randell 10:46 am on 11 May 2023 Permalink | Reply
    Tags:   

    Teaser 1978: Sunday study 

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

    This TEASER is odd and it needs careful STUDY. Throughout, different letters consistently stand for different non-zero digits. Each letter on the lower line is the sum of the two letters above it (so that, for example, U = A + S).

    What is SUNDAY‘s number?

    [teaser1978]

     
    • Jim Randell's avatar

      Jim Randell 10:47 am on 11 May 2023 Permalink | Reply

      See also: Teaser 1688, Teaser 2533.

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

      The following run file executes in 66ms. (Internal runtime of the generated program is 80µs).

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      --digits="1-9"
      
      "T + E = S"
      "E + A = T"
      "A + S = U"
      "S + E = D"
      "E + R = Y"
      
      # TEASER is odd
      "R % 2 = 1"
      
      --answer="SUNDAY"
      

      Solution: SUNDAY = 469528.

      Like

    • GeoffR's avatar

      GeoffR 12:09 pm on 11 May 2023 Permalink | Reply

      
      % A Solution in MiniZinc
      include "globals.mzn";
      
      set of int: INT = 1..9;
      var INT: T; var INT: E; var INT: A;
      var INT: S; var INT: R; var INT: U;
      var INT: D; var INT: Y; var INT: N;
      
      constraint all_different([T, E, A, S, R, U, D, Y, N]);
      
      constraint S == T + E /\ T == E + A /\ U == A + S /\ D == S + E
       /\ Y == E + R /\ R mod 2 == 1;
      
      solve satisfy;
      output ["SUNDAY = " ++  "\(S)\(U)\(N)\(D)\(A)\(Y)" ];
      
      % SUNDAY = 469528
      % ----------
      % ==========
      
      

      Like

    • GeoffR's avatar

      GeoffR 1:47 pm on 11 May 2023 Permalink | Reply

      from itertools import permutations
      
      digits = {1, 2, 3, 4, 5, 6, 7, 8, 9}
      
      for p1 in permutations(range(1, 10), 4):
          T, E, A, S = p1
          if S != T + E:continue
          if T != E + A:continue
          
          # find 5 remaining digits
          q1 = digits.difference(p1)
          for p2 in permutations(q1):
             U, D, R, Y, N = p2
             if U != A + S:continue
             if D != S + E:continue
             if R % 2 != 1:continue  # TEASER is odd
             if Y != E + R:continue
             SUNDAY = 100000*S + 10000*U + 1000*N + 100*D + 10*A + Y
             print('SUNDAY = ', SUNDAY)
      
      
      

      Like

  • Unknown's avatar

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

    Teaser 2628: Unnaturally quiet 

    From The Sunday Times, 3rd February 2013 [link] [link]

    I have given each letter of the alphabet a different value from 0 to 25, so some letters represent a single digit and some represent two digits. Therefore, for example, a three-letter word could represent a number of three, four, five or six digits. With my values it turns out that:

    NATURAL = NUMBER.

    What is the sum of the digits in the number MUTE?

    [teaser2628]

     
    • Jim Randell's avatar

      Jim Randell 9:15 am on 9 May 2023 Permalink | Reply

      Presumably the value of a word is the number formed by the concatenation of the digits in the values for each letter.

      So we are looking for assignments that make ATURAL = UMBER (as the value of N is immaterial).

      This Python program looks for assignments of letters to numeric values that make the translated versions of the strings equal. It does this by assigning values to the leading letters that allow the corresponding digits to match.

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

      Run: [ @replit ]

      from enigma import (irange, subsets, filter2, group, item, update, remove, translate, join, catch, printf)
      
      # replace letters in <X>, <Y> with numeric values from <ns>, so the numeric strings formed are equal
      # <ns> groups values by their first digit
      # return the map: letter -> value
      def _solve(X, Y, ns, d):
        # are we done?
        if X == Y:
          yield d
        elif X and Y:
          # remove any common prefix
          while X and Y and X[0] == Y[0]: (X, Y) = (X[1:], Y[1:])
          if not (X and Y): return
          # split the leading characters into numeric / non-numeric
          (nums, nons) = filter2((lambda x: x.isdigit()), [X[0], Y[0]])
          # different leading numerics = failure
          if len(nums) > 1: return
          # choose values with the same leading digit
          # if there is a numeric value use that, otherwise use the groups
          for k in (nums or ns.keys()):
            ss = ns.get(k, [])
            for vs in subsets(ss, size=len(nons), select='P'):
              # update the strings
              d_ = update(d, nons, vs)
              (X_, Y_) = (translate(x, d_, embed=0) for x in (X, Y))
              ns_ = update(ns, [(k, remove(ss, *vs))])
              # and solve for the translated strings
              yield from _solve(X_, Y_, ns_, d_)
      
      # replace letters in <X>, <Y>
      def solve(X, Y, ns):
        # group numeric values (as strings) by their leading digit
        ns = group(map(str, ns), by=item(0), fn=set)
        # and call the solver
        return _solve(X, Y, ns, dict())
      
      # possible values
      ns = irange(0, 25)
      
      # word values we are interested in
      (w1, w2, w3) = ("NATURAL", "NUMBER", "MUTE")
      
      # turn a word into a string
      fmt = lambda w, sep=':': join((d.get(x, x) for x in w), sep=sep)
      # calculate digit sum
      dsum = lambda w: sum(map(int, fmt(w, sep='')))
      
      # solve the puzzle
      for d in solve(w1, w2, ns):
        # sum the digits in w3
        s = catch(dsum, w3)
        if s is not None:
          # output solution
          printf("dsum({w3}) = {s} [{w1}={f1} {w2}={f2} {w3}={f3}]", f1=fmt(w1), f2=fmt(w2), f3=fmt(w3))
      

      Solution: The sum of the digits in MUTE is 12.

      M and U are 12 and 21, in some order. And T and E are 11 and 22, in some order.

      So MUTE is either “12:21:11:22” or “21:12:22:11”. Either way there are four copies of each of the digits “1” and “2”.

      There are 8 ways to assign the letters in ATURAL and UMBER (and N can be any of the remaining values).

      The assignments are of the form:

      NATURAL = N:x:yy:xy:z:x:xz
      NUMBER = N:xy:yx:yz:xx:z

      where: (x, y) are (1, 2), in some order; and z ∈ {0, 3, 4, 5}.


      As noted by Brian Gladman [link], it is more efficient if the words are considered starting at the end, rather than the beginning.

      I think this is because there are many values with leading characters of “1” and “2”, but when values are grouped by their final character they form into much smaller sets (max size = 3).

      The internal runtime of my code is about twice as fast if the words and values are simply reversed (and the answer is the same). But it is straightforward to adapt the program to process the strings from the end.

      This program runs in 90ms. (Internal runtime is 33ms). [@replit].

      from enigma import (irange, subsets, filter2, group, item, update, remove, translate, join, catch, printf)
      
      # replace letters in <X>, <Y> with numeric values from <ns>, so the numeric strings formed are equal
      # <ns> groups values by their final digit
      # return the map: letter -> value
      def _solve(X, Y, ns, d=dict()):
        # are we done?
        if X == Y:
          yield d
        elif X and Y:
          # remove any common suffix
          while X and Y and X[-1] == Y[-1]: (X, Y) = (X[:-1], Y[:-1])
          if not (X and Y): return
          # split the final characters into numeric / non-numeric
          (nums, nons) = filter2((lambda x: x.isdigit()), [X[-1], Y[-1]])
          # different final numerics = failure
          if len(nums) > 1: return
          # choose values with the same final digit
          # if there is a numeric value use that, otherwise use the groups
          for k in (nums or ns.keys()):
            ss = ns.get(k, [])
            for vs in subsets(ss, size=len(nons), select='P'):
              # update the strings
              d_ = update(d, nons, vs)
              (X_, Y_) = (translate(x, d_, embed=0) for x in (X, Y))
              ns_ = update(ns, [(k, remove(ss, *vs))])
              # and solve for the translated strings
              yield from _solve(X_, Y_, ns_, d_)
      
      # replace letters in <X>, <Y>
      def solve(X, Y, ns):
        # group numeric values (as strings) by their final digit
        ns = group(map(str, ns), by=item(-1), fn=set)
        # and call the solver
        return _solve(X, Y, ns, dict())
      
      # possible numeric values
      ns = irange(0, 25)
      
      # word values we are interested in
      (w1, w2, w3) = ("NATURAL", "NUMBER", "MUTE")
      
      # turn a word into a string
      fmt = lambda w, sep=':': join((d.get(x, x) for x in w), sep=sep)
      # calculate digit sum
      dsum = lambda w: sum(map(int, fmt(w, sep='')))
      
      # solve the puzzle
      for d in solve(w1, w2, ns):
        # sum the digits in w3
        s = catch(dsum, w3)
        if s is not None:
          # output solution
          printf("dsum({w3}) = {s} [{w1}={f1} {w2}={f2} {w3}={f3}]", f1=fmt(w1), f2=fmt(w2), f3=fmt(w3))
      

      Like

  • Unknown's avatar

    Jim Randell 4:40 pm on 5 May 2023 Permalink | Reply
    Tags:   

    Teaser 3163: Letterboxes 

    From The Sunday Times, 7th May 2023 [link] [link]

    To enable me to spell out ONE, TWO, up to NINE one or more times, I bought large quantities of the letters E, F, G, H, I, N etc. Then in a box labelled “ONE” I put equal numbers of Os, Ns and Es; in a second box labelled “TWO” I put equal numbers of Ts, Ws and Os; in box “THREE” I put equal numbers of Ts, Hs and Rs, together with double that number of Es; etc.

    In this way I made nine boxes from which my grandson could take out complete sets to spell out the relevant digit. In total there was a prime number of each of the letters, with equal numbers of Ns and Vs, but more Ts. Furthermore, the grand total number of letters in the boxes was a two-figure prime.

    In the order ONE to NINE, how many sets were in each box?

    [teaser3163]

     
    • Jim Randell's avatar

      Jim Randell 5:04 pm on 5 May 2023 Permalink | Reply

      This Python program chooses a word at each stage that has a letter not involved in any of the remaining words. So we can then start adding copies of the word to the total collection of letters, and when the uninvolved letters all have a prime number of copies, we can look to solve for the remaining words. And we do this while there are less than 100 letters in the total collection. In this way we exhaustively explore the solution space.

      The collection of words we are given is such that there is always at least one word at each stage that has letters not involved in the remaining words.

      This Python program runs in 166ms. (Internal runtime is 108ms).

      Run: [ @replit ]

      from enigma import (defaultdict, irange, inf, is_prime, multiset, peek, remove, update, map2str, printf)
      
      # map letters to the words they appear in
      def group(ns):
        g = defaultdict(set)
        for n in ns:
          for k in set(n):
            g[k].add(n)
        return g
      
      # solve for remaining words <ws>, such that the count of each letter is prime
      # m = current allocation of letters
      # d = map of words used to number of repetitions
      # return (m, d)
      def solve(ns, m=multiset(), d=dict()):
        # are we done?
        if not ns:
          yield (m, d)
        else:
          g = group(ns)
          # choose a letter with the minimum number of words
          k = min(g.keys(), key=(lambda k: len(g[k])))
          # choose a word
          w = peek(g[k])
          ns_ = remove(ns, w)
          # letters that no longer appear
          xs = set(w).difference(*ns_)
          # add in sets of letters for this word
          m_ = m.copy()
          for i in irange(1, inf):
            m_.update_from_seq(w)
            if m_.size() > 99: break
            # any letters that no longer appear must be prime
            if not all(is_prime(m_.count(x)) for x in xs): continue
            # attempt to solve for the remaining words
            yield from solve(ns_, m_, update(d, [(w, i)]))
      
      # solve the puzzle for the given labels
      words = "ONE TWO THREE FOUR FIVE SIX SEVEN EIGHT NINE".split()
      for (m, d) in solve(words):
        # total number of letters is prime
        if not is_prime(m.size()): continue
        # total number of N's and V's is the same and there are more T's
        if not (m.count('T') > m.count('N') == m.count('V')): continue
        # valid solution found, output the counts for the labels
        for w in words:
          printf("{k}x {w}", k=d[w])
        printf("total letters = {n} {m}", n=m.size(), m=map2str(m))
        printf()
      

      Solution: The numbers of sets are:

      1× ONE
      2× TWO
      3× THREE
      2× FOUR
      3× FIVE
      5× SIX
      2× SEVEN
      2× EIGHT
      1× NINE

      This gives a (prime) total of 83 letters.

      The individual letter counts are:

      E=17
      F=5
      G=2
      H=5
      I=11
      N=5
      O=5
      R=5
      S=7
      T=7
      U=2
      V=5
      W=2
      X=5

      Like

      • Jim Randell's avatar

        Jim Randell 6:28 pm on 5 May 2023 Permalink | Reply

        Or using the [[ SubstitutedExpression() ]] solver from the enigma.py library, we can construct an alphametic equivalent of the puzzle and solve that (although it assumes there are no more than 9 sets of letters in any box (but you can use a higher base parameter to allow more, allowing up to 16 sets is sufficient to provide an exhaustive search)).

        The following Python program runs in 64ms. (Internal runtime of the generated program is 2.3ms).

        Run: [ @replit ]

        from enigma import (SubstitutedExpression, irange, is_prime, union, join, sprintf, multiset, str_upper, map2str, printf)
        
        # the labels for the boxes
        words = "ONE TWO THREE FOUR FIVE SIX SEVEN EIGHT NINE".split()
        
        # make symbols for each word
        symbols = dict(zip(words, str_upper))
        
        # make an expression from a multiset of symbols
        def expr(m):
          ts = list()
          for (s, n) in m.items():
            if n == 1:
              ts.append(s)
            elif n > 0:
              ts.append(sprintf("{n}*{s}"))
          return join(ts, sep=" + ")
        
        # make a symbol count for each letter
        xs = dict((k, multiset()) for k in union(words))
        for (w, s) in symbols.items():
          for k in w:
            xs[k].add(s)
        
        # construct the expressions:
        
        # each letter count is prime
        exprs = list(sprintf("is_prime({x})", x=expr(m)) for (k, m) in xs.items())
        
        # the total letter count is prime
        m = multiset.from_pairs((s, len(w)) for (w, s) in symbols.items())
        exprs.append(sprintf("is_prime_2d({x})", x=expr(m)))
        
        # N count = V count
        (m1, m2) = xs['N'].differences(xs['V'])
        exprs.append(sprintf("{x1} == {x2}", x1=expr(m1), x2=expr(m2)))
        
        # T count > V count
        (m1, m2) = xs['T'].differences(xs['V'])
        exprs.append(sprintf("{x1} > {x2}", x1=expr(m1), x2=expr(m2)))
        
        # construct the alphametic problem
        p = SubstitutedExpression(
          exprs,
          digits=irange(1, 9),
          distinct=[],
          env=dict(is_prime_2d=lambda n: 9 < n < 100 and is_prime(n)),
          verbose=0
        )
        
        # solve it
        for r in p.solve():
          # output solution
          m = multiset()
          for w in words:
            n = r[symbols[w]]
            printf("{n}x {w}")
            m.update_from_seq(w, count=n)
          printf("total letters = {n} {m}", n=m.size(), m=map2str(m))
          printf()
        

        Like

    • Frits's avatar

      Frits 10:41 pm on 5 May 2023 Permalink | Reply

      Just a basic program with hard coded logic.

      A following step could be to remove this logic and generate a solution like with SubstitutedExpression().

       
      from enigma import int2words
      
      lenwords = [len(int2words(i)) for i in range(1, 10)]
      
      # primes up to 99
      P = {3, 5, 7}
      P |= {2} | {x for x in range(11, 100, 2) if all(x % p for p in P)}
      smallP = sorted(P)[:4]
      
      # (n1, n2, n3, n4, n5, n6, n7, n8, n9): the number of sets in each box
      # assumption: there are no more than 9 sets of letters in any box
      
      for n8 in smallP:                    
        for n3 in range(1, 10):                 
          if (n3 + n8) not in P: continue  # number of Hs  
          for n2 in smallP:               
            if (n2 + n3 + n8) not in P: continue # number of Ts 
            for n4 in smallP:             
              if (n3 + n4) not in P: continue # number of Rs 
              for n5 in range(1, 10):     
                if (n4 + n5) not in P: continue # number of Fs
                for n7 in range(1, 10):   
                  if (n5 + n7) not in P: continue # number of Vs
                  if not (n2 + n3 + n8 > n5 + n7): continue # more Ts than Vs
                  for n9 in range(1, 10): 
                    n1 = n5 - 2 * n9           # equal numbers of Ns and Vs
                    if not (0 < n1 < 10): continue 
                    if (n1 + n2 + n4) not in P: continue     # number of Os
                    
                    if (n1 + 2*n3 + n5 + 2*n7 + n8 + n9) not in P: # number of Es
                      continue 
                    for n6 in smallP:     
                      if (n5 + n6 + n8 + n9) not in P: continue # number of Is
                      if (n6 + n7) not in P: continue # number of Ss
                      
                      # total is also prime
                      answer = (n1, n2, n3, n4, n5, n6, n7, n8, n9)
                      T = sum(x * y for x, y in zip(answer, lenwords))
                      if T not in P: continue 
                      print(answer)  
      

      Like

    • Frits's avatar

      Frits 12:50 pm on 6 May 2023 Permalink | Reply

      The following program emulates SubstitutedExpression() from the enigma library.
      The programs runs fast (6 ms) although the order of rows in list lst doesn’t seem to be optimal.

         
      from enigma import int2words
      
      # return calculation string number of occurrences of character <c>
      def N(c):
        seq = [x[1] for x in lst if x[0] == c][0]
        # check if enough variables available
        if any(not sofar[i] for i, x in enumerate(seq) if x): return ""
        
        return ' + '.join(("n" if x == 1 else str(x) + "*n") + str(i) 
               for i, x in enumerate(seq, 1) if x)
      
      # primes up to 99
      P = {3, 5, 7}
      P |= {2} | {x for x in range(11, 100, 2) if all(x % p for p in P)}
      smallP = sorted(P)[:4]
      
      words = [int2words(i).upper() for i in range(1, 10)]
      lenwords = [len(w) for w in words]
      
      letters = "".join(set("".join(words)))
      
      # list of characters and usage within words
      lst = [(c, [w.count(c) for w in words]) for c in letters] 
      # sort characters  (least usage first)
      lst.sort(key=lambda x: sum(x[1]))
      
      # (n1, n2, n3, n4, n5, n6, n7, n8, n9): the number of sets in each box
      # assumption: there are no more than 9 sets of letters in any box
      
      # list of variable names which may only be a prime number     
      primes = {"n" + str([i for i, y in enumerate(x[1], 1) if y][0]) 
                for x in lst if sum(x[1]) == 1}    
                
      sofar = [1 for _ in range(10)]      
      
      # additional conditions
      extra = [("# equal numbers of Ns and Vs\n",
                "if not(" + N('N') + " == " + N('V') + "): continue\n"),
               ("# more Ts than Vs\n",
                "if not(" + N('T') + " > " + N('V') + "): continue\n")]          
      
      ex = ind = done = ""
      sofar = [0 for _ in range(10)]
      for c, seq in lst:
        # check which variables in <seq> have not been used sofar
        new = [i for i, (x, y) in enumerate(zip(seq, sofar), 1) if not y and x != y]
        
        for nv in new:
          var = "n" + str(nv)
          r = "smallP" if var in primes else "range(1, 10)"
        
          # add for loop for new variable
          ex += ind + "for " + var + " in " + r + ":\n"
          sofar[nv - 1] = 1
          ind += "  "
        
          # add prime checks 
          for c2 in [ch for ch, sq in lst if ch not in done and sum(sq) > 1]:
            # function N checks if enough variables are available
            n = N(c2)   # number of occurrences of character <c2>
            if n:       
              ex += ind + "if (" + n + ") not in P: continue # number of "
              ex += c2 + "s \n"
              done += c2
          
          # add extra conditions if enough variables are available
          sofar_vars = set("n" + str(i) for i, x in enumerate(sofar, 1) if x)
          for comm, cond in extra.copy():
            nvars = cond.count('+') + 2
            # do we have enough variable in <sofar_vars>?
            if nvars == sum(sv in cond for sv in sofar_vars):
              ex += ind + comm
              ex += ind + cond
              extra.remove((comm, cond)) # no need to process condition again
      
      # remaining conditions        
      for comm, cond in extra:  
        ex += ind + comm
        ex += ind + cond
        
      # add additional conditions to most inner loop
      ex += ind + "\n"
      ex += ind + "# total is also prime\n"
      ex += ind + "answer = (n1, n2, n3, n4, n5, n6, n7, n8, n9)\n"
      ex += ind + "T = sum(x * y for x, y in zip(answer, lenwords))\n"
      ex += ind + "if T not in P: continue\n" 
      
      ex += ind + "print('answer:', answer)\n"
      
      #print(ex)  
      exec(ex)
      

      with the following generated code:

         
      for n2 in smallP:
        for n8 in smallP:
          for n6 in smallP:
            for n4 in smallP:
              for n5 in range(1, 10):
                if (n4 + n5) not in P: continue # number of Fs
                for n7 in range(1, 10):
                  if (n5 + n7) not in P: continue # number of Vs
                  if (n6 + n7) not in P: continue # number of Ss
                  for n3 in range(1, 10):
                    if (n3 + n4) not in P: continue # number of Rs
                    if (n3 + n8) not in P: continue # number of Hs
                    if (n2 + n3 + n8) not in P: continue # number of Ts
                    # more Ts than Vs
                    if not(n2 + n3 + n8 > n5 + n7): continue
                    for n1 in range(1, 10):
                      if (n1 + n2 + n4) not in P: continue # number of Os
                      for n9 in range(1, 10):
                        if (n5 + n6 + n8 + n9) not in P: continue # number of Is
                        if (n1 + n7 + 2*n9) not in P: continue # number of Ns
                        if (n1 + 2*n3 + n5 + 2*n7 + n8 + n9) not in P: continue # number of Es
                        # equal numbers of Ns and Vs
                        if not(n1 + n7 + 2*n9 == n5 + n7): continue
      
                        # total is also prime
                        answer = (n1, n2, n3, n4, n5, n6, n7, n8, n9)
                        T = sum(x * y for x, y in zip(answer, lenwords))
                        if T not in P: continue
                        print('answer:', answer)
      

      Like

    • GeoffR's avatar

      GeoffR 9:38 am on 10 May 2023 Permalink | Reply

      % A Solution in MiniZinc
      include "globals.mzn";
      
      % Assumed ranges of primes for vowels and consonants
      set of int:vowels = {2, 3, 5, 7, 11, 13, 17, 19};
      set of int:cons = {2, 3, 5, 7, 11};
      
      var vowels:E; var vowels:I; var vowels:O; var vowels:U;
      var cons:F; var cons:G; var cons:H; var cons:N;  var cons:R;
      var cons:S; var cons:T; var cons:V; var cons:W;  var cons:X;
      
      % 2-digit primes for total sum of all letters
      set of int:primes2d = {11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 
                            53, 59, 61, 67, 71, 73, 79, 83, 89, 97};
                       
      % Numbers of groups of  spelt numbers - assumed range 1..9
      var 1..9:ONE; var 1..9:TWO; var 1..9:THREE; 
      var 1..9:FOUR; var 1..9:FIVE; var 1..9:SIX;
      var 1..9:SEVEN; var 1..9:EIGHT; var 1..9: NINE;
      
      % Given:  N == V  and  T > N;
      constraint (ONE + SEVEN + NINE * 2) == (FIVE + SEVEN)
      /\ (TWO + THREE + EIGHT) > (ONE + SEVEN + NINE * 2);
      
      % sum of total letters
      var 11..97: total_letters;
      
      % Find sums of individual letters
      constraint E == (ONE + THREE * 2 + FIVE + SEVEN * 2  + EIGHT + NINE );
      constraint F == (FOUR + FIVE ) /\ G == EIGHT ;
      constraint H == (THREE + EIGHT) /\ I == (FIVE + SIX + EIGHT + NINE);
      constraint N == (ONE + SEVEN + NINE * 2) /\ O == (ONE + TWO + FOUR) ;
      constraint R == (THREE + FOUR) /\ S == (SIX + SEVEN) /\ T == (TWO + THREE + EIGHT);
      constraint U == FOUR /\ V == (FIVE + SEVEN) /\ W == TWO /\ X == SIX;
      
      constraint total_letters == E + F + G + H + I + N + O + R + S + T + U + V + W + X;
      constraint total_letters in primes2d;
      
      solve satisfy;
      
      output["Sets:[ONE, TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE] = " ++ "\n"
      ++ show([ONE, TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE])   ++ "\n" 
      ++ "Letters:[E, F, G, H, I, N, O, R, S, T, U, V, W, X] = " ++  "\n" ++ 
      "       " ++ show( [E,F,G,H,I,N,O,R,S,T,U,V,W,X]) ]; 
       
      
      
      

      Like

  • Unknown's avatar

    Jim Randell 10:25 am on 4 May 2023 Permalink | Reply
    Tags:   

    Teaser 1977: Reverse and add 

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

    In the above sums, digits have consistently been replaced by letters, with different letters used for different digits.

    Find the number for this SUNDAY.

    [teaser1977]

     
    • Jim Randell's avatar

      Jim Randell 10:26 am on 4 May 2023 Permalink | Reply

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

      The following run file (or command line) executes in 65ms. (Internal runtime of the generated program is 312µs).

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      "AND + DNA = BUT"
      "BUT + TUB = ALSO"
      
      --answer="SUNDAY"
      
      % python3 -m enigma -r SubstitutedExpression "AND + DNA = BUT" "BUT + TUB = ALSO" --answer="SUNDAY"
      (AND + DNA = BUT) (BUT + TUB = ALSO) (SUNDAY)
      (183 + 381 = 564) (564 + 465 = 1029) (268317) / A=1 B=5 D=3 L=0 N=8 O=9 S=2 T=4 U=6 Y=7
      SUNDAY = 268317 [1 solution]
      

      Solution: SUNDAY = 268317.

      The sums are:

      AND + DNA = BUT → 183 + 381 = 564
      BUT + TUB = ALSO → 564 + 465 = 1029

      Like

    • GeoffR's avatar

      GeoffR 2:55 pm on 4 May 2023 Permalink | Reply

      from itertools import permutations
      
      digits = set(range(10))
      
      # 1st stage permutation (3 digits)
      for p1 in permutations (digits, 3):
        A, N, D = p1
        if 0 in (A, D):continue
        AND, DNA = 100*A + 10*N + D, 100*D + 10*N + A
        
        # 2nd stage permutation (3 digits)
        qs = digits.difference(p1)
        for p2 in permutations(qs, 3):
          B, U, T = p2
          if 0 in (B, T):continue 
          BUT, TUB = 100*B + 10*U + T, 100*T + 10*U + B
          if AND + DNA != BUT:continue
      
          # 3rd stage permutation (4 digits)
          qs2 = qs.difference(p2)
          for p3 in permutations(qs2):
            L, S, O, Y = p3
            ALSO = 1000*A + 100*L + 10*S + O
            if BUT + TUB == ALSO:
              SUNDAY = 100000*S + 10000*U + 1000*N + 100*D + 10*A + Y
              print('SUNDAY = ', SUNDAY)
              
      # SUNDAY =  268317
      
      
      

      Like

    • Frits's avatar

      Frits 7:28 pm on 4 May 2023 Permalink | Reply

         
      '''
      "AND + DNA = BUT"
      "BUT + TUB = ALSO"
      '''
       
      A = 1  # thousands digit
      # D, T, B are consecutive digits T = D + 1, B = T + 1
      # D >= 3 as T >= 4 (as B + T >= 9 and B = T + 1) 
      # D != 4 as T != 5 (B = T + 1, if T = 5 then O would become 1 (A))
      # D != 5 otherwise (N, U) becomes (9, 8) and O would become 7 (B))
      # D != 6 otherwise N becomes 5 and U becomes 0 and S becomes 0 (U) or 1 (A)
      #               or N becomes 9 and U becomes 8 (B)
      # D != 7 otherwise O becomes 7 as well
      (D, T, B, O) = (3, 4, 5, 9)
      
      # N != 5 otherwise U would be 0 forcing S to be 0 (U) or 1 (A)
      # N != 6 as U >= 5 as T + B = 9
      # N != 7 otherwise U would be 4 (T)
      # N != 9 as O is 9 
      (N, U) = (8, 6)
      
      # S = 2 ((2 * U) % 10)
      # L = 0 (B + T + 1 = 10)
      # Y = 7 (remaining digit)
      (S, L, Y) = (2, 0, 7)
      
      print("SUNDAY = ", ''.join(str(x) for x in (S, U, N, D, A, Y)))
      

      Like

  • Unknown's avatar

    Jim Randell 9:10 am on 2 May 2023 Permalink | Reply
    Tags:   

    Teaser 2624: Is it 2013? 

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

    I have in mind a four-digit number. Here are three clues about it:

    (1) Like 2013, it uses 4 consecutive digits in some order;
    (2) It is not divisible by any number from 2 to 11;
    (3) If I were to tell you the _____ digit, then you could work out my number.

    Those clues would have enabled you to work out my number, but unfortunately the printer has left a gap: it should have been the word “units” or “tens” or “hundreds” or “thousands”.

    What is my number?

    [teaser2624]

     
    • Jim Randell's avatar

      Jim Randell 9:13 am on 2 May 2023 Permalink | Reply

      The puzzle works equally well if the missing word is chosen from: “first”, “second”, “third”, “fourth”.

      It is straightforward to consider all possible 4-digit numbers. But we can be a bit cleverer, and there are a few sets of digits we can use, some of which can be discarded immediately, and we can then produce viable permutations from the remaining digits.

      This Python program runs in 52ms. (Internal runtime is 201µs).

      Run: [ @replit ]

      from enigma import (irange, tuples, subsets, nconcat, filter_unique, item, join, printf)
      
      # generate candidate numbers
      def generate():
        # consider possible sets of digits
        for ds in tuples(irange(0, 9), 4):
          # discard any sets where all numbers will be divisible by 3
          if sum(ds) % 3 == 0: continue
          # consider possible permutations
          for (A, B, C, D) in subsets(ds, size=4, select='P'):
            # discard final digit = 0, 2, 4, 6, 8 (divisible by 2) or 5 (divisible by 5)
            if D not in {1, 3, 7, 9}: continue
            # discard if divisible by 7 or 11
            n = nconcat(A, B, C, D)
            if n % 7 == 0 or n % 11 == 0: continue
            # return viable (<candidate>, <digits>)
            yield (A, B, C, D)
      
      # collect candidate numbers
      rs = list(generate())
      
      # consider possible missing digit positions
      for i in irange(0, 3):
        # there must be only one solution
        ss = filter_unique(rs, item(i)).unique
        if len(ss) == 1:
          printf("digit {i} => {n}", i=i + 1, n=join(ss[0]))
      

      Solution: The number is 2143.


      There are 5 candidate numbers where if we were told the digit in a certain position we would be able to work out the number:

      1st digit = 1 ⇒ 1423
      1st digit = 8 ⇒ 8567
      2nd digit = 1 ⇒ 2143
      3rd digit = 1 ⇒ 2413
      3rd digit = 3 ⇒ 4231

      But the puzzle text tells us that if we knew which of the words the printer had left out, we would be able to work out the number. (Without being told the value of the digit in that position).

      The only possibility that lets us work out the number, is if the position the printer omitted is the 2nd digit (“hundreds”) (as either of 1st (“thousands”) or 3rd (“tens”) would not allow us to work out the number).

      So the missing word must be “hundreds”, and the number is 2143.

      Like

    • Frits's avatar

      Frits 1:43 pm on 3 May 2023 Permalink | Reply

         
      from itertools import permutations as perm
      
      # sum(x, x+1, x+2, x+3) = 4.x + 6 is divisible by 3 if x is divisible by 3 
      # dgts = 1-8    (as x cannot be 0 and x+3 cannot be 9)
      mind, maxd = 1, 8
      dgts = ''.join(str(x) for x in range(mind, maxd + 1))
      
      cands = [''.join(p) for s in [x - mind for x in range(mind, maxd - 2) if x % 3]
               for p in perm(dgts[s: s + 4])
               if p[-1] in "137" and all(int(''.join(p)) % i for i in [7, 11])]
               
      # consider possible missing digit positions
      for pos in range(4):
        # check if there is a unique digit that occurs only once in this position
        occ1 = [s[0] for d in dgts if len(s := [c for c in cands if c[pos] == d]) == 1]
        if len(occ1) == 1:
          print("answer:", occ1[0]) 
      

      If we know there is exactly one solution:

         
      from itertools import permutations as perm
      
      # reverse non-empty 2-dimensional matrix (from  MxN to NxM)
      R = lambda mat: [[m[pos] for m in mat] for pos in range(len(mat[0]))]
      
      # sum(x, x+1, x+2, x+3) = 4.x + 6 is divisible by 3 if x is divisible by 3 
      # dgts = 1-8    (as x cannot be 0 and x+3 cannot be 9)
      mind, maxd = 1, 8
      dgts = ''.join(str(x) for x in range(mind, maxd + 1))
      
      cands = [''.join(p) for s in [x - mind for x in range(mind, maxd - 2) if x % 3]
               for p in perm(dgts[s: s + 4])
               if p[-1] in "137" and all(int(''.join(p)) % i for i in [7, 11])]
               
      # check reversed candidates (from  MxN to NxM)
      ind = [s[0] for r in R(cands) 
             if len(s := [r.index(d) for d in dgts if r.count(d) == 1]) == 1][0] 
      print("answer:", cands[ind])
      

      Like

  • Unknown's avatar

    Jim Randell 3:43 pm on 30 April 2023 Permalink | Reply
    Tags:   

    Teaser 2406: [Powers of three] 

    From The Sunday Times, 2nd November 2008 [link]

    For a school project, Penny was investigating the powers of 3. She wrote some down and explained to Joe that the first power was 3, the second power was 9, the third power was 27, and so on. Soon the numbers were in the millions and Joe asked how close to a whole number of millions one of the high powers could be.

    Simply by some clever logic and algebra Penny was soon able to answer the question. She also found the lowest power of 3 that actually came that close.

    Which power (e.g. 248th) was it?

    This puzzle was originally published with no title.

    [teaser2406]

     
    • Jim Randell's avatar

      Jim Randell 3:44 pm on 30 April 2023 Permalink | Reply

      For positive integers k we have 3^k is an odd number, so we are never going to land on an exact number of millions.

      But if we consider the sequence:

      a[k] = (3^k) mod 1000000

      Then it will form a repeating sequence of values, all less than a million. So we can calculate a[k] for increasing k, until we see a repeated value, and that will tell us where 3^k is closest to an exact million.

      This Python program runs in 96ms. (Internal runtime is 41ms).

      Run: [ @replit ]

      from enigma import (Accumulator, divr, number, arg, printf)
      
      M = number(arg("1_000_000", 0))  # M is the modulus
      n = arg(3, 1, int)  # n is the base
      
      # find the closest value (= smallest delta)
      r = Accumulator(fn=min, collect=1)
      
      # consider increasing powers of n mod M
      (p, k, a) = (1, 0, dict())
      while True:
        p *= n
        p %= M
        k += 1
        # have we already seen this residue?
        if p in a:
          # calculate the period
          printf("[period = {d}]", d=k - a[p])
          break
        # otherwise, record the value
        a[p] = k
        if r.count > 0 or divr(pow(n, k), M) > 0:
          r.accumulate_data(min(p, M - p), k)
      
      # output solution
      for k in r.data:
        printf("{n}^{k} mod {M} = {x} [delta = {r.value}]", x=pow(n, k, M))
      

      Solution: The lowest power of 3 that comes closest to an exact million is the 50,000th.

      We have:

      >>> pow(3, 50_000, 1_000_000)
      1
      

      In fact 3^50000 is a 23,857 digit number: 1155409630…2761000001.

      And we see that the sequence has a period of 50,000, so:

      >>> pow(3, 50_000 * 2, 1_000_000)
      1
      >>> pow(3, 50_000 * 3, 1_000_000)
      1
      >>> pow(3, 50_000 * 117, 1_000_000)
      1
      

      And of course, we have a[0] = 1.


      It is not possible for a power of 3 to be −1 (mod 10^6), which we can see by calculating values of 3^k mod 100. There are only 20 possible values, and 99 never occurs.

      But we can use Euler’s Theorem [@wikipedia] to determine that there is some value k such that:

      3^k (mod 10^6) = 1

      (as 10^6 and 3 are co-prime).

      And we can calculate one possible value of k using Euler’s totient function [@wikipedia] as φ(10^6).

      However this is not necessarily the smallest value of k (although Lagrange’s theorem [@wikipedia] tells that the smallest value of k must divide φ(10^6)).

      But we can calculate the required value with a relatively modest amount of calculation.

      If we consider the powers of 3^k mod 10^n, we find that when n = 1 we have:

      mod 10:
      3^1 = 3
      3^2 = 3×3 = 9
      3^3 = 9×3 = 7
      3^4 = 7×3 = 1

      So every a[4k] = 1 (mod 10).

      For n = 2 we can just look at 3^(4k) mod 100:

      mod 100:
      3^4 = 81
      3^8 = 81×81= 61
      3^12 = 61×81 = 41
      3^16 = 41×81 = 21
      3^20 = 21×81 = 1

      So every a[20k] = 1 (mod 100).

      For n = 3:

      mod 1000:
      3^20 = 401
      3^40 = 401×401 = 801
      3^60 = 801×401 = 201
      3^80 = 201×401 = 601
      3^100 = 601×401 = 1

      So every a[100k] = 1 (mod 1000).

      For n = 4:

      mod 10000:
      3^100 = 2001
      3^200 = 2001×2001 = 4001
      3^300 = 4001×2001 = 6001
      3^400 = 6001×2001 = 8001
      3^500 = 8001×2001 = 1

      So every a[500k] = 1 (mod 10000).

      For n = 5:

      mod 100000:
      3^500 = 10001
      3^1000 = 10001×10001 = 20001
      3^1500 = 20001×10001 = 30001
      3^2000 = 30001×10001 = 40001
      3^2500 = 40001×10001 = 50001
      3^3000 = 50001×10001 = 60001
      3^3500 = 60001×10001 = 70001
      3^4000 = 70001×10001 = 80001
      3^4500 = 80001×10001 = 90001
      3^5000 = 90001×10001 = 1

      So every a[5000k] = 1 (mod 100000).

      For n = 6:

      mod 1000000:
      3^5000 = 100001
      3^10000 = 100001×100001 = 200001
      3^15000 = 200001×100001 = 300001
      3^20000 = 300001×100001 = 400001
      3^25000 = 400001×100001 = 500001
      3^30000 = 500001×100001 = 600001
      3^35000 = 600001×100001 = 700001
      3^40000 = 700001×100001 = 800001
      3^45000 = 800001×100001 = 900001
      3^50000 = 900001×100001 = 1

      So every a[50000k] = 1 (mod 1000000).

      And this provides our answer.


      Or using Euler’s totient function (as mentioned above).

      This Python program runs in 55ms. (Internal runtime is 52µs).

      Run: [ @replit ]

      from enigma import (prime_factor, divisors, number, arg, printf)
      
      # Euler's totient function
      def phi(n):
        t = n
        for (p, _) in prime_factor(n):
          t -= t // p
        return t
      
      M = number(arg("1_000_000", 0))  # M is the modulus
      n = arg(3, 1, int)  # n is the base
      
      # the order of the multiplicative group of integers modulo M is ...
      m = phi(M)
      printf("[phi({M}) = {m}]")
      
      # Lagrange's Theorem = "The smallest k such that a^k = 1 (mod M) divides m"
      # look for the smallest value ...
      for k in divisors(m):
        if pow(n, k, M) == 1:
          printf("{n}^{k} mod {M} = 1")
          break
      

      Like

      • Jim Randell's avatar

        Jim Randell 2:16 pm on 1 May 2023 Permalink | Reply

        In fact we can be a bit cleverer by solving the equivalent problem for the prime factors of the modulus, and then the required result is the lowest common multiple of these exponents.

        Run: [ @replit ]

        from enigma import (prime_factor, divisors, mlcm, gcd, number, arg, printf)
        
        # calculate the totient of p^k, for prime p
        phi_pk = lambda p, k: pow(p, k - 1) * (p - 1)
        
        def solve(n, M):
          assert gcd(n, M) == 1
          # collect exponents for prime factors of the modulus
          ks = list()
          for (p, e) in prime_factor(M):
            f = pow(p, e)
            for k in divisors(phi_pk(p, e)):
              if pow(n, k, f) == 1:
                ks.append(k)
                break
          return mlcm(*ks)
        
        M= number(arg("1_000_000", 0))  # M is the modulus
        n = arg(3, 1, int)  # n is the base
        
        k = solve(n, M)
        r = pow(n, k, M)
        printf("{n}^{k} mod {M} = {r}")
        assert r == 1
        

        Like

  • Unknown's avatar

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

    Teaser 3162: Flash Cordon and Ming 

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

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

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

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

    [teaser3162]

     
    • Jim Randell's avatar

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

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

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

      Run: [ @replit ]

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

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

      The first set is:

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

      And the second set:

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

      Like

    • Frits's avatar

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

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

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

      Like

      • Jim Randell's avatar

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

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

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

        Internal runtimes are 16.8ms and 7.0ms.

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

        Like

        • Frits's avatar

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

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

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

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

          Like

  • Unknown's avatar

    Jim Randell 9:36 am on 27 April 2023 Permalink | Reply
    Tags:   

    Teaser 2626: Homophones 

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

    Wo, who lives in Woe, has been studying homophones (i.e. sets of words that sound the same, but have different meanings). He has now extended his studies to sets of numbers that have a common property.

    He wrote down three numbers which were prime, and a further two numbers whose different digits formed a consecutive set. Having consistently replaced different digits by different letters, this made his primes into:

    TWO
    TOO
    TO

    and his numbers using consecutive digits into:

    STRAIGHT
    STRAIT

    What is the three-figure number WOE?

    This puzzle appears in the book The Sunday Times Brain Teasers Book 2 (2020) under the title “Our secret”.

    [teaser2626]

     
    • Jim Randell's avatar

      Jim Randell 9:36 am on 27 April 2023 Permalink | Reply

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

      The following run file executes in 79ms. (Internal runtime is 10.4ms).

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      # primes
      "is_prime(TWO)"
      "is_prime(TOO)"
      "is_prime(TO)"
      
      # check for a consecutive set of digits
      --code="check = lambda ds: max(ds) - min(ds) == len(ds) - 1"
      "check({S, T, R, A, I, G, H, T})"
      "check({S, T, R, A, I, T})"
      
      # no leading zeros
      --invalid="0,TSW"
      
      --answer="WOE"
      --template="(TWO TOO TO) (STRAIGHT STRAIT) (WOE)"
      --solution=""
      

      Solution: WOE = 798.

      There are 84 possible solutions, although the following words have the same value in all of them:

      TO = 19
      TOO = 199
      TWO = 179
      WOE = 798

      Note that STRAIGHT and STRAIT are not required to be prime, but there are only 7 solutions where they are:

      STRAIGHT = 41203651, STRAIT = 412031
      STRAIGHT = 31402561, STRAIT = 314021
      STRAIGHT = 41325601, STRAIT = 413251
      STRAIGHT = 21435061, STRAIT = 214351
      STRAIGHT = 31245601, STRAIT = 312451
      STRAIGHT = 31245061, STRAIT = 312451
      STRAIGHT = 41352061, STRAIT = 413521

      Like

    • Frits's avatar

      Frits 1:57 pm on 27 April 2023 Permalink | Reply

         
      #! python3 -m enigma -r
       
      SubstitutedExpression
       
      # primes
      "is_prime(TWO)"
      "is_prime(TOO)"
      "is_prime(TO)"
      
      #  credit: Brian Gladman [ https://brg.me.uk/?page_id=485 ]
      
      # since the digits for letters other than WOE are consecutive,
      # we must have one of the following four situations:
      #
      # digit sequence  (W, O, E) values (in some order with W <> 0)
      #      0..6       (7, 8, 9)
      #      1..7       (0, 8, 9)
      #      2..8       (0, 1, 9)
      #      3..9       (0, 1, 2)
       
      # check for a consecutive set of digits in remaining digits
      "''.join(sorted(str(WOE))) in {'012', '019', '089', '789'}", 
      
      # no leading zeros
      --invalid="0,OTW"
      --invalid="3-6,WOE"
      --invalid="2|8,O"
       
      --answer="WOE"
      --template="(TWO TOO TO) (WOE)"
      --solution=""
      

      Like

  • Unknown's avatar

    Jim Randell 10:25 am on 25 April 2023 Permalink | Reply
    Tags: ,   

    Teaser 2627: Inversion 

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

    In the woodwork class Pat cut a triangle from a sheet of plywood, leaving a hole in the sheet. The sides of the triangle were consecutive whole numbers of centimetres long. He then wanted to turn the triangle over and fit it back into the hole. To achieve this he had to cut the triangle into three pieces. He did this with two straight cuts, each cut starting at a midpoint of a side. Then each of the three pieces (two of which had the same perimeter) could be turned over and placed back in exactly its starting position in the hole.

    What are the lengths of the sides of the original triangle?

    [teaser2627]

     
    • Jim Randell's avatar

      Jim Randell 10:26 am on 25 April 2023 Permalink | Reply

      I imagined the wood was painted black on one side, and the goal is then to cut the triangle into 3 shapes, such that each shape will fit back into its original hole when turned over, to produce a completely black triangle of the same shape as the original.

      My first thought, when looking for a triangle with sides that are consecutive whole numbers, was to look at a (3, 4, 5) triangle, as these often crop up in these puzzles.

      Making two cuts between midpoints of sides divides the triangle into 3 shapes as shown:

      The quadrilateral Q, can be turned over to fit back into its vacated hole, but the triangles P and R do not, as they are not isosceles (but they are identical, and so have the same perimeter).

      But we can make these triangles isosceles, but in doing this the quadrilateral shrinks to zero size (and our two cuts become one):

      But what if we make the initial triangle such that it can be divided into two smaller isosceles triangles and a quadrilateral that can be turned over?

      We end up with something like this:

      And we can make Q and R have the same perimeter by making the base of R have length 2a.

      Now, the lengths {2a, 2b, 2(a + c)} must be consecutive integers in some order.

      So either:

      b = a + 1; c = 1/2
      b = a + 1/2; c = 1

      We can calculate the height h of triangles P and R to get:

      h² = a² − c² = b² − a²
      ⇒ 2a² − b² − c² = 0

      Substituting the possible values for b and c in terms of a, gives us quadratic equations (in a), which we can solve for viable solutions.

      This Python program runs in 59ms. (Internal runtime is 241µs).

      Run: [ @replit ]

      from enigma import (enigma, Polynomial, sq, printf)
      
      Q = enigma.Rational()
      as_int = lambda x: enigma.as_int(x, include="+", default=None)
      
      # consider values for c
      for c in (Q(1, 2), 1):
        # form the polynomial
        b = Polynomial([Q(3, 2) - c, 1])
        p = Polynomial([-sq(c), 0, 2]) - sq(b)
        # find positive rational roots for a
        for a in p.quadratic_roots(domain='Q', include="+", F=Q):
          # find integer sides
          sides = list(map(as_int, [2 * a, 2 * b(a), 2 * (a + c)]))
          if all(sides):
            # output solution
            printf("sides = {sides} [c={c} p={p} -> a={a}]", sides=sorted(sides))
      

      Solution: The sides of the original triangle were 5cm, 6cm, 7cm.


      The polynomials we form are:

      a² − 2a − 5/4 = 0
      ⇒ (2a + 1)(2a − 5) = 0
      ⇒ a = 5/2, b = 7/2, c = 1/2

      a² − a − 5/4 = 0
      ⇒ no rational roots

      The second polynomial does have a real root at: a = (1 + √6) / 2.

      Which gives a triangle with sides of (3.449…, 4.449…, 5.449…).

      So although the puzzle does not work with a (3, 4, 5) triangle, it does work with a (3, 4, 5) + (√6 − 2) triangle.

      Like

  • Unknown's avatar

    Jim Randell 11:57 am on 23 April 2023 Permalink | Reply
    Tags:   

    Brain teaser 1005: Words and numbers 

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

    A familiar letters-for-digits problem is to take a correct addition sum such as:

    1 + 1 = 2

    and write it in words so:

    ONE + ONE = TWO

    The problem is to assign numbers from 0 to 9 to the letters so that we get a correct addition sum, e.g. E = 6, N = 3, O = 2, T = 4, W = 7 gives:

    236 + 236 = 472

    Two rules to be kept are that the left hand digit of any number is not zero, and different letters are assigned different numbers.

    Some addition sums clearly have no solution. e.g.:

    ONE + THREE = FOUR

    In the Kudali Language the words for 1, 2, 3, 4 are AA, BA, EB, DE, although not necessarily in that order. Now all the addition sums:

    1 + 1 = 2
    1 + 2 = 3
    1 + 3 = 4
    2 + 2 = 4
    1 + 4 = 2 + 3

    when written in Kudali, give letters-for-digits problems each of which has at least one solution.

    What are the Kudali words for 1, 2, 3, 4, in that order?

    This puzzle is included in the books Microteasers (1986) and The Sunday Times Book of Brainteasers (1994).

    [teaser1005]

     
    • Jim Randell's avatar

      Jim Randell 11:57 am on 23 April 2023 Permalink | Reply

      This Python program uses the [[ SubstitutedExpression() ]] solver from the enigma.py library to see if there are solutions to the alphametic expressions.

      It runs in 85ms.

      Run: [ @replit ]

      from enigma import (SubstitutedExpression, subsets, sprintf, printf)
      
      # does an alphametic sum have answers?
      solve = lambda expr: any(SubstitutedExpression([expr]).solve(verbose=0))
      
      # expressions involving words for 1, 2, 3, 4
      exprs = [
        "{w1} + {w1} = {w2}",
        "{w1} + {w2} = {w3}",
        "{w1} + {w3} = {w4}",
        "{w2} + {w2} = {w4}",
        "{w1} + {w4} == {w2} + {w3}",
      ]
      
      # allocate the words
      for (w1, w2, w3, w4) in subsets(["AA", "BA", "EB", "DE"], size=4, select='P'):
        # are all the expressions solvable?
        if all(solve(sprintf(x)) for x in exprs):
          # output solution
          printf("1 = {w1}; 2 = {w2}; 3 = {w3}; 4 = {w4}")
      

      Solution: 1 = DE; 2 = BA; 3 = AA; 4 = EB.

      So we have:

      DE + DE = BA → 13 + 13 = 26, …
      DE + BA = AA → 10 + 23 = 33, …
      DE + AA = EB → 13 + 22 = 35, …
      BA + BA = EB → 21 + 21 = 42, …
      DE + EB = BA + AA → 14 + 42 = 23 + 33, …

      Like

    • Frits's avatar

      Frits 5:10 pm on 23 April 2023 Permalink | Reply

       
      from enigma import subsets
      
      '''
      0: ab + ab = ef       reject if: b in {a, f}, e = f,
      1: ab + cd = ef       reject if: b = f and d in {a, e}, d = f and b in {c, e}
                                       e = f and (b = c or a = d)
      2: ab + cd = ef + gh  reject if not: b = d or f = h 
      '''
      
      # reject words depending on the type of equation
      def rej(t, w, x, y, z=0):
        (a, b) = (w[0], w[1])
        (c, d) = (x[0], x[1])
        (e, f) = (y[0], y[1])
        if t == 0 and (e == f or b in {a, f}): return True
        if t == 1 and any([b == f and d in {a, e}, d == f and b in {c, e},
                           e == f and (b == c or a == d)]): return True
        if t == 2 and not (b == d or f == z[1]): return True                   
        
        return False
      
      # allocate the words
      for (w1, w2, w3, w4) in subsets(["AA", "BA", "EB", "DE"], size=4, select='P'):
        if rej(0, w1, w1, w2): continue
        if rej(1, w1, w2, w3): continue
        if rej(1, w1, w3, w4): continue
        if rej(0, w2, w2, w4): continue
        if rej(2, w1, w4, w2, w3): continue
        print(*zip(range(1, 5), (w1, w2, w3, w4)))
      

      Like

      • Frits's avatar

        Frits 5:23 pm on 23 April 2023 Permalink | Reply

        This is more of a program saying: “if there is a solution it must be one of these: ….”

        Like

  • Unknown's avatar

    Jim Randell 4:14 pm on 21 April 2023 Permalink | Reply
    Tags:   

    Teaser 3161: Going through the hoops 

    From The Sunday Times, 23rd April 2023 [link] [link]

    I arrived very late to watch the croquet game. Someone had listed the times when the four balls (blue, black, red and yellow) had run through hoops 1 to 12; none had yet hit the central peg to finish. Blue had run each hoop earlier than black, and every hoop had been run by colours in a different order. The only change in running order from one hoop to the next-numbered hoop was that two colours swapped positions. These swapping pairs (to obtain the order for the next-numbered hoop) were in non-adjacent positions for hoops 5 and 10, but no others. For one particular colour, all the hoops where it passed through earlier than the yellow were before all those where it passed through later than the yellow.

    In what order had the balls run the twelfth hoop?

    [teaser3161]

     
    • Jim Randell's avatar

      Jim Randell 4:44 pm on 21 April 2023 Permalink | Reply

      This Python program runs in 60ms. (Internal runtime is 4.9ms).

      Run: [ @replit ]

      from enigma import (subsets, update, join, filter2, irange, printf)
      
      # the colours (K = black)
      colours = "BKRY"
      
      # x before y
      before = lambda s, x, y: s.index(x) < s.index(y)
      
      # is there a colour where all "before yellow" < all "after yellow"
      def check(ss, n):
        for k in colours:
          if k == "Y": continue
          (bY, aY) = filter2((lambda i: before(ss[i], k, 'Y')), irange(n))
          if bY[-1] < aY[0]: return True
        return False
      
      # solve the puzzle
      def solve(ss):
        n = len(ss)
        # are we done?
        if n == 12:
          if check(ss, n):
            yield ss
        else:
          # choose 2 indices to swap
          s = ss[-1]
          for ((i, x), (j, y)) in subsets(enumerate(s), size=2):
            if (j == i + 1) == (n in {5, 10}): continue
            s_ = update(s, [(i, y), (j, x)])
            # blue before black, and no repeats
            if before(s_, 'B', 'K') and s_ not in ss:
              # move on to the next hoop
              yield from solve(ss + [s_])
      
      # choose an order for the first hoop, with blue before black
      for s in subsets(colours, size=len, select='P', fn=join):
        if not before(s, 'B', 'K'): continue
        # solve for the remaining hoops
        for ss in solve([s]):
          # output solution
          printf("hoop 12 = {s12} [{ss}]", s12=ss[-1], ss=join(ss, sep=" "))
      

      Or [here] is a (longer) version that verifies the “yellow” condition as it goes along (instead of just checking it at the end).

      Solution: The order for the 12th hoop was: Red, Yellow, Blue, Black.

      The full list of orders is (B = Blue, K = Black, R = Red, Y = Yellow):

       1: B K Y R (K before Y)
       2: B K R Y (3/4 swapped; K before Y)
       3: B R K Y (2/3 swapped; K before Y)
       4: R B K Y (1/2 swapped; K before Y)
       5: R B Y K (3/4 swapped; Y before K)
       6: Y B R K (1/3 swapped; Y before K)
       7: Y B K R (3/4 swapped; Y before K)
       8: B Y K R (1/2 swapped; Y before K)
       9: B Y R K (3/4 swapped; Y before K)
      10: B R Y K (2/3 swapped; Y before K)
      11: Y R B K (1/3 swapped; Y before K)
      12: R Y B K (1/2 swapped; Y before K)

      And we see the non-adjacent swaps are between hoops 5 & 6 and 10 & 11.

      Like

    • Frits's avatar

      Frits 11:44 pm on 21 April 2023 Permalink | Reply

      Without recursion.

         
      from itertools import permutations, product
      from collections import defaultdict
      
      # the colours (K = black)
      colours = "BKRY"
      
      # possible running orders (blue before black)
      ps = list(p for p in permutations(range(4)) if p.index(0) < p.index(1))
      
      adj_moves = defaultdict(list)
      nonadj_moves = defaultdict(list)
      
      # determine all possible next running orders for a specific running order
      for (x, y) in product(ps, repeat=2):
        if x == y: continue
        swaps = [i for i, (a, b) in enumerate(zip(x, y)) if a != b]
        # only one swap took place ...
        if len(swaps) != 2: continue
        # .. and store for adjacent fields and non-adjacent fields
        (adj_moves if abs(swaps[0] - swaps[1]) == 1 else nonadj_moves)[x].append(y)
       
      
      # first hoop
      ss = [(x, ) for x in ps]
      # process remaining hoops after first hoop
      for n in range(1, 12):
        ss_ = []
        for s in ss:
          # determine next running order
          for ro in (adj_moves if n not in {5, 10} else nonadj_moves)[s[-1]]:
            if ro not in s:
              ss_.append(s + (ro, ))
        ss = ss_
       
      # is there a colour where all "before yellow" < all "after yellow"
      for s in ss:  
        for k in range(3):
          # make string with only digits 3 and k 
          # (like k3-k3-3k-3k-3k-3k-k3-k3-3k-3k-3k-3k)
          s_3k = '-'.join([''.join(str(x) for x in y if x in {k, 3}) for y in s])
          f_3k = s_3k.index('3' + str(k))   # first 3k string
          
          if f_3k == 0: continue
          # all k3 strings must occur before first 3k string
          if s_3k[f_3k + 3:].find(str(k) + '3') >= 0: continue
          
          print("answer:", ''.join(colours[x] for x in s[-1]))
          break
      

      Like

    • Frits's avatar

      Frits 4:21 pm on 25 April 2023 Permalink | Reply

      John Crabtree said that it was not hard to determine the “particular colour”.
      The following code will reject two out of three colours.

       
      from itertools import permutations
      
      # the colours (K = black)
      colours = "BKRY"
      
      # possible running orders (blue before black)
      ps = list(p for p in permutations(range(4)) if p.index(0) < p.index(1))
      
      # return possible outcomes after a non-adjacent swap
      def n_adj(s):
        return set(((s[2], s[1], s[0], s[3]), 
                    (s[3], s[1], s[2], s[0]), 
                    (s[0], s[3], s[2], s[1])))
      
      particular_colours = []
      for k in range(3):
        a, b = [], []       # after/before
        for p in ps:
          (b if p.index(k) < p.index(3) else a).append(p)
        
        # the "after" group has at least 3 elements
        # with at least one non-adjacent move 
        if all(n_adj(e).isdisjoint(a) for e in a): continue
        particular_colours.append(colours[k])
      
      print("possible particular colours:", particular_colours) 
      

      Like

  • Unknown's avatar

    Jim Randell 10:07 am on 20 April 2023 Permalink | Reply
    Tags:   

    Teaser 2696: Digital display 

    From The Sunday Times, 25th May 2014 [link] [link]

    George and Martha have a seven-figure telephone number consisting of seven different digits in decreasing order. Martha commented that the seven digits added together gave a total number that did not use any of the digits from the telephone number.

    George agreed and added that their telephone number was in fact a multiple of that total number.

    What is their telephone number?

    This completes the archive of Teaser puzzles originally published in The Sunday Times in 2014.

    There are now 856 Teaser puzzles available on the site, including a complete archive of puzzles from December 2013 to the most recent puzzle (April 2023).

    [teaser2696]

     
    • Jim Randell's avatar

      Jim Randell 10:07 am on 20 April 2023 Permalink | Reply

      This Python program runs in 52ms. (Internal runtime is 320µs).

      Run: [ @replit ]

      from enigma import (irange, subsets, nsplit, nconcat, printf)
      
      # choose 7 digits in decreasing order
      for ds in subsets(irange(9, 0, step=-1), size=7):
        # the sum of the digits does not use any of the digits
        dsum = sum(ds)
        if any(d in ds for d in nsplit(dsum)): continue
      
        # compute the telephone number
        n = nconcat(ds)
        if n % dsum != 0: continue
      
        # output solution(s)
        printf("number = {n} [dsum = {dsum}]")
      

      Solution: Their telephone number is: 8654310.

      The digit sum is 27 and:

      8654310 = 320530 × 27

      Like

      • Jim Randell's avatar

        Jim Randell 1:39 pm on 10 May 2023 Permalink | Reply

        Here is a version that uses the [[ SubstitutedExpression ]] solver from the enigma.py library, using the newly implemented macro functionality to tidy things up a bit.

        Run: [ @replit ]

        #! python3 -m enigma -rr
        
        SubstitutedExpression
        
        # suppose the 3 unused digits are X, Y, Z
        "X < Y" "Y < Z"
        
        # digit sum of the remaining digits is: 45 - (X + Y + Z)
        --macro="@ds = (45 - X - Y - Z)"
        # and must be composed entirely of digits in {X, Y, Z}
        --macro="@xs = {X, Y, Z}"
        "@xs.issuperset(nsplit(@ds))"
        
        # phone number is a multiple of ds
        --code="number = lambda xs: nconcat(diff(irange(9, 0, step=-1), xs))"
        "number(@xs) % @ds == 0"
        
        --answer="number(@xs)"
        --template=""
        

        Like

    • Frits's avatar

      Frits 11:11 am on 20 April 2023 Permalink | Reply

         
      from enigma import SubstitutedExpression
      
      vars = "ABCDEFG"
      d2i = {d: (vars[:6 - d] if d < 6 else "") + (vars[3 - d:] if d > 3 else "")
                for d in range(0, 10)} 
      
      # the alphametic puzzle
      p = SubstitutedExpression(
        [
          "A > B",
          "B > C",
          "C > D",
          "D > E",
          "E > F",
          "F > G",
          "A + B + C + D + E + F + G = HI",
          "div(ABCDEFG, HI)",
          
        ],
        answer="ABCDEFG",
        d2i=d2i,
        reorder=0,
        verbose=0,    # use 256 to see the generated code
      )
      
      # print answers
      for (_, ans) in p.solve():
        print(f"{ans}")
      

      Like

      • Jim Randell's avatar

        Jim Randell 2:54 pm on 20 April 2023 Permalink | Reply

        @Frits: I think I would allow H and I to have the same value.

        You can allow this with the following parameter to [[ SubstitutedExpression ]]:

        distinct=["ABCDEFGH", "ABCDEFGI"]
        

        Like

        • Frits's avatar

          Frits 5:12 pm on 20 April 2023 Permalink | Reply

          @Jim, I erronuously thought that the total number also had to have different digits

          Like

        • Frits's avatar

          Frits 5:15 pm on 20 April 2023 Permalink | Reply

             
          dgts = "0123456789"
          
          # decompose <t> into <k> numbers from <ns>
          # return number with decreasing digits
          def decompose(t, k, ns, s=[]):
            if k == 1:
              if t in ns:
                yield int("".join(str(x) for x in (s + [t])[::-1]))
            else:
              for (i, n) in enumerate(ns):
                if t - n < 0: continue
                yield from decompose(t - n, k - 1, ns[i + 1:], s + [n])
          
          # try each possible total number
          for tot_nr in range(sum(range(7)), sum(range(3, 10)) + 1):
            # remaining digits
            rd = [int(x) for x in dgts if x not in str(tot_nr)]
           
            # total number must lie between sum 7 smallest and sum 7 highest digits 
            if not (sum(x for x in rd[:7]) <= tot_nr <= sum(x for x in rd[-7:])):
              continue
            
            pr(tot_nr, rd, sum(x for x in rd[-7:]))
            
            # pick 7 digits with sum to <tot_nr>
            for y in decompose(tot_nr, 7, rd):
              if y % tot_nr == 0:
                print("answer:", y)
          

          Like

    • GeoffR's avatar

      GeoffR 9:14 pm on 20 April 2023 Permalink | Reply

      % A Solution in MiniZinc
      include "globals.mzn";
      
      var 1..9:A; var 0..9:B; var 0..9:C; var 0..9:D;
      var 0..9:E; var 0..9:F; var 0..9:G;
      var 1..9:H; var 0..9:I;
      
      % 7 digits of the telephone number are in descending order
      constraint A > B /\ B > C /\ C > D /\ D > E /\ E > F /\ F > G;
      
      % Telephone number is ABCDEFG
      var 6543210..9876543: ABCDEFG == A * pow(10,6) + B * pow(10,5) + C * pow(10,4)
      + D * pow(10,3) + E *  pow(10,2) + F * pow(10,1) + G;
      
      % Sum of digits in Telephone Number
      var 21..42: digsum = A + B + C + D + E + F + G;
      
      constraint H == digsum div 10;  
      constraint I == digsum mod 10;
      
      % Divisor of telephone number uses digits H and I
      constraint ABCDEFG div digsum > 0 /\  ABCDEFG mod digsum == 0;
      
      % Digits H and I could be the same or different
      constraint card({A, B, C, D, E, F, G, H}) == 8 
      /\ card({A, B, C, D, E, F, G, I}) == 8;  
      
      solve satisfy;
      
      output["Tel. Num. = " ++ show(ABCDEFG) ++ ", Digit sum = " ++ show(digsum)];
      
      % Tel. Num. = 8654310, Digit sum = 27
      % ----------
      % ==========
      
      
      
      
      

      Like

  • Unknown's avatar

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

    Teaser 2675: Should old acquaintance … 

    From The Sunday Times, 29th December 2013 [link] [link]

    I have given each letter of the alphabet a different whole number value from 1 to 26 (for instance, A has the value 5 and B has the value 24). In this way I can work out the value of any word by adding up the values of its letters. Looking at the values of the words:

    SHOULD
    OLD
    ACQUAINTANCE

    I find that the value of OLD is the average of the other two values.

    I also find that NEW and YEAR have the same value as each other – and it is like the value of OLD but with its two digits in the reverse order. It is possible from all this information to work out the values of N, E and W, but all we want to know is…

    … what is the value of NEW?

    I replaced “for example” in the original puzzle text by “for instance” to make it clear these are the actual values that are being given. In the book The Sunday Times Brain Teasers 2 (2020), the values are simply given as “(eg, A=5 and B=24)”, which I think is more ambiguous than the original text.

    [teaser2675]

     
    • Jim Randell's avatar

      Jim Randell 9:16 am on 18 April 2023 Permalink | Reply

      From:

      (SHOULD + ACQUAINTANCE) / 2 = OLD

      we determine:

      SHU + ACQUAINTANCE = OLD
      ⇒ 3A + 2CNU + CEHIQST = OLD

      And A = 5, so the smallest possible value for OLD is: 3×5 + 2×(1 + 2 + 3) + (4 + 6 + 7 + 8 + 9 + 10) = 71.

      And this is enough to give us a way to attack the problem programatically.

      This Python program runs in 81ms. (Internal runtime is 23.5ms).

      Run: [ @replit ]

      from enigma import (irange, subsets, group, diff, nreverse, cproduct, intersect, singleton, printf)
      
      # given values
      A = 5
      B = 24
      
      # collect sums of 3 digit values for OLD, NEW, YER
      g = group(subsets(diff(irange(1, 26), {A, B}), size=3), by=sum)
      
      # find 2-digit keys with whose reverse is also a key
      for OLD in g.keys():
        if OLD < 71: continue
        NEW = nreverse(OLD)
        if NEW < 10 or NEW == OLD or NEW not in g: continue
        YER = NEW - A
        if YER not in g: continue
      
        # NEW and YER must have E in common (but nothing else)
        for (vNEW, vYER) in cproduct([g[NEW], g[YER]]):
          E = singleton(intersect([vNEW, vYER]))
          if E is None: continue
          # OLD has no letters in common with them
          vs = vNEW + vYER
          for vOLD in g[OLD]:
            if intersect([vOLD, vs]): continue
      
            # find a value for N (which also gives W)
            for N in diff(vNEW, {E}):
              # calculate remaining amount for 2CU + HIQST
              r = OLD - (3*A + E + 2*N)
              ns = diff(irange(1, 26), {A, B}, vs, vOLD)
              # can we make this from the remaining values?
              if r < sum(ns[0:2]) + sum(ns[0:7]): continue
      
              # output solution
              printf("OLD={OLD} NEW={NEW} YER={YER} -> E={E} NEW={vNEW} YER={vYER} OLD={vOLD} -> N={N} {ns}", ns=ns[0:7])
      

      Solution: NEW has a value of 37.

      And we can determine: N = 3, E = 11, W = 23

      OLD has a value of 73, so the individual letters have values 22, 25, 26 (in some order).

      And Y and R are 9 and 12 (in some order).

      To complete the given letters, U and C can be 1 and 2 (in some order).

      And H, I, Q, S, T can be 4, 6, 7, 8, 10 (in some order).

      Like

    • GeoffR's avatar

      GeoffR 1:56 pm on 18 April 2023 Permalink | Reply

      The Chuffed Solver took about 1 sec to run, but the Geocode Solver took about 40 sec.

      % A Solution in MiniZinc
      include "globals.mzn";
      
      var 1..26:A; var 1..26:C; var 1..26:E; var 1..26:D; var 1..26:H; 
      var 1..26:I; var 1..26:L; var 1..26:N; var 1..26:O; var 1..26:Q;
      var 1..26:S; var 1..26:T; var 1..26:U; var 1..26:Y; var 1..26:R;
      var 1..26:W; var 1..26:B;
      
      constraint A == 5 /\ B == 24; % given in teaser
      
      constraint all_different ([A,C,E,D,H,I,L,N,O,Q,S,T,U,Y,R,W,B]);
      
      % OLD is the average of SHOULD and ACQUAINTANCE
      constraint 2 * (O + L + D) == (S + H + O + U + L + D) 
      + (A + C + Q + U + A + I + N + T + A + N + C + E);
      
      % NEW and YEAR have the same value
      constraint (N + E + W) == (Y + E + A + R);
      
      % NEW is similar OLD, but with its two digits in the reverse order
      var 10..99:NEW == N + E + W;
      var 10..99:OLD == O + L + D;
      var 1..9:a; var 1..9:b;
      var 1..9:c; var 1..9:d;
      
      constraint a == NEW mod 10 /\ b == NEW div 10;
      constraint c == OLD mod 10 /\ d == OLD div 10;
      constraint a == d /\ b == c;
      
      solve satisfy;
      
      output ["[N, E, W] = " ++ show([N, E, W]) 
      ++ "\n" ++ "NEW = " ++ show(NEW)
      ++ "\n" ++ "OLD = " ++ show(OLD)
      ];
      
      % [N, E, W] = [3, 11, 23]
      % NEW = 37
      % OLD = 73
      
      
      

      Like

    • Frits's avatar

      Frits 1:05 pm on 19 April 2023 Permalink | Reply

      Not as fast as my version of Brian’s program.

      [https://brg.me.uk/?page_id=1756#comment-3740]

       
      from enigma import SubstitutedExpression
      
      # invalid digit / symbol assignments
      d2i = dict()
      for d in range(0, 27):
        vs = set()
        if d == 0: vs.update('ABCDEHILNOQRSTUWY')
        # 71 <= OLD <= 74
        # 3A + 2CNU + EHIQST = OLD
        # 3×5 + 2×(1 + 2 + 3) + (4 + 6 + 7 + 8 + 9 + x) <= 74 
        # so x <= 13
        if d > 13: vs.update('NECUHIQST')
        
        if d < 20: vs.update('OLD')
        if d < 21: vs.update('LD')
        if d < 22: vs.update('D')
        if d > 24: vs.update('O')
        if d > 25: vs.update('OL')
      
        d2i[d] = vs
      
      # the alphametic puzzle
      p = SubstitutedExpression(
        [
          # order not important
          "O < L < D",      # order not important
          # NEW is like the value of OLD with digits in the reverse order
          "(10 * ((old := O + L + D) % 10) + old // 10) - N - E = W",
          
          "C < U",          # order not important
          
          # 3×5 + 2×(C + N + U) + (1 + 2 + 3 + 4 + 6 + 7) <= 74 
          # thus the maximum of C + N + U is 18 
          "(cnu := C + N + U) <= 18",
          
          # 3A + 2CNU + EHIQST = OLD, determine value of HIQST 
          # check if remaining numbers are too high for making target HIQST
          "(hiqst := old - 3 * A - 2 * cnu - E) >= 16",
          
          "hiqst >= sum(sorted(set(range(1, 14)) - {A, C, E, N, U})[:5])",
          
          "H < I < Q < T",  # order not important
          
          # OLD is the average of the values SHOULD and ACQUAINTANCE 
          "old - (H+U + A+C+Q+U+A+I+N+T+A+N+C+E) = S",
          "Q < S < T",     # order not important
          
          #  NEW and YEAR have the same value
          "N + W - Y - A = R",
          "R < Y",         # order not important
          
        ],
        answer="N + E + W",
        base=27,
        d2i=d2i,
        s2d=dict(A=5, B=24),
        reorder=0,
        verbose=0,    # use 256 to see the generated code
      )
      
      # print answers
      for (_, ans) in p.solve():
        print(f"{ans}")  
      

      Like

  • Unknown's avatar

    Jim Randell 9:09 am on 16 April 2023 Permalink | Reply
    Tags: by: R J Webster   

    Brain teaser 977: Little boy blue 

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

    The baby department of our local store has just organised a raffle. The tickets were numbered consecutively from one upwards and were made into large books, each containing the same number of consecutive tickets.

    Numbers having at least one repeated digit were printed on blue paper and those having all their digits different were printed on pink paper. It turned out that there was an equal number of blue and pink tickets. The prize was a generously equipped layette in the colour of the winning ticket.

    Being the first person to buy a ticket, I had the opportunity to see that among the large collection of books only one of them consisted of tickets all of the same colour, and it was the colour I wanted too.

    Spoilt for choice, I bought the ticket in the middle of this book. My choice was fully justified: I won the prize!

    What was my winning number?

    The version of the puzzle published in the book Microteasers (1986), also includes the additional question:

    How many tickets were there?

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

    [teaser977]

     
    • Jim Randell's avatar

      Jim Randell 9:09 am on 16 April 2023 Permalink | Reply

      This Python program runs in 72ms. (Internal runtime is 13.0ms).

      Run: [ @replit ]

      from enigma import (
        irange, inf, is_duplicate, divisors_pairs,
        seq_all_same, singleton, printf
      )
      
      blue = pink = 0
      ts = [None] # there is no ticket 0
      
      # look for books with tickets the same colour
      def same_colour(ts, n, k):
        for i in irange(1, n, step=k):
          if seq_all_same(ts[i] for i in irange(i, i + k - 1)):
            yield i
      
      # consider number of tickets
      for n in irange(1, inf):
        f = is_duplicate(n)
        ts.append(f)
        if f:
          blue += 1
        else:
          pink += 1
      
        if blue == pink:
          printf("total={n} [blue={blue} pink={pink}]")
      
          # consider j books, each with k tickets
          done = 0
          for (j, k) in divisors_pairs(n, every=1):
            # k is odd (and  > 1)
            if not (k > 1 and k % 2 == 1): continue
            # find books with tickets all the same colour
            b = singleton(same_colour(ts, n, k))
            if b is None: continue
            printf("-> {j} books, each with {k} tickets -> {b} .. {x}", x=b + k - 1)
            if k % 2 == 1:
              # find the middle ticket
              t = b + (k // 2)
              printf("-> middle ticket = {t}")
              done += 1
      
          if done: break
      

      Solution: The winning number was: 10073.

      There were 44 books, each with 255 tickets, making 11220 tickets in total. 5610 of them are blue, and 5610 of them are pink.

      The book of tickets from 9946 – 10200 consists of 255 tickets, all of which have repeated digits, so are coloured blue.

      The ticket in the middle of this book is 10073 (there are 127 tickets before it and 127 tickets after it).

      Like

    • Hugo's avatar

      Hugo 10:48 am on 16 April 2023 Permalink | Reply

      Is it possible that there were 187 tickets per book? In that case there would be a book 9912 – 10098,
      with middle number 10005. If it had been fewer than 187, there would be two all-blue books.

      I think the longest possible run with repeated digits is 357, from 9877 to 10233.

      Like

      • Frits's avatar

        Frits 11:28 am on 16 April 2023 Permalink | Reply

        @Hugo, as 187 is less than 220 (there are 11220 tickets in total) the last book must be all-blue as well (all tickets of the form 11xxx).

        Like

  • Unknown's avatar

    Jim Randell 4:34 pm on 14 April 2023 Permalink | Reply
    Tags: ,   

    Teaser 3160: Hymn number anagrams 

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

    At Church on Sunday, the hymn board showed just four different hymn numbers, each having the same three, different, non-zero digits, but in a different order. I noticed that the first hymn number was a perfect square, and that there was at least one other perfect square, formed from the same three digits, which did not appear on the board. The sum of the four hymn numbers was a prime number. If I told you the first digit of that prime number, you would be able to tell me the first hymn number on the board.

    What was the first hymn number on the board?

    [teaser3160]

     
    • Jim Randell's avatar

      Jim Randell 5:02 pm on 14 April 2023 Permalink | Reply

      As worded there are multiple answers to this puzzle.

      However we can get a unique answer if we are told the last digit of the prime sum (not the first digit).

      This Python program runs in 64ms. (Internal runtime is 503µs).

      Run: [ @replit ]

      from enigma import (
        irange, sq, nsplit, ordered, group, item, subsets,
        nconcat, is_prime, filter_unique, unpack, printf
      )
      
      # generate possible 3-digit squares, and their digits (ordered)
      def squares():
        # consider 3-digit squares
        for i in irange(10, 31):
          n = sq(i)
          # there should be 3 different non-zero digits
          ds = nsplit(n, fn=set)
          if len(ds) != 3 or 0 in ds: continue
          # return (<square>, <digits>)
          yield (n, ordered(*ds))
      
      # generate candidate solutions
      def generate():
        # group possible squares by their digits
        g = group(squares(), by=item(1), f=item(0), fn=set)
      
        # choose a set of digits with (at least) 2 squares
        for (ds, sqs) in g.items():
          if len(sqs) < 2: continue
          # construct all rearrangements of the digits
          rs = list(nconcat(ss) for ss in subsets(ds, size=len, select='P'))
          # choose 4 of the candidates
          for ns in subsets(rs, size=4):
            # at least one square must be unused
            if sqs.issubset(ns): continue
            # at least one square must be present
            fs = sqs.intersection(ns)
            if not fs: continue
            # the sum should be a prime
            t = sum(ns)
            if not is_prime(t): continue
            # yield solutions (<first>, <sum>, <all>)
            for n in fs:
              printf("[first={n} hymns={ns} sum={t}; unused={xs}]", xs=sorted(sqs.difference(ns)))
              yield (n, t, ns)
      
      # if we knew the first digit of the total, we could deduce the number of the first hymn
      f = unpack(lambda n, t, ns: nsplit(t)[0])  # first digit of total
      g = unpack(lambda n, t, ns: n)  # first hymn number
      ss = filter_unique(generate(), f, g).unique
      
      # output solution
      for (n, t, ns) in ss:
        printf("prime = {t} -> hymns = {ns}, first = {n}")
      

      Solution: There are two possibilities: 256 and 961.


      There are only two sets of 3 distinct digits that can form multiple squares:

      {1, 6, 9} → 169 (=13²), 196 (= 14²), 961 (= 31²)
      {2, 5, 6} → 256 (=16²), 625 (= 25²)

      And these lead to the 5 possible arrangements of hymns that fit the requirements:

      first = 256; rest = 265, 526, 562; sum = 1609; unused square = 625
      first = 256; rest = 265, 526, 652; sum = 1699; unused square = 625
      first = 961; rest = 196, 619, 691; sum = 2467; unused square = 169
      first = 196; rest = 619, 691, 961; sum = 2467; unused square = 169
      first = 961; rest = 619, 691, 916; sum = 3187; unused square = 169, 196

      If we are told the first digit of the sum is 1, then we see the first hymn must be 256. (We can also determine that two of the three other hymns are 265 and 526, but without more information we can’t determine if the remaining hymn is 562 or 652).

      And if we are told the first digit of the sum is 3, then we see the first hymn must be 961. (In this case we can determine that the other three hymn are 619, 691 and 916).

      These are the two possible answers.


      It is possible we are meant to read “if I told you the first digit [of the sum], you would be able to tell me the first hymn number on the board” to mean that although we can deduce the first hymn number on the board, we cannot deduce with certainty all of the other three numbers. And this removes one of the viable answers, leaving a unique solution to the puzzle.

      But I think if this was the intended interpretation, the puzzle text should have been worded more clearly.

      Like

    • Frits's avatar

      Frits 7:19 pm on 14 April 2023 Permalink | Reply

      from enigma import SubstitutedExpression, is_square_p, is_prime
      from itertools import combinations as comb
      
      # return number of perfect squares in a sequence of numbers
      def n_squares(seq):
        return sum(is_square_p(x) for x in seq)
      
      # the alphametic puzzle
      p = SubstitutedExpression(
        [
          # first item ABC must be a perfect square
          "n_squares([ABC])",
          # there was at least one other perfect square
          "n_squares(r5 := [ACB, BAC, BCA, CAB, CBA])",
          
          # the sum of the four hymn numbers is a prime number
          # (choose c for the 2 numbers not displayed on the board)
          "len(primes := [p for c in comb(r5, 2) if n_squares(c) \
               if is_prime(p := (ABC + sum(x for x in r5 if x not in c)))]) > 0",
        ],
        answer="ABC, primes",
        env=dict(n_squares=n_squares, comb=comb),
        digits=range(1, 10),
        reorder=0,    # advisable when using assignment statements
        verbose=0,    # use 256 to see the generated code
      )
      
      d = dict()
      # process answers
      for (_, ans) in p.solve():
        # store first hymn number for first digit of prime number
        for p in ans[1]:
          d[str(p)[0]] = d.get(str(p)[0], set()) | {ans[0]}
      
      print("answers:", [vs.pop() for k, vs in d.items() if len(vs) == 1])   
      

      Like

      • Jim Randell's avatar

        Jim Randell 10:22 pm on 14 April 2023 Permalink | Reply

        @Frits: That’s a clever use of assignment expressions. I hadn’t thought about how they could be useful in [[ SubstitutedExpression ]] recipes. Although, as you say, you need [[ reorder=0 ]].

        Like

    • GeoffR's avatar

      GeoffR 5:04 pm on 16 April 2023 Permalink | Reply

      I also found two solutions with different digits of the first hymn number.
      The method of solution is described at the start of the program.

      # Method
      # I noticed that the first hymn number was a perfect square,
      # and that there was at least one other perfect square,
      # formed from the same three digits, which did not appear on the board.
      # ... so 1 or 2 squares not on the hymn board and 1 square on the board.
      
      from math import isqrt
      from collections import defaultdict
      from itertools import product, combinations
      from enigma import is_prime, all_different
      
      HYMNS = defaultdict(list)
      # Dictionary key is sum of first four numbers (a prime)
      # Dictionary value is a tuple of the six 3-digit numbers
      
      def is_sq(n):
        return (isqrt(n) ** 2 == n)
          
      def sq_count (n1, n2, n3, n4, n5, n6):
        try:
          cnt = 0
          for c in [n1, n2, n3, n4, n5, n6]:
            if is_sq(c):
              cnt += 1
            # 2 or 3 squares in 6 numbers
            if cnt == 2 or cnt == 3:
              return cnt
        except:
          print("Error in counting squares")
          return
      
      # Assumed 2 or 3 squares in the 6 no. 3-digit numbers 
      for sq_cnt in range(2, 4):
        for c1, c2, c3 in product(range(1, 10), repeat=3):
          if all_different(c1, c2, c3):
            # find 6 no. 3-digit numbers
            ABC, DEF = 100*c1 + 10*c2 + c3, 100*c1 + 10*c3 + c2
            GHI, JKL = 100*c2 + 10*c1 + c3, 100*c2 + 10*c3 + c1
            MNO, PQR = 100*c3 + 10*c1 + c2, 100*c3 + 10*c2 + c1
            # count number of squares in these 6 numbers
            sq_cnt = sq_count(ABC,DEF,GHI,JKL,MNO,PQR)
            
            # The number of squares in the 6 numbers is either 2 or 3
            # check for 2 squares in 6 numbers        
            if sq_cnt == 2:
              # the first 4 hymn nos. contain 1 square only (i.e. 1st)
              # .. and the last 2  numbers 1 square only
              for p1 in combinations((ABC, DEF,GHI,JKL,MNO,PQR), 6):
                if is_sq(ABC):
                  # 4 hymn numbers are ABC, DEF, GHI, JKL
                  if all(not is_sq(x) for x in (DEF,GHI,JKL)):
                    if is_prime(ABC + DEF + GHI + JKL):
                        HYMNS[ABC+DEF+GHI+JKL] += [(ABC,DEF,GHI,JKL,MNO,PQR)]
                      
            # check for 3 squares in 6 numbers        
            if sq_cnt == 3:
              for p1 in combinations((ABC,DEF,GHI,JKL,MNO,PQR), 6):
                # squares are 1st, 5th and 6th 3-digit numbers
                #... as first 4 numbers assumed 1 square only(i.e 1st)
                if is_sq(ABC) and is_sq(MNO) and is_sq(PQR):
                  if is_prime(ABC + DEF + GHI + JKL):
                    HYMNS[ABC+DEF+GHI+JKL] += [(ABC,DEF,GHI,JKL,MNO,PQR)]
      
      # Find the four numbers on the hymn board                   
      for k, v in HYMNS.items():
          print(f"The first hymn number on the board was {v[0][0]}.")
          print(f"HYMN NUMBERS: {v[0][0]}, {v[0][1]}, {v[0][2]}, {v[0][3]}.")
          print()
      

      Like

    • GeoffR's avatar

      GeoffR 7:17 pm on 17 April 2023 Permalink | Reply

      Much shorter code in MiniZinc ,with same two possible answers.

      % A Solution in MiniZinc
      include "globals.mzn";
      
      var 1..9:a; var 1..9:b; var 1..9:c; 
      constraint all_different([a, b, c]);
       
      predicate is_sq(var int: y) =
      exists(z in 1..ceil(sqrt(int2float(ub(y))))) (z*z = y );
      
      predicate is_prime(var int: x) = 
      x > 1 /\ forall(i in 2..1 + ceil(sqrt(int2float(ub(x))))) 
      ((i < x) -> (x mod i > 0));
        
      % ABC, DEF, GHI and JKL are Hymn Board numbers - ABC is 1st number
      var 100..999:ABC = 100*a + 10*b + c;
      var 100..999:DEF = 100*a + 10*c + b;
      var 100..999:GHI = 100*b + 10*a + c;
      var 100..999:JKL = 100*b + 10*c + a;
      % MNO and PQR are possible squares off the Hymn Board
      var 100..999:MNO = 100*c + 10*a + b;
      var 100..999:PQR = 100*c + 10*b + a;
      
      constraint is_sq(ABC); % 1st number on Hymn Board is a square
      % One or both numbers off the Hymn Board are squares
      constraint (is_sq(MNO) \/ is_sq(PQR)) \/ (is_sq(MNO) /\ is_sq(PQR));
      constraint not is_sq(DEF) /\ not is_sq(GHI) /\ not is_sq(JKL);
      
      % The sum of the numbers on the Hymn Board is a prime
      constraint is_prime(ABC + DEF + GHI + JKL);
      
      solve satisfy;
      % Four Hymn numbers are ABC, DEF,GHI and JKL
      output [  "[ABC, DEF, GHI, JKL, MNO, PQR] =" 
      ++ show( [ABC, DEF, GHI, JKL, MNO, PQR]  )];
      
      
      

      Like

  • Unknown's avatar

    Jim Randell 9:14 am on 13 April 2023 Permalink | Reply
    Tags:   

    Teaser 2697: Gimme five! 

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

    In the list of words below each different letter consistently represents a different digit. Each of the coded numbers has the property that the sum of its digits is divisible by (but not equal to) the number it spells out:

    THREE
    FIVE
    EIGHT
    NINE
    TEN
    ELEVEN
    THIRTEEN

    What number does FIVE represent?

    [teaser2697]

     
    • Jim Randell's avatar

      Jim Randell 9:15 am on 13 April 2023 Permalink | Reply

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

      The following run file executes in 70ms. (Internal runtime of the generated program is 3.0ms).

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      "div(T + H + R + E + E, 3) > 1"
      "div(F + I + V + E, 5) > 1"
      "div(E + I + G + H + T, 8) > 1"
      "div(N + I + N + E, 9) > 1"
      "div(T + E + N, 10) > 1"
      "div(E + L + E + V + E + N, 11) > 1"
      "div(T + H + I + R + T + E + E + N, 13) > 1"
      
      --template="THREE FIVE EIGHT NINE TEN ELEVEN THIRTEEN"
      --answer="FIVE"
      

      Solution: FIVE = 8237.

      We have:

      THREE = 45177
      FIVE = 8237
      EIGHT = 72654
      NINE = 9297
      TEN = 479
      ELEVEN = 707379
      THIRTEEN = 45214779

      Like

    • GeoffR's avatar

      GeoffR 10:24 am on 14 April 2023 Permalink | Reply

      % A Solution in MiniZinc
      include "globals.mzn";
      
      % Leading digits of numbers
      var 1..9:T; var 1..9:F; var 1..9:E; var 1..9:N;
      % Other digits in numbers
      var 0..9:I; var 0..9:H; var 0..9:R;
      var 0..9:G; var 0..9:V; var 0..9:L;
       
      constraint all_different([T, F, E, N, I, H, R, G, V, L]);
      
      % For each number, sum of its digits is divisible by (but not equal to) the number it spells out
      constraint (T + E + N) mod 10 == 0 /\ (T + E + N) div 10 > 1;
      constraint (N + I + N + E) mod 9 == 0 /\ (N + I + N + E) div 9 > 1;
      constraint (T + H + R + E + E) mod 3 == 0 /\ (T + H + R + E + E) div 3 > 1;
      constraint (F + I + V + E) mod 5 == 0 /\ (F + I + V + E) div 5 > 1;
      constraint (E + I + G + H + T) mod 8 == 0 /\ (E + I + G + H + T) div 8 > 1;
      constraint (E + L + E + V + E + N) mod 11 == 0 /\ (E + L + E + V + E + N) div 11 > 1;
      constraint (T + H + I + R + T + E + E + N) mod 13 == 0 /\ (T + H + I + R + T + E + E + N) div 13 > 1;
      
      solve satisfy;
      
      % Digits of numbers in teaser
      output ["[T,H,R,E,E ] = " ++ show([T,H,R,E,E ] )
      ++ "\n" ++ "[F,I,V,E] = " ++ show ([F,I,V,E] )
      ++ "\n" ++ "[E,I,G,H,T] = " ++ show([E,I,G,H,T] )
      ++"\n" ++ "[N,I,N,E] = " ++ show([N,I,N,E])
      ++"\n" ++ "[T,E,N] = " ++ show ([T,E,N] )
      ++ "\n" ++ "[E,L,E,V,E,N] = " ++ show ([E,L,E,V,E,N] )
      ++ "\n" ++ "[T,H,I,R,T,E,E,N] = " ++ show ([T,H,I,R,T,E,E,N] )
      ];
      
      % [T,H,R,E,E ] = [4, 5, 1, 7, 7]
      % [F,I,V,E] = [8, 2, 3, 7]   - so FIVE = 8237 (answer)
      % [E,I,G,H,T] = [7, 2, 6, 5, 4]
      % [N,I,N,E] = [9, 2, 9, 7]
      % [T,E,N] = [4, 7, 9]
      % [E,L,E,V,E,N] = [7, 0, 7, 3, 7, 9]
      % [T,H,I,R,T,E,E,N] = [4, 5, 2, 1, 4, 7, 7, 9]
      % ----------
      % ==========
      % Finished in 180msec
      
      
      
      

      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