LCA算法

LCA

LCA=Lowest   Common   Ancestor \textbf{LCA=Lowest Common Ancestor} LCA=Lowest Common Ancestor

即最近公共祖先
下文以 LCA(a,b)表示节点a与节点b的最近公共祖先 \textbf{LCA(a,b)表示节点a与节点b的最近公共祖先} LCA(a,b)表示节点a与节点b的最近公共祖先

F1: 暴力算法

步骤:

(1)求出每个节点的深度 s i z e size size

(2)询问两个点是否重合,若重合,则 LCA(a,b)=当前重合的节点 \textbf{LCA(a,b)=当前重合的节点} LCA(a,b)=当前重合的节点

(3)否则,选择 max(size[a],size[b]) \textbf{max(size[a],size[b])} max(size[a],size[b]) 并移动到他的父亲节点

F2:倍增写法

倍增写法是基于F1暴力写法的优化版本,改进了时间复杂度

步骤:

(1)定义倍增数组 L C A [ M A X N ] [ M A X N L O G ] LCA[MAXN][MAXNLOG] LCA[MAXN][MAXNLOG],其中 L C A [ i ] [ j ] LCA[i][j] LCA[i][j] 表示 i i i 节点向上减少 2 j 2^j 2j 步可到达的节点

因为想要跳 2 j 2^j 2j 步可以先网上跳 2 j − 1 2^{j-1} 2j1 步然后再站在 2 j − 1 2^{j-1} 2j1 的地方再跳 2 j − 1 2^{j-1} 2j1

由上可得公式:
L C A [ i ] [ j ] = L C A [ L C A [ i ] [ j − 1 ] ] [ j − 1 ] LCA[i][j]=LCA[ LCA[i][j-1] ][ j-1 ] LCA[i][j]=LCA[LCA[i][j1]][j1]

2 j = 2 j − 1 + 2 j − 1 2^j = 2^{j-1} + 2^{j-1} 2j=2j1+2j1
(2)把 a , b a,b a,b 移动到同一深度,即 size[a]==size[b] \textbf{size[a]==size[b]} size[a]==size[b]

m a x a b = m a x ( s i z e [ a ] , s i z e [ b ] ) maxab=max(size[a],size[b]) maxab=max(size[a],size[b]) m i n a b = m i n ( s i z e [ a ] , s i z e [ b ] ) minab=min(size[a],size[b]) minab=min(size[a],size[b])

则我们将 m a x a b maxab maxab向上跳 size[minab]-size[maxab] \textbf{size[minab]-size[maxab]} size[minab]-size[maxab]

并将这个步数表示成二进制,即 2 n 2^n 2n 步,其中
n = l o g 2 s i z e [ m i n a b ] − s i z e [ m a x a b ] n=log_2^{size[minab]-size[maxab]} n=log2size[minab]size[maxab]
由此即可通过 倍增数组 ( LCA[MAXN] [MAXLOG] ) 来向上跳 2 i 2^i 2i

即可实现在时间复杂度为 O ( l o g 2 n ) O(log_2^n) O(log2n) 的情况下到达目标深度

(3)求出 L C A ( a , b ) LCA(a,b) LCA(a,b)

L L L a a a b b b 节点向上走的第 L L L 步后到达了同一节点,则此节点就是 L C A ( a , b ) LCA(a,b) LCA(a,b)

同理,节点 a a a b b b 向上走的第 L − 1 L-1 L1 步肯定为不同的节点

反之,向上走的第 L + 1 L+1 L+1 步就为节点 a a a 与节点 b b b 第二近公共祖先,次公共祖先

我们可以从大到小枚举往上走 2 i 2^i 2i 步,若节点 a a a b b b 为同一节点则停止,否则一起向上走,直到根节点

算法代码:

int size[MAXN],LCA[MAXN][MAXLOG];
int fi[MAXN];//节点的后代个数
int to[MAXN];//节点的具体每个儿子
int nxt[MAXN];//nxt[i]表示节点i最后一条连出去的边
void dfs(int a,int father)
{
    size[a]=size[father]+1;//子节点深度是父亲节点深度+1
    LCA[a][0]=fa;//a节点向上走2^0 步为其父亲节点
    for(int i=1;i<MAXLOG;i++)
    {
        LCA[a][i]=LCA[LCA[a][i-1]][i-1];//刚推的公式
    }
    for(int i=fi[a];i!=0;i=nxt[i])//类似于最短路中的链式向前星
    {
        if(to[i]!=father)
        {
            dfs(to[i],a);//递归调用
        }
    }
    return;
}
int getLCA(int x,int y)
{
    if(size[x]<size[y])//刚才我们是定义了maxab和minab,这里直接进行比较,选出深度较大的节点
    {
        swap(x,y);
	}
    for(int i=MAXLOG-1;i!=0;i--)
    {
        if(size[LCA[x][i]]>=size[y])
        {
            x=size[x][i];
	    }
	}
    if(x==y)
    {
        return x;//找到LCA了
	}
    for(int i=MAXLOG-1;i!=0;i--)
    {
        if(LCA[x][i]!=LCA[y][i])
        {
            x=LCA[x][i];
            y=LCA[y][i];
		}
	}
    return LCA[x][0];
}
F3:Tarjan 算法
步骤:

(1)使用 DFS 深搜整棵树,开始时每个节点都是一个独立的集合( 并查集

(2)DFS (x)时,每次访问完子树 y ,把集合 y 合并到 x

(3)当 x 的所有子节点都被访问结束后,标记 x t r u e true true,即已被访问

(4)遍历所有关于 x 的询问 ( x , y ) (x,y) (x,y) ,如果 y 已被访问,则本访问答案为并查集的 f i n d ( y ) find(y) find(y)

代码实现:
string g[MAXN],q[MAXN];
int f[MAXN];
bool book[MAXN];
void init()
{
    for(int i=1;i<=n;i++)
    {
        f[i]=i;
	}
    return;
}
int findfather(int x)
{
    if(x==f[x])
    {
        return x;
	}
    f[x]=findfather(x);
    return f[x];
}
void Union(int x,int y)//合并函数,将节点x所在的集合合并到集合y中
{
    f[findfather(x)]=f[findfather(y)];
    return;
}
void dfs(int x)
{
    for(int i=0;i<g[x].size();i++)
    {
        dfs(g[x][i]);
        Union(g[x][i],x);
    }
    book[x]=true;
    for(int i=0;i<q[x].size();i++)
    {
        int m=q[]
	}
    return;
}

树剖算法

树剖,全名树链剖分,是处理LCA的一种算法

预处理:

第一次dfs: 求出dep[] fa[] size[] son[]

dep[u]:u的深度

fa[u]:u的父亲节点

size[u]:以u为根节点的子树的节点数

son[u]:u的重儿子,u-son[u]为重边

第二次dfs: 求出top[]

top[u]:u 所在的重链上的顶端节点编号(重链上深度最小的节点)

id[u]:u在节点序列中位置的下标(可有可无)

rev[x]:树链剖分后节点序列中第x个位置的节点(可有可无)

  • 30
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值