LCA-最近公共祖先-Tarjan解法

LCA的各种解法见:July的《程序员编程艺术:面试和算法心得》电子版(非官方)
下面只讲Tarjan算法解决这个问题的方法,本质上仅仅是普通的dfs而跟Tarjan算法是没关系的,只是因为思路像所以这么说。它解决LCA是离线处理的,即等待全部输入完后一起处理再输出,而Tarjan好处在于查询次数再多也只需要遍历一次树。

思路

对树进行一次深搜遍历,遍历中做此操作:

假设当前遍历到 t 这个顶点,每遍历 t 的一个子树就把子树和 t 点归为一个集合,且设定这个集合的祖先是 t 。
遍历完所有子树后遍历跟 t 点相关的查询,若另外个点已经访问过,则这次查询的LCA就是另外个点所在集合的祖先。

伪代码

void dfs(int u){
    设定u为已访问
    设定u的祖先为u自身
    遍历u的所有邻接点v
        若未访问过,则dfs(v),合并u所在集合和v所在集合为一个新集合,设定新集合的祖先为u
        若访问过则不再访问
    检查跟这个u点有关的查询(u,v)
        若v已访问,则lca = v所在集合的祖先
        若v未访问不做处理
}

原理

对查询边(u,v),只有下面两种情况:
1) u,v在t的同一个子树下,则u,v中距离整个树的根最近的就是LCA。
假设u点在上v在下,在搜索到u点时去递归遍历u的子树(里面含v),遍历完这棵子树之后按照操作规则,子树就和u合为一个集合且集合祖先为u,然后在u点遍历完所有子树后就检查(u,v)查询发现v已访问过,一查v所在集合的祖先不就是u么。

2) u,v在t的不同子树下(如在二叉树中则是一左一右),则t点就是lca,一看图就知道。
假设u已经被遍历过了(u肯定在某个t的集合里且集合祖先为t),则访问到v(不同子树意味着v肯定在u这个集合之外)时,直接找u点所在集合的祖先就是u,v点的LCA。

例子

为方便描述,盗上面链接的图
栗子图
比如查询(5,6)和(3,5),而搜索从1号开始,当前遍历到2号点,然后递归遍历5,6点,先遍5号点,5号点没有子树了,就直接查询跟5号点相关查询,发现对应的6号点没访问过,先跳过不管(没访问过的没有信息去判断)。
根据递归性质返回2号点,把2和5合成一个集合{2,5},集合祖先为2。接下来去访问6,6也没有子树,然后有(5,6)这个查询,就直接查询5所在的集合祖先,嗯就是2。
之后返回2号点,把{2,5}和{6}合并成{2,5,6},祖先设为2。
返回1号点,把{2,5,6}和{1}合并成{1,2,5,6},祖先为1。
去访问1的其他子树比如3,到3号点去遍历3的全部子树,这里略过子树访问说明,访问完3的全部子树返回3后{3,7,8,9,10,11,12}的祖先是3,然后找到跟3有关的查询(3,5),直接查询5所在集合的祖先,嗯就是1啦!

例子中得到的结论

  1. 例子的遍历顺序不一定是子树就按图中画的从左到右,也可以先访问到3这个子树再去2号子树,这跟加边顺序有关,不过结果一样,不过发现答案的时候是这次查询的另外一个点而已。【这意味着代码中查询的边要设为双向】
  2. 对于每个点所在的集合、所在集合的祖先是在遍历过程中一直改变的,集合的大小一直在增大,而祖先就越来越靠近整棵树的树根。
  3. 集合怎么表示?并查集啊。

结合下面代码去看上文的图很好理解的
验证的话可以提交到 POJ 1330 Nearest Common Ancestors
虽然题目是只查询一次,可以水,不过我们就当作是多次查询来算

#include <cstdio>
#include <cmath>
#include <cstring>
#include <string>
#include <iostream>
#include <algorithm>
using namespace std;
#define ll long long
#define clr( a , x ) memset ( a , x , sizeof (a) );
#define RE freopen("1.in","r",stdin);
#define WE freopen("1.out","w",stdout);
#define SpeedUp std::cout.sync_with_stdio(false);
#define debug(x) cout << "Line " << __LINE__ << ": " << #x << " = " << x << endl;
const int maxn = 10005;
const int maxm = 10005;
const int inf = 0x3f3f3f3f;
const int maxq = 1;

int eCnt;
int head[maxn];
struct Edge
{
    int v, next;
} edge[maxm];
void addEdge(int u, int v) {
    edge[eCnt].v = v, edge[eCnt].next = head[u]; head[u] = eCnt++;
}

//---离线处理用到的查询
int ans[maxq];
int qCnt;
int qHead[maxn];
struct Query
{
    int v,next;
    int index;  //查询的编号
}query[maxq*2];
void addQuery(int u, int v,int index) { //双向,因为查u,v和查v,u是一样的
    query[qCnt].index = index;
    query[qCnt].v = v, query[qCnt].next = qHead[u]; qHead[u] = qCnt++;
    query[qCnt].index = index;
    query[qCnt].v = u, query[qCnt].next = qHead[v]; qHead[v] = qCnt++;
}

//---并查集
int father[maxn];
int find(int x){
    if(x != father[x]){
        father[x] = find(father[x]);
    }
    return father[x];
}
void merge(int x,int y){
    x = find(x);
    y = find(y);
    if(x != y)
        father[y] = x;
}


int du[maxn];
int vis[maxn];
void init(){
    clr(head,-1);
    clr(qHead,-1);
    clr(vis,0);
    eCnt = 0;
    qCnt = 0;
    clr(du,0);
}

void lca(int u){
    vis[u] = 1;
    //遍历u点的邻接边
    for (int i = head[u]; ~i; i = edge[i].next){
        int v = edge[i].v;
        if(vis[v]){
            continue;
        }
        lca(v);
        merge(u,v); //注意此处应该把 u 设置为 v 的父节点,不能反,因为u在树上就是v的父/祖先节点
    }
    //找跟当前u点有关的查询,这个uv查询的答案是v点所在集合的祖先
    for (int i = qHead[u]; ~i; i = query[i].next){
        int v = query[i].v;
        if(vis[v]){
            ans[query[i].index] = find(v);
            // debug(u)debug(v)debug(find(v))
        }
    }
}

int main() {
    // RE
    int t;
    int n,u,v;
    while(scanf("%d",&t)!=EOF){
        while(t--){
            init();
            scanf("%d",&n);
            for (int i = 1; i <= n-1; ++i){
                scanf("%d%d",&u,&v);
                addEdge(u,v);
                du[v]++;
                father[i] = i;  //每个结点的父亲/祖先 都初始化为自己
            }
            father[n] = n;

            int queryCnt = 1;   //查询次数
            for (int i = 1; i <= queryCnt; ++i){
                scanf("%d%d",&u,&v);
                addQuery(u,v,i);
            }

            //找入度为0的点当树根
            int root = 1;
            for (int i = 1; i <= n; ++i){
                if(!du[i]){
                    root = i;
                    break;
                }
            }
            lca(root);
            for (int i = 1; i <= queryCnt; ++i){
                printf("%d\n", ans[i]);
            }
        }
    }
    return 0;
}

猜你喜欢

转载自blog.csdn.net/u012469987/article/details/51305685