Updates from Jim Randell Toggle Comment Threads | Keyboard Shortcuts

  • 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

  • Unknown's avatar

    Jim Randell 9:07 am on 23 March 2021 Permalink | Reply
    Tags:   

    Teaser 2781: Number time 

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

    I have written down some numbers and then consistently replaced digits by capital letters, with different letters used for different digits. In this way my numbers have become:

    TRIPLE (which is a multiple of three)
    EIGHT (which is a cube)
    NINE (which is divisible by nine)
    PRIME (which is a prime)

    What is the number TIME?

    [teaser2781]

     
    • Jim Randell's avatar

      Jim Randell 9:08 am on 23 March 2021 Permalink | Reply

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

      The following run file executes in 122ms.

      Run: [ @replit ]

      #! python3 -m enigma -rr
      
      SubstitutedExpression
      
      "TRIPLE % 3 = 0"
      "is_cube(EIGHT)"
      "NINE % 9 = 0"
      "is_prime(PRIME)"
      
      --answer="TIME"
      

      Solution: TIME = 3901.

      There are 4 solutions to the alphametic expressions, but the value for TIME is the same in all of them.

      Like

    • Frits's avatar

      Frits 12:39 pm on 23 March 2021 Permalink | Reply

      With a complicated formula for N.

      from enigma import is_prime
      
      alldiff = lambda seq: len(seq) == len(set(seq))
      
      # EIGHT is a cube
      for i in range(22, 47):
        s3 = str(i ** 3)
        E = int(s3[0])
        I = int(s3[1])
        T = int(s3[-1])
        if T == 0: continue             
        if E % 2 == 0: continue  # E may not be even (due to PRIME)
        if not alldiff(s3): continue
        
        # NINE is a multiple of 9
        # sumIE plus 2*N must be equal to k*9   (k is 1,2 or 3)
        sumIE = I + E
        N = (9 + 18 * (sumIE > 9) - sumIE) // 2 if sumIE % 2 else (18 - sumIE) // 2  
        if N == 0 or not alldiff(s3 + str(N)): continue   
        
        # TRIPLE is a multiple of 3
        LMPR = [x for x in range(10) if str(x) not in s3 + str(N)]
        candsM = [x for x in LMPR if (sumIE + T + sum(LMPR) - x) % 3 == 0]
         
        for M in candsM:
          LPR = [x for x in LMPR if x != M]
          for L in LPR:
            # PRIME may not be a multiple of 3
            if (sumIE + M + sum(LPR) - L) % 3 == 0: continue
            PR = [x for x in LPR if x != L]
            # consider permutations of PR
            for (P, R) in zip(PR, PR[::-1]):
              if P == 0: continue
              PRIME = 10000*P + 1000*R + 100*I + 10*M + E
              if not is_prime(PRIME): continue
              print(f"TIME={T}{I}{M}{E}")
      

      Like

  • Unknown's avatar

    Jim Randell 10:41 pm on 21 March 2021 Permalink | Reply
    Tags: by: Mrs. K. Y. Gunson   

    Brain-Teaser 27: Foggy birthday 

    From The Sunday Times, 24th September 1961 [link]

    Mr and Mrs Herbert Fogg, in celebration of Herbert’s forty-third birthday, invited three friends, Ann, Bill, and Cuthbert, to dinner.

    “Of course, Herbert is quite a bit older than any one of you”, Mrs Fogg declared to the three guests. Herbert frowned.

    “It’s odd”, Ann quickly interposed, “if you multiply Bill’s and Cuthbert’s ages together, then divide by Herbert’s age the remainder is just my age”.

    “Whereas if you multiply together Ann’s and Cuthbert’s ages, again divide by Herbert’s age, then the remainder is just how old I was a year ago”, added Bill.

    “Not so simple for me”, said Cuthbert, the youngest present. “If you multiply together Ann’s and Bill’s ages, divide by Herbert’s, then the remainder is just one and a half times my age”.

    “Very interesting”, Herbert reflected. “In fact, from what you have told me it may be possible to find all three of your ages, if I knew how to start!”

    Show that Herbert was almost right and find the exact ages of Ann, Bill, and Cuthbert.

    [teaser27]

     
    • Jim Randell's avatar

      Jim Randell 10:42 pm on 21 March 2021 Permalink | Reply

      This Python program runs in 49ms.

      from enigma import (irange, printf)
      
      # H's age
      H = 43
      
      # choose C's age (the youngest)
      for C in irange(10, H - 3, step=2):
        # choose B's age
        for B in irange(C + 1, H - 1):
          # calculate A's age (using A's statement)
          A = (B * C) % H
          if not (A > C and A != B): continue
          # check B's and C's statements
          if not (B - 1 == (A * C) % H): continue
          if not (3 * C == 2 * ((A * B) % H)): continue
      
          # output solution
          printf("A={A} B={B} C={C}")
      

      Solution: Ann is 27. Bill is 25. Cuthbert is 20.

      Like

    • Frits's avatar

      Frits 10:02 am on 22 March 2021 Permalink | Reply

      % A Solution in MiniZinc 
       
      % ages
      var 11..42:A; var 11..42:B; var 10..38:C; var 43..43:H;
       
      constraint C < A /\ C < B /\ A + 2 < H /\ B + 2 < H;
       
      constraint (B * C) mod H = A;
      constraint (A * C) mod H + 1 = B;
      constraint 2 * ((A * B) mod H) = 3 * C;
       
      solve satisfy;
       
      output [ "Ann is " ++ show(A)]
      ++ [ "\nBill is " ++ show(B)]
      ++ [ "\nCuthbert is " ++ show(C)];
      

      Like

    • John Crabtree's avatar

      John Crabtree 4:16 pm on 23 March 2021 Permalink | Reply

      1. BC = A mod 43
      2. AC = B – 1 mod 43
      3. AB = 3C / 2 mod 43 with C even and C < 30
      Combining Equations 1 and 3 leads to B^2 = 23 mod 43
      Equations 1, 2 and 3 give ABC = A^2 = B^2 – B = 23 – B = 3C^2 / 2
      One can also show that (A ^2 – 23)^2 = 23 mod 43 and (C^2 – 1)^2 = 15 mod 43

      The easiest way forward is to start with (C^2 – 1)^2 = 15 mod 43
      (C^2 – 1) = 12 mod 43 (C = 20 or 23), or (C^2 – 1) = 31 mod 43 with no value of C
      And so C = 20
      B = 23 – 3C^2 / 2 mod 43 = 25
      A = BC mod 43 = 27
      This would make for a simple and fast program. It can also be done by hand.

      Starting with B^2 = 23 mod 43 gives:
      B = 18, A^2 = 5 mod 43, with no possible values of A < 22
      Also B = 43 – 18 = 25, when A^2 = -2 = 41 mod 43
      ..A = 16 gives 3C / 2 = 13 which is invalid (and C = 23 mod 43)
      ..A = 43 – 16 = 27 gives 3C / 2 = 30, ie C = 20.

      Ann is 27, Bill is 25 and Cuthbert is 20.

      The information that C is the youngest is redundant, although it does reduce the solution space for a brute force search.
      This is an interesting teaser.

      Like

  • Unknown's avatar

    Jim Randell 4:40 pm on 19 March 2021 Permalink | Reply
    Tags:   

    Teaser 3052: Porcus facit griphus 

    From The Sunday Times, 21st March 2021 [link] [link]

    “Argent bend sinister abased sable in dexter chief a hog enraged proper” blazons our shield (shaped as a square atop a semi-circle, with a 45° diagonal black band meeting the top corner). We’ve three shields. For the first, in centimetres, the top width (L) is an odd perfect cube and the vertical edge height of the band (V) is an odd two-figure product of two different primes. The others have, in inches, whole-number L (under two feet) and V values (all different). For each shield, the two white zones have almost identical areas. All three V/L values, in percent, round to the same prime number.

    Give the shortest top width, in inches.

    [teaser3052]

     
    • Jim Randell's avatar

      Jim Randell 6:00 pm on 19 March 2021 Permalink | Reply

      The puzzle is set by Stephen Hogg.

      This Python routine calculates the V/L ratio where the areas are equal numerically, and then looks for permissible L values for the shield measured in centimetres.

      We then look for integer V values within 10% of the “ideal” value, that have 2 different prime factors, and give a rounded percentage that is a prime.

      We then look for multiple shields that have an L value less than 24 inches, and a corresponding V value that give the same rounded percentage.

      This Python program runs in 50ms.

      Run: [ @replit ]

      from math import (pi, sqrt, atan2)
      from enigma import (find_value, intf, intc, Accumulator, irange, divf, divc, fdiv, group, item_from, is_prime, factor, iroot, powers, printf)
      
      # calculate the difference between the upper and lower areas
      def areas(V, L):
        # lower area: triangular bit
        t = L - V
        T = 0.5 * t * t
        # lower area: semi-circular bit
        a = 0.5 * L
        b = a - V
        a2 = a * a
        b2 = b * b
        # solve: x^2 + y^2 = a^2, y = x - b for x, y
        r = sqrt(2 * a2 - b2)
        x = 0.5 * (r + b)
        y = 0.5 * (r - b)
        theta = pi - atan2(y, x)
        # calculate the areas
        upper = a * L
        lower = T + 0.5 * (a2 * theta + b * y)
        # return the difference in areas
        return fdiv(2 * abs(upper - lower), upper + lower)
      
      # find the V/L ratio when the white areas are equal
      r0 = find_value((lambda v: areas(v, 1)), 0, 0.25, 0.50).v
      printf("[r0 = {r0}]")
      
      # generate (V, L, rounded V/L percentage) values
      def generate(Ls):
        for L in Ls:
          # calculate exact V
          v = r0 * L
          # allow a tolerance of 10%
          for V in irange(intc(0.9 * v), intf(1.1 * v)):
            # calculate the rounded V/L percentage
            p = divf(200 * V + L, 2 * L)
            if not is_prime(p): continue
            # return values
            yield (V, L, p)
      
      # group (V, L, p) values by the percentage, for the shields measured in inches
      d = group(generate(irange(2, 23)), by=item_from("p", "V, L, p"))
      
      # find the shield measured in centimetres
      M = iroot(divc(100, 0.9 * r0), 3)
      for (V, L, p) in generate(powers(3, M, 3, step=2)):
        # V is an odd, 2-digit number
        if V % 2 == 0 or V < 10 or V > 99: continue
        # V should have 2 prime factors
        fs = factor(V)
        if not (len(fs) == 2 and len(set(fs)) == 2): continue
      
        # look for shields measured in inches
        vs = d.get(p)
        if (not vs) or len(vs) < 2: continue
      
        # output solution
        r = Accumulator(fn=min)
        inch = lambda x: fdiv(x, 2.54)
        printf("V/L ~ {p}%")
        printf("-> 1: L = {L}cm ({Li:.2f}in), V = {V}cm ({Vi:.2f}in) [error = {r:.6f}]", Li=inch(L), Vi=inch(V), r=areas(V, L))
        r.accumulate(inch(L))
        for (Vi, Li, _) in vs:
          printf("-> 2: L = {Li}in, V = {Vi}in [error = {r:.6f}]", r=areas(Vi, Li))
          r.accumulate(Li)
        printf("=> min L = {r:.2g}in", r=r.value)
        printf()
      

      Solution: The shortest L value is: 17 inches.

      The measurements for each of the three shields are:

      L = 125cm (49.2in), V = 51cm (20.1in)
      L = 17in, V = 7in
      L = 22in, V = 9in
      V/L ≈ 41%

      The program calculates a more precise ratio for equal areas: V/L = 0.408083900721218.


      In order to calculate the area of the lower white area on the shield, I started with a polynomial approximation based on the three values at V=0, V=L/2, V=L.

      This is sufficient to find the answer (as, I suspect, are many other rough estimates).

      But for the program above I came up with a a way of calculating the area exactly, based on the following diagram:

      The shield is turned upside down and we place x,y axes through the centre of the semicircle.

      Then using: a = L/2, b = L/2 − V

      The semicircle is part of the circle: x² + y² = a²

      The (now) upper line of the stripe is the line: y = x − b

      And these meet at: (x, y) = ((r + b)/2, (r − b)/2), where: r = √(2a² − b²)

      So we can calculate the angle θ = arctan(y, x)

      And the area of the yellow triangle = (1/2)ab sin(θ) = by/2

      The orange region is then the area of the sector of the circle that subtends the angle θ, less the area of the yellow triangle.

      And once we know the area of the orange region the area of the (formerly) lower region of the shield can be easily determined.

      Like

      • Tony Brooke-Taylor's avatar

        Tony Brooke-Taylor 10:13 am on 22 March 2021 Permalink | Reply

        I found you can get away with approximating away the arctan term and I eliminated the need to set a tolerance by finding the valid V/L ratios closest to the target prime. However, my code takes 40 times longer to run than yours Jim so I have some other work to do!

        Like

        • Tony Brooke-Taylor's avatar

          Tony Brooke-Taylor 10:56 am on 22 March 2021 Permalink | Reply

          … so it takes 1/4s to import numpy and 5ms for the rest of my program to run. Now I know why people bother with the math library.

          Like

    • Robert Brown's avatar

      Robert Brown 8:25 pm on 20 March 2021 Permalink | Reply

      The information about equal white zones is redundant. There is only one solution to the metric shield, which defines the prime. Then there are only two possible imperial solutions where v/a rounds to give the same prime. That solves the teaser. It’s good of him to tell us that all 3 solutions have white zones with ‘almost’ identical areas, but it adds nothing to the teaser solution.

      Like

      • Jim Randell's avatar

        Jim Randell 9:04 pm on 20 March 2021 Permalink | Reply

        @Robert: If we didn’t have the “equal area” constraint what would stop a solution like this:

        V/L ≈ 31%
        L = 125cm, V = 39cm
        L = 13in, V = 4in
        L = 16in, V = 5in

        I think we need the additional constraint to eliminate such solutions (in this one the areas differ by about 16%).

        But you don’t need to be completely accurate. A rough estimate will get you to the answer.

        Like

    • Hugh Casement's avatar

      Hugh Casement 7:04 am on 21 March 2021 Permalink | Reply

      I thought I knew a bit about heraldry, but had never come across the term ‘abased’. However, it’s clear that the bend has slipped down so that its upper edge, rather than its centre line, meets the corner of the shield. ‘Enraged’ is surely an invention, n’est-ce pas?

      The area of white space must mean before the piglet has flown in. Then it’s a quick back-of-the-envelope calculation — no computer needed. Teasers are seldom so easy.

      Like

    • Hugh Casement's avatar

      Hugh Casement 11:17 am on 21 March 2021 Permalink | Reply

      Whatever S. Hogg’s ability to blazon may be, his Latin is shocking!
      As every schoolboy knows, a direct object goes in the accusative: porcus imitat gryphem (for example).
      Better might be porci volent, though I’m not sure how far we can trust the googly translator.

      Like

  • Unknown's avatar

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

    Brain-Teaser 702: That’s torn it … again 

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

    Last time it was vertical. But no one could accuse Uncle Bungle of being consistent and this time it was horizontal. The way, I mean, in which he tore the piece of paper on which were written the details of the matches between 4 local football teams, ABC, and D, who are to play each other once.

    All that was left was:

    It is not known whether all the matches have been played. And not more than 7 goals were scored in any game.

    With the information that it is possible to discover the score in each match, you should be able to discover them.

    What was the score in each match?

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

    [teaser702]

     
    • Jim Randell's avatar

      Jim Randell 8:29 am on 18 March 2021 Permalink | Reply

      I wasn’t entirely happy about this puzzle, but after some thought I was able to convince myself that there is a way to arrive at a single solution.

      We are told that there is enough information to determine the scores in each match (even though on the face of it it seems that we have not), so that fact it is possible to determine the scores means that there must be some ambiguous situations that cannot arise.

      We have no information at all about C and D, so any solution that we find will be equally applicable if C and D’s labels are swapped over. But as we are told we can determine the scores in the matches, this must mean swapping the labels of C and D would have no effect on the scores, so C and D must have performed identically.

      From this we can see that the C vs D match cannot be won by either of the teams, so it must be a draw, or not yet played. If it were a draw, then it could be 0-0, 1-1, 2-2, 3-3 and we don’t know which. So the C vs D match must be not yet played.

      We know A and B have both played each of the other teams (as they have played 3 matches each), so all 6 matches involving A and B must have been played. And so the A vs B and A vs C matches must have identical scores, as must the B vs C and B vs D matches.

      So we only need to calculate three scorelines: A vs B (win), A vs (C and D) (draw), B vs (C and D) (win).

      This Python program runs in 48ms.

      Run: [ @replit ]

      from itertools import product
      from enigma import Football, printf
      
      # possible scores (no more than 7 goals scored per match)
      win = [
        (1, 0), (2, 0), (3, 0), (4, 0), (5, 0), (6, 0), (7, 0),
        (2, 1), (3, 1), (4, 1), (5, 1), (6, 1),
        (3, 2), (4, 2), (5, 2),
        (4, 3),
      ]
      draw = [(0, 0), (1, 1), (2, 2), (3, 3)]
      
      # for computing goals for/against
      football = Football()
      
      # CD is not yet played
      CD = None
      
      # A has 2 draws (which must be AC = AD) and a win (which must be AB)
      # B has 2 wins (which must be BC = BD)
      for (AB, AX, BX) in product(win, draw, win):
        # check goals for/against
        if not(football.goals([AB, AX, AX], [0, 0, 0]) == (7, 5)): continue
        if not(football.goals([AB, BX, BX], [1, 0, 0]) == (5, 5)): continue
        # output solution
        printf("AB={AB} AC={AX} AD={AX} BC={BX} BD={BX} CD={CD}")
      

      Solution: A vs B = 3-1; A vs C = 2-2; A vs D = 2-2; B vs C = 2-1; B vs D = 2-1; C vs D = not yet played.

      Like

    • John Crabtree's avatar

      John Crabtree 7:33 pm on 18 March 2021 Permalink | Reply

      At the point where you can say “So we only need to calculate three scorelines”, you are almost home.
      A must beat B by 2 goals, and score an odd number of goals. That fixes the score in A vs B, and the remaining scores follow.

      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