LCA小结。

记录一个菜逼的成长。。

/***update 2017.5.11***/
对于查找一棵的LCA有种算法。
第一种,暴力搜索。
对于数据很小的题可以这样写。
先标记其中一个人的所有祖先,再从第二个人开始往上,向他的父亲搜索,直到搜索到第一个被标记的人,这个人就是他们两个人的最近公共祖先。

第二种,基于tarjan的离线算法。是将所有的查询都整理在一起,在去搜索判断,运用并查集找到最近公共祖先。
整理好所有的查询。

query[a].push_back(b);
query[b].push_back(a);

从根节点开始搜索。
如果有儿子一直往下搜索。
回溯合并集合。
回溯标记搜索过的。
对于回溯的每一个点,判断和这个点一对的另一个是否被标记过,如果是,他们的最近公共祖先就是另一个点的集合的当前根节点。如果不是,则不理这个点,继续搜索。

void unio(int x,int y)
{
    int fx = findr(x),fy = findr(y);
    if(fx != fy)pre[fy] = fx;
}
void tarjan(int u)
{
    for( int i = head[u]; ~i; i = edge[i].next ){
        int v = edge[i].to;
        tarjan(v);
        unio(u,v);//回溯合并
    }
    vis[u] = 1;
    for( int i = 0; i < query[u].size(); i++ ){
        if(vis[query[u][i]]){
            ancestor[u] = findr(query[u][i]);
        }
    }
}

具体可参考:http://hihocoder.com/contest/hiho15/problem/1

第三种,基于ST算法的在线算法。
简单介绍ST算法
是用来解决RMQ问题,即区间最值查询问题
dp[i][j] := 从下标i开始长度为2^j的最小值
显然dp[i][0] = a[i];
那么通过二分区间 ,以最小值为例
dp[i][j] = min(dp[i][j-1],dp[i+(1 << j - 1)][j-1];

void ST()
{
    for( int i = 0; i < n; i++ )        ///下标从0开始
        dp[i][0] = a[i];
    for( int j = 1; (1 << j) <= n; j++ ){
        for( int i = 0; i + (1 << j) - 1 < n; i++ ){
            dp[i][j] = min(dp[i][j - 1],dp[i + (1 << j - 1)][j - 1]);
            //查询最小值,改为max则最大值
        }
    }
}

LCA
先通过dfs得到点的序列号、每一个点的第一次出现的序列号和每一个序列号的深度.

void dfs(int x,int dep)
{
    first[x] = ind;
    depth[ind] = dep;
    a[ind++] = x;
    for( int i = head[x]; ~i; i = edge[i].next ){
        int v = edge[i].to;
        dfs(v,dep+1);
        depth[ind] = dep;
        a[ind++] = x;
    }
}

再对深度用ST算法计算
并同时保存当前最小深度的序列号

void ST(int n)
{
    for( int i = 0; i < n; i++ )        ///下标从0开始
    ///first 为深度  second 为 序列号即下标
        dp[i][0].first = depth[i],dp[i][0].second = i;
    for( int j = 1; (1 << j) <= n; j++ ){
        for( int i = 0; i + (1 << j) - 1 < n; i++ ){
            if(dp[i][j-1].first < dp[i+(1<<j-1)][j-1].first){
                dp[i][j].first = dp[i][j-1].first;
                dp[i][j].second = dp[i][j-1].second;
            }
            else {
                dp[i][j].first = dp[i+(1<<j-1)][j-1].first;
                dp[i][j].second = dp[i+(1<<j-1)][j-1].second;
            }
        }
    }
}

然后对first[u] 和 first[v]进行判断 ,查询。
如果first[u] < first[v] ,说明在访问到v点是u点已访问过(可联系第二种tarjan算法的并查集过程理解)。
只要知道在first[u]~first[v]之间的最小深度的序列号

int query(int u,int v)
{
    if(first[u] > first[v])swap(u,v);
    int w = first[v] - first[u] + 1;
    int k = (int)(log(w*1.0) / log(2.0));
    return dp[first[u]][k].first < dp[first[v]-(1<<k)+1][k].first ? dp[first[u]][k].second : dp[first[v]-(1<<k)+1][k].second;
}

第四种 倍增法求 LCA
相关知识

int deep[maxn];//每个点的深度
//ans[i][j] :=表示i点的2^j个祖先,,1倍(j==0)祖先是i点的父亲,2倍(j==1)祖先是i的父亲的父亲,以此类推
int anc[maxn][20];
//处理处每个点的深度和父亲
void dfs(int u,int pre,int d)
{
    deep[u] = d;
    for( int i = head[u]; ~i; i = edge[i].next ){
        int v = edge[i].to;
        if(v == pre)continue;
        anc[v][0] = u;
        dfs(v,u,d+1);
    }
}
void Init()
{
    for( int j = 1; (1<<j) <= n; j++ ){
        for( int i = 1; i <= n; i++ ){
            if(anc[i][j-1] != -1){
                //i的第2^j祖先就是i的第2^(j-1)祖先的第2^(j-1)祖先
                anc[i][j] = anc[anc[i][j-1]][j-1];
            }
        }
    }
}
/*
从深度大的节点上升至深度小的节点同层,如果此时两节点相同直接返回此节点,即lca。
否则,利用倍增法找到最小深度的anc[a][j]!=anc[b][j],此时他们的父亲anc[a][0]即lca。
*/
int lca(int a,int b)
{
    int i,j;
    if(deep[a] < deep[b])swap(a,b);
    for( i = 0; (1<<i) <= deep[a]; i++ );
    i--;
    //使a和b的深度相等  (这里运用了二进制的思想,比如,1,2,4可以表示出7以内的所有正整数
    for( j = i; j >= 0; j-- ){
        if(deep[a] - (1<<j) >= deep[b]){
            a = anc[a][j];
        }
    }
    if(a == b)return a;

    for( j = i; j >= 0; j-- ){
        if(anc[a][j] != -1 && anc[a][j] != anc[b][j]){
            a = anc[a][j];
            b = anc[b][j];
        }
    }
    return anc[a][0];
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值