前几天做了一下有关tarjan算法的专题。这篇算是做一个总结吧。
- 求强连通分量
- 求无向图的割和桥
- 最近公共祖先
求强连通分量
基本概念:
强连通是有向图才有的概念。一个有向图是强连通的是对于每个有序对 u,v,存在一条从 u到v 的路径。一个有向图的强连通分量是指它的极大连通子图。就是你可以从 a地走到 b 地,同样可以从b地走到a地,a,b就是连通的。
下图(图片来自这里)中 顶点 {1,2,3,4}是一个强连通分量,因为它们两两都可以到达。直观上来讲,所谓的强连通,(如果有至少有一个顶点)就是可以至少形成一个环。如1->3->4->1就是一个环,它们就是强连通的。注意也有像{5},{6}这样的的连通分量。
![](https://img-my.csdn.net/uploads/201207/12/1342068293_9692.png)
强连通的tarjan 算法是基于图的深度优先搜索(这个经常用于获取图的各种信息)。下面说一下几个约定:
- 时间戳 :DFN[i]是指结点i被遍历的时间。
- Low[i] :是指在搜索树中,结点i和其子孙可以访问到的最早的祖先,Low[i] = Min(DFN[i], DFN[j], Low[k])其中j是i的祖先(我们把子孙连到祖先的边叫后向边),k是i 的子女。
- 结点的颜色:color[i]是用于标示结点i的状态:白色指还没到搜索到,灰色正在被搜索,黑色处理完毕。在实际操作中用-1,0,1分别代表白色、灰色、黑色。
tarjan算法的步骤:
- 先把所有的结点的颜色都初始化白色,并把栈清空。
- 找到一个白色的结点i(即结点的颜色为白色)
- 给结点一个时间戳,把结点圧入栈中,并把结点标记为灰色。令Low[i] = DFN[i]
- 遍历结点i 的每条边(i,j)。若color[j]是白色,就对结点i重复2~5步骤。并令Low[i] = min(Low[j],low[i]).如果color[j]是灰色,令Low[i] = min(Low[i],DFN[j])。如果是黑色不做任何处理。
- 把结点的颜色改为黑色,如果Low[i] = DFN[i],就把从栈顶到结点i间的元素弹出
- 重复步骤2,至到没有白色顶点
下面是算法的一个模板:
#include <math.h> #include <stdio.h> #include <string.h> #include <stdlib.h> //从顶点0开始 // 要用的话要初始化:调用Adj.initial 和 tarjan.initial //要解决问题用调用tarjan.solve //对tarjan.initial要传入的参数是图边集Adj,和顶点个数n const int maxn = 11000; //顶点的规模 const int maxm = 210000; //边的规模,如果是无向图要记得乘以2 const int GRAY = 0; const int WHITE =-1; const int BLACK = 1; typedef struct Edge{ int s; int e; int next; }Edge; typedef struct Adj{ int edge_sum; int head[maxn]; Edge edge[maxm]; void initial(){ edge_sum = 0; memset(head,-1,sizeof(head)); } void add_edge(int a, int b){ edge[edge_sum].s = a; edge[edge_sum].e = b; edge[edge_sum].next = head[a]; head[a] = edge_sum++; } }Adj; typedef struct Tanjan{ int n; int *head; Adj *adj; Edge *edge; int cnt; int top; int cur; int dfn[maxn]; int low[maxn]; int color[maxn]; int stack[maxn]; int belong[maxn]; void initial(Adj *_adj,int _n){ n = _n; adj = _adj; head = (*adj).head; edge = (*adj).edge; } void solve(){ memset(dfn,-1,sizeof(dfn)); memset(color,WHITE,sizeof(color)); top = cnt = cur = 0; for(int i = 0; i < n; i++) if(color[i] == WHITE)//找到一个白色的顶点,就开始处理 tarjan(i); } inline int min(int a, int b){ if(a < b) return a; else return b; } void tarjan(int i){ int j = head[i]; color[i] = GRAY;//标记为灰色 stack[top++] = i;//把结点圧入栈顶 dfn[i] = low[i] = ++cur;//给结点一个时间戳,并给Low初始化 while(j != -1){ int u = edge[j].e; if (dfn[u] == WHITE){ tarjan(u); low[i] = min(low[i],low[u]); //更新low }else if (color[u] == GRAY) low[i] = min(low[i],dfn[u]); //一条后向边 j = edge[j].next; } color[i] = BLACK; if(low[i] == dfn[i]){ do{ j = stack[--top]; belong[j] = cnt; }while(i != j); ++cnt; } } }Tarjan; Adj adj; Tarjan tj;
tarjan算法的简单证明:
首先,这边再重复一下什么是后向边:就是在深度优先搜索中,子孙指向祖先的边。在一棵深度优先搜索树中,对于结点v, 和其父亲结点u而言,u,v 属于同一个强连通分支的充分必要条件是 以v为根的子树中,有一条后向边指向u或者u的祖先。
1 、必要性。
如果 u,v属于同一个强连通分支则必定存在一条 u到 v的路径和一条v到u的路径。合并两条则有 u->v->v1->v2->..vn->u, 若顶点v1到vn都是v 的子孙,则有 vn->u这样一条后向边。
如果v1到vn 不全是vn的子孙,则必定有一个是u的祖先,我们不妨设vi为u的祖先,则有一条后向边 V[i-1] ->v[i]。
2.、充分性。 我们设 u1->u2->u3..->un->u->v->v1->v2..->vn,我们假设后向边vn指向ui则有这样一个环:u[i]->u[i+1]...->u->v->v1->v2..->v[n-1]->v[n]->u[i],易知,有一条u->v的路径,同时有v->u的路径。固u,v属于同一连通分支。
在算法开始的时候,我们把i圧入栈中。
根据low[i] 和 dfn[i]的定义我们知道,
如果low[i] < dfn[i] 则以i为顶点的子树中,有指向祖先的后向边,则说明i和i的父亲为在同一连通分支,也就是说
留在栈中的元素都是和父结点在同一连通分支的
。
如果low[i] == dfn[i],则
i为顶点的子树中没有后向边,那么由于 留在栈中的元素都是和父结点在同一连通分支的,我们可以知道,从栈顶到元素i构成了一个连通分支。显然,low[i]不可能小于dfn[i]
转载请注明出处--nothi
参考资料
《算法艺术和信息学竞赛》