[HN Gopher] A Linear Algebra Trick for Computing Fibonacci Numbe... ___________________________________________________________________ A Linear Algebra Trick for Computing Fibonacci Numbers Fast Author : todsacerdoti Score : 51 points Date : 2023-11-06 13:54 UTC (7 hours ago) (HTM) web link (codeconfessions.substack.com) (TXT) w3m dump (codeconfessions.substack.com) | davesque wrote: | This is also a nice way to show that the growth of fib numbers is | exponentially bounded. | abhi9u wrote: | Yes, that's a good point. | mjcohen wrote: | Any solution to a linear recurrence relation is exponentially | bounded by the root(s) of the characteristic polynomial. | pfdietz wrote: | The closed form solution as implemented will use floats with some | fixed number of bits, right? So it cannot possible compute the | numbers precisely except in a finite number of initial cases. | | Computing the matrix power by squaring means the sizes of the | integers are small until the final step, so that final step | dominates the run time. | scythe wrote: | All of this gets much easier if you notice that the smaller | eigenvalue is less than one. So its contribution becomes | exponentially smaller at higher F_i. Hence, you can just take | the corresponding power of phi, multiply by some constant I | forgot, and round to the nearest integer. No need for an exact | formula. | munchler wrote: | I think you're right, and the article says "in some rare cases | the method may produce incorrect result due to approximation | errors". | pfdietz wrote: | If by "rare cases" one means "for all but a small number of | cases". | tedunangst wrote: | What's "small" in this case? I only have 32GB of RAM, which | is only enough for a vanishingly small set of Fibonacci | numbers. | chowells wrote: | 79. There are only 79 Fibonacci numbers (starting at 0, | counting the 1 twice) that are exactly represented by a | double-precision float. That's a lot less than 32GB of | RAM. | __rito__ wrote: | Yup, I implemented all these, and only naive Python array | based approach is the most practical, Python being a GC | language. | | For all other things that I tried, I either get integer | overflows (for linear algebra based implementations) or | floating point errors (for the closed form). | dietrichepp wrote: | I think the proof for the closed-form version is accessible to | people with a background in linear algebra. The matrix is | diagonalizable (not _all_ matrixes are diagonalizable, but this | one is): M = P D P^-1 | | Here, P is some invertible matrix and D is a diagonal matrix. The | powers of M reveal how useful this is: M^N = (P D | P^-1) * ... * (P D P^-1) | | If you adjust the parentheses, you'll see N-1 terms of (P^-1 P), | which can be removed, giving: M^N = P D^N P^-1 | | The powers of a diagonal matrix is done by taking powers of the | entries on the diagonal. | | You see the ph values (1+-[?]5)/2 in the matrix D. | | Diagonalization of a 2x2 matrix is simple to do on paper. The | diagonal of D contains the eigenvalues of M, which can be found | using the quadratic formula. Proving that this is a correct way | to diagonalize any diagonalizable matrix is more of a chore, but | for this specific 2x2 matrix, you can just show that that you've | found the values for P and D. | | This is very elegant mathematically, but I would not use the | closed-form solution if I wanted the exact answer, because you'd | need a lot of precision, and that's inconvenient. | abhi9u wrote: | (Author of the article here) Thanks for writing this up, it's | much much more intuitive than what I've read everywhere else. I | primarily looked up Knuth (TAOCP Vol 1) for this part, and he | showed a longish proof using generating functions which looked | too long to show in the article, and I would not have done a | better job than him. | | The thirty three miniatures book showed a more abstract proof, | which would have been accessible to people deeply familiar with | the concept of basis vectors. | dietrichepp wrote: | My own personal experience is that, in linear algebra, there | are a lot of different ways to prove the same thing. When you | search for a proof of something, you may find a proof that is | overly basic and verbose, or a proof that is overly advanced | and terse. | | Finding an explanation of a linear algebra concept that | strikes the right balance can be time-consuming or difficult. | This is why you see, for example, a hojillion different | articles explaining what quaternions are. Everybody is most | comfortable at a different place on the complexity spectrum-- | from "a vector is (x,y,z), here are rules for how that works" | to "a vector space is an abelian group V, field F, and a map | in Hom(F,End(V))". | abhi9u wrote: | That's quite true. And so many books to teach it. | | I am a software engineer and don't have a major in math | beyond studying discrete math in college. So I really have | to work hard to understand these proofs. And I certainly | value proofs explained in simple terms. :) | kragen wrote: | what if you use the closed-form answer, but without any | floating-point or other approximations | | i mean it all comes out to an integer in the end, all the | radicals cancel | | i see tromp already suggested a form of this | abhi9u wrote: | But that seems to be computationally same amount of work as | the matrix form, so we get similar performance? | kragen wrote: | hmm, maybe? i haven't tried it so i can't be sure, but it's | plausible | kmill wrote: | Yes, I believe so, up to some multiplicative factor. | | You can carry out the exponentiations in the field | Q[sqrt(5)], which is two-dimentional over Q. The | interesting thing here is that diagonalization is trading | one 2d vector space for another -- one with a very well- | behaved multiplication operation (it's distributive, | commutative, associative, and invertible). | | There's no need to do this to quickly compute fibonacci | numbers, but it's pretty neat. | tromp wrote: | > Another interesting thing to note here is the performance of | fib_fast and fib_closed_form algorithms. Both of these algorithms | essentially compute the nth power of an expression and therefore | their complexities in terms of n are same. However, the golden | ratio based algorithm is computing the nth power of a scalar | value which can be done much faster than computing the nth power | of a 2X2 matrix, thus the difference in their actual run-time | performance. | | Computing powers of the golden ratio should not be done as | scalars, since as other posters noted that requires a lot of | precision and makes for a messy computation. It's more simply | done on two dimensional numbers of the form a + sqr(5)*b with a | and b integers, analogous to complex numbers. Then the | computational effort can be seen to equal that of the matrix | powers. | reginaldo wrote: | For the initiated, this is SICP[1] Exercise 1.19. That exercise | walks you through a proof without explicitly using linear | algebra. I remember having a blast solving this back in the day. | | [1] https://web.mit.edu/6.001/6.037/sicp.pdf => page 61 | abhi9u wrote: | Interesting, TIL. | | Thanks for sharing. :) | abhi9u wrote: | I just read the exercise. That's very clever. | | Makes me want to sit and go through the book. | nimish wrote: | It's easier to treat these as linear difference equations with | constant coefficients and use the ansatz of l^{n}. This is | morally the same thing but without the need for matrix | manipulation. | | Knuth, Oren and Patashnik's Concrete Mathematics is full of these | and is required reading for discrete math. | SuchAnonMuchWow wrote: | The comparison with the closed form is weird to me: since it uses | sqrt(5), I suppose the author intended to use floating point | numbers to compute it. But when doing so: | | - You get a constant time algorithm, since the power function is | constant time on most mathematical librarie. Without entering | into details on how the pow function is actually computed, on | real number you can compute it with: pow(x,y) = exp(y * log(x)). | It is not visible on the performance graph probably because the | matrix-based algo and the closed formula are dwarfed by the | linear time algo. | | - You gen an incorrect result very quickly: not even considering | the fact that sqrt(5) can not be represented exactly by a | floating point number, the result will be out of the range of | floating point numbers with n proportional to the number of bits | your floating point number holds, so probably between n=50 and | n=100. | abhi9u wrote: | Author here: | | At the time of writing, I was not aware that the matrix form | and the closed form are same, because you can derive the golden | ratio based formula from the matrix formula. Full disclaimer: I | am not a math major, and I can make such mistakes :) | | Although, I am curious to learn about exponentiation of real | numbers. While writing this article, I was investigating how | different libraries/languages implement exponentiation. While | for matrix exponentiation, numpy used the algorithm I showed in | the article. I also looked at the pow() function in libm of | FreeBSD and it used the form you provided. I've been trying to | look at an explanation behind this. Please share a pointer if | you have any references. | SuchAnonMuchWow wrote: | The main issue with floating point number is that it doesn't | act like a ring | (https://en.wikipedia.org/wiki/Ring_(mathematics)) because of | rounding errors. For example the equality x*(y*z) = (x*y)*z | doesn't hold because each multiplication introduce small | rounding errors. | | And this kind of equations are required for the | exponentiation-by-squaring method (consider for example how | x**5 is computed: x**5 = (((x*x)*x)*x)*x) using a naive | exponentiation approach, but also x**5 = x*(x*x)*(x*x) = | x*(x*x)**2 using the exponentiation-by-squaring method). | | Integer operations and matrix operations are both rings, so | the exponentiation-by-squaring approach works. For floating | points, it doesn't give an exact result, but could still be | used in practice to get an approximate result. | | The pow function doesn't use this approach however, because | both arguments of the pow function are floating points | numbers, and can thus compute things like pow(5, 0.5) = | sqrt(5), or even 534.2**12.7. The exponentiation-by-squaring | approach works only when the exponent is an integer, so we | need other solutions. | | How exactly the pow function (or other math function like | exp, log, cos, ...) are computed is a vast research subject | in itself, but if you are interested, the Handbook of | Floating-Point Arithmetic (https://perso.ens-lyon.fr/jean- | michel.muller/Handbook.html) is both a good introduction to | the domain (and the problems associated with the finite | precision of floating point number), and provides a lot of | techniques to work around or minimize the rounding errors | that happens when manipulating floating point numbers. | wrsh07 wrote: | Please do look at the results of taking that matrix to different | powers: | https://www.wolframalpha.com/input?i=%5B%5B1%2C+1%5D%2C+%5B1... | | What is this matrix doing? When you left multiply it, you're | summing the elements of the left column and putting them in the | top left (next fib number). Then you're moving the current number | to bottom left (and top right). Incidentally, you move the | previous to bottom right. | | That's interesting!! It's very simple | abhi9u wrote: | Yes, indeed. If you see the thirty three miniatures book, it | gives the matrix exponentiation formula for computing Fibonacci | numbers, without any explanation behind it. To write the | article, I tried to figure out ways it can be explained. And | this pattern you suggested shows up nicely even when you | express successive Fibonacci numbers in terms of the base cases | F0 and F1. Which ultimately lead me to realize, the | coefficients are the elements of the matrix. | __rito__ wrote: | Well, I am also in the reading group, and I was very happy to see | this pop up here. | | But, all these method, in practicality, leads to integer | overflows. | | Works very well for smaller Fibonacci numbers, but not for, say, | the 1500th Fibonacci number. | | That is very easy and practical to do instead with the naive | formula using an array based approach. fibs = | [0, 1] for i in range(2,n): | fibs.append(fibs[-2] + fibs[-1]) | | That's the core. | | I have been meaning to write it up, but dealing with a bad flu. | | The closed form leads to approximation errors. | | I tried Julia, Numpy, Numba, Jax, Rust, but integer overflows are | there in any parallelized approach. | | The naive implementation with LRU caching is the fastest and most | reliable so far. | | I am surely missing something, you can actually use some things | to overcome integer overflows, right? | | I hoped to see those in this article, but they were simply not | there. | | Can anyone else help? | | Even UInt128 is not enough for 1500th Fibonacci number. The | upside of Rust, is that it doesn't fail silently. The compiler is | really helpful. | | Jax, Julia, Numpy confidently give wrong answers. Only way to get | the feel is to eyeball a bunch of entries and check for negative | numbers. Very easy to spot overflows when you are at, say, 1600th | fibonacci, but one can feel confident on wrong answers on smaller | numbers. | g0xA52A2A wrote: | Reminds me of similar post with some nice visualisations | | https://ianthehenry.com/posts/fibonacci/ | | https://news.ycombinator.com/item?id=36942033 | Avshalom wrote: | A (i think) related trick: | | https://kukuruku.co/hub/algorithms/using-the-quick-raise-of-... | | https://news.ycombinator.com/item?id=8799088 | kmill wrote: | We essentially implemented this matrix version in Lean/mathlib to | both compute the fibonacci number and generate an efficient proof | for the calculation. | | https://github.com/leanprover-community/mathlib4/blob/master... | | In practice this isn't very useful (the definition of Nat.fib | unfolds quick enough and concrete large fibonacci numbers don't | often appear in proofs) but still it shaves a bit of time off the | calculation and the proof verification. | cfen wrote: | Why is there no mention of a generating function approach to | derive the analytical form? | cfen wrote: | Why is there no mention of using a generating function approach | to calculate the analytical function? ___________________________________________________________________ (page generated 2023-11-06 21:00 UTC)