【詳解】快速冪&龜速乘&快速乘
我相信進來看的人都會快速冪,對吧(和善的眼神)
如果不會。。。。那我們現在開始講吧(要不然為什麼叫詳解2333 )
如果已經知道,就跳到下面去看吧~
1. 快速冪
1.0 快速冪的誕生——最初的思路
我們通常需要求解形如 ab mod c 的式子,當b比較小的時候,我們通常可以想到用迴圈乘來解決這個問題,就像這樣:
long long a,a1,b,c;
cin>>a>>b>>c;
a1=a;
for(int i=1;i<=b;i++)
a*=a1,a%=c;
cout<<a;
顯然這個的時間複雜度是O(b)的,在平時基本夠用了。
不過在某些大型的資料結構題裡面(比如無良的線段樹
比如這個例子:
23333 123456787654321 19260817
你可以試著拿剛才那個迴圈跑一下(微笑)
如果你把資料中的b換成123454321,也就只需要跑2.8s而已(微笑x2)
所以,正是為了解決這樣的問題,快速冪出現了。
1.1 快速冪的根據——二進位制
沒錯,快速冪和我們的老朋友二進位制又扯上關係了。
其主要原因是因為二進位制一個很重要的性質,就是當十進位制轉化為二進位制,會出現很有趣的地方。
我們來看這兩個例子:
11= 8+2+1 =23+21+20
34= 32+2 =25
如你所見,它們可以拆成很多個2n的和。這是廢話
我們再來看這兩個例子:
311=38 32 31
1334=1332 132
有沒有嗅到一絲危險的氣息 發現什麼有趣的東西?
1.2 快速冪的實現——位運算
如果你已經理解到它與二進位制的關係,那麼接下來的程式碼就很容易懂了:
//計算 (x^y)%mod
long long quick_pow(long long x,long long y,long long mod)
{
long long sum=1;
while(y!=0){
if(y&1==1)sum= sum*x%mod;
x=x*x%mod;
y=y>>1;
}
}
(其實從我個人角度來說,一般在初學某個知識點時看到模板裡面有位運算會感到頭疼。不過,跟二進位制相關的一些東西需要例外,因為這樣反倒會更加容易理解)
程式碼看不懂?沒關係,我們舉個例子就好了,就用上面的311來說:
- 開始前:x=3 y=(1011)2=11;
- 第一輪:11&1=1,sum=3, x=9, y=(1011>>1)2=(101)2=5;
- 第二輪:5&1=1,sum=27, x=81,y=(101>>1)2=(10)2=2;
- 第三輪:2&1=0,sum=27,x=6561,y=(10>>1)2=(1)2=1;
- 第四輪:1&1=1,sum=177147,x=65612,y=(1>>1)2=(0)2=0;
- 迴圈結束,退出,得到sum=311=177147.
看到了嗎?原本迴圈11次的演算法被優化成了迴圈五次,而且由於與二進位制相關聯,指數b每一次減少一半,也就是說這個演算法的時間複雜度是 O(logN)!log級別的演算法代表什麼,我不說大家也清楚~
所以,剛才的那個例子你可以拿來試試,博主不懂怎麼測程式執行時間,只會加檔案輸入輸出。 在本機上跑只花了0.57s,比起剛才的時間明顯是大大優化了。
1.3快速冪總結——二進位制大法好
二進位制,在我們學習的道路上一直起著很大的用處,無論是加快執行的速度的位運算,或是快速處理字首和的樹狀陣列,還是今天提到的快速冪,都靈活運用了二進位制的特性,從而大大優化了我們處理某些問題時浪費的時間和空間。
沒錯,快速冪的核心思想就是通過二進位制的特有屬性,先把次數轉化為二進位制,然後把那些為1的位(有貢獻的)存下來,把為0的位(無貢獻的)丟棄。我們之所以要進行這樣的迴圈,就是為了依次處理出那些為1的位,然後累乘到答案裡面。
實際上,快速冪的應用範圍不止求ab mod c這一種,矩陣快速冪就是它的一個變式,有興趣的同學可以去了解一下,在處理很多數學問題時非常有用。
(而且似乎也是考點之一??)
那麼,瞭解了快速冪的這些思想,我們就要開始挑刺思考它被hack的可能性了。
2.龜速乘
2.0 龜速乘的誕生——快速冪的BUG
假設給你一個ab mod c的式子,現在的你已經可以說:我能夠在log b的時間裡算出來!
了嗎?(和善的眼神x2)
你確定嗎?(和善的眼神x3)
來來來,算一算這個,看看你的快速冪還活著不:
19260817 2333333 1234567654321
(我會說我只是把數字換了下順序嗎)
很好,現在我們找到了快速冪的一個大問題:當模數>1e9的時候,光是在相乘的時候就已經爆long long了。(當然你可以用__int128,不過NOIP似乎不允許 )
那怎麼辦?我們不能用快速冪了嗎?
事實上想用還是可以的,不過看看標題,你就知道為了適應新的資料範圍,我們需要付出什麼代價了……
2.1 龜速乘的根據&實現——慢工出細活
比起計算機自帶的乘法,龜速乘的的執行速度還要慢上一些。
但是,它可以有效地保證你的long long不會boom的一聲炸掉,然後送給你一個神奇的數字。
程式碼奉上:
long long quick_mul(long long x,long long y,long long mod)
{
long long ans=0;
while(y!=0){
if(y&1==1)ans+=x,ans%=mod;
x=x+x,x%=mod;
y>>=1;
}
return ans;
}
long long quick_pow(long long x,long long y,long long mod)
{
long long sum=1;
while(y!=0){
if(y&1==1)sum=quick_mul(sum,x,mod),sum%=mod;
x=quick_mul(x,x,mod),x%=mod;
y=y>>1;
}
return sum;
}
相信你一眼就能看出來,這兩個東西長的不是一般的像。
如果再仔細觀察一下就會發現,快速冪裡的x是指數級增長,而龜速乘變成了翻倍,僅此而已。
2.2 龜速乘總結——快速冪的補充
實際上,龜速乘的確慢,甚至比直接用開始提到的迴圈乘法還要慢(因為龜速乘相當於一個自行取模的乘號),然而慢工出細活,正是它的慢最終為我們解決了資料過大時產生的問題。
歸根結底,龜速乘的出發點就是為了解決彌補快速冪的BUG,因而其思想與快速冪也十分接近。用一點點時間換來資料範圍的擴大,想來是個不虧的交易。
當然,平時不擔心爆long long的情況下,就沒必要把龜速乘加上了~
講了這麼多,我們再看看最上面的標題,
貌似還有個壓軸的東西沒有講……
3.快速乘
3.0快速乘的誕生—— 更精練,更簡短
人們在研發出上面龜速乘以後,再也不用為了次方運算+取模的題型煩惱了……
然而,總是有人不滿足:
就為了這種小問題,要我再多背那麼長的模板?我才不幹!
(以上純屬yy )
真相到底是什麼我們無從知曉,但我們清楚的是結果:
那就是複雜度為O(1)的,一行可以打完的,快速乘的誕生。
3.1快速乘的根據——long double和long long的轉換
這一部分也是剛剛學到,所以我只能找網上的資料了:
這是2009國家集訓隊論文:
駱可強:《論程式底層優化的一些方法與技巧》 (%%%)
說白了,其實我們就是在原地爆炸的邊緣瘋狂試探,把本來存進long long會炸掉的值先進行計算,用long double暫時存下,然後再把差值——一個不會超過long long的數字塞回去,再特判一下精度問題,就大功告成了。
所以,在壓行之後就變成了這樣:
cin>>a>>b>>mod;
cout<<((a*b-(long long)((long double)a*b/mod)*mod+mod)%mod);
簡潔明瞭,速度O(1),不過數值過大時容易造成誤差,畢竟long double轉換時會有精度上的誤差,所以在真正要用的時候,還是建議大家使用龜速乘吧
(不過聽說機房大佬dzyo用過幾次都沒什麼事,emmm……)
至此,三種與冪運算相關的演算法就全部講完啦~
(P.S:博主第一篇講解貼,若有不足敬請指出!順便謝謝各位觀看!)