啥是倍增思想?
倍增,每次將范圍擴大或減少一倍而達到加速的效果
舉個栗子,你想要跳到15米遠的地方,你怎么找到這個15這個地方,一步一步跳嗎,利用倍增的話
預設一個k使2^k>15值 ,這里我們假設k=5,
2^5=32 >15 k--; k=4; 跳過了,不跳
2^4=16 >15 k--; k=3; 跳過了,不跳
2^3=8 <=15 n=15-8=7; k--; k=2; 沒跳到,可以跳
2^2=4 <= 7 n=7-4=3 ; k-- ; k=1;沒跳到,可以跳
2^1=2 <= 3 n=3-2=1 ; k-- ; k=0;沒跳到,可以跳
2^0=1 <= 1 n=1-1=0; 跳到了,停
這樣我們只跳了4次,與朴素的15次相比,優化了11次!而且隨着數據越來越大,這就是O(n) 和 O(logn)的區別了
因為每一個數都可以由二進制來表示,所以每一個距離我們都可以把他拆分成幾個 2^k 相加的形式
覺得倍增很像是二分的逆過程(自底向上的二分),二分是對一個很大的區間折半的查找看當前是否成立,是用一個值去對應區間,利用值不停的縮小區間,而倍增是利用二進制的思想用區間去對應一個值,一個區間一個區間的減少去逼近一個值,雖然沒有什么卵用,但是感覺很神奇,你品,你細品
關於倍增思想的總結
兔子跳:很多時候,倍增思想的使用過程就像兔子跳躍的過程一樣(設兔子單次跳躍的距離為,假設在每次跳躍完成后,
).學習這只兔子的跳躍過程,我們就可以嘗試把極高的時間復雜度優化成
對數據查詢的優化:倍增很多時候是在查詢過程中使用的(單次查詢的時間復雜度通常為O(1)至 .在查詢速度優化的同時,往往也需要對數據的預處理.
圖論:在大多數情況下,圖論中的點或邊都可以按某種方式排序.如果問題的解要求O(NlogN)的時間復雜度,可以考慮倍增思想.在使用倍增思想的情況下,可以結合DP的"最優子結構"和ST表的思想嘗試求解.
二進制:在許多情況下,倍增的具體實現與二進制運算有關.在使用倍增思想的過程中,往往可以結合二進制進行考慮.比如:嘗試使用位運算優化狀態轉移.(如:(1<<i)在ST表和倍增求LCA中的應用和(i=i>>1,i&1)在快速冪中的應用)
加速DP狀態轉移:有時候可以使用倍增的方法優化DP(如:快速冪),使用這種方法可以參考快速冪的實現.
倍增與LCA
LCA最近公共祖先,相當於在樹上找最短路,因為找到了最近公共祖先就相當於找到了最短路
找到u和v第一個不同祖先不同的位置,然后這個位置向上走一步就是最近公共的祖先
但是想找到u,v第一個不同祖先的位置,就要保證u,v在同一深度(才能一起往上移動)
所以這個過程分為三部分,預處理找到每個節點深度,把較深的一點移動到較淺一點的高度,兩個一起往上移動直到他們的父親相同
可以先用一個dfs找到所有節點的深度,用deep數組存下
vector<int>ve[100009]; void dfs(int u) { for(int i=0;i<ve[u].size();i++) { int v=ve[u][i]; if(vis[v]==0) { vis[v]=1;///標記 deep[v]=deep[u]+1; dfs(v); } } }
找到了深度怎么利用倍增往上移動呢
我們定義father[i][j]為i這個位置往上移動2^j次的祖先是誰
因為i移動2^j次就相當於從i移動2^(j-1)次后再移動2^(j-1)次 找到狀態轉移方程 father [ i ] [ j ] = father [ father [ i ] [ j -1] ] [ j-1 ] ;
然后利用dp做一個預處理
void st(int n)///倍增預處理i 節點上面1<<j步的節點 { for(int j=1; (1<<j)<=n; j++) for(int i=1; i<=n; i++) father[i][j]=father[father[i][j-1]][j-1]; }
輸入邊的時候就把father[i][0]初始化好,father[i][0]表示i節點上面2的0次方的祖先
father[1][0]=0表示沒有祖先,這個1節點就是根
father[2][0]=1表示2號節點祖先是1
以此內推father[4][0]=2 father[5][0]=2 father[3][0]=1
處理father[4][1]的時候就可以通過 father[ father[4][0]] [ 0 ]得到
father[4][0]=2 所以 father[4][1]=father[2][0]就意味着 i的上面2個節點可以通過i的上面1個節點的再跳1個節點得到 同理4個節點可以跳2次2個節點得到.
int LCA(int u,int v) { if(deep[u]<deep[v])///默認u的深度大 swap(u,v); int h=deep[u]-deep[v];///求出高度差 for(int i=0; i<20; i++) ///這個操作可以將u節點向上提升任意高度 { if(h&(1<<i))///二進制位上存在1就提升 1<<i步 { u=father[u][i]; } } if(u==v)///如果u==v表示 u就是v下面的分支節點 return u; for(int i=19; i>=0; i--) ///找到第一個不相同的節點 { if(father[u][i]!=father[v][i]) { u=father[u][i]; v=father[v][i]; } } return father[u][0];///第一個不相同的節點的上一個就是最近公共祖先 }
通過這種方法,u和v一定能夠到達這樣一種狀態——它們當前 不重合,如果再往上蹦一步,就會重合。所以再往上蹦一步得 到的就是LCA
附洛谷模板題代碼:https://www.luogu.com.cn/problem/P3379
#include<bits/stdc++.h> using namespace std; #define ll long long #define lowbit(a) ((a) & -(a)) #define clean(a, b) memset(a, b, sizeof(a)) const int mod = 1e9 + 7; const int inf = 0x3f3f3f3f; const int maxn = 5e5 + 9; const int Maxn = 29; int deep[maxn],father[maxn][Maxn]; vector<int>ve[maxn]; int vis[maxn]; void dfs(int now) { int len=ve[now].size(); vis[now]=1; for(int i=0;i<len;i++) { int next=ve[now][i]; if(!vis[next]) { deep[next]=deep[now]+1; father[next][0]=now; dfs(next); } } } void st(int n)///倍增預處理i 節點上面1<<j步的節點 { for(int j=1; (1<<j)<=n; j++) for(int i=1; i<=n; i++) father[i][j]=father[father[i][j-1]][j-1]; } int LCA(int u,int v) { if(deep[u]<deep[v])///默認u的深度大 swap(u,v); int h=deep[u]-deep[v];///求出高度差 for(int i=0; i<20; i++) ///這個操作可以將u節點向上提升任意高度 { if(h&(1<<i))///二進制位上存在1就提升 1<<i步 { u=father[u][i]; } } if(u==v)///如果u==v表示 u就是v下面的分支節點 return u; for(int i=19; i>=0; i--) ///找到第一個不相同的節點 { if(father[u][i]!=father[v][i]) { u=father[u][i]; v=father[v][i]; } } return father[u][0];///第一個不相同的節點的上一個就是最近公共祖先 } int main() { int n,m,s,u,v,a,b; scanf("%d%d%d",&n,&m,&s); for(int i=1;i<n;i++) { scanf("%d%d",&u,&v); ve[u].push_back(v); ve[v].push_back(u); } dfs(s); st(n); while(m--) { scanf("%d%d",&a,&b); printf("%d\n",LCA(a,b)); } return 0; }
倍增與RMQ
RMQ(Range Minimum/Maximum Query),即區間最值查詢,是指這樣一個問題:對於長度為n的數列A,回答若干詢問RMQ(A,i,j)(i,j<=n),返回數列A中下標在i,j之間的最小/大值
這樣的問題我們可以用線段樹來解決,但代碼比較冗長,用ST算法也可以解決這個問題,該算法一般用較長的時間做預處理,待信息充足以后便可以用較少的時間回答每個查詢
ST(Sparse Table)算法是一個非常有名的在線處理RMQ問題的算法,它可以在O(nlogn)時間內進行預處理,然后在O(1)時間內回答每個查詢
預處理
我們定義f[i, j]表示從第i個數起連續2^j個數中的最大值
f[i, j]表示從第i個數起連續2^j個數中的最大值
f[i][j]表示i開始的連續2j 個點的最大值。
則f[i][0]表示i開始連續1個點的最大值即a[i];
f[i][1]表示i開始連續2個點的最大值即a[i]和a[i+1]的最大值;
f[i][2]表示i開始連續4個點的最大值即a[i]~a[i+3]中的最大值;
f[i][3]表示i開始連續8個點的最大值即a[i]~a[i+7]中的最大值;
......
f[i][logn]開始連續n個點的最大值即 a[i]~a[i+n-1];(i+n-1<=n)
我們把f[i,j]平均分成兩段(因為f[i,j]一定是偶數個數字),從 i 到i + 2 ^ (j - 1) - 1為一段,i + 2 ^ (j - 1)到i + 2 ^ j - 1為一段(長度都為2 ^ (j - 1))
f[i,j]就是這兩段各自最大值中的最大值
狀態轉移方程f[i, j]=max(f[i,j-1], f[i + 2^(j-1),j-1])
void ST(int n)///預處理 { for(int i=1;i<=n;i++) f[i][0]=a[i];///初始化 for(int j=1;(1<<j)<=n;j++)///枚舉區間長度 { for(int i=1;i+(1<<j)-1<=n;i++)///枚舉起點 f[i][j]=max(f[i][j-1],f[i+(1<<(j-1))][j-1]); } }
查詢
假如我們需要查詢的區間為(i,j),那么我們需要找到覆蓋這個閉區間(左邊界取i,右邊界取j)的最小冪 (區間重復並不影響答案,因為我們要找的是最大或最小值)
因為這個區間的長度為j - i + 1,所以我們可以取k=log2( j - i + 1),則有:RMQ(A, i, j)=max{f[i , k], f[ j - 2 ^ k + 1, k]}
int RMQ(int l,int r) { int k=floor(log2( r-l+1 ));///向下取整 printf("k=%d\n",k); return max(f[l][k],f[r-(1<<k)+1][k]); }
整理自用,如有問題請盡快聯系我 QQ:1661027159