演算法競賽進階指南 - 位運算3題詳解

ACM演算法日常發表於2020-12-19

演算法進階指南看了開頭一部分,個人感覺講解的比較透徹,於是打算寫一些個人的讀書筆記,主要是做題後做一個總結,不求快,但求能一點點講清楚每個知識點。這一節來看看第一章的位運算部分。

演算法進階指南的的題目都在AcWing上面,這裡就按照AcWing上的題號來寫題目編號。

題目89、求 a 的 b 次方對 p 取模的值。

題意:

如題,就是求 a b % p a^b\%p ab%p

這道題也是快速冪模板,作為書中的第一道例題,有必要重新看一下快速冪的原理。

比如求 3 13 % 100 3^{13}\%100 313%100,這裡a是3,b是13,p是100。

如果我們直接用乘法,那麼就是

3 ∗ 3 ∗ 3 ∗ 3 ∗ 3 ∗ 3 ∗ 3 ∗ 3 ∗ 3 ∗ 3 ∗ 3 ∗ 3 ∗ 3 3*3*3*3*3*3*3*3*3*3*3*3*3 3333333333333

對於b非常大(比如10的9次方)的情況,效率會很低。快速冪能很好的解決這個問題。

快速冪可以理解為二進位制冪,將b轉換為二進位制格式,比如13的二進位制就是8+4+1 => 1000 ^ 0100 ^ 0001 => 1101

也就是:

3 13 = 3 1101 = 3 ( 2 3 + 2 2 + 2 0 ) = 3 2 3 ∗ 3 2 2 ∗ 3 2 0 3^{13} = 3^{1101} = 3^{(2^3+2^2+2^0)} = 3^{2^3}*3^{2^2}*3^{2^0} 313=31101=3(23+22+20)=323322320

如果從b的二進位制末位開始遍歷i(i從0開始)到最左邊,凡是遇到1,那麼就會將 3 2 i 3^{2^i} 32i作為一項乘到結果中。

從乘式右邊開始,每一項都是前一項的平方。

C++程式碼如下:

#include <iostream>
using namespace std;

int main()
{
    long long a, b, p;
    scanf("%ld%ld%ld", &a, &b, &p);

    long long res = 1;

    while (b)
    {
        // b二進位制中的1,表示這次可以將當前a的迭代值作為一項乘到res中
        if (b & 1)
            res = res * a % p;
        // b每次都會右移一位,每次我們都是處理最末位的二進位制值
        b >>= 1; //b右移了一位後,a也需要更新

        // 迭代a,a在這裡其實是每次的迭代值,等於a^{2^i},其中i從0開始
        a = a * a % p;
    }

    printf("%ld\n", res % p);
    return 0;
}

題目90、64位整數乘法

題意:

求 a 乘 b 對 p 取模的值。

資料範圍:

1 ≤ a , b , p ≤ 1 0 18 1≤a,b,p≤10^{18} 1a,b,p1018

這裡直接相乘顯然會超過64位,還是快速冪,上面我們對快速冪有了大概的理解,再來看下這題的變化。

舉個例子,對於 3 ∗ 13 3*13 313用快速冪怎麼做呢?

我們還是對b進行二進位制處理,那麼:

3 ⋅ 13 = 3 ⋅ 1101 = 3 ⋅ ( 2 3 + 2 2 + 2 0 ) 3\cdot13 = 3\cdot1101 = 3\cdot(2^3+2^2+2^0) 313=31101=3(23+22+20)

也就是

3 ⋅ 2 3 + 3 ⋅ 2 2 + 3 ⋅ 2 0 3\cdot2^3 + 3\cdot2^2+3\cdot2^0 323+322+320

從最右邊開始,每一項都是前一項的2倍,參考題目89的程式碼,

程式碼:

#include <iostream>
#include <cstdio>
#define ll long long
using namespace std;

int main()
{
    ll a, b, p, res;
    cin >> a >> b >> p;
    res = 0;

    while (b)
    {
        // 只有位為1的部分加入到res中
        if (b & 1)
            res = (res + a) % p;
        
        // b右移一位
        b >>= 1;

        // 每一項都是前一項的2倍
        a = 2 * a % p;
    }
    
    cout << res << endl;
    return 0;
}

題目3、最短Hamilton路徑

題意:

給定一張 n 個點的帶權無向圖,點從 0~n-1 標號,求起點 0 到終點 n-1 的最短Hamilton路徑。 Hamilton路徑的定義是從 0 到 n-1 不重不漏地經過每個點恰好一次。

解題思路:

這道題暴力解法就是搜尋,看最終哪條路徑的總值最小,但是複雜度非常高,肯定會超時。

這道題不再是像上面2題可以用快速冪解決,而是需要動態規劃來解決,採用位的方式儲存狀態。

動態規劃思路:首先需要思考狀態方程,在任意時刻,如何表示哪些點走過,哪些點沒走過呢?顯然可以用一個n位的二進位制數。若第i位位1,就表示第i個點走過了。

在任意時刻,還需要知道當前在那個點,所以可以用 f [ i , j ] f[i, j] f[i,j]來表示狀態, f [ i ] [ j ] f[i][j] f[i][j]中的i表示點經過的狀態,j表示到了那個點。

目標值是 f [ ( 1 ≪ n ) − 1 ] [ n − 1 ] f[(1\ll n)-1][n-1] f[(1n)1][n1],比如n為3,那麼(1<<n)-1恰好為7(二進位制為111),表示所有點都經過了。

那麼狀態方程呢?

在任意時刻,有公式 f [ s t a t e j ] [ j ] = f [ s t a t e k ] [ k ] + w e i g h t [ k ] [ j ] f[state_j][j] =f[state_k][k] + weight[k][j] f[statej][j]=f[statek][k]+weight[k][j]

這裡 s t a t e k = s t a t e j state_k = state_j statek=statej除掉 j j j之後的狀態

注意這裡 k k k可以是0到 n − 1 n-1 n1 f [ s t a t e j ] [ j ] f[state_j][j] f[statej][j]會遍歷k取最小值。

來看程式碼:

#include <iostream>
#include <algorithm>
#include <string.h>
using namespace std;

const int N = 20, M = 1<<20;

// 這裡f佔用空間非常大,不能開在main函式中,全域性變數是在堆中開空間
int f[M][N], weight[N][N];

int main()
{
    int n;
    cin>>n;
    
    for(int i=0; i<n; i++){
        for(int j=0; j<n; j++){
            cin>>weight[i][j];
        }
    }
    
    // 初始化最大值
    memset(f, 0x3f, sizeof(f));
    // i為00...01表示在經過位置0
    // j為0表示目前處於位置0
    f[1][0] = 0;
    
    for(int i=0; i<1<<n; i++){
        for(int j=0; j<n; j++){
            if(i>>j & 1){
                for(int k=0; k<n; k++){
                    // i-(1<<j)作用是去掉j位的1
                    // i-(1<<j) >> k & 1在位置k為1(經過k)
                    if(i-(1<<j) >> k & 1){
                        f[i][j] = min(f[i][j], f[i-(1<<j)][k]+weight[k][j]);
                    }
                }
            }
        }
    }

    cout<<f[(1<<n)-1][n-1]<<endl;
    return 0;
}

這裡還要注意的是位的優先順序,位操作的優先順序都比較低,如果不確定可以都用括號,防止優先順序錯誤。這道題需要記住位移操作的優先順序低於加減的優先順序,需要括起來才行。

本題中 i − ( 1 ≪ j ) i-(1\ll j) i(1j)還可以使用i ^ (1<<j)來寫,異或操作是啥,異或就是不同的就是1,所以稱為異,這裡i中j位為1,異或肯定為0,所以效果是一樣的,不過前者可能更好理解一點,直接減去也是0,後者可能效能更好。(前者是yxc的視訊中的寫法,後者是李煜東的演算法書上的寫法)

然後就是f的空間佔用比較大,放在全域性裡面用堆記憶體進行分配,切記~

總結

以上3題就是演算法進階指南里面的入門題目啦,新手學習的時候多思考,因為快速冪和狀態壓縮的理解都需要一點時間。快速冪能夠應用在指數運算和乘法運算中,其實就是對二進位制進行分解處理,每一項都是一次迭代的結果,從最小的項開始運算,每次運算都依賴上次的運算,最終只需要log(n)複雜度完成運算。

狀態壓縮後面在動態規劃的習題中再著重講解,本題也是一個很典型的題目,關鍵是確定DP方程的影響因素,題目3中主要是當前點的狀態和當前位置2個因素可以涵蓋所有情況,狀態轉移需要進行位運算,主要是對某位進行賦值和取值操作,多練習就能夠熟悉使用了。

如果還有不懂的,可以留言給我再講解,也可以參考《演算法競賽進階指南》,還可以參考AcWing的yxc的視訊教程,相對來說視訊教程更容易理解。

原創不易,如果看了這篇能夠提高你對位運算的理解,請點個贊哦。

微信公眾號:ACM演算法日常

專注於基礎演算法的研究工作,深入解析ACM演算法題,五分鐘閱讀,輕鬆理解每一行原始碼。內容涉及演算法、C/C++、軟體設計等。

image

相關文章