LCA

最近公共祖先

最近公共祖先问题:LCA问题便是指,给定一棵树T和两个节点u和v,找出u和v的离根节点最远的公共祖先。


Documents

既然要查找,就需要遍历整棵树。能不能只通过一次遍历,就找到最近公共祖先呢?

答案当然是可以的。

下面是利用后序遍历二叉树的递归算法如下:

typedef struct lca
{
     int data;
     struct lca *left, *right;
 } lca;

 int LCA(lca *root, lca *a, lca *b, lca **result)
 {
     int l, r;
     if (root == NULL)
         return 0;
     if ((l = LCA(root->left, a, b, result)) == 2) return 2;
     if ((r = LCA(root->right, a, b, result)) == 2) return 2;
     if (l + r == 2) { *result = root; return 2; }
     if (root == a || root == b) {
         if (l + r == 1) { *result = root; return 2; }
         return 1;
     }
     return l + r;
 }

Problem: 如果树不是二叉树该怎么办呢?

这个问题很简单,本文就不在叙述了。留给大家实践一下。毕竟优化才是编程的源动力。

但往往查询不仅仅只有一次,当查询的数量变大时,每次查询都要遍历一次树。我们能否经行优化呢?

离线算法-Tarjan算法

Idea: 将一段时间内的询问收集起来统一处理,利用一些与询问个数无关的算法来进行计算,这样时间复杂度在平摊到每个询问的时候就不会很高了。

下面棵树,代表着我们这个问题的一个输入,而这几个是我们要处理的询问。

我现在要做的呢,是以深度优先搜索的顺序来访问这棵树,在这个过程中,我会给这棵树的结点染色,一开始所有结点都是白色的。而当我第一次经过某个结点的时候,我会将它染成灰色,而当我第二次经过这个结点的时候——也就是离开这棵子树的时候,我会将它染成黑色。

举个例子,当我们深度优先搜索到A结点时,结点情况如图。我们会发现(A,B)(A,C)(A,P),这三组询问与A有关。

显然对于(A,B)这组询问来说,结点B的颜色为灰。说明B就是A和B的最近公共祖先。

对于(A,C)来说,C的结点是黑色,说明C结点之前已经被遍历过,最近公共祖先肯定在这些灰色的结点之中,即是最近公共祖先就是这个黑色结点向上的第一个灰色结点。

(A,P),P结点为白色。不做处理

#include <iostream>
#include <map>
#include <string>
#include <vector>
using namespace std;

class person;

class query {
public:
    query(person *p,int n){
        this->p = p;
        this->n = n;
    }
    person *p;
    int n;
};

class person {
public:
    person(string n) {
        name = n;
        parent = 0;
        state = 0; //white
    }
    vector<person*> children;
    vector<query*> querys;
    string name;
    int state;
    person* parent;
};

const int maxn = 1e5 + 10;
int n,m;
map<string,person*> d;
string ans[maxn];


person* get(string n){
    map<string,person*>::iterator it = d.find(n);
    if(it == d.end()) {
        d[n] = new person(n);
    }
    return d[n];
}

person* gray(person* p){
    if(p->state == 1)
        return p;
    return p->parent = gray(p->parent);
}

void dfs(person *p) {
    p->state = 1; //gray

    for(int i=0;i<p->querys.size();++i){
        query* q = p->querys[i];
        if(q->p->state != 0) {
            person* a = gray(q->p);
            ans[q->n] = a->name;
        }
    }

    for(int i=0;i<p->children.size();++i) {
        dfs(p->children[i]);
    }
    p->state = 2; //black
}

int main() {
    string s1,s2;
    person* root = 0;
    cin >> n;
    while(n--) {
        cin>>s1>>s2;
        person *p1 = get(s1), *p2 = get(s2); 
        p1->children.push_back(p2);
        p2->parent = p1;
        if(root == 0)
            root = p1;
    }
    cin >> m;
    for(int i=0;i<m;++i) {
        cin>>s1>>s2;
        person *p1 = get(s1), *p2 = get(s2); 
        p1->querys.push_back(new query(p2,i));
        p2->querys.push_back(new query(p1,i));
    }

    dfs(root);

    for(int i=0;i<m;++i){
        cout << ans[i] << endl;
    }

    return 0;
}

在极端情况下,每次去查询黑色结点向上的第一个灰色结点,也会产生重复操作,降低效率。

利用并查集可以减少重复操作。

int find(int x)
{
    if(father[x]==x)
        return x;
    else 
        return father[x]=Find(father[x]);
}
在线算法

这样一个离线算法就出现了问题:如果只有一个人提出了询问,很难决定到底是针对这个询问就直接进行计算还是等待一定数量的询问一起计算。毕竟无论是一个询问还是很多个询问,使用离线算法都是只需要做一次深度优先搜索就可以了的。

那么问题就来了,如果每次计算都只针对一个询问进行的话,那么这样的算法事实上还不如使用最开始的朴素算法呢!但是如果每次要等上很多人一起的话,因为说不准什么时候才能够凑够人——所以事实上有可能要等上很久很久才能够进行一次计算,实际上也是很慢的!

有离线算法就有在线算法。

最近公共祖先其实就是这两个点连通路径上的那个折点。

树->线段

树的根节点开始进行深度优先搜索,每次经过某一个点——无论是从它的父亲节点进入这个点,还是从它的儿子节点返回这个点,都按顺序记录下来。这样就把一棵树转换成了一个数组。而找到树上两个节点的最近公共祖先,无非就是找到这两个节点最后一次出现在数组中的位置所囊括的一段区间中深度最小的那个点。

这就是一个很好的将树转换成数组来进行某些特殊算法的方法,而且你仔细看看就会发现转换出的数组的长度其实就是边数的2倍而已,也是O(n)的级别。

线段->树

target: 求线段一段区域的最小值

可以利用建立树来解决重复查找的问题

note: RMQ-ST算法也是一个解决方法。

#include <iostream>
#include <map>
#include <string>
#include <vector>
#include <math.h>

using namespace std;

#define min(x,y)  ( (x)<(y)?(x):(y) )

class person {
public:
    vector<person*> children;
    string name;
    int level;
    int loc;
    person* parent;
    person(string n) {
        name = n;
        parent = 0;
        loc = 0;
        level = 10000;
    }

};
int n,m;
map<string,person*> d;
map<int,person*> dloc;
const int maxLength = 6e5;
int loc = 0;
int level[19] = {2,4,8,
    16,32,64,
    128,256,512,
    1024,2024,4096,
    8192,16384,32768,
    65536,131072,262144,0};

person* get(string n){
    map<string,person*>::iterator it = d.find(n);
    if(it == d.end()) {
        d[n] = new person(n);
    }
    return d[n];
}
person* getfather(person* p){
    while(p->parent != 0){
        p = p->parent;
    }
    return p;
}

void dfs(person *p,int level = 0) {
    p->level = level;
    p->loc = loc;
    dloc[loc++] = p;
    for(int i=0;i<p->children.size();++i) {
        dfs(p->children[i],level+1);
        p->level = level;
        p->loc = loc;
        dloc[loc++] = p;

    }
}

person* getMinRegion_fast(int l,int r,int noLev = 0){
    int pMax = 10000;
    person* p;
    while(l < r){
        if(l%2 == 1){
            if(dloc[level[noLev]+l]->level < pMax){
                p = dloc[level[noLev]+l];
                pMax = dloc[level[noLev]+l]->level;
            }
            l++;
        }
        if(r%2 == 0){       
            if(dloc[level[noLev]+r]->level < pMax){
                p = dloc[level[noLev]+r];
                pMax = dloc[level[noLev]+r]->level;
            }
            r--;
        }
        l /= 2;
        r /= 2;
        noLev -= 1;
    }
    if(l == r){
        if(dloc[level[noLev]+l]->level < pMax){
            return dloc[level[noLev]+l];
        }
        else{
            return p;
        }
    }
    else{
        return p;
    }
}
void filltree(int noLev){   
    int i;
    int num = 0;
    num = pow(2.0,noLev);
    for(i=0;i<num;i=i+2){
        if (dloc.find(level[noLev]+i) != dloc.end()){
            if(dloc.find(level[noLev]+i+1) != dloc.end()){
                if(dloc[level[noLev]+i]->level <dloc[level[noLev]+i+1]->level){
                    dloc[level[noLev-1]+i/2] = dloc[level[noLev]+i];
                }
                else{
                    dloc[level[noLev-1]+i/2] = dloc[level[noLev]+i+1];
                }
            }
            else{
                dloc[level[noLev-1]+i/2] = dloc[level[noLev]+i];
            }
        }
    }
    if(noLev-1 == 0)return;
    filltree(noLev-1);
}
int main() {
    string s1,s2;
    person* root = 0;
    person *p1,*p2; 
    cin >> n;
    while(n--) {
        cin>>s1>>s2;
        p1 = get(s1);
        p2 = get(s2); 
        p1->children.push_back(p2);
        p2->parent = p1;
    }
    root = getfather(p1);


    dfs(root);

    int maxLev = 18;

    for(int i=18;i>0;i--){
        level[i-1] += level[i];
    }

    filltree(maxLev);

    cin >> m;

    for(int i=0;i<m;++i) {
        cin>>s1>>s2;
        p1 = get(s1);
        p2 = get(s2);
        if(p1->loc < p2->loc){
            cout<<getMinRegion_fast(p1->loc,p2->loc,maxLev)->name<<endl;
        }
        else{
            cout<<getMinRegion_fast(p2->loc,p1->loc,maxLev)->name<<endl;
        }
    }

    return 0;
}

引用:http://hihocoder.com/problemset/problem/1067
引用:http://hihocoder.com/problemset/problem/1069

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值