强连通分量
概念
图
是一个二元组 。 其中 是非空集,称为 ,对于 中的每个元素,我们称其为 或 ,简称 点; 为 各结点之间边的集合,称为 。 则有一条边 的两个端点分别为 和
无向图
对于一张无向图 ,对于 ,若存在一条途径使得 ,,则称 和 是 。由定义,任意一个顶点和自身连通,任意一条边的两个端点连通。
若无向图 ,满足其中任意两个顶点均连通,则称 是 或 环, 的这一性质称作 。
若 是 的一个连通子图,且不存在 满足 且 为连通图,则 是 的一个 (极大连通子图)。
有向图
对于一张有向图 ,对于 ,若存在一条途径使得 ,,则称 可达 。由定义,任意一个顶点可达自身,任意一条边的起点可达终点。(无向图中的连通也可以视作双向可达。)
若有向图 ,满足其中任意两个节点两两互相可达,则称这张图是 或 环。
若一张有向图的边替换为无向边后可以得到一张连通图,则称原来这张有向图是 。
与连通分量类似,也有 (极大弱连通子图)和 。
算法
1. 什么是
算法是一种用于求解有向图的强连通分量的算法,时间复杂度为 。它可以求出每个强连通分量的大小、属于其的顶点和强连通分量的总数。
2. 认识 生成树
dfs生成树处理强连通分量的一个有力的工具:在 时,每当通过某条边 访问到一个新节点 ,就加入这个点和这条边,最后得到的便是 生成树。例如对于下面这张有向图: 它的 生成树可能是这样(黑色实线):
有向图的 生成树主要有 种边(不一定全部出现):
- :黑色实线,每次搜索找到一个还没有访问过的结点的时候就形成了一条树边。
- :灰色虚线,从某个点到它的某个子孙节点(注意不是子节点)的边。
- (也称反祖边):绿色虚线,也被叫做回边,即指向祖先结点的边。
- :蓝色虚线,从某个点到一个以被访问过且既非它子孙节点、也非它祖先节点的边。
定理:反向边和横叉边都有一个特点:起点的 序必然大于终点的 序。1
这可以导出一个有用的结论:对于每个强连通分量,存在一个点是其他所有点的祖先。若不然,则可以把强连通分量划成 个分支,使各分支的祖先节点互相不为彼此的祖先。这些分支间不能通过树边相连,只能通过至少 条横叉边相连,但这必然会违背上一段讲的性质。
我们把这个唯一的祖先节点称为强连通分量的根。显然,根是强连通分量中 序最小的节点。
定理:如果结点 是某个强连通分量的根(也就是在在搜索树中遇到的第一个结点),那么这个强连通分量的其余结点肯定是在搜索树中以 为根的子树中。2
3. 的基本思想
在 算法中,每个结点 维护了以下几个变量:
-
:深度优先搜索遍历时结点 被搜索的次序(也称为 序)。
-
:定义为 所在子树的节点经过最多一条非树边 (其中 必须可达 )能到达的节点中最小的 序。 根据这样的定义,某个点 是强连通分量的根,等价于 。
证明:如果 ,说明 不能到达 序比 小的节点,或者说不存在一个强连通分量同时包含 和某个 序比 小的节点。因此, 只能是某个强连通分量的根。
我们这里必须强调 可达 ,否则在下图中,会使 ,但它是一个强连通分量的根。
枚举图中的顶点 ,如果 ,说明 属于一个新的强连通分量,从 开始搜索。
接下来,按照深度优先搜索算法搜索的次序对图中所有的结点进行搜索。在搜索过程中,对于以某个点 为起点的边 :
- 如果 未访问过,则 在 所在的子树上,如果某节点 从 起可以经过最多一条后向边到达,则从 起也可以(先从 到 ,再到 ),于是先递归处理点 ,然后令 。
- 如果 已访问过,且从 可以到达 ,令 。
- 如果 已访问过,且从 不能到达 ,不做处理。(后两种情况都是非树边)
但是我们怎么确认一个点能不能到达另一个点呢?因为反向边和横叉边都指向 序较小的节点,而前向边的存在又不影响状态转移方程,所以我们只需要确认比该点 序小的哪些点能到达该点即可,这可以用一个栈动态地维护:
-
每当搜索到新点,就令它入栈。
-
如果发现点 满足 ,则说明 是某个强连通分量的根,它和栈中的子孙节点相互可达。但同时,它和栈中的子孙节点也无法到达 的祖先节点,以及祖先节点其他尚未搜索的分支了,所以不断从栈顶弹出元素,直到弹出 (注意这样维护的栈中节点的 序是单调增的),同时记录答案。
算法
简单描述
算法依靠两次简单的 实现。
它有一个重要的特点:求出的强连通分量是按拓扑序排列的。
第一次 ,选取任意顶点作为起点,遍历所有未访问过的顶点,并在回溯之前给顶点编号,也就是后序遍历。
第二次 ,对于反向后的图(反图),以标号最大的顶点作为起点开始 。这样遍历到的顶点集合就是一个强连通分量。对于所有未访问过的结点,选取标号最大的,重复上述过程。
两次 结束后,强连通分量就找出来了, 算法的时间复杂度为
练习
一道求强连通分量的裸题,要注意判断字典序最小。(然而数据太水)
可以用以下数据检测自己判断字典序的程序是否正确
输入
5 4
4 3 1
3 2 1
4 5 2
3 1 2
输出
2
1 3
#include <bits/stdc++.h>
using namespace std;
const int maxn = 10005 * 2;
struct node
{
int to, nxt;
} edge[maxn];
int n, m, mx = -1, cnt, cnt_node, cntn, head[maxn], dfn[maxn], low[maxn], siz[maxn], id[maxn];
bool vis[maxn];
stack<int> s;
inline void add_edge(int u, int v)
{
edge[++cnt].to = v;
edge[cnt].nxt = head[u];
head[u] = cnt;
}
inline void tarjan(int u)
{
cnt_node++;
dfn[u] = low[u] = cnt_node;
s.push(u);
vis[u] = 1;
for (int &e = head[u]; e; e = edge[e].nxt)
{
if (!dfn[edge[e].to])
{
tarjan(edge[e].to);
low[u] = min(low[edge[e].to], low[u]);
}
else if (vis[edge[e].to])
low[u] = min(low[u], dfn[edge[e].to]);
}
if (low[u] == dfn[u])
{
cntn++;
while (1)
{
int now = s.top();
s.pop();
vis[now] = 0;
id[now] = cntn;
siz[cntn]++;
if (now == u)
break;
}
}
}
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1, u, v, op; i <= m; i++)
{
scanf("%d%d%d", &u, &v, &op);
add_edge(u, v);
if (op == 2)
add_edge(v, u);
}
for (int i = 1; i <= n; i++)
if (!dfn[i])
tarjan(i);
for (int i = 1; i <= cntn; i++)
mx = max(mx, siz[i]);
cout << mx << endl;
for (int i = 1; i <= n; i++)
if (siz[id[i]] == mx)
cout << i << " ";
return 0;
}
#include <bits/stdc++.h>
using namespace std;
#define maxn 10005
#define maxm 100005
struct node
{
int to, nxt;
} edge1[maxm], edge2[maxm];
int n, m, cnt, res1, res2;
int head1[maxn], head2[maxn];
int color[maxn], siz[maxn];
int mx;
bool vis[maxn];
stack<int> s;
void add_edge1(int u, int v)
{
edge1[++res1].to = v;
edge1[res1].nxt = head1[u];
head1[u] = res1;
}
void add_edge2(int u, int v)
{
edge2[++res2].to = v;
edge2[res2].nxt = head2[u];
head2[u] = res2;
}
void dfs1(int u)
{
vis[u] = true;
for (int i = head1[u]; i; i = edge1[i].nxt)
if (!vis[edge1[i].to])
dfs1(edge1[i].to);
s.push(u);
}
void dfs2(int u)
{
color[u] = cnt;
siz[cnt]++;
for (int i = head2[u]; i; i = edge2[i].nxt)
if (!color[edge2[i].to])
dfs2(edge2[i].to);
}
void kosaraju()
{
for (int i = 1; i <= n; i++)
if (!vis[i])
dfs1(i);
while (!s.empty())
{
int v = s.top();
s.pop();
if (!color[v])
{
cnt++;
dfs2(v);
}
}
}
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i++)
{
int u, v, op;
scanf("%d%d%d", &u, &v, &op);
add_edge1(u, v);
add_edge2(v, u);
if (op == 2)
{
add_edge1(v, u);
add_edge2(u, v);
}
}
kosaraju();
for (int i = 1; i <= cnt; i++)
{
mx = max(mx, siz[i]);
}
cout << mx << endl;
for (int i = 1; i <= n; i++)
if (siz[color[i]] == mx)
{
int now = color[i];
for (int j = i; j <= n; j++)
if (color[j] == now)
cout << j << " ";
break;
}
return 0;
}
Footnotes
本文来自博客园,作者:蒟蒻orz,转载请注明原文链接:https://www.cnblogs.com/orzz/p/18122179