预备知识
- 强连通:对于图((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。