点双连通分量是指在无向图中,由极大点双连通的子图组成的一种特殊结构。割点是指对于一个连通图中的点 x,如果删去这个点以及与所有 x 相连的边之后图不连通,那么称 x 为该图的割点。点双联通是指对于一个无向图,如果该图中不存在割点,那么称这个图是点双联通的。
下面是一个简单的证明:
设 G(V,E) 是无向图,则 G 的邻接矩阵 A 为 $n \times n$ 的矩阵,其中 $A[i][j]$ 表示节点 i 和节点 j 之间是否存在一条边。设 D(V) 是 D(V) 的一个子集,且 D(D) 是 D(D) 的一个子集。若 G 为双连通图,则对于任意两个不同的顶点 i、j,都有至少一条路径 p=(i1,i2...ik-1,ij...jk-1,j),使得 p∩ D=D' 或 p∩ D=φ。因此 D' 是双连通子图。
为了方便起见,我们将上述结论简化为以下三个性质:
1. 任意两点间都存在至少两条非重复路径;
2. 图中任意一个割点都在至少两个点双中;
3. 任意一个不是割点的点都只存在于一个点双中。
设 dfn[x] 表示遍历到 x 之前遍历过多少个点,low[x] 表示从 x 出发在不经过父子边的情况下能到达的最早的祖先的 dfn 值。
回看割点的定义:把这个点删除后图会不连通,那么肯定存在一些点,在不经过该割点的情况下无法到达割点的祖先,用这个性质来找就好了。
代码如下:
重构后的代码如下:
```cpp
int dfn[maxn], low[maxn], id;
bool cut[maxn];
void dfs1(int x, int from) {
dfn[x] = low[x] = ++id;
int son = 0;
for (int i = first[x]; i; i = e[i].next) {
int y = e[i].y;
if (y == (from ^ 1)) continue; // 不能通过反向边走回去
if (!dfn[y]) {
dfs1(y, x);
son++;
if (low[y] < low[x]) low[x] = low[y]; // 更新low,假如y能走到更小的,那么x也能走到
if (low[y] >= dfn[x]) cut[x] = true; // 假如儿子在不经过父子边的情况下不能到达x的祖先,那么x就是个点
} else if (dfn[y] < low[x]) low[x] = dfn[y]; // 假如走到了一个dfn更小的,那么更新low
}
if (fa == -1 && son == 1) cut[x] = false; // 特判出发点在一个环中的情况
}
void tarjan() {
for (int i = 1; i <= n; i++) // 如果图不连通,那么每一个连通块都要跑一遍
if (dfn[i] == 0) dfs1(i, -1);
}
```
特判是用来处理从红色节点出发的情况。如果从红色节点出发,最后红色节点会被判定成割点,因为没有儿子能走到它的祖先。但是它压根就没有祖先,所以这时候,我们判断一下,假如没有祖先,那么不能算割点,于是有了 `fa == -1` 这个判定条件。而 `son == 1` 这个条件用来判断出发点是否在环中。如果出发点在环中,那么它也可能是割点。
这个限制了,因为如果起点是割点,那么son肯定大于1。
在这个问题中,我们需要重构以下内容:
```
发现只有遍历到一个dfn[y]=0的y时,son才会+1,所以son的实际意义就是该点所连接的点双数量。
```
重构后的内容如下:
当遍历到`dfn[y] = 0`且`y`满足条件时,我们认为`son`的数量会增加1。因此,可以得出结论:`son`的实际意义是表示与该点相连的点的对数。
代码示例(Python):
```python
if dfn[y] == 0:
son += 1
```
根据上述性质,我们可以得出以下结论:
1. 如果$s\oplus n=1$,那么这个点肯定不是割点。
2. 如果$s\oplus n=1$,并且$son=1$,那么这个点一定是割点。
3. 如果$fa=-1$,那么这个点一定不是割点。
4. 如果$fa=-1$,并且$son=1$,那么这个点一定是割点。
在第4种情况下,我们需要进行$son=1$的判断。这是因为在割点的定义中,对于任意一条有向边$(u,v)$,如果存在一个顶点$w$,使得$uw\neq uv$,那么$w$就是割点。而在这个例子中,由于$fa=-1$,所以存在一个顶点$w$,使得$uw\neq uv$。因此,根据第4种情况,我们可以得出结论:在给定的图中,存在一个割点。
那可能有人会问了,为什么其他的割点不用如下的方法来判断呢?仔细想想,其他的点和出发点的区别就是:他们都有父亲。而遍历的时候是不能往回走的,也就是说,我们不知道父亲的状态,所以不能用如下的方法来判断。
找点双
这个时候就要请出伟大的 Tarjan 了!我们依然考虑使用上面的深度优先搜索(DFS)和广度优先搜索(BFS)来求,我们将深搜时遇到的所有边加入到栈里面,当找到一个割点的时候,就将这个割点往下走到的所有边弹出,而这些边所连接的点就是一个点双了。
代码:
以下是重构后的内容:
```cpp
int dfn[maxn], low[maxn], id, t;
edge zhan[(maxn * maxn) << 1]; //存边的栈
int belong[maxn], cnt; //belong记录每个点属于哪一个点双,cnt记录点双个数
bool cut[maxn];
set<int> s[maxn]; //记录每个点双包含哪些点,如果题目不需要也可以不求
void dfs(int x, int from) {
dfn[x] = low[x] = ++id;
int son = 0;
for (int i = first[x]; i; i = e[i].next) {
if (i == (from ^ 1)) continue;
int y = e[i].y;
if (!dfn[y]) {
zhan[++t] = e[i];
dfs(y, i);
son++; //先压栈再遍历
if (low[y] < low[x]) low[x] = low[y];
if (low[y] >= dfn[x]) //发现x是割点
{
cnt++;
edge xx;
cut[x] = true;
do {
xx = zhan[t--]; //弹出
belong[xx.x] = belong[xx.y] = cnt; //标记
s[cnt].insert(xx.x); s[cnt].insert(xx.y); //记录
} while (xx.x != x || xx.y != y); //假如已经弹到 x到y 这条边了,就停下来
}
} else if (dfn[y] < low[x]) low[x] = dfn[y];
}
if (from == -1 && son == 1) cut[x] = false;
}
```
请根据提供的内容完成内容重构,并保持段落结构:
边不重复的路径。找割边和找割点类似,直接上代码:
```cpp
int dfn[maxn], low[maxn], id;
bool cut[maxn];
void dfs(int x, int from) {
dfn[x] = low[x] = ++id;
for (int i = first[x]; i; i = e[i].next) {
int y = e[i].y;
if (!dfn[y]) {
dfs(y, i);
if (low[y] < low[x]) low[x] = low[y];
if (low[y] > dfn[x]) cut[i] = cut[i ^ 1] = true;
// 注意这里是大于而不是大于等于,原因想想就明白了
} else if (dfn[y] < low[x]) low[x] = dfn[y];
}
}
```
找边双的做法:
做法1:在请出Tarjan之前,我们先介绍另外一种做法:第一次找出割边,然后第二次找出割边。在不经过割边的情况下遍历所有点,每一次遍历经过的一个子图就是一个边双。
做法2:用类似找点双的做法,但是栈里面压点,不压边。代码如下:
以下是重构后的代码:
```cpp
int dfn[maxn], low[maxn], belong[maxn];
int id = 0, cnt = 0;
int zhan[maxn], t = 0;
void dfs(int x, int from) {
dfn[x] = low[x] = ++id;
zhan[++t] = x;
for (int i = first[x]; i; i = e[i].next) {
if (i == (from ^ 1)) continue;
int y = e[i].y;
if (!dfn[y]) {
dfs(y, i);
if (low[y] < low[x]) low[x] = low[y];
} else if (dfn[y] < low[x]) low[x] = dfn[y];
}
if (dfn[x] == low[x]) {
cnt++;
int xx;
do {
xx = zhan[t--];
belong[xx] = cnt;
} while (xx != x);
}
}
```
题表:Caocao's Bridges、Juice Junctions、Redundant Paths。