图论学习笔记——强连通分量/双连通分量
预备知识
- 强连通:对于图(V,E)上的两个顶点u,v,若存在从u到v的有向路径,同时也存在从v到u的有向路径,(注意有路径即可,也就是允许有中间顶点)则称这两个顶点强连通。
- 强连通图:若图(V,E)上的任意两个顶点都强连通,则称这个图为强连通图。
- 强连通分量:有向图的极大强连通子图
强连通分量(SCC,Strongly Connected Components)
求强连通分量(Tarjan算法)
dfn[i]:= 顶点i在搜索过程中的次序编号(时间戳),即记录顶点i是第几个被搜索到的顶点。
low[i]:= 顶点u及其后代顶点所能追溯到的最早的顶点(即祖先顶点)v的时间戳dfn[v]。当顶点u第一次被搜索到时,初始化为low[i]=dfn[i]
.
int dfs_clock, scc_cnt;
int dfn[maxn], low[maxn], sccno[maxn];
void dfs(int u) {
dfn[u] = low[u] = ++dfs_clock;
//给顶点u打上时间戳
s.push(u);
for (int i = head[u]; i; i = e[i].next) {
int v = e[i].to;
if (!dfn[v]) {
//顶点v还没被搜索到
dfs(v);
low[u] = min(low[u], low[v]);
//维护祖先中最小的时间戳
}
else if (!sccno[v]) {
low[u] = min(low[u], dfn[v]);
//顶点v已经被搜索过了,但还不属于某一个SCC
}
}
//对顶点u的所有后代顶点完成搜索之后
//开始判断顶点u是不是这个强连通分量中第一个出现的顶点
if (low[u] == dfn[u]) {
scc_cnt++;
//SCC数量+1
while (1) {
int x = s.top(); s.pop();
sccno[x] = scc_cnt;
//给分量中的所有顶点记录所在SCC的编号
if (x == u) break;
//访问完u之后,就完成了对这个SCC所有顶点的访问,跳出
}
}
}
void find_scc(int n) {
dfs_clock = scc_cnt = 0;
mem(sccno, 0);
mem(dfn, 0);
for (int i = 1; i <= n; i++) {
if (!dfn[i]) dfs(i);
}
}
缩点
顾名思义,将图中的强连通分量看作是一个点,就是缩点。同时原图变为一个DAG,如此便可以将一个有环的图转化为DAG,可以利用DAG的性质解决问题。
问题一:给定一个有向图(V,E),包含n个点和m条边,问至少还要再添加多少条边才能使整个图变成强连通图。
对于DAG,这个问题的答案是max(a,b),其中a是入度为零的顶点个数,b是出度为零的顶点个数。特别地,如果DAG中只有一个点,则答案为0。但问题中给出的图不一定无环,此时就可以用缩点的方法将图转化为DAG。
问题二:给定一个有向图(V,E),包含n个点和m条边,每个点有一个权值。求一条路径,使得路径上点的权值和最大。允许多次经过一条边或一个点,但权值只计算一次。
对于DAG,这个问题就是求DAG上的最长路,用DAG上的dp即可解决。定义dp[i]为从顶点i出发的路径的最大权值和,则转移方程为dp[i]=max(dp[i],dp[j]+val[i]),其中顶点j满足:存在i→j的有向边。
需要注意的是,在方程中需要先求出dp[j],才能用它来更新dp[i]。具体代码实现有两种方法:
- 记忆化搜索;
- 先求图的拓扑排序,再以拓扑排序的倒序进行dp。
题目给定的图可能有环,只需对原图求强连通分量,缩点,建立新图,则新图就是DAG。
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步