1. 程式人生 > 其它 >快速冪 & 大數取模及例題(極其詳細)

快速冪 & 大數取模及例題(極其詳細)

技術標籤:zero

首先,以杭電OJ的一道題引入話題

人見人愛A ^ B

接著,先給出取模的幾個重要結論

  • (a * b) % p = (a % p * b) % p
  • (a + b) % p = (a % p + b) % p
  • (a + b) % p = (a % p + b % p) % p
  • (a - b)% p = (a % p - b % p) % p
  • (a * b)% p = (a % p * b % p) % p

對於最後一條性質,即多個因子連續的乘積取模的結果等於每個因子取模後的乘積再取模的結果

上題的要求也簡潔明瞭。即A的B次方結果的最後三位數所表示的整數。之所以把所表示的整數加粗,是由於要考慮到最後三位數字的首位為0的情況,如最後三位是024,那麼程式輸出的整數應該為24。

很多人看到這道題,第一反應是這還不簡單,直接一個pow函式求出A的B次方,再對結果取最後三位數字不久好了?下面附上程式碼

#include <iostream>
#include <cstdio>
#include <cmath>

using namespace std;

int main()
{
    int a,b; //a表示底數,b表示指數
    cin >> a >> b;
    int p = pow(a,b); //pow(a,b)可以求出a的b次方
    cout << p % 1000 << endl;
return 0; }

測試一下程式碼,2的10次方是大家熟悉的1024,結果也得到了24

在這裡插入圖片描述
但如果輸入的數字非常大呢?比如題目所給樣例的最後一個。答案會得到0或者是一個負數,因為資料太大發生了溢位。換成long long也不行,大家可以閱讀文章最後連結給出的部落格。
在這裡插入圖片描述
那麼,問題該如何解決?
需要注意:解決這類問題首先就不能使用pow函式,這是沒有演算法基礎的同學求a的b次方時首先會想到的方法,但其實沒啥用,大資料過不去。下面將程式碼改進如下:

#include <iostream>
#include <cstdio>
#include <cmath>
using namespace std; int main() { //底數和指數在int的範圍之內 int base,power; cin >> base >> power; long long res = 1; //執行power次迴圈,每次結果都乘一個base,即base的power次方 for (int i = 1;i <= power;i++) { res = res * base; } long long ans = res % 1000; cout << ans << endl; return 0; }

但是測試程式碼會發現,如果要求2的100次方,結果輸出還是0
在這裡插入圖片描述
此時,就應該想到上面提到的取模的性質了。本題只需用到最後一個性質 (a * b)% p = (a % p * b % p) % p

解析公式的使用

我們在上面的程式碼中定義了
long long ans = res % 1000
需要注意,此時的res其實是for迴圈執行完畢得到的結果res
變形一下
long long ans = ( res * 1 ) % 1000
再利用取模性質
long long ans = ( res % 1000 * 1 % 1000 ) % 1000
而1 % 1000 = 1,所以
long long ans = ( res % 1000 ) % 1000
比較①和②,會發現多取一次模對於結果沒有影響,那麼為何不提前取模呢??(減少資料運算的次數,降低資料的範圍,這是提前取模的兩個優點)
下面給出杭電OJ練習的AC程式碼,其中為防止底數和指數大於 int ,故將所有的資料都改為了long long,並且加入了迴圈輸入以測試多組資料

#include <iostream>
#include <cstdio>

using namespace std;

int main()
{
    long long base,power;
    while (cin >> base >> power) //多組資料
    {
        if (base == 0 && power == 0) break;
        long long res = 1;
    
        //執行power次迴圈,每次結果都乘一個base,即base的power次方
        for (long long i = 1;i <= power;i++)
        {
            res = res * base;
            res = res % 1000;
        }
        long long ans = res % 1000;
        cout << ans << endl;
    }
    return 0;
}

下面是大數取模的另一道練習,需要用到另一個公式,有興趣和時間的讀者可以see see
練習
到此大數取模告一段落。

那麼,如果繼續思考。雖然大數取模方法可以很快求出A的B次方的後幾位數,但如果考慮演算法的時間複雜度,假設我們求2的100次方,計算機則需要執行100次迴圈,分析得這個演算法的時間複雜度為O(n)。當n不大時沒有影響,但如果我們要求2的1000000000次方,顯而易見,OJ會提示TLE(超時)

快速冪演算法可以解決時間上的問題
前面已經分析出,樸素的的求冪演算法時間複雜度之所以高,就是因為當n很大時,執行的迴圈操作次數也很大。由此得到了快速冪演算法的核心思想:指數減半,底數平方。
先舉個例子讓大家熟悉一下快速冪演算法:
如果我們要求3的10次方
3 ^ 10 = 3 * 3 * 3 * 3 * 3 * 3 * 3 * 3 * 3 * 3
可將其轉化為3的平方的5次方 (3 ^ 2 ^ 5)
即3 ^ 10 = (3 * 3) * (3 * 3) * (3 * 3) * (3 * 3) * (3 * 3)
即3 ^ 10 = (3 * 3) ^ 5
即3 ^ 10 = 9 ^ 5
此時指數由10變成了5(減半),而底數從3變成了9(平方) ,雖然結果沒變,但原本求3 ^ 10需要執行10次操作,求9 ^ 5只需要執行5次操作,當指數極大時,如把指數從10000變成了5000,一下減少了5000次的操作,極大的減少了操作的次數,故提高了效率,降低了時間複雜度
但是快速冪演算法中需要注意指數為奇數的問題
當指數是偶數時,由指數減半,底數平方,可以得到:
2 ^ 16 = 4 ^ 8 = 16 ^ 4 = 256 ^ 2 = 256 * 256 = 65536

但是,對於 9 ^ 5,5是一個奇數,5的一半是2.5,而指數不能為小數,因此不能直接執行5 / 2,故採取另一種方法表示9 ^ 5
即 9 ^ 5 =(9 ^ 4)* 9,然後對9 ^ 4繼續執行指數減半,底數平方的操作即可,直至出現一個數的0次方,便可以計算出結果

下面給出快速冪模板

//快速冪,a為底數,n為指數
long long quick_power(long a,long n)
{
    long long ans = 1;
    while(n)
    {
        if (n % 2) ans *= a;//指數為奇數
        a = a * a;//底數平方
        n /= 2;//指數減半
    }
    return ans;
}

讀者可以根據以上程式碼計算一下2的16次方加深板子的理解和記憶,當然也可將資料型別改為int

那麼,當快速冪和大數取模相結合,就可以求出超過long long範圍的數(long long 最大是2的63次方)的最後幾位
例如:

以下程式碼可以快速求出2的1000000000次方的最後三位數字

#include <iostream>

using namespace std;

//快速冪與大數取模結合
long long quick_power(long long a,long long n)
{
    long long ans = 1;
    while(n)
    {
        if (n % 2) ans = ans * a % 1000;//奇數
        a = a * a % 1000;//底數平方的最後3位數字
        n /= 2;//指數減半
    }
    return ans;
}

int main()
{
    cout << quick_power(2,1000000000) << endl;
    return 0;
}

在這裡插入圖片描述

最後,可以使用位運算來優化快速冪
power%2==1可以用更快的位運算power&1來代替
理由如下:

  • **將一個數n與1的二進位制做“與”運算,得到的就是n的二進位制的最後一位的數字。**如:
    5的二進位制為00000101
    6的二進位制為00000110
    5是奇數,則5&1=1。6是偶數,則6&1=0。因此奇偶數的判斷就可以用位運算來替換。

  • 如果一個數n為偶數,則其二進位制表示的最後一位一定是0,且該偶數n&1=0

  • 如果一個數n是奇數,則其二進位制表示的最後一位一定是1,且該奇數n&1=1

  • 總結:

  • 奇數&1=1

  • 偶數&1=0

最後,總結出大數取模 + 快速冪 + 位運算的最終優化程式碼

#include <iostream>

using namespace std;

long long quick_power(long long a,long long n)
{
    long long ans = 1;
    while(n)
    {
        if (n & 1) ans = ans * a % 1000;//奇數
        a = a * a % 1000;
        n >>= 1;//n >> 1可將原數減半,n >>= 1等價於n = n / 2
    }
    return ans;
}
int main()
{
    cout << quick_power(2,1000000000) << endl;
    return 0;
}

每次看見別人可以寫下優秀的部落格,都很羨慕,然後開始嫌棄自己又笨又懶。今天偶然又看見大佬分享的一篇部落格,所以決定參考他的部落格寫一篇很詳細的關於快速冪和大數取模的知識點。下面給出參考部落格