####My search for the perfect solution to find a number in the Fibonacci sequence in Ruby My name is Daniela Grossmann and started learning Ruby in July this year when I got accepted for the Fall Cohort at Dev Bootcamp. As preparation we got a few exercises with RSpec running in the background to control our solutions. One week ago, out of curiosity, I tried to create exercises by writing my own RSpec tests and made all of them available for other students after consulting with Jesse Farmer, my future teacher. One of my exercises was to write a program which takes a number and checks if it's one of the Fibonacci sequence. I also wrote a solution for this problem which was the following, but Jesse told me, there is a better one.
def is_fibonacci?(i)
arr = [0,1]
until arr.last >= i
a,b = arr.pop(2)
arr.push(a,b,a+b)
end
if arr.last == i
true
else
false
end
endI took the bait and worked on it until today - more or less intense (but I dreamed about it, as they predicted, that this could happen, when I applied). Jesse led my through the problem, motivated and calmed me, when I needed it. I learned a lot, like the use of Benchmark, lots of Math, one or two things about me and how much I will love starting Dev Bootcamp in October. Now I'm going to tell you about the path from my first solution to find that one:
require 'Abst'
def is_fibonacci?(n)
perfect_square?(5*n**2 + 4) || perfect_square?(5*n**2 - 4)
end
def perfect_square?(n)
RESIDUES[256][n % 256] ? Abst.isqrt(n)**2 == n : false
end
RESIDUES = {
256 => [true, true, false, false, true, false, false, false, false, true, false, false, false, false, false, false, true, true, false, false, false, false, false, false, false, true, false, false, false, false, false, false, false, true, false, false, true, false, false, false, false, true, false, false, false, false, false, false, false, true, false, false, false, false, false, false, false, true, false, false, false, false, false, false, true, true, false, false, true, false, false, false, false, true, false, false, false, false, false, false, false, true, false, false, false, false, false, false, false, true, false, false, false, false, false, false, false, true, false, false, true, false, false, false, false, true, false, false, false, false, false, false, false, true, false, false, false, false, false, false, false, true, false, false, false, false, false, false, false, true, false, false, true, false, false, false, false, true, false, false, false, false, false, false, true, true, false, false, false, false, false, false, false, true, false, false, false, false, false, false, false, true, false, false, true, false, false, false, false, true, false, false, false, false, false, false, false, true, false, false, false, false, false, false, false, true, false, false, false, false, false, false, false, true, false, false, true, false, false, false, false, true, false, false, false, false, false, false, false, true, false, false, false, false, false, false, false, true, false, false, false, false, false, false, false, true, false, false, true, false, false, false, false, true, false, false, false, false, false, false, false, true, false, false, false, false, false, false, false, true, false, false, false, false, false, false]
}My first step on this path was the insight, that I can use the fact, that a number n is a Fibonacci number when 5n^2+4 or 5n^2-4 is a perfect square. Not that much of a problem so far, but using Math.sqrt(n) even with #to_i doesn't provide the precise results for large numbers that I would need to solve this. So the following solution was fast for small numbers, but the output for larger numbers was wrong.
def is_fibonacci?(n)
perfect_square?(5n^2+4) || perfect_square?(5n^2-4)
end
def perfect_square?(n)
Math.sqrt(n).round**2 == n
endSo my next quess was to find a better way to calculate a root. Therefore the Newton's Method was handy and I added the following code which made the program very slow and me very disappointed.
# ...
def perfect_square?(n)
isqrt(n)**2 == n
end
def isqrt(n)
a = newton(1,n)
b = newton(a,n)
until b >= a-1
a = b
b = newton(a,n)
end
b
end
def newton(x,n)
(x+(n/x))/2
endThen I learned about quadratic residue and how to reduce the amount of potential perfect squares by evaluating m % 4 == 1 and m % 4 == 0 - since only numbers meeting these two conditions can be perfect squares.
# ...
def perfect_square?(n)
n%4 == 1 || n%4 == 0 ? isqrt(n)**2 == n : false
end
# ...I experimented with more numbers and number combinations, but this one made it significantly faster again.
# ...
def perfect_square?(n)
if not [0,1].include?(n%4)
return false
elsif not [0,1,4].include?(n%5)
return false
elsif not [0,1,2,4].include?(n%7)
return false
elsif not [0,1,4,7].include?(n%9)
return false
elsif not [0,1,3,4,9,10,12].include?(n%13)
return false
elsif not [0,1,2,4,8,9,13,15,16].include?(n%17)
return false
end
isqrt(n)**2 == n
end
# ...I experimented with binary operators to get rid of the moduli by using n & (x-1) instead of n % x but it only works for a number 'x = 2^y` and the real next step was to pre-calculate the quadratic residues using code, because I calculated it for modulo 256.
def is_fibonacci?(n)
perfect_square?(5*n**2 + 4) || perfect_square?(5*n**2 - 4)
end
def perfect_square?(n)
RESIDUES[256][n % 256] ? isqrt(n)**2 == n : false
end
def isqrt(n)
a = newton(1,n)
b = newton(a,n)
until b >= a-1
a = b
b = newton(a,n)
end
b
end
def newton(x,n)
(x+(n/x))/2
end
RESIDUES = {
256 => [true, true, false, false, true, false, false, false, false, true, false, false, false, false, false, false, true, true, false, false, false, false, false, false, false, true, false, false, false, false, false, false, false, true, false, false, true, false, false, false, false, true, false, false, false, false, false, false, false, true, false, false, false, false, false, false, false, true, false, false, false, false, false, false, true, true, false, false, true, false, false, false, false, true, false, false, false, false, false, false, false, true, false, false, false, false, false, false, false, true, false, false, false, false, false, false, false, true, false, false, true, false, false, false, false, true, false, false, false, false, false, false, false, true, false, false, false, false, false, false, false, true, false, false, false, false, false, false, false, true, false, false, true, false, false, false, false, true, false, false, false, false, false, false, true, true, false, false, false, false, false, false, false, true, false, false, false, false, false, false, false, true, false, false, true, false, false, false, false, true, false, false, false, false, false, false, false, true, false, false, false, false, false, false, false, true, false, false, false, false, false, false, false, true, false, false, true, false, false, false, false, true, false, false, false, false, false, false, false, true, false, false, false, false, false, false, false, true, false, false, false, false, false, false, false, true, false, false, true, false, false, false, false, true, false, false, false, false, false, false, false, true, false, false, false, false, false, false, false, true, false, false, false, false, false, false]
}I was back on track and the program was again faster. Now I took a closer look at my isqrt(n) method. I learned that using a number closer to the solution as an initial number, it will get faster. I tried a few things like using again Math.sqrt(n).to_i which was fast but not precise enough, then I used the discrete logarithm, which is about finding the number e that has 2^e <= n < 2^(e+1) and replace 1 in the initial newton() method with 2^[(e+2)/2] and then I tried Math.log2(n).to_i which has the same issues as Math.sqrt(n).to_i.
# ...
def isqrt(n)
# a = newton(1,n)
# a = newton(Math.sqrt(n).to_i,n)
# a = newton(get_initial(n),n)
a = newton(Math.log2(n).to_i,n)
b = newton(a,n)
until b >= a-1
a = b
b = newton(a,n)
end
b
end
def get_initial(n)
e = 1
until n >= 2**e && n < 2**(e+1)
e += 1
end
2**((e+2)/2)
end
# ...Finally I found the Gem Abst and it's additions to the Math methods. It has methods like Abst.isqrt(n), Abst.ilog2(n) and #square?(). I experimented with all of them but the fastest result was this one:
require 'Abst'
# ...
def perfect_square?(n)
RESIDUES[256][n % 256] ? Abst.isqrt(n)**2 == n : false
end
# ...By the way: the resulting program was ten times faster than my first one and more than hundred times faster than some in between. It was great going through this exercise and getting a taste of how amazing the time at Dev Bootcamp will be. Thanks Jesse for this experience.