Enigmatic Code

Programming Enigma Puzzles

Enigma 7: Football substitutes

From New Scientist #1149, 5th April 1979 [link]

In the following football league table and addition sum, letters have been substituted for digits (from 0 to 9). The same letter stands for the same digit wherever it appears and different letters stand for different digits. The four teams are eventually going to play each other once. (Two points are given for a win and one point to each side in a drawn match).

Find the scores in the football matches and write out the addition sum with numbers substituted for letters.

[enigma7]

3 responses to “Enigma 7: Football substitutes

  1. Jim Randell 24 November 2012 at 11:29 am

    The following Python program runs in 2.0s.

    from collections import namedtuple
    from itertools import product
    from enigma import irange, printf
    
    # generate all possible scores to a maximum of a-b
    def scores(a, b):
      yield None # if the game isn't played
      for s in product(irange(0, a), irange(0, b)):
        yield s
    
    Table = namedtuple('Table', 'p w l d f a pts')
    
    # compute a row in the table
    def table(games, team):
      (p, w, l, d, f, a, pts) = (0, 0, 0, 0, 0, 0, 0)
      for (g, t) in zip(games, team):
        if g is None: continue
        p += 1
        (gf, ga) = (g[t], g[t ^ 1])
        f += gf
        a += ga
        if gf > ga:
          w += 1
          pts += 2
        elif gf == ga:
          d += 1
          pts += 1
        else:
          l += 1
      return Table(p, w, l, d, f, a, pts)
    
    # possible digits
    ds = set(irange(0, 9))
    
    # consider possible games for B
    # total goals for/against cannot exceed 9
    for AB in scores(9, 9):
      B = table([AB], [1])
      for BC in scores(9 - B.f, 9 - B.a):
        B = table([AB, BC], [1, 0])
        for BD in scores(9 - B.f, 9 - B.a):
          # fill out values from the table
          B = table([AB, BC, BD], [1, 0, 0])
          (N, H, T, X) = (B.p, B.w, B.f, B.pts)
          # and from the sum (there is no carry)
          (G, P) = (X - N, T - H)
          # and deduce the remaining value
          Y = X + B.a + H + N - (2 * P + T)      
          # they must all be different digits
          if len(ds.intersection((N, H, T, X, G, P, Y))) != 7: continue
    
          # now consider the remaining games for A
          A = table([AB], [0])
          for AC in scores(9 - A.f, 9 - A.a):
            A = table([AB, AC], [0, 0])
            for AD in scores(9 - A.f, 9 - A.a):
              # check the figures in the table
              A = table([AB, AC, AD], [0, 0, 0])
              if not(A.d == Y and A.f == P and A.a == X and A.pts == H): continue
    
              # and the last game is CvD
              C = table([AC, BC], [1, 1])
              D = table([AD, BD], [1, 1])
              for CD in scores(9 - max(C.f, D.f), 9 - max(C.a, D.a)):
                # check the figures in the table
                C = table([AC, BC, CD], [1, 1, 0])
                if not(C.p == G and C.f == P and C.a == H): continue
                D = table([AD, BD, CD], [1, 1, 1])
                if not(D.f == Y and D.a == N): continue
    
                # output the results
                printf("games: AB={AB} AC={AC} AD={AD} BC={BC} BD={BD} CD={CD}")
                printf("sum: {P}{N} + {H}{G} = {T}{X} [Y={Y}]")
                print("   p w l d f a pts")
                for n, t in zip('ABCD', (A, B, C, D)):
                  printf("{n}: {t.p} {t.w} {t.l} {t.d} {t.f} {t.a} {t.pts}")
                print('')
    

    Solution: The scores in the played matches are: A v B: 3-4; A v D: 2-0; B v C: 2-5; B v D: 1-0; and the addition sum is: 53 + 21 = 74.

  2. Jim Randell 17 October 2013 at 4:04 pm

    I’ve collected some routines that are useful for many of these puzzles involving football league tables into a [[ Football() ]] class, which I shall add to the enigma.py library for future use. Using them, this Python 3 program runs in 38ms.

    # there are 6 matches overall: AvB, AvC, AvD, BvC, BvD, CvD.
    
    # each team plays three matches, so "Played", "Won", "Lost", "Drawn"
    # cannot exceed 3. So: g, h, n, y are 0, 1, 2, 3 in some order. h < n.
    # and p, t, x are three digits from 4, 5, 6, 7, 8, 9.
    
    # So n + g cannot exceed 5, so there is no carry in the sum:
    # n + g = x, so x is 4 or 5.
    # p + h = t
    # and none of n, h, g can be 0, hence y = 0
    # and (h, n, g) are (1, 2, 3) or (1, 3, 2) or (2, 3, 1)
    
    from collections import namedtuple
    from itertools import product
    from enigma import irange, diff, printf
    
    class Football(object):
    
      # initialise the game rules
      def __init__(self, games=None, points=None, swap=None):
        # set the defaults
        if games is None:
          games = tuple('wdlx')
        if points is None:
          points = { 'w': 2, 'd': 1 }
        if swap is None:
          swap = { 'w': 'l', 'l': 'w' }
    
        self._games = games
        self._points = points
        self._swap = swap
        self._table = namedtuple('Table', ('played',) + games + ('points',))
    
      # generate games
      def games(self, *gs, **kw):
        if not gs: gs = [self._games]
        if 'repeat' in kw: gs = gs * kw['repeat']
        if len(gs) == 1:
          yield from gs[0]
        else:
          yield from product(*gs)
    
      # compute the table
      def table(self, gs, ts):
        r = dict((x, 0) for x in self._games)
        played = points = 0
        for (g, t) in zip(gs, ts):
          if t: g = self._swap.get(g, g)
          r[g] += 1
          if g != 'x':
            played += 1
            points += self._points.get(g, 0)
        return self._table(*((played,) + tuple(r[x] for x in self._games) + (points,)))
    
      # generate possible score lines
      def scores(self, gs, ts, f, a, s=[]):
        # are we done?
        if not gs:
          if f == a == 0:
            yield s
        else:
          # check the first game
          (g, *gs) = gs
          (t, *ts) = ts
          if t: g = self._swap.get(g, g)
          # is it unplayed?
          if g == 'x':
            yield from self.scores(gs, ts, f, a, s + [None])
          # is it a draw?
          elif g == 'd':
            for i in irange(0, min(f, a)):
              yield from self.scores(gs, ts, f - i, a - i, s + [(i, i)])
          # is it a win?
          elif g == 'w':
            for j in irange(0, a):
              for i in irange(j + 1, f):
                s0 = ((j, i) if t else (i, j))
                yield from self.scores(gs, ts, f - i, a - j, s + [s0])
          # is it a loss?
          elif g == 'l':
            for i in irange(0, f):
              for j in irange(i + 1, a):
                s0 = ((j, i) if t else (i, j))
                yield from self.scores(gs, ts, f - i, a - j, s + [s0])
    
      # compute goals for, against
      def goals(self, ss, ts):
        (f, a) = (0, 0)
        for (s, t) in zip(ss, ts):
          if s is None: continue
          f += s[t]
          a += s[t ^ 1]
        return (f, a)
    
    # initialise scoring rules
    football = Football(points={ 'w': 2, 'd': 1 })
    
    d03 = (0, 1, 2, 3)
    d49 = (4, 5, 6, 7, 8, 9)
    
    # consider possible games for B
    for (ab, bc, bd) in football.games(repeat=3):
      # check B's table
      B = football.table([ab, bc, bd], [1, 0, 0])
      (n, h, x) = (B.played, B.w, B.points)
      if x not in (4, 5): continue
      g = x - n
      y = diff(d03, (g, h, n))
      if len(y) > 1: continue
      y = y[0]
    
      # consider remaining games for A
      for (ac, ad) in football.games(repeat=2):
        # check A's table
        A = football.table([ab, ac, ad], [0, 0, 0])
        if not(A.d == y and A.points == h): continue
    
        # and the remaining game
        for cd in football.games():
          # check C's table
          C = football.table([ac, bc, cd], [1, 1, 0])
          if C.played != g: continue
          D = football.table([ad, bd, cd], [1, 1, 1])
    
          # generate possible scorelines for D's matches
          for (AD, BD, CD) in football.scores([ad, bd, cd], [1, 1, 1], y, n):
    
            # choose a value for p
            for p in d49:
              t = p + h
              if len(diff(d49, [p, t, x])) != 3: continue
    
              # generate scorelines for C's matches
              (fC, aC) = football.goals([CD], [0])
              for (AC, BC) in football.scores([ac, bc], [1, 1], p - fC, h - aC):
    
                # generate scorelines for A's matches
                (fA, aA) = football.goals([AC, AD], [0, 0])
                for (AB,) in football.scores([ab], [0], p - fA, x - aA):
    
                  printf("AB={ab}:{AB} AC={ac}:{AC} AD={ad}:{AD} BC={bc}:{BC} BD={bd}:{BD} CD={cd}:{CD}\nA={A}\nB={B}\nC={C}\nD={D}\nn={n} h={h} x={x} g={g} y={y} p={p} t={t}\n")
    
  3. Jim Randell 22 April 2016 at 1:37 pm

    Here’s a solution that combines the (newly added) [[ Football.substituted_table*() ]] solvers and the [[ SubstitutedSum() ]] solver from the enigma.py library.

    It doesn’t require any additional analysis, and runs in 80ms.

    Run: [ @replit ]

    from enigma import (Football, SubstitutedSum)
    
    # scoring system
    football = Football(points=dict(w=2, d=1))
    
    # labels for the teams
    (A, B, C, D) = (0, 1, 2, 3)
    
    # solve the table
    (table, gf, ga) = (dict(played='?ng?', w='?h??', d='y???', points='hx??'), 'ptpy', 'x?hn')
    for (matches, d) in football.substituted_table(table):
    
      # now solve the sum to get p, g, t
      for s in SubstitutedSum(['pn', 'hg'], 'tx', l2d=d).solve():
    
        # and determine the scores in the matches
        for scores in football.substituted_table_goals(gf, ga, matches, d=s, teams=[A, C, D]):
    
          # output the matches
          football.output_matches(matches, scores, teams='ABCD', d=s)
    

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.