在上一篇博客判断有向图是否有圈中从递归的角度简单感性的介绍了如何修改深度优先搜索来判断一个有向图是否有圈。事实上, 它的实质是利用了深度优先生成树(depth-first spanning tree)的性质。那么什么是深度优先生成树?顾名思义,这颗树由深度优先搜索而生成的,由于无向图与有向图的深度优先生成树有差别,下面将分别介绍。
无向图的深度优先生成树的生成步骤:
下面是一个无向图和它对应的深度优先生成树:
不难发现,该树的先序遍历过程就是DFS过程,利用该树我们可以更好的理解DFS。而对无向图而言,深度优先生成树一个重要的应用是解决
双连通性问题(该问题在通讯网络,运输网络等有重要应用)。当然,我们首先需要了解双连通性问题的相关概念。
下图是一个不是双连通的图,其中顶点C和D为割点。
利用深度优先生成树求连通图中的所有割点算法如下:
下面是分别从A与C开始遍历上图生成的树:
c++实现代码如下:
/*
数据结构:邻接表存储图
程序说明:为简单起见,设节点的类型为整型,设visited[],num[].low[],parent[]为全局变量,
为求得先序编号num[],设置全局变量counter并初始化为1。为便于单独处理根节点设置root变量。
*/
#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;
const int MAX_N = 100;
vector<int> graph[MAX_N];
vector<int> artPoint;
int num[MAX_N], low[MAX_N], parent[MAX_N];
int counter = 1;
int root;
bool visited[MAX_N];
void Init(); //初始化图
void FindArt(int v); //找到第二类割点
void PrintArtPoint(); //打印所有割点(第一类割点在此单独处理)
int main()
{
Init();
FindArt(root);
PrintArtPoint();
return 0;
}
void PrintArtPoint()
{
int rootChild = 0; //根节点的孩子个数
for (int i = 0; i < graph[root].size(); i++) //计算根节点的孩子个数
{
if (parent[graph[root][i]] == root)
rootChild++;
}
if (rootChild > 1) //根节点孩子个数大于1则为割点
artPoint.push_back(root);
for (int i = 0; i < artPoint.size(); i++)
printf("%d\n", artPoint[i]);
}
void Init()
{
int a, b;
root = 1;
while (scanf("%d%d", &a, &b) != EOF)
{
graph[a].push_back(b);
graph[b].push_back(a);
visited[a] = false;
visited[b] = false;
}
}
void FindArt(int v)
{
visited[v] = true;
low[v] = num[v] = counter++; //情况(1)
for (int i = 0; i < graph[v].size(); i++)
{
int w = graph[v][i];
if (!visited[w]) //树边
{
parent[w] = v;
FindArt(w);
if (low[w] >= num[v] && v != root)
artPoint.push_back(v);
low[v] = min(low[v], low[w]); //情况(3)
}
else if (parent[v] != w) //回退边
{
low[v] = min(low[v], num[w]); //情况(2)
}
}
}
测试运行结果如下:
我们知道有向图同样可以和无向图一样进行深度优先搜索。但是,由于有向图的特点:边的方向性导致即使两个顶点有边相连也不一定是可达的,有向图的深度优先生成树的边有了更多的情况,包括树边(tree edges), 回退边(back edges),向前边(forward edges), 横边(cross edges)。其中后三者是树实际不存在的边,通向的是已经被访问过的点。下面用一张图来直观感受一下这几种情况:
事实上,有以下结论(其中num[]保存的是树节点的先序序列即DFS序列):
1、若num[v] < num[w],即v在w之后被访问,则(v,w)是树边或向前边;
此时,若visited[v]= true, visited[w] = false,(v,w)为 树边;
若visited[v]= true, visited[w] = true,(v,w) 为 向前边;比如上图的第2种情况,访问到节点3时,节点1已经被访问,且num[1]<num[3],故边(1, 3)是向前边。
2、若num[v] > num[w],即v在w之后被访问,故visited[v] = true则visited[w] = true,则(v,w)是回退边或横边;
当产生树边(i,j) 时,同时记下j的父节点:parent[j] = i, 于是对图中任一条边(v,w),由结点v沿着树边向上(parent中)查找w(可能直到根);
若找到w,则(v,w)是回退边,否则是横边。比如上图第一种情况parent[3] = 1,故边(3, 1)为回退边,而第3种情况节点3无父节点,故为横边。
到此我们就知道了如下法则:一个有向图是无圈图当且仅当它没有回退边。
查找强连通分量(SCC: Strong Connected Components)
有向图的深度优先生成树除了可以用于判断有向图是否有边,还可以用来查找强连通分量。首先给出相关概念:
强连通图:一个有向图中任意两个顶点是可以互达的。
强连通分量:对于一个非强连通图,我们可得到顶点的一些子集,使得它们到自身是强连通的。
查找强连通分量的算法:
1. Kosaraju-Sharir算法
理解该算法:如果两个顶点v和w都在一个强连通分支中,则原图G中就存在v到w和w到v的路径,所以Gr也存在。 而两个顶点互达与这两个顶点在Gr中 的同一棵深度优先生成树等价。所以步骤2每次DFS都能得到一个强连通分量。
代码如下:
/*
数据结构;邻接表
程序说明:1. 每对Gr进行一次DFS,生成一个强连通分量,topSort++,
所以topSort相同的顶点即在同一个强连通分量中。
2. 为便于得到最大编号对应的顶点,设置node[],其下标为后序编号,值为对应顶点
*/
#include <cstdio>
#include <vector>
#include <cstring>
#include <cstdlib>
using namespace std;
const int MAX_N = 100;
vector<int> G[MAX_N]; //原图
vector<int> Gr[MAX_N]; //反转后的图
vector<int> topSort[MAX_N]; //下标为所属强分支的拓扑序
int counter = 0; //用于编号
int node[MAX_N]; //后序遍历标号,下标为编号
bool visited[MAX_N];
int vNum; //图的顶点数
void DFS(int v);
void RDFS(int v, int k); //参数k为v所在的强连通分量的拓扑序
int SCC(); //返回强连通分量的个数
void Init(); //初始化图G和Gr
int main()
{
Init();
int sccNum = SCC();
printf("%d\n", sccNum);
for (int i = 0; i < sccNum; i++)
{
int j;
printf("{");
for (j = 0; j < topSort[i].size()-1; j++)
printf("%d, ", topSort[i][j]);
printf("%d}\n", topSort[i][j]);
}
return 0;
}
void Init()
{
scanf("%d", &vNum);
int u, v;
while (scanf("%d%d", &u, &v) != EOF)
{
G[u].push_back(v);
Gr[v].push_back(u); //反向
}
}
void DFS(int v)
{
visited[v] = true;
for (int i = 0; i < G[v].size(); i++)
{
if (!visited[G[v][i]])
DFS(G[v][i]);
}
node[counter++] = v; //后序遍历
}
void RDFS(int v, int k)
{
visited[v] = true;
topSort[k].push_back(v); //将属于同一强连通分量放一起
for (int i = 0; i < Gr[v].size(); i++)
{
if (!visited[Gr[v][i]])
RDFS(Gr[v][i], k);
}
}
int SCC()
{
memset(visited, false, sizeof(visited));
for (int v = 1; v <= vNum; v++)
{
if (!visited[v])
DFS(v);
}
memset(visited, false, sizeof(visited));
int k = 0; //初始化第一个强连通分量的拓扑序为1
for (int i = --counter; i >= 0; i--) //从编号最大开始
{
if (!visited[node[i]])
RDFS(node[i], k++);
}
return k;
}
测试运行结果:
2. Tarjan算法
Tarjan算法和上文所说的双连通性问题的算法非常相似。它也是通过求出深度优先生成树的先序编号num[]和low[]。利用的性质是当num[v] == low[v]时,则以v为根节点的深度优先生成树中所有的节点为一个强连通分量,而为了获得强连通分量,我们需要用一个栈来记录。
Tarjan算法的伪码描述如下:
Tarjan(u)
{
num[u]=low[u] = counter //情况(1)
Stack.push(u) // 将节点u压入栈中
for each (u, v) in E // 枚举每一条边
if (v is not visted) // 如果节点v未被访问过
Tarjan(v) // 继续向下找
low[u] = min(low[u], low[v]) //情况(3)
else if (v in Stack) // 如果节点v还在栈内
Low[u] = min(low[u], num[v]) //情况(2)
if (num[u] == low[u]) // 如果节点u是强连通分量的根
repeat
v = Stack.pop // 将v退栈,为该强连通分量中一个顶点
print v
until (u== v)
}
c++代码:
/*
数据结构:邻接表存储图
*/
#include <cstdio>
#include <vector>
#include <algorithm>
#include <stack>
#include <cstring>
using namespace std;
const int MAX_N = 100;
vector<int> graph[MAX_N];
vector<int> topSort[MAX_N]; //下标为所属强分支的拓扑序
stack<int> scc;
int num[MAX_N], low[MAX_N];
int counter = 1;
int numSCC = 0; //强连通分量个数
int vNum; //顶点个数
bool inStack[MAX_N]; //判断顶点是否在栈中
bool visited[MAX_N];
void Init(); //初始化图
void Tarjan(int v); //tarjan算法查找SCC
void PrintSCC(); //打印所有SCC
void SCC();
int main()
{
Init();
SCC();
PrintSCC();
return 0;
}
void SCC()
{
memset(visited, false, sizeof(visited));
for (int i = 1; i <= vNum; i++)
{
if (!visited[i])
Tarjan(i);
}
}
void PrintSCC()
{
for (int i = 0; i < numSCC; i++)
{
int j;
printf("{");
for (j = 0; j < topSort[i].size() - 1; j++)
printf("%d, ", topSort[i][j]);
printf("%d}\n", topSort[i][j]);
}
}
void Init()
{
int u, v;
scanf("%d", &vNum);
while (scanf("%d%d", &u, &v) != EOF)
{
graph[u].push_back(v);
}
}
void Tarjan(int v)
{
low[v] = num[v] = ++counter; //情况(1)
inStack[v] = true;
visited[v] = true;
scc.push(v);
for (int i = 0; i < graph[v].size(); i++)
{
int w = graph[v][i];
if (!visited[w])
{
Tarjan(w);
low[v] = min(low[v], low[w]); //情况(3)
}
else if (inStack[w])
{
low[v] = min(low[v], num[w]); //情况(2)
}
}
if (num[v] == low[v])
{
int w;
do
{
w = scc.top();
scc.pop();
inStack[w] = false;
topSort[numSCC].push_back(w);
} while (w != v);
numSCC++;
}
}
参考资料:《数据结构与算法分析-C语言描述》
《挑战程序设计竞赛》
博客:https://www.byvoid.com/blog/scc-tarjan/