【黑馬計劃-2】倍增算法

線性倍增(RMQ)

空間換時間

設狀態 minn[i][j] 表示區間 [i,i+2j1] 的最小值。
如下圖,易得 minn[i][j]=min(minn[i][j1],minn[i+2j1][j1])
由圖可得 $minn$ 的狀態轉移方程
注意邊界爲 minn[i][0]=a[i]a 爲原數組。
這樣,若我們要查詢區間 [i,i+2k1] 的最小值,所需時間只需 O(1)
i+2j1n ,故 j 的上界爲 logn ,該數組佔用的空間爲 O(nlogn) (有額外開銷),這即是傳統意義上的“空間換時間”。

如何求值

給定區間 [l,r] ,用上文的方法我們可以求出區間 [l,l+2j][r2j+1,r] 的最小值。
那麼要求 [l,r] 的最小值,只需讓 [l,l+2j][r2j+1,r] 的併爲 [l,r] 即可。
列方程

l+2jr2j+11

解得

jlog(rl)1

(注意這裏的 log 不是整數)取整時,爲了保險,一般取 jlog(rl)

複雜度

查詢區間最值,如上文所說,單次查詢可以在 O(1) 的時間內出解。
空間複雜度爲 O(nlogn)

代碼實現
#include<iostream>
#include<cstdio>
#include<cmath>

using namespace std;

int N,Q,a[200001],l,r;

namespace RMQ{
    int minn[200001][18],maxn[200001][18];
    void build_ST(int *a,int n){
        for(int i=1;i<=n;++i)minn[i][0]=maxn[i][0]=a[i];
        int lg=(int)log2(n);
        for(int j=1;j<=lg;++j)for(int i=1;i+(1<<j)-1<=n;++i){
            minn[i][j]=min(minn[i][j-1],minn[i+(1<<j-1)][j-1]);
            maxn[i][j]=max(maxn[i][j-1],maxn[i+(1<<j-1)][j-1]);
        }
    }
    int query_min(int l,int r){
        int lg=(int)log2(r-l+1);
        return min(minn[l][lg],minn[r-(1<<lg)+1][lg]);
    }
    int query_max(int l,int r){
        int lg=(int)log2(r-l+1)-1;
        return max(maxn[l][lg],maxn[r-(1<<lg)+1][lg]);
    }
}

int main(){
    scanf("%d%d",&N,&Q);
    for(int i=1;i<=N;++i)scanf("%d",a+i);
    RMQ::build_ST(a,N);
    for(int i=1;i<=Q;++i){
        scanf("%d%d",&l,&r);
        printf("Minimum of interval [%d,%d]: %d\n",l,r,RMQ::query_min(l,r));
        printf("Maximum of interval [%d,%d]: %d\n",l,r,RMQ::query_max(l,r));
    }
}

樹上倍增

樹上倍增的應用非常廣泛,可以在靜態樹中維護各種信息,並可以在 O(logn) 時間內查詢。

基本思想

實質上與RMQ的查詢非常類似,這裏以查詢兩點的LCA舉例。
若用 fa[u][i] 表示 u 的第 2i 個祖先,那麼易得遞推式

fa[u][i]=fa[fa[u][i1]][i1]

在dfs時預處理即可。

進軍LCA

假設對於點對 (u,v)dep[u]dep[v] ,它們的LCA爲 a
uv 深度相同,考慮 a 的祖先的性質。由於 lca(u,v)=a ,那麼顯然 a 的所有祖先都是 uv 的公共祖先,反之亦然。因此若 fa[u][i]=fa[v][i] ,那麼 fa[u][i] 要麼等於 a 要麼等於 a 的祖先。故這種情況下,可以考慮枚舉所有 i ,若 fa[u][i]=fa[v][i] ,則將 uv 更新爲 fa[u][i]fa[v][i] 。假設 fa[u][i]=fa[v][i] ,那麼對於 j>ifa[u][j]=fa[v][j] ,故要從高位(即 log2hh 爲樹高)往低位枚舉 i ,每次找到合法的 i 就更新 uv
對於深度不同的情況,其實非常簡單,前面假設了 dep[u]dep[v] ,又因爲它們深度不同,那麼 dep[u]<dep[v] ,這時若將 v 更改爲 v 的第 dep[v]dep[u] 個祖先, a 顯然不變,於是將 dep[v]dep[u] 拆爲二進制,用 fa 數組將 v 的深度置爲與 u 相同。

擴展:樹上最短路徑最大/最小點權/邊權

模仿求LCA,設 minn[u][i]ufa[u][i] 路徑上的最小邊權,那麼有狀態轉移方程:

minn[u][i]=min(minn[u][i],minn[fa[u][i1]][i1])

邊界: minn[u][0] 的值爲 ufa[u][0] 的邊權。
其餘均可同理實現。

代碼實現
#include<iostream>
#include<cstdio>
#include<vector>
#include<cmath>

using namespace std;

const int SIZE=500000,LOG2_SIZE=20;

struct tree{

    vector<int> point[SIZE+1];
    int fa[SIZE+1][LOG2_SIZE],dep[SIZE+1],log_n;

    void dfs_dp(int u){
        for(int i=1;i<=log_n;++i)
            if(fa[u][i-1])
                fa[u][i]=fa[fa[u][i-1]][i-1];
            else
                break;
        for(int v:point[u])
            if(v!=fa[u][0]){
                fa[v][0]=u;
                dep[v]=dep[u]+1;
                dfs_dp(v);
            }
    }

    void jump(int &v,int d){
        int log_d=(int)log2(d);
        for(int i=0;i<=log_d;++i)
            if(d&(1<<i))
                v=fa[v][i];
    }

    int query(int u,int v){
        if(dep[u]>dep[v])
            swap(u,v);
        jump(v,dep[v]-dep[u]);
        if(u==v)
            return u;
        for(int i=log_n;~i;--i)
            if(fa[u][i]!=fa[v][i]){
                u=fa[u][i];
                v=fa[v][i];
            }
        return fa[u][0];
    }

};

tree T;
int N,Q,R,u,v;

int main(){
    scanf("%d%d%d",&N,&Q,&R);
    for(int i=1;i<N;++i){
        scanf("%d%d",&u,&v);
        T.point[u].push_back(v);
        T.point[v].push_back(u);
    }
    T.log_n=(int)log2(N);
    T.dfs_dp(R);
    for(int i=1;i<=Q;++i){
        scanf("%d%d",&u,&v);
        printf("The lowest common ancestor of %d and %d is: %d\n",T.query(u,v));
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章