Teaser 3346: Step-free axes
From The Sunday Times, 8th November 2026 [link] [link]
Changing underground lines, we used a step-free access route: three lifts joining ends of pairs from four horizontal L-shaped corridors at different levels; the corridor segments ran on E-W or N-S axes, with a right-hand turn between the first and second.
We started on the green corridor, walking 20 yards, turning right, and going 40 yards. This was at a lower level than the yellow one (30 yards, turning right then 50 yards); for blue, the values were 60 then 10; for red, 70 then 20. I’ve forgotten the direction the corridors started in, and the order of the last three. The route ended exactly above where we started.
The arrangement had me in a knot — literally, because the route we followed actually formed a knot.
What was the order of the corridors (e.g., B, G, R, Y), and the order of their levels (bottom to top)?
[teaser3346]





Jim Randell 8:18 am on 8 November 2026 Permalink |
There are multiple variables:
This Python program tries all possible arrangements, and for each constructs the corresponding path (as a collection of (x, y, z) points in 3-space). We reject any path where the endpoint is not directly above the start, or if the path is self intersecting. We then construct a knot diagram, which determines the “crossings” in the path, and see if (when the beginning and end of the path are joined together) the resulting structure can be untangled to form a simple loop, otherwise it forms a knot. (This is an unkotting problem [@wikipedia], which are generally quite hard, but for this puzzle we only need to deal with simple diagrams that have fewer than 4 crossings, so there is essentially only one possible non-trivial knot that can be constructed).
It runs in 112ms. (Internal runtime is 24ms).
from enigma import ( Rational, irange, subsets, cproduct, tuples, delete, line_intersect, catch, compare, printf ) Q = Rational() # the segments of the corridors segs = { 'G': (20, 40), 'Y': (30, 50), 'B': (60, 10), 'R': (70, 20), } # labels for the corridors ks = sorted(segs.keys()) # choose an order for the segments ('G' is first) orders = list(ss for ss in subsets(ks, size=len, select='P') if ss[0] == 'G') # assign levels to the segments ('G' is lower than 'Y') levels = list() for ss in subsets([0, 20, 40, 60], size=len, select='P'): d = dict(zip(ks, ss)) if d['G'] < d['Y']: levels.append(d) # choose a start direction (0 = E, 1 = N, 2 = W, 3 = S) for each leg turns = list((0,) + ss for ss in subsets([0, 1, 2, 3], size=3, select='M')) # check if line segments (p0, p1) and (q0, q1) intersect # segments must be parallel to one of the x, y, z axes def seg_intersect(p0, p1, q0, q1): for (a, b, c, d) in zip(p0, p1, q0, q1): if max(min(a, b), min(c, d)) > min(max(a, b), max(c, d)): return False return True # is path self intersecting? def self_intersecting(path): for (i, j) in subsets(irange(len(path) - 1), size=2): if j - i == 1: continue if seg_intersect(path[i], path[i + 1], path[j], path[j + 1]): return True return False # now some knot theory (for reduced diagrams with < 4 crossings) # collapse the path along axis <z>, and determine crossings def crossings(path, z=2): segs = list((p, q) for (p, q) in tuples(path, 2, circular=1) if p[z] == q[z]) collapse = lambda p: delete(p, [z]) # determine the crossings cs = list() for (i, j) in subsets(irange(len(segs)), size=2): (p1, p2) = (collapse(p) for p in segs[i]) (p3, p4) = (collapse(p) for p in segs[j]) r = catch(line_intersect, p1, p2, p3, p4, internal=1+2, div=Q) if r is None or (r.q1 in {0, 1} and r.q2 in {0, 1}): continue # level is -1 for under, +1 for over c = compare(segs[i][0][z], segs[j][0][z]) # each crossing is encountered twice cs.extend([((i, r.q1), (i, j), c), ((j, r.q2), (i, j), -c)]) # return the list of (<crossing id>, <level>) in order encountered cs.sort() return tuple(delete(t, [0]) for t in cs) # perform reductions on a crossing sequence using Reidemeister moves # type I (kink): both visits to a crossing are adjacent (cyclically) def type1(seq): n = len(seq) for i in irange(n): j = (i + 1) % n if seq[i][0] == seq[j][0]: return {i, j} # type II (parallel bigon): crossings c and d are met in a row with # the same strand on top (or underneath) at both, and are met again in # the same order with the other strand on top (or underneath) def type2(seq): n = len(seq) for i in irange(n): j = (i + 1) % n ((c, s), (d, t)) = (seq[i], seq[j]) if c == d or s != t: continue k = seq.index((c, -s)) # each crossing is met once over and once under l = (k + 1) % n if seq[l] == (d, -t): return {i, j, k, l} # type III is not necessary for small knots (and doesn't reduce the number of crossings) # calculate the reduced sequence of crossings def reduced(seq): seq = list(seq) # apply type I and type II moves until none remain while seq: drop = type1(seq) or type2(seq) if not drop: break seq = delete(seq, drop) return seq # generate a Gauss word for a sequence of crossings def gauss(cs): (rs, d, n) = (list(), dict(), 0) for (i, _) in cs: if i not in d: n += 1 d[i] = n rs.append(d[i]) return tuple(rs) # recognised knots (with < 4 crossings) knots = { (): None, # "UNKNOT" (1, 2, 3, 1, 2, 3): "KNOT (trefoil)", } # determine if <path> is a knot # only detects LOOP (= None), and "KNOT (trefoil)" def is_knot(path): # walk the path and determine crossings cs = crossings(path) # reduce the sequence of crossings cs = reduced(cs) # look for recognised patterns return knots.get(gauss(cs), "UNKNOWN") # x,y multipliers for the directions (0 .. 3) mvx = [+1, 0, -1, 0] mvy = [ 0, +1, 0, -1] # check if visiting the corridors in the specified order, levels, turn # results in a path that is a knot def check(order, level, turn): x = y = 0 path = list() # accumulate line segments for (k, i) in zip(order, turn): z = level[k] path.append((x, y, z)) (d1, d2) = segs[k] # move along the first corridor x += mvx[i] * d1 y += mvy[i] * d1 path.append((x, y, z)) # turn right i = (i - 1) % 4 # move along the second corridor x += mvx[i] * d2 y += mvy[i] * d2 path.append((x, y, z)) # path must finish directly above starting point ((x0, y0, z0), (x1, y1, z1)) = (path[0], path[-1]) if not (x0 == x1 and y0 == y1 and z0 < z1): return # path must not be self intersecting if self_intersecting(path): return # path must form a knot (not an UNKNOT (LOOP)) r = is_knot(path) if r is None: return printf("order = {order}; level = {level}; turn = {turn}") printf("-> path = {path}") printf("-> {r}") printf() # choose one value from each category for (order, level, turn) in cproduct([orders, levels, turns]): check(order, level, turn)Solution: [To Be Revealed]
LikeLike