Problem 662: Fibonacci Paths
View on Project EulerProject Euler Problem 662 Solution
EulerSolve provides an optimized solution for Project Euler Problem 662, Fibonacci Paths, with C++, Python, Java, and a step-by-step mathematical explanation.
Problem Summary We must count directed lattice paths from \((0,0)\) to \((w,h)\). Every step is a nonzero vector \((a,b)\in \mathbb{Z}_{\ge 0}^2\) whose Euclidean length is a Fibonacci number, so each admissible move satisfies $$a^2+b^2=F_k^2$$ for some Fibonacci number \(F_k\). The answer is required modulo \(10^9+7\). The main difficulty is that the step set is neither purely local nor regular. Horizontal and vertical Fibonacci jumps are always possible when they fit inside the rectangle, while off-axis moves only appear when a Fibonacci number is also the hypotenuse of an integer right triangle, such as \((3,4)\) for length \(5\). Mathematical Approach Let \(D(x,y)\) denote the number of admissible paths from the origin to \((x,y)\). The solution has two layers: first enumerate all legal step vectors, then evaluate an acyclic dynamic program over the grid. Step 1: Restrict the Relevant Fibonacci Lengths Any usable step must satisfy \(0\le a\le w\) and \(0\le b\le h\). Therefore its length cannot exceed the diagonal of the target rectangle: $$R=\left\lfloor\sqrt{w^2+h^2}\right\rfloor.$$ Only Fibonacci numbers \(F_k\le R\) can matter. This immediately makes the geometric preprocessing finite and small, because the Fibonacci sequence grows exponentially....
Detailed mathematical approach
Problem Summary
We must count directed lattice paths from \((0,0)\) to \((w,h)\). Every step is a nonzero vector \((a,b)\in \mathbb{Z}_{\ge 0}^2\) whose Euclidean length is a Fibonacci number, so each admissible move satisfies
$$a^2+b^2=F_k^2$$
for some Fibonacci number \(F_k\). The answer is required modulo \(10^9+7\).
The main difficulty is that the step set is neither purely local nor regular. Horizontal and vertical Fibonacci jumps are always possible when they fit inside the rectangle, while off-axis moves only appear when a Fibonacci number is also the hypotenuse of an integer right triangle, such as \((3,4)\) for length \(5\).
Mathematical Approach
Let \(D(x,y)\) denote the number of admissible paths from the origin to \((x,y)\). The solution has two layers: first enumerate all legal step vectors, then evaluate an acyclic dynamic program over the grid.
Step 1: Restrict the Relevant Fibonacci Lengths
Any usable step must satisfy \(0\le a\le w\) and \(0\le b\le h\). Therefore its length cannot exceed the diagonal of the target rectangle:
$$R=\left\lfloor\sqrt{w^2+h^2}\right\rfloor.$$
Only Fibonacci numbers \(F_k\le R\) can matter. This immediately makes the geometric preprocessing finite and small, because the Fibonacci sequence grows exponentially.
Step 2: Enumerate the Admissible Step Vectors
Define the step set
$$S=\left\{(a,b)\in \mathbb{Z}_{\ge 0}^2\setminus\{(0,0)\}: a^2+b^2=F_k^2 \text{ for some } F_k\le R\right\}.$$
For each relevant Fibonacci number \(F_k\), we search for all nonnegative integer lattice points on the circle \(a^2+b^2=F_k^2\) that also satisfy \(a\le w\) and \(b\le h\). Axis-aligned moves \((F_k,0)\) and \((0,F_k)\) appear automatically when they fit. Additional moves come from Pythagorean triples.
After sorting and deduplication, split \(S\) into
$$V=\{d\ge 1:(0,d)\in S\},\qquad O=\{(a,b)\in S:a>0\}.$$
The set \(V\) contains the purely vertical moves, while \(O\) contains everything that advances in the \(x\)-direction, including horizontal moves.
Step 3: Write the Global Recurrence
The path count satisfies the direct recurrence
$$D(x,y)=\sum_{\substack{(a,b)\in S\\a\le x,\ b\le y}} D(x-a,y-b),\qquad D(0,0)=1.$$
This is valid because every admissible path reaching \((x,y)\) has a well-defined final step, and removing that final step leaves a path to \((x-a,y-b)\). Since \(a\) and \(b\) are never both zero, every dependency points strictly left, strictly down, or both. The state graph is therefore acyclic.
Step 4: Separate Earlier Columns from the Current Column
The only awkward part is that vertical moves have \(a=0\), so they stay inside the same column. For a fixed column \(x\), define the contribution from all moves that come from earlier columns:
$$A_x(y)=\sum_{\substack{(a,b)\in O\\a\le x,\ b\le y}} D(x-a,y-b).$$
Once \(A_x(y)\) is known, the current column can be filled from bottom to top using
$$D(x,y)=A_x(y)+\sum_{\substack{d\in V\\d\le y}} D(x,y-d),$$
with one extra \(1\) added at \((x,y)=(0,0)\) for the empty path. This transforms the two-dimensional recurrence into a clean sweep: all non-vertical steps are accumulated first, then vertical steps are resolved in increasing \(y\) order within the same column.
Step 5: Keep Only a Sliding Window of Columns
Let
$$M=\max\{a:(a,b)\in O\},$$
with the convention \(M=0\) if no such step exists. A transition into column \(x\) can only read columns \(x-1,x-2,\dots,x-M\). Anything older is irrelevant. So instead of storing the whole \((w+1)\times(h+1)\) table, it is enough to store columns modulo \(M+1\). This is exactly why a ring buffer is correct.
Worked Example: \((w,h)=(3,4)\)
Here the diagonal length is
$$R=\sqrt{3^2+4^2}=5,$$
so the relevant Fibonacci lengths are \(1,2,3,5\). The admissible steps are
$$V=\{1,2,3\},\qquad O=\{(1,0),(2,0),(3,0),(3,4)\}.$$
Column \(x=0\) is determined only by vertical moves:
$$D(0,0)=1,\quad D(0,1)=1,\quad D(0,2)=2,\quad D(0,3)=4,\quad D(0,4)=7.$$
For \(x=1\), the only non-vertical source is \((1,0)\), so \(A_1(y)=D(0,y)\). After adding vertical continuations we get
$$D(1,0)=1,\quad D(1,1)=2,\quad D(1,2)=5,\quad D(1,3)=12,\quad D(1,4)=26.$$
For \(x=2\), the source term is \(A_2(y)=D(1,y)+D(0,y)\), which yields
$$D(2,0)=2,\quad D(2,1)=5,\quad D(2,2)=14,\quad D(2,3)=37,\quad D(2,4)=89.$$
For \(x=3\), we add contributions from \((1,0)\), \((2,0)\), \((3,0)\), and the diagonal step \((3,4)\) from the origin. The final value becomes
$$D(3,4)=278,$$
which matches the small checkpoint used by the implementation.
How the Code Works
The C++, Python, and Java implementations all follow the same mathematical pipeline. They first generate Fibonacci numbers up to \(R\), then enumerate every admissible pair \((a,b)\) by scanning \(a\) and checking whether \(F_k^2-a^2\) is a perfect square. After sorting and deduplicating the pairs, they store vertical moves separately from moves with positive \(x\)-advance, and they record the largest horizontal component \(M\).
The dynamic program then processes columns from \(x=0\) to \(x=w\). For each column, a temporary array accumulates every contribution coming from earlier columns. After that, the column itself is filled from \(y=0\) upward, so same-column vertical dependencies are already available when needed. The C++ and Java implementations execute this recurrence directly, while the Python implementation reuses the same native computation and returns the parsed final value. In every case, all arithmetic is reduced modulo \(10^9+7\).
Complexity Analysis
Let \(s_O=|O|\), \(s_V=|V|\), and \(M=\max\{a:(a,b)\in O\}\). Generating Fibonacci numbers costs \(O(\log R)\), and enumerating candidate steps costs
$$O\left(\sum_{F_k\le R}\min(F_k,w)\right),$$
which is modest because the Fibonacci sequence grows exponentially. The dominant part is the grid DP: each column applies every non-vertical step across the valid \(y\)-range and then applies every vertical step while sweeping upward. Thus the running time is
$$O\bigl((w+1)(h+1)(s_O+s_V)\bigr),$$
and the memory usage is
$$O\bigl((M+1)(h+1)\bigr),$$
plus one extra temporary array of length \(h+1\).
Footnotes and References
- Problem page: Project Euler 662
- Fibonacci numbers: Wikipedia - Fibonacci number
- Pythagorean triples: Wikipedia - Pythagorean triple
- Dynamic programming: Wikipedia - Dynamic programming
- Circular buffer: Wikipedia - Circular buffer
Problem 662 source code
C++
#include <algorithm>
#include <cassert>
#include <cstdint>
#include <iostream>
#include <utility>
#include <vector>
#include <cmath>
#include <functional>
namespace {
using u32 = std::uint32_t;
using u64 = std::uint64_t;
constexpr u32 kMod = 1'000'000'007U;
struct Steps {
std::vector<int> vertical;
std::vector<std::pair<int, int>> other;
int max_dx = 0;
};
std::vector<int> fibonacci_upto(int limit) {
std::vector<int> fib{1, 2};
while (true) {
const int nxt = fib[fib.size() - 1] + fib[fib.size() - 2];
if (nxt > limit) break;
fib.push_back(nxt);
}
return fib;
}
Steps build_steps(int w, int h) {
const int max_len = static_cast<int>(std::sqrt(static_cast<long double>(w) * w +
static_cast<long double>(h) * h));
const auto fib = fibonacci_upto(max_len);
std::vector<std::pair<int, int>> all;
for (int f : fib) {
const int fsq = f * f;
const int x_max = std::min(f, w);
for (int x = 0; x <= x_max; ++x) {
const int y2 = fsq - x * x;
if (y2 < 0) continue;
const int y = static_cast<int>(std::sqrt(static_cast<long double>(y2)));
if (y * y != y2) continue;
if (y > h) continue;
if (x == 0 && y == 0) continue;
all.emplace_back(x, y);
}
}
std::sort(all.begin(), all.end());
all.erase(std::unique(all.begin(), all.end()), all.end());
Steps s;
for (const auto& [dx, dy] : all) {
if (dx == 0) {
s.vertical.push_back(dy);
} else {
s.other.emplace_back(dx, dy);
if (dx > s.max_dx) s.max_dx = dx;
}
}
std::sort(s.vertical.begin(), s.vertical.end());
std::sort(s.other.begin(), s.other.end());
return s;
}
u32 solve_case(int w, int h) {
const Steps steps = build_steps(w, h);
const int row_len = h + 1;
const int ring = steps.max_dx + 1;
std::vector<u32> rows(static_cast<std::size_t>(ring) * static_cast<std::size_t>(row_len), 0);
std::vector<u32> acc(static_cast<std::size_t>(row_len), 0U);
auto row_ptr = [&](int ridx) -> u32* {
return rows.data() + static_cast<std::size_t>(ridx) * static_cast<std::size_t>(row_len);
};
for (int x = 0; x <= w; ++x) {
std::fill(acc.begin(), acc.end(), 0U);
u32* cur = row_ptr(x % ring);
std::fill(cur, cur + row_len, 0U);
for (const auto& [dx, dy] : steps.other) {
if (dx > x) break;
const u32* src = row_ptr((x - dx) % ring);
for (int y = dy; y <= h; ++y) {
u32 v = acc[static_cast<std::size_t>(y)] + src[static_cast<std::size_t>(y - dy)];
if (v >= kMod) {
v -= kMod;
}
acc[static_cast<std::size_t>(y)] = v;
}
}
for (int y = 0; y <= h; ++y) {
u32 val = acc[static_cast<std::size_t>(y)];
if (x == 0 && y == 0) {
val += 1;
if (val >= kMod) val -= kMod;
}
for (int dy : steps.vertical) {
if (dy > y) break;
val += cur[static_cast<std::size_t>(y - dy)];
if (val >= kMod) val -= kMod;
}
cur[static_cast<std::size_t>(y)] = static_cast<u32>(val);
}
}
const u32* target_row = row_ptr(w % ring);
return target_row[h];
}
} // namespace
int main() {
assert(solve_case(3, 4) == 278U);
assert(solve_case(10, 10) == 215846462U);
std::cout << solve_case(10'000, 10'000) << "\n";
return 0;
}
Python
from __future__ import annotations
import re
import shutil
import subprocess
from pathlib import Path
ANSWER_RE = re.compile(r"answer\s*:\s*(.+)$", re.IGNORECASE)
EQUAL_RE = re.compile(r"=\s*(.+)$")
def parse_output(stdout: str) -> str:
lines = [line.strip() for line in stdout.splitlines() if line.strip()]
if not lines:
return ""
answers = []
equals = []
for line in lines:
m1 = ANSWER_RE.search(line)
if m1:
answers.append(m1.group(1).strip())
m2 = EQUAL_RE.search(line)
if m2:
equals.append(m2.group(1).strip())
if answers:
return answers[-1]
if equals:
return equals[-1]
return lines[-1]
def should_skip_cpp_checkpoints(src: Path) -> bool:
try:
text = src.read_text(encoding="utf-8", errors="ignore")
except OSError:
return False
return "--skip-checkpoints" in text
def run_cpp(binary: Path, src: Path, root: Path) -> str:
cmd = [str(binary)]
if should_skip_cpp_checkpoints(src):
cmd.append("--skip-checkpoints")
try:
return subprocess.check_output(cmd, text=True, cwd=root)
except subprocess.CalledProcessError:
return subprocess.check_output(cmd, text=True, cwd=src.parent)
def solve() -> str:
problem_id = __file__.split("Euler")[-1].split(".")[0]
root = Path(__file__).resolve().parent.parent
src = root / "solutionsCpp" / f"Euler{problem_id}.cpp"
binary = root / "solutionsCpp" / f".euler{problem_id}_py_bridge"
if not binary.exists() or src.stat().st_mtime > binary.stat().st_mtime:
compiler = shutil.which("clang++") or shutil.which("g++")
if not compiler:
raise RuntimeError("No C++ compiler found (clang++/g++).")
subprocess.check_call([compiler, "-std=c++17", "-O2", str(src), "-o", str(binary)])
output = run_cpp(binary=binary, src=src, root=root)
parsed = parse_output(output)
if not parsed:
raise RuntimeError(f"Euler{problem_id} bridge produced empty output.")
return parsed
if __name__ == "__main__":
print(solve())
Java
import java.util.*;
public class Euler662 {
public static String solve() {
int MOD=1000000007,w=10000,h=10000;
int maxLen=(int)Math.sqrt((double)w*w+(double)h*h);
List<Integer> fibs=new ArrayList<>();fibs.add(1);fibs.add(2);
while(true){int nxt=fibs.get(fibs.size()-1)+fibs.get(fibs.size()-2);if(nxt>maxLen)break;fibs.add(nxt);}
List<Integer> vertical=new ArrayList<>();List<int[]> other=new ArrayList<>();Set<Long> seen=new HashSet<>();
for(int f:fibs){long fsq=(long)f*f;for(int x=0;x<=Math.min(f,w);x++){long y2=fsq-(long)x*x;if(y2<0)continue;
int y=(int)Math.sqrt(y2);if((long)y*y!=y2||y>h)continue;if(x==0&&y==0)continue;
long key=(long)x*100001+y;if(seen.add(key)){if(x==0)vertical.add(y);else other.add(new int[]{x,y});}}}
Collections.sort(vertical);other.sort(Comparator.comparingInt(a->a[0]));
int maxDx=0;for(int[] o:other)maxDx=Math.max(maxDx,o[0]);
int ring=maxDx+1,rl=h+1;long[][] rows=new long[ring][rl];
for(int x=0;x<=w;x++){long[] acc=new long[rl];long[] cur=rows[x%ring];Arrays.fill(cur,0);
for(int[] o:other){if(o[0]>x)break;long[] src=rows[(x-o[0])%ring];
for(int y=o[1];y<=h;y++)acc[y]=(acc[y]+src[y-o[1]])%MOD;}
for(int y=0;y<=h;y++){long val=acc[y];if(x==0&&y==0)val=(val+1)%MOD;
for(int dy:vertical){if(dy>y)break;val=(val+cur[y-dy])%MOD;}cur[y]=val;}}
return String.valueOf(rows[w%ring][h]);}
public static void main(String[] args){System.out.println(solve());}
}