Tarjan 算法是基于深度优先搜索的算法,用于求解图的连通性问题。
用途:Tarjan 算法可以在线性时间内求出无向图的割点与桥,进一步地可以求解无向图的双连通分量;同时,也可以求解有向图的强连通分量、必经点与必经边。
其中需要两个重要的数组low,dfn。
dfn:作为这个点搜索的次序编号(时间戳),简单来说就是 第几个被搜索到的。
low:追溯值---(用来表示从当前节点 x 作为搜索树的根节点出发,能够访问到的所有节点中,时间戳最小的值)。
大致模板如下:
void Tarjan(int u) //顶点
{
++cnt; //访问时间值
dfn[u] = low[u] = cnt;
for (int i = 0; i < edge[u].size(); ++i)
{
int v = edge[u][i];
if (!dfn[v])
{
Tarjan(v);
low[u] = min(low[u], low[v]);
}
else
low[u] = min(low[u], dfn[v]);
}
}
若从图中删除节点 x 以及所有与 x 关联的边之后,图将被分成两个或两个以上的不相连的子图,那么称 x 为图的割点。
根结点:如果是割点条件是,当且仅当其子节点数大于等于 2。
非根节点 :u 如果是割点,当且仅当 u 至少存在一个子树v,v中没有连向 u 的祖先的边(返祖边)。
即v访问结束时满足low[v]>=dfn[u]。
若从图中删除边 e 之后,图将分裂成两个不相连的子图,那么称 e 为图的桥或割边。
边(u, v)在dfs 树中。如果u 为v 的父亲,v 的子树中没有向u 或其祖先连的边。如果桥(u,v)的两个端点都不是叶子节点,则节点u和v为割点
判断条件:low[v]>dfn[u]
点双连通分量:点双连通的极大子图称为点双连通分量(简称双连通分量)
特点:
该连通分量的点在同一简单环;该连通分量没有桥;一个割点可以多个点连通分量。
边连通分量:边双连通的极大子图称为边双连通分量。
特点:
任意一条边至少包含在一个简单环;连通分量里没有桥;割点只属于一个边双连通分量;
两个边双连通分量最多只有一条边,且必为桥;进一步地,所有边双与桥可抽象为一棵树结构。
求强连通分量个数。代码如下:
#include
#include
#include
using namespace std;
const int MAX = 1e4 + 5;
int n, m, cnt, cntb;
vector edge[MAX]; //存边
vector belong[MAX];
bool instack[MAX]; //表示某结点是否在栈中
int dfn[MAX]; //记录搜索顺序
int low[MAX]; //记录所属强连通
stack s; //一个栈存储搜索路径
void Tarjan(int u)
{
++cnt; //访问时间值
dfn[u] = low[u] = cnt;
s.push(u);
instack[u] = true;
for (int i = 0; i < edge[u].size(); ++i)
{
int v = edge[u][i];
if (!dfn[v])
{
Tarjan(v);
low[u] = min(low[u], low[v]);
}
else if (instack[v])
low[u] = min(low[u], dfn[v]);
}
if (dfn[u] == low[u]) 如果一个点的dfn和low值相同,这个点就是根(找到一个强连通分量)
{
++cntb;
int node;
do
{ 把该强连通分量的所有节点序号存入belong数组中
node = s.top();
s.pop();
instack[node] = false;
belong[cntb].push_back(node);
} while (node != u);
}
}
int main()
{
cin >> n >> m;
for (int i = 1; i <= m; ++i)
{
int u, v;
cin >> u >> v;
edge[u].push_back(v);
}
Tarjan(1);
for (int i = 1; i <= n; i++) {
if (!dfn[i])Tarjan(i);
}
int cont = 0;
for (int i = 1; i <= cntb; ++i)
{
if (belong[i].size() > 1)cont++;
}
cout << cont << '\n';
return 0;
}
无向图求桥(割边),并按给定规则输出。代码如下:
#include
#include
#include
#include
#include
using namespace std;
const int MAX = 5005;
int n, m, cnt;
int cont;
vector edge[2 * MAX];
int dfn[MAX];
int low[MAX];
struct node{
int u, v;
}node[MAX*2];
bool cmp(struct node a, struct node b) {
if (a.u == b.u) {
return a.v < b.v;
}
else {
return a.u < b.u;
}
}
void tarjan(int u,int in_edge) {
++cnt;
dfn[u] = low[u] = cnt;
for (int i = 0; i < edge[u].size(); i++) {
int v = edge[u][i];
if (!dfn[v]) {
tarjan(v, u);
low[u] = min(low[u], low[v]);
if (low[v] > dfn[u]) { 桥的判定条件
node[cont].u = u;
node[cont++].v = v;
}
}
else if(v != in_edge){
low[u] = min(low[u], dfn[v]);
}
}
}
int main() {
ios::sync_with_stdio(false);
cin >> n >> m;
for (int i = 0; i < m; i++) {
int u, v;
cin >> u >> v;
edge[u].push_back(v);
edge[v].push_back(u);
}
for (int i = 1; i <= n; i++) {
if (!dfn[i])tarjan(i, 0);
}
sort(node, node + cont, cmp);
for (int i = 0; i < cont; i++) {
cout << node[i].u << ' ' << node[i].v << '\n';
}
return 0;
}
无向图求割点。代码如下:
#include
#include
#include
#include
#include
using namespace std;
const int MAX = 1e5 + 5;
int n, m, cnt;
int cont;
vector g[2*MAX];
int dfn[MAX];
int low[MAX];
int flag[MAX];
// v:当前点 r:本次搜索树的root
void tarjan(int u, int r) {
dfn[u] = low[u] = ++cnt;
int child = 0;
for (unsigned i = 0; i < g[u].size(); i++) {
int v = g[u][i];
if (!dfn[v]) {
tarjan(v, r);
low[u] = min(low[u], low[v]);
if (low[v] >= dfn[u] && u != r)cont += !flag[u], flag[u] = 1;//不是根而且他的孩子无法跨越他回到祖先
if (r == u)child++; //如果是搜索树的根,统计孩子数目
}
else low[u] = min(low[u], dfn[v]);//已经搜索过了
}
if (child >= 2 && u == r)cont += !flag[r], flag[r] = 1;
}
int main() {
ios::sync_with_stdio(false);
cin >> n >> m;
for (int i = 0; i < m; i++) {
int u, v;
cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
}
for (int i = 2; i <= n; i++) {
if (!dfn[i])tarjan(i, i);
}
cout << cont << '\n';
for (int i = 1; i <= n; i++) {
if (flag[i])cout << i << ' ';
}
return 0;
}