Fibonacci Fun
A Riddler Classic problem gives us an excuse to implement and benchmark Fibonacci calculations in Julia.
This week's Riddler Classic is framed as a curious exchange between a postal worker and a customer. The goal is to encode an integer (a street number) by its place in a Fibonacci sequence. Specifically, if $m$ and $n$ are the first two elements, then our street address should be the number $q$ places in the sequence after $n$, i.e. the $(2 + q)^{\text{th}}$ number in that sequence. For example, the standard Fibonacci sequence ${1, 1, 2, 3, 5, 8, 13, \dots}$ has $m=1$ and $n=1$, and the number $8$ would have $q=4$. Thus, we would encode our street number $8$ as $(m, n, q) = (1, 1, 4)$. However, we wish to further require that the choices of $m$ and $n$ maximize $q$. Our goal is to find the encodings for $81$ and $179$ with maximal values of $q$.
Coding
Implementing Fibonacci sequences with double recursion is a famously touted microbenchmark for Julia. Still, we'll implement it in a more sane way. This function iterates up the chosen sequence to find the value of $q$ for the given integer goal, and returns $0$ if goal is not in the sequence.
function qfib(m, n, goal)
m = convert(typeof(goal), m)
n = convert(typeof(goal), n)
q = 0
while true
q += 1
total = m + n
if total == goal
return q
end
if total > goal
return 0
end
m = n
n = total
end
end
One note about the first two lines of the function. Why are we converting m and n to have the same type as goal? Fibonacci numbers increase very quickly. For our problem of encoding $81$ and $179$, it's not a big deal, but if we generalize even a little bit to later values, the numbers will not fit in the default Int64. If our goal is an Int128, we will introduce type instability which is ruinous to performance. (When we go beyond Int128, will need BigInt.)
Benchmarks
Curious about performance? Obviously it doesn't matter at all for values like $81$ and $179$, but how does this implementation fare against very large values? Using this table of Fibonacci numbers, we find:
# This is the largest Fibonacci number to fit in Int64
julia> @btime qfib(1, 1, 7540113804746346429)
73.004 ns (0 allocations: 0 bytes)
90
This is the $92^{\text{nd}}$ Fibonacci number (so $q = 90$ is correct). Seems fast enough. Let's try something larger. The $167^{\text{th}}$ Fibonacci number should give $q=165$.
julia> typeof(35600075545958458963222876581316753)
Int128
julia> @btime qfib(1, 1, 35600075545958458963222876581316753)
315.485 ns (0 allocations: 0 bytes)
165
Now for something really big, the $300^{\text{th}}$ Fibonacci number:
julia> typeof(222232244629420445529739893461909967206666939096499764990979600)
BigInt
julia> @btime qfib(1, 1, 222232244629420445529739893461909967206666939096499764990979600)
20.770 μs (600 allocations: 16.70 KiB)
298
Pretty fast indeed :)
Back to the coding …
Now that we have a much-faster-than-needed Fibonacci calculator, we should implement the search for the choices of $m$ and $n$ that maximize $q$. This is straightforward. The function maxq returns the triple $(m, n, q)$ that maximizes $q$ for a given goal.
function maxq(goal)
best = (0, 0, 0)
for m in 1:(goal - 1)
for n in 1:(goal - m)
q = qfib(m, n, goal)
if q > best[3]
best = (m, n, q)
end
end
end
best
end
Results
For the desired "street numbers" $81$ and $179$ we find:
julia> @btime maxq(81)
11.997 μs (0 allocations: 0 bytes)
(3, 2, 7)
julia> @btime maxq(179)
57.371 μs (0 allocations: 0 bytes)
(11, 7, 6)
The number $81$ is encoded by $(3, 2, 7)$ and $179$ is encoded by $(11, 7, 6)$. Interestingly, they both maximize $q$ with choices of $m$ and $n$ where $m > n$.