LCA算法解析-Tarjan&倍增

LCA_Tarjan

LCA的Tarjan算法的时间复杂度为O(n+q)是一种离线算法,要用到并查集。
Tarjan算法基于dfs,在dfs的过程中,对于每个节点位置的询问做出相应的回答。
dfs的过程中,当一棵子树被搜索完成之后,就把他和他的父亲合并成同一集合;在搜索当前子树节点的询问时,如果该询问的另一个节点已经被访问过,那么该编号的询问是被标记了的,于是直接输出当前状态下,另一个节点所在的并查集的祖先;如果另一个节点还没有被访问过,那么就做下标记,继续dfs。
当然,暂时还没那么容易弄懂,所以建议结合下面的例子和标算来看看。

比如:8-1-(13,14),此时如果询问(13,14)的话,则(13,14)的LCA为1;如果没有相应的询问,则往上回溯,(13,14)的父亲都是1了,再往上就是8了。再DFS 8-4-6-(15,7),一样的,回溯时,15,7,4的LCA就成4了。

#include <cstring>
#include <cstdio>
#include <algorithm>
#include <cstdlib>
#include <cmath>
using namespace std;
const int N=40000+5;
struct Edge{
    int cnt,x[N],y[N],z[N],nxt[N],fst[N];
    void set(){
        cnt=0;
        memset(x,0,sizeof x);
        memset(y,0,sizeof y);
        memset(z,0,sizeof z);
        memset(nxt,0,sizeof nxt);
        memset(fst,0,sizeof fst);
    }
    void add(int a,int b,int c){
        x[++cnt]=a;
        y[cnt]=b;
        z[cnt]=c;
        nxt[cnt]=fst[a];
        fst[a]=cnt;
    }
}e,q;
int T,n,m,from,to,dist,in[N],rt,dis[N],fa[N],ans[N];
bool vis[N];
void dfs(int rt){
    for (int i=e.fst[rt];i;i=e.nxt[i]){
        dis[e.y[i]]=dis[rt]+e.z[i];
        dfs(e.y[i]);
    }
}
int getf(int k){
    return fa[k]==k?k:fa[k]=getf(fa[k]);
}
void LCA(int rt){
    for (int i=e.fst[rt];i;i=e.nxt[i]){
        LCA(e.y[i]);
        fa[getf(e.y[i])]=rt;
    }
    vis[rt]=1;
    for (int i=q.fst[rt];i;i=q.nxt[i])
        if (vis[q.y[i]]&&!ans[q.z[i]])
            ans[q.z[i]]=dis[q.y[i]]+dis[rt]-2*dis[getf(q.y[i])];
}
int main(){
    scanf("%d",&T);
    while (T--){
        q.set(),e.set();
        memset(in,0,sizeof in);
        memset(vis,0,sizeof vis);
        memset(ans,0,sizeof ans);
        scanf("%d%d",&n,&m);
        for (int i=1;i<n;i++)
            scanf("%d%d%d",&from,&to,&dist),e.add(from,to,dist),in[to]++;
        for (int i=1;i<=m;i++)
            scanf("%d%d",&from,&to),q.add(from,to,i),q.add(to,from,i);
        rt=0;
        for (int i=1;i<=n&&rt==0;i++)
            if (in[i]==0)
                rt=i;
        dis[rt]=0;
        dfs(rt);
        for (int i=1;i<=n;i++)
            fa[i]=i;
        LCA(rt);
        for (int i=1;i<=m;i++)
            printf("%d\n",ans[i]);
    }
    return 0;
}

LCA_倍增

LCA_倍增是LCA的在线算法,时间和空间复杂度分别是O((n+q)log n)和O(n log n)。
对于这个算法,我们从最暴力的算法开始:
①如果a和b深度不同,先把深度调浅,使他变得和浅的那个一样
②现在已经保证了a和b的深度一样,所以我们只要把两个一起一步一步往上移动,直到他们到达同一个节点,也就是他们的最近公共祖先了。

#include <cstring>
#include <cstdio>
#include <cstdlib>
#include <algorithm>
#include <cmath>
#include <vector>
using namespace std;
const int N=10000+5;
vector <int> son[N];
int T,n,depth[N],fa[N],in[N],a,b;
void dfs(int prev,int rt){
    depth[rt]=depth[prev]+1;
    fa[rt]=prev;
    for (int i=0;i<son[rt].size();i++)
        dfs(rt,son[rt][i]);
}
int LCA(int a,int b){
    if (depth[a]>depth[b])
        swap(a,b);
    while (depth[b]>depth[a])
        b=fa[b];
    while (a!=b)
        a=fa[a],b=fa[b];
    return a;
}
int main(){
    scanf("%d",&T);
    while (T--){
        scanf("%d",&n);
        for (int i=1;i<=n;i++)
            son[i].clear();
        memset(in,0,sizeof in);
        for (int i=1;i<n;i++){
            scanf("%d%d",&a,&b);
            son[a].push_back(b);
            in[b]++;
        }
        depth[0]=-1;
        int rt=0;
        for (int i=1;i<=n&&rt==0;i++)
            if (in[i]==0)
                rt=i;
        dfs(0,rt);
        scanf("%d%d",&a,&b);
        printf("%d\n",LCA(a,b));
    }
    return 0;
}

而实际上,一步一步往上移动太慢,我们可以做一个预处理:
fa[i][j]表示节点i往上走2^j次所到达的祖先,那么不难写出转移方程:
fa[i][0]=father[i],fa[i][j]=fa[fa[i][j-1]][j-1]
然后在求LCA的时候,有这样一个性质:(假设a和b深度一样)
设anst[x][y]为节点x网上走y步到达的祖先,对于一个k,如果anst[a][k]==anst[b][k],那么对于k‘(k‘>k),一定有anst[a][k‘]==anst[b][k‘];对于一个k,如果anst[a][k]!=anst[b][k],那么对于k‘(k‘<k),一定有anst[a][k‘]!=anst[b][k‘],而且LCA(a,b)=LCA(anst[a][k],anst[b][k])。
于是求法就渐渐的现行了:
1. 把a和b移到同一深度(设depth[x]为节点x的深度),假设depth[a]<=depth[b],所以我们的目的是把b向上移动i=(depth[b]-depth[a])层,那么,由于之前有预处理的fa数组,我们把i写成二进制形势,然后利用fa数组来在log n的复杂度中完成;
2. 寻找a和b的LCA下一层的两个祖先。利用之前的那个性质,再利用倍增,如果a和b的第2^k个祖先不是同一个,那么把a改为fa[a][k],b改为fa[b][k],k减1;否则直接k减1;当然在这之前要实现确定k的最大值,从大往小处理下去。最终的结果就是fa[a][0]或者fa[b][0]。
注意点:如果a和b在调节深度之后已经是同一个祖先的,那么直接返回a或者b。

 1 #include <cstring>
 2 #include <cstdio>
 3 #include <cstdlib>
 4 #include <algorithm>
 5 #include <cmath>
 6 #include <vector>
 7 using namespace std;
 8 const int N=10000+5;
 9 vector <int> son[N];
10 int T,n,depth[N],fa[N][17],in[N],a,b;
11 void dfs(int prev,int rt){
12     depth[rt]=depth[prev]+1;
13     fa[rt][0]=prev;
14     for (int i=1;(1<<i)<=depth[rt];i++)
15         fa[rt][i]=fa[fa[rt][i-1]][i-1];
16     for (int i=0;i<son[rt].size();i++)
17         dfs(rt,son[rt][i]);
18 }
19 int LCA(int a,int b){
20     if (depth[a]>depth[b])
21         swap(a,b);
22     for (int i=depth[b]-depth[a],j=0;i>0;i>>=1,j++)
23         if (i&1)
24             b=fa[b][j];
25     if (a==b)
26         return a;
27     int k;
28     for (k=0;(1<<k)<=depth[a];k++);
29     for (;k>=0;k--)
30         if ((1<<k)<=depth[a]&&fa[a][k]!=fa[b][k])
31             a=fa[a][k],b=fa[b][k];
32     return fa[a][0];
33 }
34 int main(){
35     scanf("%d",&T);
36     while (T--){
37         scanf("%d",&n);
38         for (int i=1;i<=n;i++)
39             son[i].clear();
40         memset(in,0,sizeof in);
41         for (int i=1;i<n;i++){
42             scanf("%d%d",&a,&b);
43             son[a].push_back(b);
44             in[b]++;
45         }
46         depth[0]=-1;
47         int rt=0;
48         for (int i=1;i<=n&&rt==0;i++)
49             if (in[i]==0)
50                 rt=i;
51         dfs(0,rt);
52         scanf("%d%d",&a,&b);
53         printf("%d\n",LCA(a,b));
54     }
55     return 0;
56 }

练习题

POJ1330
HDU2586
CodeVS2370
POJ1470
HDU4547

时间: 2024-11-08 17:35:20

LCA算法解析-Tarjan&倍增的相关文章

(转载)LCA问题的Tarjan算法

转载自:Click Here LCA问题(Lowest Common Ancestors,最近公共祖先问题),是指给定一棵有根树T,给出若干个查询LCA(u, v)(通常查询数量较大),每次求树T中两个顶点u和v的最近公共祖先,即找一个节点,同时是u和v的祖先,并且深度尽可能大(尽可能远离树根).LCA问题有很多解法:线段树.Tarjan算法.跳表.RMQ与LCA互相转化等.本文主要讲解Tarjan算法的原理及详细实现. 一 LCA问题 LCA问题的一般形式:给定一棵有根树,给出若干个查询,每个

LCA 算法学习 (最近公共祖先)poj 1330

poj1330 在求解最近公共祖先为问题上,用到的是Tarjan的思想,从根结点开始形成一棵深搜树,处理技巧就是在回溯到结点u的时候,u的子树已经遍历,这时候才把u结点放入合并集合中,这样u结点和所有u的子树中的结点的最近公共祖先就是u了,u和还未遍历的所有u的兄弟结点及子树中的最近公共祖先就是u的父亲结点.这样我们在对树深度遍历的时候就很自然的将树中的结点分成若干的集合,两个集合中的所属不同集合的任意一对顶点的公共祖先都是相同的,也就是说这两个集合的最近公共祖先只有一个.时间复杂度为O(n+q

hdu3078 建层次树+在线LCA算法+排序

题意:n个点,n-1条边构成无向树,每个节点有权,Q次询问,每次或问从a->b的最短路中,权第k大的值,/或者更新节点a的权, 思路:在线LCA,先dfs生成树0,标记出层数和fa[](每个节点的父亲节点).在对每次询问,走一遍一次公共祖先路上 的权,保持,快排.n*logn*q #include<iostream> //187MS #include<algorithm> #include<cstdio> #include<vector> using

[转]SURF算法解析

SURF算法解析 一.积分图像    积分图像的概念是由Viola和Jones提出的.积分图像中任意一点(i,j)的值为原图像左上角到任意点(i,j)相应的对焦区域的灰度值的总和,其数学公式如图1所示: 那么,当我们想要计算图片一个区域的积分,就只需计算这个区域的四个顶点在积分图像里的值,便可以通过2步加法和2步减法计算得出,其数学公式如下: 二.Hession矩阵探测器1.斑点检测    斑点:与周围有着颜色和灰度差别的区域.    在一个一维信号中,让它和高斯二阶导数进行卷积,也就是拉普拉斯

KMP串匹配算法解析与优化

朴素串匹配算法说明 串匹配算法最常用的情形是从一篇文档中查找指定文本.需要查找的文本叫做模式串,需要从中查找模式串的串暂且叫做查找串吧. 为了更好理解KMP算法,我们先这样看待一下朴素匹配算法吧.朴素串匹配算法是这样的,当模式串的某一位置失配时将失配位置的上一位置与查找串的该位置对齐再从头开始比较模式串的每一个位置.如下图所示. KMP串匹配算法解析 KMP串匹配算法是Knuth-Morris-Pratt算法的简称,KMP算法的思想就是当模式串的某一位置失配时,能不能将更前面的位置与查找串的该位

Android中锁屏密码算法解析以及破解方案

一.前言 最近玩王者荣耀,下载了一个辅助样本,结果被锁机了,当然破解它很简单,这个后面会详细分析这个样本,但是因为这个样本引发出的欲望就是解析Android中锁屏密码算法,然后用一种高效的方式制作锁机恶意样本.现在的锁机样本原理强制性太过于复杂,没意义.所以本文就先来介绍一下android中的锁屏密码算法原理. 二.锁屏密码方式 我们知道Android中现结单支持的锁屏密码主要有两种: 一种是手势密码,也就是我们常见的九宫格密码图 一种是输入密码,这个也分为PIN密码和复杂字符密码,而PIN密码

Mmseg中文分词算法解析

@author linjiexing 开发中文搜索和中文词库语义自己主动识别的时候,我採用都是基于mmseg中文分词算法开发的Jcseg开源project.使用场景涉及搜索索引创建时的中文分词.新词发现的中文分词.语义词向量空间构建过程的中文分词和文章特征向量提取前的中文分词等,整体使用下来,感觉jcseg是一个非常优秀的开源中文分词工具,并且可配置和开源的情况下,能够满足非常多场景的中文分词逻辑.本文先把jcseg使用到最主要的mmseg算法解析一下. 1. 中文分词算法之争 在分析mmseg

mwc飞控PID算法解析

0.说明 基于mwc2.3的pid算法解析,2.3中增加了一种新的pid算法,在此分别解析. P:比例 I:积分 D:微分 1.老版PID代码 代码大概在MultiWii.cpp的1350行上下. 1 if ( f.HORIZON_MODE ) prop = min(max(abs(rcCommand[PITCH]),abs(rcCommand[ROLL])),512); 2 3 // PITCH & ROLL 4 for(axis = 0; axis < 2; axis++) { 5 rc

地理围栏算法解析(Geo-fencing)

地理围栏算法解析 http://www.cnblogs.com/LBSer/p/4471742.html 地理围栏(Geo-fencing)是LBS的一种应用,就是用一个虚拟的栅栏围出一个虚拟地理边界,当手机进入.离开某个特定地理区域,或在该区域内活动时,手机可以接收自动通知和警告.如下图所示,假设地图上有三个商场,当用户进入某个商场的时候,手机自动收到相应商场发送的优惠券push消息.地理围栏应用非常广泛,当今移动互联网主要app如美团.大众.手淘等都可看到其应用身影. 图1 地理围栏示意图