对lca的理解总结

lca 在心中一直都是一个很水的东西,,但是昨天被问lca tarjan 的具体做法竟然不会了。。在这里再一次系统地总结一遍, 希望可以把这个东西彻底理解透彻。

lca, 即 lowest common ancestor , 最近公共祖先。对于有根树T的两个字节点 u, v,  最近公共祖先LCA(T,u, v)表示一个节点x, 满足x 是u, v的祖先且x的深度尽可能大。 显然从u到v的路径一定经过点x。

O(n)计算树上两点最短路的多组询问, 仔细想想lca 的算法还是很厉害哒!


算法一, tarjan

说道Tarjan 想先说一下它的这个读音问题。。很多人把它读成 塔阳 这样的, 我也是这样读的, 但是也有一些同学本着J发音的原则读成 塔江 咔咔咔应该也可以吧。当时学德语的时候J的读音应该就是类似“耶” 这样的, 但是这货是个美国人。。算了言归正传。

tarjan可以实现lca的离线询问, 思想非常简单, 就是把这棵树遍历一遍。 我们想象整棵树中的一个子树t, 在这棵树中, 任意两个点的lca 要么是根, 要么就是根下面的另一个节点, 但这个节点显然已经在遍历根节点之前遍历过了, 也就是说我们在dfs的时候每到达一个节点, (u, v)都属于它的子节点的所有询问就已经处理完了, 所以当我们遍历到根的时候, 已经获得了所有询问的解。

接下来要解决的问题是当遇到一个点u, 并且与之配对的点v也已经访问过了, 这时候怎么确定他们两个共同存在的最小的子树的根(x)在哪呢。我们想象一下访问的顺序, 应该是先到了x 然后走到了v, 然后再回到x的点上, 然后再走到v。而x就是v的祖先中, 深度最大的一个还没有开始处理询问问题, 也就是还没有遍历完所有子树的点。可以想到一个直观的方法就是每开始处理关于一个点的询问, 就在这个点上做上标记, 然后暴力查找到v的祖先中没有被标记的第一个点就好了。但是这样还是需要n log n的查询时间, 难道不可以把它优化成O(1)的吗? 当然可以! 用并查集在每一次询问的时候把这个点加入到并查集里面就可以啦!所以说这个算法的总时间复杂度就是边的个数加上询问的个数O(m + q)。

算法流程:

Tarjan(u){

           u 的父节点 为 u;

           遍历u的每一个儿子{

                    lca(u 的儿子);

                    u的儿子的父亲节点为u;

           }

           遍历和u有关的每一个询问

                         如果询问中的另一个节点v被访问过了

                                           ans[u][v] = find(v);


}

void dfs(int u){
	fat[u] = u;
	vis[u] = 1;
	for(int i = head[u]; i != -1; i = edge[i].next)
		if(! vis[edge[i].to]){
			dfs(edge[i].to);
			fat[edge[i].to] = u;	
		}
	for(int i = qhead[u]; i != -1; i = qedge[i].next)
		if(vis[qedge[i].to]){
			qedge[i].lca = find(qedge[i].to);
			qedge[i ^ 1].lca = qedge[i].lca;	
		}
}


算法二:倍增法

这种方法相对来说就非常的直观了。

        基本思想是:

        deep[i] 表示 i节点的深度, fa[i,j]表示 i 的 2^j (即2的j次方) 倍祖先,那么fa[i , 0]即为节点i 的父亲,然后就有一个递推式子:

                                                      fa[i,j]= fa [ fa [i,j-1] , j-1 ] ,可以这样理解:

设tmp = fa [i, j - 1] ,tmp2 = fa [tmp, j - 1 ] ,即tmp 是i 的第2 ^ (j - 1) 倍祖先,tmp2 是tmp 的第2 ^ (j - 1) 倍祖先 , 所以tmp2 是i 的第 2 ^ (j - 1) + 2 ^ (j - 1) =  2^ j 倍祖先,注意:这里的“倍”可不能理解为倍数的意思,而是距离节点i有多远的意思,节点i的第2 ^ j 倍祖先表示的节点u满足deep[ u ] - deep[ i ] = 2 ^ j
        这样子一个O(NlogN)的预处理求出每个节点的 2^k 的祖先  
        然后对于每一个询问的点对a, b的最近公共祖先就是: 

 先判断是否 d[x]< d[y] ,如果是的话就交换一下(保证 x 的深度大于 y 的深度), 然后把 x 调到与 y 同深度, 同深度以后再把a, b 同时往上调,调到有一个最小的 j 满足fa [x,j] != fa [y,j] (x,y是在不断更新的), 最后再把(x,y)往上调(x=p[x,0], y=p[y,0])  ,一个一个向上调直到x = y, 这时 x或y 就是他们的最近公共祖先。

const int POW = 18;  
void dfs(int u,int fa){  
    d[u]=d[fa]+1;  
    p[u][0]=fa;  
    for(int i=1;i<POW;i++) p[u][i]=p[p[u][i-1]][i-1];  
    int sz=edge[u].size();  
    for(int i=0;i<sz;i++){  
        int v=edge[u][i];  
        if(v==fa) continue;  
        dfs(v,u);  
    }  
}  
int lca( int a, int b ){  
    if( d[a] > d[b] ) a ^= b, b ^= a, a ^= b;  
    if( d[a] < d[b] ){  
        int del = d[b] - d[a];  
        for( int i = 0; i < POW; i++ ) if(del&(1<<i)) b=p[b][i];  
    }  
    if( a != b ){  
        for( int i = POW-1; i >= 0; i-- )   
            if( p[a][i] != p[b][i] )   
                 a = p[a][i] , b = p[b][i];  
        a = p[a][0], b = p[b][0];  
    }  
    return a;  
} 



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值