快速冪(Exponentiation by squaring,平方求冪)是一種簡單而有效的小算法,它可以以的時間復雜度計算乘方。快速冪不僅本身非常常見,而且后續很多算法也都會用到快速冪。
讓我們先來思考一個問題:7的10次方,怎樣算比較快?
方法1:最朴素的想法,7*7=49,49*7=343,... 一步一步算,共進行了9次乘法。
這樣算無疑太慢了,尤其對計算機的CPU而言,每次運算只乘上一個個位數,無疑太屈才了。這時我們想到,也許可以拆分問題。
方法2:先算7的5次方,即7*7*7*7*7,再算它的平方,共進行了5次乘法。
但這並不是最優解,因為對於“7的5次方”,我們仍然可以拆分問題。
方法3:先算7*7得49,則7的5次方為49*49*7,再算它的平方,共進行了4次乘法。
模仿這樣的過程,我們得到一個在 時間內計算出冪的算法,也就是快速冪。
遞歸快速冪
剛剛我們用到的,無非是一個二分的思路。我們很自然地可以得到一個遞歸方程:
計算a的n次方,如果n是偶數(不為0),那么就先計算a的n/2次方,然后平方;如果n是奇數,那么就先計算a的n-1次方,再乘上a;遞歸出口是a的0次方為1。
遞歸快速冪的思路非常自然,代碼也很簡單(直接把遞歸方程翻譯成代碼即可):
//遞歸快速冪 int qpow(int a, int n) { if (n == 0) return 1; else if (n % 2 == 1) return qpow(a, n - 1) * a; else { int temp = qpow(a, n / 2); return temp * temp; } }
注意,這個temp變量是必要的,因為如果不把記錄下來,直接寫成qpow(a, n /2)*qpow(a, n /2),那會計算兩次
,整個算法就退化為了
。
在實際問題中,題目常常會要求對一個大素數取模,這是因為計算結果可能會非常巨大,但是在這里考察高精度又沒有必要。這時我們的快速冪也應當進行取模,此時應當注意,原則是步步取模,如果MOD較大,還應當開long long。
//遞歸快速冪(對大素數取模) #define MOD 1000000007 typedef long long ll; ll qpow(ll a, ll n) { if (n == 0) return 1; else if (n % 2 == 1) return qpow(a, n - 1) * a % MOD; else { ll temp = qpow(a, n / 2) % MOD; return temp * temp % MOD; } }
大家知道,遞歸雖然簡潔,但會產生額外的空間開銷。我們可以把遞歸改寫為循環,來避免對棧空間的大量占用,也就是非遞歸快速冪。
非遞歸快速冪
我們換一個角度來引入非遞歸的快速冪。還是7的10次方,但這次,我們把10寫成二進制的形式,也就是 。
現在我們要計算 ,可以怎么做?我們很自然地想到可以把它拆分為
. 實際上,對於任意的整數,我們都可以把它拆成若干個
的形式相乘。而這些
,恰好就是
、
、
……我們只需不斷把底數平方就可以算出它們。
我們先看代碼,再來仔細推敲這個過程:
//非遞歸快速冪 int qpow(int a, int n){ int ans = 1; while(n){ if(n&1) //如果n的當前末位為1 ans *= a; //ans乘上當前的a a *= a; //a自乘 n >>= 1; //n往右移一位 } return ans; }
最初ans為1,然后我們一位一位算:
1010的最后一位是0,所以a^1這一位不要。然后1010變為101,a變為a^2。
101的最后一位是1,所以a^2這一位是需要的,乘入ans。101變為10,a再自乘。
10的最后一位是0,跳過,右移,自乘。
然后1的最后一位是1,ans再乘上a^8。循環結束,返回結果。
這里的位運算符,>>是右移,表示把二進制數往右移一位,相當於/2;&是按位與,&1可以理解為取出二進制數的最后一位,相當於%2==1。這么一等價,是不是看出了遞歸和非遞歸的快速冪的關系了?雖然非遞歸快速冪因為牽扯到二進制理解起來稍微復雜一點,但基本思路其實和遞歸快速冪沒有太大的出入。