kosaraju 和 tarjan算法详解(强连通分量)

定 义
在有向图G中,如果任意两个不同的顶点相互可达,则称该有向图是强连通的。
有向图G的极大强连通子图称为G的强连通分支。
 转置图的定义:将有向图G中的每一条边反向形成的图称为G的转置GT 。(注意到原图和GT 的强连通分支是一样的)

Korasaju算法

 1.深度优先遍历G,算出每个结点u的结束时间f[u],起点如何选择无所谓。

    每个结点的结束时间和开始时间是dfs序,开始时间是此点第一次被遍历到时,结束时间为此点已经没法拓展,从栈中弹出,即已经遍历结束,不懂dfs序,可以看这个dfs序
 2.深度优先遍历G的转置图GT ,选择遍历的起点时,按照结点的结束时间从大到小进行。遍历的过程中,
     一边遍历,一边给结点做分类标记,每找到一个新的起点,分类标记值就加1。

    如果此节点已经在反图中遍历过,就不再从它遍历,挑选下一个结束时间晚的
 3. 第2步中产生的标记值相同的结点构成深度优先森林中的一棵树,也即一个强连通分量

至于证明。。。emmm,我没看懂,就自己想了一下

 如果正图中 b->a,那么反图中为 a->b,而b的结束时间一定比a要晚,所以先遍历b,如果b->a,则说明在原图中a->b


 

#include
using namespace std;
int f[1000][1000];//存储正图 
int rf[1000][1000];//存储反图 
int vis[1000];
stack s;//用来存储节点离开时间 
stack s1[1000];
int n;
//对原图dfs,找出每个节点的离开时间,用栈存储,直接pop,就不用再逆序 
void dfs(int a){
	vis[a]=1;
	for(int i=1;i<=n;i++){
		if(f[a][i]&&!vis[i]){
			dfs(i);
		}
	}
	s.push(a);
}
void rdfs(int a,int k){
	vis[a]=1;
	for(int i=1;i<=n;i++){
		if(rf[a][i]&&!vis[i]){
			rdfs(i,k);
		}
	}
	s1[k].push(a);//对每个连通分量分支,记录下来 
}
int main(){
	int m;
	cin>>n>>m;
	for(int i=1;i<=m;i++){
		int x,y;
		cin>>x>>y;
		f[x][y]=1;
		rf[y][x]=1;
	}
	//对原图dfs 
	for(int i=1;i<=n;i++){
		if(!vis[i])
		dfs(1);
	}
	memset(vis,0,sizeof(vis));
	int k=0;
	//对反图dfs 
	while(!s.empty()){
		if(!vis[s.top()])//节点按照节点离开时间从大到小遍历 
		rdfs(s.top(),++k);//计算连通分量个数 
		s.pop();
	} 
	cout<

用邻接表写的代码(老师写的)

#include 
#include 
const int V = 1e4 + 7, E = 5e4 + 7;
int hd[V], to[E], fr[E], nt[E], pr[E], tl[E];
int n, m, id[V], vis[V], cnt[V], scc;
bool out[V];
void dfs(int u, int &clk) {//用引用,减少全局变量的使用
	id[u] = 1;
	for (int i = hd[u]; i; i = nt[i])
		if (!id[to[i]]) dfs(to[i], clk);
	vis[++clk] = u;//当一个节点是叶子节点时,记录结束时间且只记录结束时间
}
void rdfs(int v, int clk) {
	id[v] = clk, ++cnt[clk];
	for (int i = tl[v]; i; i = pr[i])
		if (!id[fr[i]]) rdfs(fr[i], clk);
}
void Korasaju() {
	for (int i = 1, clk = 0; i <= n; ++i) if (!id[i]) dfs(i, clk);//最晚结束时间必然是n
	for (int i = 1; i <= n; ++i) id[i] = 0;
	for (int i = n; i; --i)
		if (!id[vis[i]]) rdfs(vis[i], ++scc);
}
int main() {
	scanf("%d%d", &n, &m);
	for (int i = 1, u, v; i <= m; ++i) {
		scanf("%d%d", &u, &v);
		nt[i] = hd[u], pr[i] = tl[v], hd[u] = tl[v] = i;
		to[i] = v, fr[i] = u;//同时存储正向与反向图
	}
	int ans = 0;
	Korasaju();
	//自此,产生了一个以强连通分量编号作为结点,构成的一个DAG图 
	for (int i = 1; i <= n; ++i)
		for (int j = hd[i]; j; j = nt[j])
			if (id[i] != id[to[j]]) out[id[i]] = true;
//如果度用++统计的话,需要考虑两个连通分量之间存在重复边(此题并不需要)
	for (int i = 1; i <= scc; ++i)
		if (!out[i]) {
			if (ans) { puts("0"); return 0; }
			ans = cnt[i];
		}
	printf("%d\n", ans);
	return 0;
}

tarjan算法(很重要)

low数组和dfn数组,dfn是第一次搜到此点的时间戳,low值是此点所连接的点中最小dfn

栈用来存储搜索到的点

沿着起点一直搜索,如果搜到返祖边,则将此点的low值更改,low[u]=min(low[u],dfn[v])   

至于为什么是dfn[v],不是low[v]?

low[v]并不是最终结果,不该使用,就算low[v]=dfn[v],也不使用他

当此点无法再搜下去,比较dfn与low值,相等则说明栈中从此点一直到栈顶为一个连通分量,将这些从栈中弹出,然后回溯,回溯时比较点的low值,再次更新low值,low[u]=min(low[u],low[v]) 

至于缩点只要将一个分量的染色就好了

受欢迎的牛

有用的定理

有向无环图中唯一出度为0的点,一定可以由任何点出发均可达(由于无环,所以从任何点出发往前走,必然终止于
一个出度为0的点)

因为强连通所有点都可互相到达,可以把强连通分量缩成一个点,如果有唯一的一个点的出度为0,那么这个分量的个数即为答案

如果不止一个出度为0,则无解。

#include
#include
#include
#include 
using namespace std;
int n,m;
const int e=1e4+4;
//标记时间戳 
int low[e],dfn[e],index;
//tarjan中的栈,和判断是否在栈中 
stack s;
bool ins[e];
//标记所属分量,用于缩点 
int col[e];
//计算有几个分量 
int cnt;
//每个分量中的成员 
vector ve[e];
//邻接表 
struct edge{
	int to,next;
}; 
edge f[5*e];
int head[e],id; 
void add(int from,int to){
	f[++id].next=head[from];
	f[id].to=to;
	head[from]=id;
}
//Tarjan 
void Tarjan(int u){
	dfn[u]=low[u]=++index;
	s.push(u);
	ins[u]=1;
	for(int i=head[u];i;i=f[i].next){
		int v=f[i].to;
		if(!dfn[v])//没有深搜的点
			Tarjan(v),low[u]=min(low[u],low[v]);//回溯阶段,无法继续探索,则从底往上逐级返回//low值 
		else if(ins[v])
			low[u]=min(low[u],dfn[v]);//在栈中的点
	}
	//当此点全部探索完成,low值依然没有改变,说明为联通分量的根节点 ,可以出栈 
	if(dfn[u]==low[u]){
		++cnt;
		int t;
		do{
			t=s.top();
			s.pop();
			ins[u]=0;//标记出栈 
			ve[cnt].push_back(t);
			col[t]=cnt;//缩点(染色标记) 
		}while(t!=u); 
	}
}
int main(){
   cin>>n>>m;
   for(int i=1;i<=m;i++){
   	int a,b;
   	cin>>a>>b;
   	add(a,b);
   }
   for(int i=1;i<=n;i++){
   	if(!dfn[i])
   	  Tarjan(i);
   } 
   int out[e]={0};//统计每个联通块的出度 
   for(int i=1;i<=n;i++){
   	  for(int j=head[i];j;j=f[i].next){
   	  	   int to=f[i].to;
   	  	   if(col[i]!=col[to])//不是同一个连通块,则出度+1 
   	  	     ++out[col[i]];
		} 
   }
   int sum=0,ans;
   for(int i=1;i<=cnt;i++){
   	  if(!out[i])  sum++,ans=ve[i].size();
   } 
   if(sum==1)
     cout<

对于tarjan中的栈的用处,有个博客写的特别详细

https://blog.csdn.net/qq_38234381/article/details/79981531

用数组模拟栈的写法(老师写的)

#include 
#include 
const int V = 1e4 + 7, E = 5e4 + 7;
//hd-head, fr-from, nt-next, pr-pre, tl-tail
int hd[V], to[E], nt[E], lbl[V], cnt[V], low[V], dfn[V], stk[V], tp, scc;
bool out[V];

void Tarjan(int u, int &clk){
	low[u] = dfn[u] = ++clk;
	stk[++tp] = u;
	for (int i = hd[u]; i; i = nt[i])
		if (not dfn[to[i]])
			Tarjan(to[i], clk), low[u] = std::min(low[u], low[to[i]]);
		else if (not lbl[to[i]])
			low[u] = std::min(low[u], dfn[to[i]]);
	if (low[u] == dfn[u])
		for (++scc; stk[tp+1] != u; --tp)
			lbl[stk[tp]] = scc, ++cnt[scc];
}

int main() {
	int n, m;
	scanf("%d%d", &n, &m);
	for (int i = 1, u, v; i <= m; ++i) {
		scanf("%d%d", &u, &v);
		nt[i] = hd[u], hd[u] = i, to[i] = v;
	}
	for (int i = 1, clk = 0; i <= n; ++i)
		if (not lbl[i]) Tarjan(i, clk);
	for (int i = 1; i <= n; ++i)
		for (int j = hd[i]; j; j = nt[j])
			if (lbl[i] != lbl[to[j]]) out[lbl[i]] = true;
	int ans = 0;
	for (int i = 1; i <= scc; ++i)
		if (not out[i]) {
			if (ans) { puts("0"); return 0; }
			ans = cnt[i];
		}
	printf("%d\n", ans);
	return 0;
}

缩点+建图

用pair存储每条边的两个端点,重点在于处理自环边和重复边,用sort处理,重新建图

#include 
#include 
#include 
const int N = 5e5 + 7;
int hd[N], to[N], fr[N], nx[N], stk[N], tp, scc;
int low[N], dfn[N], id[N], sum[N], w[N], ans;
bool bar[N], inq[N], stop[N];
std::pair eg[N];
void Tarjan(int u, int &clk) {
	low[u] = dfn[u] = ++clk;
	stk[++tp] = u;
	for (int i = hd[u], v; i; i = nx[i])
		if (!dfn[v=to[i]])
			Tarjan(v, clk), low[u] = std::min(low[u], low[v]);
		else if (!id[v])
			low[u] = std::min(low[u], dfn[v]);
	if (low[u] == dfn[u])
		for (++scc; stk[tp+1] != u; --tp)
			id[stk[tp]] = scc, sum[scc] += w[stk[tp]], stop[scc] |= bar[stk[tp]];
}
int dis[N];
void spfa(int s) {
	std::queue que;
	que.push(s), ans = dis[s] = sum[s];
	while (que.size()) {
		int u = que.front(); que.pop();
		inq[u] = false;
		for (int i = hd[u]; i; i = nx[i]) {
			int v = to[i], tot = dis[u] + sum[v];
			if (dis[v] < tot) {
				dis[v] = tot;
				if (stop[v]) ans = std::max(ans, dis[v]);
				if (not inq[v]) que.push(v);
			}
		}
	}
}
int main() {
	int n, m, s, p;
	scanf("%d%d", &n, &m);
	for (int i = 1, u, v; i <= m; ++i) {
		scanf("%d%d", &u, &v);
		nx[i] = hd[u], hd[u] = i, to[i] = v, fr[i] = u;
	}
	for (int i = 1; i <= n; ++i) scanf("%d", w+i);
	scanf("%d%d", &s, &p);
	for (int i = 1, x; i <= p; ++i)
		scanf("%d", &x), bar[x] = true;
	for (int i = 1, clk = 0; i <= n; ++i) if (!dfn[i]) Tarjan(i, clk);
	//缩点建新图 
	for (int i = 1; i <= m; ++i)
		eg[i].first = id[fr[i]], eg[i].second = id[to[i]];
	std::sort(eg+1, eg+m+1);
	for (int i = 1; i <= scc; ++i) hd[i] = 0;
	for (int i = 1; i <= m; ++i) {
		int u = eg[i].first, v = eg[i].second;
		int uu = eg[i-1].first, vv = eg[i-1].second;
		if (u!=v && (u!=uu || v!=vv))//清除自环边和重复边
			nx[i] = hd[u], hd[u] = i, to[i] = v;
	}
	//在新图上直接跑spfa
	spfa(id[s]);
	printf("%d\n", ans); 
	return 0;
}

 

你可能感兴趣的:(图)