班长竞选 || Kosaraju算法

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

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

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

Sample input
2
4 3
3 2
2 0
2 1

3 3
1 0
2 1
0 2

Sample output
Case 1: 2
0 1
Case 2: 2
0 1 2

解题思路
在这里插入图片描述

我们可以知道,1投票给2,2投票给3,那么3得到了2票。
所以一个连通分量里面的所有点的票数都是相同的。

所以我们可以把连通分量压缩成点,然后可以知道 : 最大票数的点肯定在出度为0的点上,如果我们把压点后的图给求个反图,那么就是入度为0的点里存在最大的票。

强连通分量(scc)

利用Kosaraju算法求scc

压点后求反图,然后计算最大票数

Kosaraju算法: DFS1次求逆后序序列,根据逆后序序列DFS遍历染色SCC。

代码实现

#include<iostream>
#include<vector>
#include<algorithm>
#define nmax 5005
#include<cstdio>
using namespace std;
vector<int> G1[nmax], G2[nmax], G3[nmax];  //G1--原图  G2--反图  G3--scc的反图
int N, M, A, B;
bool vis[nmax];
int scc_size;   //SCC的个数
int c[nmax];   //该点属于哪个scc
int reder;       //染色scc
int dfn[nmax];   //记录序列
int dfni;        //索引
int sum[nmax];   //入度为0 的scc的票数
int degree[nmax]; //入度
bool G3vis[nmax][nmax];  //G3存反图时避免重复存入   /   标记
struct scc { vector<int> ppp; };  //缩点
scc Scc[nmax];

void add(int u, int v, int g)  //g=1时加G1,g==2时加G2 ,g==3时加G3
{
	if (u == v)  return;
	if (g == 1)
		G1[u].push_back(v);
	if (g == 2)
		G2[u].push_back(v);
	if (g == 3)
		G3[u].push_back(v);
}

void dfs1(int x)    //求后序列
{
	vis[x] = 1;
	for (int i = 0; i < G1[x].size(); i++)
		if (vis[G1[x][i]] == 0) dfs1(G1[x][i]);
	dfn[dfni++] = x;
}

void dfs2(int x)   //遍历逆后序列
{
	c[x] = reder;
	for (int i = 0; i < G2[x].size(); i++)
		if (c[G2[x][i]] == 0)  dfs2(G2[x][i]);
}

void buildG()    //构造图
{
	for (int i = 1; i <= M; i++)
	{

		scanf("%d %d",&A,&B);
		add(A, B, 1);
		add(B, A, 2);
	}
}

void ini()  //初始化
{
	for (int i = 0; i < nmax; i++)
	{
		G1[i].clear();
		G2[i].clear();
		dfn[i] = 0;
		c[i] = 0;
		vis[i] = 0;
		for (int j = 0; j <nmax; j++)
			G3vis[i][j] = 0;
	}
	for (int i = 0; i <= scc_size; i++) 
	{G3[i].clear();
	sum[i] = 0;
	Scc[i].ppp.clear();
	degree[i]=0;
	}
	dfni = 0;
	reder = 0;
	scc_size=0;
}

void kosaraju()
{
	//求后序列
	for (int i = 0; i < N; i++)
	{
		if (vis[i] == 0)  dfs1(i);
	}
	//dfs 逆后序 ,得到scc
	for (int i = N-1; i >= 0; i--)
		if (c[dfn[i]] == 0) ++reder, dfs2(dfn[i]);  //reder染色SCC
}

void suodian_and_G3()   //缩点和创建G3
{
	for (int i = 0; i < N; i++)
	{
		Scc[c[i]].ppp.push_back(i);
		for (int j = 0; j < G1[i].size(); j++)
		{
			if (c[i] != c[G1[i][j]])  //注意这里是scc的反图
			{
				if (G3vis[G1[i][j]][i] == 0)  //判断有没有加入过
				{
					add(c[G1[i][j]],c[i], 3);
					degree[c[i]]++;   //scc的反图入度更新
					G3vis[G1[i][j]][i]++;
				}
			}
		}
	}
}

//SCC求反图是为了这里好求票数
int dfs3(int x)  //求第i个SCC的票数
{
	vis[x] = 1;
	int ans = Scc[x].ppp.size();
	for (int i = 0; i < G3[x].size(); i++)
	{
		if (vis[G3[x][i]] == 0) ans+=dfs3(G3[x][i]);
	}
	return ans;
}

int main()
{
	ios::sync_with_stdio(0);
	int count = 0;
	int T;

	scanf("%d",&T);
	for( int Ti=0;Ti<T;Ti++)
	{

		scanf("%d %d",&N,&M);
		count++;
		buildG();
		kosaraju();
		suodian_and_G3();
		scc_size = reder;
		for (int i = 1; i <= scc_size; i++) //求每个SCC的票数
		{
			if (degree[i] == 0) {
				for (int k = 0; k <= scc_size; k++)
					vis[k] = 0;
				sum[i] = dfs3(i)-1;
			}
		}
		int smax = 0;
		for (int i = 1; i <= scc_size; i++) //找出最大的
		{
			smax = max(sum[i], smax);
		}

		int res[nmax]; //答案
		int resi = 0;
		for (int i = 1; i <= scc_size; i++)
		{
			if (sum[i] == smax)  //找到最大的SCC
			{
				for (int j = 0; j < Scc[i].ppp.size(); j++)
				{
					res[resi++] = Scc[i].ppp[j];
				}
			}
		}

		sort(res, res + resi);

		printf("Case %d: %d\n",count,smax);

		printf("%d",res[0]);
		for (int i = 1; i < resi; i++)
		{

			printf(" %d",res[i]);
		}

		printf("\n");
		ini();
	}

	return 0;
}

小结
Kosaraju算法是一个很精妙的算法,2遍dfs可以求出不同颜色的SCC,然后把SCC压点,取压点后的图的反图,便于dfs3求得sum,然后把最大的压点SCC,提取出来他的点,排序后输出。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值