Updates from Jim Randell Toggle Comment Threads | Keyboard Shortcuts

  • Unknown's avatar

    Jim Randell 8:04 am on 22 May 2025 Permalink | Reply
    Tags:   

    Teaser 2546: Secret agents 

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

    Secret agents James and George exchange coded messages. The code is given by an addition sum, with different letters replacing different digits. The message is then written below the sum. James needs to send George the time (24-hour clock) at which their next secret operation is to begin. He texts as follows:

    THREE + THREE + TWO = EIGHT
    time: DATA

    When does this secret operation begin?

    [teaser2546]

     
    • Jim Randell's avatar

      Jim Randell 8:05 am on 22 May 2025 Permalink | Reply

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

      It runs in 77ms. (Internal runtime of the generated program is 94µs).

      #! python3 -m enigma -rr
      
      SubstitutedExpression.split_sum
      
      "THREE + THREE + TWO = EIGHT"
      --invalid="0,ET"
      
      # presumably DATA is a time expressed using the 24 hour clock (00:00 - 23:59)
      --extra="DA < 24"
      --extra="TA < 60"
      
      # format answer as a time using a 24 hour clock
      --code="time = lambda h, m: sprintf('{h:02d}:{m:02d}')"
      --answer="time(DA, TA)"
      

      Solution: The operation begins at 15:35.

      Like

    • Ruud's avatar

      Ruud 5:04 pm on 24 May 2025 Permalink | Reply

      import istr
      
      print(*(d|a|':'|t|a for t,h,r,e,w,o,i,g,a,d in istr.permutations(istr.digits()) if d|a<24 and t|a<60 and 2*(t|h|r|e|e)+(t|w|o)==(e|i|g|h|t)))
      

      Like

  • Unknown's avatar

    Jim Randell 9:06 am on 20 May 2025 Permalink | Reply
    Tags:   

    Teaser 2522: [Letter values] 

    From The Sunday Times, 23rd January 2011 [link] [link]

    I have numbered the letters of the alphabet from 1 to 26, but not in alphabetical order, giving each letter its own unique value. So, for any word, I can work out its value by adding up the values of its letters. In this way, I have found some words with equal values. For example, the following nine words all have the same value:

    I
    SAY
    ALL
    THESE
    HERE
    HAVE
    EQUAL
    VALUES
    TOO

    What are the values of Y, O, U, R, S?

    This puzzle was originally published with no title.

    [teaser2522]

     
    • Jim Randell's avatar

      Jim Randell 9:08 am on 20 May 2025 Permalink | Reply

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

      It runs in 356ms. (Internal runtime of the generated code is 206ms).

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      # use values 1-26
      --base=27
      --digits="1-26"
      
      # all the words give sums equal to I
      "S + A + Y = I"
      "A + L + L = I"
      "T + H + E + S + E = I"
      "H + E + R + E = I"
      "H + A + V + E = I"
      "E + Q + U + A + L = I"
      "V + A + L + U + E + S = I"
      "T + O + O = I"
      
      --template=""
      --answer="(Y, O, U, R, S)"
      

      Solution: Y = 20; O = 10; U = 3; R = 8; S = 2.

      The complete list of values determined are:

      A = 4
      E = 1
      H = 16
      I = 26
      L = 11
      O = 10
      Q = 7
      R = 8
      S = 2
      T = 6
      U = 3
      V = 5
      Y = 20

      Like

  • Unknown's avatar

    Jim Randell 6:50 am on 18 May 2025 Permalink | Reply
    Tags:   

    Teaser 3269: Combination lock 

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

    Phil buys a four-digit combination lock where each dial can be set from 0 to 9, and needs to decide what number to set to unlock it. He opts for a number with all different digits and a leading zero so he only has to remember a three-digit number. When the chosen number is set for the lock to open, nine other four-digit numbers are visible. For instance, if he chooses 0123, then 1234, 2345, 3456, 4567, 5678, 6789, 7890, 8901 and 9012 are all visible. As a retired Maths teacher with a natural interest in numbers he examines the other nine numbers that are displayed when the lock is set to open. None of these numbers are prime but two of them are perfect squares.

    What three-digit number does Phil need to remember?

    [teaser3269]

     
    • Jim Randell's avatar

      Jim Randell 7:06 am on 18 May 2025 Permalink | Reply

      A brute force approach gives a short and acceptably fast program.

      The following Python program runs in 77ms. (Internal runtime is 8.4ms).

      from enigma import (irange, nconcat, subsets, is_prime, is_square_p, printf)
      
      # shift digits <ds> by increment <i> (modulo <n>)
      shift = lambda ds, i, n=10: ((d + i) % n for d in ds)
      
      # consider possible 3 digit numbers
      for (X, Y, Z) in subsets(irange(1, 9), size=3, select='P'):
        # calculate each of the "other" numbers
        ns = list(nconcat(shift((0, X, Y, Z), i)) for i in irange(1, 9))
        # (exactly) 2 are squares
        if sum(is_square_p(n) for n in ns) != 2: continue
        # none are prime
        if any(is_prime(n) for n in ns): continue
        # output solution
        printf("0{X}{Y}{Z} -> {ns}")
      

      Solution: The number Phil has to remember is 912.

      So the combination is: 0912.

      Of the other numbers visible (1023, 2134, 3245, 4356, 5467, 6578, 7689, 8790, 9801):

      4356 = 66^2
      9801 = 99^2

      There is one other combination that gives 2 squares: 0327.

      Of the other numbers visible (1438, 2549, 3650, 4761, 5872, 6983, 7094, 8105, 9216):

      2549 is prime
      4761 = 69^2
      6983 is prime
      9216 = 96^2

      So this fails the requirement that none of the other numbers is prime.

      And these are the only 2 combinations that give more than one square among the “other” numbers.

      Like

      • Jim Randell's avatar

        Jim Randell 10:39 am on 18 May 2025 Permalink | Reply

        Here is a slightly longer, but faster, alternative approach.

        It considers the possible 4-digit squares, and then looks for 2 that give the same combination number (0xyz).

        It has an internal runtime of 253µs.

        from enigma import (defaultdict, irange, nsplit, nconcat, is_prime, join, printf)
        
        # shift digits <ds> by increment <i> (modulo <n>)
        shift = lambda ds, i, n=10: ((d + i) % n for d in ds)
        
        # count 4-digit squares by the combination (smallest number on the lock)
        m = defaultdict(int)
        for i in irange(32, 99):
          ds = nsplit(i * i)
          if len(set(ds)) != len(ds): continue
          # reduce the first digit to zero
          k = tuple(shift(ds, -ds[0]))
          m[k] += 1
        
        # consider combinations that give exactly 2 squares
        for (k, n) in m.items():
          if n != 2: continue
        
          # construct the 9 "other" numbers
          ns = list(nconcat(shift(k, i)) for i in irange(1, 9))
          # none of them are prime
          if any(is_prime(n) for n in ns): continue
        
          # output solution
          printf("{k} -> {ns}", k=join(k))
        

        Like

        • Frits's avatar

          Frits 11:04 am on 18 May 2025 Permalink | Reply

          Nice, I was also considering possible 4-digit squares but was using the less efficient “itertools.combinations”.

          Like

  • Unknown's avatar

    Jim Randell 8:24 am on 15 May 2025 Permalink | Reply
    Tags:   

    Teaser 2552: Ever-increasing circles 

    From The Sunday Times, 21st August 2011 [link] [link]

    I took a piece of A4 paper and cut it straight across to make a square and a rectangle. I then cut the square along a diagonal and, from the rectangle, cut as large a circle as I could. My friend Ron said that if I needed two triangles of those sizes and as large a circle in one piece as possible from an A4 sheet, then he could do much better. With a fresh sheet of A4, he produced the two triangles and the biggest circle possible.

    What is the area of his circle divided by the area of mine?

    [teaser2552]

     
    • Jim Randell's avatar

      Jim Randell 8:24 am on 15 May 2025 Permalink | Reply

      A-series paper has the aspect ratio 1 : √2.

      Cutting a square from a sheet will leave a 1 × (√2 − 1) rectangle, out of which we can cut out a circle with radius (√2 − 1)/2.

      By rotating one of the triangles so that the √2 side lies along one of the long edges of the sheet, we open up a kite shaped area, in which we can inscribe a circle.

      The radius of a semi-circle inscribed in a right-angled triangle with sides (a, b, h) is given by:

      1/r = 1/a + 1/b; or:
      r = a.b / (a + b)

      (See proof below).

      And in this case we have:

      a = 1
      b = √2 − 1

      r = (√2 − 1) / √2
      r = (√2 − 1)(√2/2)

      And so we see the radius of Ron’s circle is √2 times the radius of the setters original circle.

      And so the area of Ron’s circle is twice the area of the setters circle.

      Solution: The answer is 2.


      Proof:

      Consider the right-angled triangle ABC, where the length of the sides BC = a, AC = b.

      The area of the triangle is: a.b/2.

      But we can also write the area as the sum of the areas of triangles AXC and BXC, both of which have a height of r, the radius of the semicircle.

      So:

      a.r/2 + b.r/2 = a.b/2
      r(a + b) = a.b
      r = a.b / (a + b)

      Like

  • Unknown's avatar

    Jim Randell 7:32 am on 13 May 2025 Permalink | Reply
    Tags: ,   

    Teaser 2350: [Circles and tangents] 

    From The Sunday Times, 7th October 2007 [link]

    A puzzle in a magazine was flawed: it had two different answers. The puzzle showed two circles that did not meet. It gave the radii (prime numbers of centimetres) and the distance between the centres (a whole number of centimetres less than 50).

    It then said:

    “Imagine a straight line that is a tangent to both the circles. The distance between the two points where that line touches the circles is. a whole number of centimetres.

    How many?”

    What was the radius of the smaller circle?

    This puzzle was originally published with no title.

    [teaser2350]

     
    • Jim Randell's avatar

      Jim Randell 7:32 am on 13 May 2025 Permalink | Reply

      We are to construct the tangents to two non-touching circles. See: [ @wikipedia ]

      I am assuming that, as we are asked to find the radius of the smaller circle, then the two circles must be of different sizes. (And without this condition the Teaser puzzle itself is flawed).

      If the circles have radii r and R (where r < R), and the separation between their centres is d, then the tangents have length:

      outer tangent, length T: d^2 = T^2 + (R − r)^2
      inner tangent, length t: d^2 = t^2 + (R + r)^2

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

      from enigma import (primes, irange, ircs, printf)
      
      # r = radius of smaller circle (a prime)
      for r in primes.between(2, 24):
        # R = radius of the larger circle (a prime)
        for R in primes.between(r + 1, 48 - r):
          # d = separation of centres (< 50)
          for d in irange(r + R + 1, 49):
            # calculate: T = length of outer tangent, t = length of inner tangent
            (T, t) = (ircs(d, -(R - r)), ircs(d, -(R + r)))
            if T is None or t is None or t == T: continue
            # output viable configurations
            printf("r={r} R={R} d={d} -> T={T} t={t}")
      

      Solution: The smaller circle has a radius of 7 cm.

      There are two possible scenarios that provide tangents with two different integer lengths:

      d=26, r=7, R=17 → T=24, t=10
      d=34, r=7, R=23 → T=30, t=16

      And so one of these must be the scenario presented in the magazine puzzle.

      The first of these solutions looks like this:


      If the circles were allowed to be the same size, then we can find the following additional viable configurations, that give two different integer tangent lengths:

      d=5, r=2, R=2 → T=5, t=3
      d=10, r=3, R=3 → T=10, t=8
      d=26, r=5, R=5 → T=26, t=24

      And the Teaser puzzle would not have a unique solution.

      These additional solutions can be seen by changing line 6 in the program to start the search for R from the value of r.

      Like

      • Frits's avatar

        Frits 7:13 pm on 14 May 2025 Permalink | Reply

        In both scenarios we have (not a general rule):

        d^2 = T^2 + t^2 = 2.(r^2 + R^2)

        Like

    • Frits's avatar

      Frits 11:02 am on 16 May 2025 Permalink | Reply

      from enigma import primes, pythagorean_triples, subsets
      from collections import defaultdict
      
      prms = set(primes.between(2, 48))
      
      # collect Pythagorean triples for each distance between the centres
      pt = defaultdict(list)
      for x, y, h in pythagorean_triples(49):
        pt[h] += [x]
        pt[h] += [y]
      
      for d, vs in pt.items():  
        for t, T in subsets(sorted(vs), 2):
          # make sure r and R are integers
          if t % 2 != T % 2: continue
          r, R = (T - t) // 2, (T + t) // 2
          if any(x not in prms for x in (r, R)): continue 
          print(f"{r=} {R=} {d=} -> {T=} {t=}")
      

      Like

  • Unknown's avatar

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

    Teaser 3268: W-hoops! 

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

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

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

    How many hoops does the box hold?

    [teaser3268]

     
    • Jim Randell's avatar

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

      See [ @wikipedia ].

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

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

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

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

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

      Solution: The box holds 5 hoops.

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

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

      The two approximations for 𝛑 give:

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


      Using analysis:

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

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

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

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

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

      In the second case:

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

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

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

      Like

  • Unknown's avatar

    Jim Randell 8:13 am on 9 May 2025 Permalink | Reply
    Tags:   

    Teaser 2553: The X-Factor 

    From The Sunday Times, 28th August 2011 [link] [link]

    George and Martha’s daughter entered a singing competition. The entrants were numbered from one upwards. The total number of entrants was a three-digit number and their daughter’s number was a two-digit number. The five digits used in those two numbers were all different and non-zero. Martha noted that the number of entrants was a single-digit multiple of their daughter’s number. George realised the same would be true if the two digits of their daughter’s number were reversed.

    How many entrants were there?

    [teaser2553]

     
    • Jim Randell's avatar

      Jim Randell 8:14 am on 9 May 2025 Permalink | Reply

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

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

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      # total number of entrants = ABC
      # daughters number = XY
      
      --digits="1-9"
      
      "ediv(ABC, XY) < 10"
      "ediv(ABC, YX) < 10"
      
      --answer="ABC"
      

      Solution: There were 168 entrants.

      The daughter’s number is either 24 or 42.

      168 / 24 = 7
      168 / 42 = 4

      Like

      • Frits's avatar

        Frits 3:31 pm on 9 May 2025 Permalink | Reply

        I tested that “ediv” is a bit faster than “div” as for “div” an error/exception may occur when comparing “None” with an integer.

        Like

        • Jim Randell's avatar

          Jim Randell 4:19 pm on 9 May 2025 Permalink | Reply

          @Frits: [[ ediv() ]] throws an error if the division is not exact, whereas [[ div() ]] returns [[ None ]].

          In Python 3 you will get type error if you compare [[ None ]] to a number. But in Python 2 [[ None ]] compares as less than any number (including [[ -inf ]]). So using [[ ediv() ]] also allows the run file to work in older versions of Python.

          Like

          • Frits's avatar

            Frits 3:18 pm on 10 May 2025 Permalink | Reply

            or skipping a loop over “J”.

            #! python3 -m enigma -rr
             
            SubstitutedExpression
             
            # total number of entrants = ABC
            # daughters number = XY
             
            --distinct="ABCXY"
            --digits="1-9"
            --invalid="1,I"
            
             
            # find the single digit multiples I, J
            "XY * I = ABC"
            "ABC % YX == 0 and ABC // YX < 10"
             
            --answer="ABC"
            

            Like

      • Jim Randell's avatar

        Jim Randell 8:33 am on 10 May 2025 Permalink | Reply

        Or without using [[ *div() ]] function calls.

        This run file has an internal runtime of 188µs.

        #! python3 -m enigma -rr
        
        SubstitutedExpression
        
        # total number of entrants = ABC
        # daughters number = XY
        
        --distinct="ABCXY,IJ"
        --digits="1-9"
        
        # find the single digit multiples I, J
        "XY * I = ABC"
        "YX * J = ABC"
        
        --answer="ABC"
        

        (Further speed improvements left as a simple exercise for the reader).

        Like

    • Ruud's avatar

      Ruud 3:01 pm on 9 May 2025 Permalink | Reply

      import peek
      import istr
      
      for daughter in istr(range(10, 100)):
          for n, m in istr.combinations(range(1, 10), r=2):
              if (
                  (entrants := daughter * n) == (daughter_reversed := daughter.reversed()) * m
                  and len(entrants) == 3
                  and ("0" not in entrants)
                  and (entrants | daughter).all_distinct()
                  and (entrants | daughter_reversed).all_distinct()
              ):
                  peek(entrants, daughter, daughter_reversed)
      

      Like

      • Frits's avatar

        Frits 4:09 pm on 9 May 2025 Permalink | Reply

        @Ruud,

        Line 11 doesn’t seem to be logically necessary.

        I read the readme of the peek module at your Salabim site. I wish I had known this before. I will have a look at it (I’ll send you an email concerning an installation issue).

        I made my own simple debugging tool, I don’t need to import anything as I use preprocessing.
        A basic version is described at:

        [ https://enigmaticcode.wordpress.com/notes/#comment-10234 ]

        Like

        • ruudvanderham's avatar

          ruudvanderham 1:16 pm on 10 May 2025 Permalink | Reply

          Yes, you are right. Line 11 could be omitted (although it is not wrong).
          Nice to be in contact (in Dutch!) with you on the subject of peek.

          Like

  • Unknown's avatar

    Jim Randell 9:42 am on 6 May 2025 Permalink | Reply
    Tags:   

    Teaser 2523: [Unusual Avenue] 

    From The Sunday Times, 30th January 2011 [link] [link]

    George and Martha moved to a two-figure house number in Unusual Avenue. The avenue runs from south to north and each house is directly opposite another. No 1 is the southernmost on the west side, then the houses run consecutively up the west side, then southwards back down the east side. So, No 1 is opposite the highest-numbered house.

    George and Martha’s number is a multiple of the number of the house opposite. The same is true of the five houses their daughters live in, but all their houses have three-figure numbers.

    How many houses are there in the avenue?

    This puzzle was originally published with no title.

    [teaser2523]

     
    • Jim Randell's avatar

      Jim Randell 9:42 am on 6 May 2025 Permalink | Reply

      If there are k houses on each side of the road, then opposite houses sum to (2k + 1).

      There are 2k houses on the road in total, and the number of George and Martha’s house n is a 2-digit number that is a multiple of the house opposite (which has number 2k + 1 − n).

      Hence:

      n > 2k + 1 − n

      k < (2n − 1) / 2

      And the maximum possible 2-digit value for n is 99, hence:

      k ≤ 98

      This gives us an upper bound for an exhaustive search.

      The following Python program runs in 56ms. (Internal runtime is 332µs).

      from enigma import (irange, group, ndigits, printf)
      
      # consider possible k values (number of houses on one side of the road)
      for k in irange(52, 98):
        t = 2 * k  # total number of houses
        # collect numbers that are a multiple of the house opposite
        ns = list(x for x in irange(k + 1, t) if x % (t + 1 - x) == 0)
        if len(ns) < 6: continue
        # group the numbers by digit count
        g = group(ns, by=ndigits)
        # ensure there is a 2-digit and 5 3-digits
        (n2s, n3s) = (g.get(2), g.get(3))
        if (not n2s) or (not n3s) or (len(n3s) < 5): continue
        # output solution
        printf("total houses = {t} [n2s={n2s} n3s={n3s}]")
      

      Solution: There are 134 houses in the road.

      And so the numbers of opposite houses sum to 135.

      George and Martha live at 90, which is opposite 45.

      And there are 6 houses that their 5 daughters could live at:

      108 (opposite 27)
      120 (opposite 15)
      126 (opposite 9)
      130 (opposite 5)
      132 (opposite 3)
      134 (opposite 1)


      If instead of an Avenue the road was a Close, with a house at the closed end (or several houses), then there would be further possible solutions.

      Like

    • Frits's avatar

      Frits 12:39 pm on 8 May 2025 Permalink | Reply

      Using ideas of Jim and Brian.

      # the number of houses is even, say 2.k, so we want numbers 
      # n such that:
      # 
      #    n = m.(2.k + 1 - n)  ==>  (m + 1).n = m.(2.k + 1)
      #
      # this makes both n and m even (credit: B. Gladman)
      
      d3 = 5  # five 3-digit numbers
      mn = 49 + d3            # 2.k >= 100 + (d3 - 1) * 2 
      mx = (3 * 99 - 2) // 4  # n >= 2 * (2k + 1) - n
      
      # are there at least <a> house numbers with correct opposite house number?
      def find(k, a, strt, m=-1):
        c, x, k2 = 0, strt, k + k
        while c <= a and x >= m:
          c += (x % (k2 + 1 - x) == 0)
          x -= 2
        return c >= a  
      
      # consider possible k values (number of houses on one side of the road)
      for k in range(mn, mx + 1):
        # opposite housenumbers t.m / (m + 1) and t / (m + 1) with t = 2.k + 1
        # t.m / (m + 1) <= 98 or m <= 98 / (t - 98)  
        m = 98 // ((k2 := k + k) - 97) 
        # check for solutions of even n less than 100
        if any((k2 + 1) % x == 0 for x in range(3, m + 2, 2)):
          # check for at least <d3 - 1> solutions for even numbers 
          # in range 100 ... 2.k - 2
          if find(k, d3 - 1, k2 - 2, 100):
            print("answer:", k2)
      

      Like

  • Unknown's avatar

    Jim Randell 7:14 am on 4 May 2025 Permalink | Reply
    Tags:   

    Teaser 3267: Leaf-eaters 

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

    My encyclopaedia consists of many volumes, each containing the same number (over 40) of leaves. (Each leaf has a page number on each side. With n leaves Volume 1 would have pages 1 to 2n, Volume 2 would have pages 2n+1 to 4n, etc.) I keep the encyclopaedia on one long shelf so that on their spines I can read “Volume 1”, “Volume 2”, etc, from left to right.

    A voracious bookworm started at the left-hand end of the shelf and ate through some covers and leaves, finishing by eating through the leaf with a page number double the number of leaves it had eaten through. Meanwhile, another bookworm started at the right-hand end of the shelf and ate through twice as many leaves as the first bookworm. Then in two of the volumes, the percentage of the nibbled leaves was equal to the volume number.

    How many volumes are there in the set? And how many leaves per volume?

    [teaser3267]

     
    • Jim Randell's avatar

      Jim Randell 8:57 am on 4 May 2025 Permalink | Reply

      See also: Enigma 660, Tantalizer 386. (In particular the note about page numbering).

      If the worms pass each other, then 100% of the pages in each volume will have been nibbled, and the only possibility in this case is that vol 100 is 100% nibbled (providing there are at least 100 volumes).

      However if they both reach partially into the same volume (from opposite sides), then we could have a situation where this volume has a smaller percentage nibbled, while all the remaining volumes are 100% nibbled, so if we have at least 100 volumes, this could lead to a viable scenario.


      For volumes with n leaves per volume. If worm 1 nibbles v whole volumes, and x (= 1 .. n) additional leaves in the final volume it visits, then the total number of leaves nibbled is:

      t1 = nv + x

      and the even page number of the final leaf nibbled is:

      p1 = 2n(v + 1) − 2(x − 1)

      and this page number is twice the number of leaves nibbled, hence:

      2n(v + 1) − 2(x − 1) = 2(nv + x)

      x = (n + 1) / 2

      Hence n must be an odd number. (This effectively eliminates worm 1 from making the other partially nibbled volume by itself).

      And worm 2 must nibble twice as many leaves:

      t2 = 2(nv + x)
      t2 = (2v + 1)n + 1

      Hence worm 2 nibbles (2v + 1) whole volumes and 1 additional page of volume (v + 1). (And this effectively eliminates worm 2 from making the other partially nibbled volume by itself).

      We want there to be at least 100 volumes, and so:

      k = v + (2v + 1) + 1
      k ≥ 100 ⇒ v ≥ 33

      And the volume nibbled from both ends (v + 1) must have the percentage of its pages nibbled that is the number of the volume.

      100 (x + 1) / n = v + 1

      n(v − 49) = 150

      So we can determine the solution by looking at divisor pairs of 150, such that n is greater than 40 and odd. It turns out there is only one possibility:

      n = 75, v = 51 → x = 38, p = 52%

      Programatically:

      from enigma import (divisors_pairs, div, printf)
      
      # consider possibilities for n = leaves per volume (odd divisor of 150)
      for (v, n) in divisors_pairs(150):
        if not (n > 40): break
        if not (n % 2 == 1): continue
        v += 49  # v = number of whole volumes nibbled by worm 1
        x = div(n + 1, 2)  # x = number of extra leaves nibbled by worm 1
        p = v + 1  # p = volume where percentage nibbled = volume number
        v2 = 2 * v + 1  # v2 = number of whole volumes nibbled by worm 2
        x2 = 1  # x2 = number of extra leaves nibbled by worm 2
        k = v + v2 + 1  # k = total number of volumes
        # output solution
        printf("n={n} k={k} [v={v} x={x} -> v2={v2} x2={x2} p={p}%]")
      

      Solution: There are 155 volumes in the set. And 75 leaves per volume.

      Each volume has 150 pages, and the complete set consists of 23250 pages.

      Worm 1 has eaten through vols 1 → 51, and 38 leaves of vol 52, so the last leaf consumed is pages 7725/7726. The total number of leaves consumed is 51×75 + 38 = 3863, and 3863×2 = 7726.

      Worm 2 starts from the other end of the shelf and eats through 7726 leaves, which is 103 volumes and 1 leaf. It eats through vols 155 → 53, and 1 leaf of vol 52 (= page 7651/7652).

      So vol 52 has had 38 leaves eaten by worm 1 and 1 leaf eaten by worm 2, for a total of 39/75 leaves = 52% of the leaves in the volume. And this percentage is the same as the volume number.

      The remaining volumes, vols 1-51 and 53-155, have been eaten through entirely by worm 1 or worm 2 respectively. In particular vol 100 has been eaten through 100% by worm 2.

      And so there are exactly 2 volumes where the percentage of leaves eaten is equal to the volume number.

      Like

      • Frits's avatar

        Frits 4:37 pm on 4 May 2025 Permalink | Reply

        I have gotten a similar manual solution, I didn’t want to publish it. The formula and variables are based on Jim’s original program.

         
        # n = 2x - 1
        # percentage 100(x + x2) = n(v + 1)
        # 50(n + 1) = n.v + n - 100x2
        # v = 49 + (100.x2 + 50) / n
        
        # 2t/n = (2nv + 2x) / n = 2v + (n + 1) / n = 2v + 1 + 1/n
        # so v2 = 2v + 1 and x2 must be 1
        
        # x2 = 1 implies v = 49 + 150 / n
        # as n must be odd and > 40 only one value remains for n
        # all other values can now be calculated.
        

        Like

      • Frits's avatar

        Frits 6:48 pm on 4 May 2025 Permalink | Reply

        We should also prove that there is/are no solution(s) if the percentage of the nibbled leaves for worm 1 was equal to the volume number (v < 100).
        As we have v = 50 + 50 / n this can only happen for n = 50 (not odd).

        So we can disregard this option.

        Like

  • Unknown's avatar

    Jim Randell 7:37 am on 1 May 2025 Permalink | Reply
    Tags:   

    Teaser 2554: Odd and even 

    From The Sunday Times, 4th September 2011 [link] [link]

    Roddy and Eve have a set of cards on which is written a consecutive sequence of whole numbers, one number on each card. Roddy challenges Oliver to choose two cards at random, promising a prize if the sum of the numbers is odd. Eve issues the same challenge, but offers a prize for an even sum. Oliver accepts the challenge that gives him the greater probability of winning. This probability is a whole number percentage and, when reversed, it gives the two-digit number of cards.

    Whose challenge did Oliver accept and how many cards are there?

    [teaser2554]

     
    • Jim Randell's avatar

      Jim Randell 7:37 am on 1 May 2025 Permalink | Reply

      We can consider sets of cards numbered 1 .. n, as if each card has a offset x from these values, then the sum of the pairs is increased by 2x, and this doesn’t change whether the sum is odd or even.

      The percentage must be > 50%, and it is the reverse of the number of cards.

      This Python program runs in 77ms. (Internal runtime is 16ms).

      from enigma import (irange, subsets, div, nrev, printf)
      
      # consider 2-digit percentages >50%
      for p in irange(51, 99):
        # reverse is the number of cards
        n = nrev(p)
        if n < 10: continue
      
        # count odd/even pairs
        t = [0, 0]
        for (x, y) in subsets(irange(1, n), size=2):
          t[(x + y) % 2] += 1
        T = sum(t)
      
        # calculate higher probability as a percentage
        x = max(t)
        q = div(100 * x, T)
        if q == p:
          # output solution
          printf("n={n} -> t={t} p={p}% (= {x}/{T})")
      

      Solution: There are 25 cards. Oliver chooses Roddy’s challenge.

      With 25 cards there are C(25, 2) = 300 ways of choosing a pair of cards.

      144 of them give an even total, and 156 of them give an odd total.

      So the probability of choosing cards that give an odd total is:

      156 / 300 = 0.52 or 52%

      Like

    • Frits's avatar

      Frits 6:06 pm on 1 May 2025 Permalink | Reply

      # only consider odd number of cards, n = 10 * a + b
      # number odd sums = 1st even * 2nd odd + 1st odd * 2nd even
      # ((n - 1) // 2) * ((n + 1) // 2) + ((n + 1) // 2) * ((n - 1) // 2) or
      # (n**2 - 1) / 2
      # percentage = number odd / (n * (n - 1) = (n + 1) / (2 * n)
      
      # a whole number percentage and, when reversed, 
      # it gives the two-digit number of cards
      # 100 * ((10 * a + b) + 1) / ((10 * a + b) * 2) = 10 * b + a
      # 500 * a + 50 * b + 50 = (a + 10 * b) * (10 * a + b)
      # 10 * a**2 + (101 * b - 500) * a + 10 * b**2 - 50 * b - 50 = 0
      
      for b in range(5, 10, 2):
        a, r = divmod(500 - 101 * b + 3 * (1089 * b**2 - 11000 * b + 28000)**.5, 20)
        if r: continue
        print(f"There are {10 * int(a) + b} cards. "
              f"Oliver chooses Roddy's challenge")
      

      Like

  • Unknown's avatar

    Jim Randell 8:04 am on 29 April 2025 Permalink | Reply
    Tags: ,   

    Teaser 2526: [Lucky dates] 

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

    Orla was married in this century, on her lucky day of the week, and her twin sister Enya’s lucky-number day of the month. Enya told Orla on that big day that she intended her own wedding to be on the same lucky day of the week and lucky-number day of the month. She realised that it could not be in the same year as Orla’s, but Orla added that Enya could not be married in the following year, either. Enya married months later, the actual number of months being the sum of the six digits of Orla’s wedding date.

    What was Orla’s wedding date?

    This puzzle was originally published with no title.

    [teaser2526]

     
    • Jim Randell's avatar

      Jim Randell 8:04 am on 29 April 2025 Permalink | Reply

      I am assuming that “this century” means the 21st century (which started on 2001-01-01).

      And “the sum of the six digits of Orla’s wedding date” refers to the sum of date expressed as “DD/MM/YY” (i.e. using the final 2 digits of the year, rather than indicating that Orla’s wedding date is of the form “D/M/YYYY”).

      This Python program runs in 67ms. (Internal runtime is 9.0ms).

      from datetime import (date, timedelta)
      from enigma import (irange, repeat, inc, dsum, catch, printf)
      
      # increment date (<d>, <m>, <y>) by <i> months
      def incmonth(d, m, y, i=1):
        m += i
        while m > 12:
          m -= 12
          y += 1
        return catch(date, y, m, d)
      
      # consider dates in the 21st century for Orla's wedding
      for d1 in repeat(inc(timedelta(days=1)), date(2001, 1, 1)):
        if d1.year > 2009: break
        # calculate the day of the week
        wd = d1.isoweekday()
        valid = lambda x: x is not None and x.isoweekday() == wd
        # calculate the digit sum of the date (6 digits)
        (d, m, y) = (d1.day, d1.month, d1.year)
        n = dsum(d) + dsum(m) + dsum(y % 100)
        # consider a date <n> months in advance
        d2 = incmonth(d, m, y, n)
        if not valid(d2): continue
        if not (d2.year - d1.year > 1): continue
        # and check no intervening date would do
        if any(valid(incmonth(d, m, y, i)) for i in irange(1, n - 1)): continue
        # output solution
        printf("Orla = {d1}, Enya = {d2} [{n} months later]")
      

      Solution: Orla’s wedding day was: Wednesday, 31st December 2008.

      The next (Wednesday, 31st) occurs on: Wednesday, 31st March 2010, and so this is Enya’s wedding day.

      Enya’s wedding day occurs 15 months after Orla’s, and the sum of the digits in Orla’s date expressed as: 31/12/08 is 15.


      If we were to use an 8-digit format for the dates (e.g. “DD/MM/YYYY” or “YYYY-MM-DD”), we can get a solution of:

      Orla = Tuesday, 31st July 2007
      Enya = Tuesday, 31st March 2009 (20 months later)

      Like

    • Frits's avatar

      Frits 12:32 pm on 30 April 2025 Permalink | Reply

      from enigma import SubstitutedExpression
      from datetime import date
      
      # check date format and possible other checks
      def check(AB, CD, EF, extra=False):
        try:
          wd = date(2000 + EF, CD, AB).weekday()
        except ValueError:
          return False
        
        if extra:
          # check if in the next year there are any dates with day number AB
          # on the same weekday as Orla's
          for m in range(1, 13):
            try:
              if date(2000 + EF + 1, m, AB).weekday() == wd: 
                return False
            except ValueError:
              continue
        return True
      
      # the alphametic puzzle
      p = SubstitutedExpression(
        [ # Orla : AB-CD-EF  Enya: AB-GH-IJ  (dd-mm-yy)
          "AB < 32",
          "CD < 13",
          # Enya married months later but not in the following year
          "(sd := A + B + C + D + E + F) > 24 - CD",
          # check validity of Orla's date and that Enya could not be married 
          # in the following year
          "check(AB, CD, EF, 1)",
          
          "IJ <= 11",   # 2011
          # Enya could not be married in the following year
          "IJ > EF + 1",
          
          # Enya married months later, the actual number of months being the sum 
          # of the six digits of Orla's wedding date
          "sd - 12 * (IJ - EF) + CD = GH",
          "0 < GH < 13",
          
          # check validity of Enya's date
          "check(AB, GH, IJ)",
          # Enya married on the same day of the week as Orla
          "date(2000 + EF, CD, AB).weekday() == date(2000 + IJ, GH, AB).weekday()"
        ],
        answer="(AB, CD, 2000 + EF)",
        d2i=dict([(k, "ICGE") for k in range(2, 4)] +
                 [(k, "AICGE") for k in range(4, 10)]),
        distinct="",
        s2d=dict(E=0),
        env=dict(date=date, check=check),
        reorder=0,
        verbose=0,    # use 256 to see the generated code
      )
      
      # print answers
      for ans in p.answers():
        print(f"answer: {'-'.join(str(x) for x in ans)}")
      

      Like

  • Unknown's avatar

    Jim Randell 6:41 am on 27 April 2025 Permalink | Reply
    Tags:   

    Teaser 3266: Where are the keys? 

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

    Skaredahora’s venture into atonal music — music with no fixed key — had as its (so-called!) melody a long sequence employing 8 different notes, which he labelled Q to X. Behind his composition were 7 keys each using 4 notes. Every three consecutive melody notes obeyed specific rules:

    (1) all three had to be different;
    (2) they had to belong to exactly one of the keys;
    (3) they had not to repeat in the same order any other three consecutive notes;
    (4) the key had to change at every step.

    His chosen melody was:

    WUQRVWQRXWTRUQWRVQWTSRQWVRSTWQVRWQURTWXRQVWRTSX

    What were the keys involving X, in alphabetical order individually and collectively (e.g. QRTX, SUVX)?

    [teaser3266]

     
    • Jim Randell's avatar

      Jim Randell 8:08 am on 27 April 2025 Permalink | Reply

      Be careful to get the melody sequence exactly right! (I initially had a typo that made it impossible to find a solution).

      This Python 3 program solves the puzzle constructively. It first checks that triples of 3 consecutive notes are all different (3), and each consists of 3 different notes (1). It then fills out keys that can be used to play the triples in the required order, with a key change at each step (4). And then finally, once a set of keys is found, it checks that each triple belongs to exactly one of the keys (2).

      The program runs in 72ms. (Internal runtime is 1.2ms).

      from enigma import (tuples, join, fail, update, singleton, seq2str, printf)
      
      # the chosen melody
      melody = "WUQRVWQRXWTRUQWRVQWTSRQWVRSTWQVRWQURTWXRQVWRTSX"
      
      K = 4  # number of notes in a key
      N = 7  # number of keys
      
      # split into triples
      ts = list(tuples(melody, 3, fn=join))
      # check they are all different (3), and consist of different notes (1)
      if not (len(ts) == len(set(ts)) and all(len(t) == len(set(t)) for t in ts)):
        fail(msg="invalid melody")
      
      # find keys <ks> for the triples <ts>, with no adjacent repeats (4)
      # ts = triples to solve
      # n = number of next key to allocate
      # ks = map of keys to notes
      # pk = previous key
      def solve(ts, n=1, ks=dict(), pk=0):
        # are we done?
        if not ts:
          yield ks
          return
      
        # look at the next triple
        t = set(ts[0])
      
        # examine its relation to existing keys
        (ks1, ks2) = (list(), list())
        for (k, v) in ks.items():
          if v.issuperset(t):
            # it is already fully covered by an existing key
            ks1.append(k)
          else:
            vt = v.union(t)
            if len(vt) <= K:
              # an existing key can be extended to cover it
              ks2.append((k, vt))
      
        # there are three cases:
        if ks1:
          # it is already covered by exactly one key
          k = singleton(ks1, default=0)
          if k > 0 and k != pk: yield from solve(ts[1:], n, ks, k)
          return  # we are done
      
        # extend an existing key
        for (k, v) in ks2:
          if k != pk:
            yield from solve(ts[1:], n, update(ks, [(k, v)]), k)
      
        # allocate a new key
        if  n <= N:
          yield from solve(ts[1:], n + 1, update(ks, [(n, t)]), n)
      
      # construct the progression of keys, where each triple belongs to exactly one key (2)
      def check_keys(ts, ks):
        ss = list()
        # check each triple belongs to just one of the keys
        for t in ts:
          ss.append(singleton(k for (k, v) in ks.items() if v.issuperset(t)))
          if ss[-1] is None: return
        return ss
      
      # solve the puzzle
      for ks in solve(ts):
        ss = check_keys(ts, ks)
        if not ss: continue
        # output solution
        for k in sorted(ks):
          printf("{k}: {vs}{s}", vs=seq2str(ks[k], sort=1, sep=" "), s=(' <-' if 'X' in ks[k] else ''))
        printf("{ss}", ss=seq2str(ss, sep=" "))
        printf()
      

      Solution: The keys involving X are: QRUX, RVWX, STWX.

      The full set of keys is:

      1: (Q U V W)
      2: (Q R U X)
      3: (Q R S V)
      4: (R V W X)
      5: (Q R T W)
      6: (S T W X)
      7: (R S T U)

      (The required answer to the puzzle is keys 2, 4, 6, which all contain the note X).

      And this gives the following progression of keys:

      (1 2 3 4 1 5 2 4 6 5 7 2 1 5 4 3 1 5 6 7 3 5 1 4 3 7 6 5 1 3 4 5 1 2 7 5 6 4 2 3 1 4 5 7 6)

      Like

      • Jim Randell's avatar

        Jim Randell 12:06 pm on 4 May 2025 Permalink | Reply

        We can slightly simplify the code using the following observation (due to JohnZ [link]).

        Each set of 3 consecutive notes in the melody must uniquely identify a key, and adjacent triples must identify different keys, hence no key can consist of 4 consecutive notes in the melody, so we can identify a set of invalid keys that we can avoid to ensure each consecutive key must be different. And this means we don’t have to drag information about the previously used key as we construct the progression of keys.

        Here is a modified version of my program adapted to use this method. It has an internal runtime of 524µs.

        from enigma import (tuples, join, fail, update, singleton, seq2str, printf)
        
        # the chosen melody
        melody = "WUQRVWQRXWTRUQWRVQWTSRQWVRSTWQVRWQURTWXRQVWRTSX"
        
        K = 4  # number of notes in a key
        N = 7  # number of keys
        
        # split into triples
        ts = list(tuples(melody, 3, fn=join))
        # check they are all different (3), and consist of different notes (1)
        if not (len(ts) == len(set(ts)) and all(len(t) == len(set(t)) for t in ts)):
          fail(msg="invalid melody")
        
        # but no key can include 4 consecutive notes from the melody, so if we avoid
        # the following keys, there can be no adjacent repeats (4)
        invalid = set(tuples(melody, 4, fn=frozenset))
        
        # find keys <ks> for the triples <ts>, with no adjacent repeats (4)
        # ts = triples to solve
        # n = number of next key to allocate
        # ks = map of keys to notes
        def solve(ts, n=1, ks=dict()):
          # are we done?
          if not ts:
            yield ks
            return
        
          # look at the next triple
          t = set(ts[0])
        
          # examine its relation to existing keys
          (ks1, ks2) = (list(), list())
          for (k, v) in ks.items():
            if v.issuperset(t):
              # it is already fully covered by an existing key
              ks1.append(k)
            else:
              vt = v.union(t)
              if len(vt) <= K and vt not in invalid:
                # an existing key can be extended to cover it
                ks2.append((k, vt))
        
          # there are three cases:
          if ks1:
            # it is already covered by exactly one key
            k = singleton(ks1, default=0)
            if k > 0: yield from solve(ts[1:], n, ks)
            return  # we are done
        
          # extend an existing key
          for (k, v) in ks2:
            yield from solve(ts[1:], n, update(ks, [(k, v)]))
        
          # allocate a new key
          if  n <= N:
            yield from solve(ts[1:], n + 1, update(ks, [(n, t)]))
        
        # construct the progression of keys, where each triple belongs to exactly one key (2)
        def check_keys(ts, ks):
          ss = list()
          # check each triple belongs to just one of the keys
          for t in ts:
            ss.append(singleton(k for (k, v) in ks.items() if v.issuperset(t)))
            if ss[-1] is None: return
          return ss
        
        # solve the puzzle
        for ks in solve(ts):
          ss = check_keys(ts, ks)
          if not ss: continue
          # output solution
          for k in sorted(ks):
            printf("{k}: {vs}{s}", vs=seq2str(ks[k], sort=1, sep=" "), s=(' <-' if 'X' in ks[k] else ''))
          printf("{ss}", ss=seq2str(ss, sep=" "))
          printf()
        

        Like

    • Pete Good's avatar

      Pete Good 2:57 pm on 30 April 2025 Permalink | Reply

      Jim, The Sunday Times published this teaser under the title “Where Are the Keys?”

      Like

  • Unknown's avatar

    Jim Randell 6:45 am on 24 April 2025 Permalink | Reply
    Tags:   

    Teaser 2555: Today’s value 

    From The Sunday Times, 11th September 2011 [link] [link]

    These 10 children’s bricks are numbered from 1 to 10. Where a brick rests on two others its number is the difference of their two numbers. Given that U = 1 …

    What is the number DAY?

    [teaser2555]

     
    • Jim Randell's avatar

      Jim Randell 6:46 am on 24 April 2025 Permalink | Reply

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

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

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      # numbers range from 1 to 10
      --base=11
      --digits="1-10"
      
      "abs(U - N) = S"
      "abs(D - A) = U"
      "abs(A - Y) = N"
      "abs(T - I) = D"
      "abs(I - M) = A"
      "abs(M - E) = Y"
      
      --assign="U,1"
      --answer="(D, A, Y)"
      
      --template=""
      

      Solution: DAY = 672.

      At least the digits have values: D = 6, A = 7, Y = 2.

      But since the digits go from 1 to 10, you could argue that the result is expressed in base 11, so:

      DAY = 672 [base 11] = 805 [base 10]

      Like

      • Ruud's avatar

        Ruud 6:14 am on 28 April 2025 Permalink | Reply

        Brute force one liner:

        import itertools
        
        print(
            *(
                f"{d}{a}{y}"
                for s, n, d, a, y, t, i, m, e in itertools.permutations(range(2, 11), 9)
                if s == abs(1 - n) and 1 == abs(d - a) and n == abs(a - y) and d == abs(t - i) and a == abs(i - m) and y == abs(m - e)
            )
        )
        

        Like

    • Frits's avatar

      Frits 10:51 am on 26 April 2025 Permalink | Reply

      #    S
      #   U N                            U = 1
      #  D A Y
      # T I M E
      
      sols = set()
      U, mx = 1, 10
      r0 = set(range(2, mx + 1)) # remaining digits to use
      
      for N in range(2, mx - 1):  # no using <mx - 1> and <mx> on this level
        S = N - U
        r1 = r0 - {N, S}
        if len(r1) != len(r0) - 2: continue
        for A in r1 - {mx}:       
          r2 = r1 - {A}
          for D in (A - U, A + U):
            if D not in r2 or D == mx: continue
            r3 = r2 - {D}
            for Y in (A - N, A + N):
              if Y not in r3 or Y == mx: continue
              r4 = r3 - {Y}
              if max(r4) - min(r4) < max(D, A, Y): continue
              for I in r4:
                for M in (I - A, I + A):
                  if M not in r4: continue
                  for T in (I - D, I + D):
                    if T not in r4: continue
                    for E in (M - Y, M + Y):
                      if {T, I, M, E} != r4: continue
                      sols.add(100 * D + 10 * A + Y)
      
      print("answer(s):", ' or '.join(str(s) for s in sols))
      

      Like

  • Unknown's avatar

    Jim Randell 8:18 am on 22 April 2025 Permalink | Reply
    Tags:   

    Teaser 2550: Cricket averages 

    From The Sunday Times, 7th August 2011 [link] [link]

    Playing for our local team, Sam and Oliver between them took 5 wickets in each innings, taking 5 wickets each overall. Sam’s averages (i.e. runs per wicket) were lower than Oliver’s in both innings, but overall Sam had the higher average. All six averages were single non-zero digits.

    If you knew Sam’s overall average, it would then be possible to calculate the number of runs scored against Sam and against Oliver.

    What were the total runs scored against: (a) Sam and (b) Oliver?

    [teaser2550]

     
    • Jim Randell's avatar

      Jim Randell 8:19 am on 22 April 2025 Permalink | Reply

      This Python program chooses possible numbers of wickets taken by each bowler in each innings, and then considers possible averages such than Sam has a lower average than Oliver in each innings, but a higher average overall.

      From the possible candidate scenarios we then looks for situations where knowing Sam’s overall average allows you to determine the overall number of runs scored against each bowler.

      It runs in 65ms. (Internal runtime is 2.2ms).

      from enigma import (Record, decompose, subsets, irange, div, filter_unique, printf)
      
      # generate possible scenarios
      def generate():
        # choose numbers of wickets taken by S and O in the first innings
        for (wS1, wO1) in decompose(5, 2, increasing=0, sep=0, min_v=1):
          # numbers of wickets taken in the second innings
          (wS2, wO2) = (5 - wS1, 5 - wO1)
      
          # S and O's averages in the first innings (single digit numbers)
          for (aS1, aO1) in subsets(irange(1, 9), size=2):
            # calculate runs against S and O in the first innings
            (rS1, rO1) = (aS1 * wS1, aO1 * wO1)
      
            # averages in the second innings
            for (aS2, aO2) in subsets(irange(1, 9), size=2):
              # calculate runs against S and O in the second innings
              (rS2, rO2) = (aS2 * wS2, aO2 * wO2)
      
              # overall averages
              (orS, orO) = (rS1 + rS2, rO1 + rO2)
              (oaS, oaO) = (div(orS, 5), div(orO, 5))
              if oaS is None or oaO is None or not (oaS > oaO): continue
      
              # output: first innings; second innings; totals
              printf("[1: S={wS1}w for {rS1}r avg={aS1}, O={wO1}w for {rO1}r avg={aO1}; 2: S={wS2}w for {rS2}r avg={aS2}, O={wO2}w for {rO2}r avg={aO2}; T: S=5w for {orS}r avg={oaS}, O=5w for {orO}r avg={oaO}]")
              yield Record(
                wS1=wS1, wO1=wO1, rS1=rS1, rO1=rO1, aS1=aS1, aO1=aO1,
                wS2=wS2, wO2=wO2, rS2=rS2, rO2=rO2, aS2=aS2, aO2=aO2,
                orS=orS, orO=orO, oaS=oaS, oaO=oaO,
              )
      
      # if you knew oaS, then you could determine (orS, orO)
      rs = filter_unique(generate(), (lambda r: r.oaS), (lambda r: (r.orS, r.orO)))
      for r in rs.unique:
        printf("S={r.orS}r O={r.orO}r [{r}]")
      

      Solution: (a) 35 runs were scored against Sam; (b) 25 runs were scored against Oliver.

      Here is one scenario:

      First innings:
      Sam took 1 wicket, for 3 runs (avg = 3/1 = 3).
      Oliver took 4 wickets, for 16 runs (avg = 16/4 = 4).

      Second innings:
      Sam took 4 wickets, for 32 runs (avg = 32/4 = 8).
      Oliver took 1 wicket, for 9 runs (avg = 9/1 = 9).

      Overall:
      Sam took 5 wickets, for 35 runs (avg = 35/5 = 7).
      Oliver took 5 wickets, for 25 runs (avg = 25/5 = 5).

      In each innings Sam has a lower average than Oliver, but overall Oliver has a lower average than Sam.

      There is a second scenario, with the innings in the opposite order.

      Like

    • Frits's avatar

      Frits 3:35 pm on 22 April 2025 Permalink | Reply

      Inventing the wheel again to determine a duplicate entry. I didn’t feel like grouping entries per overall average.

      from enigma import SubstitutedExpression
      
      # the alphametic puzzle
      p = SubstitutedExpression(
        [ # Sam:    1st: A wickets, avg B, 2nd: C wickets, avg D, overall avg E
          # Oliver: 1st: P wickets, avg Q, 2nd: R wickets, avg S, overall avg T
          
          # between them took 5 wickets in each innings and taking 
          # 5 wickets each overall (assume Sam took more wickets in the 2nd inning)
          "5 - A = P", "5 - P = R", "5 - R = C",
          "A < C", "A + C == 5", 
          # Sam had lower averages ...
          "B < Q", "D < S",
          # total averages"
          "div(trs := A * B + C * D, 5) = E",
          "div(tro := P * Q + R * S, 5) = T",
          # ... but a higher overall average
          "E > T",
        ],
        answer="E, (trs, tro)",
        d2i=dict([(0, "ACPRQS")] +
                 [(k, "ACPR") for k in range(5, 9)] +
                 [(9, "ACPRBD")]),
        distinct="",
        verbose=0,    # use 256 to see the generated code
      )
      
      c, prev, sols = 0, (-1, -1), set()
      # get unique values of Sam's overall average
      for (soa, tr) in sorted(p.answers()) + [(-1, prev)]:
        if soa != prev[0]:
          if c == 1:
            sols.add(prev[1])
          c = 1
        else:
          c += 1
        prev = (soa, tr)
      
      # print total runs
      for trS, trO in sols:  
        print(f"answer: a() = {trS} runs, b() = {trO} runs")
      

      Like

  • Unknown's avatar

    Jim Randell 6:59 am on 20 April 2025 Permalink | Reply
    Tags:   

    Teaser 3265: Easter prayer 

    From The Sunday Times, 20th April 2025 [link] [link]

    Millions of people will today turn their thoughts to those throughout the world whose lives are made miserable by the ravages of war and bitter conflict. I have taken a selection of letters from the alphabet and given each a different single-digit value. In this way the two words that form the title of this teaser represent two six-digit numbers, one of which is a factor of the other.

    There are two letters in EASTER PRAYER for which the following is true. If I told you the value of that letter alone, you wouldn’t be able to work out all the others, but if I told you both their values then you could work out all the values.

    What is the value of PRAYER?

    [teaser3265]

     
    • Jim Randell's avatar

      Jim Randell 7:49 am on 20 April 2025 Permalink | Reply

      This Python program uses the [[ SubstitutedExpression ]] solver from the enigma.py library to find solutions to the alphametic puzzle, and then looks for a pair of symbols that allow a unique solution to be determined if both their values are known, but not if only one of their values is known.

      It runs in 155ms. (Internal runtime is 72ms).

      from enigma import (SubstitutedExpression, filter_unique, singleton, subsets, translate, printf)
      
      # find solutions to the alphametic puzzle
      expr = "EASTER % PRAYER == 0 or PRAYER % EASTER == 0"
      tmpl = "[EASTER={EASTER} PRAYER={PRAYER}]"
      p = SubstitutedExpression(expr, template=tmpl)
      ss = list(p.solve(verbose="T"))
      
      # for each symbol record values with multiple solutions
      rs = set()
      for k in p.symbols:
        for s in filter_unique(ss, (lambda s: s[k])).non_unique:
          rs.add((k, s[k]))
      
      # look for pairs that give a unique solution
      for ((k1, v1), (k2, v2)) in subsets(rs, size=2):
        if k1 == k2: continue
        s = singleton(s for s in ss if s[k1] == v1 and s[k2] == v2)
        if s is None: continue
      
        # output solution
        (EASTER, PRAYER) = (translate(x, s) for x in ["EASTER", "PRAYER"])
        printf("{k1}={v1} {k2}={v2} -> EASTER={EASTER} PRAYER={PRAYER}")
      

      Solution: PRAYER = 159275.

      There are 3 candidate solutions:

      EASTER = 796375; PRAYER = 159275
      EASTER = 796875; PRAYER = 159375
      EASTER = 798375; PRAYER = 159675

      In each case EASTER = 5 × PRAYER.

      And if we were told either of S = 6 or T = 3 we could only narrow the candidates down to 2 solutions. But if we are told both together, then we can narrow the candidates down to a single solution (the first one).

      Like

    • Frits's avatar

      Frits 1:49 pm on 20 April 2025 Permalink | Reply

      As usual built for speed.
      Always difficult to code a branch that is not easy to test.

      from itertools import permutations, combinations, product
      from functools import reduce
      
      # digits to number
      d2n = lambda *s: reduce(lambda x,  y: 10 * x + y, s)
      # return <n>th digit of number <num>, n has to be > 0
      nth = lambda num, n, ln=1: int(str(num)[n - 1:n - 1 + ln])
      
      # EASTER % PRAYER == 0 or PRAYER % EASTER == 0 
      cands = []
      for E, R in permutations(range(10), 2):  
        if not E: continue
        ER = d2n(E, R)
        for m in range(2, 10):                   # multiplier
          if (m * ER) % 100 != ER: continue
          
          if E >= m:  # EASTER might be the numerator
            for P in [n for n in range(1, E // m + 1) if n not in {E, R}]:
              PR = d2n(P, R)
              # check if m * PR9999 can become at least Exxxxx
              if nth(m * (PR + 1), 1) < E: continue
              for A in set(range(10)) - {E, R, P}:
                PRA = d2n(P, R, A)
                EA = d2n(E, A)
                # check if m * PRA999 can become at least EAxxxx
                if nth(m * (PRA + 1), 1, 2) < EA: continue
                for Y in set(range(10)) - {E, R, P, A}:
                  PRAYER = d2n(P, R, A, Y, E, R)
                  EASTER = m * PRAYER
                  if nth(EASTER, 1, 2) != EA: continue
                  S = nth(EASTER, 3)
                  T = nth(EASTER, 4)
                  if S == T or not {E, R, P, A, Y}.isdisjoint({S, T}): continue
                  cands.append(([A, E, P, R, S, T, Y]))
                  
          if m * E < 10:  # EASTER might be the denominator 
            for P in range(m * E, 10):
              if P == R: continue
              PR = d2n(P, R)
              for A in set(range(10)) - {E, R, P}:
                EA = d2n(E, A)
                # check if m * EA9999 can become at least PRxxxx
                if nth(m * (EA + 1), 1, 2) < PR: continue
                PRA = d2n(P, R, A)
                for S in set(range(10)) - {E, R, P, A}:
                  EAS = d2n(E, A, S)
                  # check if m * EAS999 can become at least PRAxxx
                  if nth(m * (EAS + 1), 1, 3) < PRA: continue
                  for T in set(range(10)) - {E, R, P, A, S}:
                    EASTER = d2n(E, A, S, T, E, R)
                    PRAYER = m * EASTER
                    if nth(PRAYER, 1, 3) != PRA: continue
                    Y = nth(PRAYER, 4)
                    if Y in {E, R, P, A, S, T}: continue
                    cands.append(([A, E, P, R, S, T, Y]))
      
      if not cands:
        print("no solution")
      
      # positions of letters that have multiple values (but not all different)
      pos = [j  for j in range(7) if 1 < len({c[j] for c in cands}) < len(cands)]
      
      # choose two of those letters
      for a, b in combinations(pos, 2):
        # collect letter values ...
        lv = [[c[x] for c in cands] for x in (a, b)]
        # ... that occur more than once
        lv = [{x for i, x in enumerate(v) if x in v[i + 1:]} for v in lv]
        # choose for each letter a value
        for p1, p2 in product(*lv):
          # corresponding candidates
          if len(cs := [c for c in cands if c[a] == p1 and c[b] == p2]) != 1: 
            continue 
          # select PRAYER
          print("answer:", ''.join(str(cs[0][i]) for i in [2, 3, 0, 6, 1, 3]))
      

      Like

  • Unknown's avatar

    Jim Randell 10:08 am on 17 April 2025 Permalink | Reply
    Tags:   

    Teaser 2535: Eastertide 

    From The Sunday Times, 24th April 2011 [link] [link]

    In the following division sum I have consistently replaced digits with letters, with different letters used for different digits. All three numbers featured are even:

    EASTER ÷ TIDE = EGG

    What is the numerical value of my TEASER?

    [teaser2535]

     
    • Jim Randell's avatar

      Jim Randell 10:08 am on 17 April 2025 Permalink | Reply

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

      The following command line executes in 83ms. (Internal runtime of the generated code is 5.6ms).

      % python3 -m enigma SubstitutedExpression "EGG * TIDE = EASTER" --answer="TEASER"
      (EGG * TIDE = EASTER) (TEASER)
      (244 * 1062 = 259128) (125928) / A=5 D=6 E=2 G=4 I=0 R=8 S=9 T=1
      TEASER = 125928 [1 solution]
      

      Solution: TEASER = 125928.

      Like

  • Unknown's avatar

    Jim Randell 9:06 am on 15 April 2025 Permalink | Reply
    Tags:   

    Brain-Teaser 380: [Feeler gauges] 

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

    “Feeler gauges”, says Bell at the pub, “are remarkably  interesting things. You’ve all seen garage men use them — fingers of steel fastened together at one end and you can fan them out if you want. When you measure a space you find out which lot of fingers fits in. The thicknesses are marked in thous, that’s thousandths of an inch”.

    “I’ve got a set of five gauges here which will measure any number up to 13 thous, always using either a single gauge or a touching set of them. You don’t need to use gauges which aren’t touching for a measurement like you do with the usual gauges. So mine’s better. I might patent it and make a packet”.

    Clark examined it for few minutes and said “Not bad at all. I notice you could add an extra gauge in front and then your set would give all measurements up to 18 thous”.

    How would the six gauges then read starting at the top?

    This puzzle was originally published with no title.

    [teaser380]

     
    • Jim Randell's avatar

      Jim Randell 9:08 am on 15 April 2025 Permalink | Reply

      See also: Teaser 119, Teaser 560, Teaser 777, BrainTwister #61.

      I reused the code to generate sparse rulers that I wrote for Teaser 119.

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

      from enigma import (irange, diff, append, tuples, csum, subsets, printf)
      
      # generate sparse rulers
      # L = length
      # M = number of marks
      # ms = existing marks (set)
      # k = number of remaining marks
      # xs = distances not yet satisfied (ordered list)
      # zl = left zone
      # zr = right zone
      def rulers(L, M, ms, k, xs, zl, zr):
        if k == 0:
          if not xs:
            yield sorted(ms)
        else:
          # perform some early termination checks:
          # are there too many unsatisfied differences remaining?
          if xs and len(xs) > (k * (k - 1)) // 2 + k * (M - k): return
          # is max distance too big?
          if xs and xs[-1] > max(zr, L - zl): return
          # can we fit k marks in the gaps?
          if zr - zl + 1 < k: return
      
          # extend left zone?
          if not (zl > L - zr):
            # try with mark at zl
            ds = set(abs(m - zl) for m in ms)
            yield from rulers(L, M, append(ms, zl), k - 1, diff(xs, ds), zl + 1, zr)
            # try without mark at zl
            yield from rulers(L, M, ms, k, xs, zl + 1, zr)
          else:
            # try without mark at zr
            yield from rulers(L, M, ms, k, xs, zl, zr - 1)
            # try with mark at zr
            ds = set(abs(m - zr) for m in ms)
            yield from rulers(L, M, append(ms, zr), k - 1, diff(xs, ds), zl, zr - 1)
      
      # generate rulers that can measure 1..N
      sparse_rulers = lambda L, M, N: rulers(L, M, {0, L}, M - 2, diff(irange(1, N), [L]), 1, L - 1)
      
      # consider possible sparse rulers with length L, to measure 1 - 13
      for L in irange(13, 19):
        for ss in sparse_rulers(L, 6, 13):
          # calculate gauge thicknesses
          ts = list(b - a for (a, b) in tuples(ss, 2))
      
          # look for an additional gauge to make all thicknesses up to 18
          for x in irange(max(18 - L, 1), 17):
            ts_ = [x] + ts
            # calculate all measures
            ms_ = set(y - x for (x, y) in subsets(csum(ts_, empty=1), size=2))
            if ms_.issuperset(irange(1, 18)):
              # output solution
              printf("[1..13] = {ts} -> [1..18] = {ts_}")
      

      Solution: The six gauges are: 14, 1, 3, 6, 2, 5.

      Gauges [1, 3, 6, 2, 5] can be use to measure 1..13 (as well as 16, 17). It corresponds to a sparse ruler of length 17 with 6 marks.

      Adding a gauge of 14 we get the set [14, 1, 3, 6, 2, 5], which can measure 1..18 (as well as 24, 26, 31). Which corresponds to a sparse ruler of length 31 with 7 marks.

      Each of the gauges in the original 5-set has a different thickness, and there is another 5-set that would also work:

      [1, 7, 3, 2, 4], which can measure 1..13 (as well as 16, 17).

      But it cannot be extended to a 6-set that measures 1..18.

      Like

    • Frits's avatar

      Frits 12:08 pm on 17 April 2025 Permalink | Reply

      Assuming that the first five gauges are single digit.
      Instead of “SubstitutedExpression” also “decompose” could have been used to find five numbers that add up to “L”.

      from enigma import SubstitutedExpression
      from itertools import combinations, accumulate
      
      # can we make numbers 1 - <n> with sequence <s> using "touching" sets 
      def check(s, n):
        # calculate all measures
        s_ = set(y - x for (x, y) in combinations(accumulate([0] + s), 2))
        return s_.issuperset(range(1, n + 1))
      
      # the alphametic puzzle
      p = SubstitutedExpression(
        [ # we have to make numbers 1 - 13 with a touching set of gauges A, B, C, D, E
          "A + B + C + D < 19",
          "12 < A + B + C + D + E < 20",  # Jim: 13 <= L <= 19
          "check([A, B, C, D, E], 13)",
          
          # we have to make numbers 1 - 18 with a touching set of gauges XY, A, B, C, D, E
          "XY < 18",                  # Jim: 18 - L <= XY <= 17
          "check([XY, A, B, C, D, E], 18)"
        ],
        answer="[XY, A, B, C, D, E]",
        d2i=dict([(0, "ABCDE")] +
                 [(k, "X") for k in range(2, 10)]),
        distinct="",
        env=dict(check=check),
        reorder=0,
        verbose=0,    # use 256 to see the generated code
      )
      
      # print answers
      for ans in p.answers():
        print(f"{ans}")
      

      Like

      • Frits's avatar

        Frits 12:41 pm on 17 April 2025 Permalink | Reply

        The decomposition version (without assuming single digits).

        from itertools import combinations, accumulate
        
        # can we make numbers 1 - <n> with sequence <s> using "touching" sets 
        def check(s, n):
          # calculate all measures
          s_ = set(y - x for (x, y) in combinations(accumulate([0] + s), 2))
          return s_.issuperset(range(1, n + 1))
          
        # decompose <t> into <k> numbers from <ns> so that sum(numbers) equals <t>
        def decompose(t, k, ns, s=[]):
          if k == 1:
            if t in ns:
              yield s + [t]
          else:
            for n in ns:
              # <ns> must be a sorted list
              if n > t - (k - 1) * ns[0]: break
              yield from decompose(t - n, k - 1, ns, s + [n])
        
        # consider possible sparse rulers with length L, to measure 1 - 13
        for L in range(13, 20):
          # find five numbers that up to <L>
          for s in decompose(L, 5, range(1, L - 3)):
            # can we make numbers 1 - 13 with sequence <s> using "touching" sets
            if not check(s, 13): continue
            # look for an additional gauge to make all thicknesses up to 18
            for x in range(max(1, 18 - L), 18):
              # can we make numbers 1 - 18 using "touching" sets 
              if not check([x] + s, 18): continue
              # output solution
              print(f"[1..13] = {s} -> [1..18] = {[x] + s}")  
        

        Like

  • Unknown's avatar

    Jim Randell 6:53 am on 13 April 2025 Permalink | Reply
    Tags:   

    Teaser 3264: Saving rum 

    From The Sunday Times, 13th April 2025 [link] [link]

    Captain Noah was tired of spilling his rum at sea. His glass (twice the density of the rum) is a hollow cylinder with a solid flat base. The outer diameter is 72 mm. The glass thickness and base height are both 4 mm. After testing the stability of the upright glass, he instructed the steward to keep his glass topped up to one-third full, where it has the lowest centre of mass.

    What is the overall height of the glass (including the base)?

    [teaser3264]

     
    • Jim Randell's avatar

      Jim Randell 8:03 am on 13 April 2025 Permalink | Reply

      Here is a numerical solution, using the [[ find_min() ]] function from the enigma.py library.

      I treat the glass and rum system as 3 separate components:

      a hollow cylinder of glass, with external diameter 72 mm (external radius = 36 mm) and 4 mm thick walls (internal radius = 32 mm), and height h.

      a solid disc of glass, with diameter 72 mm (radius = 36 mm) and height 4 mm, that sits underneath the cylinder, on a flat table.

      a cylinder of rum with radius 32 mm, and height x (between 0 and h), that sits on the disc, inside the cylinder. The density of the rum is 1/2 that of the glass.

      The individual centres of mass of the three components are then combined to give the combined centre of mass. Each of them is somewhere on the central line of the cylinder, so we only need to worry about their heights above the table.

      For a given value of h we can calculate the value of x that gives the minimum height for the combined centre of mass, and then look for a value of h where h = 3x.

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

      from enigma import (sq, fdiv, find_min, find_value, printf)
      
      # given height h find height of rum that gives the minimal centre of mass
      def fn(h):
      
        # calculate centre of mass for rum height x
        def f(x):
          # mass of cylinder, base, rum (half the density)
          mc = (sq(36) - sq(32)) * h * 2
          mb = sq(36) * 4 * 2
          mr = x * sq(32)
          # heights of their respective centres of mass
          hc = 4 + h * 0.5
          hb = 2
          hr = 4 + x * 0.5
          # calculate height of combined centre of mass
          return fdiv(mc * hc + mb * hb + mr * hr, mc + mb + mr)
      
        # find minimal centre of mass = x
        r = find_min(f, 0, h)
        x = r.v
        # return h/x
        return fdiv(h, x)
      
      # find when h/x = 3 (i.e. the minimal centre of mass is when the glass is 1/3 full)
      r = find_value(fn, 3, 20, 200)
      # output overall glass height (including base = 4 mm)
      h = r.v + 4
      printf("height = {h:.2f} mm")
      

      Solution: The glass is 112 mm high.

      So the internal height is 108 mm, and the lowest centre of mass occurs with 36 mm of rum in the glass.

      We can get an exact answer to the puzzle using analysis.

      Like

      • Jim Randell's avatar

        Jim Randell 1:14 pm on 15 April 2025 Permalink | Reply

        Here is my analytical solution:

        The CoM of the cylinder is in the centre of the cylinder, and adding the base will bring the CoM down a little.

        Suppose we then start adding rum to the glass:

        We start by adding a little rum. This forms a layer of rum below the current CoM of the glass, so the combined CoM moves down a bit. And this continues until the combined CoM is level with the top of the rum. At this point, when we add additional rum, it forms a layer above the current CoM, and so the combined CoM starts to move upwards.

        Hence the combined CoM is at a minimum when it is level with the top of the rum.

        So we are interested in when:

        ∑(m[i] . h[i]) / ∑(m[i]) = x + 4

        ∑(m[i] . h[i]) = (x + 4) ∑(m[i])

        And we want to find solutions when h = 3x, which we can do manually, or use a program.

        from enigma import (Polynomial, rdiv, sq, printf)
        
        x = Polynomial('x')
        h = 3 * x
        half = rdiv(1, 2)
        
        # mass of cylinder, base, rum (half the density)
        mc = (sq(36) - sq(32)) * h * 2
        mb = sq(36) * 4 * 2
        mr = x * sq(32)
        # heights of their respective centres of mass
        hc = 4 + h * half
        hb = 2
        hr = 4 + x * half
        
        # polynomial to solve: sum(m[i] * h[i]) = (x + 4) * sum(m[i])
        p = (mc * hc + mb * hb + mr * hr) - (x + 4) * (mc + mb + mr)
        p = p.scale()
        printf("[p = {p}]")
        
        # find real, positive solutions for p(x) = 0
        for x in p.roots(domain='F', include='+'):
          h = 3 * x
          height = h + 4
          printf("height = {height} mm [x={x} h={h}]")
        

        Manually we find the resulting polynomial p(x) factorises as follows:

        p(x) = 19x^2 − 648x − 1296
        p(x) = (19x + 36)(x − 36)

        From which we see there is a single positive real root that is an integer, and this provides an integer answer to the puzzle.


        Or we can use calculus to determine when the minimum height centre of mass occurs:

        For a glass with an internal height h and a depth of rum x we can calculate the centre of mass as:

        f(x) = (544h(4 + h/2) + 10368 . 2 + 1024x(4 + x/2)) / (544h + 10368 + 1024x)
        f(x) = (17h^2 + 136h + 32x^2 + 256x + 1296) / (34h + 64x + 648)

        And we can calculate when the derivative of this function with respect to x is 0, using the quotient rule:

        f(x) = p(x) / q(x)

        f'(x) = (p'(x) q(x) − p(x) q'(x)) / q(x)^2

        So the minimum value occurs when p’ q = p q’.

        In this case:

        p(x) = (17h^2 + 136h + 32x^2 + 256x + 1296)
        p'(x) = 64x + 256

        q(x) = (34h + 64x + 648)
        q'(x) = 64

        (64x + 256)(34h + 64x + 648) = (17h^2 + 136h + 32x^2 + 256x + 1296)(64)

        32x^2 + (648 + 34h)x + (1296 − 17h^2) = 0

        And we want to find the minimum value when h = 3x:

        32x^2 + (648 + 34 . 3x)x + (1296 − 17 . (3x)^2) = 0

        19x^2 − 648x − 1296 = 0
        (19x + 36)(x − 36) = 0

        x = 36
        h = 108

        And the answer to the puzzle follows directly.

        Like

  • Unknown's avatar

    Jim Randell 8:32 am on 11 April 2025 Permalink | Reply
    Tags:   

    Teaser 2557: The legacy 

    From The Sunday Times, 25th September 2011 [link] [link]

    In my will I have set aside a five-digit sum of pounds for charity — a group of four animal charities and a group of seven children’s charities. This five-digit sum consists of five consecutive non-zero digits, not in numerical order. On my death this legacy is to be divided equally between these charities. But I have left it to my executors’ discretion to choose to divide it instead equally among the charities in just one of the groups. Each donation will be a whole number of pounds, whichever course they take.

    What is the five-digit sum?

    [teaser2557]

     
    • Jim Randell's avatar

      Jim Randell 8:32 am on 11 April 2025 Permalink | Reply

      The amount must be divisibly by 4, 7, 11, and LCM(4, 7, 11) = 308.

      So we are looking for a 5-digit number that is a multiple of 308, and is composed from a set of 5 different non-zero consecutive digits, but not in numerical order.

      This Python program looks at 5-digit multiples of 308. It has a runtime of 63ms, and an internal runtime of 1.7ms.

      from enigma import (irange, mlcm, nsplit, is_consecutive, printf)
      
      # look for 5-digit multiples of 4, 7, 11
      k = mlcm(4, 7, 11)
      for n in irange.round(10000, 99999, step=k, rnd='I'):
        # split into digits
        ds = nsplit(n)
        # look for consecutive non-zero digits
        if (0 in ds) or is_consecutive(ds) or not is_consecutive(sorted(ds)): continue
        # output solution
        printf("amount = {n}")
      

      But it is slightly faster (but also slightly longer) to start with the possible non-zero consecutive digits, and then consider rearrangements of those digits.

      This Python program also runs in 63ms, but has an internal runtime of 784µs.

      from enigma import (irange, mlcm, tuples, subsets, nconcat, is_consecutive, printf)
      
      # amount must be divisible by 4, 7, 11
      k = mlcm(4, 7, 11)
      
      # choose 5 consecutive digits
      for ds in tuples(irange(1, 9), 5):
        # consider possible numbers formed from these digits
        for ds in subsets(ds, size=len, select='P'):
          n = nconcat(ds)
          # check divisibility
          if not (n % k == 0): continue
          # check the digits are not consecutive
          if is_consecutive(ds): continue
          # output solution
          printf("amount = {n}")
      

      Solution: The amount is: £ 74536.

      £ 74536 = 4× £ 18634
      £ 74536 = 7× £ 10648
      £ 74536 = 11× £ 6776

      If a 0 digit is permitted there is a second solution of £ 43120.

      Like

    • Frits's avatar

      Frits 4:59 pm on 11 April 2025 Permalink | Reply

      Using divisibility rules.

      from itertools import permutations
      
      # suppose d1 + d3 + d5 = d2 + d4 + k   
      # then k % 11 = 0 as legacy is divisible by 11 (alternating sum)
      # t = sum(d1...d5) = 2 * (d2 + d4) + k
      
      for i, t in enumerate(range(15, 36, 5), 1):
        # it t odd then k = 11, it t even then k = 0
        k = 11 if t % 2 else 0
        d2d4 = (t - k) // 2
        if d2d4 < 2 * i + 1 or d2d4 > 2 * i + 7: continue
        for d2 in range(i, i + 5):
          d4 = d2d4 - d2
          if d4 == d2 or d4 not in range(i, i + 5): continue
          for d1, d3, d5 in permutations(set(range(i, i + 5)) - {d2, d4}):
            if d5 % 2: continue
            n45 = 10 * d4 + d5
            # divisibility by 4
            if n45 % 4: continue
            # divisibility by 7 rule (alternating sum of blocks of three)
            if (100 * d3 + n45 - 10 * d1 - d2) % 7: continue 
      
            print("answer:", ''.join(str(x) for x in (d1, d2, d3, d4, d5)))
      

      Like

  • Unknown's avatar

    Jim Randell 8:27 am on 8 April 2025 Permalink | Reply
    Tags:   

    Teaser 2547: Multiple celebration 

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

    Today is my birthday and the birthday of my granddaughter Imogen. My age today is a whole-number multiple of hers, and this has been true on one third of our joint anniversaries.

    If we both live until I am six times her current age, then my age will be a multiple of hers on two more birthdays.

    How old are we today?

    [teaser2547]

     
    • Jim Randell's avatar

      Jim Randell 8:27 am on 8 April 2025 Permalink | Reply

      This Python program runs in 63ms. (Internal runtime is 139µs).

      from enigma import (irange, is_divisor, printf)
      
      # find multiple anniversaries for [a, b] and [a+d, b+d]
      def multiples(d, a, b):
        for x in irange(a, b):
          y = x + d
          if is_divisor(y, x):
            yield (x, y)
      
      # consider possible ages for the granddaughter (must be a multiple of 3)
      for g in irange(3, 20, step=3):
        # consider the setters age (a multiple of g, less than 6g)
        for k in [2, 3, 4, 5]:
          s = k * g
      
          # calculate earlier multiples
          d = s - g
          ms = list(multiples(d, 1, g))
          # exactly 1/3 of the joint anniversaries so far are multiples
          if not (len(ms) * 3 == g): continue
      
          # and in there future there will be 2 more multiple anniversaries
          ms_ = list(multiples(d, g + 1, 6 * g - d))
          if not (len(ms_) == 2): continue
      
          # output solution
          printf("g={g} s={s} [k={k} d={d}] -> {ms} {ms_}")
      

      Solution: The setter is 72. The granddaughter is 18.

      So the age difference is 54 years.

      The multiples in the past are:

      1 year old & 55 years old (multiple = 55)
      2 years old & 56 years old (multiple = 28)
      3 years old & 57 years old (multiple = 19)
      6 years old & 60 years old (multiple = 10)
      9 years old & 63 years old (multiple = 7)
      18 years old & 72 years old (multiple = 4)

      Which is 6 anniversaries out of 18, so one third of them are multiples.

      In the future (with the setters age up to 6 × 18 = 108) we can have the following multiples:

      27 years old & 81 years old (multiple = 3)
      54 years old & 108 years old (multiple = 2)

      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