先介绍几个概念:
1.割边:在连通图中,删除了连通图的某条边后,图不再连通。,这样的边叫做割边,也称作桥。
2.割点:在连通图中,删除了连通图的某个点以及与这个点相连的边后,图不再连通。这样的点叫做割点。
3.dfs搜索树:用 dfs 遍历图时,按照遍历次序的不同,我们可以得到一颗 dfs 搜索树。
举个例子,对这个图一来说,其dfs搜索树便是图二
图中还有几个概念
树边:在搜索树中实线所示,表示dfs过程中访问 未访问结点 时所经过的边,也叫父子边
回边:在搜索树中虚线所示,表示dfs过程中遇到 已访问结点 时所经过的边,也叫返祖边、后向边。
割点包含两类结点:
1.对于根节点u,如果u有不止一棵子树(两棵及两棵以上),则该跟节点u为割点
2.对于非叶子且非根结点(叶子结点一定不是割点),若其中的某棵子树的结点均没有指向u的祖先结点的回边,说明删除u之后,根节点与该棵子树的结点不再连通,则结点 u 为割点。
我们用dfn[u]数组来记录u结点的dfs次序,low[u]来记录结点 u 或 u 的子树通过非父子边(这里的子指的是u,父指的是u的父亲结点)追溯到最早的祖先结点(dfs次序最小的结点),那么low[u]可以这么得到
若(u,v)是树边,则low[u]=min(low[u],low[v]);
若(u,v)是回边且v不是u的父亲,则low[u]=min(low[u],dfs[v]);
对于图一,dfn和low数组分别为
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
dfn | 1 | 3 | 5 | 2 | 4 | ||
low | 1 | 3 | 3 | 2 | 3 |
对于割点的第一种情况,很容易判断子树的个数,用child变量记录一下就好了
对于割点的第二种情况,当(u,v)为树边且low[v]>=dfn[u]时,结点u才是割点。
而对于割边,当(u,v)为树边且dfs[u]>low[v]时,表示v结点智能通过该边(u,v)与u连通,那么(u,v)即为割边。
#pragma warning(disable:4996) #include <cstdio> #include <vector> #include <cstring> #include <set> #include <algorithm> #define make_pair pair<int,int> #define N 20010 using namespace std; vector<int>to[N]; vector<pair<int, int>>line;//存割边 set<int>ans;//存割点 int father[N]; // 0 表示根节点,-1 表示未遍历, 否则代表父亲结点 //dfn[u]数组表示结点 u 的dfs遍历次序,low[u]数组表示 u 及其子树能到达的最远祖宗结点 int dfn[N], low[N], dfs_clock = 0; void init(){ dfs_clock = 0; memset(father, -1, sizeof father); father[1] = 0; } void dfs(int u){ dfn[u] = low[u] = ++dfs_clock; int child = 0; for (int i = 0; i < to[u].size(); i++){ int v = to[u][i]; //未访问过 v 点 if (father[v] == -1){ child++; father[v] = u; dfs(v); low[u] = min(low[u], low[v]); //结点 u 是根节点,且有不止一个子树 if (father[u] == 0 && child>1) ans.insert(u); //结点 u 不是根节点, if (father[u] != 0 && dfn[u] <= low[v]) ans.insert(u); //割边的情况 if (low[v] > dfn[u])line.push_back(make_pair(min(u, v), max(u, v))); } else if (father[u] != v){ low[u] = min(low[u], dfn[v]); } } }