强连通分量
- 强连通分量(Strongly Connected Component)简称SCC
- 首先要理解一个定义,什么是强连通分量
- 所谓强连通,有两个点 u u u和 v v v,从 u u u到 v v v有一条路径,从 v v v到 u u u也有一条路径,那么说这两个点之间是强连通的,也叫做互相可达;如果一个有向图 G G G中的任意两个点之间都是强连通的,那么就说这个图是一个强连通图
- 强连通分量。如果一个有向图 G G G不是强连通图,那么可以把它分成多个子图,且每个子图的内部是强连通的,而且这些子图已经扩展到最大,不能与这个子图以外的任意点强连通,可称这个图是一个极大联通子图。那么这个极大联通子图就是图 G G G的一个强连通分量
- 画一个图理解一下,见下图
- 上图中有四个SCC,分别是 { 1 , 2 , 3 , 4 } , { 5 } , { 6 } , { 7 } \{1,2,3,4\},\{5\},\{6\},\{7\} {1,2,3,4},{5},{6},{7}
- 关于SCC可以容易的得到一个定理,就是在一个SCC中,从其中任何一个点出发,都至少有一条路径能绕回到自己,这个定理是显然的
Tarjan算法求解SCC
算法详解
- 那么如何去求一个有向图中有多少个SCC呢?Tarjan提出了一种求解的办法
- 设置三个数组, n u m [ i ] num[i] num[i]表示节点 i i i的 d f s dfs dfs序,也叫做时间戳; l o w [ i ] low[i] low[i]表示 i i i节点能够回溯到的最远祖先, s c c n o [ i ] sccno[i] sccno[i]表示 i i i节点位于第几个 S C C SCC SCC
- 设置一个栈,栈上方的一些连续元素用来储存当前SCC的节点
- 还是用这个图做例子,我们先自己看看怎么求SCC,首先我们很容易的可以看出来 { 1 , 2 , 3 , 4 } \{1,2,3,4\} {1,2,3,4}属于一个SCC,那么我们就看它,怎么把它和图里其他SCC分开
- 首先我们从1出发,进行 d f s dfs dfs搜索,显然搜索路径为1-3-4-2,把这四个元素都入栈,前面说了 n u m num num数组表示的是节点的时间戳(进入递归的顺序), l o w low low数组初始值和 n u m num num相同,那么显然,这四个节点的初始时间戳、回溯值和SCC标号如下
i | num[i] | low[i] | sccno[i] |
---|---|---|---|
num[1] | 1 | 1 | 0 |
num[2] | 4 | 4 | 0 |
num[3] | 2 | 2 | 0 |
num[4] | 3 | 3 | 0 |
- 现在的这些值都已经记录好了,到达节点2,接下来要到节点1了,这时候发现节点1已经进入递归了,也就是 n u m num num数组有标记,所以这时候1就不再进入递归,而是更新它的 l o w low low值为它能够回溯到的最早的祖先节点显然是它本身,更新方式为 l o w [ u ] = m i n ( l o w [ u ] , n u m [ v ] ) low[u]=min(low[u],num[v]) low[u]=min(low[u],num[v])
- 上述公式改为 l o w [ u ] = m i n ( l o w [ u ] , l o w [ v ] ) low[u]=min(low[u],low[v]) low[u]=min(low[u],low[v])也不影响正确性,原因是到这一步更新必然是到达了祖先节点,而祖先节点的 l o w [ v ] = n u m [ v ] low[v]=num[v] low[v]=num[v]始终成立
- 可以看出,第一个进入 d f s dfs dfs的节点就是它所在SCC的祖先节点
- 接下来,需要进行的是更新这个SCC上面的所有节点的 l o w low low值,因为整个过程是递归进行的,所以这步操作是一个回溯的过程,写在递归后面即可,方程为 l o w [ u ] = m i n ( l o w [ u ] , l o w [ v ] ) low[u]=min(low[u],low[v]) low[u]=min(low[u],low[v])意思就是从后往前更新回溯值,这样一圈操作下来,得到这个SCC的各个节点的信息如下
i | num[i] | low[i] | sccno[i] |
---|---|---|---|
num[1] | 1 | 1 | 0 |
num[2] | 4 | 1 | 0 |
num[3] | 2 | 1 | 0 |
num[4] | 3 | 1 | 0 |
- 容易发现,只有祖先节点的 n u m num num和 l o w low low是相等的,所以如果发现某个节点在递归结束以后满足这个特性,那么就一定是祖先节点,这时候,将栈顶祖先节点之前(包括本身)的元素全部弹出,加上SCC标号,这样就成功的划分出了一个SCC
i | num[i] | low[i] | sccno[i] |
---|---|---|---|
num[1] | 1 | 1 | 1 |
num[2] | 4 | 4 | 1 |
num[3] | 2 | 2 | 1 |
num[4] | 3 | 3 | 1 |
- 可能有人会觉得这个例子过于简单,要是走到了别的SCC内部怎么办,这就牵扯到了一个关键话题,就是这个栈里面并不是只有一个SCC,它是根据 l o w low low和 n u m num num划分的不同SCC,也就是以 l o w [ u ] = = n u m [ u ] low[u]==num[u] low[u]==num[u]为一层,将栈划分成了若干个SCC
- 还是上面这个例子,比如要是从5号节点开始 d f s dfs dfs会怎样呢?比如说它先到4,那么 d f s dfs dfs序为5-4-2-1-3-7-6,那么显然5在栈底,到3以后,接下来是4,发现4已经进入了递归,这时候更新3、1、2的 l o w low low值与4相同,结果还是 l o w [ 4 ] = n u m [ 4 ] low[4]=num[4] low[4]=num[4],这样把栈顶元素一直到4这一段弹出,标记为第一个SCC,然后现在栈里就一个5,继续遍历到7-6,接下来是3,发现3进入了递归且已经标好了SCC,所以3就和接下来没关系了,现在从栈顶到栈底依次为6、7、5,因为没有接下来的节点了,所以一看 l o w [ 6 ] = n u m [ 6 ] low[6]=num[6] low[6]=num[6],所以就弹出6,标记为第二个SCC,弹出7标记为第三个SCC,最后弹出5,标记为第四个SCC, t a r j a n tarjan tarjan算法结束
- 代码实现如下
#include <iostream>
#include <stack>
#include <vector>
using namespace std;
const int MAXN = 1e6 + 100;
vector<int> g[MAXN];
int num[MAXN];
int sccno[MAXN];
int low[MAXN];
int dfn;
int cnt;
stack<int> st;
void tarjan(int s){
st.push(s);
low[s] = num[s] = ++cnt;
for(int i=0;i<g[s].size();i++){
int v = g[s][i];
if(!num[v]){
tarjan(v);
low[s] = min(low[s], low[v]);
}else if(!sccno[v]){
low[s] = min(low[s], num[v]);
}
}
if(low[s] == num[s]){
cnt++;
while(1){
int v = st.top();
st.pop();
sccno[v] = cnt;
if(v == s) break;
}
}
}
- 这里我暂且使用邻接表存图,之后再使用链式前向星改写
- 还有一个问题,图不连通怎么办,我们只需要把每个没有进入递归的节点都进行一次 t a r j a n tarjan tarjan算法即可
缩点
-
经过上面的操作,我们已经将整个图划分为了若干个SCC,这样其实就起到了一个化简图的过程,每个SCC可以看作为一个点,这也就是我们所说的缩点,根据SCC标号,对于上述例子,我们可以构建一个新图如下
-
这就是缩点之后得到的图,可以发现,SCC之间的指向关系和之前的关系是一致的,这是显然成立的,我们利用这一点来建图
-
这时候我们可以仔细观察一下SCC的标号,这个标号实际上是一个逆拓扑序,这个性质在后面的 2 − S A T 2-SAT 2−SAT问题中有应用
练习
- 这个题思路如下:首先我们要建图,接着使用 t a r j a n tarjan tarjan算法进行缩点,同时记录每一个SCC的内部权值和,之后要重新建图,最后对于每一个SCC计算它能够达到的最大点权和,取所有的最大值即为答案
#include <iostream>
#include <stack>
#include <cstring>
#include <vector>
using namespace std;
const int MAXN = 2e5 + 100;
vector<int> g[MAXN];
int num[MAXN];
int sum[MAXN];
int low[MAXN];
int sccno[MAXN];
int val[MAXN];
int x[MAXN];
int y[MAXN];
int f[MAXN];
int dfn;
int cnt;
inline int read(){
int x = 0, f = 1;char c = getchar();
while(c < '0' || c > '9'){if(c == '-') f = -1;c = getchar();}
while(c >= '0' && c <= '9'){x = (x << 1) + (x << 3) + (c ^ 48);c = getchar();}
return x * f;
}
stack<int> st;
void tarjan(int u){
low[u] = num[u] = ++dfn;
st.push(u);
for(int i=0;i<g[u].size();i++){
int v = g[u][i];
if(!num[v]){
tarjan(v);
low[u] = min(low[u], low[v]);
}else if(!sccno[v]){
low[u] = min(low[u], num[v]);
}
}
if(low[u] == num[u]){
cnt++;
while(1){
int v = st.top();
st.pop();
sccno[v] = cnt;
sum[cnt] += val[v];
if(u == v) break;
}
}
}
void dfs(int u){
if(f[u]) return;
f[u] = sum[u];
int maxsum = 0;
for(int i=0;i<g[u].size();i++){
int v = g[u][i];
if(!f[v]){
dfs(v);
}
maxsum = max(maxsum, f[v]);
}
f[u] += maxsum;
}
int main(){
int n, m;
n = read();
m = read();
for(int i=1;i<=n;i++) val[i] = read();
for(int i=0;i<m;i++){
x[i] = read();
y[i] = read();
g[x[i]].push_back(y[i]);
}
for(int i=1;i<=n;i++){
if(!num[i]){
tarjan(i);
}
}
memset(g, 0, sizeof g);
for(int i=0;i<m;i++){
if(sccno[x[i]] != sccno[y[i]]){
g[sccno[x[i]]].push_back(sccno[y[i]]);
}
}
int ans = -1;
for(int i=1;i<=cnt;i++){
if(!f[i]){
dfs(i);
}
ans = max(ans, f[i]);
}
cout << ans;
return 0;
}
- 修改为链式前向星
#include <iostream>
#include <stack>
#include <cstring>
#include <vector>
using namespace std;
const int MAXN = 2e5 + 100;
int num[MAXN];
int sum[MAXN];
int low[MAXN];
int sccno[MAXN];
int val[MAXN];
int x[MAXN];
int y[MAXN];
int f[MAXN];
int head[MAXN];
int dfn;
int cnt;
int tot;
struct Edge{
int to;
int next;
int val;
}edge[MAXN];
inline int read(){
int x = 0, f = 1;char c = getchar();
while(c < '0' || c > '9'){if(c == '-') f = -1;c = getchar();}
while(c >= '0' && c <= '9'){x = (x << 1) + (x << 3) + (c ^ 48);c = getchar();}
return x * f;
}
void Add_Edge(int u, int v, int w){
edge[cnt].to = v;
edge[cnt].next = head[u];
edge[cnt].val = w;
head[u] = cnt++;
}
stack<int> st;
void tarjan(int u){
low[u] = num[u] = ++dfn;
st.push(u);
for(int i=head[u];i!=-1;i=edge[i].next){
int v = edge[i].to;
if(!num[v]){
tarjan(v);
low[u] = min(low[u], low[v]);
}else if(!sccno[v]){
low[u] = min(low[u], num[v]);
}
}
if(low[u] == num[u]){
tot++;
while(1){
int v = st.top();
st.pop();
sccno[v] = tot;
sum[tot] += val[v];
if(u == v) break;
}
}
}
void dfs(int u){
if(f[u]) return;
f[u] = sum[u];
int maxsum = 0;
for(int i=head[u];i!=-1;i=edge[i].next){
int v = edge[i].to;
if(!f[v]){
dfs(v);
}
maxsum = max(maxsum, f[v]);
}
f[u] += maxsum;
}
int main(){
int n, m;
n = read();
m = read();
memset(head, -1, sizeof head);
for(int i=1;i<=n;i++) val[i] = read();
for(int i=0;i<m;i++){
x[i] = read();
y[i] = read();
Add_Edge(x[i], y[i], 1);
}
for(int i=1;i<=n;i++){
if(!num[i]){
tarjan(i);
}
}
memset(head, -1, sizeof head);
cnt = 0;
for(int i=0;i<m;i++){
if(sccno[x[i]] != sccno[y[i]]){
Add_Edge(sccno[x[i]], sccno[y[i]], 1);
}
}
int ans = -1;
for(int i=1;i<=tot;i++){
if(!f[i]){
dfs(i);
}
ans = max(ans, f[i]);
}
cout << ans;
return 0;
}
- 时间优化幅度非常巨大