Enigmatic Code

Programming Enigma Puzzles

Enigma 665: Occupational hazard

From New Scientist #1820, 9th May 1992 [link]

Alan, Brian and Charles have surnames Adams, Brown and Collins (not necessarily respectively) and occupations of architect, builder and carpenter (again not necessarily respectively). Each of them is either thoroughly honest or thoroughly dishonest. Below are some statements (not necessarily by more than one person) which involve these three people’s names and jobs: (each blank space originally contained one of the surnames — Mr Adams deleted the names after seeing the first few statements):

Alan says:

(i) I am not the architect.
(ii) Brian is a carpenter.
(iii) Charles’s surname is [……….].

Mr Adams says:

(i) The architect’s surname is not Brown.
(ii) The builder’s surname is [………].
(iii) The two spaces contain the same surname.

The architect says:

(i) The builder isn’t called Charles.
(ii) It’s now possible to work out all our names and jobs.

Please state (in order) the [deleted] surnames.

If I’ve counted correctly there are now “only” 200 Enigma puzzles remaining to post. (Actually I think there are 196).

[enigma665]

3 responses to “Enigma 665: Occupational hazard

  1. Jim Randell 27 June 2022 at 10:26 am

    This Python program runs in 65ms. (Internal run time is 219µs).

    Run: [ @replit ]

    from enigma import (subsets, group, unpack, printf)
    
    # always tells the truth
    def T(s): return bool(s)
    
    # never tells the truth
    def F(s): return not(s)
    
    # record candidate solutions
    rs = list()
    
    # assign behaviours for Alan, Brian, Charles
    for fs in subsets((T, F), size=3, select="M"):
      (fA, fB, fC) = fs
    
      # assign jobs
      for js in subsets("ABC", size=3, select="P"):
        (jA, jB, jC) = js
        jAi = js.index('A')
        jBi = js.index('B')
        arch = fs[jAi]
    
        # Alan: "Alan is not the architect"
        if not fA(jA != 'A'): continue
    
        # Alan: "Brian is the carpenter"
        if not fA(jB == 'C'): continue
    
        # architect: "The builder isn't called Charles"
        if not arch(jC != 'B'): continue
    
        # assign surnames
        for ss in subsets("ABC", size=3, select="P"):
          (sA, sB, sC) = ss
          sAi = ss.index('A')
          sBi = ss.index('B')
          adams = fs[sAi]
    
          # Mr Adams: "The architect's surname is not Brown"
          if not adams(ss[jAi] != 'B'): continue
    
          # choose two surnames for the blanks
          for (b1, b2) in subsets("ABC", size=2, select="M"):
    
            # Alan: "Charles' surname is {b1}"
            if not fA(sC == b1): continue
    
            # Mr Adams: "The builder's surname is {b2}"
            if not adams(ss[jBi] == b2): continue
    
            # Mr Adams: "The two blanks contain the same surname"
            if not adams(b1 == b2): continue
    
            # record the results
            rs.append((fs, js, ss, b1, b2, arch))
    
    # group solutions by jobs and surname
    d = group(rs, by=unpack(lambda fs, js, ss, b1, b2, arch: (js, ss)))
    
    # consider the solutions
    for vs in d.values():
      for (fs, js, ss, b1, b2, arch) in vs:
        # architect says there is a single solution
        if not arch(len(d.keys()) == 1): continue
    
        # output solution
        ((fA, fB, fC), (jA, jB, jC), (sA, sB, sC)) = (fs, js, ss)
        f = lambda x: x.__name__
        printf("blanks = {b1}, {b2} [A={A} {sA} {jA}; B={B} {sB} {jB}; C={C} {sC} {jC}]", A=f(fA), B=f(fB), C=f(fC))
    

    Solution: The deleted names are: “Brown” and “Adams”.

    Alan Collins is the builder, he always tells the truth.

    Brian Adams is the carpenter, he never tells the truth.

    Charles Brown is the architect, he always tells the truth.

    So:

    Alan (Collins): “I am not the architect” (true); “Brian is the carpenter” (true); “Charles’ surname is Brown” (true)
    (Brian) Adams: “The architect’s surname is not Brown” (false); “The builder’s surname is Adams” (false); “The two blanks contain the same surname” (false)
    (Charles Brown) architect: “The builder isn’t called Charles” (true); “You can work out our names/jobs” (true)

  2. Frits 30 June 2022 at 10:57 am

    It is probably not possible to directly code the architect’s (ii) statement unless [SubstitutedExpression] supports a 2-pass mode.

       
    from enigma import SubstitutedExpression
    
    name     = {0: "Adams",     1: "Brown",    2: "Collins"}
    job      = {0: "architect", 1: "builder",  2: "carpenter"}
    speakers = {0: "Alan",      1: "Mr Adams", 2: "Architect"}
    
    #            name  job  
    # Alan   :    D     G      A = Alan says:           X = first space
    # Brian  :    E     H      B = Mr Adams says:       Y = second space
    # Charles:    F     I      C = The architect says:
    
    # the alphametic puzzle
    p = SubstitutedExpression(
      [
        # ------------ Alan says:
        
        # (i) I am not the architect.
        # (ii) Brian is a carpenter.
        # (iii) Charles's surname is [……….].
        "(G != 0) = A",
        "A == (H == 2) == (F == X)",
         
        # ------------ Mr Adams says:
        
        # (i) The architect's surname is not Brown.
        # (ii) The builder's surname is [………].
        # (iii) The two spaces contain the same surname.
        "all(x != 1 for x, y in zip([D, E, F], [G, H, I]) if y == 0) = B",
        "B == ([x for x, y in zip([D, E, F], [G, H, I]) if y == 1][0] == Y) \
         == (X == Y)",
        
        # ------------ The architect says:
        
        # (i) The builder isn't called Charles.
        # (ii) It's now possible to work out all our names and jobs.
        "(I != 1) = C",         # we can't code (ii) yet
       
    
        # ------------ cross checks
        
        # if Alan is the architect then their statements must be coherent
        "G != 0 or A == C",
        
        # if Alan is Mr Adams then their statements must be coherent
        "D != 0 or A == B",
        
        # if Mr Adams is the architect then their statements must be coherent
        "(0, 0) not in {(D, G), (E, H), (F, I)} or B == C",
      ],
      answer="(D, G), (E, H), (F, I), (X, Y), (A, B, C)",
      base=3,
      distinct=("DEF","GHI"),
      verbose=0,    # use 256 to see the generated code
    )
    
    d = dict()
    
    # collect solutions
    sols = [y for _, y in p.solve()]
    
    # group solutions by jobs and surname
    for s in sols:
      d[s[0] + s[1] + s[2]] = s[3] + s[4]
    
    # is it possible to work out all names and jobs 
    oneSolution = True if (len(d) == 1) else False
    
    # print answers
    for s in sols:
      # architect says there is a single solution
      if oneSolution and s[4][2] == 0: continue
      if not oneSolution and s[4][2] == 1: continue
      
      print(f"Alan {name[s[0][0]]}, {job[s[0][1]]}")
      print(f"Brian {name[s[1][0]]}, {job[s[1][1]]}")
      print(f"Charles {name[s[2][0]]}, {job[s[2][1]]}")
      print(", ".join(speakers[i] + ' always ' + 
           ('speaks the truth' if x else 'lies') for i, x in enumerate(s[4])))
      print()
      print(f"the [deleted] surnames: {' and '.join(name[n] for n in set(s[3]))}")
    
    • Jim Randell 2 July 2022 at 1:03 pm

      Note that the use of set() means this program won’t necessarily print the correct answer on a Python implementation that does not preserve insertion order in sets. Which includes the standard CPython implementation. (Although I believe PyPy does preserve set insertion order).

Leave a Comment

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

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

%d bloggers like this: