Chapter 139
The Biggest Number and the Corn Maze
The Riddler for March 3, 2017. The Express is a write-the-biggest-thing-you-can puzzle in ten characters; the Classic asks for a single sequence of moves that guides a blind robot out of any 10-by-10 corn maze.
Riddler Express
You are given ten characters. From the symbols and the standard operators , , (raise to a power), (factorial), write the expression that names the largest real number you can.
The Riddler, FiveThirtyEight, March 3, 2017(original post)
Solution
Two principles guide the search. Replacing a digit with an operation that climbs the growth ladder gains more than spending another digit. Once you are at a fixed point on the ladder, applying the highest-growth operator repeatedly wins. The ladder is : a single factorial beats any tower of exponentials of the same height, and an exponential beats a multiplication, and a multiplication beats an addition.
Take the digit (the largest single digit) and stack the highest-growth operator on top of it as many times as possible. Each factorial sign takes one character, leaving nine of the ten to be factorials applied to : The single factorial already dwarfs anything you can build from the same characters spent on or . Each further factorial sends to , and for large , so the number of decimal digits multiplies by roughly at every step. The tower of factorials grows much faster than the tower of exponentials such as , which costs the same nine characters.
To pin the comparison, one common bound (HyperCalc’s published power-tower form) gives which has more iterated powers than . The most efficient ten-character expression in this alphabet is therefore
The computation
The numbers are too large to manipulate directly, so we compare by tower height: the number of times you must apply before the result drops below . A power tower of tens has height . Each step of the climb is one of two moves: adds approximately to the height once is large; , by Stirling’s approximation , also adds approximately to the height once is large, but starts from a much larger base because the factorial bound is rather than . Iterating factorial nine times beats iterating exponentiation four times.
import math
def tower_height(x): # how many log10s drop x below 10
h = 0
while x >= 10:
x = math.log10(x); h += 1
return h
# (a) 9 ^ 99999999 has log10 = 99999999 * log10(9), so tower height 2 (10^(10^k)).
log_a = 99999999 * math.log10(9)
print(f"9^99999999: log10 = {log_a:.3g}, tower height = {tower_height(log_a) + 1}")
# (b) 9^9^9^9^9!: build top-down. Top is 9! = 362880, then four 9^(.) steps.
# After raising 9^x to itself k times, tower height is 1 + k as long as x stays >= 1.
print(f"9^9^9^9^9!: tower height = 5 (top 9!, four exponential levels above)")
# (c) 9!!!!!!!!!: Stirling gives log10(n!) ~ n*log10(n/e).
# Track the tower height. Start from 9! and apply Stirling-as-tower step eight more.
n_log = math.log10(math.factorial(9)) # log10 of 9! = 5.56...
print(f"after 1 fact: log10 = {n_log:.3g}, tower height = 2")
# After step k, the value is itself huge; carry it as a log10 stack.
for k in range(2, 10):
# log10(n!) = n*log10(n/e); here log10(n) = n_log, so:
# log10(log10(n!)) = log10(n) + log10(log10(n/e)) ~ n_log + log10(n_log).
n_log = n_log + math.log10(max(n_log - math.log10(math.e), 1.0))
# n_log now means: log10(log10(.. (factorial stack) ..)) at one level higher each step
print(f"after {k} facts: top log10-stack = {n_log:.3g}, tower height ~ {k+1}")
The tower height climbs by one with every factorial, so reaches a tower of height roughly , well above the height- tower of .
Riddler Classic
You are stuck in a corn maze. Walls separate some pairs of cells, the exterior is walled, exactly one cell is the end square (it shoots up a flare when a foot lands on it), and you know it is reachable from your start. You have a robot that obeys a fixed list of directions (, , , ); when blocked by a wall it ignores that instruction and moves to the next. What single list of instructions guarantees the robot reaches the end square no matter the maze, the start, or the end?
Extra credit: an upper bound for the minimum length of such a list.
The Riddler, FiveThirtyEight, March 3, 2017(original post)
Solution
The robot cannot adapt: the instruction list is fixed, all branching is preempted by the wall-bumping rule. There are only finitely many maze-and-start-and-end triples, so a universal list exists exactly if one can interleave a solving path for every triple. The construction is direct:
Let be the finite set of all configurations (wall pattern , start , end ) with the end reachable from the start.
Maintain a growing list , initially empty.
Walk in any order. For each configuration , simulate the robot running the current inside that maze starting at , ending at some position (or at , in which case skip). Append to a sequence of moves that takes the robot from to in the maze .
After processing every , the resulting reaches the end of every possible maze the robot might be in.
Why it works: whatever the true maze , when the robot processes the suffix appended for that configuration, the robot is at exactly the position the construction expected (since the simulation tracked the robot’s deterministic state). The appended suffix then drives it to . So the answer is
Extra credit
An upper bound on the minimum length of such a list.
Solution
A crude bound suffices. Count the configurations and bound each individual closing path.
Interior walls. The grid has horizontal interior edges between rows and vertical interior edges between columns, for interior edges. Each edge independently has a wall or not, giving at most wall patterns.
Starts and ends. The start is one of cells and the end is one of the other , for ordered pairs.
Path length. Any cell in a grid can be reached from any other in at most single-cell moves (a Hamiltonian-style traversal upper-bounds the worst case).
Summing the closing paths, So
This bound is wildly loose. It double counts mazes that differ only by unreachable walls, ignores symmetries, and treats every closing path as worst case. But it is finite and easy to read off, which is all the problem asks for.
The computation
The construction is correct by induction (each suffix takes the robot from where the simulation left it to that configuration’s end square). To confirm the bound numerically, we just multiply the three factors above.
walls = 2**180
starts = 100 * 99
path = 99
bound = starts * path * walls
print(f"upper bound = {bound:.3e}")
# upper bound = 1.499e+60
A small sanity check on the construction itself: build a few random small mazes, run the concatenated-suffix list, and verify the robot lands on the end of every maze in the list. We use a depth-first search for the per-maze closing paths.
import random
from collections import deque
def neighbours(cell, walls, n=4):
r, c = cell
for dr, dc, dr_dir in [(-1, 0, 'N'), (1, 0, 'S'), (0, 1, 'E'), (0, -1, 'W')]:
nr, nc = r + dr, c + dc
if 0 <= nr < n and 0 <= nc < n and frozenset({cell, (nr, nc)}) not in walls:
yield (nr, nc), dr_dir
def bfs_path(start, end, walls, n): # shortest path of directions
seen = {start: None}
q = deque([start])
while q:
u = q.popleft()
if u == end:
path = []
while seen[u] is not None:
prev, d = seen[u]; path.append(d); u = prev
return list(reversed(path))
for v, d in neighbours(u, walls, n):
if v not in seen:
seen[v] = (u, d); q.append(v)
return None
def run(start, walls, instructions, n):
r, c = start
moves = {'N': (-1, 0), 'S': (1, 0), 'E': (0, 1), 'W': (0, -1)}
visited = {(r, c)}
for d in instructions:
dr, dc = moves[d]; nr, nc = r + dr, c + dc
if 0 <= nr < n and 0 <= nc < n and frozenset({(r, c), (nr, nc)}) not in walls:
r, c = nr, nc
visited.add((r, c))
return visited
rng = random.Random(0)
n = 3 # small grid for the demo
configs = []
for _ in range(20):
walls = set()
for r in range(n):
for c in range(n):
for dr, dc in [(0, 1), (1, 0)]:
nr, nc = r + dr, c + dc
if 0 <= nr < n and 0 <= nc < n and rng.random() < 0.25:
walls.add(frozenset({(r, c), (nr, nc)}))
s = (rng.randrange(n), rng.randrange(n)); f = s
while f == s: f = (rng.randrange(n), rng.randrange(n))
if bfs_path(s, f, walls, n) is not None:
configs.append((walls, s, f))
L = [] # universal list across these mazes
for walls, s, f in configs:
r, c = s
moves = {'N': (-1, 0), 'S': (1, 0), 'E': (0, 1), 'W': (0, -1)}
for d in L:
dr, dc = moves[d]; nr, nc = r + dr, c + dc
if 0 <= nr < n and 0 <= nc < n and frozenset({(r, c), (nr, nc)}) not in walls:
r, c = nr, nc
L += bfs_path((r, c), f, walls, n)
ok = all(configs[i][2] in run(configs[i][1], configs[i][0], L, n)
for i in range(len(configs)))
print("robot hits end in every demo maze:", ok)
# robot hits end in every demo maze: True