C-班长竞选
一、题目描述
大学班级选班长,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
二、思路与算法
本题主要算法为Kosaraju算法。
Kosaraju算法中,首先dfs一次确定原图的逆后序序列,之后第二遍 dfs,在反图中按照逆后序序列进行遍历。
(反图即将原图中的有向边反向)
(每次由起点遍历到的点即构成一个 SCC)
Kosaraju算法主要作用就是寻找图中所有SCC,所以如果需要求SCC,可以优先考虑Korasaju算法。
本题中为了求最高票数和获得最高票数的人,所以是有向图,需要先求出 SCC 再进行缩点,即将互相可达与单向可达分开考虑。
• 缩点后,不难发现对于属于第 i 个 SCC 的点来说,答案分为两部分,令 SCC[i] 表示第 i 个 SCC 中点的个数。
(1)当前 SCC 中的点,ans += SCC[i] – 1(去除自己)
(2)其它 SCC 中的点 SUM ( SCC[ j] ),其中 j 可到达 i
最终结果一定在于出度为0的SCC中。(可用反证法证明)
因此我们计算出反图,对每个入度为 0 的点进行 dfs,计算其 能到达的点的 SUM(SCC[ j]),即可得到答案。
三、代码实现
#include<cstdio>
#include<string.h>
#include<vector>
#include<algorithm>
using namespace std;
int n,c[5500],dfn[5500],vis[5500],dcnt=0,scnt=0;
//dcnt - dfs序计数,scnt - scc计数
//dfn[i] - dfs后序列中第i个点
//c[i] - i号点所在的scc编号
vector<int> G1[5500];
vector<int> G2[5500]; //G1 - 原图,G2 - 反图
vector<int> G3[5500]; //scc图
int N=0,M=0,T=0; //N个点,M条边,T组数据
int in_deg[5500]={0};//记录各点入度
int result=0;
vector<int> S[5500];//存储每个scc中的元素
void dfs1(int x){
vis[x]=1;
for(int i=0;i<G1[x].size();i++){
int y=G1[x][i];
if(!vis[y]){dfs1(y); }
}
dfn[++dcnt]=x;
}
void dfs2(int x){
c[x]=scnt;
for(int i=0;i<G2[x].size();i++){
int y=G2[x][i];
if(!c[y]){dfs2(y); }
}
}
int dfs3(int x){//计算入度为0的点的sum
vis[x]=1;//打标记
int weight=S[x].size();//所在scc的点数
for(int i=0;i<G3[x].size();i++){
if(vis[G3[x][i]]==0){//没有访问过
weight=weight+dfs3(G3[x][i]);
}
}
return weight;
}
void kosaraju(){
//初始化
dcnt=0; scnt=0;
memset(c,0,sizeof(c));
memset(vis,0,sizeof(vis));
//第一遍dfs
for(int i=0;i<=N;i++){
if(!vis[i]){dfs1(i); }
}
//第二遍
for(int i=N-1;i>=1;i--){
if(!c[dfn[i]]){
++scnt;
dfs2(dfn[i]);
}
}
}
int main(){
scanf("%d",&T);
for(int i=1;i<=T;i++){
scanf("%d %d",&N,&M);
for(int j=0;j<N;j++){
G1[i].clear(); G2[i].clear();
}
for(int j=0;j<M;j++){
int t1=0,t2=0;
scanf("%d %d",&t1,&t2);
G1[t1].push_back(t2);
G2[t2].push_back(t1);
}
kosaraju();
//构建scc图
for(int j=0;j<N;j++){
S[c[j]].push_back(j);//加入相应scc中
for(int k=0;k<G1[j].size();k++){
if(c[j]!=c[G1[j][k]]){//j通往的点若不在同一个scc中
G3[c[G1[j][k]]].push_back(c[j]);//这里一定注意!!别忘了!
in_deg[c[j]]++;
}
}
}
for(int j=1;j<=scnt;j++){//每个scc中去重
sort(G3[j].begin(),G3[j].end());
G3[j].erase(unique(G3[j].begin(),G3[j].end()),G3[j].end());
}
//反图中入度为0的是最终结果
int sum[5500];
memset(sum,0,sizeof(sum));
for(int j=1;j<=scnt;j++){
if(in_deg[j]==0){//入度为0
memset(vis,0,sizeof(vis));
sum[j]=dfs3(j)-1;
}
}
result=0;
for(int j=1;j<=scnt;j++){
if(result<sum[j]){
result=sum[j];
}
}
int zero[5500];
int num=0;
for(int j=1;j<=scnt;j++)
{
if(sum[j]==result)
{
for(int k=0;k<S[j].size();k++)
{
zero[num]=S[j][k];
num++;
}
}
}
sort(zero,zero+num);//升序排列
printf("Case %d: %d\n",i,result);
int j=0;
for(j=0;j<num-1;j++){
printf("%d ",zero[j]);
}
printf("%d\n",zero[j]);
for(int j=0;j<5500;j++){//为处理下一组数据准备
G1[j].clear(); G2[j].clear(); G3[j].clear(); S[j].clear();
vis[j]=0; c[j]=0; dfn[j]=0; in_deg[j]=0;
}
}
return 0;
}
四、经验与总结
- 因为有很多循环,所以一定要注意每个循环的起始和结尾!从0还是1开始,最后有没有等于号,同时要与存储和处理的编号相匹配。
- 注意!!容易忘记,构造scc图时,一定要把边的两个端点都处理了,否则会遗漏点!(例如有的点出度为0,如果不在被到达的时候加入强连通分支中,那么它就不会被处理)
- 各种数据为准备处理下一组数据而进行的初始化,一定要仔细!要在合适的位置清空或置常数!