对于 "从每个点出发, 将其能到达的点标记为一个强连通分量" 这个算法, 其实搞出来的是原图缩点之后的一条链, 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;
}