Tarjan离线算法求最近公共祖先(LCA)

LCA问题,是当给定一个有根树T时,对于任意两个结点uv,要求LCA(T,u,v),即是要找得一个离根最远的结点x,使得x同时是uv的祖先。也就是距离它们最近的祖先。


结点3和结点4的最低公共祖先是结点2,即LCA3 4=2。结点3和结点2的最低公共祖先为2,即 LCA3 2=2。同理:LCA5 6=4LCA6 10=1


Tarjan离线算法求LCA(转)

     在上图中,我们可以看出1号结点中的左子树任意找一个节点,再从1号结点的右子树中任意找出的一个结点组成结点对,那么它们的LCA一定是1号点。于此,我们可以得知:若两节点分别分布于某节点的左右子树,那么该节点即为其LCA。再考虑到一个节点自己就是LCA的情况,得知:若某节点是两节点的祖先之一,且这两节点并不分布于该节点的一棵子树中,那么该节点即为两节点的LCA这个定理就是Tarjan算法的基础。

    这种算法是基于DFS和并查集来实现的。设fa[x]为x的父亲,dist[x]为x节点到根节点的距离。首先从一号根节点(记为u)开始访问他的每一个子节点(记为v),并用根节点与当前访问的子节点的距离更新dist值,即dist[v]=dist[u]+map[v][u],其中map[v][u]表示v到u的距离,然后将当前子节点当做根点用上述同样步骤递归下去,并在递归回溯后将其fa[v]值更新,这样的目的是保证子节点v的所有子树全部被访问过。

    现在我们需知道第k个询问的LCA是什么,那么这个操作应在询问中两个节点的子树全部访问完的基础上再进行。对于现在状态的根点u,访问它的子节点v,若v点“作过”根点,即被递归过,才能保证v的所有子树被全部访问完,这时才能将与之有关的询问<即询问中包含v点>更新其LCA=get(v),get为并查集,即找到v所在集合的起点,因为并查集在这里的作用就是将同一子树中的子节点的父亲指向该子树的根节点,相当于归为了一个集合,这个集合的起点就是当前子树的根节点。

     这样,从1号根节点出发,向下递归直到到达叶子节点位置,树中每个节点都被访问过了一次,在回溯后,为了对每个询问(记总询问次数为Q)更新LCA值访问了相关点,则时间复杂度为O(N)+O(Q),因此这是个O(N+Q)的算法!

实现代码:

//father为并查集,Find为并查集的查找操作
//QUERY为询问结点对集合
//TREE为有根树
 Tarjan(u)
      visit[u] = true
      for each (u, v) in QUERY
          if visit[v]
              ans(u, v) = Find(v)
      for each (u, v) in TREE    
          if !visit[v]
              Tarjan(v)
              father[v] = u

图解源于JarjingX--【白话系列】最近公共祖先


STEP 1

节点

1

2

3

4

5

6

7

8

祖先

1

2

3



STEP 2

节点

1

2

3

4

5

6

7

8

祖先

1

2

2


STEP 3

节点

1

2

3

4

5

6

7

8

祖先

1

2

2

4

5



STEP 4

节点

1

2

3

4

5

6

7

8

祖先

1

2

2

4

4

STEP 5

节点

1

2

3

4

5

6

7

8

祖先

1

2

2

4

4

6


STEP 6

节点

1

2

3

4

5

6

7

8

祖先

1

2

2

4

4

4


STEP 7

节点

1

2

3

4

5

6

7

8

祖先

1

2

2

2

2

2


STEP 8

节点

1

2

3

4

5

6

7

8

祖先

1

1

1

1

1

1


STEP 9

节点

1

2

3

4

5

6

7

8

祖先

1

1

1

1

1

1

7


STEP 10

节点

1

2

3

4

5

6

7

8

祖先

1

1

1

1

1

1

7

8


STEP 11

节点

1

2

3

4

5

6

7

8

祖先

1

1

1

1

1

1

7

7


STEP 12

节点

1

2

3

4

5

6

7

8

祖先

1

1

1

1

1

1

1

1


STEP 13

节点

1

2

3

4

5

6

7

8

祖先

1

1

1

1

1

1

1

1



hdu2586 How far away ?

这道题题意是,给定一棵树,每条边都有一定的权值,q次询问,每次询问某两点间的距离。这样就可以用LCA来解,首先找到u, v 两点的lca,然后计算一下距离值就可以了。这里的计算方法是,记下根结点到任意一点的距离dis[],这样ans = dis[u] + dis[v] - 2 * dis[lca(v, v)]了,

#include <iostream>
#include <stdio.h>
using namespace std;
const int MAX=40005;
struct node
{
    int v;
    int w;
    int next;
}a[MAX*2];

struct ques
{
    int v;
    int num;
    int next;
}b[MAX];

bool vis[MAX];
int cnt1,cnt2;
int head[MAX],q[MAX],father[MAX],LCA[MAX],dist[MAX],num_u[MAX],num_v[MAX];

//head[MAX]  邻接表头
//q[MAX]  问题集合
//father[MAX]  元素结点集合
//LCA[i]   i的最近公共祖先
//dist[i]记录了i点到根节点的距离
// num_u[MAX] num_v[MAX] 询问的结点u,v

void Add_Edge(int u,int v,int w)
{
    a[cnt1].v=v; //这条边的到达点
    a[cnt1].w=w;  //边的权值
    
    //邻接表元素插入过程
    a[cnt1].next=head[u];
    head[u]=cnt1++;
}
void Add_Ques(int u,int v,int number)
{
    b[cnt2].v=v; //询问点的标号
    b[cnt2].num=number;   //询问点对应的编号
    
    b[cnt2].next=q[u];
    q[u]=cnt2++;
}
int Find(int x)  //查找根节点  并路径压缩
{
    if(father[x]!=x)
    {
        return father[x]=Find(father[x]);
        
    }
    return father[x];
}
void Tarjan(int x)
{
    father[x]=x;  //作为当前的根节点,将其父亲指向自己
    vis[x]=true;  //标记访问过的结点
    for (int i=q[x]; i!=-1; i=b[i].next)
    {
        int now=b[i].v;
        if(vis[now])  //如果其子结点及其子节点的子树全部访问完才会进入这一步。
        {
            LCA[b[i].num]=Find(now); //更新LCA的值
        }
    }
    for (int i=head[x]; i!=-1; i=a[i].next)
    {
        int now=a[i].v;
        if(!vis[now])  //未访问 向下递归计算
        {
            dist[now]=dist[x]+a[i].w;  //更新dist的值
            Tarjan(now);    //向下递归计算
            father[now]=x;   //更新关系
        }
    }
}
int main()
{
    int t,n,m;
    scanf("%d",&t);
    while (t--)
    {
        memset(vis, false, sizeof(vis));
        memset(head, -1, sizeof(head));
        memset(q, -1, sizeof(q));
        memset(dist, 0, sizeof(dist));
        cnt1=cnt2=0;
        scanf("%d%d",&n,&m);
        for (int i=1; i<=n; i++)
        {
            father[i]=i;
        }
        for (int i=1; i<n; i++)
        {
            int u,v,w;
            scanf("%d%d%d",&u,&v,&w);
            Add_Edge(u, v, w);
            Add_Edge(v, u, w);
        }
        for (int i=0; i<m; i++)
        {
            int u,v;
            scanf("%d%d",&u,&v);
            Add_Ques(u, v, i);
            Add_Ques(v, u, i);
            num_u[i]=u,num_v[i]=v;
        }
        Tarjan(1);
        for (int i=0; i<m; i++)
        {
            printf("%d\n",dist[num_u[i]]+dist[num_v[i]]-2*dist[LCA[i]]);
        }
    }
    return 0;
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值