Open Source · 7 Languages · 900+ Problems

Project Euler Solutions

Complete solutions in C++, Python & Java — with step-by-step mathematical explanations

All Problems

Problem 220: Heighway Dragon

View on Project Euler

Project Euler Problem 220 Solution

EulerSolve provides an optimized solution for Project Euler Problem 220, Heighway Dragon, with C++, Python, Java, and a step-by-step mathematical explanation.

Problem Summary The Heighway Dragon in this problem is generated from the symbolic rule set \(D_0=Fa\), together with the substitutions \(a\to aRbFR\) and \(b\to LFaLb\). In turtle-graphics terms, \(F\) means “move forward one unit”, \(L\) and \(R\) mean quarter-turns, and the letters \(a\) and \(b\) are only structural markers: they do not move the turtle. After \(n\) substitution rounds, deleting \(a\) and \(b\) leaves the instruction word \(D_n\). The task is to find the turtle's position after \(10^{12}\) forward moves in \(D_{50}\). The statement also provides a checkpoint: in \(D_{10}\), after 500 forward moves, the position is \((18,16)\). A direct expansion is impossible because \(D_{50}\) contains \(2^{50}\) forward moves, about \(1.13\times 10^{15}\), so the solution has to work with compressed recursive blocks rather than the full string. Mathematical Approach The right object to precompute is not the text of a block, but its geometric effect. Every recursive block can be replaced by a small summary that records displacement, heading change, and the number of forward moves it contains. Once those summaries are known for all depths, the required position is a prefix query on a recursive grammar tree....

Detailed mathematical approach

Problem Summary

The Heighway Dragon in this problem is generated from the symbolic rule set \(D_0=Fa\), together with the substitutions \(a\to aRbFR\) and \(b\to LFaLb\). In turtle-graphics terms, \(F\) means “move forward one unit”, \(L\) and \(R\) mean quarter-turns, and the letters \(a\) and \(b\) are only structural markers: they do not move the turtle. After \(n\) substitution rounds, deleting \(a\) and \(b\) leaves the instruction word \(D_n\).

The task is to find the turtle's position after \(10^{12}\) forward moves in \(D_{50}\). The statement also provides a checkpoint: in \(D_{10}\), after 500 forward moves, the position is \((18,16)\). A direct expansion is impossible because \(D_{50}\) contains \(2^{50}\) forward moves, about \(1.13\times 10^{15}\), so the solution has to work with compressed recursive blocks rather than the full string.

Mathematical Approach

The right object to precompute is not the text of a block, but its geometric effect. Every recursive block can be replaced by a small summary that records displacement, heading change, and the number of forward moves it contains. Once those summaries are known for all depths, the required position is a prefix query on a recursive grammar tree.

Rigid-Motion Summaries for Instruction Blocks

For any instruction block \(W\), define its summary by

$\Sigma(W)=(\vec v(W),d(W),s(W)),$

where \(\vec v(W)=(\Delta x,\Delta y)\) is the net displacement, \(d(W)\in\{0,1,2,3\}\) is the net number of right turns modulo 4, and \(s(W)\) is the number of forward commands \(F\) inside \(W\).

If \(\operatorname{rot}(x,y)=(y,-x)\) denotes a \(90^\circ\) right rotation, then concatenation of two blocks is described by

$\Sigma_1\star\Sigma_2=\left(\vec v_1+\operatorname{rot}^{d_1}(\vec v_2),\ (d_1+d_2)\bmod 4,\ s_1+s_2\right),$

for \(\Sigma_i=(\vec v_i,d_i,s_i)\). This is exactly the turtle rule: the second block must be interpreted in the heading left behind by the first block.

The atomic commands therefore have summaries

$\Sigma(F)=((1,0),0,1),\qquad \Sigma(R)=((0,0),1,0),\qquad \Sigma(L)=((0,0),3,0).$

These vectors are written in the local frame of the block itself. When a stored block is later applied to the actual walk, its displacement is rotated by the current heading first and then added to the global position.

Recursive Macro Families Behind the Dragon

For clarity, let \(A_n\) and \(B_n\) denote the fully expanded instruction blocks produced by the two silent symbols after \(n\) remaining substitution levels, with empty base case

$A_0=B_0=\varepsilon.$

The substitution rules become the recurrences

$A_{n+1}=A_n\,R\,B_n\,F\,R,\qquad B_{n+1}=L\,F\,A_n\,L\,B_n.$

The whole dragon word is then

$D_n=F\,A_n.$

Applying the composition law from the previous subsection gives

$\Sigma(A_{n+1})=\Sigma(A_n)\star\Sigma(R)\star\Sigma(B_n)\star\Sigma(F)\star\Sigma(R),$

$\Sigma(B_{n+1})=\Sigma(L)\star\Sigma(F)\star\Sigma(A_n)\star\Sigma(L)\star\Sigma(B_n).$

So every summary at depth \(n+1\) is built from a constant number of summaries at depth \(n\). This is the mathematical reason the preprocessing is linear in the order rather than exponential in the expanded word length.

Forward-Step Counts and a Useful Heading Invariant

Let \(f_n=s(A_n)=s(B_n)\). Both recursive definitions contain one explicit \(F\) and one copy each of the previous \(A_n\) and \(B_n\), so

$f_{n+1}=2f_n+1,\qquad f_0=0.$

Solving this recurrence gives

$f_n=2^n-1.$

Therefore \(D_n=F A_n\) contains exactly

$1+f_n=2^n$

forward moves. In particular, \(D_{50}\) has \(2^{50}\) forward steps, which is why explicit expansion is hopeless.

A second invariant concerns the final heading. If \(t_n=d(A_n)\) and \(u_n=d(B_n)\), then

$t_{n+1}\equiv t_n+1+u_n+1 \pmod 4,\qquad u_{n+1}\equiv 3+t_n+3+u_n \pmod 4,$

with \(t_0=u_0=0\). From this it follows that

$t_n=u_n=2\qquad (n\ge 1).$

So every non-empty \(A_n\) or \(B_n\) turns the turtle around by \(180^\circ\). The implementations do not need a closed-form displacement formula, but this heading invariant is useful for understanding why the summaries compose so cleanly.

Prefix Evaluation Without Expanding the Whole Word

Suppose we only want the first \(K\) forward moves inside \(A_n\). If \(K\ge s(A_n)\), then the entire block lies inside the requested prefix, so one application of \(\Sigma(A_n)\) is enough. If \(K<s(A_n)\), then the desired prefix lies somewhere inside

$A_n=A_{n-1}\,R\,B_{n-1}\,F\,R,$

and we process those pieces from left to right. The crucial detail is that the budget counts only forward moves. Turns are still applied when they occur before the \(K\)-th forward move, but they do not decrease the budget.

The same reasoning applies to \(B_n\). An induction on \(n\) shows that this recursive descent returns exactly the turtle state after the requested prefix. Structurally, at each depth there is at most one child block whose contribution is only partially used; everything before it can be swallowed whole from a precomputed summary, and everything after it is irrelevant once the budget reaches zero. That single-partial-branch invariant is what keeps the runtime linear in the order.

Worked Example: The First Two Nontrivial Levels

The first non-empty blocks are

$A_1=RFR,\qquad B_1=LFL.$

Using the local east-facing frame, their summaries are

$\Sigma(A_1)=((0,-1),2,1),\qquad \Sigma(B_1)=((0,1),2,1).$

At the next level,

$A_2=A_1\,R\,B_1\,F\,R,$

so

$\Sigma(A_2)=\Sigma(A_1)\star\Sigma(R)\star\Sigma(B_1)\star\Sigma(F)\star\Sigma(R)=((-1,-2),2,3).$

This already shows the compressed viewpoint at work: \(A_2\) contains three forward moves, but once its summary has been stored, those three moves can be consumed in one constant-time composition whenever the remaining budget covers them.

Since \(D_2=F A_2\), the whole word has \(2^2=4\) forward moves. Starting from the origin facing north, the initial \(F\) reaches \((0,1)\). If the remaining budget is 3, the entire \(A_2\) block can be applied at once. If the remaining budget were only 2, the algorithm would descend into \(A_1\), pass the explicit turn, and recurse only as far into \(B_1\) as the budget still allows. The actual \(10^{12}\)-step query uses exactly the same idea, just fifty levels deeper.

How the Code Works

Bottom-Up Summary Tables

The C++, Python, and Java implementations allocate one stored summary for each depth of the \(A\)-family and the \(B\)-family. The deepest level is the empty block, and the tables are filled upward using the two recurrence formulas above. Each entry stores only four small pieces of data: displacement, net quarter-turn, and forward-step count.

Because the problem asks for \(D_{50}\), only 51 levels are needed. That makes the preprocessing tiny: the implementations never build the enormous string itself, only the summary tables for the recursive macros.

Top-Down Prefix Traversal

Execution starts at \((0,0)\) facing north, applies the initial \(F\), and then traverses the \(A\)-family recursively. Whenever the remaining forward-step budget covers an entire stored macro, the implementation subtracts that macro's step count and composes its summary into the current state in \(O(1)\).

If the budget does not cover the full macro, the implementation expands only that one macro level: it visits the child blocks in grammar order, applies turns immediately, and decreases the remaining budget only when a forward move is actually taken. The maintained invariant is simple and exact: after each recursive return, the stored global state equals the turtle state after the already processed prefix of forward moves.

All three languages implement the same mathematics. They also agree with the checkpoint from the statement: after 500 forward moves in \(D_{10}\), the position is \((18,16)\). That sanity check confirms both the summary-composition law and the prefix-recursion logic.

Complexity Analysis

If the dragon order is \(n\), building all summaries for \(A_0,\dots,A_n\) and \(B_0,\dots,B_n\) costs \(O(n)\) time and \(O(n)\) memory. Each new level is obtained from a constant number of already known summaries.

Computing the position after one target prefix also costs \(O(n)\) time. The recursion may descend through all depths, but only one branch can remain partial at each level; every fully covered sibling is handled by a constant-time summary application. For Problem 220 this means the \(10^{12}\)-step query on \(D_{50}\) is solved with a few dozen summary compositions instead of expanding a word with \(2^{50}\) forward moves.

Footnotes and References

  1. Project Euler Problem 220: https://projecteuler.net/problem=220
  2. Dragon curve: Wikipedia - Dragon curve
  3. L-system: Wikipedia - L-system
  4. Turtle graphics: Wikipedia - Turtle graphics
  5. Rotation matrix: Wikipedia - Rotation matrix

Mathematical approach · C++ solution · Python solution · Java solution

Previous: Problem 219 · All Project Euler solutions · Next: Problem 221

Need help with a problem? Ask me! 💡
e
✦ Euler GLM 5.2
Hi! I'm Euler. Ask me anything about Project Euler problems, math concepts, or solution approaches! 🧮