c++倍增

一、ST表

倍增

1.预处理

f [ i ] [ j ] f[i][j] f[i][j]表示从i开始的长度为2j的区间 ( ( 即区间 [ i , i + 2 j − 1 ] ) [i,i+2j−1]) [i,i+2j1]

递推公式 ( j (j j在外层递增 ): ): ):

f [ i ] [ j ] = m a x f [ i ] [ j − 1 ] , f [ i + 2 j − 1 ] [ j − 1 ] f[i][j]=max{f[i][j−1],f[i+2j−1][j−1]} f[i][j]=maxf[i][j1],f[i+2j1][j1]

即将区间 [ l , r ] [l,r] [l,r]分为两个区间合并

2.查询

分为两段,第一段为区间 [ l , 2 k ] [l,2k] [l,2k],第二段为区间 [ r − 2 k + 1 , r ] [r−2k+1,r] [r2k+1,r],其中 k k k为满足 2 k ≤ r − l + 1 2k≤r−l+1 2krl+1的所有数中最大的那个数

即区间 [ l , r ] [l,r] [l,r]的最大值为 m a x f [ l ] [ k ] , f [ r − 2 k + 1 ] [ k ] max{f[l][k],f[r−2k+1][k]} maxf[l][k],f[r2k+1][k]

3.例子

忠诚 洛谷 P1816

多次查询区间最小值

#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
int m,n;
#define MAXN 100001
#define LOG 18
int st[MAXN][LOG];
int lg2[MAXN];
int main(){
    scanf("%d %d", &n, &m);
    memset(st, 0x3f, sizeof st);
    for(int i=1;i<=n;++i) scanf("%d", &st[i][0]);
    for(int len=1;len<LOG;++len)
        for(int i=1;i+(1<<len)-1<=n;++i)
            st[i][len]=min(st[i][len-1], st[i+(1<<(len-1))][len-1]);
    for(int i=2;i<=n;++i) lg2[i]=lg2[i>>1]+1;
    while(m--){
        int l,r;scanf("%d %d", &l, &r);
        int sz=lg2[r-l+1];
        printf("%d ", min(st[l][sz], st[r+1-(1<<sz)][sz]));
    }
    return 0;
}


二、树上倍增

1.求解LCA

(1) 预处理

首先结合dfs预处理出 f [ i ] [ j ] , f [ i ] [ j ] f[i][j],f[i][j] f[i][j]f[i][j]表示节点i向上跳2j层的节点

递推公式:

f [ i ] [ j ] = f [ f [ i ] [ j − 1 ] ] [ j − 1 ] f[i][j]=f[f[i][j−1]][j−1] f[i][j]=f[f[i][j1]][j1]

即节点i分两次向上跳,每次跳 2 j − 1 2^{j−1} 2j1层跳到的节点就是节点 i i i向上跳 2 j 2^j 2j层的节点 ( 2 j − 1 × 2 = 2 j ) (2^{j−1}×2=2^j) 2j1×2=2j

void load(int x, int fa){
    f[x][0]=fa;
    dep[x]=dep[fa]+1;
    for(int i=1;i<20;++i)
        f[x][i]=f[f[x][i-1]][i-1];
}
void dfs(int u, int fa){
    load(u, fa);
    for(int i=head[u];i;i=nxt[i]){
        int v=vv[i];
        if(v==fa) continue;
        dfs(v, u);
    }
}

同时也可以像下面预处理出 l o g 2 n log^n_2 log2n的所有值以优化常数

for(int i=2;i<=tot;++i)
    lg2[i]=lg2[i>>1]+1; // 预处理log2n
(2) 查询

首先使两个查询节点跳至同一高度后(因为它们的最近公共祖先不可能低于这两点,跳跃方法同下),当前层记为 x x x,然后从 l o g 2 x log^x_2 log2x 0 0 0枚举(递减能保证可以完全分解成二进制) j j j,如果上跳 2 j 2^j 2j层后不重合,那么就继续跳,重合则不跳,使两点层数一直逼近最近公共祖先,最后跳完 2 0 2^0 20层后,两点必定会停在最近公共祖先的下一层,所以最后直接取当前层 i i i f [ i ] [ 0 ] f[i][0] f[i][0]就好了。

其中,可以直接从最大可能的 i i i开始枚举,因为反正 i i i也很小。

inline int lca(int a, int b){
    if(dep[a]<dep[b]) swap(a, b);
    for(int i=20;i>=0;--i)
        if(dep[f[a][i]]>=dep[b])
            a=f[a][i];
    if(a==b) return a;
    for(int i=20;i>=0;--i)
        if(f[a][i]!=f[b][i])
            a=f[a][i], b=f[b][i];
    return f[a][0];
}

这是一种在线求LCA的算法,其实还有Tarjan这种效率高的离线算法。

2. O ( 1 ) O(1) O(1)求LCA

另外还有一种 n l o g n nlogn nlogn预处理,每次 O ( 1 ) O(1) O(1)查询的方法。

即用欧拉序 + R M Q +RMQ +RMQ可实现 O ( 1 ) O(1) O(1)求得 L C A LCA LCA。先 d f s dfs dfs全树,记录欧拉序(就是记下 d f s dfs dfs走过的节点),然后在欧拉序上以节点深度作为权值建 S T ST ST表,每次查欧拉序上深度最小的点即为 L C A LCA LCA。此时将问题转换为区间求最小值 R M Q RMQ RMQ问题。

(1)预处理
void dfs(int u, int fa){
    dfn[u]=++tot;
    f[tot][0]=u;
    dep[u]=dep[fa]+1;
    for(int i=head[u];i;i=nxt[i]){
        int v=vv[i];
        if(v==fa) continue;
        dfs(v, u);
        f[++tot][0]=u; // 再记录
    }
}
for(int i=1;i<20;++i)
    for(int j=1;j+(1<<i)-1<=tot;++j)
        if(dep[f[j][i-1]]<dep[f[j+(1<<(i-1))][i-1]])
            f[j][i]=f[j][i-1];
		else
            f[j][i]=f[j+(1<<(i-1))][i-1];
for(int i=2;i<=tot;++i)
    lg2[i]=lg2[i>>1]+1; // 预处理log2n
(2)查询
int lca(int a, int b){
    int l=dfn[a],r=dfn[b];
    if(l>r) swap(l,r);
    int lg=lg2[r-l+1];
    if(dep[f[l][lg]]<dep[f[r-(1<<lg)+1][lg]])
        return f[l][lg];
    else return f[r-(1<<lg)+1][lg];
}

3.求路径上最值

求树上两点 a , b a,b a,b路径上的最小权值

我们再设一个 g [ i ] [ j ] g[i][j] g[i][j]表示节点 i i i向上跳 2 j 2^j 2j内所经过的最小权值即可,转移方程:

g [ i ] [ j ] = m i n ( g [ i ] [ j − 1 ] , g [ f [ i ] [ j − 1 ] ] [ j − 1 ] ) g[i][j]=min(g[i][j−1],g[f[i][j−1]][j−1]) g[i][j]=min(g[i][j1],g[f[i][j1]][j1])

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值