演算法競賽進階指南 - 位運算3題詳解
演算法進階指南看了開頭一部分,個人感覺講解的比較透徹,於是打算寫一些個人的讀書筆記,主要是做題後做一個總結,不求快,但求能一點點講清楚每個知識點。這一節來看看第一章的位運算部分。
演算法進階指南的的題目都在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 3∗3∗3∗3∗3∗3∗3∗3∗3∗3∗3∗3∗3
對於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)=323∗322∗320
如果從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} 1≤a,b,p≤1018
這裡直接相乘顯然會超過64位,還是快速冪,上面我們對快速冪有了大概的理解,再來看下這題的變化。
舉個例子,對於 3 ∗ 13 3*13 3∗13用快速冪怎麼做呢?
我們還是對b進行二進位制處理,那麼:
3 ⋅ 13 = 3 ⋅ 1101 = 3 ⋅ ( 2 3 + 2 2 + 2 0 ) 3\cdot13 = 3\cdot1101 = 3\cdot(2^3+2^2+2^0) 3⋅13=3⋅1101=3⋅(23+22+20)
也就是
3 ⋅ 2 3 + 3 ⋅ 2 2 + 3 ⋅ 2 0 3\cdot2^3 + 3\cdot2^2+3\cdot2^0 3⋅23+3⋅22+3⋅20
從最右邊開始,每一項都是前一項的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[(1≪n)−1][n−1],比如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 n−1, 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−(1≪j)還可以使用i ^ (1<<j)來寫,異或操作是啥,異或就是不同的就是1,所以稱為異,這裡i中j位為1,異或肯定為0,所以效果是一樣的,不過前者可能更好理解一點,直接減去也是0,後者可能效能更好。(前者是yxc的視訊中的寫法,後者是李煜東的演算法書上的寫法)
然後就是f的空間佔用比較大,放在全域性裡面用堆記憶體進行分配,切記~
總結
以上3題就是演算法進階指南里面的入門題目啦,新手學習的時候多思考,因為快速冪和狀態壓縮的理解都需要一點時間。快速冪能夠應用在指數運算和乘法運算中,其實就是對二進位制進行分解處理,每一項都是一次迭代的結果,從最小的項開始運算,每次運算都依賴上次的運算,最終只需要log(n)複雜度完成運算。
狀態壓縮後面在動態規劃的習題中再著重講解,本題也是一個很典型的題目,關鍵是確定DP方程的影響因素,題目3中主要是當前點的狀態和當前位置2個因素可以涵蓋所有情況,狀態轉移需要進行位運算,主要是對某位進行賦值和取值操作,多練習就能夠熟悉使用了。
如果還有不懂的,可以留言給我再講解,也可以參考《演算法競賽進階指南》,還可以參考AcWing的yxc的視訊教程,相對來說視訊教程更容易理解。
原創不易,如果看了這篇能夠提高你對位運算的理解,請點個贊哦。
微信公眾號:ACM演算法日常
專注於基礎演算法的研究工作,深入解析ACM演算法題,五分鐘閱讀,輕鬆理解每一行原始碼。內容涉及演算法、C/C++、軟體設計等。
相關文章
- 《演算法競賽進階指南》藍書重做記錄演算法
- 位運算進階
- 【演算法技巧】位運算裝逼指南演算法
- 詳解前端進階指南教程前端
- 【演算法技巧】位運算裝逼指南 ---- 帶你領略位運算的魅力演算法
- 二進位制、位運算、位移運算
- 某演算法競賽題——把一個二進位制的串轉換為十進位制整數演算法
- puppet進階指南——service資源詳解
- CTF競賽進階 (一) 密碼學密碼學
- 使用位運算進行加法運算
- java二進位制運算十進位制(精確運算)Java
- 一道位運算的演算法題演算法
- 【演算法】位運算技巧演算法
- 【每日演算法】位運算演算法
- 演算法之位運算演算法
- 【C進階】16、位運算子
- 二進位制與二進位制運算
- 如何巧妙著運用「位運算」來解決問題?
- 演算法之美 : 位運算演算法
- 關於Java中進位制轉換以及位運算問題Java
- 【技巧總結】位運算裝逼指南
- 演算法競賽入門經典訓練指南 pdf演算法
- 瞭解面試演算法之 - 棧&佇列&位運算面試演算法佇列
- BOT大賽計算機視覺賽題經驗分享:賽題詳解與思路分析計算機視覺
- 位運算的操作與演算法演算法
- 競賽選手問題的解答演算法演算法
- 有趣的二進位制2—高效位運算
- 位運算解決多標籤問題【原創】
- 詳解 SQL 集合運算SQL
- (位運算)兩個字串的位運算字串
- 機器學習 - 競賽網站,演算法刷題網站機器學習網站演算法
- 前端進階課程之跨域問題詳解前端跨域
- 位運算
- 演算法競賽日誌演算法
- 演算法競賽小技巧演算法
- 倍增與ST演算法 --演算法競賽專題解析(28)演算法
- 進位制詳解:二進位制、八進位制和十六進位制
- 北美競賽-加拿大計算機競賽CCC-收穫滑鐵盧計算機