Kosaraju 强连通分量算法
对于 "从每个点出发, 将其能到达的点标记为一个强连通分量" 这个算法, 其实搞出来的是原图缩点之后的一条链, Kosaraju 算法就是利用这些链并基于 "原图所有有向边取反后, 强连通分量依然不变, 缩点后所有边取反" 的性质, 以一个十分妙的姿势使用上方加粗算法求出所有强连通分量。(具体地, 按照原图的缩点之后的拓扑序遍历, 可以避免很多问题
算法的正确性证明并不难, 在此不述。
Kosaraju 强连通分量算法的时空效率基本都被 Tarjan 强连通分量算法吊打(这两种算法的时间和空间复杂度是完全一样的, 只有常数的差别), 但其代码编写难度是很低的, 且算法容易记住, 算是可以用来应急。(这算法根本不用刻意记啊
至于原图、反图和缩点图如何和谐共存, 其实很简单的, 要么用 vector, 要么用结构体存图, 要么用邻接表, 把边的数量开成所有图的总边数, 然后把 head[maxN]
数组改成 head[图数][maxN]
, 然后把 ad(u,v,w)
函数改成 ad(图编号,u,v,w)
, 然后把遍历代码 for(int i=hd[x],y;i;i=nt[i]) if(y=vr[i]...)
改成 for(int i=hd[图编号][x],y;i;i=nt[i]) if(y=vr[i]...)
, 就可以很好地管理多个图了, 具体原理可以参考下 我的博客。
以下简单地用 Kosaraju 算法实现了 LuoguP3387缩点。
#include<bits/stdc++.h>
using namespace std;
const int N = 1e4+15;
const int M = 1e5+15;
int n, m, val[N], val2[N];
int ct, hd[3][N], nt[M*6], vr[M*6];
void ad(int id,int a,int b) {
vr[++ct]=b, nt[ct]=hd[id][a], hd[id][a]=ct;
}
int deg[N];
int sccno[N], scccnt;
int s[N], tp, vis[N];
void dfs1(int x) {
vis[x] = 1;
for(int i=hd[1][x],y;i;i=nt[i]) if(!vis[y=vr[i]]) dfs1(y);
s[++tp] = x;
}
void dfs2(int x) {
sccno[x] = scccnt;
for(int i=hd[0][x],y;i;i=nt[i]) if(!sccno[y=vr[i]]) dfs2(y);
}
void kosaraju() {
scccnt = 0;
for(int i=1;i<=n;++i)
if(!vis[i]) dfs1(i);
for(int i=n;i>=1;--i)
if(!sccno[s[i]]) {
++scccnt;
dfs2(s[i]);
}
}
int f[N];
void topo() {
int q[N] = {0};
for(int i=1;i<=scccnt;++i) if(!deg[i]) f[q[++q[0]]=i] = val2[i];
for(int h=1;h<=q[0];++h) {
int x=q[h];
for(int i=hd[2][x];i;i=nt[i]) {
int y=vr[i];
f[y] = max(f[y], f[x]+val2[y]);
if(--deg[y] == 0) q[++q[0]] = y;
}
}
}
int main()
{
scanf("%d%d", &n, &m);
for(int i=1;i<=n;++i) scanf("%d", &val[i]);
for(int i=0,x,y;i<m;++i) {
scanf("%d%d",&x,&y); ad(1,x,y); ad(0,y,x);
}
kosaraju();
for(int x=1;x<=n;++x) {
val2[sccno[x]] += val[x];
for(int j=hd[1][x];j;j=nt[j]) {
int y=vr[j];
if(sccno[x] != sccno[y]) ad(2,sccno[x],sccno[y]), ++deg[sccno[y]];
}
}
topo();
int ans = 0;
for(int i=1;i<=scccnt;++i) ans=max(ans, f[i]);
cout << ans;
return 0;
}