概念
连通性
在无向图 G G G中,若从顶点 v i v_i vi到顶点 v j v_j vj有路径(当然从 v j v_j vj到 v i v_i vi也一定有路径),则称 v i v_i vi和 v j v_j vj是连通的。
连通图
在无向图 G G G中,若 V ( G ) V(G) V(G)中任意两个不同的顶点 v i v_i vi和 v j v_j vj都连通(即有路径),则称 G G G为连通图(Con-nected Graph)。
在有向图 G G G中,如果两个顶点 v i v_i vi, v j v_j vj间有一条从 v i v_i vi到 v j v_j vj的有向路径,同时还有一条从 v j v_j vj到 v i v_i vi的有向路径,则称两个顶点强连通(strongly connected)。如果有向图 G G G的任意两个顶点都强连通,称 G G G是一个强连通图。
连通分量
无向图的 G G G的极大连通子图称为 G G G的连通分量(Connected)。任何连通图的连通分量都只有一个,即使是其本身,非连通的无向图有多个连通分量。
有向图的极大强连通子图,称为强连通分量。
一、有向图的强连通分量
强连通分量作用:将有向图通过缩点转换为一个有向无环图(拓扑图、DAG),其中缩点指将所有连通分量表示成一个点。
求强连通分量的方法:DFS。在DFS过程中将树中的边分成四大类:
- 树枝边:比如两个点 x 、 y x、y x、y,其中 x x x是 y y y的父结点,两点之间的连线即为树枝边;
- 前向边:比如两个点 x 、 y x、y x、y,其中 x x x是 y y y的祖先结点(中间可能间隔多个结点),如果存在一条从 x x x直接指向 y y y的边,则称这条边为前向边(树枝边是一种特殊的前向边);
- 后向边:将前向边方向取反,就得到后向边;
- 横叉边:从正在搜索的结点指向已经搜索过得结点的边(因为是DFS,所以应该指向左边的结点)。
如何判断一个点是否在强连通分量(SCC)中呢?一个点在强连通分量中:
- 可能存在后向边指向祖先结点,使其可以返回祖先结点;
- 可能存在横叉边,使其可以沿着横叉边返回搜索过的点,然后走到祖先结点;
因此,如果某个点在某个强连通分量中的话,它一定可以沿着横叉边或者后向边走到其某个祖先结点(前向边不可能构成环)。基于上述启发,有了Tarjan算法求SCC。
引入概念:时间戳,即在dfs过程中对遍历到的结点标记一个从小到大的顺序。
Tarjan算法求SCC
在DFS过程中,对遍历到的点按遍历顺序给定两个时间戳:
- d f n [ n ] dfn[n] dfn[n]:表示遍历到结点 u u u的时间戳;
- l o w [ u ] low[u] low[u]:从 u u u开始走,所能遍历到最小时间戳(即以 u u u为根节点的子树中,包含根节点 u u u,所能遍历到最小时间戳)
若 u u u是其所在的强连通分量的最高点,等价于 d f n [ u ] = = l o w [ u ] dfn[u]==low[u] dfn[u]==low[u],即意味着这个强连通分量不可能走到其它任何一个点,那么就可以找出这个强连通分量。
代码模板
// 栈中存储当前还没有遍历完的强连通分量的所有点
void tarjan(int u)
{
dfn[u] = low[u] = ++ timestamp; // 使二者等于时间戳
stk[ ++ top] = u, in_stk[u] = true; // 将当前点入栈,并记录当前点是否在栈中
for (int i = h[u]; ~i; i = ne[i]) // 遍历u所有能够到的点
{
int j = e[i];
if (!dfn(j)) // 如果当前结点没有被遍历过
{
tarjan(j);
low[u] = min(low[u], low[j]); // 用j能够到的点的最小值来更新u的最小值
}
else if (in_stk[j]) // 如果当前点依然在栈中
low[u] = min(low[u], dfn[j]); // 用当前点来更新low值
}
if (dfn[u] == low[u])
{
int y;
++ scc_cnt;
do {
y = stk[top -- ];
in_stk[y] = false;
id[y] = scc_cnt; // 标记当前点属于哪个强连通分量
} while (y != u);
}
}
后续操作:缩点
for int i = 1; i <= n; i ++
for i的所有邻边
if i和j不在同一SCC中
加一条边,id(i) → id(j)
在做完上述操作之后,这样整张图就变成了DAG图(不用在进行topsort
了,因为按连通分量编号递减的顺序一定是拓扑序)
1.1 受欢迎的牛
如果这个题目的图是一个拓扑图,那么拓扑图最后仅有一个终点的话,即仅有一个出度为零的点,那么终点这头牛一定可以被所有牛欢迎。如果存在两个及以上出度为零的点,这些点之间不可能相互连接,因此不存在一头牛牛被除自己之外的所有牛认为是受欢迎的。
所以可以我们可以先求图中所有强连通分量,然后缩点,将图转化为DAG图,然后观察出度为零的点(缩点前是一个强连通分量,自己一个点也可能是一个强连通分量)里面有多少个点,即有多少头符合结果要求的牛。
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 10010, M = 50010;
int n, m;
int h[N], e[M], ne[M], idx;
int dfn[N], low[N], timestamp; // 时间戳
int stk[N], top;
bool in_stk[N];
// id每个点属于哪个连通分量
// scc_cnt表示图中连通分量个数
// size表示每个连通分量中点的数量
int id[N], scc_cnt, Size[N];