欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

强连通分量(WEEK8-C)

程序员文章站 2022-07-12 18:10:29
...

在有向图中,若一个子图中任意两点之间可连通,则称极大的强连通子图为强连通分量(SCC),如:
强连通分量(WEEK8-C)

要求一个有向图的SCC,必须了解dfs序列的前序序列、后序序列、逆后序序列(后序序列的逆序)
前序:dfs过程中第一次到达点x的次序,用d[x]表示。
后序:dfs中x点遍历完成的次序,用f[x]表示。

下面为求各序列的过程:
强连通分量(WEEK8-C)
强连通分量(WEEK8-C)
强连通分量(WEEK8-C)
求解scc的算法:kosaraju算法,算法步骤:
强连通分量(WEEK8-C)
例如:
强连通分量(WEEK8-C)

代码:
强连通分量(WEEK8-C)

应用题目:C - 班长竞选

大学班级选班长,N 个同学均可以发表意见 若意见为 A B 则表示 A 认为 B 合适,意见具有传递性,即 A 认为 B 合适,B 认为 C 合适,则 A 也认为 C 合适 勤劳的 TT 收集了M条意见,想要知道最高票数,并给出一份候选人名单,即所有得票最多的同学,你能帮帮他吗?

本题有多组数据。第一行 T 表示数据组数。每组数据开始有两个整数 N 和 M (2 <= n <= 5000, 0 <m <= 30000),接下来有 M 行包含两个整数 A 和 B(A != B) 表示 A 认为 B 合适。

对于每组数据,第一行输出 “Case x: ”,x 表示数据的编号,从1开始,紧跟着是最高的票数。 接下来一行输出得票最多的同学的编号,用空格隔开,不忽略行末空格!

2
4 3
3 2
2 0
2 1

3 3
1 0
2 1
0 2
Case 1: 2
0 1
Case 2: 2
0 1 2

解题思路

求出强连通分量后,进行缩图,将属于一个SCC的点用连通分量编号表示,存储在edges3[]中。

缩点后,对于第i个SCC,答案分为两部分:
当前 SCC 中的点,ans += SCC[i] – 1(去除自己);
其它 SCC 中的点: SUM ( SCC[ j] ),其中 j 可到达 i。

又思考一下,发现最后答案一定出现在出度为 0 的 SCC中,因此我们将边反向,对每个入度为 0 的点进行 dfs,计算其能到达的点的 SUM(SCC[ j]),即可得到答案。

代码

#include<iostream>
#include<algorithm>

#include<vector>
using namespace std;

//有向图
const int maxn=5050;
const int maxm=30010;
int vis[maxn],dfn[maxn],dcnt;
struct edge
{
	int v,next;
}edges[maxm],edges2[maxm],edges3[maxm];//正图,反图 ,缩图 
int head[maxn],tot,head2[maxn],tot2,head3[maxn],tot3,c[maxn],cnt,deg[maxn],vis2[maxn];
int maxV=0,cn[maxn],sum[maxn],res;//存储最大票数、每个连通分量有几个节点 
void init()
{
	for(int i=0;i<maxn;i++)
		head[i]=-1,head2[i]=-1,head3[i]=-1,vis[i]=0,c[i]=-1,deg[i]=0,vis2[i]=0,cn[i]=0,sum[i]=0,dfn[i]=0;
	tot=0,tot2=0,tot3=0,cnt=0,res=0,dcnt=0;
}
void addEdge(int _u,int _v,int type)
{
	if(type==1)//原图 
	{
		edges[tot].v=_v;
		edges[tot].next=head[_u];
		head[_u]=tot;
		tot++;
	}
	else if(type==2)//反图 
	{
		edges2[tot2].v=_v;
		edges2[tot2].next=head2[_u];
		head2[_u]=tot2;
		tot2++;
	}
	else if(type==3)//缩图的反图 
	{
		edges3[tot3].v=_v;
		edges3[tot3].next=head3[_u];
		head3[_u]=tot3;
		tot3++;
	} 
}

void dfs1(int s)
{
	vis[s]=1;
	for(int j=head[s];j!=-1;j=edges[j].next)
	{
		int v=edges[j].v;
		if(!vis[v])
			dfs1(v);
	}
	
	dfn[++dcnt]=s;//计算逆后序序列 
}
void dfs2(int s)
{
	c[s]=cnt;
	cn[cnt]++;//第cnt个连通分量的个数++ 
	for(int i=head2[s];i!=-1;i=edges2[i].next)
		if(c[edges2[i].v]==-1)
			dfs2(edges2[i].v);
}
void dfs3(int s)
{
	
	vis2[s]=1;
	for(int i=head3[s];i!=-1;i=edges3[i].next)
		if(!vis2[edges3[i].v])
		{
			maxV+=cn[edges3[i].v];
			dfs3(edges3[i].v);
		}
}
void kosarajo(int n)
{
	
	for(int i=0;i<n;i++)
	{
		if(!vis[i])
			dfs1(i);
	}
	for(int i=n;i>=1;i--)//按照逆后序顺序遍历,记录每个点属于哪个强连通分量 
		if(c[dfn[i]]==-1)
		{
			++cnt;
			dfs2(dfn[i]);
		}
}

void degEzero()
{
	for(int i=1;i<=cnt;i++)
	{
		if(deg[i]==0)
		{
			for(int i=0;i<maxn;i++)vis2[i]=0;
			maxV=cn[i]-1;
			dfs3(i);
			sum[i]=maxV;//存储该连通分量可到达的点的个数
			if(res<maxV)res=maxV; 
		}
	}
}
int main()
{
	int t,tt=1;
	scanf("%d",&t);
	while(t--)
	{
		
		int n,m;
		scanf("%d %d",&n,&m);
		init();
		while(m--)
		{
			int a,b;
			scanf("%d %d",&a,&b);
			addEdge(a,b,1);
			addEdge(b,a,2);//反图 
		}
		
		
		//找到强连通分量 
		kosarajo(n);
		//for(int i=0;i<n;i++)printf("s%d\n",c[i]);
		//建立缩图的反图 
		for(int i=0;i<n;i++)
			for(int j=head[i];j!=-1;j=edges[j].next)
			{
				int v=edges[j].v;
				if(c[i]!=c[v])
				{
					addEdge(c[v],c[i],3);//若i v属于不同的连通分量,就建立c[v]->c[i]
					deg[c[i]]++;//入度增加 
				}
			}
			
		//找到入度为0的连通分量并计算可到达的点的个数 
		degEzero();
		printf("Case %d: %d\n",tt,res);
		int k=0;
		for(int i=0;i<n;i++)
		{
			if(sum[c[i]]==res)
			{
				if(k==0){printf("%d",i);k=1;}
				else   printf(" %d",i);
			}
		}
		tt++;
		printf("\n");
	}
	return 0;
}