【黑马计划-2】倍增算法

线性倍增(RMQ)

空间换时间

设状态 minn[i][j] m i n n [ i ] [ j ] 表示区间 [i,i+2j1] [ i , i + 2 j − 1 ] 的最小值。
如下图,易得 minn[i][j]=min(minn[i][j1],minn[i+2j1][j1]) m i n n [ i ] [ j ] = min ( m i n n [ i ] [ j − 1 ] , m i n n [ i + 2 j − 1 ] [ j − 1 ] )
由图可得 $minn$ 的状态转移方程
注意边界为 minn[i][0]=a[i] m i n n [ i ] [ 0 ] = a [ i ] a a 为原数组。
这样,若我们要查询区间 [i,i+2k1] 的最小值,所需时间只需 O(1) O ( 1 )
i+2j1n i + 2 j − 1 ⩽ n ,故 j j 的上界为 logn ,该数组占用的空间为 O(nlogn) O ( n log ⁡ n ) (有额外开销),这即是传统意义上的“空间换时间”。

如何求值

给定区间 [l,r] [ l , r ] ,用上文的方法我们可以求出区间 [l,l+2j] [ l , l + 2 j ] [r2j+1,r] [ r − 2 j + 1 , r ] 的最小值。
那么要求 [l,r] [ l , r ] 的最小值,只需让 [l,l+2j] [ l , l + 2 j ] [r2j+1,r] [ r − 2 j + 1 , r ] 的并为 [l,r] [ l , r ] 即可。
列不等式

l+2jr2j+11 l + 2 j ⩾ r − 2 j + 1 − 1

解得

jlog(rl)1 j ⩾ log ⁡ ( r − l ) − 1

(注意这里的 log log 不是整数)取整时,为了保险,一般取 jlog(rl) j ⩾ log ⁡ ( r − l )

复杂度

查询区间最值,如上文所说,单次查询可以在 O(1) O ( 1 ) 的时间内出解。
空间复杂度为 O(nlogn) O ( n log ⁡ n )

代码实现
#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) O ( log ⁡ n ) 时间内查询。

基本思想

实质上与RMQ的查询非常类似,这里以查询两点的LCA举例。
若用 fa[u][i] f a [ u ] [ i ] 表示 u u 的第 2i 个祖先,那么易得递推式

fa[u][i]=fa[fa[u][i1]][i1] f a [ u ] [ i ] = f a [ f a [ u ] [ i − 1 ] ] [ i − 1 ]

在dfs时预处理即可。

进军LCA

假设对于点对 (u,v) ( u , v ) dep[u]dep[v] d e p [ u ] ⩽ d e p [ v ] ,它们的LCA为 a a
u v v 深度相同,考虑 a 的祖先的性质。由于 lca(u,v)=a l c a ( u , v ) = a ,那么显然 a a 的所有祖先都是 u v v 的公共祖先,反之亦然。因此若 fa[u][i]=fa[v][i] ,那么 fa[u][i] f a [ u ] [ i ] 要么等于 a a 要么等于 a 的祖先。故这种情况下,可以考虑枚举所有 i i ,若 fa[u][i]=fa[v][i] ,则将 u u v 更新为 fa[u][i] f a [ u ] [ i ] fa[v][i] f a [ v ] [ i ] 。假设 fa[u][i]=fa[v][i] f a [ u ] [ i ] = f a [ v ] [ i ] ,那么对于 j>i j > i fa[u][j]=fa[v][j] f a [ u ] [ j ] = f a [ v ] [ j ] ,故要从高位(即 log2h l o g 2 h h h 为树高)往低位枚举 i ,每次找到合法的 i i 就更新 u v v
对于深度不同的情况,其实非常简单,前面假设了 dep[u]dep[v] ,又因为它们深度不同,那么 dep[u]<dep[v] d e p [ u ] < d e p [ v ] ,这时若将 v v 更改为 v 的第 dep[v]dep[u] d e p [ v ] − d e p [ u ] 个祖先, a a 显然不变,于是将 dep[v]dep[u] 拆为二进制,用 fa f a 数组将 v v 的深度置为与 u 相同。

扩展:树上最短路径最大/最小点权/边权

模仿求LCA,设 minn[u][i] m i n n [ u ] [ i ] u u fa[u][i] 路径上的最小边权,那么有状态转移方程:

minn[u][i]=min(minn[u][i],minn[fa[u][i1]][i1]) m i n n [ u ] [ i ] = m i n ( m i n n [ u ] [ i ] , m i n n [ f a [ u ] [ i − 1 ] ] [ i − 1 ] )

边界: minn[u][0] m i n n [ u ] [ 0 ] 的值为 u u fa[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));
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值