LCA小结

  1. LCA
  2. 在线算法 RMQ之ST加DFS
  3. 离线算法Tarjan加DFS
    以poj1330为例 题意:T组测试案例,m个节点,m-1条变,最后一个询问;问u,v的最近公共祖先;

学习资料:http://blog.csdn.net/shahdza/article/details/7689338
http://www.cnblogs.com/kuangbin/archive/2012/09/21/2697395.html

数组含义,摘抄于资料

注意LCAT(u, v)是在对T进行dfs过程当中在访问u和v之间离根结点最近的点。因此我们可以考虑树的欧拉环游过程u和v之间所有的结点,并找到它们之间处于最低层的结点。为了达到这个目的,我们可以建立三个数组:

E[1, 2*N-1] - 对T进行欧拉环游过程中所有访问到的结点;E[i]是在环游过程中第i个访问的结点
L[1,2*N-1] - 欧拉环游中访问到的结点所处的层数;L[i]是E[i]所在的层数
H[1, N] ,H[i] - 是E中结点i第一次出现的下标(任何出现i的地方都行,当然选第一个不会错)

假定H[u]<H[v](否则你要交换u和v)。可以很容易的看到u和v第一次出现的结点是E[H[u]..H[v]]。现在,我们需要找到这些结点中的最低层。为了达到这个目的,我们可以使用RMQ。因此 LCAT(u, v) = E[RMQL(H[u],H[v])] (记住RMQ返回的是索引),

在线算法:DFS+RMQ的ST算法

#include<iostream>
#include<cstdlib>
#include<cmath>
#include<algorithm>
#include<cstring>
#include<cstdio>
#include<queue>
#include<set>
#include<stack>
#define cl(a,b) memset(a,b,sizeof(a));
#define LL long long
#define P pair<int,int>
#define X first
#define Y second
#define pb push_back
using namespace std;
const int maxn=10010;
const int inf=9999999;
const int mod=100007;
int L[maxn<<1];///编号1-n,注意编号
struct ST{///RMQ的ST算法,返回最小元素的下标
    int mm[maxn<<1];//最高位1的位置mm[0]=-1,mm[1]=0,mm[2]=2;
    int dp[maxn<<1][20];
    void init(int n){
        mm[0]=-1;
        for(int i=1;i<=n;i++){
            dp[i][0]=i;
            mm[i]=(i&(i-1))==0?mm[i-1]+1:mm[i-1];
        }
        for(int j=1;j<=mm[n];j++){
            for(int i=1;i+(1<<j)-1<=n;i++){
                dp[i][j]=L[dp[i][j-1]]<L[dp[i+(1<<(j-1))][j-1]]?dp[i][j-1]:dp[i+(1<<(j-1))][j-1];
            }
        }
    }
    int query(int i,int j){
        if(i>j)swap(i,j);
        int k=mm[j-i+1];
        return L[dp[i][k]]<L[dp[j-(1<<k)+1][k]]?dp[i][k]:dp[j-(1<<k)+1][k];
    }
};
struct node{//边的结构,邻接表
    int to,next;
};
struct LCA{
    int n;
    int head[maxn<<1];
    int tol;
    node edge[maxn<<1];//边的相关数组

    int E[maxn<<1];//欧拉环游数组,i出现的顺序
    int H[maxn];//i第一次出现在E数组中的位置

    ST st;

    void init(int n){
        this->n=n;
        tol=0;
        memset(head,-1,sizeof(head));
    }
    void addedge(int u,int v){//加边
        edge[tol].to=v;
        edge[tol].next=head[u];
        head[u]=tol++;
        edge[tol].to=u;
        edge[tol].next=head[v];
        head[v]=tol++;
    }

    bool vis[maxn];//DFS标志数组
    int cnt;//访问的编号
    void dfs(int x,int level){
        vis[x]=true;
        E[++cnt]=x;
        L[cnt]=level;
        H[x]=cnt;
        for(int i=head[x];i!=-1;i=edge[i].next){
            int v=edge[i].to;
            if(vis[v])continue;
            dfs(v,level+1);
            E[++cnt]=x;
            L[cnt]=level;
        }
    }
    int query(int a,int b){
        return E[st.query(H[a],H[b])];
    }
    void solve(int root){
        cnt=0;
        memset(vis,false,sizeof(vis));
        dfs(root,0);
        st.init(n*2-1);//数组的长度,要注意
    }
}lca;
bool flag[maxn];
int main(){
    int T;
    scanf("%d",&T);
    while(T--){
        int n;
        scanf("%d",&n);
        memset(flag,false,sizeof(flag));
        lca.init(n);
        for(int i=1;i<n;i++){
            int u,v;
            scanf("%d%d",&u,&v);
            lca.addedge(u,v);
            flag[v]=true;
        }
        int root;
        for(int i=1;i<=n;i++)if(!flag[i]){
            root=i;break;
        }
        lca.solve(root);
        int l,r;
        scanf("%d%d",&l,&r);
        printf("%d\n",lca.query(l,r));
    }
    return 0;
}

离线算法:Tarjan算法,DFS+并查集 时间 O(n+q)线性复杂度,n是元素个数,q是询问次数

//伪代码
//parent为并查集,FIND为并查集的查找操作
//QUERY为询问结点对集合
//TREE为有根树
 Tarjan(u)//从根节点开始
      for each (u, v) in TREE    
          Tarjan(v)
          parent[v] = u
      visit[u] = true
      for each (u, v) in QUERY
          if visit[v]
              ans(u, v) = FIND(v)

离线算法的学习资料:http://noalgo.info/476.html

算法从根节点root开始搜索,每次递归搜索所有的子树,然后处理跟当前根节点相关的所有查询。
算法用集合表示一类节点,这些节点跟集合外的点的LCA都一样,并把这个LCA设为这个集合的祖先。当搜索到节点x时,创建一个由x本身组成的集合,这个集合的祖先为x自己。然后递归搜索x的所有儿子节点。当一个子节点搜索完毕时,把子节点的集合与x节点的集合合并,并把合并后的集合的祖先设为x。因为这棵子树内的查询已经处理完,x的其他子树节点跟这棵子树节点的LCA都是一样的,都为当前根节点x。所有子树处理完毕之后,处理当前根节点x相关的查询。遍历x的所有查询,如果查询的另一个节点v已经访问过了,那么x和v的LCA即为v所在集合的祖先。
其中关于集合的操作都是使用并查集高效完成。
算法的复杂度为,O(n)搜索所有节点,搜索每个节点时会遍历这个节点相关的所有查询。如果总的查询个数为m,则总的复杂度为O(n+m)。

POJ1330
基于伪代码实现的代码

#include<iostream>
#include<cstdlib>
#include<cmath>
#include<algorithm>
#include<cstring>
#include<cstdio>
#include<queue>
#include<set>
#include<stack>
#define cl(a,b) memset(a,b,sizeof(a));
#define LL long long
#define P pair<int,int>
#define X first
#define Y second
#define pb push_back
using namespace std;
const int maxn=10010;
const int inf=9999999;
const int mod=100007;

bool flag[maxn];//找根节点的数组
struct que{//查询结构体
    int q,index;
    que(){}
    que(int a,int b):q(a),index(b){}
};
vector<que> query[maxn];//存储查询
vector<int> tree[maxn];//存树
int ans[maxn];//存答案

struct Tarjan{//tarjan算法
    int  f[maxn];//并查集
    bool vis[maxn];//dfs标记数组
    void init(int n){//初始化操作
        for(int i=0;i<=n;i++){
            f[i]=i;
            vis[i]=false;
            flag[i]=false;
            tree[i].clear();
            query[i].clear();
        }
    }
    int Find(int x){//并查集查找函数,这是路径压缩的
        return f[x]==x?x:f[x]=Find(f[x]);
    }
    void dfs(int u){//dfs遍历,进行查询
        int n=tree[u].size();
        for(int i=0;i<n;i++){
            int v=tree[u][i];
            dfs(v);
            f[v]=u;
        }
        vis[u]=true;//遍历完节点u的所有子节点,就可以解答出与u有关的询问了
        n=query[u].size();
        for(int i=0;i<n;i++){
            int v=query[u][i].q;
            if(vis[v]){
                ans[query[u][i].index]=Find(v);
            }
        }
    }
    void solve(int root){
        dfs(root);
    }
}tar;
int main(){
    int T;
    scanf("%d",&T);
    while(T--){
        int n;
        scanf("%d",&n);

        tar.init(n);
        for(int i=1;i<n;i++){
            int u,v;
            scanf("%d%d",&u,&v);
            tree[u].pb(v);
            flag[v]=true;
        }

        int Q=1;
        for(int i=0;i<Q;i++){
            int u,v;
            scanf("%d%d",&u,&v);
            query[u].pb(que(v,i));
            query[v].pb(que(u,i));///注意查询交换
        }

        for(int i=1;i<=n;i++)if(!flag[i]){
            tar.solve(i);break;
        }

        for(int i=0;i<Q;i++){
            printf("%d\n",ans[i]);
        }
    }
    return 0;
}

再写一个,和资料里讲解的一样的,二者不同在于下面多了个ancestor数组,见注释的**部分

#include<iostream>
#include<cstdlib>
#include<cmath>
#include<algorithm>
#include<cstring>
#include<cstdio>
#include<queue>
#include<set>
#include<stack>
#define cl(a,b) memset(a,b,sizeof(a));
#define LL long long
#define P pair<int,int>
#define X first
#define Y second
#define pb push_back
using namespace std;
const int maxn=10010;
const int inf=9999999;
const int mod=100007;

bool flag[maxn];
struct que{
    int q,index;
    que(){}
    que(int a,int b):q(a),index(b){}
};
vector<que> query[maxn];
vector<int> tree[maxn];
int ans[maxn];

struct Tarjan{
    int  f[maxn];//并查集
    bool vis[maxn];//dfs标记数组
    int  ancestor[maxn];//祖先
    void init(int n){
        for(int i=0;i<=n;i++){
            f[i]=i;
            vis[i]=false;
            ancestor[i]=i;
            flag[i]=false;
            tree[i].clear();
            query[i].clear();
        }
    }
    int Find(int x){
        return f[x]==x?x:f[x]=Find(f[x]);
    }
    void Union(int x,int y){
        x=Find(x),y=Find(y);
        if(x!=y)f[y]=x;
    }
    void dfs(int u){
        int n=tree[u].size();
        for(int i=0;i<n;i++){
            int v=tree[u][i];
            dfs(v);
            Union(u,v);//*********
            ancestor[Find(u)]=u;//*********
        }
        vis[u]=true;
        n=query[u].size();
        for(int i=0;i<n;i++){
            int v=query[u][i].q;
            if(vis[v]){
                ans[query[u][i].index]=ancestor[Find(v)];//*********
            }
        }
    }
    void solve(int root){
        dfs(root);
    }
}tar;
int main(){
    int T;
    scanf("%d",&T);
    while(T--){
        int n;
        scanf("%d",&n);

        tar.init(n);
        for(int i=1;i<n;i++){
            int u,v;
            scanf("%d%d",&u,&v);
            tree[u].pb(v);
            flag[v]=true;
        }

        int Q=1;
        for(int i=0;i<Q;i++){
            int u,v;
            scanf("%d%d",&u,&v);
            query[u].pb(que(v,i));
            query[v].pb(que(u,i));///注意查询交换
        }

        for(int i=1;i<=n;i++)if(!flag[i]){
            tar.solve(i);break;
        }

        for(int i=0;i<Q;i++){
            printf("%d\n",ans[i]);
        }
    }
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值