Updates from Jim Randell Toggle Comment Threads | Keyboard Shortcuts

  • Unknown's avatar

    Jim Randell 9:05 am on 27 April 2021 Permalink | Reply
    Tags:   

    Brain-Teaser 744: Job prediction 

    From The Sunday Times, 19th October 1975 [link]

    Professor Knowall has recently become very interested in dreams.

    A friend of mine has been doing some research about the extent to which dreams can be used foretell the future. He persuaded his five employees (Alf, Bert, Charlie, Duggie and Ernie) to help him in his enquiries. They liked the idea of being in the forefront of anything new and their beds seemed a nice cosy place for research.

    They met a year ago, and predicted their jobs now. Thus:

    Alf: “Ernie will not be the Door-Opener.”
    Bert: “Duggie will not be the Bottle-Washer.”
    Charlie: “Alf will not be the Welfare Officer.”
    Duggie: “Ernie will be the Bottle-Washer.”
    Ernie: “Bert’s prediction will be true.”

    Their jobs now are those of Bottle-Washer, Welfare Officer, Door-Shutter, Door-Opener and Worker.

    The Professor was most interested in this

    “But, my dear Sergeant Bungle”, he said, “how many of these predictions were correct and who made them?”

    In fact only two of the predictions were correct and they were made by the men who became the Welfare Officer and the Worker.

    What are all their jobs now?

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

    [teaser744]

     
    • Jim Randell's avatar

      Jim Randell 9:05 am on 27 April 2021 Permalink | Reply

      This Python program runs in 49ms.

      Run: [ @replit ]

      from enigma import subsets, printf
      
      jobs = ("BW", "WO", "DS", "DO", "W")
      
      # consider assignments of jobs
      for (A, B, C, D, E) in subsets(jobs, size=len, select="P"):
      
        # evaluate the statements:
        # A: "E will not be DO"
        sA = (E != "DO")
        # B: "D will not be BW"
        sB = (D != "BW")
        # C: "A will not be WO"
        sC = (A != "WO")
        # D: "E will be BW"
        sD = (E == "BW")
        # E: "B's statement is true"
        sE = (sB == True)
      
        # only 2 of the statements are true, and they were made by WO and W
        ts = set(j for (s, j) in zip([sA, sB, sC, sD, sE], [A, B, C, D, E]) if s)
        if ts != {"WO", "W"}: continue
      
        # output solution
        printf("A={A} B={B} C={C} D={D} E={E}")
      

      Solution: Alf is the Worker. Bert is the Door-Opener. Charlie is the Welfare Officer. Duggie is the Bottle-Washer. Ernie is the Door-Shutter.

      Like

    • Frits's avatar

      Frits 10:18 am on 28 April 2021 Permalink | Reply

      from enigma import SubstitutedExpression
      
      # BW = 1, WO = 2, DS = 3, DO = 4, W = 5
      jobs = ("n/a", "BW", "WO", "DS", "DO", "W")
      
      # the alphametic puzzle
      p = SubstitutedExpression( [
        # A: "E will not be DO"
        "(E != 4) = V",
        # B: "D will not be BW"
        "(D != 1) = W",
        # C: "A will not be WO"
        "(A != 2) = X",
        # D: "E will be BW"
        "(E == 1) = Y",
        # E: "B's statement is true"
        "(W == 1) = Z",
        
        # only 2 of the statements are true, and they were made by WO and W
        "sorted([V * A, W * B, X * C, Y * D, Z * E]) == [0, 0, 0, 2, 5]",
        ],
        answer="A, B, C, D, E",
        # let values range from 1-5 so the sorted formula is enough 
        # to check that only 2 of the statements are true
        digits=range(0, 6),
        d2i=dict([(0, "ABCDE")]),
        distinct="ABCDE",
        verbose=256
      )
      
      # print answers
      for (_, ans) in p.solve():
        for i, a in enumerate(ans):
          print(f"{'ABCDE'[i]}={jobs[a]}", end=" ")
        print()  
      
      # A=W B=DO C=WO D=BW E=DS  
      

      Like

      • Frits's avatar

        Frits 10:48 am on 28 April 2021 Permalink | Reply

         
        If E is true:
          B is true
            A is false
              E = DO
              DO talks the truth -- > incorrect
        Else:
          E is false 
            B is false
            D = BW
              D is false
                A is true
                C is true
                  A = W
                  C = WO
                  E = DS
                  B = DO
        

        Like

        • John Crabtree's avatar

          John Crabtree 4:30 pm on 29 April 2021 Permalink | Reply

          One does not need to know D’s prediction to solve the teaser. It must be false, which in fact it is.

          Like

  • Unknown's avatar

    Jim Randell 4:55 pm on 23 April 2021 Permalink | Reply
    Tags:   

    Teaser 3057: Cut for partners 

    From The Sunday Times, 25th April 2021 [link] [link]

    George and Martha are playing bridge with an invited married couple. Before play starts, the players have to cut for partners. Each player draws a card from a standard pack and those drawing the two highest-ranking cards play together against the other two. For this purpose, the rank order is ♠ A, ♥ A, ♦ A, ♣ A, ♠ K, ♥ K etc. down to ♦ 3, ♣ 3, ♠ 2, ♥ 2, ♦ 2, ♣ 2 (the lowest).

    George drew the first card, then Martha drew a lower-ranking card. “That is interesting!” commented the male visitor to his wife. “The probability that we shall play together is now P. Had Martha drawn the ♥ 7 instead of her actual card, that chance would have been reduced to P/2, and had she drawn the ♥ 6, the chance would have been reduced to P/3”.

    Which cards did George and Martha draw?

    [teaser3057]

     
    • Jim Randell's avatar

      Jim Randell 5:30 pm on 23 April 2021 Permalink | Reply

      The equations can be solved manually without too much trouble to give a direct solution, but here is a Python program that solves the puzzle. It runs in 45ms.

      Run: [ @replit ]

      from enigma import P, irange, peek, printf
      
      # the cards in order (highest to lowest)
      cards = list(v + s for v in "AKQJX98765432" for s in "SHDC")
      
      # index of certain cards we are interested in
      (i6H, i7H) = (cards.index(c) for c in ('6H', '7H'))
      
      # count number of pairs where X+Y play together
      # given G chose card index g and M chose card index m
      N = lambda g, m: P(g, 2) + P(51 - m, 2)
      
      # choose a card for G
      for g in irange(0, i7H - 2):
      
        # count pairs if M had chosen 6H or 7H
        ns = { 3 * N(g, i6H), 2 * N(g, i7H) }
        if len(ns) > 1: continue
      
        # choose a card for M
        for m in irange(g + 1, i7H - 1):
      
          # that gives the correct probability
          if N(g, m) in ns:
            # output solution
            printf("G={G} [{g}]; n={n}; M={M} [{m}]", G=cards[g], M=cards[m], n=peek(ns))
      

      Solution: George’s card is A♣. Martha’s card is 9♠.


      Manually:

      There are 52 cards to start with, and after George has chosen one (at index g in the list of cards) there are 51 remaining.

      Martha chooses a lower card than George (at index m in the list of cards), and now there are 50 cards remaining.

      The other couple (X and Y) then choose cards, and if they are both lower valued than Martha’s card, or higher valued than George’s card then they will play together.

      The number of possible (unordered) pairs of cards for X+Y is: P(50, 2) = 2450, but that is the same in all scenarios, so we can just compare the number of pairs which result in X+Y playing together (n = 2450p).

      The number of pairs which are better than George’s card is: P(g, 2)

      And the number of pairs which are worse than Martha’s card is: P(51 − m, 2)

      If Martha had chosen 6♥ (the card at index 33) the probability is p/3, so:

      P(g, 2) + P(51 − 33, 2) = n / 3

      If Martha had chosen 7♥ (the card at index 29) the probability is p/2, so:

      P(g, 2) + P(51 − 29, 2) = n / 2

      Together these solve to give:

      P(22, 2) − P(18, 2) = n / 6
      ⇒ n = 936
      g² − g − 6 = 0
      (g + 2)(g − 3) = 0
      ⇒ g = 3

      So G chose the card at index 3 = A♣.

      (And the probability is p = 936/2450 ≈ 0.382).

      To find Martha’s card:

      P(g, 2) + P(51 − m, 2) = 936
      m² − 101m + 2556 = 936
      m² − 101m + 1620 = 0
      (m − 20)(m − 81) = 0
      ⇒ m = 20

      So M chose the card at index 20 = 9♠.

      Like

    • Frits's avatar

      Frits 11:45 pm on 23 April 2021 Permalink | Reply

      from enigma import div
      
      # chance is numerator / denominator
      # P = lambda g, m: ((m - 1) * (m - 2)  + (52 - g) * (51 - g)) / (50 * 49)
      
      # range cards: 1 - 52
      # H7 is card 23, H6 = 19
      
      suits = ["clubs", "diamonds", "hearts", "spades"]
      nums = ["2", "3", "4", "5", "6", "7", "8", "9", "10", 
              "jack", "queen", "king", "ace"]
        
      # check values for George
      for g in range(25, 53):
        G = (52 - g) * (51 - g)
        
        h7_numerator = (23 - 1) * (23 - 2) + G
        if h7_numerator % 3: continue
        
        h6_numerator = 2 * h7_numerator // 3
        if h6_numerator != (19 - 1) * (19 - 2) + G: continue
        
        m_numerator = 2 * h7_numerator
        
        # (m - 1) * (m - 2) + G = m_numerator, m^2 -3m + (2 + G - m_numerator) = 0
        m = div(3 + (1 + 4 * (m_numerator - G)) ** 0.5, 2)
        if m is None: continue
        m = int(m)
        
        print(f"George and Martha drew {nums[(g - 1) // 4]} of {suits[(g - 1) % 4]}"
              f" and {nums[(m - 1) // 4]} of {suits[(m - 1) % 4]}")
      

      With more analysis:

      from enigma import div
      
      # range cards: 1 - 52
      
      suits = ["clubs", "diamonds", "hearts", "spades"]
      nums = ["2", "3", "4", "5", "6", "7", "8", "9", "10", 
              "jack", "queen", "king", "ace"]
      
      # numerator of P = (m - 1) * (m - 2) + (52 - g) * (51 - g) 
      # P is twice the chance for H7 (card 23)
      
      # (m - 1) * (m - 2) + (52 - g) * (51 - g) = 
      # 2 * ((23 - 1) * (23 - 2) + (52 - g) * (51 - g))
      
      # g = 103 / 2 - ((4 * m^2 - 12*m - 3687) ^ 0.5) / 2
      
      # P is three times the chance for H6 (card 19)
      
      # (m - 1) * (m - 2) + (52 - g) * (51 - g) = 
      # 3 * ((19 - 1) * (19 - 2) + (52 - g) * (51 - g))
      
      # g = 103 / 2 - ((2 * m^2 - 6*m - 1831) ^ 0.5) / 2
      
      # (4 * m^2 - 12*m - 3687) - (2 * m^2 - 6*m - 1831) = 0
      # 2 * m^2 - 6 * m - 1856 = 0
      m = div(6 + (36 + 4 * 2 * 1856) ** 0.5, 4)
      if m is None: exit() 
      m = int(m)
      
      g = div(103 - (4 * m ** 2 - 12 * m - 3687) ** 0.5, 2)
      if g is None: exit() 
      g = int(g)
      
      print(f"George and Martha drew {nums[(g - 1) // 4]} of {suits[(g - 1) % 4]}"
            f" and {nums[(m - 1) // 4]} of {suits[(m - 1) % 4]}")
      

      Like

  • Unknown's avatar

    Jim Randell 9:23 am on 22 April 2021 Permalink | Reply
    Tags:   

    Brain-Teaser 737: Regular as clockwork 

    From The Sunday Times, 31st August 1975 [link]

    Our old watchmaker works weekdays 9:30am to 5pm as regular as, well, clockwork. I recently took there to be regulated two “8-day” striking clocks – the sort which fully wound will go nearly 8 days before stopping; they were keeping different times and each was wrong by an exact number of minutes per day, i.e. less than an hour in either case.

    He immediately wound the clocks fully, set them to the right time (which was an exact number of minutes after the hour) and put them up on a shelf for observation.

    The next Monday, when he went to take down the clocks to start regulating them, he found both of them just starting to strike 8 o’clock simultaneously, which was some hours plus an exact number of minutes past the correct time.

    What day and exact time was it when he originally set them?

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

    [teaser737]

     
    • Jim Randell's avatar

      Jim Randell 9:24 am on 22 April 2021 Permalink | Reply

      A clock that gained 1 hour a day would take 12 days (or 12.5 days clock time) to show the right time (but actually be 12 hours fast). Similarly a clock that lost 1 hour a day would take 12 days (or 11.5 day clock time) to show the right time (but actually be 12 hours slow).

      And the clocks we are interested would run out long before they achieved this.

      But if we look at both the clocks it would only take 6 days for them to both show the same time (i.e. have a difference of 12 hours between them).

      This would be achievable with the clocks in the puzzle.

      If the clocks were set at 2 o’clock on Tuesday afternoon, then at 2pm on the following Monday they would both read 8 o’clock (one of them having gained 6 hours, and the other having lost 6 hours). So the puzzle seems reasonable, and the solution is probably close to this, although the clocks we have to consider must only gain or lost at most 59 minutes a day.

      We are interested in when the difference between the clocks is a multiple of 12 hours. For clocks that show a difference of d minutes per day, the time t at which they show a difference that is a multiple k of 12 hours is:

      t = k × 720 × 1440 / d

      We know d is in the range [1, 118], t is less than 8 days. So:

      t < 8 × 1440
      ⇒ 720k < 8d
      ⇒ 90k < d

      Hence k = 1 and d is in the range [91, 118], and t is an exact number of minutes.

      This Python program examines possible (t, d) values, and eliminates t values that make impossible to finish on a Monday. It then looks for gain/loss values for the clocks that give an exact number of minutes when they are both showing the same time, give a start time that is during working hours. It runs in 47ms.

      Run: [ @replit ]

      from enigma import (irange, divisors_pairs, div, sprintf, fdiv, printf)
      
      # working in minutes
      R = 12 * 60  # repeat period of clocks
      D = 24 * 60  # minutes in a day
      W = 7 * D  # minutes in a week
      
      # work hours (9:30am - 5:00pm, Mon(= 0) to Fri(= 4)
      work = list([570 + x, 1020 + x] for x in irange(0, 4 * D, step=D))
      
      # check if time is during work hours
      def check(t):
        t %= W
        return any(x <= t <= y for (x, y) in work)
      
      def fmt(m):
        (d, m) = divmod(m % W, D)
        day = ("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun")[d]
        (h, m) = divmod(m, 60)
        return sprintf("{day} {h:02d}:{m:02d}")
      
      # find valid (d, t) pairs
      for (d, t) in divisors_pairs(R * D, every=1):
        if d < 91: continue
        if d > 118: break
      
        # calculate start window, for a finish during Monday's work hours
        a = (work[0][0] - t) % W
        b = (work[0][1] - t) % W
        if not (check(a) or check(b)): continue
      
        # look for times separated by d that give integer start times
        for x in irange(59, -59, step=-1):
          y = x - d
          if y < -59: break
      
          # calculate the time differences
          dy = div(t * (D + y), D)
          if dy is None: continue
      
          # clock Y is slow and showing 8am on Monday (= 480)
          # find out when it was set
          t0 = (480 - dy) % W
          if not check(t0): continue
      
          # output solution
          printf("start = {t0} [end = {end}; d={d}m t={t:g}d; x={x}m y={y}m]", t0=fmt(t0), end=fmt(t0 + t), t=fdiv(t, D))
      

      Solution: The clocks were originally set on Monday at 9:48am.


      The difference in the time the clocks show is 100 minutes per day, so after 7.2 days they are different by 720 minutes = 12 hours.

      One clock gains 45 minutes per day, and the other loses 55 minutes per day.

      So after 7.2 days the fast clock has gained 324 minutes (so it thinks it has been running for 7.425 days), and the slow clock has lost 396 minutes (so it thinks it has been running for 6.925 days).

      The slow clock is showing 8am, but it is actually 6h36m later than that, i.e. 2:36pm.

      And the fast clock is showing 8pm, but it is actually 5h24m earlier than that, i.e. 2:36pm.

      So the clocks are showing the same time at 2:36pm on Monday (which is during working hours), so they must have been set 7.2 days before this, i.e. one week and 4h48m earlier = 9:48am the previous Monday (also during working hours).

      Like

    • Frits's avatar

      Frits 10:26 pm on 22 April 2021 Permalink | Reply

      from enigma import div
      
      # between Friday 17:00 - Monday 09:30 there are 3870 minutes
      # between Monday 09:30 - Monday 17:00 there are 3870 + 6660 minutes
      
      # assume 3870 + M minutes have gone by 
      # number of days passed = (3870 + M) / 1440
      
      # loop over clock minutes running too slow or too fast
      for X in range(1, 60):
        for W in range(1, 60):
          # for variables x, w use values -1, 1 for sign of resp. numbers X and W 
          for x in [-1, 1]:
            for w in [-1, 1]:
              # avoid duplicate solutions
              if x * X >= w * W: continue
             
              # clocks strike at the same time 
              # 480 + X * no_days   (08:00)   or    1200 - X * no_days   (20:00)
              
              # (3 * x + 7) * 120  - x * X * (3870 + M) / 1440 == \
              # (3 * w + 7) * 120  - w * W * (3870 + M) / 1440",
              
              # calculate minutes
              M = div(518400 * (w - x) - (w * W - x * X) * 3870, w * W - x * X)
              if M is None: continue
              
              # maximum range Monday 09:30 - Monday 17:00
              if M > 6660: continue
              
              # gain has to be a whole number
              gain = div(X * (3870 + M),  1440)
              if gain is None: continue
              
              # time = real time (in minutes) the clocks strikes at the same time
              # "480 + gain" or "1200 - gain"
              time = (3 * x + 7) * 120  - x * gain
              
              # clocks were set (3870 + M) / 1440 days before
              # 9:30 = 570 minutes,  17:00 = 1020 minutes
              if not(570 <= (1440 + time - (3870 + M)) % 1440 <= 1020): continue
              
              days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
              
              h = (time - (3870 + M) % 1440) // 60
              m = (time - (3870 + M) % 1440) % 60
              
              d = (3870 + M) // 1440
              day = days[7 - d] 
             
              print(f"day = {day}, exact time {h}:{m}")
      

      Like

  • Unknown's avatar

    Jim Randell 9:13 am on 20 April 2021 Permalink | Reply
    Tags:   

    Teaser 2787: Crime-writers convention 

    From The Sunday Times, 21st February 2016 [link] [link]

    A group of twelve crime writers attended a convention. They were Bonfiglioli, Durrenmatt, Fyfield, Hiaasen, Highsmith, Hill, Innes, James, Knox, Le Carre, Rendell and Sayers, They sat in one long row on the stage, with Hiaasen further to the left than Hill. It turned out that, for any two sitting next to each other, there was just one letter of the alphabet that occurred (perhaps more than once) in both their surnames.

    List the initial of each author from left to right along the row.

    [teaser2787]

     
    • Jim Randell's avatar

      Jim Randell 9:16 am on 20 April 2021 Permalink | Reply

      This Python program runs in 51ms.

      Run: [ @replit ]

      from enigma import join, printf
      
      names = [
        'bonfiglioli', 'durrenmatt', 'fyfield', 'hiaasen', 'highsmith',
        'hill', 'innes', 'james', 'knox', 'le carre', 'rendell', 'sayers',
      ]
      
      # add names in rs to ns, so that adjacent names share exactly 1 letter
      # ns - names assigned so far
      # rs - remaining names
      def solve(ns, rs):
        # are we done?
        if not rs:
          yield ns
        else:
          # consider one of the remaining names
          for (i, n) in enumerate(rs):
            # check adjacent names share exactly 1 letter
            if not(ns) or len(set(n).intersection(ns[-1])) == 1:
              # solve for the remaining names
              yield from solve(ns + [n], rs[:i] + rs[i + 1:])
      
      # find lists of names
      for ns in solve([], names):
        # check 'hiaasen' is further left than 'hill'
        if not(ns.index('hiaasen') < ns.index('hill')): continue
        # output the names and the initals
        s = list(n[0].upper() for n in ns)
        printf("{s}\n  -> {ns}", s=join(s, sep=' '), ns=join(ns, sep=', '))
      

      Solution: The initial letters of the surnames are: H K D B L I H R J F H S.

      The order is (left to right):

      Hiaasen
      Knox
      Durrenmatt
      Bonfiglioli
      Le Carre
      Innes
      Hill
      Rendell
      James
      Fyfield
      Highsmith
      Sayers

      Like

  • Unknown's avatar

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

    Brain-Teaser 31: [Birthday weddings] 

    From The Sunday Times, 22nd October 1961 [link]

    Jane said:

    My mother and I were each married on our birthday and each at the same age. I was born four years and a day after my mother’s marriage, and my twins were born four years and a day after my marriage to John, who is three years older than I am, and who shares a birthday with my mother.

    If you write a date, say Christmas Day this year, in a continuous line of figures it looks like this – 25121961. Very well, if you write down our five dates of birth in that way and add the resultant numbers, the total is 8829685.

    When was her wedding day?

    This puzzle was originally published with no title.

    [teaser31]

     
    • Jim Randell's avatar

      Jim Randell 8:18 am on 18 April 2021 Permalink | Reply

      I was wondering when exactly “a year and a day” after 29th February was. It’s probably easiest to imagine there is a zero length 29th February in non-leap years, and then it is easier to calculate these “year and a day” offsets. In the analysis it turns out there is only one possible set of dates anyway.

      Assuming the 5 years of birth are all before 2000, we see that their sum must be a 4-digit number, i.e. 9685. And there is no carry into the sum of the day and month digits, which are 882. So we can deal this these three columns separately.

      As there are 5 birthdays they cannot all be in the same month (as the final digit of the day/month sum would end in 5 or 0). In order for the final digit to be 2, twins birthday must at the beginning of a month, Jane at the end of the previous month, and John and Mum the day before that.

      So the twins could be born on:

      1st Mar: sum = 13 + 13 + 282 + 272 + 272 = 852 (non leap-year)
      1st Mar: sum = 13 + 13 + 292 + 282 + 282 = 882 (leap year)
      1st May: sum = 15 + 15 + 304 + 294 + 294 = 922
      1st Jul: sum = 17 + 17 + 306 + 296 + 296 = 932
      1st Sep: sum = 19 + 19 + 318 + 308 + 308 = 972
      1st Oct: sum = 111 + 111 + 3110 + 3010 + 3010 = 9352

      And only 1st Mar (twins), preceded by 29th Feb (Jane), and 28th Feb (John, Mum), gives the sum 882.

      If the twins were born (on 1st Mar) in year y, then the year of Jane’s marriage is 4 years and 1 day earlier. And Jane is married on her birthday, which is 29th Feb, so (y − 4) (and hence y) must be a leap year.

      If Jane is married at age x years, then her birth date must be (y − x − 4) (also a leap year, so x must be a multiple of 4).

      And John is born on 28th Feb, 3 years earlier, i.e. in year (y − x − 7).

      Jane’s Mum was married exactly 1 year before that, i.e. in year (y − x − 8).

      So she was born in year: (y − 2x − 8).

      And the sum of the 5 birth years is 9685:

      2y + (y − x − 4) + (y − x − 7) + (y − 2x − 8) = 9685
      5y − 4x − 19 = 9685
      x = (5y − 9704) / 4

      Looking at leap years, going backwards from 1960:

      y = 1960: x = 24
      y = 1956: x = 19
      y = 1952: x = 14

      So the only viable candidate for (y, x) is (1960, 24) (as x must be a multiple of 4).

      So the dates we are interested in are:

      1960-03-01 = twins born
      1956-02-29 = Jane and John’s wedding; Jane’s 24th birthday
      1932-02-29 = Jane born
      1929-02-28 = John born
      1928-02-28 = Jane’s Mum’s wedding; Mum’s 24th birthday
      1904-02-28 = Jane’s Mum born

      Adding the 5 birth dates converted to integers we get:

      131960 + 131960 + 2921932 + 2821929 + 2821904 = 8829685

      Solution: Jane’s wedding day was: 29th February 1956.

      Like

  • Unknown's avatar

    Jim Randell 5:04 pm on 16 April 2021 Permalink | Reply
    Tags:   

    Teaser 3056: Rose garden 

    From The Sunday Times, 18th April 2021 [link] [link]

    The six rose bushes in my garden lie on a circle. When they were very small, I measured the six internal angles of the hexagon that the bushes form. These were three-digit whole numbers of degrees. In a list of them, of the ten digits from 0 to 9, only one digit is used more than once and only one digit is not used at all. Further examination of the list reveals that it contains a perfect power and two prime numbers.

    In degrees, what were the smallest and largest angles?

    [teaser3056]

     
    • Jim Randell's avatar

      Jim Randell 5:51 pm on 16 April 2021 Permalink | Reply

      (See also: Teaser 1885).

      The internal angles of a hexagon must sum to 720°, and as they are all 3-digit whole numbers of degrees we see that they are all in the range 100° to 179°. So the repeated digit must be 1. (And with a little bit of analysis we can reduce the range of the angles further).

      Additionally the alternating angles of a cyclic hexagon, must sum to 360°, so the angles must be able to be formed into 2 groups of 3, each group summing to 360°. (In general for cyclic 2n-gon the alternating angles must have the same sum).

      These constraints, along with the other conditions give two possible sets of angles, but they have the same largest and smallest values.

      I assumed the angles were all different (although 111° could potentially be repeated, but there are no solutions where it is).

      This Python 3 program runs in 122ms.

      Run: [ @replit ]

      from enigma import (irange, mgcd, unpack, prime_factor, subsets, multiset, nsplit, printf)
      
      # check digits have no repeats (other than 1)
      def digits(ns, ds=None):
        ds = (multiset() if ds is None else ds.copy())
        for n in ns:
          ds.update_from_seq(nsplit(n))
        # check the digits
        if all(v == 1 or k == 1 for (k, v) in ds.items()): return ds
      
      # determine if a number is a prime or an exact power from its prime factorisation
      is_prime = lambda fs: len(fs) == 1 and fs[0][1] == 1
      is_power = lambda fs, gcd=unpack(mgcd): gcd(e for (p, e) in fs) > 1
      
      # collect candidate primes, powers and others
      (primes, powers, others) = (list(), list(), set())
      for n in irange(100, 179):
        if not digits([n]): continue
        fs = list(prime_factor(n))
        if is_prime(fs):
          primes.append(n)
        elif is_power(fs):
          powers.append(n)
        else:
          others.add(n)
      
      # decompose <t> into <k> numbers in range [m, M]
      # that are not in primes or powers
      def decompose(t, k, m=100, M=179, ns=[]):
        # are we done?
        if k == 1:
          if not (t < m or t > M) and t in others:
            yield ns + [t]
        else:
          # choose the next number
          k_ = k - 1
          for n in irange(m, min(M, t - k_ * m)):
            if n in others:
              yield from decompose(t - n, k_, n + 1, M, ns + [n])
      
      # choose the two primes
      for (b, c) in subsets(primes, size=2):
        ds1 = digits([b, c])
        if ds1 is None: continue
      
        # choose the power
        for a in powers:
          ds2 = digits([a], ds1)
          if ds2 is None: continue
      
          # find the remaining angles
          for xs in decompose(720 - (a + b + c), 3):
            ds3 = digits(xs, ds2)
            if ds3 is None: continue
            # only one of the 10 digits is missing
            if len(ds3.keys()) != 9: continue
      
            # and the sum of alternate angles must be 360 degrees
            ns = sorted([a, b, c] + xs)
            if any(sum(ss) == 360 for ss in subsets(ns, size=3)):
              printf("min={ns[0]} max={ns[-1]} {ns}")
      

      Solution: The smallest angle is 101°. The largest angle is 146°.


      Measuring angles in degrees. The 6 angles are all integers between 101 and 179 (100 is ruled out because of the repeated 0 digit).

      And they must form two groups of 3 that add up to 360, so the largest possible angle is 360 − 101 − 111 = 148.

      This limits the possible powers to: 121, 125, 128.

      So, whichever power is chosen the digit 2 will be used.

      The primes are therefore limited to: 101, 103, 107, 109, 113, 131, 137, 139.

      So, whichever primes are chosen one will contain the digit 0 and one will contain the digit 3 (so 103 cannot be chosen).

      Which means the remaining three angles cannot contain the digits 0, 2, or 3.

      The remaining angles are therefore limited to: 111, 114, 115, 116, 117, 118, 119, 141, 145, 146, 147, 148.

      There are no pairs from the permissible remaining angles that pair with 121 to give 360, so the power is either 125 or 128.

      For a power of 125, there are two sets of angles that can make 360, but only one of them leaves another set of angles that can make 360 according to the constraints:

      (125, 117, 118) + (101, 113, 146)

      For a power of 128, there are four sets of angles that can make 360, but only one of them leaves another set of angles that can make 360 according to the constraints (and it is the same set of additional angles as the previous solution):

      (128, 115, 117) + (101, 113, 146)

      And in both cases the unused digit is 9 and the minimum and maximum values both lie in the set without the power.


      There are many ways to produce a cyclic hexagon from a set of angles, but here are two diagrams corresponding to the two solutions. For each diagram I have maximised the smallest distance between vertices:

      Like

      • Jim Randell's avatar

        Jim Randell 12:30 pm on 19 April 2021 Permalink | Reply

        Here is a faster version. It builds up sets of 3 angles that sum to 360° and don’t share non-1 digits, and then looks for two of these sets that don’t share non-1 digits, and checks that together they have exactly 1 power and 2 primes.

        It also allows for a 111° angle to appear multiple times (although this doesn’t give any further solutions).

        It runs in 51ms.

        Run: [ @replit ]

        from enigma import (irange, mgcd, unpack, prime_factor, subsets, nsplit, union, printf)
        
        # check digits have no repeats (other than 1), return set of digits used
        def digits(ns):
          ds = set()
          for n in ns:
            for d in nsplit(n):
              if d == 1: continue
              if d in ds: return
              ds.add(d)
          return ds
        
        # determine if a number is a prime or an exact power from its prime factorisation
        is_prime = lambda fs: len(fs) == 1 and fs[0][1] == 1
        is_power = lambda fs, gcd=unpack(mgcd): gcd(e for (p, e) in fs) > 1
        
        # max and min possible angles
        (m, M) = (101, 148)
        
        # collect candidate primes, powers and others
        (primes, powers, others) = (set(), set(), set())
        for n in irange(m, M):
          if not digits([n]): continue
          fs = list(prime_factor(n))
          if is_prime(fs):
            primes.add(n)
          elif is_power(fs):
            powers.add(n)
          else:
            others.add(n)
        
        # decompose <t> into <k> numbers in range [m, M] chosen from <ns>
        def decompose(t, k, ns, m=m, M=M, xs=[]):
          # are we done?
          if k == 1:
            if not (t < m or t > M) and t in ns:
              yield xs + [t]
          else:
            # choose the next number
            k_ = k - 1
            for n in irange(m, min(M, t - k_ * m)):
              if n in ns:
                yield from decompose(t - n, k_, ns, n + 1, M, xs + [n])
        
        # find sets of numbers that give 360
        ss = list()
        for ns in decompose(360, 3, union([primes, powers, others])):
          # find digits used
          ds = digits(ns)
          if ds:
            ss.append((ns, ds))
        
        # also allow a repeat of 111
        ns = [111, 111, 138]
        ds = digits(ns)
        if ds:
          ss.append((ns, ds))
        
        # choose two sets with no shared digits (other than 1)
        for ((ns1, ds1), (ns2, ds2)) in subsets(ss, size=2):
          if ds1.intersection(ds2): continue
          ns = sorted(ns1 + ns2)
          # check there is 1 power and 2 primes
          if not (len(powers.intersection(ns)) == 1 and len(primes.intersection(ns)) == 2): continue
          # output solution
          printf("min={ns[0]} max={ns[-1]} {ns1} + {ns2}")
        

        Like

    • Frits's avatar

      Frits 10:10 pm on 16 April 2021 Permalink | Reply

      Checks for prime numbers and powers are done a limited number of times so it was not worth to calculate prime numbers and powers (for 101-159) in advance.

      from enigma import SubstitutedExpression, is_prime, unpack, mgcd, prime_factor
      
      # check a number is _not_ an exact power  (see Brain-Teaser 683)
      is_no_power = lambda n, gcd=unpack(mgcd): gcd(e for (p, e) in prime_factor(n)) == 1
      
      # main checks for sequence <s>
      def check(s):
        # only one digit is missing so we have four 1's and eight others
        s1 = "".join(str(x).zfill(2) for x in s)
        if len(set(s1)) != 9 or s1.count("1") != 4 :
          return False 
       
        # list contains two prime numbers 
        if len([x for x in s if is_prime(100 + x)] ) != 2:
          return False
       
        # list contains one power
        if len([x for x in s if is_no_power(100 + x)]) != 5:
          return False
       
        return True
      
      p = SubstitutedExpression(
        [
         # 1AB + 1CD + 1?? = 360   ?? = 60 - AB - CD
         "AB < CD",
         "CD < 60 - AB - CD",
        
         # 1GH + 1IJ + 1?? = 360   ?? = 60 - GH - IJ
         "GH < IJ",
         "IJ < 60 - GH - IJ",
        
         # limit duplicate solutions
         "AB <= GH",
        
         # main check
         "check([AB, CD, 60 - AB - CD, GH, IJ, 60 - GH - IJ])",
        ],
        answer="(100 + AB, 100 + CD, 160 - AB - CD, \
                 100 + GH, 100 + IJ, 160 - GH - IJ)",
        d2i=dict([(0, "CG")] +
                 [(2, "AG")] +
                 [(k, "ACGI") for k in range(3, 10)]),
        env=dict(check=check),
        distinct="",
      verbose=0,
      )
      
      # print answer
      for (_, ans) in p.solve():
        print(f"min={min(ans)} max={max(ans)} {ans}")
      

      Like

    • Frits's avatar

      Frits 12:30 pm on 17 April 2021 Permalink | Reply

      Built for speed (hardcoded values).

      # check if sequence <s> contains different digits (except 1)
      def diffdgts(s, no1s=0):
        s1 = "".join(str(x).zfill(2) for x in s)
        s2 = s1.replace("1", "")
        if len(set(s2)) != len(s2):
          return False  
        else:
          if no1s and s1.count("1") != no1s:
              return False
          
          # F needs to contain 2 or 3 so don't allow both in A, B, C    
          if len(s) == 3 and "2" in s2 and "3" in s2: 
            return False
            
          return True
      
      # form angles into 2 groups of 3, each group summing to 360  
      for A in range(1, 18):
        # F needs to contain 2 or 3 so limit B to 19
        for B in range(max(11, A + 1), 20):
          C = 60 - A - B
          if not diffdgts([A, B, C]): continue
          
          # second group 
          for D in range(max(11, A + 1), 19): 
            for E in range(D + 1, 20):
              F = 60 - D - E
              
              s = [A, B, C, D, E, F]
              if not diffdgts(s, 4): continue
              
              # either 100 + C or 100 + F has to be a power as A, B, C and D < 20
              if len([x for x in [C, F] if x in {21, 25, 28}]) != 1: continue
              
              # list contains two prime numbers (prime numbers < 149)  
              if len([x for x in s if x in {1, 3, 7, 9, 13, 27, 31, 37, 39}] ) != 2:
                continue
           
              ans = [100 + x for x in s]
              print(f"min={min(ans)} max={max(ans)} {ans}")
      

      Like

    • Frits's avatar

      Frits 1:10 am on 20 April 2021 Permalink | Reply

      Starting with 3 angles which sum to 360 and include a power.
      This list is limited and allows us to derive digits which may not be used in the other list of 3 angles.

      # (prime numbers < 149 and excluding numbers with digit 2)  
      primes = {101, 103, 107, 109, 113, 131, 137, 139}
      
      # create a list of 3 angles which sum to 360 and includes a power
      pwrs = []
      # power 121 is not allowed as it forces 119, 120, 121
      for A in [125, 128]:
        # set ranges so that C > B
        for B in range((241 - A), 100 + (161 - A) // 2):
          C = 360 - A - B
      
          # check for different digits (except 1)
          dABC = [x for n in [A, B, C] for x in [n // 10 - 10, n % 10] if x != 1]
          # the 9 digits must have exact 5 ones as B = 111 is not allowed
          if len(dABC) != 4 or len(set(dABC)) != 4: continue
      
          pwrs.append([[A, B, C], dABC])
      
      
      # if digits 3/4, 8 and 9 are used in ABC we can't make 360 with remaining
      # digits as you will have to make 10 with 0, 1, 5, 6, 7 (3/4 is for tens)
      pwrs = [[p, d] for p, d in pwrs if not ((3 in d or 4 in d) 
                                         and 8 in d and 9 in d)]
      
      # check which digits occur in all power entries
      common_dgts = [i for i in range(2, 10) if all(i in d for p, d in pwrs)]
      
      # second group without power
      for D in range(101, 115): 
        if (D % 10) in common_dgts: continue
        for E in range(max(111, D + 1), min(231 - D, 120)):
          if (E % 10) in common_dgts: continue
          F = 360 - D - E
          if (F % 10) in common_dgts: continue
      
          # check for different digits (except 1)
          dDEF = [x for n in [D, E, F] for x in [n // 10 - 10, n % 10] if x != 1]
          if len(dDEF) != 4 or len(set(dDEF)) != 4: continue
      
          # check if D, E, F complements pwrs (A, B, C)
          for p, d in pwrs:
            # skip if they have digits in common
            if any(x in d for x in dDEF): continue
      
            s = [D, E, F] + p
      
            # list contains two prime numbers 
            if len([x for x in s if x in primes]) == 2:
               print(f"{min(s)}, {max(s)} [{s}]")
      

      Like

  • Unknown's avatar

    Jim Randell 8:22 am on 15 April 2021 Permalink | Reply
    Tags:   

    Teaser 2786: Out of order 

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

    I have written down five positive whole numbers whose sum is less than 100. If you wrote the numbers in words, then you would find that each of them begins with a different letter of the alphabet. (Surprisingly, the same is true of the five numbers obtained by increasing each of my five numbers by one). If you write my five numbers in words and put them in alphabetical order, then they will be in decreasing order.

    What (in decreasing order) are my five numbers?

    [teaser2786]

     
    • Jim Randell's avatar

      Jim Randell 8:23 am on 15 April 2021 Permalink | Reply

      The following Python program runs in 50ms.

      Run: [ @replit ]

      from enigma import int2words, irange, seq_all_different, printf
      
      N = 5  # how many numbers in the sequence
      T = 100  # sum must be less than this
      
      # map numbers to their initial letter
      m = dict((i, int2words(i)[0]) for i in irange(1, T - 1))
      
      # add <k> numbers to <ns> in decreasing numerical order,
      # but increasing alphabetical order, that sum less than <t>
      def solve(ns, k, t):
        # are we done?
        if k == 0:
          yield ns
        else:
          # add in a lower number
          n0 = ns[-1]
          for n in irange(min(n0 - 1, t - k + 1), k, step=-1):
            # that is later alphabetically
            if m[n] > m[n0]:
              yield from solve(ns + [n], k - 1, t - n)
      
      # check the numbers in ns all start with different letters when offset by i
      check = lambda ns, i=0: seq_all_different(m[n + i] for n in ns)
      
      # start with the largest number
      for n in irange(5, 89):
        for ns in solve([n], N - 1, T - n):
          # check the starting letters are all different when offset by 1
          if check(ns, 1):
            # output a solution
            printf("{ns} sum={t}", t=sum(ns))
      

      Solution: The five numbers are: 18, 15, 9, 7, 3.

      So the initial letters are: E, F, N, S, T (which are in alphabetical order).

      Adding one to each term we get: 19, 16, 10, 8, 4; with initial letters: N, S, T, E, F.

      And we can also increment each term again to get: 20, 17, 11, 9, 5; initial letters: T, S, E, N, F.

      Like

    • Frits's avatar

      Frits 2:52 pm on 15 April 2021 Permalink | Reply

      from collections import defaultdict
      
      upto19 = ['', 'one', 'two', 'three', 'four', 'five', 'six', 'seven',
                'eight', 'nine', 'ten', 'eleven', 'twelve', 'thirteen', 'fourteen',
                'fifteen', 'sixteen', 'seventeen', 'eighteen', 'nineteen']
      tens = ['twenty', 'thirty', 'forty', 'fifty', 'sixty', 'seventy', 'eighty',
              'ninety']
      
      # map numbers to their initial letter
      m = dict((i, tens[(i - 20) // 10][0] if i > 19 else upto19[i][0])
                   for i in range(1, 100))
      
      # letters = sorted(set(m[i] for i in range(1, 90)))
      
      # we have 6 letters of which 'o' only occurs once for number 1
      # as 'o' in alphabetical order precedes 's' and 't' we can't use letter 'o'
      # thus numbers [E, D, C, B, A] must have letters ['e', 'f', 'n', 's', 't']
      
      d = defaultdict(list)
      for i in range(1, 90):
        d[m[i]].append(i)
      
      # list with minimal values (from A to D)
      mv = [d['t'][0]]
      for i in "snf":
        mv.append([x for x in d[i] if x > max(mv)][0])
      
      # loop from highest number
      for E in [x for x in d['e'] if x > mv[3] and x < 100 - sum(mv)]:
        for D in [x for x in d['f'] if x > mv[2] and
                  x < min(E, 100 - E - sum(mv[:3]))]:
          for C in [x for x in d['n'] if x > mv[1] and
                    x < min(D, 100 - D - E - sum(mv[:2]))]: 
            for B in [x for x in d['s'] if x > mv[0] and
                      x < min(C, 100 -  C - D - E - sum(mv[:1]))]:
              for A in [x for x in d['t'] if x < min(B, 100 - B - C - D - E)]:
       
                # check if the numbers start with different letters when offset by 1       
                if len(set(m[E+1] + m[D+1] + m[C+1] + m[B+1] + m[A+1])) == 5:
                  print("answer:", (E, D, C, B, A))
      

      Like

  • Unknown's avatar

    Jim Randell 9:40 am on 13 April 2021 Permalink | Reply
    Tags:   

    Brain-Teaser 30: [Football table] 

    From The Sunday Times, 15th October 1961 [link]

    In a football tournament each country played each other country twice, the scores in all twelve matches being different.

    The records for the top and bottom teams were:

    England beat Wales twice by the same margin as she beat Ireland once.

    The sum of the aggregate number of goals scored against Scotland, who finished second, and Ireland was 20.

    What were the respective scores in the Ireland vs. Scotland matches?

    This puzzle was originally published with no title.

    [teaser30]

     
    • Jim Randell's avatar

      Jim Randell 9:41 am on 13 April 2021 Permalink | Reply

      This seems to be the earliest “football table” Teaser.

      We are told that the sum of Scotland and Ireland’s “goals against” values is 20, which means the total of the “goals against” column must be 38. And so the sum of Scotland and Ireland’s “goals for” values must be 38 − (16 + 8) = 14.

      The following Python program uses the [[ Football() ]] helper class from the enigma.py library. It runs in 1.22s.

      from itertools import product
      from enigma import (Football, ordered, chunk, subsets, irange, multiset, join, printf)
      
      # scoring system
      football = Football(games="wdl", points=dict(w=2, d=1))
      
      # identify matches with the same scoreline
      keys = lambda ss: list(ordered(*s) for s in ss)
      
      # check a sequence of scores, all different and in ordered pairs
      check = lambda ss, ps: len(set(ss)) == len(ss) and all(x > y for (x, y) in chunk(ps, 2))
      
      # margin
      margin = lambda ss: ss[0] - ss[1]
      
      # record scores in the S vs I matches
      rs = multiset()
      
      # scorelines for E (who have won all their matches)
      (ew1, ew2, es1, es2, ei1, ei2) = mes = 'w' * 6
      for ssE in football.scores(mes, [0] * 6, 16, 3):
        # check scorelines
        ss0 = keys(ssE)
        if not check(ss0, ss0): continue
      
        # E wins E vs W by same margin, same as exactly one of the E vs I
        (EW1, EW2, ES1, ES2, EI1, EI2) = ssE
        d = margin(EW1)
        if not (d == margin(EW2) and (margin(EI1), margin(EI2)).count(d) == 1): continue
      
        # W have 2 draws and 2 losses remaining
        for mws in subsets("ddll", size=len, select="mP"):
          for ssW in football.scores(mws, [0] * 4, 8, 15, [EW1, EW2], [1, 1]):
            ss1 = keys(ssW)
            if not check(ss0 + ss1, ss1): continue
      
            # calculate current goals for/against S and I (so far)
            (WS1, WS2, WI1, WI2) = ssW
            (fS, aS) = football.goals([ES1, ES2, WS1, WS2], [1, 1, 1, 1])
            (fI, aI) = football.goals([EI1, EI2, WI1, WI2], [1, 1, 1, 1])
            # goals against S and I sum to 20 (and goals for S and I sum to 14)
            (ga, gf) = (20 - aS - aI, 14 - fS - fI)
            if ga < 0 or gf < 0: continue
      
            # choose outcomes for S vs I matches
            (ws1, ws2, wi1, wi2) = mws
            for (si1, si2) in football.games(repeat=2):
              S = football.table([es1, es2, ws1, ws2, si1, si2], [1, 1, 1, 1, 0, 0])
              I = football.table([ei1, ei2, wi1, wi2, si1, si2], [1, 1, 1, 1, 1, 1])
              if not (12 >= S.points >= I.points >= 2): continue
      
              # chose remaining "for" and "against" goals for S
              for (x, y) in product(irange(0, gf), irange(0, ga)):
                # look for scorelines
                for (SI1, SI2) in football.scores([si1, si2], [0, 0], x, y):
                  ss2 = keys([SI1, SI2])
                  if not check(ss0 + ss1 + ss2, ss2): continue
                  # and check goals "for"/"against" I
                  (x_, y_) = football.goals([SI1, SI2], [1, 1])
                  if not (x + x_ == gf and y + y_ == ga): continue
                  # check S is second and I is third
                  if not (S.points > I.points or fS - aS + x - y > fI - aI + x_ - y_): continue
                  if not (I.points > 2 or fI - aI + x_ - y_ > -7): continue
                  printf("[EW = {EW1} {EW2}; ES = {ES1} {ES2}; EI = {EI1} {EI2}; WS = {WS1} {WS2}; WI = {WI1} {WI2}; SI = {SI1} {SI2}]")
                  rs.add((SI1, SI2))
      
      # output solution
      for (k, v) in rs.most_common():
        printf("S vs I = {k} [{v} solutions]", k=join(k, sep=", "))
      

      Solution: The scores in the Scotland vs. Ireland matches were 4-0 and 0-0.

      There are many scenarios which lead to this solution. One of them is:

      E vs W = 3-2, 2-1
      E vs S = 3-0, 2-0
      E vs I = 5-0, 1-0
      W vs S = 2-2, 1-3
      W vs I = 1-4, 1-1
      S vs I = 4-0, 0-0

      Like

    • John Crabtree's avatar

      John Crabtree 4:28 pm on 16 April 2021 Permalink | Reply

      There were 38 goals scored, ie the minimum possible, and so the match scores were 0-0; 1-0; 2-0, 1-1; 3-0, 2-1; 4-0, 3-1, 2-2; 5-0, 4-1, 3-2.
      Wales scored 8 goals, with scores of 2-3 vE, 1-2 vE, 1-1, 2-2, 1-3 and 1-4.
      And so the scores in England’s other games were 1-0 vI, 2-0, 3-0 and 5-0.
      The other two matches were between Ireland and Scotland with scores of 0-0 and 4-0.

      It is interesting to know when these puzzles started. Some of the later ones were much more convoluted

      Like

  • Unknown's avatar

    Jim Randell 9:46 am on 11 April 2021 Permalink | Reply
    Tags:   

    Brain-Teaser 736: Moving NEWS 

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

    Four friends and I live in the same town, one of us at the town centre, and the others at places due north, south, east and west of the town centre. Our names are North, South, East, West and Middle, but we do not necessarily live at the places which accord with our names.

    In visiting one another we use the only connecting roads which run north-south and east-west through the town centre.

    Before last year, when North and I exchanged houses (to accommodate his increasing family, mine by then having left home), I lived farther north than West, who lives farther east than Middle, who lives farther west than East. North lived farther east than South. (When visiting East, North had to turn right at the town centre, but I could go straight ahead when visiting North).

    What is my name, and who lives in the north, east, south, west and middle positions respectively?

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

    [teaser736]

     
    • Jim Randell's avatar

      Jim Randell 9:47 am on 11 April 2021 Permalink | Reply

      I think the wording of this puzzle could be improved.

      I initially took: “I could go straight ahead when visiting North”, to mean that the journey I→N involved passing straight through the centre (i.e. was one of: n→s, e→w, s→n, w→e), but with this condition there are no solutions. So instead I used the condition, that the journey I→N does not involve making a turn at the centre, and that gives a unique solution.

      For each of the directions we can use equivalent statements, for example:

      “X is north of Y” ⇔ “(X is northernmost) ∨ (Y is southernmost)”

      This Python program runs in 47ms.

      Run: [ @replit ]

      from enigma import (subsets, printf)
      
      # map slots to locations
      m = { 0: "centre", 1: "north", 2: "east", 3: "south", 4: "west" }
      
      # possible turns at the centre
      right = {(1, 4), (2, 1), (3, 2), (4, 3)}
      #straight = {(1, 3), (2, 4), (3, 1), (4, 2)}
      
      # choose current positions for (N, S, E, W, M)
      for (N, S, E, W, M) in subsets((0, 1, 2, 3, 4), size=len, select="P"):
      
        # "W is east of M"
        if not (W == 2 or M == 4): continue
      
        # "M is west of E"
        if not (M == 4 or E == 2): continue
      
        # "N (was I) is north of W"
        if not (N == 1 or W == 3): continue
      
        # choose current position for I (not N; not W)
        for I in (S, E, M):
          oldN = I
          oldI = N
          oldS = (N if I == S else S)
          oldE = (N if I == E else E)
          
          # "N was east of S"
          if not (oldN == 2 or oldS == 4): continue
      
          # "N -> E was a right turn"
          if not ((oldN, oldE) in right): continue
      
          # "I -> N was straight ahead"
          #if not ((oldI, oldN) in straight): continue
          if (oldI, oldN) in right or (oldN, oldI) in right: continue
      
          # output solution
          printf("I={I}; N={N} S={S} E={E} W={W} M={M}",
           I="NSEWM"[(N, S, E, W, M).index(I)],
           N=m[N], S=m[S], E=m[E], W=m[W], M=m[M],
          )
      

      Solution: You are South. The current map is as shown:

      (Previously North and South were swapped over).

      And it can be seen that the journey from South (I) to North does not involve making a turn at the centre, but it also doesn’t involve passing through the centre.

      Like

    • John Crabtree's avatar

      John Crabtree 5:58 pm on 11 April 2021 Permalink | Reply

      Looking at the original map:
      M lived west of W and E, and so M lived at [w]
      N lived east of S, and so N lived at [e]
      N turned right to visit E, and so E lived at [n]
      I lived north of W, and I cannot be E, and so I, South, lived at [m] and W lived at [s].

      Swapping S and N gives the current map, as shown by Jim. Logically it is the only possible arrangement.

      Like

      • John Crabtree's avatar

        John Crabtree 12:21 am on 12 April 2021 Permalink | Reply

        I mixed up the tenses in my comment above. Please read the following before the above:

        I cannot be W, N or E. If I were M, I would have to be in [w] after the swap to live west of E and W. Then N must be in [w] before the swap, which is invalid as he lived east of S. And so I am South. E, M and W did not change positions.

        Like

  • Unknown's avatar

    Jim Randell 8:11 pm on 9 April 2021 Permalink | Reply
    Tags:   

    Teaser 3055: Proceed to checkout 

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

    The dartboard at the Trial and Arrow pub is rather different from the standard one: there are only 3 sectors, each with a positive whole number label, and no central bullseye scoring region. There are still double and treble rings: for instance, if the sector label is 3, a dart in that sector can score 3, 6 or 9.

    As usual, scores are counted down from a starting value, the idea being to finish (“check out”) by reaching exactly zero. Players take turns throwing three darts, or fewer if they check out before that. Unusually, the checkout doesn’t have to finish on a double.

    The lowest impossible checkout is the smallest value that can’t be achieved in one turn; on this board that value is 36.

    What are the sector labels?

    [teaser3055]

     
    • Jim Randell's avatar

      Jim Randell 8:42 pm on 9 April 2021 Permalink | Reply

      (See also: Puzzle #06).

      In order to have a score of 1 there must be a “1” sector. This Python program considers sets of three sectors of the form (1, a, b), and calculates the lowest score not achievable with 3 darts, until it finds a value of 36. It runs in 54ms.

      Run: [ @replit ]

      from enigma import union, subsets, irange, inf, peek, printf
      
      # find the lowest score not achievable with k darts and values vs
      def lowest(vs, k):
        # scores with 1 dart
        scores = union((v, 2 * v, 3 * v) for v in vs)
        # scores with up to k darts
        ss = set(sum(s) for s in subsets(scores, max_size=k, select="R"))
        # find the lowest score that cannot be made
        return peek(n for n in irange(0, inf) if n not in ss)
      
      # find 3 sectors with lowest impossible score of x
      def solve(x, k=3):
        # consider sectors [1, a, b], where a + b = t
        for t in irange(5, inf):
          for a in irange(2, (t - 1) // 2):
            vs = [1, a, t - a]
            if lowest(vs, k) == x:
              yield vs
      
      for vs in solve(36):
        printf("sectors = {vs}")
        break
      

      Solution: The sector labels are: 1, 5, 22.

      Like

    • Frits's avatar

      Frits 3:08 pm on 10 April 2021 Permalink | Reply

      from enigma import SubstitutedExpression
      from itertools import combinations_with_replacement
      
      # find the lowest score not achievable with 3 darts
      # only consider values less equal to <n>
      def low(vs, n):
        s = {p for v in vs for i in range(1, 4) if (p := v * i) <= n}
        s = set(su if (su := sum(c)) <= n else 0
                   for c in combinations_with_replacement(s, 3))
                  
        for x in range(10, n + 1):
          if x not in s:
            return x
      
        return 99      # high number
      
      p = SubstitutedExpression(
        [
         # look for sectors [1, X, YZ], Y >= X
         # X < 11 as otherwise 10 will be the lowest impossible checkout
        
         # with 2 sectors [1, X] we can never make number Y = 6X + 4
         # with 2 sectors [1, X] we can't make number Y = 4X + 4 unless X < 5
         "YZ < 4 * X + 5 + (X < 5) * 2 * X",
       
         "YZ > max(X, 4)",                # max total [1, X, YZ] >= 35
      
         # The lowest impossible checkout is 36
         "low([1, X, YZ], 36) == 36",
        ],
        answer="1, X, YZ",
        # exclude X = 4, 6 and 9 as they are factors of 36
        # exclude X = 7 as 5 * 7 + 1 = 36
        # exclude X = 10 as 3 * 10 + 6 * 1 = 36
        # highest value for Y is 2 (as of YZ = 30 you can make 36)
        d2i=dict([(0, "X")] + [(1, "X")] + [(3, "Y")] + [(4, "YX")] + [(5, "Y")] +
        [(k, "YX") for k in range(6, 8)] +
        [(8, "Y")] +
        [(9, "YX")]),
        env=dict(low=low),
        distinct="",   # allow variables with same values
        reorder=0,
        verbose=0,
      )
      
      # print answer
      for (_, ans) in p.solve():
        print(f"The sectors are: {ans}")
      

      Like

    • Frits's avatar

      Frits 9:04 pm on 12 April 2021 Permalink | Reply

      # sector candidates which can go with 1 without immediately making 36 
      cands = [X for X in range(2, 36) if X * 9 != 36 and
               not any((29 if i < 4 else 32) < X * i < 37 for i in range(1, 7))]  
      
      # consider the 2nd sector, it must be less than 11 
      # as otherwise 10 will be the lowest impossible checkout
      for X in [c for c in cands if c < 11]:
        one = [0, 1, 2, 3, X, 2 * X, 3 * X]
        two = {o1 + o2 for o1 in one for o2 in one}   
        
        mi = max(5, X + 1)
        # determine lowest score which can't be made with 3 darts in sectors [1, X]
        if X > 7:
          ma = 15 + (X - 8)
        elif X > 4:  
          ma = 4 * X + 4
        else:
          ma = 6 * X + (4 if 4 % X else 5) 
        
        # one dart in sector Y
        candsY = [Y for Y in cands if mi <= Y <= ma and 
                  all(36 - i not in two for i in [Y, 2 * Y, 3 * Y])]
        
        # two darts in sector Y scoring at least 4 * Y
        candsY = [Y for Y in candsY if
                  all(36 - i not in one for i in [4 * Y, 5 * Y, 6 * Y])]
        
        # if X > 2 and Y > 18 then number Y + 3 * X + 4 is no checkout
        candsY = [Y for Y in candsY if X == 2 or Y < 18 or Y + 3 * X + 4 > 35]   
        
        if candsY:
          three = {t + o for t in two for o in one}  
          remaining = [x for x in range(ma, 36) if x not in three]  
         
        for Y in candsY:
          if all(r - Y in two for r in remaining):
            print(f"The sectors are: 1, {X} and {Y}")
      

      Like

  • Unknown's avatar

    Jim Randell 12:05 pm on 8 April 2021 Permalink | Reply
    Tags:   

    Teaser 2789: Uncle’s gift (2) 

    From The Sunday Times, 6th March 2016 [link] [link]

    Last month I told you about Uncle Bill’s gifts to his nephews. Uncle Ben has also sent a whole number of pounds (less than fifty) to be shared among his three nephews Tom, Dick and Harry. Each has received a different whole number of pounds, with Tom receiving the most and Harry the least, but with Tom getting less than twice as much as Harry. Each boy’s fraction of the total gift, when expressed as a decimal, consists of three different digits recurring (as in 0.abcabc…), and each boy’s decimal uses the same three digits.

    How much did Tom, Dick and Harry get from Uncle Ben?

    [teaser2789]

     
    • Jim Randell's avatar

      Jim Randell 12:06 pm on 8 April 2021 Permalink | Reply

      A similar puzzle to Teaser 2785.

      In fact we can just change the selection criteria at line 21 of my original program for Teaser 2785 to give a program that solves this puzzle.

      This Python program runs in 64ms.

      Run: [ @replit ]

      from enigma import (irange, divc, divf, recurring, subsets, arg, printf)
      
      M = arg(49, 0, int)
      
      # consider the total amount
      for t in irange(6, M):
      
        # look for amounts that give 3 recurring digits
        r = dict()
        for n in irange(divc(t, 5), min(t - 3, divf(2 * t, 3))):
          (i, nr, rr) = recurring(n, t)
          if nr or len(rr) != 3: continue
          r[n] = rr
      
        # now find three amounts that sum to t
        for (H, D, T) in subsets(sorted(r.keys()), size=3):
          if H + D + T != t: continue
          # "Tom gets less than twice as much as Harry"
          if not (T < 2 * H): continue
          # and check the recurring digits are all the same
          if not (set(r[H]) == set(r[D]) == set(r[T])): continue
          printf("total={t}; Harry={H} [0.({rH})...], Dick={D} [0.({rD})...], Tom={T} [0.({rT})...]", rH=r[H], rD=r[D], rT=r[T])
      

      Solution: Tom got £ 16, Dick got £ 12, and Harry got £ 9. In total they received £ 37.

      And the fractions are:

      T: 16 / 37 = 0.(432)…
      D: 12 / 37 = 0.(324)…
      H: 9 / 37 = 0.(243)…

      And, again, we can find further solutions, using the same fractions, at multiples of these amounts.

      The next essentially different solution occurs at £ 111, where we have the original solution multiplied by 3, but also:

      T: 47 / 111 = 0.(423)…
      D: 38 / 111 = 0.(342)…
      H: 26 / 111 = 0.(234)…

      Like

    • John Crabtree's avatar

      John Crabtree 4:17 pm on 9 April 2021 Permalink | Reply

      There is not much change to the manual solution either.
      The fractions are of the form of abc / 999 = abc / (27 * 37)
      The three digits are 2, 3 and 4, with one of each in each column.
      If the sum were £27, abc = 0 mod 37, but 9 * 37 = 333
      And so the sum = £37, abc = 0 mod 27.
      And so, by inspection, 9 * 27 = 243, and then 12 * 27 = 324 and 16 * 27 = 432
      The solution follows.

      Like

  • Unknown's avatar

    Jim Randell 10:52 am on 6 April 2021 Permalink | Reply
    Tags:   

    Teaser 2785: Uncle’s gift (1) 

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

    Uncle Bill has sent a whole number of pounds (less than fifty) to be shared among his three nephews Tom, Dick and Harry. Each has received a whole number of pounds, with Tom receiving the most and Harry the least, but with Tom getting less than twice as much as Harry. Each boy’s fraction of the total gift, when expressed as a decimal, consists of three digits recurring (as in 0.abcabc…), and the nine digits that appear in the three decimals are all different. (Uncle Ben also sent some money, but I’ll tell you about that next month).

    How much did Tom, Dick and Harry get from Uncle Bill?

    [teaser2785]

     
    • Jim Randell's avatar

      Jim Randell 10:53 am on 6 April 2021 Permalink | Reply

      This Python program uses the [[ recurring() ]] function from the enigma.py library. It runs in 69ms.

      Run: [ @replit ]

      from enigma import (irange, divc, divf, recurring, subsets, arg, printf)
      
      M = arg(49, 0, int)
      
      # consider the total amount
      for t in irange(6, M):
      
        # look for amounts that give 3 recurring digits
        r = dict()
        for n in irange(divc(t, 5), min(t - 3, divf(2 * t, 3))):
          (i, nr, rr) = recurring(n, t)
          if nr or len(rr) != 3: continue
          r[n] = rr
      
        # now find three amounts that sum to t
        for (H, D, T) in subsets(sorted(r.keys()), size=3):
          if H + D + T != t: continue
          # "Tom gets less than twice as much as Harry"
          if not (T < 2 * H): continue
          # and check the recurring digits are all different
          if len(set(r[H] + r[D] + r[T])) != 9: continue
          printf("total={t}; Harry={H} [0.({rH})...], Dick={D} [0.({rD})...], Tom={T} [0.({rT})...]", rH=r[H], rD=r[D], rT=r[T])
      

      Solution: Tom got £ 15, Dick got £ 14, and Harry got £ 8. In total they received £ 37.

      The fractions are:

      T: 15 / 37 = 0.(405)…
      D: 14 / 37 = 0.(378)…
      H: 8 / 37 = 0.(216)…

      There are further solutions at multiples of these amounts (the fractions, of course, remain unchanged).

      The next essentially different solution occurs with a total of £ 333, where we can have the original solution multiplied by 9, but also:

      T: 136 / 333 = 0.(408)…
      D: 125 / 333 = 0.(375)…
      H: 72 / 333 = 0.(216)…

      T: 135 / 333 = 0.(405)…
      D: 106 / 333 = 0.(318)…
      H: 92 / 333 = 0.(276)…

      T: 136 / 333 = 0.(408)…
      D: 105 / 333 = 0.(315)…
      H: 92 / 333 = 0.(276)…

      Like

    • Frits's avatar

      Frits 2:57 pm on 8 April 2021 Permalink | Reply

      from collections import defaultdict
      
      alldiff = lambda seq: len(seq) == len(set(seq))
      
      # ceil a positive number
      ceil = lambda n: int(n) if n == int(n) else int(n) + 1
      
      # if we have P/Q = 0.abcabc…
      # then there must be an R so that R * Q = 999
      # 999 is 27 * 37
      
      d = defaultdict(list)
      
      # whole number of pounds is less than fifty (and more than 5)
      for N in [27, 37]:          # skip 9 as 1/9 = 0.111111...
      
       d[N] = [[0, 0]]
       # collect abc's where i / N = 0.abcabc...
       for i in range(1, N):
         d[N].append([i, str(i / N)[2:5]])
      
       # min Harry: H + (H + x) + (H + y) = N and H + y < 2 * H  with 0 < x < y
       #            introduce y = H - p, x = H - q with 0 < p < q
       #            5 * H - p - q = N so H >= N / 5 + 0.6
       minH = ceil(N / 5 + 0.6)
      
       # max Harry: H + (H + x) + (H + y) = N --> H <= (N - 3)/3
       maxH = (N - 3) // 3
       maxH = min((N - 3) // 3, 2 * minH - 1)
      
       for H in d[N][minH:maxH + 1]:
         if not alldiff(H[1]): continue
         for D in d[N][H[0] + 1:(N - H[0] - 1) // 2 + 1]:
           if not alldiff(H[1] + D[1]): continue
      
           # remainder is Tom's number
           T = N - H[0] - D[0]
           if T >= 2 * H[0] or T <= D[0]: continue
           if not alldiff(H[1] + D[1] + d[N][T][1]): continue
      
           print(f"Tom, Dick, Harry: {T}, {D[0]}, {H[0]}")
      

      Like

    • John Crabtree's avatar

      John Crabtree 4:15 pm on 9 April 2021 Permalink | Reply

      The fractions are of the form of abc / 999 = abc / (27 * 37).
      The sum of the “abc”s is 999. The individual digits are 0 or 9, and 1 to 8.
      The “a”s must be 2, 3 and 4. Then the “b”s are 0, 1 and 7, and the “c”s are 5, 6 and 8.
      If the sum were £27, abc = 0 mod 37, but 5 * 37 = 185 and 15 * 37 = 535.
      And so the sum = £37, and abc = 0 mod 27
      And so, by inspection, 15 * 27 =405, and then 8 * 27 = 216 and 14 * 27 = 378
      The solution follows.

      Like

    • Frits's avatar

      Frits 10:51 am on 10 April 2021 Permalink | Reply

      Based on Jim’s unpublished program (in code section of replit).

      This time not using the fact that XY must be 27 or 37.

      #!/usr/bin/env python3 -m enigma -r
      
      # Harry = 0.(EFG)...  EFG = HA * PQ
      # Dick  = 0.(JKL)...  JKL = DI * PQ
      # Tom   = 0.(RST)...  RST = TO * PQ
      #
      # where XY * PQ = 999   (skip XY = 9 as 1/9 = 0.111111...)
      #
      # each gets an integer share of XY (< 50)
      
      SubstitutedExpression
      
      --distinct=""
      # HA is maximal if Dick and Tom have values HA + 1 and HA + 2 
      # so HA < 16 and TO < 30
      --invalid="2|3|4|5|6|7|8|9,H"
      --invalid="3|4|5|6|7|8|9,T"
      --invalid="0|5|6|7|8|9,X"
      
      # XY * PQ = 999
      "div(999, XY) = PQ"                # exact division
      
      # HA is minimal if Dick and Tom have values 2 * HA - 2 and 2 * HA - 1
      # HA is maximal if Dick and Tom have values HA + 1 and HA + 2
      "3 * XY + 9 <= 15 * HA <= 5 * XY - 15"
      
      # TO > DI means TO > XY - HA - TO so 2 * TO > XY - HA
      # Tom got less then twice as much as Harry
      # TO is maximal if Dick is HA + 1 so TO <= XY - 2 * HA - 1
      "XY - HA < 2 * TO <= min(4 * HA - 2, 2 * XY - 4 * HA - 2)"
      
      # together they give the entire amount
      "XY - HA - TO = DI"
      
      # EFG, JKL and RST must use different digits
      "len(set(str(HA * PQ) + str(DI * PQ) + str(TO * PQ))) == 9"
      
      # (Total, Tom, Dick, Harry)
      --answer="XY, TO, DI, HA"
      --verbose=16
      --reorder=0
      

      Like

  • Unknown's avatar

    Jim Randell 9:05 am on 4 April 2021 Permalink | Reply
    Tags: by: Dr K Ollerenshaw   

    Brain-Teaser 29: [Mileage dates] 

    From The Sunday Times, 8th October 1961 [link]

    My birthday was on Sunday last. As I drove my car out of the garage in the morning I noticed that the mileage read the same as the date, 11061 or 1.10.61. The car is driven to the station and back twice every day of the year and is used for no other purpose, the total distance each week being exactly 178 miles. By a coincidence the next occasion when the mileage at some time during the day will read the same as the date will be on my husband’s birthday.

    When is this?

    This puzzle was originally published with no title.

    [teaser29]

     
    • Jim Randell's avatar

      Jim Randell 9:05 am on 4 April 2021 Permalink | Reply

      This Python program runs in 61ms.

      from datetime import (date, timedelta)
      from enigma import (Rational, sprintf, first, arg, printf)
      
      Q = Rational()  # rational implementation
      
      # find "special" days
      def solve():
        m = 11061  # start mileage
        d = date(1961, 10, 1)  # start date
        i = Q(178, 7)  # daily mileage increment
        j = timedelta(days=1)  # daily date increment
      
        while m < 311299:
          # calculate mileage at the end of the day
          m_ = m + i
          # write the date as a string
          s = sprintf("{dd}{mm}{yy:02d}", dd=d.day, mm=d.month, yy=d.year % 100)
          # turn them integers
          (a, b, x) = map(int, (m, m_, s))
          # is x in [a, b]?
          if a <= x <= b:
            printf("{d}: {x} in [{a}, {b}]")
            yield (d, a, b)
          (m, d) = (m_, d + j)
      
      # find the first n solutions
      n = arg(2, 0, int)
      first(solve(), n)
      

      Solution: Sunday, 17th June 1962.

      There is one more 5-digit mileage that occurs on a “special” day:

      21162 on Friday, 2nd November 1962

      And then there are three 6-digit mileages that occur on “special” days in the 1990s:

      281090 on Sunday, 28th October 1990
      291191 on Friday, 29th November 1991
      301292 on Wednesday, 30th December 1992

      Like

  • Unknown's avatar

    Jim Randell 6:03 pm on 1 April 2021 Permalink | Reply
    Tags:   

    Teaser 3054: Discs a go-go 

    From The Sunday Times, 4th April 2021 [link] [link]

    My kitchen floor is tiled with identically-sized equilateral triangle tiles while the floor of the bathroom is tiled with identically-sized regular hexagon tiles, the tiles being less than 1m across. In both cases the gaps between tiles are negligible. After much experimenting I found that a circular disc dropped at random onto either the kitchen or bathroom floor had exactly the same (non-zero) chance of landing on just one tile.

    The length of each side of the triangular tiles and the length of each side of the hexagon tiles are both even triangular numbers of mm (i.e., of the form 1+2+3+…).

    What are the lengths of the sides of the triangular and hexagonal tiles?

    [teaser3054]

     
    • Jim Randell's avatar

      Jim Randell 9:50 pm on 1 April 2021 Permalink | Reply

      For a disc of radius x dropped randomly onto the floor, the centre of it will lie in one of the tiles and we want to find out if the entire disc lies within the tile. Which it will do if the centre of the disc lies within a smaller version of the tile that has a perimeter that is a distance of x from the perimeter of the actual tile.

      If the triangular tiles have a side t, then the area of one tile is: A = (t²√3)/4

      And the inradius is: r = t/(2√3)

      The radius of the disc cannot be larger than the inradius of the triangle.

      So the smaller version of the triangle has an inradius of: (r − x)

      And the corresponding area is: a = (3√3)(r − x)²

      The probability of the disc landing entirely within a single tile is then: P = a / A

      Similarly for a hexagon with side h:

      The area of one hexagonal tile is: B = 3(h²√3)/2 (it is composed of 6 equilateral triangles).

      And the inradius is: s = h(√3)/2

      Again the radius of the disc cannot be larger than the inradius of the hexagon.

      We make a smaller hexagon with inradius = (s − x)

      And the corresponding area is: b = (2√3)(s − x)²

      And the probability of the disc landing entirely within a single tile is: Q = b / B

      This is enough to permit a programmed solution.


      The following program finds possible sides for the triangular and hexagonal tiles, and then looks for pairs that can be solved to give a disc that makes the probabilities the same.

      It runs in 73ms.

      Run: [ @replit ]

      from itertools import product
      from enigma import irange, inf, sqrt, fdiv, find_zero, printf
      
      # root 3
      r3 = sqrt(3)
      
      # find the radius of a disc that makes the probabilities equal
      def solve(t, h):
        # area of a triangular tile
        A = t * t * r3 * 0.25
        r = fdiv(t, 2 * r3)
      
        # hexagonal tile
        B = h * h * r3 * 1.5
        s = h * r3 * 0.5
      
        # find the difference between the probabilities
        def f(x):
          a = 3 * r3 * (r - x) ** 2
          b = 2 * r3 * (s - x) ** 2
          P = fdiv(a, A)
          Q = fdiv(b, B)
          return P - Q
      
        # find a zero of f
        try:
          r = find_zero(f, 1, min(r, s))
        except ValueError:
          return
      
        # output solution
        printf("t={t}mm h={h}mm [x={r.v:.2f}mm]")
      
      # generate even triangular numbers
      def tris(f):
        t = 0
        for i in irange(1, inf):
          t += i
          if t * f > 1000: break
          if t % 2 == 0: yield t
      
      # collect possible sides for the triangular tiles
      ts = list(tris(0.5 * r3))
      
      # collect possible sides for the hexagonal tiles
      hs = list(tris(r3))
      
      # select t and h, and look for a solution
      for (t, h) in product(ts, hs):
        solve(t, h)
      

      Solution: The triangular tiles have sides of 630mm. The hexagonal tiles have sides of 210mm.

      A bit more analysis (see below), shows us that the probabilities are the same when the triangular tiles have sides that are 3 times the sides of the hexagonal tiles. And as long as the disc is small enough to fit inside the tiles its size does not matter.

      So we are just looking for a pair of even triangular numbers (t, h) where t = 3h, which can be found manually or with a simple program.

      Like

      • Jim Randell's avatar

        Jim Randell 8:27 am on 2 April 2021 Permalink | Reply

        [Note: See my next comment for a better way of approach the problem and deriving the relationship between t and h].

        Wolfram Alpha [ link ] simplifies the calculation of x to:

        x = (√3)ht / (3h + t)

        Which gives a faster program:

        from itertools import product
        from enigma import irange, inf, sqrt, fdiv, printf
        
        # root 3
        r3 = sqrt(3)
        
        # find the radius of a disc that makes the probabilities equal
        def solve(t, h):
          if 3 * h <= t and t <= 3 * h:
            # output solution
            printf("t={t}mm h={h}mm")
        
        # generate even triangular numbers
        def tris(f):
          t = 0
          for i in irange(1, inf):
            t += i
            if t * f > 1000: break
            if t % 2 == 0: yield t
        
        # collect possible sides for the triangular tiles
        ts = list(tris(0.5 * r3))
        
        # collect possible sides for the hexagonal tiles
        hs = list(tris(r3))
        
        # select t and h, and look for a solution
        for (t, h) in product(ts, hs):
          solve(t, h)
        

        Like

        • Frits's avatar

          Frits 10:42 am on 2 April 2021 Permalink | Reply

          @Jim,

          Could you explain the line 9 formula (which actually says if t == 3 * h)?

          If we forget about triangular numbers and we choose t=300 and h=100 and a disc so that exactly 3 discs fit in the triangle (touching the sides) do you now say that both chances are not the same?

          Like

          • Jim Randell's avatar

            Jim Randell 11:39 am on 2 April 2021 Permalink | Reply

            @Frits: It looks like the size of the disc doesn’t matter, as long as t = 3h the probabilities will be the same. I’ll look again at my equations to see why x didn’t drop out. (Line 9 is just a restatement of the inradius conditions).

            John Crabtree pointed me towards a better derivation of the relationship:


            If the triangular tiles have a side t, then the area of one tile is: A(t) = T⋅t², where T = (√3)/4

            And the inradius is: r = t/(2√3), so the disc has a radius less than this: x < r

            We make a smaller triangle with inradius = (r − x), it has sides of length: u = (t − 2(√3)x)

            If the centre of the disc lands within this triangle the entire disc will be inside the tile.

            So the area of this smaller triangle is: A(u) = T⋅u²

            And the probability of the disc falling entirely within the triangle (P) is the ratio of these areas:

            P = A(u) / A(t) = (u / t)²

            If the hexagonal tiles have a side h, then the area of the hexagon is: B(h) = H⋅h², where H = (3/2)(√3) (as it is composed of 6 equilateral triangles).

            And the inradius is: s = h(√3)/2, so the radius of the disc is also less than this: x < s

            We make a smaller hexagon with inradius = (s − x), it has sides of length: i = (h − 2x/√3), and the area is: B(i)

            So the probability of the disc falling entirely within the hexagon (Q) is the ratio of these two areas:

            Q = B(i) / B(h) = (i / h)²

            The probabilities are equal when their square roots are equal:

            P = Q
            → (u / t) = (i / h)
            → i⋅t = h⋅u
            → (h − 2x/√3)t = h(t − 2(√3)x)
            → ht − 2xt/√3 = ht − 2(√3)xh
            → 2xt/√3 = 2(√3)xh
            → t = 3h


            Hence the solution is found when t = 3h (and 0 < x < (√3)h/2), and we just need to find two even triangular numbers in the required range, where one is 3 times the other:

            from enigma import (irange, inf, sqrt, divf, printf)
            
            # max tile size
            M = divf(2000, sqrt(3))
            
            # collect even triangular numbers
            ts = set()
            t = 0
            for i in irange(1, inf):
              t += i
              if t > M: break
              if t % 2 == 0:
                (h, r) = divmod(t, 3)
                if r == 0 and h in ts:
                  printf("t={t}mm h={h}mm")
                ts.add(t)
            

            Like

    • Frits's avatar

      Frits 9:55 pm on 1 April 2021 Permalink | Reply

      [corrected]

      # get triangular root
      tri = lambda n: 0.5 * ((8 * n + 1) ** 0.5 - 1.0)
      
      # For the same inner circle the side length of the (smallest outer) triangle
      # must be three times the side length of the (smallest outer) hexagon.
      
      # check triangular numbers
      for i in range(1, 50):
        lHex = (i * (i + 1)) // 2
        lTri = 3 * lHex
       
        # both sides must be even
        if lHex % 2 or lTri % 2: continue
       
        # the tiles being less than 1m across.
        # length across triangle: lTri, length across hexagon: lHex * sqrt(3)
        if lTri > 999:
          break
       
        if tri(3 * lHex) % 1 == 0 :
          print(f"lengths of the sides of the triangular and hexagonal tiles: "
                f"{lTri} mm and {lHex} mm")
      

      Like

      • Jim Randell's avatar

        Jim Randell 10:36 pm on 1 April 2021 Permalink | Reply

        @Frits: I don’t think that can be correct, because the answers have to be even numbers (that are also triangular).

        Like

        • Frits's avatar

          Frits 10:53 pm on 1 April 2021 Permalink | Reply

          @Jim, I didn’t see that (about even).

          I calculated that for the same inner circle the side length of the (smallest outer) triangle must be three times the side length of the (smallest outer) hexagon. I hoped that would answer the probability question as well.

          Like

    • Jim Randell's avatar

      Jim Randell 6:28 pm on 3 April 2021 Permalink | Reply

      Another derivation using sectors of a regular n-gon:


      For a regular n-gon with a side length of s, we can consider a 1/n sector.

      The angle at the centre is 2θ = 360°/n ⇒ θ = 180°/n

      And the area of the entire sector is:

      A = s² / 4tan(θ)

      Reducing the height of the sector by the radius of the disc x gives:

      a = (s/2tan(θ) − x)² tan(θ) = (s − 2x tan(θ))² / 4 tan(θ)

      And the probability of the disc landing entirely within the complete tile is P = na/nA = a/A

      P = ((s − 2x tan(θ)) / s)² = (1 − (2x/s) tan(θ))²

      For the triangle tan(θ) = tan(60°) = √3, and the side length is t.

      For the hexagon tan(θ) = tan(30°) = 1/√3, and the side length is h.

      And the probabilities are equal when:

      (2x/t) tan(60°) = (2x/h) tan(30°)
      h tan(60°) = t tan(30°)
      3h = t


      Here’s a graphical demonstration of the probabilities when 3h = t.

      On the left, the hexagonal tile (green) fits exactly in the triangular tile (red), and we see that if each small triangle has area Z, then the hexagonal tile has an area 6Z and the triangular tile has an area 9Z.

      On the right there is also a smaller version of each tile, that is one radius of the disc away from the perimeter of the corresponding actual tile. If the small triangles have area z, then the small version of the hexagonal tile has an area 6z and the small version of the triangular tile has an area of 9z.

      The probability then of the disc falling in the hexagonal tile is 6z / 6Z = z/Z, and the probability of the disc falling in the triangular tile is 9z / 9Z = z/Z. Hence the probabilities are the same for each tile.

      Like

    • Tony Brooke-Taylor's avatar

      Tony Brooke-Taylor 10:03 am on 4 April 2021 Permalink | Reply

      I finally satisfied myself by working in terms of the vertical height of the triangles. Referring to Jim’s first diagram, for the triangular tiles, the vertical height of the inner triangle has the relationship:

      Mt’ = Mt-3x

      This is because the triangles are congruent and have the same centroid, the centroid being 1/3 of the way along the vertical height.

      For each of the six triangles in the hexagon, the inner triangle has vertical height:

      Mh’ = Mh-x

      This is because we only need to have a border for the circular disc at the outer edge of the hexagon, or the ‘bottom’ of each triangular sector.

      The area of each triangle is proportionate to the square of any relevant length, so we can ignore all constants of proportionality and state that:

      (Mt’/Mt)^2 = (Mh’/Mh)^2

      implies ((Mt-3x)/Mt)^2 = ((Mh-x)/Mh)^2

      This condition is satisfied if we make the substitution Mt=3Mh (or just take square roots of each side of the equation and rearrange).

      To find the solution, I just looped over possible values for h out of the set of even triangular numbers, setting the limit assuming the ‘across’ distance was measured from flat to flat, like a spanner.

      
      h_lim = 1000/3**(1/3)#limit width of a horizontal tile
      
      i_lim = int(((8*h_lim+1)**(1/2)-1)/2)+1#implied limit of possible triangular numbers
      
      result=[h for h in [member for item in [[i*(i+1)/2,(i+2)*(i+1)/2] for i in range(3,i_lim,4)] for member in item] if ((h*24+1)**(1/2)-1)%2==0]
      
      print("The triangles have side",result[0]*3,"mm; the hexagons",result[0],"mm")
      
      

      Like

  • Unknown's avatar

    Jim Randell 10:02 am on 1 April 2021 Permalink | Reply
    Tags:   

    Teaser 2782: Spiders 

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

    Spiders Beth and Sam wake up in the bottom corner of a cuboidal barn (all of whose sides are whole numbers of metres). They want to reach the opposite bottom corner without actually walking across the floor. Beth decides to walk on one of five possible shortest routes, two of them being around the edge of the floor and the other three being over the walls and ceiling. Sam decides instead to spin a web directly to the point on the ceiling diagonally opposite the starting point and then to drop down into the corner. The total length of his journey is within five centimetres of a whole number of metres.

    How high is the barn?

    [teaser2782]

     
    • Jim Randell's avatar

      Jim Randell 10:04 am on 1 April 2021 Permalink | Reply

      Suppose the cuboid barn has dimensions width = x, length = y, height = z (all of which are positive integer values).

      The spiders wish to traverse from one corner of the floor to the diagonally opposite corner, but without crossing the floor.

      Beth walks along one of the 5 shortest routes (which presumably means that there are 5 shortest routes that have the same distance travelled).

      Two of these routes (p1, p2) are along the edges of the floor:

      To see the other routes we can “unfold” the barn (imagine it is a cardboard box), and the shortest paths will appear as straight lines.

      We get a route that crosses the front, ceiling and left wall (p3):

      And routes that cross the right wall, ceiling, back wall (p4), and right wall, ceiling, left wall (p5).

      So the following expressions all give the square of the minimal path length:

      [p1, p2] (x + y)² = x² + y² + 2xy
      [p5] (x + 2z)² + y² = x² + y² + 4z² + 4xz
      [p3, p4] (x + z)² + (y + z)² = x² + y² + 2z² + 2xz + 2yz

      So, given values for x and y, we can calculate z, and then look for diagonals of the cube (= hypot(x, y, z)) that are within 5cm of a whole number of metres, to give Sam’s path.

      This Python program runs in 47ms.

      Run: [ @replit ]

      from enigma import (irange, inf, quadratic, hypot, first, arg, printf)
      
      # generate solutions
      def solve(verbose=1):
      
        # consider increasing lengths
        for y in irange(2, inf):
          # and widths
          for x in irange(1, y - 1):
            # compute corresponding z (using p1, p2 & p5)
            for z in quadratic(4, 4 * x, -2 * x * y, domain="Z"):
              if not (z > 0): continue
      
              # check against p3, p4
              if not ((x + y) ** 2 == (y + z) ** 2 + (x + z) ** 2): continue
      
              # calculate the length of diagonal through the cuboid
              h = hypot(x, y, z)
              d = abs(h - int(h + 0.5)) 
              # check it is within 5cm of a whole number of metres
              if d > 0.05: continue
      
              # lengths of paths for Beth and Sam
              (b, s) = (x + y, h + z)
              if verbose:
                printf("z={z} [x={x} y={y}; h={h:.3f} d={d:.3f}, beth={b} sam={s:.3f}]")
      
              yield (x, y, z)
      
      # find the first n solutions
      n = arg(1, 0, int)
      first(solve(), n)
      

      Solution: The barn in 4 metres high.

      In fact there is a family of solutions, the program stops after the first (smallest) solution, which is the only reasonable one, where the barn is less 25m high (and the spiders journeys are each less than 100m).


      Analytically:

      Equating the sum of the first two of the expressions with twice the third, we get:

      2x² + 2y² + 4z² + 2xy + 4xz = 2x² + 2y² + 4z² + 4xz + 4yz
      2xy = 4yz
      x = 2z

      Then, substituting for x in the first 2 expressions, and equating them:

      4z² + y² + 4yz = 16z² + y²
      4yz = 12z²
      y = 3z

      Hence the barn has dimensions (2z, 3z, z), and the shortest paths have length 5z.

      The diagonal across the barn is: √(x² + y² + z²) = √(14z²) = (√14)z.

      And we want to know when this is within 5cm if a whole number of metres.

      Here is a shorter and faster program to generate solutions:

      Run [ @replit ]

      from enigma import (irange, inf, sqrt, first, arg, printf)
      
      def solve():
        r14 = sqrt(14)
      
        for z in irange(1, inf):
          h = z * r14
          d = abs(h - int(h + 0.5))
          if d > 0.05: continue
      
          (x, y) = (2 * z, 3 * z)
          (b, s) = (x + y, h + z)
          printf("z={z} [x={x} y={y}; h={h:.3f} d={d:.3f}; beth={b} sam={s:.3f}]")
      
          yield (x, y, z)
      
      # find the first n solutions
      n = arg(1, 0, int)
      first(solve(), n)
      

      Like

  • Unknown's avatar

    Jim Randell 11:29 am on 30 March 2021 Permalink | Reply
    Tags:   

    Brain-Teaser 730: Miss Brain-Teaser 

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

    Five girls entered our Village Beauty Contest. Each of the five judges had to put them into a definite order and then allot fifteen marks between them. The aggregate marks for each girl decided the issue. The judges each gave a different maximum mark and also chose a different girl to be top. No girl had two identical marks in her score.

    Joe gave the maximum possible to Rose, and he placed Gwen above Linda. Tom gave Ann one more than Sam did. Pam had no zero in her score and although she finished ahead of Rose she didn’t win. Ann scored the only five that was allotted. Dan placed the girls as closely together as he could.

    The judge who put Gwen first put Rose last. Brian succeeded in putting them all in their correct final order.

    Who won, and what were her individual marks from each judge?

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

    [teaser730]

     
    • Jim Randell's avatar

      Jim Randell 11:30 am on 30 March 2021 Permalink | Reply

      I think this one is easier to do with a bit of scribbling on the back of an envelope.

      But here is a Python program that solves the puzzle in 851ms.

      The program groups together the possible sets of scores by the maximum number of marks given. And then selects scores that give a different maximum for each judge. We know Joe gives the absolute maximum, and Dan gives scores that as close together as possible. The distribution of scores is chosen such that no contestant receives the same number of marks from two judges. And then we check the other conditions as we go along.

      Run: [ @replit ]

      from enigma import (irange, group, Accumulator, diff, update, flatten, tuples, seq_all_different, find, map2str, printf)
      
      # labels for the contestants, and number of contestants (N)
      (A, G, L, P, R, N) = irange(0, 5)
      
      # decompose <t> into <k> increasing numbers, minimum <m>
      def decompose(t, k, m=0, ns=[]):
        if k == 1:
          if not (t < m):
            yield ns + [t]
        else:
          k_ = k - 1
          for n in irange(m, t - k_ * m):
            yield from decompose(t - n, k_, n + 1, ns + [n])
      
      # collect possible scores, by maximum marks given
      scores = group(decompose(15, N), by=max)
      
      # find scores containing the minimim possible difference (for Dan)
      r = Accumulator(fn=min, collect=1)
      for s in flatten(scores.values()):
        r.accumulate_data(max(b - a for (a, b) in tuples(s, 2)), s)
      mds = r.data
      
      # check a (partial) collection of scores
      def check(*ss):
        # consider the scores for the judges:
        # each judge places a different contestant first
        fs = set()
        for s in ss:
          f = s.index(max(s))
          if f in fs: return
          # G first -> R last
          if f == G and s.index(min(s)) != R: return
          fs.add(f)
        # consider the scores for the contestants:
        for (i, s) in enumerate(zip(*ss)):
          # A scores the only 5
          if i == A and len(s) == N and 5 not in s: return
          if i > A and 5 in s: return
          # P has no zeros
          if i == P and 0 in s: return
        # looks OK
        return 1
      
      # empty scores
      s0 = [None] * N
      
      # generate scores with max marks in <mms>
      # <f5> current value of the 5 flag
      def generate(mms, f5):
        for (k, vs) in scores.items():
          if k in mms:
            for ss in vs:
              # is a 5 involved?
              s5 = (5 in ss)
              if not (f5 + s5 > 1):
                yield (k, s5, ss)
      
      # assign marks <ms> to score <s> starting at index <k>, respecting <gs>
      def _assign(s, gs, ms, k=0):
        # are we done?
        if not ms:
          yield s
        elif s[k] is not None:
          yield from _assign(s, gs, ms, k + 1)
        else:
          # fill out index k
          for (i, v) in enumerate(ms):
            if v not in gs[k]:
              yield from _assign(update(s, [(k, v)]), gs, ms[:i] + ms[i + 1:], k + 1)
      
      # assign marks <ms>, respecting (i, v) pairs, and current sequences <ss>
      def assign(ms, ss, ivs=()):
        # find current marks per contestant
        gs = (list(zip(*ss)) if ss else [[]] * N)
        # assign any given values
        s = list(s0)
        for (i, v) in ivs:
          if v in gs[i]: return
          s[i] = v
        # and fill out remaining values
        yield from _assign(s, gs, diff(ms, s))
      
      # select marks for Joe, must include the maximum possible score
      mxj = max(scores.keys())
      for (_, j5, js) in generate([mxj], 0):
        # Joe gave the max score to R
        for J in assign(diff(js, [mxj]), [], [(R, mxj)]):
          # Joe gave G more marks than L
          if not (J[G] > J[L]): continue
          if not check(J): continue
      
          # select marks for Dan, as close together as possible
          for ds in mds:
            mxd = max(ds)
            if mxd == mxj: continue
            # is a 5 involved?
            d5 = (5 in ds)
            if j5 + d5 > 1: continue
            for D in assign(ds, [J]):
              if not check(J, D): continue
      
              # select marks for Sam
              kss = diff(scores.keys(), {mxj, mxd})
              for (mxs, s5, ss) in generate(kss, j5 + d5):
                for S in assign(ss, [J, D]):
                  if not check(J, D, S): continue
      
                  # select marks for Tom
                  kst = diff(kss, [mxs])
                  for (mxt, t5, ts) in generate(kst, j5 + d5 + s5):
                    # Tom gave A 1 point more than Sam
                    i = find(ts, S[A] + 1)
                    if i == -1: continue
                    for T in assign(diff(ts, [ts[i]]), [J, D, S], [(A, ts[i])]):
                      if not check(J, D, S, T): continue
      
                      # select marks for Brian
                      ksb = diff(kst, [mxt])
                      for (mxb, b5, bs) in generate(ksb, j5 + d5 + s5 + t5):
                        if d5 + j5 + s5 + t5 + b5 != 1: continue
                        for B in assign(bs, [J, D, S, T]):
                          if not check(J, D, S, T, B): continue
      
                          # find the totals for the contestants
                          pts = list(sum(s) for s in zip(J, D, S, T, B))
                          if not seq_all_different(pts): continue
                          # P finished ahead of R, but didn't win
                          if not (pts[P] > pts[R] and pts[P] != max(pts)): continue
      
                          # Brian chose the correct order
                          order = lambda s: sorted(irange(N), key=(lambda i: s[i]), reverse=1)
                          pos = order(pts)
                          if not (order(B) == pos): continue
      
                          # output solution
                          w = pos[0]
                          ws = tuple(x[w] for x in [J, D, S, T, B])
                          printf("winner={w} {ws}\n\n    A  G  L  P  R\n J={J}\n D={D}\n S={S}\n T={T}\n B={B}\n",
                            w="AGLPR"[w], ws=map2str(zip("JDSTB", ws)),
                          )
      

      Solution: Linda won. Her marks were: Joe = 0, Dan = 3, Sam = 4, Tom = 2, Brian = 8.

      The full scores are:

      1st: Linda = 17; (J = 0, D = 3, S = 4, T = 2, B = 8)
      2nd: Pam = 16; (J = 3, D = 2, S = 1, T = 6, B = 4)
      3rd: Rose = 15; (J = 9, D = 1, S = 0, T = 3, B = 2)
      4th: Gwen = 14; (J = 2, D = 4, S = 7, T = 0, B = 1)
      5th: Ann = 13; (J = 1, D = 5, S = 3, T = 4, B = 0)

      Like

      • Frits's avatar

        Frits 1:52 pm on 30 March 2021 Permalink | Reply

        Wow, 142 lines of code.

        Like

      • Jim Randell's avatar

        Jim Randell 4:25 pm on 31 March 2021 Permalink | Reply

        With a bit of analysis we can get a neater program (although it’s not really any faster – I timed it at 805ms, so it is still under a second).

        Looking at the decompositions of 15 into 5 different numbers we see that the maximum possible mark in any decomposition is 9, and there is only one decomposition corresponding to this maximum, so Joe’s marks must be (in some order):

        0, 1, 2, 3, 9

        Similarly there is only one decomposition where the marks are consecutive, so this must be Dan’s:

        1, 2, 3, 4, 5

        So Dan gives out the only 5 (to Ann), and that is his maximum mark. This means we know where the 5 is, and don’t have to worry about tracking it in the code.

        And for the three remaining judges they must give maximum marks of 6, 7, 8 and for each maximum there is only one decomposition for each that does not include 5. So the marks from the remaining judges (Sam, Tom and Brian) are (in some order):

        0, 2, 3, 4, 6
        0, 1, 3, 4, 7
        0, 1, 2, 4, 8

        The following program awards Joe’s 9 to Rose, and Dan’s 5 to Ann and fills out the remaining marks recursively.

        Run: [ @replit ]

        from itertools import permutations
        from enigma import (irange, diff, update, seq_all_different, map2str, printf)
        
        # available scores (highest to lowest)
        # each sums to 15, and has a different max
        score = [
          (9, 3, 2, 1, 0),  # this is J's
          (5, 4, 3, 2, 1),  # this is D's
          (8, 4, 2, 1, 0),
          (7, 4, 3, 1, 0),
          (6, 4, 3, 2, 0),
        ]
        # indices for BST (we don't know which is which, yet)
        BST = (2, 3, 4)
        
        # labels for the contestants (and number of contestants)
        girls = (A, G, L, P, R) = (0, 1, 2, 3, 4)
        N = 5
        
        # check map <m>, up to row <k>
        def check(m, k, gs):
          mk = m[k]
          # P has no zeros
          if mk[P] == 0: return
          s = score[k]
          # if G is first, R must be last
          if mk[G] == s[0] and mk[R] != s[4]: return
          if k == 0:
            # check for Joe: G > L
            if not (mk[G] > mk[L]): return
          else:
            # no girl recieves the same mark from 2 different judges
            if any(x in y for (x, y) in zip(mk, gs)): return
          # looks OK
          return 1
        
        # associate with each score: m[score-index][contestant] = marks
        # <gs> = marks for each contestant so far
        # <fs> = contestants that have been awarded a judges highest mark
        def solve(m, k=0, gs=[[]] * N, fs=[]):
          # are we done?
          if k == N:
            yield (m, gs)
          else:
            # consider possibilities for score k
            mk = m[k]
            s = score[k]
            mx = max(s)
            # find assigned contestants
            cs = set(i for i in girls if mk[i] is None)
            # map the unassigned scores to the unassigned contestants
            for ms in permutations(diff(s, mk)):
              # make the updated row
              mk_ = update(mk, cs, ms)
              # check the highest score is to a new contestant
              f = mk_.index(mx)
              if f in fs: continue
              # update the map
              m_ = update(m, [(k, mk_)])
              if not check(m_, k, gs): continue
              gs_ = list(xs + [x] for (xs, x) in zip(gs, mk_))
              yield from solve(m_, k + 1, gs_, fs + [f])
        
        # initial map
        m0 = list([None] * N for _ in irange(N))
        m0[0][R] = 9  # J's highest score goes to R
        m0[1][A] = 5  # D's 5 goes to A
        
        # order girls by scores <s> (highest to lowest)
        order = lambda s: sorted(girls, key=(lambda g: s[g]), reverse=1)
        
        # consider possible completed maps
        for (m, gs) in solve(m0):
          # calculate the total score for each contestant
          pts = list(sum(g) for g in gs)
          if not seq_all_different(pts): continue
          # P finished ahead of R, but didn't win
          if not (pts[P] > pts[R] and pts[P] != max(pts)): continue  
          pos = order(pts)
          # Brian placed the girls in the correct order
          for B in BST:
            if order(m[B]) != pos: continue
            # Tom gave 1 more point to A than Sam
            for (S, T) in permutations(diff(BST, [B])):
              if m[S][A] + 1 != m[T][A]: continue
              # output solution
              w = pos[0]
              ws = tuple(x[w] for x in m)
              js = update("JD???", (B, S, T), "BST")
              printf("winner = {w} {ws}\n\n    A  G  L  P  R\n {m}\n",
                w="AGLPR"[w], ws=map2str(zip(js, ws)), m=map2str(zip(js, m), sep="\n ", enc=""),
              )
        

        In my first program I wrote the [[ assign() ]] stuff to avoid permutations that would fail (which did make it run a bit faster). I didn’t bother for this program.

        Like

    • Frits's avatar

      Frits 12:42 pm on 31 March 2021 Permalink | Reply

      Use denest=1 if not running with PyPy.

      It would be nice if an intermediate assignment could be added to SubstitutedExpression (for the sumcols list). It is now calculated multiple times.

      from enigma import SubstitutedExpression
      
      '''    An Gw Li Pa Ro
        Joe=[A  B  C  D  E]
        Dan=[F  G  H  I  J]
        Sam=[K  L  M  N  O]
        Tom=[P  Q  R  S  T]
      Brian=[U  V  W  X  Y] 
      ''' 
      
      # column totals
      sumcols = lambda s: list(map(sum, zip(*s)))
      
      # are list a and b ordered the same?
      def same_order(a, b):
        s1 = sorted((x, i) for i, x in enumerate(a))
        s2 = sorted((x, i) for i, x in enumerate(b))
        return all(x[1] == y[1] for x, y in zip(s1, s2))
      
      # the alphametic puzzle
      p = SubstitutedExpression(
        [
        # row totals must be 15, choose RHS the variable with the most candidates
        "15 - (A + C + D + E) = B",
        "15 - (F + G + I + J) = H",
        "15 - (K + L + N + O) = M",
        "15 - (P + Q + S + T) = R",
        "15 - (U + V + X + Y) = W",
       
        # Joe placed Gwen above Linda.
        "B > C",
       
        # Dan placed the girls as closely together as he could
        # Dan's scores has to involve a 5 and may not include maximums 6, 7, 8 or 9
        "sorted([G, H, I, J]) == [1, 2, 3, 4]",
       
        # the judge who put Gwen first put Rose last.
        "L != max([K, L, M, N, O]) or O == min([K, L, M, N, O])",
        "Q != max([P, Q, R, S, T]) or T == min([P, Q, R, S, T])",
        "V != max([U, V, W, X, Y]) or Y == min([U, V, W, X, Y])",
      
        # Tom gave Ann one more than Sam did.
        "K + 1 = P",
       
        # Pam finished ahead of Rose
        "sum([D, I, N, S, X]) > sum([E, J, O, T, Y])",
      
        # the judges each gave a different maximum mark
        "len(set([9, 5, max([K,L,M,N,O]), max([P,Q,R,S,T]), \
                  max([U,V,W,X,Y])])) == 5",
       
        # the judges chose a different girl to be top (Dan in col 0, Joe in col 4)
        "len(set([0, 4, \
        [i for i, x in enumerate([K,L,M,N,O]) if x == max([K,L,M,N,O])][0],\
        [i for i, x in enumerate([P,Q,R,S,T]) if x == max([P,Q,R,S,T])][0],\
        [i for i, x in enumerate([U,V,W,X,Y]) if x == max([U,V,W,X,Y])][0]])) == 5",
      
        # the aggregate marks for each girl decided the issue
        "len(set(sumcols([[A,B,C,D,E], [F,G,H,I,J], [K,L,M,N,O], [P,Q,R,S,T], \
                          [U,V,W,X,Y]]))) == 5",
       
        # Pam didn't win
        "max(enumerate(sumcols([[A,B,C,D,E], [F,G,H,I,J], [K,L,M,N,O], \
            [P,Q,R,S,T], [U,V,W,X,Y]])), key=(lambda x: x[1]))[0] != 3",
                               
        # Brian succeeded in putting them all in their correct final order
        "same_order(sumcols([[A,B,C,D,E], [F,G,H,I,J], [K,L,M,N,O], [P,Q,R,S,T], \
                             [U,V,W,X,Y]]), [U,V,W,X,Y])",
        ],
        answer="[A,B,C,D,E], [F,G,H,I,J], [K,L,M,N,O], [P,Q,R,S,T], [U,V,W,X,Y],\
                max(enumerate(sumcols([[A,B,C,D,E], [F,G,H,I,J], [K,L,M,N,O], \
                             [P,Q,R,S,T], [U,V,W,X,Y]])), key=(lambda x: x[1]))[0]",
        # no girl had two identical marks in her score
        distinct=("ABCDE", "FGHIJ", "KLMNO", "PQRST", "UVWXY",
                  "AFKPU", "BGLQV", "CHMRW", "DINSX", "EJOTY" ),
        env=dict(sumcols=sumcols, same_order=same_order),
        # Pam had no zero in her score, Joe gave maximum to Rose (E = 9)
        # Ann scored the only five (in first column, F = 5))
        # first and last column may not contain maximums 6, 7 and 8
        s2d=dict(E=9, F=5),
        digits=(0, 1, 2, 3, 4, 6, 7, 8),     # remaining digits
        d2i=dict([(0, "EFGHJBPDINSX")] +
             [(k, "EF") for k in range(1, 4)] +
             [(4, "EFJOTYK")] +
             [(k, "FGHIJEOTYAKPU") for k in range(6, 8)] +
             [(8, "FGHIJEOTYKAPUC")]),
        denest=1,      # use denest=1 if not run with PyPy
        verbose=0
      )
      
      judges = ["Joe", "Dan", "Sam", "Tom", "Brian"]
      girls = ["Agnes", "Gwen", "Linda", "Pam", "Rose"]
      
      for (_, ans) in p.solve():
        print(girls[ans[-1]], "has won")
        print(*[judges[i] + ": " + str(x[ans[-1]]) for i, x in enumerate(ans[:-1])])
      

      Like

    • Frits's avatar

      Frits 1:29 pm on 31 March 2021 Permalink | Reply

      Faster than Jim’s program with PyPy but slower with Python 3.9.2.

      from itertools import permutations
      
      # do all columns have different values in the multidimensional array <s> ?
      diffcols = lambda s: all(len(set(c)) == len(c) for c in list(zip(*s)))
      
      # are list a and b ordered the same?
      def same_order(a, b):
        s1 = sorted((x, i) for i, x in enumerate(a))
        s2 = sorted((x, i) for i, x in enumerate(b))
        return all(x[1] == y[1] for x, y in zip(s1, s2))
        
      # decompose <t> into <k> increasing numbers, minimum <m>
      def decompose(t, k, m=0, ns=[]):
        if k == 1:
          if not(t < m):
            yield ns + [t]
        else:
          k_ = k - 1
          for n in range(m, t + 1 - k_ * m):
            yield from decompose(t - n, k_, n + 1, ns + [n])
            
      def check(s, mult, m, ind, imult):
        if ind in imult:           # the judges chose a different girl to be top
          return False 
       
        if sum(x[0] == 5 for x in mult) > 1: # Ann scored the only five
          return False
        
        if s[1] == m and s[4] != min(s): # the judge who put Gwen first put Rose last
          return False
        
        if not diffcols(mult):     # no girl had two identical marks in her score
          return False 
        
        return True
      
      judges = ["Joe", "Dan", "Sam", "Tom", "Brian"]
      girls = ["Agnes", "Gwen", "Linda", "Pam", "Rose"]
      scores = list(x for y in [list(permutations(s)) for s in decompose(15, 5)] 
                    for x in y)
      
      # Ann scored the only five. Pam had no zero in her score
      scores = [x for x in scores if 5 not in x[1:] and 9 not in x[:4] and x[3] != 0]
      scores9 = [x for x in scores if x[4] == 9]
      scoresnot9 = [(x, max(x)) for x in scores if x[4] != 9]
      
      for D in [x[0] for x in scoresnot9]:
        # Dan placed the girls as closely together as he could
        s = sorted(D)
        if not all(y == (x + 1) for (x, y) in list(zip(s, s[1:]))): continue
        
        mD = max(D)
        # the judge who put Gwen first put Rose last
        if D[1] == mD and D[4] != min(D): continue
        iD = D.index(mD)
        
        for J in scores9:                       # Joe gave maximum to Rose
          iJ = 4                                # last position
          if iJ == iD: continue
          if J[1] < J[2]: continue              # Joe placed Gwen above Linda
          if not diffcols([D, J]): continue     # no identical marks per girl
          
          for S in [x[0] for x in scoresnot9 if x[1] not in {mD}]:
            mS = max(S)
            iS = S.index(mS)
            if not check(S, [D, J, S], mS, iS, {iD, iJ}): continue 
                         
            for T in [x[0] for x in scoresnot9 if x[1] not in {mD, mS}]:            
              if T[0] != S[0] + 1: continue     # Tom gave Ann one more than Sam
              
              mT = max(T)
              iT = T.index(mT)
              if not check(T, [D, J, S, T], mT, iT, {iD, iJ, iS}): continue 
              
              for B in [x[0] for x in scoresnot9 if x[1] not in {mD, mS, mT}]:  
                mB = max(B)
                iB = B.index(mB)
                if not check(B, [D, J, S, T, B], mB, iB, {iD, iJ, iS, iT}): continue 
               
                sumcols = list(map(sum, zip(D, J, S, T, B)))
                if len(set(sumcols)) != 5: continue  # totals must be different
                
                winning = max(sumcols)
                # Pam finished ahead of Rose but she didn’t win.
                if sumcols[3] < sumcols[4] or sumcols[3] == winning: continue
      
                # Brian succeeded in putting them all in their correct final order
                if not same_order(sumcols, B): continue
                
                # Ann scored the only five 
                if [J[0], D[0], S[0], T[0], B[0]].count(5) != 1: 
                  continue
                
                ind = sumcols.index(winning)
                print(girls[ind], "has won")
                print(*[judges[i] + ": " + str(x[ind]) 
                        for i, x in enumerate([J, D, S, T, B])])
      

      Like

    • Jim Randell's avatar

      Jim Randell 7:03 pm on 31 March 2021 Permalink | Reply

      Here is a solution that uses the analysis given above to determine the five sets of scores (but not the orderings of the sets), and then using the [[ SubstitutedExpression() ]] solver from the enigma.py library to find candidate solutions, followed by a check of the remaining conditions. It runs in 208ms.

      Run: [ @replit ]

      from enigma import (
        SubstitutedExpression, join, encl, seq_all_different, subsets, update, map2str, printf
      )
      
      SubstitutedExpression.set_default(denest=-1, verbose=0)
      
      #      ann gwe lin pam ros
      # joe:  A   B   C   D   E   += 15 
      # dan:  F   G   H   I   J   += 15
      # xxx:  K   L   M   N   P   += 15
      # yyy:  Q   R   S   T   U   += 15
      # zzz:  V   W   X   Y   Z   += 15
      
      # marks from the judges
      (joe, dan, xxx, yyy, zzz) = ("ABCDE", "FGHIJ", "KLMNP", "QRSTU", "VWXYZ")
      # marks for the contestants
      (ann, gwe, lin, pam, ros) = ("AFKQV", "BGLRW", "CHMSX", "DINTY", "EJPUZ")
      # indices for top girls
      tops = "abcde"
      
      xset = lambda x: join(x, fn=encl, sep=", ", enc="{}")
      xseq = lambda x: join(x, fn=encl, sep=", ", enc="()")
      xsum = lambda x: join(x, fn=encl, sep=" + ", enc="()")
      
      top = lambda t: t.index(max(t))
      
      p = SubstitutedExpression(
        [
          # we know the marks from each judge (but not the order)
          f"{xset(joe)} == {{0, 1, 2, 3, 9}}",
          f"{xset(dan)} == {{1, 2, 3, 4, 5}}",
          f"{xset(xxx)} == {{0, 1, 2, 4, 8}}",
          f"{xset(yyy)} == {{0, 1, 3, 4, 7}}",
          f"{xset(zzz)} == {{0, 2, 3, 4, 6}}",
      
          # Joe placed G above L
          "B > C",
      
          # P finished ahead of R ...
          f"{xsum(pam)} > {xsum(ros)}",
      
          # ... but didn't win
          f"{xsum(pam)} < max({xsum(gwe)}, {xsum(lin)}, {xsum(ann)})",
      
          # each judge chose a different top girl
          f"top({xseq(joe)}) = {{a}}",
          f"top({xseq(dan)}) = {{b}}",
          f"top({xseq(xxx)}) = {{c}}",
          f"top({xseq(yyy)}) = {{d}}",
          f"top({xseq(zzz)}) = {{e}}",
      
          # whichever of xxx, yyy, zzz placed G top, also placed R bottom
          f"implies(max({xseq(xxx)}) == {{L}}, min({xseq(xxx)}) == {{P}})",
          f"implies(max({xseq(yyy)}) == {{R}}, min({xseq(yyy)}) == {{U}})",
          f"implies(max({xseq(zzz)}) == {{W}}, min({xseq(xxx)}) == {{Z}})",
        ],
      
        # each judge allocates different marks to each girl
        # no girl had the same marks from different judges
        distinct=(joe, dan, xxx, yyy, zzz, ros, gwe, lin, ann, pam, tops),
      
        # Joe gave 9 to R; Dan gave 5 to A
        s2d=dict(E=9, F=5),
      
        # so the remaining digits are...
        digits=(0, 1, 2, 3, 4, 6, 7, 8),
      
        # P had no 0 from any judge ...
        d2i={
          0: pam + dan,
          1: zzz,
          2: yyy,
          3: xxx,
          4: joe,
          6: joe + dan + xxx + yyy + tops,
          7: joe + dan + xxx + zzz + tops,
          8: joe + dan + yyy + zzz + tops,
        },
        
        env=dict(top=top),
      )
      
      # order of contestants
      girls = (A, G, L, P, R) = (0, 1, 2, 3, 4)
      
      # order girls by scores <s> (highest to lowest)
      order = lambda s: sorted(girls, key=(lambda g: s[g]), reverse=1)
      
      # check a candidate solution
      def check(s):
        # calcuate the total score for each contestant
        ms = list(list(s[x] for x in xs) for xs in (ann, gwe, lin, pam, ros))
        pts = list(sum(m) for m in ms)
        if not seq_all_different(pts): return
        pos = order(pts)
        # assign xxx, yyy, zzz to B, S, T
        BST = (xxx, yyy, zzz)
        for (B, S, T) in subsets(BST, size=len, select="P"):
          # Tom gave 1 more point to A than Sam
          if s[S[A]] + 1 != s[T[A]]: continue
          # Brian placed the girls in the correct order
          if order(list(s[x] for x in B)) != pos: continue
      
          # output solution
          w = pos[0]
          ws = ms[w]
          js = "JD" + update("???", (BST.index(x) for x in (B, S, T)), "BST")
          ss = list(list(s[x] for x in xs) for xs in (joe, dan, xxx, yyy, zzz))
          printf("winner = {w} {ws}\n\n    A  G  L  P  R\n {m}\n",
            w="RGLAP"[w], ws=map2str(zip(js, ws)), m=map2str(zip(js, ss), sep="\n ", enc=""),
          )
      
      # run the solver
      p.run(check=check)
      

      Like

  • Unknown's avatar

    Jim Randell 9:05 am on 29 March 2021 Permalink | Reply
    Tags: by: L. J. Upton   

    Brainteaser 1040: An uncommon number 

    From The Sunday Times, 4th July 1982 [link]

    Your problem this week is to find an unusual nine-digit number. It comprises the digits from 1 to 9, in some order, each used once and only once.

    The number formed by the first digit (reading from the left) is exactly divisible by 1 (which doesn’t tell you a lot!), the number formed by the first two digits is exactly divisible by 2, that formed by the first three digits is exactly divisible by 3, and so on, which the number formed by the first eight digits being divisible by 8, and with the complete number of nine digits being divisible by 9.

    It is perhaps surprising that such a number exists, and even more surprising that is should be unique.

    What is the number?

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

    [teaser1040]

     
    • Jim Randell's avatar

      Jim Randell 9:06 am on 29 March 2021 Permalink | Reply

      This puzzle has also appeared recently as Puzzle #102, Teaser 3053.

      See my comment on Enigmatic Code for the solution.


      Here are the results for an n-digit (decimal) number, consisting of some arrangement of the digits 1..n, such that the leftmost k digits are divisible by k, for k = 1..n:

      [1] = 1
      [1..2] = 12
      [1..3] = 123; 321
      [1..4] = none
      [1..5] = none
      [1..6] = 123654; 321654
      [1..7] = none
      [1..8] = 38165472
      [1..9] = 381654729
      [1..0] = 3816547290

      And taking the final entry, a 10-digit number in base 10, using all 10 digits, such that the leftmost k digits are divisible by k, for k = 1..10; we can extend this to other bases:

      base 2: 10
      base 4: 1230; 3210
      base 6: 143250; 543210
      base 8: 32541670; 52347610; 56743210
      base 10: 3816547290
      base 12: none
      base 14: 9C3A5476B812D0
      base 16-30: none

      (see: OEIS A11456 [ @oeis ]).

      Like

      • Jim Randell's avatar

        Jim Randell 6:01 pm on 29 March 2021 Permalink | Reply

        And if we drop the requirement that digits cannot be reused, we can see that any left prefix of a polydivisible number [ @wikipedia ] must also be polydivisible.

        This program generates all possible polydivisible numbers in a given base (and with a given set of digits):

        from enigma import (irange, int2base, args, printf)
        
        ds = args([10], 0, int)
        base = ds.pop(0)
        if not ds: ds = list(irange(base))
        printf("[base = {base}; digits = {ds}]")
        
        (k, ns) = (1, list(ds))
        while ns:
          (k_, ns_) = (k + 1, list())
          
          for n in ns:
            # output current numbers
            printf("{k} -> {n}", n=int2base(n, base=base))
            # can we extend this number?
            if n > 0:
              for d in ds:
                n_ = base * n + d
                if n_ % k_ == 0:
                  ns_.append(n_)
        
          (k, ns) = (k_, ns_)
        

        And we find that the longest base 10 polydivisible number, using the digits 0-9 (although 1 and 9 do not appear), has 25 digits:

        3608528850368400786036725

        In base 16 there are 3 maximal length (39-digit) polydivisible numbers:

        34E4A468166CD8604EC0F8106AB4326098286CF;
        AA44CE207C78FC30003C3CC0D8382E2078D07EF;
        FAE06678C2E884607EB8B4E0B0A0F0603420342

        Like

  • Unknown's avatar

    Jim Randell 8:57 am on 28 March 2021 Permalink | Reply
    Tags:   

    Brain-Teaser 28: Army squares 

    From The Sunday Times, 1st October 1961 [link]

    Kublis Ghen was very particular about his army formations. Originally, each company consisted of a certain number of men who could be drawn up in the form of a perfect square. Nor was this all, for when the companies were drawn up one behind the other (each company being spread out to form a single row) the entire army itself thus constituted a square. It was an army to be proud of, but when the great conqueror determined to attack Thalbazzar he was not content, and summoned his chief of staff: “My army is not big enough”, he declared. “Double it”.

    Knowing the temper of his master, the chief of staff saw to it that the size of the army was doubled — exactly. But an unforeseen difficulty arose: the army could no longer form a perfect square — there was just one man too many.

    “Kill him”, ordered the conqueror on hearing the news, and the offending supernumerary was duly dispatched, so that the army marched into battle in the form of a square though, of course, its company formations had been completely disorganised.

    A million men did Kublis Ghen
    Against Thalbazzar thow;

    says the poet, but that is an exaggeration.

    How many men were in the army that Kublis Ghen threw against Thalbazzar?

    [teaser28]

     
    • Jim Randell's avatar

      Jim Randell 8:58 am on 28 March 2021 Permalink | Reply

      If there are k² men in a company, then in order to form a square when each company is in a line there must be as many companies as there are men in a company. So (k²)² men in total.

      When the army size is doubled, the remaining number is 1 more than a square:

      2(k^4) − 1 = n^2

      We can consider possible values of k until (2(k^4) − 1) exceeds 1 million (at k = 27).

      This Python program runs in 49ms.

      Run: [ @replit ]

      from enigma import (irange, inf, is_square, printf)
      
      # consider k values
      for k in irange(1, inf):
        n = 2 * pow(k, 4) - 1
        if n > 1000000: break
        if is_square(n):
          printf("k={k} -> n={n}")
      

      Solution: There were 57121 men in the army marching into battle.


      Each company consisted of 13² = 169 men, which could be formed into a 13×13 square or a 1×169 line.

      The 169 companies can then be formed into a 169×169 square = 28561 men in total.

      The army size is doubled to 57122 = 239² + 1. And one of these is removed.

      So, the army of 57121 men marched into battle as a 239×239 square.

      Like

    • John Crabtree's avatar

      John Crabtree 4:34 pm on 30 March 2021 Permalink | Reply

      2x^2 = y^2 + 1 where x is square. y/x is an approximation to sqrt(2).
      For 2x^2 = y^2 +/- 1, y/x = 1/1, 3/2, 7/5, 17/12, 41/29, 99/70, 239/169, 577/408, 1393/985 etc
      x(k) = x(k-1) + y(k-1). and y(k) = 2 * x(k-1) + y(k-1).
      169 = 13^2, and so there were 239^2 = 57121 men in the army.
      As a check, 2 * 169^2 = 239^2 + 1

      Like

  • Unknown's avatar

    Jim Randell 5:01 pm on 26 March 2021 Permalink | Reply
    Tags:   

    Teaser 3053: Endless number 

    From The Sunday Times, 28th March 2021 [link] [link]

    George and Martha have a telephone number consisting of nine digits; there is no zero and the others appear once each. The total of the digits is obviously 45, so that the number is divisible by nine. Martha noticed that, if she removed the last (i.e., the least significant) digit, an eight-digit number would remain, divisible by eight. George added that you could continue this process, removing the least significant digit each time to be left with an n-digit number divisible by n right down to the end.

    What is their telephone number?

    [teaser3053]

     
    • Jim Randell's avatar

      Jim Randell 5:04 pm on 26 March 2021 Permalink | Reply

      See also: Enigma 339b, Enigma 1643, Enigma 1667, and the recently posed Puzzle #102.

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

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      --digits="1-9"
      
      "ABCDEFGH % 8 = 0"
      "ABCDEFG % 7 = 0"
      "ABCDEF % 6 = 0"
      "ABCDE % 5 = 0"
      "ABCD % 4 = 0"
      "ABC % 3 = 0"
      "AB % 2 = 0"
      
      --answer="ABCDEFGHI"
      

      Solution: The number is 381654729.

      Like

    • Frits's avatar

      Frits 10:40 pm on 26 March 2021 Permalink | Reply

      Tested in a Python online editor.

      from itertools import permutations
      
      print(*["".join(x) for x in permutations("123456789")
            if all(int("".join(x[:i])) % i == 0 for i in range(2, 9))])
      

      Like

      • Tony Brooke-Taylor's avatar

        Tony Brooke-Taylor 9:02 pm on 28 March 2021 Permalink | Reply

        I used a few shortcuts with the aim of minimising wasted iterations. Running my code on Replit takes about 3.5ms, compared with 1.13s for Frits’ sledgehammer approach. On the other hand, mine takes a few more lines!

        Hope I manage to paste this in an appropriate format:

        
        evens=list(range(2,9,2))
        Xs=list(range(2,9,4))
        odds=list(range(1,10,2))
        odds.remove(5)
        
        #Exploit the fact that the three-digit and six-digit numbers must be divisible by 3, and that the 6-digit number can be expressed as the sum of ABC*1000 and XYZ, so the digits of both ABC and XYZ must sum separately to 3. Let the last three digits be RST. 
        
        #For a four-digit number to be divisible by four, the last two digits must make a number divisible by four. Therefore if the third digit is odd, the fourth must be an odd multiple of 2
        for X in Xs:
          vec=[None for i in range(9)]
          vec[4]=5#no zeros allowed, and the five-digit number divisible by 5
          vec[3]=X
          Z=10-X#consequence of sum(X,Y,Z) divisible by three
          vec[5]=Z
          Bs=[i for i in evens if i not in vec]#remaining evens go to 2nd and 8th
          for B in Bs:
            vec[1]=B
            S=Bs[(Bs.index(B)-1)%2]
            vec[7]=S  
        
        #Try each pair of odd numbers (other than 5) in positions 1 and 3, subject to requiring that ABC divisible by 3 (requiring A+B+C divisible by 3)  
            for A in odds:
              Cs=odds.copy()
              Cs.remove(A)
              for C in Cs:
                if sum([A,B,C])%3==0:
                  vec[0]=A
                  vec[2]=C
        
        #Complementary odds then fall into positions 7 and 9          
                  Rs=Cs.copy()
                  Rs.remove(C)
                  for R in Rs:
                    vec[6]=R
                    if sum([i*10**(6-vec.index(i)) for i in vec[:7]])%7==0 and sum([i*10**(7-vec.index(i)) for i in vec[:8]])%8==0:
                      Ts=Rs.copy()
                      Ts.remove(R)
                      for T in Ts:
                        vec[8]=T
                        print("The telephone number is",sum([i*10**(8-vec.index(i)) for i in vec[:9]]))
        
        
        
        
        

        Like

        • Frits's avatar

          Frits 10:57 am on 29 March 2021 Permalink | Reply

          Hi Tony,

          A couple of recommendations:

          index() is expensive so line 35 may be written to :

           
          if sum([x * 10**(2 - i) for i, x in enumerate(vec[5:8])]) % 8 == 0 and \
             sum([x * 10**(6 - i) for i, x in enumerate(vec[:7])]) % 7 == 0:
          

          You can also use something like “dgts2nbr” as in puzzle [[ https://enigmaticcode.wordpress.com/2017/04/10/enigma-1120-assorted-numbers/ ]]

          – line 16-19 can be rewritten to:

           
          for j, B in enumerate(Bs):
            vec[1] = B
            vec[7] = Bs[1 - j]
          

          or

           
          for j in range(2):
            vec[1] = Bs[j]
            vec[7] = Bs[1 - j]
          

          or

           
          for (B, S) in permutations(Bs):
            vec[1] = B
            vec[7] = S 
          

          Like

          • Tony's avatar

            Tony 6:01 pm on 29 March 2021 Permalink | Reply

            Thanks Frits: now I know about enumerate and reduce. As I’m getting the hang of looping over iterables I had forgotten it is still possible to loop with a counter, so I would probably choose your second approach to lines 16-19. I was trying to avoid using itertools once I realised I only needed to deal with pairs.

            Like

    • Hugh Casement's avatar

      Hugh Casement 12:51 pm on 27 March 2021 Permalink | Reply

      Isn’t this effectively the same as Brainteaser 1040 (4 July 1982)?

      That one at least spared us George and Martha, who get dragged in again and again for no good reason.

      Like

      • Jim Randell's avatar

        Jim Randell 3:36 pm on 27 March 2021 Permalink | Reply

        @Hugh: It does seem to be. I suppose it is hard to come up with over 3000 puzzles without some overlap. Or to check that there is no overlap!

        Like

        • Hugh Casement's avatar

          Hugh Casement 1:14 pm on 30 March 2021 Permalink | Reply

          And the recent Puzzle 102 in New Scientist. I call that plagiarism!

          Like

    • GeoffR's avatar

      GeoffR 1:39 pm on 20 May 2021 Permalink | Reply

      from itertools import permutations
      from enigma import join
      
      # the number 45 is divisible by nine
      i = 9
      
      for p in permutations('123456789', 8):
        a, b, c, d, e, f, g, h = p
      
        # A geometric pattern to this code  
      
        if int(a + b + c + d + e + f + g + h) % 8 == 0:
          if int(a + b + c + d + e + f + g) % 7 == 0:
            if int(a + b + c + d + e + f) % 6 == 0:
              if int(a + b + c + d + e) % 5 == 0:
                if int(a + b + c + d) % 4 == 0:
                  if int(a + b + c) % 3 == 0:
                    if int(a + b) % 2 == 0:
                      tel_num = join((a, b, \
                      c, d, e, f, g, h, i))
                      print('No. is',tel_num)
                      # No. is 381654729
      
      

      Like

    • Dirk's avatar

      Dirk 9:43 am on 9 July 2021 Permalink | Reply

      Teaser 1882 (11/10/1998): Multiple Solutions by Rex Gooch is also linked with brainteaser 1040/3053.

      Like

  • Unknown's avatar

    Jim Randell 8:24 am on 25 March 2021 Permalink | Reply
    Tags:   

    Brain-Teaser 717: Wrong hands 

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

    Waking in the night I glanced at my bed-side clock and thought it indicated just after 2:20. Putting on my spectacles and looking more carefully I saw that it was actually just after 4:10. I had, of course, interchanged the hands at my first glance.

    I began to wonder what time around then that my observation would have occurred, to the exact fraction of a minute, if the hands could have been exactly interchanged.

    What do you think?

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

    [teaser717]

     
    • Jim Randell's avatar

      Jim Randell 8:26 am on 25 March 2021 Permalink | Reply

      I we divide the circle of the clock face into 60 sectors, then the minute has travels at 1 sector/minute, and the hour hand at 1/12 sector/minute.

      If we suppose the difference (in minutes) between the times is d, then the hour hand has moved a distance d / 12 sectors (which is roughly 1/6 of the circle), and the minute hand has moved distance d sectors (and roughly twice – 1/6 times around the circle):

      d = 120 − (d / 12)
      d = 1440 / 13
      d = 110 + 10/13

      So the difference between the times is nearly 111 minutes, about what we would expect.

      If we suppose the actual time is m minutes after 4 o’clock (i.e. 240 + m minutes), then the minute hand will be showing m minutes, which is m sectors around the circle.

      And at the mistaken time = (240 + m − d) the hour hand would be showing the same point:

      (240 + m − d) / 12 = m
      240 − d = 11m
      m = (240 − d) / 11
      m = 1680 / 143
      m = 11 + 107/143

      Solution: The exact time would be 11 + 107/143 minutes after 4 o’clock.

      Like

    • Hugh Casement's avatar

      Hugh Casement 1:45 pm on 25 March 2021 Permalink | Reply

      I make that 11 minutes, 44 and 128/143 seconds,
      mistaken for 20 minutes, 58 and 106/143 seconds (20 and 140/143 minutes) after 2.

      There’s a lot to be said for digital clocks, especially in the middle of the night!

      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