从零开始的LCA(最近公共祖先)

清明在机房听学长讲树上差分时提到了 \(LCA\) 这个东西,就顺带着学习了一波。由于本人是个蒟蒻,为了避免出现看不懂自己写的 \(Blog\) 这种情况,文中涉及到的知识概念都会 概括性地 讲一下。

先甩个定义

\(LCA\) \((Lowest\) \(Common\) \(Ancestors)\)
即最近公共祖先,是指在一个树或者有向无环图中同时拥有 \(v\)\(w\) 作为后代的最深的节点,。在这里,我们定义一个节点也是其自己的后代,因此如果 \(v\)\(w\) 的后代,那么 \(w\) 就是 \(v\)\(w\) 的最近公共祖先。

——参考自 \(Wikipedia\)

再贴个板子题链接:LuoguP3379 【模板】最近公共祖先(LCA)

题意:给定一颗有根多叉树的节点数、直连边、根节点,求节点 \(a\) 和节点 \(b\)\(LCA\)

为方便,以下我们记节点 \(a\) 和节点 \(b\)\(LCA\)\(lca(a,b)\)

下面介绍四种求 \(LCA\) 的方法

  • 倍增法
  • \(RMQ\)
  • \(Tarjan\)
  • 树链剖分


倍增法

思路
先考虑最基本的想法,我们让它们中深度较大的节点往上“爬”,直至两节点深度相同,然后让它们同时往上“爬” ,相遇的节点既是 \(lca(a,b)\) 。这个想法的正确性显而易见,但如果我们是一个节点一个节点地“爬”的话,效率上行不通,所以我们考虑改进——让它们“爬”得快一点。倍增法,顾名思义,就是通过倍增“爬”的节点数来加快“爬”的速度。

具体实现
定义

  • \(dep[i]\) :节点 \(i\) 的深度。
  • \(far[i][j]\) :节点 \(i\) 往上“爬” \(2^j\) 个节点后到达的祖先节点。

这部分 \(DFS\) 一次预处理即可,比较简单,代码应该都看得懂,不讲。
不妨设 \(dep[a] \geq dep[b]\) ,下面分两步走:

  1. 为了让节点 \(a\) 往上“爬”到与节点 \(b\) 同一深度的位置,我们不断令 \(a=far[a][\log_2(dep[a]-dep[b])]\) ,直至 \(dep[a]=dep[b]\)。 如果此时 \(a=b\) ,说明 \(a\) 即为所求 \(LCA\) ,否则继续下一步。
  2. 每次取最大的 \(j\) 使得 \(far[a][j] \neq far[b][j]\) ,令 \(a=far[a][j]\) , \(b=far[b][j]\) ,直至 \(far[a][0]=far[b][0]\) 。此时,\(far[a][0]\) 即为所求 \(LCA\)

Code

#include <bits/stdc++.h>
using namespace std;

const int n_size=500005;
const int m_size=1e6+5;
const int bit_size=25;
int N,M,S,x,y,a,b;
int cnt,head[n_size],to[m_size],nxt[m_size];
int dep[n_size],far[n_size][bit_size],lg[n_size];

void add(int pre,int pos);
void dfs(int pre,int pos);
void prelg(void);
int lca(void);

int main(void){
    scanf("%d%d%d",&N,&M,&S);
    for(int i=1;i<N;i++){
        scanf("%d%d",&x,&y);
        add(x,y);
        add(y,x);
    }
    dfs(0,S);
    prelg();
    for(int i=0;i<M;i++){
        scanf("%d%d",&a,&b);
        printf("%d\n",lca());
    }
    return 0;
}

inline void add(int pre,int pos){
    to[++cnt]=pos;
    nxt[cnt]=head[pre];
    head[pre]=cnt;
}

void dfs(int pre,int pos){
    dep[pos]=dep[pre]+1;
    far[pos][0]=pre;
    for(int i=1;(1<<i)<=dep[pos];i++)   far[pos][i]=far[far[pos][i-1]][i-1];
    for(int i=head[pos];i>0;i=nxt[i])
        if(to[i]!=pre)    dfs(pos,to[i]);
}

void prelg(void){
    for(int i=1;i<=N;i++)   lg[i]=lg[i-1]+(1<<(lg[i-1]+1)==i);
}

int lca(void){
    if(dep[a]<dep[b])   swap(a,b);
    while(dep[a]>dep[b])    a=far[a][lg[dep[a]-dep[b]]];
    if(a==b)    return a;
    for(int i=lg[dep[a]];i>=0;i--)
        if(far[a][i]!=far[b][i]){
            a=far[a][i];
            b=far[b][i];
        }
    return far[a][0];
}


\(RMQ\)

甩个定义

\(RMQ\) \((Range\) \(Minimum\) \(Query)\)
即范围最值查询,是针对数据集的一种条件查询。若给定一个数组 \(A[1,n]\) ,范围最值查询指定一个范围条件 \(i\)\(j\) ,要求取出 \(A[i,j]\) 中最大/小的元素。通常情况下,数组 \(A\) 是静态的,即元素不会变化,例如插入、删除和修改等,而所有的查询是以在线的方式给出的,即预先并不知道所有查询的参数。

——参考自 \(Wikipedia\)

\(RMQ\) 算法主要借助于 \(ST\)
再甩一个定义

\(ST\) \((Sparse\) \(Table)\)
设要查询的静态数组为 \(A[1,n]\) ,创建一个二维数组 \(ST[1,N][0,log_2N]\) 。在这个数组中 \(ST[i][j]\) 表示 \(A[i,i+2^j-1]\) 中的最大/小值。

——参考自 \(Wikipedia\)

看完定义是不是感觉 \(ST\) 表和倍增法中的数组 \(far\) 很像?实际上 \(ST\) 表在这里所起的作用确实和数组 \(far\) 是类似的。

前置知识

  • 树的 \(DFS\) 序:遍历一棵树时节点的访问顺序。
  • 树的欧拉序:遍历一棵树时途径节点所成的序列。

树的 \(DFS\) 序和欧拉序的联系:\(DFS\) 序就是去重的欧拉序(每个节点只保留首次出现的那个)。

思路
遍历树时,某个时刻会首次访问 \(lca(a,b)\) ,接着会遍历子树,不妨设先遍历的是节点 \(a\) 所在的子树,那么从首次访问节点 \(a\) 开始算起,接下来访问完节点 \(a\) 的子节点后会一路回溯到 \(lca(a,b)\) ,遍历完其他无关子树后接着再遍历节点 \(b\) 所在的子树,最后一路往下访问直到首次访问节点 \(b\) 为止。
记住这个过程:首次访问节点 \(a\) \(\rightarrow\) 回溯到 \(lca(a,b)\) \(\rightarrow\) 首次访问节点 \(b\)
这个过程对应的欧拉序涉及的所有节点中,\(lca(a,b)\) 是深度最小的节点。那么,求 \(lca(a,b)\) ,只需求欧拉序的这一段中深度最小的节点即可。

具体实现
定义

  • \(num[i]\) :欧拉序中第 \(i\) 个节点。
  • \(fap[i]\) :欧拉序中节点 \(i\) 首次出现的位置。
  • \(dep[i]\) :欧拉序中第 \(i\) 个节点的深度。
  • \(st[i][j]\)\(dep[i]\) 最小值 \(ST\) 表,为了方便,我们存的是值的索引而不是值本身,即 \(st[i][j]\) 表示 \(dep[i,i+2^j-1]\) 中最小值的位置。

前面的三个数组 \(DFS\) 一次预处理,不讲。
\(st[i][j]\) 需要单独 \(DP\) 预处理,这里给出状态转移方程:

\(x=st[i][j-1]\)\(y=st[i+2^{j-1}-1]][j-1]\) ,则有
\[st[i][j]=\begin{cases}
x & dep[x]<dep[y] \y & dep[x] \geq dep[y]
\end{cases}\]

边界条件是 \(st[i][0]=i\) ,比较好理解,不细讲。
最后是 \(lca(a,b)\) 的求法:
不妨设 \(fap[a] \leq fap[b]\) ,我们只需找到 \(ST\) 表对 \([fap[a]\),\(fap[b]]\) ,即我们所要的欧拉序段上的最小覆盖,即可利用 \(ST\) 表快速求出这一段中深度最小的节点。

\(bit=\log_2(fap[b]-fap[a]+1)\) ,最小覆盖显然就是 \([fap[a],fap[a]+2^{bit}-1]\)\([fap[b]-2^{bit}+1,fap[b]]\) ,即:

\(x=st[fap[a]][bit]\)\(y=st[fap[b]-2^{bit}+1][bit]\) ,则有

\[lca(a,b)=\begin{cases}
x & dep[x]<dep[y] \y & dep[x] \geq dep[y]
\end{cases}\]

Code

#include <bits/stdc++.h>
using namespace std;

const int n_size=500005;
const int m_size=1e6+5;
const int bit_size=25;
int N,M,S,x,y,a,b;
int cnt,head[n_size],to[m_size],nxt[m_size];
int node,num[m_size],fap[n_size],dep[m_size];
int lg[m_size],st[m_size][bit_size];

void add(int pre,int pos);
void dfs_init(int pos,int predep);
void st_init(void);
int lca(void);
void prelg(void);

int main(void){
    scanf("%d%d%d",&N,&M,&S);
    for(int i=1;i<N;i++){
        scanf("%d%d",&x,&y);
        add(x,y);
        add(y,x);
    }
    dfs_init(S,0);
    st_init();
    for(int i=0;i<M;i++){
        scanf("%d%d",&a,&b);
        printf("%d\n",lca());
    }
    return 0;
}

inline void add(int pre,int pos){
    to[++cnt]=pos;
    nxt[cnt]=head[pre];
    head[pre]=cnt;
}

void dfs_init(int pos,int predep){
    fap[pos]=++node;
    num[node]=pos;
    dep[node]=predep+1;
    for(int i=head[pos];i>0;i=nxt[i])
        if(fap[to[i]]==0){
            dfs_init(to[i],dep[node]);
            num[++node]=pos;
            dep[node]=predep+1;
        }
}

void st_init(void){
    prelg();
    for(int i=1;i<=node;i++)   st[i][0]=i;
    for(int i=1;i<=lg[node];i++){
        int upp=node-(1<<i)+1;
        for(int j=1;j<=upp;j++){
            int x=st[j][i-1],y=st[j+(1<<(i-1))][i-1];
            st[j][i]=dep[x]<dep[y]?x:y;
        }
    }
}

int lca(void){
    if(fap[a]>fap[b])   swap(a,b);
    int inc=fap[b]-fap[a]+1;
    int sta=st[fap[a]][lg[inc]];
    int stb=st[fap[b]-(1<<lg[inc])+1][lg[inc]];
    if(dep[sta]<dep[stb])   return num[sta];
    return num[stb];
}

void prelg(void){
    for(int i=1;i<=node;i++)    lg[i]=lg[i-1]+(1<<(lg[i-1]+1)==i);
}


\(Tarjan\)

前置知识

  • 并查集 \((DSU)\)

应该都会吧
还是简单提一下吧~
甩定义

\(DSU\) \((Disjoint\) \(Sets\) \(Union)\)
即并查集,是一种树型的数据结构,用于处理一些不交集 \((Disjoint\) \(Sets)\) 的合并及查询问题。
有一个联合-查找算法 \((Union\) - \(Find\) \(Algorithm)\) 定义了两个用于此数据结构的操作:
\(Find\):确定元素属于哪一个子集。它可以被用来确定两个元素是否属于同一子集。
\(Union\):将两个子集合并成同一个集合。

——参考自 \(Wikipedia\)

先看定义,知道 \(Find\)\(Union\) 操作是怎么回事再往下看,以下记 \(find(x)\) 为节点 \(x\) 所在集合的头,\(far(x)\) 为节点 \(x\) 在并查集中的父节点,\(marge(a,b)\) 为合并节点 \(a\) 和节点 \(b\) ,那么我们有

\[find(x)=\begin{cases}
x & x=far(x) \far(x)=find(far(x)) & x \neq far(x)
\end{cases}\]

这里实际上就是一个递归,画画图就懂了~

\[marge(a,b):far(find(a))=find(b)\]

这个也简单~

思路
先引用知乎上《如何理解 \(Tarjan\)\(LCA\) 算法?》这一问题下高赞回答的一段话

一个熊孩子 \(Link\) 从一棵有根树的最左边最底下的结点灌岩浆,\(Link\) 表示很讨厌这种倒着长的树。岩浆会不断的注入,直到注满整个树…如果岩浆灌满了一棵子树,\(Link\) 发现树的另一边有一棵更深的子树,\(Link\) 会先去将那棵子树灌满。岩浆只有在迫不得已的情况下才会向上升高,找到一个新的子树继续注入。机 \((yu)\)\((chun)\)\(Link\) 发现了找 \(LCA\) 的好方法,即如果两个结点都被岩浆烧掉时,他们的 \(LCA\) 即为那棵子树上岩浆最高的位置。

注意这段话中“如果两个结点都被岩浆烧掉时”这一句,实际上就是当节点 \(a\)\(b\) 都恰被访问过的时候。看完是不是想到了什么?是不是感觉和 \(RMQ\)\(LCA\) 的思路差不多?没错,思路本质上是一样的,但实现则很不一样,\(Tarjan\)\(LCA\) 的过程要更为巧妙。先明确一件事,当节点 \(a\)\(b\) 都恰被访问过时,我们还没有最终回溯到“岩浆最高的位置”,即 \(lca(a,b)\) ,记住这个这个性质。

遍历树时,某个时刻会首次访问 \(lca(a,b)\) ,接着会遍历子树,不妨设先遍历的是节点 \(a\) 所在的子树,我们每访问完一个节点及其子树并最终回溯到这个节点时,将这个节点标记为已访问,并把它和它的父节点用 \(Union\) 操作合并起来,令这个集合的祖先是它的父节点。当首次访问节点 \(a\) \(\rightarrow\) 回溯到 \(lca(a,b)\) 这个过程结束后,节点 \(a\) 所在子树上所有节点都已经和 \(lca(a,b)\) 合并成了一个集合,因为 \(lca(a,b)\) 还没有最终回溯,集合的祖先就一定是是 \(lca(a,b)\) ,接着一直到首次访问节点 \(b\) ,此时我们注意到节点 \(a\) 标记为已访问,亦即已与 \(lca(a,b)\) 合并,那么就可以判断 \(a\) 所在集合的祖先即 \(lca(a,b)\) 。注意,从这里可以看出,\(Tarjan\)\(lca(a,b)\) 需要我们预先知道询问的节点 \(a\) 和节点 \(b\) ,也就是说这是一个离线算法,我们要预处理询问。

具体实现
定义

  • \(ans[i]\):第 \(i\) 个询问的答案。
  • \(query[i]\) :与节点 \(i\) 有关的询问,除了存储另一个节点以外还要存储这是第几个询问,且与节点 \(i\) 有关的询问可能不止一个,故最好用 \(pair\) 类型的 \(vector\) 实现,这里令 \(first\) 为另一个节点,\(second\) 为询问的编号。
  • \(anc[i]\):以节点 \(i\) 为头的集合的祖先。
  • \(vis[i]\):节点 \(i\) 是否已访问。

\(DFS\) 遍历树,搜索到某一节点 \(i\) 时,先着搜索节点 \(i\) 的所有子节点,对于节点 \(i\) 的某一子节点 \(j\),搜索节点 \(j\)\(marge(i,j)\) ,并令 \(anc[find(j)]=i\),搜索完所有子节点回溯到节点 \(i\) 时,令 \(vis[i]=true\) ,接着遍历 \(query[i]\) 看有无已访问的节点,若有已访问的节点 \(query[i][j].first\) ,则 \(ans[query[i][j].second]=anc[find(query[i][j].first)]\)

实际上,我们每次令集合的头为当前节点的父节点就可以不使用数组 \(anc\) ,但有些优化过的并查集(比如我在这里用的按树高合并的并查集)不支持这样操作,所以这里还是选择使用这个数组,这样代码也更清晰。

Code

#include <bits/stdc++.h>
using namespace std;
#define fir first
#define sec second
#define mp make_pair
typedef pair<int,int> pii;

const int n_size=500005;
const int m_size=1e6+5;
int N,M,S,x,y,a,b,ans[n_size];
vector<pii> query[n_size];
int cnt,head[n_size],to[m_size],nxt[m_size];
int far[n_size],rk[n_size],anc[n_size];
bool vis[n_size];

void add(int pre,int pos);
void lca(int pos,int pre);
void dsu_init(void);
void marge(int x,int y);
int find(int x);

int main(void){
    scanf("%d%d%d",&N,&M,&S);
    for(int i=1;i<N;i++){
        scanf("%d%d",&x,&y);
        add(x,y);
        add(y,x);
    }
    for(int i=1;i<=M;i++){
        scanf("%d%d",&a,&b);
        query[a].push_back(mp(b,i));
        query[b].push_back(mp(a,i));
    }
    dsu_init();
    lca(S,0);
    for(int i=1;i<=M;i++)   printf("%d\n",ans[i]);
    return 0;
}

inline void add(int pre,int pos){
    to[++cnt]=pos;
    nxt[cnt]=head[pre];
    head[pre]=cnt;
}

void dsu_init(void){
    for(int i=1;i<=N;i++)   far[i]=i;
}

void lca(int pos,int pre){
    for(int i=head[pos];i>0;i=nxt[i])
        if(to[i]!=pre&&vis[to[i]]==false){
            lca(to[i],pos);
            marge(to[i],pos);
            anc[find(to[i])]=pos;
        }
    vis[pos]=true;
    int upp=query[pos].size();
    for(int i=0;i<upp;i++){
        int x=query[pos][i].fir,y=query[pos][i].sec;
        if(ans[y]==0&&vis[x]==true) ans[y]=anc[find(x)];
    }
}

void marge(int x,int y){
    x=find(x);
    y=find(y);
    if(x==y)    return;
    if(rk[x]<rk[y]) far[x]=y;
    else{
        far[y]=x;
        if(rk[x]==rk[y])    rk[x]++;
    }
}

int find(int x){
    if(x==far[x])   return x;
    return far[x]=find(far[x]);
}


树链剖分

刚写完板子顺便温习一下!~
其实没有想象中的那么难~
这个人已经忘记了第一次写调了两三个小时还是写炸了的事实
之前不会树剖的可以借这个问题学习一下~

树链剖分
指一种对树进行划分的算法,它先通过轻重边剖分将树分为多条链,保证每个点属于且只属于一条链,然后再通过数据结构(树状数组 \((BIT)\)、二叉搜索树 \((BST)\)、伸展树 \((Splay)\)、线段树 \((Segment Tree)\) 等)来维护每一条链。

——参考自 \(Baidupedia\)

先引入几个概念

  • 重儿子:父节点的节点数最多的子树的根节点。
  • 轻儿子:不是重儿子的子节点。
  • 重边:连接父节点和重儿子的边。
  • 轻边:连接父节点和轻儿子的边。
  • 重链:重边组成的链。
  • 轻链:轻边组成的链。

思路
我们要做的就是把数分成重链和轻链。然后就好办了,分两种情况:

若节点 \(a\) 和节点 \(b\) 在同一条链上,那么深度较小的即是 \(lca(a,b)\)
否则,不断令链端深度较大的节点为它的链端的父节点,直至两节点在同一条链上,此时便转换为了第一种情况。

实现
定义

  • \(dep[i]\):节点 \(i\) 的深度。
  • \(siz[i]\):以节点 \(i\) 为根的树的节点数。
  • \(far[i]\): 节点 \(i\) 的父节点。
  • \(son[i]\):节点 \(i\) 的重儿子。
  • \(top[i]\):节点 \(i\) 所在链的链端。

用两次 \(DFS\) 预处理这些东西,树剖就算做完了~第一次 \(DFS\) 除数组 \(top\) 外都预处理了,这部分正常 \(DFS\) 遍历树即可,不细讲。重点是 \(top\) 的预处理,我们搜索到一个节点时,如果该节点不为叶节点,那么重儿子要和轻儿子区分开来搜索!知道这一点后代码应该也能 很简单地 写出来叭qwq

接着求 \(lca(a,b)\) ,把思路翻译过来,很简单~

\(top[a]=top[b]\) ,设 \(dep[a] \leq dep[b]\) ,则 \(lca(a,b)=a\)
否则,设每次操作后两节点中链端深度较大的节点为 \(k\) ,则不断令 \(k=far[top[k]]\) ,直至 \(top[a]=top[b]\) ,此时便转换为了第一种情况。

Code

#include <bits/stdc++.h>
using namespace std;

const int n_size=500005;
const int m_size=1e6+5;
int N,M,S,x,y,a,b;
int cnt,head[n_size],to[m_size],nxt[m_size];
int dep[n_size],siz[n_size],far[n_size],son[n_size],top[n_size];

void add(int pre,int pos);
void dfs_init_fir(int pos);
void dfs_init_sec(int pos,int tpos);
int lca(void);

int main(void){
    scanf("%d%d%d",&N,&M,&S);
    for(int i=1;i<N;i++){
        scanf("%d%d",&x,&y);
        add(x,y);
        add(y,x);
    }
    dfs_init_fir(S);
    dfs_init_sec(S,S);
    for(int i=0;i<M;i++){
        scanf("%d%d",&a,&b);
        printf("%d\n",lca());
    }
    return 0;
}

inline void add(int pre,int pos){
    to[++cnt]=pos;
    nxt[cnt]=head[pre];
    head[pre]=cnt;
}

void dfs_init_fir(int pos){
    dep[pos]=dep[far[pos]]+1;
    siz[pos]=1;
    int son_siz=0;
    for(int i=head[pos];i>0;i=nxt[i])
        if(to[i]!=far[pos]){
            far[to[i]]=pos;
            dfs_init_fir(to[i]);
            siz[pos]+=siz[to[i]];
            if(siz[to[i]]<=son_siz) continue;
            son[pos]=to[i];
            son_siz=siz[to[i]];
        }
}

void dfs_init_sec(int pos,int tpos){
    top[pos]=tpos;
    if(son[pos]>0)  dfs_init_sec(son[pos],tpos);
    for(int i=head[pos];i>0;i=nxt[i])
        if(to[i]!=far[pos]&&to[i]!=son[pos])    dfs_init_sec(to[i],to[i]);
}

int lca(void){
    while(top[a]!=top[b])
        if(dep[top[a]]<dep[top[b]]) b=far[top[b]];
        else    a=far[top[a]];
    return dep[a]<dep[b]?a:b;
}


\(LCA\) 的解法就这么 愉快地 讲完了呢~
我们小结一下四种解法的特点

  1. 倍增法:好理解,也好实现,较快~
  2. \(RMQ\) :就你最慢了,哼唧~ 可能是我写得菜
  3. \(Tarjan\) :意外地挺好写,嘤~
  4. 树链剖分:虽然很多人说没必要用这个写,但评测姬显示这个是最快的呢~ 就求 \(LCA\) 这个问题来说,代码量也只比倍增法稍多一点而已嘤~ 反正树剖天下第一!!!

原文地址:https://www.cnblogs.com/MakiseVon/p/10699322.html

时间: 2024-10-21 09:39:22

从零开始的LCA(最近公共祖先)的相关文章

lca 最近公共祖先

http://poj.org/problem?id=1330 1 #include<cstdio> 2 #include<cstring> 3 #include<algorithm> 4 #define mt(a,b) memset(a,b,sizeof(a)) 5 using namespace std; 6 const int inf=0x3f3f3f3f; 7 class LCA { ///最近公共祖先 build_o(n*logn) query_o(1) 8 t

Codeforces Round #362 (Div. 2) C. Lorenzo Von Matterhorn LCA(最近公共祖先)

C. Lorenzo Von Matterhorn time limit per test 1 second memory limit per test 256 megabytes input standard input output standard output Barney lives in NYC. NYC has infinite number of intersections numbered with positive integers starting from 1. Ther

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

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

笔记:LCA最近公共祖先 Tarjan(离线)算法

LCA最近公共祖先 Tarjan他贱(离线)算法的基本思路及其算法实现 本文是网络资料整理或部分转载或部分原创,参考文章如下: https://www.cnblogs.com/JVxie/p/4854719.html http://blog.csdn.net/ywcpig/article/details/52336496 https://baike.baidu.com/item/最近公共祖先/8918834?fr=aladdin 最近公共祖先简称LCA(Lowest Common Ancesto

连通分量模板:tarjan: 求割点 &amp;&amp; 桥 &amp;&amp; 缩点 &amp;&amp; 强连通分量 &amp;&amp; 双连通分量 &amp;&amp; LCA(最近公共祖先)

PS:摘自一不知名的来自大神. 1.割点:若删掉某点后,原连通图分裂为多个子图,则称该点为割点. 2.割点集合:在一个无向连通图中,如果有一个顶点集合,删除这个顶点集合,以及这个集合中所有顶点相关联的边以后,原图变成多个连通块,就称这个点集为割点集合. 3.点连通度:最小割点集合中的顶点数. 4.割边(桥):删掉它之后,图必然会分裂为两个或两个以上的子图. 5.割边集合:如果有一个边集合,删除这个边集合以后,原图变成多个连通块,就称这个点集为割边集合. 6.边连通度:一个图的边连通度的定义为,最

[转]LCA 最近公共祖先

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

(转)Tarjan应用:求割点/桥/缩点/强连通分量/双连通分量/LCA(最近公共祖先)

本文转载自:http://hi.baidu.com/lydrainbowcat/item/f8a5ac223e092b52c28d591c 作者提示:在阅读本文之前,请确保您已经理解并掌握了基本的Tarjan算法,不会的请到http://hi.baidu.com/lydrainbowcat/blog/item/42a6862489c98820c89559f3.html阅读.   基本概念:   1.割点:若删掉某点后,原连通图分裂为多个子图,则称该点为割点. 2.割点集合:在一个无向连通图中,如

poj1330 lca 最近公共祖先问题学习笔记

首先推荐两个博客网址: http://dongxicheng.org/structure/lca-rmq/ http://scturtle.is-programmer.com/posts/30055.html [转]tarjan算法的步骤是(当dfs到节点u时): 1 在并查集中建立仅有u的集合,设置该集合的祖先为u 1 对u的每个孩子v:    1.1 tarjan之    1.2 合并v到父节点u的集合,确保集合的祖先是u 2 设置u为已遍历 3 处理关于u的查询,若查询(u,v)中的v已遍

LCA最近公共祖先

Nearest Common Ancestors Description A rooted tree is a well-known data structure in computer science and engineering. An example is shown below: In the figure, each node is labeled with an integer from {1, 2,...,16}. Node 8 is the root of the tree.

LCA 最近公共祖先 小结

以poj 1330为例,对LCA的3种常用的算法进行介绍,分别为 1. 离线tajian 2. 基于倍增法的LCA 3. 基于RMQ的LCA 1. 离线tajian /*poj 1330 Nearest Common Ancestors 题意: 给出一棵大小为n的树和一个询问(u,v), 问(u,v)的最近公共祖先. 限制: 2 <= n <= 10000 思路: 离线tajian */ #include<iostream> #include<cstdio> #incl