最近公共祖先(Least Common Ancestors)

题意:

给定一棵有根树T,给出若干个查询lca(u, v)(通常查询数量较大),每次求树T中两个顶点u和v的最近公共祖先,即找一个节点,同时是u和v的祖先,并且深度尽可能大(尽可能远离树根)。通常有以下几种算法:

  • 在线算法,每次读入一个查询,处理这个查询,给出答案。
  • 离线算法,一次性读入所有查询,统一进行处理,给出所有答案。

在线:

倍增(基于二分搜索):

基本思想就是让u和v同时走到同一高度,然后再一起一步步往上走。
将父亲结点的父亲结点利用起来,依次计算,便可以得到从当前结点向上走2k步所到达的顶点,这样便有了k以内的点的所有信息,进行二分查找答案即可~
预处理时间复杂度O(nlogn),查询时间复杂度O(logn)

关键代码:

首先预处理阶段

//DFS预处理所有结点的深度和父节点
void dfs(int v, int p, int d)
{
    pa[0][v] = p;
    dept[v] = d;
    for(int i = head[v]; i != -1; i = edge[i].next){
        int u = edge[i].to;
        if(u == p) continue;
        dfs(u, v, d + 1);
    }
}
void init()
{
    dfs(root, -1, 0);
    //预处理祖先,向上走2^i所到的结点
    for(int i = 0; i < maxm - 1; i++){
        for(int j = 1; j <= V; j++){
            if(pa[i][j] < 0) pa[i + 1][j] = -1;
            else pa[i + 1][j] = pa[i][pa[i][j]];
        }
    }
}

计算u和v的lca

int lca(int u, int v)
{
    //让u和v 向上走到同一高度
    if(dept[u] > dept[v]) swap(u, v);
    for(int i = 0; i < maxm; i++){
        if((dept[v] - dept[u]) >>i &1)
            v = pa[i][v];
    }
    if(u == v) return u;

    //二分搜索计算lca
    for(int i = maxm - 1; i >= 0; i--){
        if(pa[i][u] != pa[i][v]){
            u = pa[i][u];
            v = pa[i][v];
        }
    }
    return pa[0][u];
}

基于RMQ的算法:

初始化过程O(nlogn),查询过程O(1)
有根树处理的一个技巧就是将树转化为从根DFS标号后得到的序列。而这种算法的基本思想就是将树看成一个无向图,u和v的公共祖先一定在u和v之间的最短路上。
算法分三步:

  • 首先DFS对结点从跟开始标号,用数组vs保存访问顺序,height记录深度。每条边恰好经过两次,因此一共记录了2n1个结点
  • 计算对于每个顶点首次出现子的下标,保存在id中。
  • 获取LCA(u,v)LCA(u,v)=vs[id[u]iid[v]中深度最小的i]

预处理:

void dfs(int u, int pre, int dept)
{
    vs[cnt] = u;
    height[cnt] = dept;
    id[u] = cnt++;
    for(int i = head[u]; i != -1; i = edge[i].next){
        dfs(edge[i].to, u, dept + 1);
        vs[cnt] = u;
        height[cnt++] = dept;
    }
}
void init()
{
    cnt = 1; //vs数组下标从1开始
    dfs(root, root, 0);
    st.init(2 * V - 1);
}

而最后一步属于RMQ(Range Minimum/Maximum Query),即区间最值查询问题,我们可以用线段树解决,也可以使用ST(Sparse Table)算法,在O(nlogn)时间内进行预处理,然后在O(1)时间内回答每个查询。
预处理使用动态规划,设dp[i][j]是从i开始的2j个数中的深度最小的值的下标。则有状态转移方程:

if(height[dp[i][j - 1]] < height[dp[i + (1<<(j - 1))][j - 1]])  
    dp[i][j] = dp[i][j - 1];
else   
    dp[i][j] = dp[i + (1<<(j - 1))][j - 1];

初始化:

 for(int i = 1; i <= n; i++)  dp[i][0] = i;

查询:

int query(int a, int b)
{
   if(a > b) swap(a, b);
   int k = lg[b - a + 1] ;
   if(height[dp[a][k]] <= height[dp[b - (1<<k) + 1][k]])
        return dp[a][k];
   else 
        return dp[b - (1<<k) + 1][k];
}

离线Tarjan算法:

讲的很好

Tarjan算法是离线算法,基于后序DFS和并查集。
算法从根节点root开始搜索,每次递归搜索所有的子树,然后处理跟当前根节点相关的所有查询。

算法用集合表示一类节点,这些节点跟集合外的点的LCA都一样,并把这个LCA设为这个集合的祖先。当搜索到节点x时,创建一个由x本身组成的集合,这个集合的祖先为x自己。然后递归搜索x的所有儿子节点。

所有子树处理完毕之后,处理当前根节点x相关的查询。遍历x的所有查询,如果查询的另一个节点v已经访问过了,那么x和v的LCA即为v所在集合的祖先。

建树可以用数组写链表也可以用vector保存,而查询可以用矩阵保存,这样可以减少重复,也可以用链表的形式,将一个结点的查询连在一起。

Tarjan关键代码:

void LCA(int u)
{
    ance[u] = u;
    vis[u] = 1;
    for(int i = head[u]; i != -1; i = edge[i].next){
        int v = edge[i].to;
        if(vis[v]) continue;
        LCA(v);//访问子树
        unite(u, v);//子树与当前结点合并
        ance[_find(u)] = u;//祖先为u
    }
    for(int i = h[u]; i != -1; i = query[i].next){
        int v = query[i].q;
        if(vis[v])  ans[query[i].index] = ance[_find(v)];
    }
}

//感觉这个ance数组完全可以不用~~

posted @ 2016-03-24 22:33  zhuyujiang  阅读(270)  评论(0编辑  收藏  举报