最近公共祖先(三种算法)

  最近研究了一下最近公共祖先算法,根据效率和实现方式不同可以分为基本算法、在线算法和离线算法。下面将结合hihocoder上的题目分别讲解这三种算法。

1、基本算法

对于最近公共祖先问题,最容易想到的算法就是从根开始遍历到两个查询的节点,然后记录下这两条路径,两条路径中距离根节点最远的节点就是所要求的公共祖先。

题目参见 #1062 : 最近公共祖先·一

附上AC代码,由于记录的方式采取的是儿子对应父亲,所以实现的时候有点小技巧,就是对第一个节点的路径进行标记,查找第二个节点的路径时一旦发现访问到第一个被标记的节点,即为公共祖先。时间复杂度为QlogN,Q为查询的次数。

#include <stdio.h>
#include <stdlib.h>
#include <string>
#include <vector>
#include <iostream>
#include <map>
using namespace std;

map<string, string> sonToFather;//record son and it‘s father
map<string, int> visit;//record the visited node
void findAnscetor(string str1, string str2){
    visit.clear();
    map<string, int> visit;
    string ans = str1;
    while (!ans.empty()){
        visit[ans] = 1;
        ans = sonToFather[ans];
    }
    ans = str2;
    while (!ans.empty()){
        if (visit[ans]){
            break;
        }
        ans = sonToFather[ans];
    }
    if (ans.empty()){
        printf("%d\n", -1);
    }
    else{
        cout << ans<<endl;
    }
}

int main(){
    int N;
    scanf("%d", &N);
    int step = 0;
    while (step < N){
        step++;
        string father, son;
        cin >> father >> son;
        sonToFather[son] = father;
    }
    step = 0;
    int M;
    scanf("%d", &M);
    while (step < M){
        step++;
        string str1, str2;
        cin >> str1 >> str2;
        findAnscetor(str1, str2);
    }
    system("pause");
    return 0;
}

2、离线算法

离线算法称为“tarjan”算法,实现思想是以“并查集”+深搜。即首先从根节点开始进行深度搜索,搜索的时候标记每个节点的集合属于自己,当回溯的时候,把每个儿子节点的集合并到父亲节点。回溯是tarjan的核心思想之一。当对某个节点node的子树全部遍历结束后,对这颗子树内的所有节点通过可以通过并查集的并操作,一定是可以保证其集合为node所在的集合,而不会到node的父亲集合(因为node的父亲节点还未进行回溯操作,没有改变node的指向)。先假设需要查询的节点为x和y,如果发现遍历到y的时候,x已经遍历,可以推出,x一定是在y的左边,这时候找到x所在的集合,即为x和y的最近公共祖先。可能说的比较抽象,下面画图说明。

  假设要查询A和C节点,当访问到A节点的时候,可以发现C节点已经被访问。此时用并查集的并操作得到C节点的集合为D,由于D还没有回溯到D的父亲,所以对于D的子树都并到了D的集合中。最终可以得到A和C的最近公共祖先为D。

  tarjan算法的时间复杂度为O(N+Q),其中Q为查询的次数,比基本算法好,但是需要一次性输入所有的查询数据。下面附上AC代码,题目详见#1067 : 最近公共祖先·二

#include <stdio.h>
#include <stdlib.h>
#include <string>
#include <fstream>
#include <map>
#include <vector>
#include <iostream>

using namespace std;

const int N = 1e5 + 10;

vector<int> vecArray[N];//the map of father and son
map<string, int> indexNode;//the index of each node
vector<pair<int, int>>  indexQuery[N];//the qeury collection
int fa[N],result[N];
string Name[N];

int findX(int x){//find and union
    if (fa[x] == x)
        return x;
    else
        return fa[x] = findX(fa[x]);
}
void LCA(int u){
    fa[u] = u;
    for (int i = 0; i < vecArray[u].size(); i++){
        int v = vecArray[u][i];
        LCA(v);
        fa[v] = u;
    }
    for (int j = 0; j < indexQuery[u].size(); j++){
        pair<int, int> p = indexQuery[u][j];
        if (fa[p.first] != -1){
            result[p.second] = findX(p.first);
        }
    }
}
int main(){
    #ifdef TYH
        freopen("in.txt", "r", stdin);
    #endif // TYH
    int n;
    int i, j, k;
    scanf("%d", &n);
    int num = 0;
    //memset(fa, -1, sizeof(fa));
    for (i = 0; i < N; i++)
        fa[i] = -1;
    indexNode.clear();
    for (i = 0; i < n; i++){
        string father, son;
        cin >> father >> son;
        if (indexNode.count(father)==0){
            indexNode[father] = num;
            Name[num] = father;
            num++;
        }
        if (indexNode.count(son)==0){
            indexNode[son] = num;
            Name[num] = son;
            num++;
        }
        vecArray[indexNode[father]].push_back(indexNode[son]);
    }
    int m;
    scanf("%d", &m);
    for (i = 0; i < m; i++){
        string str1, str2;
        cin >> str1 >>str2;
        int index1 = indexNode[str1];
        int index2 = indexNode[str2];
        indexQuery[index1].push_back(make_pair(index2, i));
        indexQuery[index2].push_back(make_pair(index1, i));
    }
    LCA(0);
    for (i = 0; i < m; i++){
        cout << Name[result[i]] << endl;
    }
    return 0;
}

3、在线算法

  虽然离线算法具有较好的时间复杂度,但由于离线的特性可能在某些场合不适用。在线算法是深搜+RMQ,可以实现O(N)处理数据和O(1)的查询效率。RMQ算法在稍后进行介绍,是用来求某个区间内的最值问题。如果能够把树转换成一个线性数组,然后再应用RMQ算法,就能够实现O(1)的查询。对有根树T进行DFS,将遍历到的结点按照顺序记下,我们将得到一个长度为2N – 1的序列,称之为T的欧拉序列F。每个结点都在欧拉序列中出现,我们记录结点u在欧拉序列中第一次出现的位置为pos(u),如下图所示。

  根据DFS的性质,对于两结点u、v,从pos(u)遍历到pos(v)的过程中经过LCA(u, v)有且仅有一次,且深度是深度序列B[pos(u)…pos(v)]中最小的即LCA(T, u, v) = RMQ(B, pos(u), pos(v)),并且问题规模仍然是O(N)的。题目参见#1069 : 最近公共祖先·三,实现的时候有些细节需要注意,尤其是RMQ部分。附上AC代码。

#include <stdio.h>
#include <stdlib.h>
#include <string>
#include <fstream>
#include <map>
#include <vector>
#include <iostream>
#include <math.h>
using namespace std;

const int N = 1e5 + 10;
const int M = 30;
int arrayData[N];
int rmqData[2*N][M];
vector<int> vecArray[N];//the map of father and son
map<string, int> indexNode;//the index of each node
int firstNode[N];//the first position of the node
string Name[N];
int pos[2 * N],depArray[2*N];
int countNum = -1;
int n,m;
bool flag[N];
int min(int x, int y){
    return (x <y ? x : y);
}
void Dfs(int u,int dep){
    countNum++;
    if (firstNode[u] == -1){
        firstNode[u] = countNum;
    }
    depArray[countNum] = dep;
    pos[countNum] = u;
    int i = 0;
    for (i = 0; i < vecArray[u].size(); i++){
        if (flag[i] == false){
            Dfs(vecArray[u][i], dep + 1);
            countNum++;
            pos[countNum] = u;
            depArray[countNum] = dep;
        }
    }
}
void RMQ(){
    int i, j;
    for (i = 0; i < 2*n-1; i++){
        rmqData[i][0] = i;//record the index instead of dep
    }
    int m = (int)(log(2 * n) / log(2));
    for (j = 1; j <= m; j++){
        for (i = 0; i + (1 << j) - 1 <2*n-1; i++){
            int x = rmqData[i][j - 1];
            int y = rmqData[i + (1 << (j - 1))][j - 1];
            if (depArray[x] < depArray[y])
                rmqData[i][j] = x;
            else
                rmqData[i][j] = y;
        }
    }
}

int main(){
    int i, j, k;
    scanf("%d", &n);
    m = (log(n*1.0) / log(2.0));
    int num = 0;
    for (i = 0; i < N; i++){
        firstNode[i] = -1;
        flag[i] = false;
    }
    indexNode.clear();
    for (i = 0; i < n; i++){
        string father, son;
        cin >> father >> son;
        if (indexNode.count(father) == 0){
            indexNode[father] = num;
            Name[num] = father;
            num++;
        }
        if (indexNode.count(son) == 0){
            indexNode[son] = num;
            Name[num] = son;
            num++;
        }
        vecArray[indexNode[father]].push_back(indexNode[son]);
    }
    Dfs(0, 0);
    RMQ();
    int s;
    scanf("%d", &s);
    for (i = 0; i < s; i++){
        string str1, str2;
        cin >> str1 >> str2;
        int l = indexNode[str1];
        int r= indexNode[str2];
        l = firstNode[l];
        r = firstNode[r];
        if (l > r){
            int temp = l;
            l = r;
            r = temp;
        }
        int k = 0;
        while ((1 << (k + 1)) <= (r - l + 1)) k++;
        int x = rmqData[l][k];
        int y=rmqData[r - (1 << k) + 1][k];
        if (depArray[x] < depArray[y]){
            cout << Name[pos[x]] << endl;
        }
        else{
            cout<< Name[pos[y]]<<endl;
        }
    }
    return 0;
}
时间: 2024-10-26 17:39:26

最近公共祖先(三种算法)的相关文章

hihoCoder_#1069_最近公共祖先&#183;三(RMQ-ST模板)

#1069 : 最近公共祖先·三 时间限制:10000ms 单点时限:1000ms 内存限制:256MB 描述 上上回说到,小Hi和小Ho使用了Tarjan算法来优化了他们的"最近公共祖先"网站,但是很快这样一个离线算法就出现了问题:如果只有一个人提出了询问,那么小Hi和小Ho很难决定到底是针对这个询问就直接进行计算还是等待一定数量的询问一起计算.毕竟无论是一个询问还是很多个询问,使用离线算法都是只需要做一次深度优先搜索就可以了的. 那么问题就来了,如果每次计算都只针对一个询问进行的话

我对最近公共祖先LCA(Tarjan算法)的理解

LCA 最近公共祖先 Tarjan(离线)算法的基本思路及我个人理解 首先是最近公共祖先的概念(什么是最近公共祖先?): 在一棵没有环的树上,每个节点肯定有其父亲节点和祖先节点,而最近公共祖先,就是两个节点在这棵树上深度最大的公共的祖先节点. 换句话说,就是两个点在这棵树上距离最近的公共祖先节点. 所以LCA主要是用来处理当两个点仅有唯一一条确定的最短路径时的路径. 有人可能会问:那他本身或者其父亲节点是否可以作为祖先节点呢? 答案是肯定的,很简单,按照人的亲戚观念来说,你的父亲也是你的祖先,而

Opencv——彩色图像灰度化的三种算法

为了加快处理速度在图像处理算法中,往往需要把彩色图像转换为灰度图像.24为彩色图像每个像素用3个字节表示,每个字节对应着RGB分量的亮度. 当RGB分量值不同时,表现为彩色图像:当RGB分量相同时,变现为灰度图像: 一般来说,转换公式有3中. (1)Gray(i,j)=[R(i,j)+G(i,j)+B(i,j)]/3; (2)Gray(i,j)=0.299*R(i,j)+0.587*G(i,j)+0.144*B(i,j); (3)Gray(i,j)=G(i,j);//从2可以看出G的分量比较大所

Java利用 DES / 3DES / AES 这三种算法分别实现 对称加密

转载请注明出处:http://blog.csdn.net/smartbetter/article/details/54017759 有两句话是这么说的: 1)算法和数据结构就是编程的一个重要部分,你若失掉了算法和数据结构,你就把一切都失掉了. 2)编程就是算法和数据结构,算法和数据结构是编程的灵魂. 注意,这可不是我说的,是无数程序员总结的,话说的很实在也很精辟,若想长久可持续发展,多研究算法还是很有必要的,今天我给大家说说加密算法中的对称加密算法,并且这里将教会大家对称加密算法的编程使用.包含

快速排序、归并排序、堆排序三种算法性能比较

快速排序.归并排序.堆排序三种排序算法的性能谁最好呢?网上查了一下说快速排序最快.其次是归并排序,最差的是堆排序:而理论上三种排序算法的时间复杂度都是O(nlogn),只不过快速排序最差的会达到O(n^2),但是数据的随机性会消除这一影响,今天就来实际比较一下: 1 #include <iostream> 2 #include<time.h> 3 using namespace std; 4 #define MAX 100000000 5 int data1[MAX],data2[

字符串匹配的三种算法

下面将介绍三种有关字符串匹配的算法,一种是朴素的匹配算法,时间复杂度为O(mn),也就是暴力求解.这种方法比较简单,容易实现.一种是KMP算法,时间复杂度为O(m+n),该算法的主要任务是求模式串的next数组.另外还有一种对KMP算法的改进,主要是求nextval数组. 第一种朴素的匹配算法: int index(char str[], char subStr[]) { int i = 0, j = 0,index = 0; while (str[i] != '\0' && subStr

Java常用三种算法排序比较

Java常用三种算法排序比较 冒泡排序: package demo1; /** * * @author xiaoye 2014-5-13 */ /** * 有N 个数据需要排序,则从第0 个数开始,依次比较第0 和第1 个数据, * 如果第0 个大于第1 个则两者交换,否则什么动作都不做,继续比较第 1 个第2个-, * 这样依次类推,直至所有数据都"冒泡"到数据顶上. 冒泡排序的效率 O(N*N ),比较 N*N/2 ,交换N*N/4 . */ public class Bubble

最近公共祖先LCA(Tarjan算法)的思考和算法实现——转载自Vendetta Blogs

最近公共祖先LCA(Tarjan算法)的思考和算法实现 LCA 最近公共祖先 Tarjan(离线)算法的基本思路及其算法实现 小广告:METO CODE 安溪一中信息学在线评测系统(OJ) //由于这是第一篇博客..有点瑕疵...比如我把false写成了flase...看的时候注意一下! //还有...这篇字比较多 比较杂....毕竟是第一次嘛 将就将就 后面会重新改!!! 首先是最近公共祖先的概念(什么是最近公共祖先?): 在一棵没有环的树上,每个节点肯定有其父亲节点和祖先节点,而最近公共祖先

POJ 1330 LCA最近公共祖先 离线tarjan算法

题意要求一棵树上,两个点的最近公共祖先 即LCA 现学了一下LCA-Tarjan算法,还挺好理解的,这是个离线的算法,先把询问存贮起来,在一遍dfs过程中,找到了对应的询问点,即可输出 原理用了并查集和dfs染色,先dfs到底层开始往上回溯,边并查集合并 一边染色,这样只要询问的两个点均被染色了,就可以输出当前并查集的最高父亲一定是LCA,因为我是从底层层层往上DSU和染色的,要么没被染色,被染色之后,肯定就是当前节点是最近的 #include <iostream> #include <