一、基础
1、搜索树:在无向图中,我们以某一个节点 x 出发进行深度优先搜索,每一个节点只访问一次,所有被访问过的节点与边构成一棵树,称为无向连通图的搜索树
1、割点:若从图中删除节点x(以及所有与x关联的边之后),图将被分成不相连的子图,那么称 x 为图的割点
2、割边(桥):若从图中删除边e之后,图将分裂成两个不相连的子图,那么称e为图的割边或桥
3、时间戳:用来标记图中每个节点在进行深度优先搜索时被访问的时间顺序,用 dfn[x] 来表示
4、追溯值:表示从当前节点x作为根节点出发,能够访问到的所有节点中,时间戳最小的值,用 low[x]来表示
计算追溯值:
(1)、先令low[x] = dfn[x]
(2)、若搜索树上x是y的父节点,则令low[x] = min(low[x], low[y])
(3)、若无向边(x,y)不是搜索树上的边,则令low[x] = min(low[x], dfn[y])
5、双连通分量:若一张无向连通图不存在割点,则称它为点双联通图,记为v-DCC;若一张无向连通图不存在桥,则称它为边双连通图,记为e-DCC
6、强连通分量:对于图中任意两个结点x、y,既存在从x到y的路径,也存在从y到x的路径,则称该图为强连通图;有向图的极大连通子图被称为强连通分量
7、Tarjan算法是基于深度优先搜索的算法,用于求解图的连通性问题。Tarjan算法可以在线性时间内求出无向图的割点与桥,进一步地可以求解无向图的双连通分量;同时,也可以求解有向图的强连通分量、必经点与必经边
二、求割边
一条边(u,v)是割边,当且仅当(u,v)为树枝边(即非负边),且满足:dfn(u)<low(v)(没有重边)
公式说明:从v的子树出发,在不经过(u, v)的前提下,不管走哪条路,都无法到达u或比u更早访问的结点。也就是说,u的儿子v之间只有一条边(无重边),且v点只能到u点到不了u点前
int dfn[maxn], low[maxn], bridge[maxn];
int to[maxn << 1], nex[maxn << 1], head[maxn];
int root, total, cnt; //邻接表从2开始
void add(int u, int v)
{
to[++cnt] = v;
nex[cnt] = head[u];
head[u] = cnt;
}
void Tarjan(int u, int father)
{
dfn[u] = low[u] = ++total;
for (int i = head[u]; i; i = nex[i])
{
int v = to[i];
if (!dfn[v])
{
Tarjan(v, i);
low[u] = min(low[u], low[v]);
if (low[v] > dfn[u])
bridge[i] = bridge[i ^ 1] = 1; //记录路径
}
else if (i != (father ^ 1)) //判断重边
{
low[u] = min(low[u], dfn[v]);
}
}
}
void solve()
{
int x, y, n, m;
cin >> n >> m;
cnt = 1;
for (int i = 0; i < m; i++)
{
cin >> x >> y;
add(x, y);
add(y, x);
}
for (int i = 1; i <= n; i++)
{
if (!dfn[i])
Tarjan(i, 0);
}
int ans = 0;
for (int i = 2; i < cnt; i += 2) //邻接表存边从2开始
{
if (bridge[i])
ans++;
}
cout << ans << '\n';
for (int i = 2; i < cnt; i += 2)
{
if (bridge[i])
cout << to[i ^ 1] << "->" << to[i];
}
}
三、求边双连通分量
在求出所有桥的基础上,把桥都删去,就可以得到边双连通分量
实现:先用Tarjan算法标记所有桥边,然后对整个无向图进行dfs,历遍过程不访问桥边,由此划分出每个连通块
//在求割边的基础上加上以下代码
int c[maxn], dcc; //存储每个点的归属,和dcc的数量
void dfs(int x)
{
c[x] = dcc;
for (int i = head[x]; i; i = nex[i])
{
int y =to[i];
if (c[y] || bridge[i])
continue;
dfs(y);
}
}
//下面加在main
for (int i = 1; i <= n; i++)
{
if (!c[i])
{
dcc++;
dfs(i);
}
}
cout << dcc << '\n';
for (int i = 1; i <= n; i++)
{
cout << i << " belongs to " << c[i] << '\n';
}
四、e-DCC缩点
在求出e-DCC的基础上,把每个e-DCC缩成一个点构成一棵新的树,存在邻接表中
int eto[maxn << 1], enex[maxn << 1], ehead[maxn], ecnt;
//在求出e-DCC的基础上加入以下代码
void add_dcc(int u, int v)
{
eto[++ecnt] = v;
enex[ecnt] = ehead[u];
ehead[u] = ecnt;
}
//下面代码加到main
ecnt = 1;
for (int i = 2; i <= cnt; i++)
{
int x = to[i ^ 1], y = to[i];
if (c[x] == c[y])
continue;
add_dcc(c[x], c[y]);
}
for (int i = 2; i < ecnt; i+=2)
{
cout << eto[i ^ 1] << eto[i]; //输出边
}
五、求割点
如果x不是搜索树的根节点,则x是割点当且仅当搜索树上存在x的一个子节点y,满足:dfn(x)<=low(y)
int dfn[maxn], low[maxn], point[maxn];
int head[maxn], to[maxn << 1], nex[maxn << 1];
int cnt, total, root;
void add(int u, int v)
{
to[++cnt] = v;
nex[cnt] = head[u];
head[u] = cnt;
}
void Tarjan(int u)
{
dfn[u] = low[u] = ++total;
int child = 0;
for (int i = head[u]; i; i = nex[i])
{
int v = to[i];
if (!dfn[v])
{
Tarjan(v);
low[u] = min(low[u], low[v]);
if (low[v] >= dfn[u] && u != root)
point[u] = 1;
if (u == root && ++child >= 2)
point[u] = 1;
}
else
low[u] = min(low[u], dfn[v]);
}
}
void solve()
{
int x, y, n, m;
cin >> n >> m;
for (int i = 1; i <= m; i++)
{
cin >> x >> y;
add(x, y);
add(y, x);
}
for (int i = 1; i <= n; i++)
{
if (!dfn[i])
{
root = i;
Tarjan(i);
}
}
int ans = 0;
for (int i = 1; i <= n; i++)
{
if (point[i])
ans++;
}
cout << ans << '\n';
for (int i = 1; i <= n; i++)
{
if (point[i])
cout << i << ' ';
}
}
六、求点双连通分量
点双连通分量不是将割点删去后的剩余连通块,而是剩余连通块加上割点
我们将深搜时遇到的所有边加入到栈里面(第一次访问一个结点时入栈),当找到一个割点的时候(dfn(x)<=low(y)),就将这个割点往下走到的所有边弹出(从x开始向外弹出,直到y被弹出),而这些点构成的就是一个点双连通分量
int dfn[maxn], low[maxn], point[maxn];
int head[maxn], to[maxn << 1], nex[maxn << 1];
int cnt, total, root;
stack<int>stk;
vector<int>dcc[maxn];
int vcnt;
void add(int u, int v)
{
to[++cnt] = v;
nex[cnt] = head[u];
head[u] = cnt;
}
void Tarjan(int u)
{
dfn[u] = low[u] = ++total;
stk.push(u);
if (u == root && head[u] == 0)
{
dcc[++vcnt].push_back(u);
return;
}
int child = 0;
for (int i = head[u]; i; i = nex[i])
{
int v = to[i];
if (!dfn[v])
{
Tarjan(v);
low[u] = min(low[u], low[v]);
if (low[v] >= dfn[u])
{
if (u != root)
point[u] = 1;
vcnt++;
int temp;
do
{
temp = stk.top();
stk.pop();
dcc[vcnt].push_back(temp);
} while (temp != v);
dcc[vcnt].push_back(u);
}
if (u == root && ++child >= 2)
point[u] = 1;
}
else
low[u] = min(low[u], dfn[v]);
}
}
void solve()
{
int x, y, n, m;
cin >> n >> m;
for (int i = 1; i <= m; i++)
{
cin >> x >> y;
add(x, y);
add(y, x);
}
for (int i = 1; i <= n; i++)
{
if (!dfn[i])
{
root = i;
Tarjan(i);
}
}
cout << vcnt << '\n';
for (int i = 1; i <= vcnt; i++)
{
cout << dcc[i].size() << ' ';
for (int j = 0; j < dcc[i].size(); j++)
{
cout << dcc[i][j] << ' ';
}
cout << '\n';
}
}
七、v-DCC缩点
由于一个割点可能属于多个点双连通分量,因此我们建一个包含p(割点数量)+t(v-DCC数量)个结点的新图,由每个割点连接v-DCC
//在求出v-DCC的基础上
int vto[maxn << 1], vnex[maxn << 1], vhead[maxn], vvcnt;
int newid[maxn],c[maxn]; //给割点的new id
void add_dcc(int u, int v)
{
vto[++vvcnt] = v;
vnex[vvcnt] = vhead[u];
vhead[u] = vvcnt;
}
//在main中加入以下代码
int num = vcnt;
for (int i = 1; i <= n; i++)
{
if (point[i])
newid[i] = ++num;
}
vvcnt = 1;
for (int i = 1; i <= vcnt; i++)
{
for (int j = 0; j < dcc[i].size(); j++)
{
int x = dcc[i][j];
if (point[x])
{
add_dcc(i, newid[x]);
add_dcc(newid[x], i);
}
else
c[x] = i;
}
}
for (int i = 2; i < vvcnt; i += 2)
{
cout << vto[i ^ 1] << ' ' << vto[i] << '\n';
}