最近公共祖先(LCA)算法实现过程 【Tarjan离线+倍增在线+RMQ】

最近公共祖先(LCA)


首先来介绍下最近公共祖先(LCA)的概念

  1. 百度上的解释:对于有根树T的两个结点u、v,最近公共祖先LCA(T,u,v)表示一个结点x,满足x是u、v的祖先且x的深度尽可能大。
  2. 通俗语言:在一棵没有环的树上,每个节点肯定有其父亲节点和祖先节点,而最近公共祖先节点,就是两个节点在这棵树上深度最大的公共的祖先节点,即两个点在这棵树上距离最近的公共祖先节点。易知,在树上这两个点的最短路径唯一时,一定经过该公共祖先节点。(父亲节点也是祖先节点,另外某个节点本身也是它的祖先节点)


    给出个例子加深理解,如图,3和5的最近公共祖先为1,5和6的最近公共祖先为2,2和7的最近公共祖先为2, 6和7的最近公共祖先为4。


求公共最近祖先的算法(设询问次数为q)
  1. 暴力(实际做题不可行):对于每个询问,遍历所有的点,时间复杂度为O(n*q)。
  2. Tarjan(离线)算法: 在一次遍历中把所有询问一次性解决,预处理时间复杂度O(nlogn),每次查询时间复杂度O(1),总时间复杂度是O(nlogn+q)。
  3. 倍增算法:利用二分两个节点同时往上走,直到相遇,预处理时间复杂度O(nlogn),每次查询时间复杂度O(logn),总时间复杂度O(nlogn+qlogn)。
  4. RMQ算法:留坑。


Tarjan(离线)算法



一.Tarjan算法大致实现过程
  1. 先选择一个节点u为根节点,从根节点开始搜索。(标记u已访问过)
  2. 遍历该点u的所有儿子节点v,并标记v已访问过。
  3. 若v还有儿子节点,对v重复ii操作,否则进入下一操作。
  4. 把v合并到u上(并查集)。
  5. 把当前的点设为u,遍历与u有询问关系的节点v。
  6. 如果v在之前已经被访问过,那么u和v的最近公共祖先就是v通过并查集合并后的父亲节点(注意是合并后),即当前的find(v)。

二.Tarjan算法的伪代码

Tarjan(u)           //根节点u
{
    for each(u,v)
    {
        Tarjan(v);  //v还有儿子节点
        join(u,v);  //把v合并到u上
        vis[v]=1;   //访问标记
    }
    for each(u,v)   //遍历与u有询问关系的节点v
    {
        if(vis[v])
        {
            ans=find(v);
        }
    }
}


三. Tarjan算法模拟




还是原来的图,如图,图中共有8个点,7条边,我们需要寻找最近公共祖先的点对为<3,5>,<5,6>,<2,7>,<6,7>

先做好初始化工作,开一个pre数组,记录父亲节点,初始化pre[i]=i;
再开一个vis数组,记录是否已经访问 (memset(vis,0,sizeof(vis)))

然后开始模拟整个过程

1.先取1为根节点, 发现其有两个子节点2和3,先搜索2,又发现2有两个子节点4和5,先搜索4,4也有两个子节点6和7,先搜索6,这时发现6没有子节点了,然后寻找与其有询问关系的节点,发现5和7均与6有询问关系,但都没被访问过。所以返回并标记vis[6]=1,pre[6]=4;

2.接着搜索7,发现7没有子节点,然后寻找与其有询问关系的节点,发现6与其有询问关系,且vis[6]=1,所以LCA(6,7)=find(6)=4。结束并标记vis[7]=1,pre[7]=4;

3.现在节点4已经搜完,且没有与其有询问关系的节点,vis[4]=1,pre[4]=2;

4.搜索5,发现其有子节点8,搜索8,发现8没有子节点,然后寻找与其有询问关系的节点,也没有,于是返回,且vis[5]=1,pre[8]=5;

5.节点5已经搜完,发现有两个与其有询问关系的节点6和7,且vis[6]=1,所以LCA(5,6)=find(6)=2;因为vis[7]=1,所以LCA(5,7)=find(7)=2;遍历完毕返回,标记vis[5]=1,pre[5]=2;

(find过程:pre[7]=4,pre[4]=2    ==》2 )

6.节点2已经搜完,发现有一个与其有询问关系的节点7,且vis[7]=1,故LCA(2,7)=find(7)=2。遍历完毕,标记vis[2]=1,pre[2]=1;

7.接着搜索3,没有子节点,发现有一个与其有询问关系的节点5,因为vis[5]=1,所以LCA(3,5)=find(5)=1;遍历结束,标记vis[3]=1,pre[3]=1;

(find过程:pre[5]=2,pre[2]=1   ==》1 )

8.这时返回到了节点1,它没有与之有询问关系的点了,且其pre[1]=1,搜索结束。

完成求最小公共祖先的操作。



 四.Tarjan 算法的代码

由于LCA的题目千变万化,下面给出最基础的模板(给出一系列边用邻接表保存,把询问也用邻接表保存,只求LCA,不维护其他值)



void Tarjan(int now)
{
    vis[now]=1;
    for(int i=head1[now];i!=-1;i=e1[i].next)
    {
        int t=e1[i].t;
        if(vis[t]==0)
        {
            Tarjan(t);
            join(now,t);
        }
    }
    for(int i=head2[now];i!=-1;i=e2[i].next)
    {
        int t=e2[i].t;
        if(vis[t]==1)
        {
            e2[i].lca=find(t);
            e2[i^1].lca=e2[i].lca;
        }
    }
}


倍增算法


一.倍增算法的前期铺垫

    

我们记节点v到根的深度为depth(v)。那么如果节点w是节点u和节点v的最近公共祖先的话,让u往上走(depth(u)-depth(w))步,让v往上走(depth(v)-depth(w))步,都将走到节点w。因此,我们首先让u和v中较深的一个往上走|depth(u)-depth(v)|步,再一起一步步往上走,直到走到同一个节点,就可以在O(depth(u)+depth(v))的时间内求出LCA。


由于节点的最大深度为n,所以这个方法在最坏的情况下一次查询时间复杂度就要O(n),这显然是不够的。于是我们开始考虑优化。


二.倍增算法的实现过程


分析刚才的算法,两个节点到达同一节点后,不论怎么向上走,达到的显然还是同一节点。利用这一点,我们就能够利用二分搜索求出到达最近公共祖先的最小步数了。


首先我们要进行预处理。对于任意的节点,可以通过fa2[v]=fa[fa[v]]得到其向上走2步到达的顶点,再利用这个信息,又可以通过fa4[v]=fa2[fa2[v]]得到其向上走4步所到的顶点。以此类推,我们可以得到其向上走2^k步所到的顶点fa[v][k],预处理的时间点复杂度为O(nlogn)。


有了k=floor(logn)以内的所有信息后,就可以进行二分所搜的,每次查询的时间复杂度为O(logn)。


三.倍增算法的代码及简要分析


void dfs(int u,int pre,int d)  //预处理出每个节点的深度及父亲节点
{
    fa[u][0]=pre;
    depth[u]=d;
    for(int i=0;i<vec[u].size();i++)
    {
        int v=vec[u][i];
        if(v!=pre)
        {
            dfs(v,u,d+1);
        }
    }
}

void init()                    //预处理出每个节点往上走2^k所到的节点,超过根节点记为-1
{
    dfs(root,-1,0);              //root为根节点
    for(int j=0;(1<<(j+1))<n;j++)   //n为节点数目
    for(int i=0;i<n;i++)
    {
        if(fa[i][j]<0) fa[i][j+1]=-1;
        else fa[i][j+1]=fa[fa[i][j]][j];
    }
}

int LCA(int u,int v)
{
    if(depth[u]>depth[v]) swap(u,v);
    int temp=depth[v]-depth[u];
    for(int i=0;(1<<i)<=temp;i++)      //使u,v在同一深度
    {
        if((1<<i)&temp)
            v=fa[v][i];
    }
    if(v==u) return u;
    for(int i=(int)log2(n*1.0);i>=0;i--)  //两个节点一起往上走
    {
        if(fa[u][i]!=fa[v][i])
        {
            u=fa[u][i];
            v=fa[v][i];
        }
    }
    return fa[u][0];
}



基于RMQ的LCA算法

一、主要思路


大家都知道DFS序吧(不知道的可以先自行百度),对一棵树可以一遍DFS处理出搜索过程中进入某个点以及走出某个点的编号,ss[i]和tt[i].同时顺便得到每个点距离根节点的距离depth[i]。


那么我们回归到LCA的定义,假设我们要求a点和b点的LCA,我们要找的便是从a点走到b点过程中离根节点最近的那个点i,即i满足min(ss[a],ss[b])<=ss[i]<=max(ss[a],ss[b])且depth[i]最大的i。而这可以利用RMQ高效求得。


二.RMQ算法的代码及简要分析



int id[2*maxn];        //保存DFS时每个编号对应的节点编号
int depth[2*maxn];     //保存DFS时每个编号对应的节点深度
int ss[maxn];          //保存每个节点在DFS时第一次出现的编号
int dp[2*maxn][30];    //ST预处理时的数组,用以查询区间里深度最小的编号(DFS序编号)

void dfs(int u,int pre,int dep)
{
    id[++tot]=u;
    ss[u]=tot;
    depth[tot]=dep;
    for(int i=head[u];~i;i=e[i].next)
    {
        int v=e[i].v;
        if(v==pre) continue;
        dfs(v,u,dep+1);
        id[++tot]=u;
        depth[tot]=dep;
    }
}

void ST(int n)      //n一般取2*n-1
{
    int k=(int)(log2(1.0*n));
    for(int i=1;i<=n;i++) dp[i][0]=i;
    for(int j=1;j<=k;j++)
    for(int i=1;i+(1<<j)-1<=n;i++)
    {
        int a=dp[i][j-1];
        int b=dp[i+(1<<(j-1))][j-1];
        if(depth[a]<depth[b]) dp[i][j]=a;
        else dp[i][j]=b;
    }
}

int RMQ(int l,int r)
{
    int k=(int)(log2(1.0*r-l+1));
    int a=dp[l][k];
    int b=dp[r-(1<<k)+1][k];
    if(depth[a]<depth[b]) return a;
    else return b;
}

int LCA(int x,int y)
{
    int l=ss[x];
    int r=ss[y];
    if(l>r) swap(l,r);
    return id[RMQ(l,r)];
}


相较而言,好像RMQ的效率略高一些。






相关题目链接

1.POJ   1330 Nearest Common Ancestors (Tarjan + 倍增 + RMQ) 模板的运用看这里
2.POJ   1986 Distance Queries
3.HDOJ  2586 How far away?
4.POJ   3728 The merchant



【更新】

2017/7/30 增加LCA倍增算法,并修改原有错误

2017/9/13 增加RMQ算法。




猜你喜欢

转载自blog.csdn.net/my_sunshine26/article/details/72717112