割边判定法则;
无向边
是桥,当且仅当搜索树上存在
的一个子节点
满足:dfn[u]<low[v]
。
如何理解这个式子:
桥是什么,桥是两个单独岛屿的桥。
什么是单独的岛屿,还记得桃花源记里面的世外桃源吗。
什么是世外桃源,就是与外人间隔
。
再者,什么是追溯值,就是一个节点可以在自己子树和子树可以拓展的节点中找到一个最小的编号。
删掉了桥,那么在世外桃源里面任何一个节点都不会再与外面的节点有任何连接。
于是世外桃源内部的所有节点,他们的最小追溯值一定不会小于 的编号。
咱们要知道,自给自足是很难成功的,总得有一个人出去买加碘海盐,那么这个人就是桃花源的源主。
我们认为源主就是所有节点中编号最小的节点,也就是有可能与外人有所连接的节点。
换言之,也就是 这个桥两端中,在世外桃源内的节点就是源主 。
:
因此,当
的时候:
- . 我们发现从 节点出发,在不经 的前提下,不管走哪一条边,我们都无法抵达 节点,或者比 节点更早出现的节点。
- . 此时我们发现 所在的子树似乎形成了一个封闭圈,那么 自然也就是桥了。(得证)。
割点判断法则:
其实和上面的判断,只有一点修改。若
不是搜索树的根节点,若
节点是割点,那么当且仅当搜索树上存在
的一个儿子节点
,满足:
"源主"节点:编号最小,所以肯定是
最先遍历到的。既然如此,那么显然他的
,就是代表
最先遍历到的,必然就是最小的。而且割点是一个世外桃源和外界的唯一通道,所有的儿子节点的
都必须大于等于它,不可以小于它,因此得证。
其实证明和上面的正经证明是一模一样的,只不过多了一个等于号罢了。
另外:如何判断根是不是割点,必须保证有至少两个儿子节点,否则不能叫作割点。
模板:
割边模板:
#include <bits/stdc++.h>
using namespace std;
const int N=1e5+20;
int head[N],edge[N<<1],Next[N<<1],tot;
int dfn[N],low[N],n,m,num;
bool bridge[N<<1];
void add_edge(int a,int b) {
edge[++tot]=b;
Next[tot]=head[a];
head[a]=tot;
}
void Tarjan(int x,int Edge) {
dfn[x]=low[x]=++num;//DFS序标记
for(int i=head[x]; i; i=Next[i]) { //访问所有出边
int y=edge[i];//出边
if (!dfn[y]) { //不曾访问过,也就是没有标记,可以认为是儿子节点了
Tarjan(y,i);//访问儿子节点y,并且设置边为当前边
low[x]=min(low[x],low[y]);//看看能不能更新,也就是定义中的,subtree(x)中的节点 最小值为low[x]
if (low[y]>dfn[x]) { //这就是桥的判定
bridge[i]=bridge[i^1]=true;//重边也是桥
}
} else if (i!=(Edge^1)) {
low[x]=min(low[x],dfn[y]);
}//第二类定义,也就是通过1跳不在搜索树上的边,能够抵达 subtree(x)的节点
}
}
int main() {
scanf("%d%d",&n,&m);
tot=1;//边集从编号1开始
for(int i=1; i<=m; i++) {
int a,b;
scanf("%d%d",&a,&b);
add_edge(a,b);
add_edge(b,a);
}
for(int i=1; i<=n; i++) {
if (!dfn[i]) { //一个无向图,可能由多个搜索树构成
Tarjan(i,0);
}
}
for(int i=2; i<=tot; i+=2) { //无向边不要管,直接跳2格
if (bridge[i]) {
printf("%d %d\n",edge[i^1],edge[i]);//桥的左右两端
}
}
return 0;
}
割点模板:
#include <bits/stdc++.h>
using namespace std;
const int N=1e6+20;
int head[N],edge[N<<1],Next[N<<1],tot;
int dfn[N],low[N],n,m,num,root,ans;
bool cut[N];
void add_edge(int a,int b) {
edge[++tot]=b;
Next[tot]=head[a];
head[a]=tot;
}
void Tarjan(int x) {
dfn[x]=low[x]=++num;//DFS序标记
int flag=0;
for(int i=head[x]; i; i=Next[i]) { //访问所有出边
int y=edge[i];//出边
if (!dfn[y]) { //不曾访问过,也就是没有标记,可以认为是儿子节点了
Tarjan(y);//访问儿子节点y,并且设置边为当前边
low[x]=min(low[x],low[y]);//看看能不能更新,也就是定义中的,subtree(x)中的节点 最小值为low[x]
if (low[y]>=dfn[x]) { //这就是割点的判定
flag++;//割点数量++
if (x!=root || flag>1) { //不能是根节点,或者说是根节点,但是有至少两个子树节点 是割点
cut[x]=true;
}
}
} else low[x]=min(low[x],dfn[y]); //第二类定义,也就是通过1跳不在搜索树上的边,能够抵达 subtree(x)的节点
}
}
int main() {
scanf("%d%d",&n,&m);
memset(cut,false,sizeof(cut));
for(int i=1; i<=m; i++) {
int a,b;
scanf("%d%d",&a,&b);
add_edge(a,b);
add_edge(b,a);
}
for(int i=1; i<=n; i++) {
if (!dfn[i]) { //一个无向图,可能由多个搜索树构成
root=i,Tarjan(i);
for(int i=1; i<=n; i++) { //统计割点个数
if (cut[i]) {
ans++;
}
printf("%d\n",ans);
for(int i=1; i<=n; i++) { //顺序遍历,康康哪些点是割点
if (cut[i]) {
printf("%d ",i);
}
}
}
}
}
return 0;
}