本文爲博主原創文章,未經允許不得轉載。如有問題,歡迎指正!
題目描述:
求 a 的 b 次方對 p 取模的值,其中 0≤a,b,p≤109 。
輸入描述:
三個用空格隔開的整數a,b和p。
輸出描述:
一個整數,表示a^b mod p
的值。
範例1
輸入
2 3 9
輸出
8
來源:牛客網
題目鏈接:https://ac.nowcoder.com/acm/contest/996/A
不考慮數據範圍,我們可以很容易想到以下的解法:
typedef long long LL;
LL POW(LL a,LL b,LL p){
LL res=1;
while(b){
res=res*a;
b--;
}
return res%p;
}
看了題目的數據範圍,發現這個演算法會出現以下兩個問題:
1. 程式超時。(b的值達到了109)
2. 乘法溢位。(res在累乘的時候可能太大)
1.超時原因:指數b過大、累乘的次數過多。故應該從指數b出發,降低累乘的次數。
2.我們知道任何一個正整數都可以由2的整數次冪相加得到。例如:
7=1+2+4 ,9=1+8 ,11=1+2+8,12=4+8,21=1+4+16。
3.令a的初始值爲A,將a的值不斷和自身相乘並不斷賦給自身,那麼A的指數也恰好爲2的整數次冪。
具體的迭代過程如下:
a=a (初始值) (a==A1)
a=a*a (a==A2)
a=a*a (a==A4)
a=a*a (a==A8)
a=a*a (a==A16)
a=a*a (a==A32)
4.由(am) *(a n)=a(m+n) 可知,在以上的迭代過程中選擇一些固定的a值和res進行累乘,就可以使得res等於Ab次方。
5.那麼要選擇哪幾次迭代的a值進行累乘呢? 我們把b轉化成二進制表示,就可以直觀地看到b是由哪些2的整數次冪相加得到。例如b等於11,其二進制爲(1011)2。故11=20 +21 +23 =1+2+8。A11 =(A1)*(A2)*(A8) =A(1+2+8) ,即選擇A1 、A2 、A8 進行累乘可以得到A11。
6.判斷b&1是否爲1可以檢測b的二進制的最後一位是否是1。不斷將b右移就可以檢測b的每一位。
7.檢測b在二進制表示下的每一位 ,若當前位是1,就把當前的迭代值a累乘。
8.回圈由b- -改爲b=b>>1,時間複雜度變爲爲log2b。
計算a^45的過程模擬:
具體程式碼如下(記爲程式碼一):
typedef long long LL;
LL POW(LL a,LL b,LL p){
int res=1;
while(b){
if(b&1)res=res*a; / /若b&1==1,就選擇當前的迭代值a和res累乘。
a=a*a; / /迭代構造a,a是初始值的2的整數次冪
b=b>>1; / /將b右移一位
} / /以上計算得到a^b
return res%p; / /取模
}
此做法降低了時間複雜度,但並沒有解決溢位的問題。
首先引入一個模運算的規律:(a * b) % p = (a % p * b % p) % p 。還可以推廣到任意多個因數因數相乘再取模:(a*b*c)%d=(a%d*b%d*c%d)%d。即任意多個因數相乘再取模等於各因數取模後的乘積再取模。對於上面優化過後的程式碼,只需每一步都對因數(a*a)取模,同時對「各因數取模後的乘積」res*a再取模,即可維持模值的正確性。由於每一步都取模,累乘後的值就不會溢位了。(注意符號*和符號%的優先順序一樣,故從左到右計算表達式)。
具體程式碼如下(記爲程式碼二):
typedef long long LL;
LL POW(LL a,LL b,LL p){
int res=1;
while(b){
if(b&1)res=res*a%p; / /對「各因數取模後的乘積」res*a再取模
a=a*a%p; / /對因數(a*a)取模
b=b>>1;
}
return res;
}
#include<iostream>
using namespace std;
typedef long long LL;
LL POW(LL a,LL b,LL p){
LL res=1%p; / /應對b=0而p=1的情況特殊情況
while(b>0){
if(b&1) res=res*a%p;
a=a*a%p;
b=b>>1;
}
return res;
}
int main(){
LL a,b,p;
cin>>a>>b>>p;
cout<<POW(a,b,p);
return 0;
}
嚴格地說,快速冪演算法應該指的是上面解決了時間複雜度的程式碼一,且程式碼返回值是res。快速冪只是快速求a^b。(字面上應該要這樣理解才合理吧)。即快速冪演算法程式碼如下。
typedef long long LL;
LL POW(LL a,LL b,LL p){
int res=1;
while(b){
if(b&1)res=res*a; / /若b&1==1,就選擇當前的迭代值a和res累乘。
a=a*a; / /迭代構造a,a是初始值的2的整數次冪
b=b>>1; / /將b右移一位
} / /以上計算得到a^b
return res; / /取模
}
至於程式碼二,應該叫「快速冪取模演算法」更合理吧。我覺得只有把兩者區分一下,逐個弄懂,纔不至於搞懵了。(很多資料都是直接上最終程式碼二,使得「求冪」和「取模」冗雜在一起,沒有遞進的邏輯分析,不容易理解)。
在一些oj上,題目的測試數據可能會是b=0,而p=1的情況(此時res應該爲0)。如果按上面的程式碼二,while回圈沒有執行則會返回錯誤值1。
typedef long long LL;
LL POW(LL a,LL b,LL p){
int res=1%p; / /應對b=0而p=1的情況
while(b){
if(b&1)res=res*a%p; / /對「各因數取模後的乘積」res*a再取模
a=a*a%p; / /對因數(a*a)取模
b=b>>1;
}
return res;
}