Library · The Fiddler: Solutions · Chapter 148
Can You See the Forest for the Trees?
You stand at the origin of an endless grid of trees. Every other point with whole-number coordinates holds a tree, a perfect cylinder of radius . Most trees are hidden behind the ones around you. Looking in every direction, how many different trees can you see?
The Fiddler, Zach Wissner-Gross, August 11, 2023(original post)
The official solution appeared in the post of August 18, 2023 and gives trees for the main puzzle and a longest line of sight of about for the Extra Credit, past the tree at . Both agree with the answers below.
Solution
Everything happens in the flat plane at eye level, where each tree is a disc of radius .
Which tree does a sight line meet first? A ray from the origin that passes through two trees cuts each of them in a chord, and because the trees do not overlap, neither do the chords. So the ray meets the trees in the order of their chords along it, which is the order of the feet of the perpendiculars from their centres. In particular a tree whose centre is farther away, measured along the ray, is always behind. A tree at distance from you blocks every direction within an angle of its centre, and that angle is the tree’s shadow.
Using the symmetry. The grid looks the same after reflection in either axis or either diagonal, so it is enough to look at directions between due east () and northeast (), and count each tree found there as many times as the symmetry copies it. The nearby trees in that range cast these shadows:
| tree | distance | direction | shadow |
|---|---|---|---|
| up to | |||
| to | |||
| to | |||
| to | |||
| from |
The shadows overlap end to end, so every direction from to meets one of these five trees, and each of them is seen. The tree is the nearest tree of all; is first in the gap from to left by and ; is first around ; is first in the narrow gap from to between and ; and is first along the diagonal.
Every other tree is hidden. A tree at distance at least lies, along any ray that meets it, at least out, while the blocking tree on that ray is at most out, so it is behind. The closer trees not in the table are , and , whose shadows lie inside that of ; , whose shadow lies inside that of ; and , whose shadow from to is covered by and , both nearer.
Counting. The trees on the symmetry lines, and , each have copies round the circle. The other three, , and , each have . So the total is
The computation
The check sweeps all the way round. It takes every tree within units and cuts the circle of directions at both edges of every tree’s shadow. Between two neighbouring cuts no tree enters or leaves the line of sight, and since the chords along a ray never cross, the first tree met stays the same. So it is enough to look along the middle of each gap, find the tree met first there by its actual entry distance, and record it. The function also keeps track of the longest line of sight, for the Extra Credit, and checks that no ray escapes the -unit patch.
import numpy as np
def sweep(r, reach=25):
"""Look all round from the origin through a lattice of
trees of radius r. Returns the trees seen and the longest
line of sight."""
pts = np.array([(x, y) for x in range(-reach, reach + 1)
for y in range(-reach, reach + 1)
if 0 < x*x + y*y <= reach*reach], float)
d = np.hypot(pts[:, 0], pts[:, 1])
centre = np.arctan2(pts[:, 1], pts[:, 0])
half = np.arcsin(r/d) # half of each shadow
cuts = np.concatenate([centre - half, centre + half])
cuts = np.sort(cuts % (2*np.pi))
cuts = np.append(cuts, cuts[0] + 2*np.pi)
def entry(i, theta): # where the ray enters tree i
off = np.angle(np.exp(1j*(theta - centre[i])))
s = np.clip(d[i]*np.sin(off), -r, r)
return d[i]*np.cos(off) - np.sqrt(r*r - s*s)
seen, longest = set(), 0.0
for a, b in zip(cuts[:-1], cuts[1:]):
if b - a < 1e-12:
continue
mid = (a + b)/2
off = np.angle(np.exp(1j*(mid - centre)))
hit = np.abs(d*np.sin(off)) < r # trees on the ray,
hit &= np.cos(off) > 0 # in front of you
assert hit.any(), "a ray escaped"
idx = np.flatnonzero(hit)
first = idx[np.argmin(entry(idx, mid))]
seen.add(tuple(int(v) for v in pts[first]))
# the same tree is first across the gap, and the sight
# line is longest at one end
ends = entry(first, a + 1e-13), entry(first, b - 1e-13)
longest = max(longest, *ends)
assert longest < reach - 1
return seen, longest
seen, _ = sweep(0.25)
print("trees seen with radius 0.25:", len(seen))
It prints
trees seen with radius 0.25: 32
Extra Credit
Now the trees are thinner, with radius . Looking due east you can see only units, to the tree at , but other directions reach farther. How long is your longest line of sight?
Solution
A line of sight ends where it first enters a tree. For a tree at distance whose centre is a perpendicular distance from the line, the line enters it at a distance from you: the foot of the perpendicular, less half the chord. As the direction turns away from the tree’s centre, grows and grows with it.
This settles where to look. Cut the directions at the shadow edges of all the trees, as before. Within one gap the same tree is met first, so the sight line is longest at one end of the gap, where the line just grazes the tree that blocks the neighbouring directions. The longest sight lines therefore slip past the edge of one tree to land on another far behind.
The trees are now thin, so the shadows are narrow and the first trees in some directions are a long way off. The most distant tree you can see is the one at , at distance . It shows through a gap only wide, between the upper edge of the shadow of , at , and the lower edge of the shadow of , at .
The lower edge of the shadow of is the line through the origin tangent to that tree on its clockwise side. Its direction is that of turned clockwise by the angle with and , which is the unit vector The perpendicular distance of from this line is the size of the cross product , which is well inside the radius , so the line does strike the tree at . It enters it after The other end of the gap, grazing , passes with and gives a sight line of , slightly shorter because it strikes nearer the middle of the tree. So That no other gap does better is a matter of checking them all, which is what the computation below does: across the trees visible at this radius, the gap that shows gives the longest sight line.
The computation
The sweep above already tracks, gap by gap, the entry distance at both ends of each gap.
seen, longest = sweep(0.1)
far = max(seen, key=lambda p: p[0]**2 + p[1]**2)
print(f"radius 0.1: {len(seen)} trees seen, farthest {far}")
print(f"longest sight {longest:.4f}")
It prints
radius 0.1: 192 trees seen, farthest (4, 9)
longest sight 9.7491
The farthest tree is reported as , the mirror image of in the diagonal, which by symmetry gives the same sight line.