倍增算法


啥是倍增思想?

倍增,每次將范圍擴大或減少一倍而達到加速的效果

舉個栗子,你想要跳到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 相加的形式

覺得倍增很像是二分的逆過程(自底向上的二分),二分是對一個很大的區間折半的查找看當前是否成立,是用一個值去對應區間,利用值不停的縮小區間,而倍增是利用二進制的思想用區間去對應一個值,一個區間一個區間的減少去逼近一個值,雖然沒有什么卵用,但是感覺很神奇,你品,你細品

關於倍增思想的總結

兔子跳:很多時候,倍增思想的使用過程就像兔子跳躍的過程一樣(設兔子單次跳躍的距離為d,假設在每次跳躍完成后,d=d^2).學習這只兔子的跳躍過程,我們就可以嘗試把極高的時間復雜度優化成O(n{log_{2}}^{n})

對數據查詢的優化:倍增很多時候是在查詢過程中使用的(單次查詢的時間復雜度通常為O(1)至 O({log_{2}}^{n}).在查詢速度優化的同時,往往也需要對數據的預處理.O(n{log_{2}}^{n})

圖論:在大多數情況下,圖論中的點或邊都可以按某種方式排序.如果問題的解要求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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM