tarjan知识点总结
一、tarjan求有向图的强连通分量
1.预备知识:
1.强连通:有向图中,设两个点 a b ,由a有一条路可以走到b,由b又有一条路可以走到a,我们就叫这两个顶点(a,b)强连通。
2.强连通分量(strongly connected components):在一个有向图G中,有一个子图,这个子图每2个点都满足强连通,我们就叫这个子图叫做 强连通分量。
2.tarjan算法流程
tarjan是基于深度优先遍历的,在求解过程中,主要涉及两个数组的维护,dfn和low。
dfn:代表的是该节点被搜索到的次序。
low:代表的是该节点能够通过次序更大的节点所能到达的次序较小节点的较小值。
在遍历过程中,使用栈结构来存储已经遍历的节点。假设当前遍历的节点为u,则遍历和u相连的所有边,假设另一个点位v,则有如下的low更新方式。
1.如果v点没有被遍历,即对应这dfn[v]的值为0,则递归调用tarjan(v),并使用low[v]来对low[u]来进行更新,即u点通过v点所能到达的次序最小的值。
2.如果v点已经被遍历了,即dfn的值非0,则此时v点位于栈中,结合low[u]的定义,使用dfn[v]来更新low[v],即u点通过自身所能到达v点。
当节点u遍历结束后,比较low[u]和dfn[u]的值,若相等,则将栈中的数据依次出栈,直到u点,由此可以获得一个强连通分量。
我们使用belong数组来标记图中的点属于哪一个强连通分量。
3.算法模板
//图的存储结构
struct edge {
int from, to, next;
};
vector<edge> E;
int h[NMAX];
//tarjan变量
stack<int > S;
int dfn[NMAX], low[NMAX],belong[NMAX];
void tarjan(int x) {
low[x] = dfn[x] = ++tot;
//标记vis
vis[x] = 1;
S.push(x);
//遍历x联通的每一个点
for (int i = h[x]; i != -1; i = E[i].next) {
if (!dfn[E[i].to]) {
tarjan(E[i].to);
low[x] = min(low[x], low[E[i].to]);
}
else if (dfn[E[i].to]) low[x] = min(low[x], dfn[E[i].to]);
}
//进行强联通分量的查找
if (low[x] == dfn[x]) {
++id;
int now = 0;
while (now != x) {
now = S.top();
S.pop();
belong[now] = id;
}
}
}
4.例题
二、tarjan求无向图的双连通分量
1.预备知识
双连通分量的定义:
(1).在一个无向图中,若任意两点间至少存在两条“点不重复”的路径,则说这个图是点双连通的(简称双连通,biconnected)。通俗来讲,是指不存在割点。在一个无向图中,点双连通的极大子图称为点双连通分量(简称双连通分量,Biconnected Component,BCC)。
点双连通分量的特点:
a. 该连通分量的点在同一简单环 该连通分量没有桥。
b.一个割点可以多个点连通分量
(2)在一个无向图中,若任意两点间至少存在两条“边不重复”的路径,则说这个图是边双连通的。通俗来讲,是指不存在桥。类似地,称为边双连通分量。
边双连通分量的特点:
a.任意一条边至少包含在一个简单环。
b.连通分量里没有桥。
c.割点只属于一个边双连通分量
d.两个边双连通分量最多只有一条边,且必为桥。进一步地,所有边双与桥可抽象为一棵树结构。
2.算法思想
(1)边双
对于无向图,tarjan求解双连通分量的过程和tarjan求解有向图的强连通分量的过程是一样的,但是加入了v不能是u的父节点(注意:父节点这个含义是在dfs的搜索树中)这个条件。同时,这个过程和tarjan求解桥的过程在本质上是一致的。
(2)点双:
同样的,在求解过程中需要注意加入了v不能是u的父节点(注意:父节点这个含义是在dfs的搜索树中)这个条件。对于点双的求解,仅仅只需要判断何时出现了割点,一旦出现了割点,就将当前栈中的元素出栈,直到该割点。需要注意的是,由于一个割点属于不止一个点双,因此割点不出栈。
3.算法模板
(1)边双
void tarjan(int x, int fa) {
dfn[x] = low[x] = ++tot;
S.push(x);
for (int i = h[x]; i != -1; i = E[i].next) {
int to = E[i].to;
if (!dfn[to]) {
tarjan(to, x);
low[x] = min(low[x], low[to]);
//用来判断是否为割边
if (low[to] == dfn[to]) E[i].is = 1;
}
else if (to != fa) low[x] = min(low[x], dfn[to]);
}
if (dfn[x] == low[x]) {
int cur = -1;
while (cur!=x) {
cur = S.top();
S.pop();
belong[cur] = connect;
}
connect++;
}
}
(2)点双
void tarjan(int u,int fa){
low[u]=dfn[u]=++Time;
st[++Top]=u;//依次进栈
for(int i=head[u];i;i=e[i].next){
int v=e[i].to;
if(!dfn[v]){
tarjan(v,u);
low[u]=min(low[u],low[v]);
if(dfn[u]<=low[v]){//如果u为割点,点双缩点
++num; //num表示第几个点双区域(一个图可能存在多个点双)
while(st[top+1]!=v){//从栈顶到v依次出栈
int w=s[top--];//去栈顶并退栈
dcc[num].push_back(w);//节点v属于编号为num的点双
}
dcc[num].push_back(u);//u可以在多个dcc,所以不出栈
}
}
else if(v!=fa){
low[u]=min(low[u],dfn[v]);
}
}
}
4.例题
三、tarjan求无向图的割点和桥
1.预备知识:
在无向图中,如果删除无向图中的某个点会使无向图的连通分量数增多,则把这个点称为割点。类似地,如果删除无向图中的某条边会使无向图的连通分量数增多,则把这个点称为割边或桥。
2.算法思想:
(注:该节图片均来自该链接)
首先,需要说明的是,以下所有的图对应的并非原图,而是DFS过程中生成的搜索树,因此祖先和儿子的概念是在搜索树的前提下的。
其次,在无向图中,如果存在一条从a到b边,则必然存在一条从b到a的边,而强连通的定义要求b不能通过b到a的边回溯到a,因此需要进行判断。
(1)割点的判别
判断一个点是否为割点,分为两种情况。
第一,x不为根,当low[v]>=dfn[u]的时候,即v必须通过u才能到达dfn序更小的点,因此x是割点。将其删去后得到的双连通分量的个数即为v的个数加上1。
第二,x为根,只要其在dfs搜索树中儿子的个数超过1,则其一定为割点,对应的双连通分量的个数即为其儿子的个数。
(2)桥的判别
对于边(x,y),总共有两种判别方式。
第一,借用边双中的概念,当形成一个以y为根的边双时,此时的(x,y)必为桥,因此可以使用dfn[y]和low[y]是否相等来判别。
第二,如图所示,low[y]>dfn[x]代表y不能通过其子节点到达其祖先,即必须通过边(x,y)到达其祖先;low[y]<=dfn[x]则代表y可以通过其子节点到达其祖先,因此边(x,y)不是割边。
3.算法模板
(1) 割点
void tarjan(int x,int fa,int key) {//key代表是否为根节点
int ret = 0;
dfn[x] = low[x] = ++tot;
inq[x] = 1;
for (int i = h[x]; i != -1; i = E[i].next) {
int to = E[i].to;
if (!dfn[to]) {
tarjan(to, x,0);
low[x] = min(low[x], low[to]);
if (low[to] >= dfn[x]) ret++;
}
else if (inq[to] && to!=fa) low[x] = min(low[x], dfn[to]);
}
if (key && ret > 1) res.insert(make_pair(x, ret)); //根节点的子节点树必须大于1
else if (!key && ret > 0) res.insert(make_pair(x, ret +1 ));//非根节点形成的双连通分量数必须加上其祖先
}
(2)桥
void tarjan(int u,int fa){
bool flag=0; //用来判断是否存在重边
low[u]=dfn[u]=++Time;
for(int i=head[u];~i;i=e[i].next){
int v=e[i].to;
if(!dfn[v]){
tarjan(v,u);
low[u]=min(low[u],low[v]);//儿子更新父亲
if(dfn[v]==low[v]){//它的子节点v的子树中,没有像u或其祖先连的边(返祖边)
bridge[i]=bridge[i^1]=1; //边i和i的反向边是桥,边的编号从0开始
}
}
else if(v!=fa){
low[u]=min(low[u],dfn[v]);
}
}
}
4.例题
四、tarjan求lca
1.预备知识
LCA(Least Common Ancestors)的意思是最近公共祖先,即在一棵树中,找出两节点最近的公共祖先。lca问题可以应用于树中的最短路径问题。
这里我们使用tarjan算法离线算法解决这个问题。
离线算法,是指首先读入所有的询问(求一次LCA叫做一次询问),然后重新组织查询处理顺序以便得到更高效的处理方法。Tarjan算法是一个常见的用于解决LCA问题的离线算法,它结合了深度优先遍历和并查集,整个算法为线性处理时间。
2.算法思想
总思路就是每进入一个节点u的深搜,就把整个树的一部分看作以节点u为根节点的小树,再搜索其他的节点。每搜索完一个点后,如果该点和另一个已搜索完点为需要查询LCA的点,则这两点的LCA为另一个点的现在的祖先。
先把该节点u的father设为他自己(也就是只看大树的一部分,把那一部分看作是一棵树),搜索与此节点相连的所有点v,如果点v没被搜索过,则进入点v的深搜,深搜完后把点v的father设为点u。
3.算法模板
#include<cstdio>
#define N 420000
struct hehe{
int next;
int to;
int lca;
};
hehe edge[N];//树的链表
hehe qedge[N];//需要查询LCA的两节点的链表
int n,m,p,x,y;
int num_edge,num_qedge,head[N],qhead[N];
int father[N];
int visit[N];//判断是否被找过
void add_edge(int from,int to){//建立树的链表
edge[++num_edge].next=head[from];
edge[num_edge].to=to;
head[from]=num_edge;
}
void add_qedge(int from,int to){//建立需要查询LCA的两节点的链表
qedge[++num_qedge].next=qhead[from];
qedge[num_qedge].to=to;
qhead[from]=num_qedge;
}
int find(int z){//找爹函数
if(father[z]!=z)
father[z]=find(father[z]);
return father[z];
}
int dfs(int x){//把整棵树的一部分看作以节点x为根节点的小树
father[x]=x;//由于节点x被看作是根节点,所以把x的father设为它自己
visit[x]=1;//标记为已被搜索过
for(int k=head[x];k;k=edge[k].next)//遍历所有与x相连的节点
if(!visit[edge[k].to]){//若未被搜索
dfs(edge[k].to);//以该节点为根节点搞小树
father[edge[k].to]=x;//把x的孩子节点的father重新设为x
}
for(int k=qhead[x];k;k=qedge[k].next)//搜索包含节点x的所有询问
if(visit[qedge[k].to]){//如果另一节点已被搜索过
qedge[k].lca=find(qedge[k].to);//把另一节点的祖先设为这两个节点的最近公共祖先
if(k%2)//由于将每一组查询变为两组,所以2n-1和2n的结果是一样的
qedge[k+1].lca=qedge[k].lca;
else
qedge[k-1].lca=qedge[k].lca;
}
}
int main(){
scanf("%d%d%d",&n,&m,&p);//输入节点数,查询数和根节点
for(int i=1;i<n;++i){
scanf("%d%d",&x,&y);//输入每条边
add_edge(x,y);
add_edge(y,x);
}
for(int i=1;i<=m;++i){
scanf("%d%d",&x,&y);//输入每次查询,考虑(u,v)时若查找到u但v未被查找,所以将(u,v)(v,u)全部记录
add_qedge(x,y);
add_qedge(y,x);
}
dfs(p);//进入以p为根节点的树的深搜
for(int i=1;i<=m;i++)
printf("%d ",qedge[i*2].lca);//两者结果一样,只输出一组即可
return 0;
}
4.例题
参考链接:
tarjan在无向图中的应用
lca离线算法