概念
割点、割边
首先引出割点和割边的概念,给定一张无向连通图,选定一条边,当我们把这条边从图中删除的时候,如果整个图分裂成了两个子图,那么我们管这条边叫做割边或者桥。
割点也是同理,删除这个点之后,如果整个图分裂成了两部分,那么我们管这个点叫做割点。
dfs序
给定一张图后,我们从某一个图开始进行 dfsdfsdfs,遍历点的顺序被我们称为 dfsdfsdfs 序。
int dfn[maxn], cnt = 0;
void dfs(int x)
{
dfn[x] = ++cnt;
for (int i = head[x]; i; i = e[i].nxt)
{
int v = e[i].to;
dfs(v);
}
}
大致写一下代码,最后 dfndfndfn 里面保存的就是每一个点是第几个被访问到的,也就是 dfsdfsdfs 序。
追溯值
首先对于一个点 xxx,在整个搜索树上(不知道的自行搜索)有一棵子树,我们将其称之为 subtree[x]subtree[x]subtree[x]。除此之外我们对这个点 xxx 还会有一个追溯值,我们将其设置为 low[x]low[x]low[x],追溯值等于这颗搜索子树里面的所有点,以及从这些点通过一条不在这棵子树里面的边可以到达的所有点的最小 dfsdfsdfs 序。
求解割点和割边
边的成对存储
首先介绍一种建图技巧,这个技巧可以在链式前向星(应该没人不会吧)的基础上实现成对存储,因为在无向图上建边的时候实际上是建了两条反向的有向边,这里的成对存储实际上也就是当我得知其中一条边的编号之后,我可以很轻松的求出反向的那条边。
int tot = 1, head[maxn];
struct edge{
int nxt, to;
}e[maxm];
void addedge(int u, int v)
{
e[++tot].nxt = head[u];
e[tot].to = v;
head[u] = tot;
}
void dfs(int x)
{
for (int i = head[x]; i; i = e[i].nxt)
{
int v = e[i].to;
// i 是当前的边,(i ^ 1) 就是对应的反向边
}
}
int main()
{
int n, m;
cin >> n >> m;
for (int i = 1; i <= m; i++)
{
int u, v;
cin >> u >> v;
addedge(u, v);
addedge(v, u);
}
dfs(1);
}
割边的判断
对于一条边来讲,假设边的两个端点分别是 uuu 和 vvv,我们在搜索的时候先到达 uuu,那么如果 low[v]>dfn[u]low[v] > dfn[u]low[v]>dfn[u],我们就可以认为这个边是一条割边。道理也很好理解,我经过这条边之后,就回不到 uuu 和在 uuu 之前遍历的点了,那么这条边就肯定是桥。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 5, maxm = 2e5 + 5; // 随便给一个范围
int tot = 1, head[maxn], dfn[maxn], low[maxn], brige[maxm];
struct edge
{
int nxt, to;
} e[maxm];
void addedge(int u, int v)
{
e[++tot].nxt = head[u];
e[tot].to = v;
head[u] = tot;
}
int cnt = 0;
void tarjan(int x, int index_edge)
{
dfn[x] = low[x] = ++cnt; // dfs 序
for (int i = head[x]; i; i = e[i].nxt)
{
int v = e[i].to;
if (!dfn[v])
{
tarjan(v, i);
low[x] = min(low[v], low[x]); // 从下面的子树找
if (low[v] > dfn[x])
brige[i] = brige[i ^ 1] = 1; // 这条边以及其对应的反向边是桥
}
else if (i != (index_edge ^ 1))
low[x] = min(low[x], dfn[v]); // 经过一条不在这棵子树的边
}
return;
}
int main()
{
int n, m;
cin >> n >> m;
for (int i = 1; i <= m; i++)
{
int u, v;
cin >> u >> v;
addedge(u, v);
addedge(v, u);
}
tarjan(1, 0);
}
割点的判断
直接给公式:low[v]>=dfn[u]low[v] >= dfn[u]low[v]>=dfn[u],在前面的基础上,多了一个 uuu 点可以遍历,但是需要特判根节点,也就是遍历的起点,因为他的 dfsdfsdfs 序最小,所以这个公式稳定成立。只要当从根节点出发完成了一次 dfs,然后发现还有另一条边可以走(达到的点没有遍历过),那么就可以判断是割点。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 5, maxm = 2e5 + 5; // 随便给一个范围
int tot = 1, head[maxn], dfn[maxn], low[maxn], cut[maxn];
struct edge
{
int nxt, to;
} e[maxm];
void addedge(int u, int v)
{
e[++tot].nxt = head[u];
e[tot].to = v;
head[u] = tot;
}
int cnt = 0;
void tarjan(int x, int index_edge)
{
dfn[x] = low[x] = ++cnt; // dfs 序
int flag = 0; // 用来记录这个节点被判断了多少次
for (int i = head[x]; i; i = e[i].nxt)
{
int v = e[i].to;
if (!dfn[v])
{
tarjan(v, i);
low[x] = min(low[v], low[x]); // 从下面的子树找
if (low[v] >= dfn[x])
{
flag++;
if (x != 1 || flag > 1) // 要么不是根节点,要么是根节点但是被判了两次
cut[x] = 1;
}
}
else
low[x] = min(low[x], dfn[v]); // 经过一条不在这棵子树的边,可以往回找,不影响结果
}
return;
}
int main()
{
int n, m;
cin >> n >> m;
for (int i = 1; i <= m; i++)
{
int u, v;
cin >> u >> v;
addedge(u, v);
addedge(v, u);
}
tarjan(1, 0);
}
作者能力有限,如果有任何错误之处,还请各位指教。

377

被折叠的 条评论
为什么被折叠?



