这篇文章上次修改于 349 天前,可能其部分内容已经发生变化,如有疑问可询问作者。
ACM程序课算法笔记15——强联通分量
问题与目的
强联通的点是指,A点能到达B点,且B点能到达A点。
对于一个图,问有多少强联通分量?
Kosaraju 算法
获取目标图G1的反图G2(所有边反向)
该算法先获取一张图G1所有点后序遍历的逆序排列。
再对G2进行dfs,每次dfs遇到的未到达的点集合就是一个强联通分量。
理解算法
首先我们要了解后序遍历的逆序排序意味着什么:
如果是一棵无环的树,后序排序的逆序一定是从根节点开始的,也就是说如果我们遍历一个点就删除它,我们就能保证,我们遍历到的每个点的时候(前面的点被删除)都是没有边指向它的。
如果这是一个连通图,再去寻找规律:
对于后序遍历的逆排序,对于其中的任意一个点,在这个点右边的所有点都是这个点能达到的,在这个点左边的所有点都是这个点碰不到的。
如果这图不一定联通,有规律:(这个规律就很重要)
这个点的后面的所有点都是这个点能到达的,或者根本不连通的(没有任何的边关系),前面的点都是到达不了的。
分析
对G2进行dfs,所有的边都反向了,此时的A点到达B点实际上就是B点能到达A点。
对所有的点从前往后运算,我们能保证,这个点后面的所有点只有两种情况,根本没有连接关系和能到达的点。例如第一个点,我们能保证除去和它根本无关的点,它能到达其他的所有点。
所以G2中,从前点开始能遇到的点,就是这些点能遇到前点,形成了一个强联通,一旦形成便删除,所以对于后面遍历的每个点都有这样的连接关系。
这也就是Kosaraju算法的原理。
代码
// g 是原图,g2 是反图
void dfs1(int u) {
vis[u] = true;
for (int v : g[u])
if (!vis[v]) dfs1(v);
s.push_back(u);
}
void dfs2(int u) {
color[u] = sccCnt;
for (int v : g2[u])
if (!color[v]) dfs2(v);
}
void kosaraju() {
sccCnt = 0;
for (int i = 1; i <= n; ++i)
if (!vis[i]) dfs1(i);
for (int i = n; i >= 1; --i)
if (!color[s[i]]) {
++sccCnt;
dfs2(s[i]);
}
}
例题一:
给定一个有向图,每个点i有点权ai,请对于每个点i,找到i能到达的点中点权的最大值(包括i点)。
对于题目要求,我们可以利用强联通分量,对于强联通分量中的每个点,它们能到达点点权最大值是一样的,所以不妨把一个强联通分量的点集合看做一个点,这就形成了一个有向无环图,再进行记忆化搜索即可。
void solve(){
int n, m;
int sccCnt = 0;
int maxV = 0;
cin >> n >> m;
vector<int> value(n + 1);
vector<int> value2(n + 1);
for(int i = 1; i <= n; i++){
cin >> value[i];
}
vector<int> G1[n + 1];
vector<int> G2[n + 1];
vector<int> G3[n + 1];
for (int i = 0; i < m; i++)
{
int a, b;
cin >> a >> b;
G1[a].push_back(b);
G2[b].push_back(a);
}
vector<int> order;
vector<bool> visited(n + 1);
vector<bool> visited2(n + 1);
vector<int> type(n + 1);
function<void(int)> dfs1 = [&](int u)
{
visited[u] = true;
for (int x : G1[u])
if (not visited[x])
dfs1(x);
order.push_back(u);
};
function<void(int)> dfs2 = [&](int u)
{
maxV = max(maxV, value[u]);
type[u] = sccCnt;
visited2[u] = true;
for (int x : G2[u])
if (not visited2[x])
dfs2(x);
};
long long result = 0;
for (int i = 1; i <= n; i++)
if(not visited[i]) dfs1(i);
for (int i = n - 1; i >= 0; i--)
{
if (not visited2[order[i]])
{
maxV = 0;
++sccCnt;
dfs2(order[i]);
value2[sccCnt] = maxV;
}
}
for(int i = 1; i <= n; i++){
for(int x:G1[i]){
if(type[i] != type[x]){
G3[type[i]].push_back(type[x]);
}
}
}
// 缩点完成
vector<bool> done(n + 1);
function<int(int)> dfs3 = [&](int u)
{
if(done[u])
return value2[u];
for(int x: G3[u]){
if(not done[x]){
value2[x] = dfs3(x);
done[x] = true;
}
value2[u] = max(value2[u], value2[x]);
}
done[u] = true;
return value2[u];
};
for(int i = 1; i <= sccCnt; i++){
if(not done[i]) dfs3(i);
}
for(int i = 1; i <= n; i++){
cout << value2[type[i]] << endl;
}
}
应用
我们可以将一张图的每个强连通分量都缩成一个点。
然后这张图会变成一个 DAG,可以进行拓扑排序以及更多其他操作。
举个简单的例子,求一条路径,可以经过重复结点,要求经过的不同结点数量最多。