傳統解法
提到斐波那契數列(Fibonacci Sequence),首先想到的是經典的動規(DP)算法。
時間復雜度O(n),這里空間復雜度可以優化到O(1)。代碼如下:
int fib_n(int n) { int dp[3] = {1, 1}; if (n <= 1) return dp[n]; for (int i = 2; i <= n; ++i) dp[i % 3] = dp[(i - 1) % 3] + dp[(i - 2) % 3]; return dp[n % 3]; }
但是初次接觸O(logn)解法有如醍醐灌頂,嘆為觀止……
O(logn)解法
思路來源 1
考慮一個求冪運算。比如求an,一般來說需要n次累乘,時間復雜度顯然是O(n)。實際上可以通過遞歸達到一種更優化的效果:
an = (an/2)2 * an%2
這里的n/2是取整,如5/2=2。這樣就可以實現相當於二分的效果,時間復雜度為O(logn)。
思路來源2
對於Fib數列an(n ≥ 0),可以通過矩陣乘法的方式進行遞推:
進而可以得到:
這樣就(很機智地)把Fib數列問題轉化成了一個求矩陣冪的運算。
解題方法
結合以上思路,首先將其轉化為矩陣求冪問題,然后進行二分,O(logn)解法由此誕生。再次感慨人類清奇的腦洞 _(:з」∠)_
以下是代碼:

int** mult(int** m1, int** m2) { int** res = new int*[2]; for (int i = 0; i < 2; ++i) res[i] = new int[2]; res[0][0] = m1[0][0] * m2[0][0] + m1[0][1] * m2[1][0]; res[0][1] = m1[0][0] * m2[0][1] + m1[0][1] * m2[1][1]; res[1][0] = m1[1][0] * m2[0][0] + m1[1][1] * m2[1][0]; res[1][1] = m1[1][0] * m2[0][1] + m1[1][1] * m2[1][1]; return res; } int** recur(int x) { if (x == 0) { int** res = new int*[2]; for (int i = 0; i < 2; ++i) res[i] = new int[2]; res[0][0] = res[1][1] = 1; res[0][1] = res[1][0] = 0; return res; } if (x == 1) { int** res = new int*[2]; for (int i = 0; i < 2; ++i) res[i] = new int[2]; res[0][1] = res[1][0] = res[1][1] = 1; res[0][0] = 0; return res; } int** half = recur(x / 2); return mult(mult(half, half), recur(x % 2)); } // time: O(logn) int fib_logn(int n) { if (n == 0 || n == 1) return 1; int** mat = recur(n - 1); return mat[0][1] + mat[1][1]; }
結果比較
簡單比較一下后者的優化效果,為了是效果更明顯,這里將參數設置成一個較大的數(如109),以下是代碼以及結果:
void test() { const int num = 1e9; clock_t t1, t2; t1 = clock(); fib_n(num); t2 = clock(); printf("O(n): %.4f s\n", (double)(t2 - t1) / CLOCKS_PER_SEC); t1 = clock(); fib_logn(num); t2 = clock(); printf("O(logn): %.4f s\n", (double)(t2 - t1) / CLOCKS_PER_SEC); }
結果
O(n)算法的速度達到了男子百米的世界頂級水平,而O(logn)只表現出一臉不屑……
好吧,我服……那我把logn的多跑幾次 for (int i = 0; i < 100; ++i) fib_logn(num);
那么結果也很明顯了,O(logn)算法表現驚艷!
THE END