强连通分量

写在前面:我是一只蒟蒻

今天,我们来介绍一下强连通分量这个玩意儿

有向图的DFS:与无向图的差别就在于,搜索时只能顺边。所以,有时的搜索树会不止一个根,因为,从一点出发不一定能走完所有点。
在一个搜索图中,每条有向边(x,y)一定是以下三(四)种之一:
父子边——具有父子关系
返祖边——指向其祖先
横叉边——dfn[y]<dfn[x]
强连通图
定义
给定一张有向图。若对于图中任意两节点x,y,既存在x到y的路径,也存在y到x的路径,则称该有向图是“强连通图”。
强连通分量(SCC)
即有向图中的极大强连通子图。

思想实现
Tarjan算法是基于对图深度优先搜索的算法,每个强连通分量为搜索树中的一棵子树。搜索时,把当前搜索树中未处理的节点加入一个堆栈,回溯时可以判断栈顶到栈中的节点是否为一个强连通分量。
定义DFN(u)为节点u搜索的次序编号(时间戳),Low(u)为u或u的子树能够追溯到的最早的栈中节点的次序号。
当DFN(u)=Low(u)时,以u为根的搜索子树上所有节点是一个强连通分量。


下面我们对这个图进行一下模拟

 

 

 

 

 这样,我们就完成了找强连通分量的过程,下面我们来看一下核心代码,

 1 void tarjan(int x){
 2     dfn[x]=low[x]=++num;//更新 
 3     s[++top]=x;in[x]=1;//当前元素入栈 
 4     for(int i=head[x];i;i=e[i].next){
 5         int y=e[i].to;
 6         if(!dfn[y]){
 7             tarjan(y);
 8             low[x]=min(low[x],low[y]);
 9         }else{
10             if(in[y])low[x]=min(low[x],dfn[y]);//如果被访问过且还在栈中,更新 
11         }
12     }
13     if(dfn[x]==low[x]){//是强连通分量 
14         cnt++;int c;//颜色块加一 
15         do{
16             c=s[top--];
17             a[c]=cnt;//进行染色处理 
18             v[cnt]++;//当前连通块的数量统计 
19             in[c]=0;//出栈标记 
20         }while(x!=c);
21     }
22 }

 

 

OK,我们就结束了基本讲解,下面是两道基本栗题。

1、最受欢迎的牛

题目描述

每头奶牛都梦想成为牛棚里的明星。被所有奶牛喜欢的奶牛就是一头明星奶牛。所有奶

牛都是自恋狂,每头奶牛总是喜欢自己的。奶牛之间的“喜欢”是可以传递的——如果A喜

欢B,B喜欢C,那么A也喜欢C。牛栏里共有N 头奶牛,给定一些奶牛之间的爱慕关系,请你

算出有多少头奶牛可以当明星。

输入输出格式

输入格式:

 第一行:两个用空格分开的整数:N和M

 第二行到第M + 1行:每行两个用空格分开的整数:A和B,表示A喜欢B

输出格式:

 第一行:单独一个整数,表示明星奶牛的数量

输入输出样例

输入样例#1: 
3 3
1 2
2 1
2 3
输出样例#1: 
1

说明

只有 3 号奶牛可以做明星

【数据范围】

10%的数据N<=20, M<=50

30%的数据N<=1000,M<=20000

70%的数据N<=5000,M<=50000

100%的数据N<=10000,M<=50000

 

题目分析:首先,题意分析,我们可以知道,这种喜欢是可以传递的。所以,当构成一个环时,那么,这当中的牛一定会互相喜欢,形成强连通图。
所以,我们就可以找出所有的强连通分量,将其缩点,重建图,判断是否连通,如果不连通,则说明没有明星牛。
如果连通,查找出度为0的个数,如果大于一个,同样无解。如果只有一个,则输出该强连通分量的个数。

下面,就是AC代码

 1 #include<iostream>
 2 #include<cstdio>
 3 #include<cstring>
 4 #include<queue>
 5 #include<cmath>
 6 #include<algorithm>
 7 #include<vector>
 8 #define N 50004 
 9 #define maxn 10007
10 using namespace std;
11 struct node{
12     int to,next;
13 }e[N];
14 int head[maxn],tot,cnt,num,low[maxn],dfn[maxn],s[maxn],top,in[maxn],v[maxn],a[maxn],p[maxn];
15 void add(int a,int b){
16     e[++tot].next=head[a];
17     e[tot].to=b;
18     head[a]=tot;
19 } 
20 void tarjan(int x){
21     dfn[x]=low[x]=++num;//更新 
22     s[++top]=x;in[x]=1;//当前元素入栈 
23     for(int i=head[x];i;i=e[i].next){
24         int y=e[i].to;
25         if(!dfn[y]){
26             tarjan(y);
27             low[x]=min(low[x],low[y]);
28         }else{
29             if(in[y])low[x]=min(low[x],dfn[y]);//如果被访问过且还在栈中,更新 
30         }
31     }
32     if(dfn[x]==low[x]){//是强连通分量 
33         cnt++;int c;//颜色块加一 
34         do{
35             c=s[top--];
36             a[c]=cnt;//进行染色处理 
37             v[cnt]++;//当前连通块的数量统计 
38             in[c]=0;//出栈标记 
39         }while(x!=c);
40     }
41 }
42 int n,m;
43 int main(){
44 //    freopen("popular.in","r",stdin);
45 //    freopen("popular.out","w",stdout);
46     scanf("%d%d",&n,&m);
47     for(int i=1;i<=m;i++){
48         int d,b;
49         scanf("%d%d",&d,&b);
50         if(d==b)continue;
51         add(d,b);
52     }
53     for(int i=1;i<=n;i++){
54         if(!dfn[i]){
55 
56             tarjan(i);
57         }
58     }
59 
60 for(int x=1;x<=n;x++){
61     for(int i=head[x];i;i=e[i].next){
62         int y=e[i].to;
63         if(a[x]!=a[y])p[a[x]]++;//统计出度 
64     }
65 }
66 int ans,nn=0;
67 for(int i=1;i<=cnt;i++)if(p[i]==0)ans=v[i],nn++;//统计出度为0的个数 
68 if(nn>1)printf("0\n");//如果不成立,则输出0 
69 else printf("%d\n",ans);//否则输出该强连通分量的个数 
70 return 0;
71 }

 

 

是不是很简单呢,我们继续切栗子。

[中山市选]杀人游戏

题目描述

一位冷血的杀手潜入Na-wiat,并假装成平民。警察希望能在N个人里面,查出谁是杀手。警察能够对每一个人进行查证,假如查证的对象是平民,他会告诉警察,他认识的人,谁是杀手,谁是平民。假如查证的对象是杀手,杀手将会把警察干掉。现在警察掌握了每一个人认识谁。每一个人都有可能是杀手,可看作他们是杀手的概率是相同的。

问:根据最优的情况,保证警察自身安全并知道谁是杀手的概率最大是多少?

输入输出格式

输入格式:

第一行有两个整数 N,M。 接下来有 M 行,每行两个整数 x,y,表示 x 认识 yy 不一定认识 x,例如President同志) 。

注:原文zz敏感内容已替换

输出格式:

仅包含一行一个实数,保留小数点后面 666 位,表示最大概率。

输入输出样例

输入样例#1:
5 4 
1 2 
1 3 
1 4 
1 5 
输出样例#1: 
0.800000

说明

警察只需要查证1。假如1是杀手,警察就会被杀。假如1不是杀手,他会告诉警察2,3,4,5谁是杀手。而1是杀手的概率是0.2,所以能知道谁是杀手但没被杀的概率是0.8

对于100%的数据有1≤N≤100000,0≤M≤300000

题目分析:

看完题,我们可以知道这道题与上道题与异曲同工之妙~~ 我们每问一个人,就可以知道那人所知道的关系。就相当于奶牛之间的喜欢。
所以我们就沿用上题思路,强连通分量缩点然后重构图,统计入度为0的点,因为找到入度为0的后,我们就可以知道其后面的所有信息。
但是,只这样考虑就可以了吗?本题还有一个特殊的地方
就是可以排除!!!
举个栗子,若一共有N个人,我们查询了一些人,使得我们知道了N-1个人的身份(都是平民,否则你就挂了),那么剩下的一个人一定是杀手(排除法),那么我们就可以少去查询一个人。
所以,我们要去寻找一个大小为1且入度为0的点((缩点后)就是一个与世隔绝的人),并且他指向的点入度不为1(就是能只通过这个人来获取信息),那么我们就可以少一次询问,则标记flag=1。
这样的话这道题就很明确了
若flag==1,ans=1-(缩点后入度为0的点-1)/n.

若flag==0,ans=1-(缩点后入度为0的点)/n



下面是AC代码~~

 

 1 #include<iostream>
 2 #include<cstdio>
 3 #include<cstring>
 4 #include<queue>
 5 #include<cmath>
 6 #include<algorithm>
 7 #include<vector>
 8 #define maxn 100007 
 9 using namespace std;
10 struct edge{
11     int to,next;
12 }ee[600007];
13 struct node{
14     int to,next;
15 }e[600007];
16 int head[maxn],last[maxn],tot,dfn[maxn],low[maxn],z[maxn],top,a[maxn],num,time,n,m,out[maxn],cnt,v[maxn];
17 bool in[maxn];
18 void add1(int a,int b){
19     ee[++tot].next=last[a];
20     ee[tot].to=b;
21     last[a]=tot;
22 } 
23 void add2(int a,int b){
24     e[++time].next=head[a];
25     e[time].to=b;
26     head[a]=time;
27 }
28 void tarjan(int x){
29     dfn[x]=low[x]=++num;
30     z[++top]=x,in[x]=1;
31     for(int i=last[x];i;i=ee[i].next){
32         int y=ee[i].to;
33         if(!dfn[y]){
34             tarjan(y);
35             low[x]=min(low[x],low[y]);
36         }else{
37             if(in[y])low[x]=min(low[x],dfn[y]);
38         }
39     }
40     if(low[x]==dfn[x]){
41         cnt++;
42         int c;
43         do{
44             c=z[top--];
45             a[c]=cnt;//染色 
46             v[cnt]++;
47             in[c]=0; 
48         }while(x!=c);
49     }
50 }
51 bool pd(int x){
52     for(int i=head[x];i;i=e[i].next){
53         if(out[e[i].to]==1)return false;//判断x指向的那个点的出边是否为1 
54     }
55     return true;
56 }
57 int main(){
58     scanf("%d%d",&n,&m);
59     for(int i=1;i<=m;i++){
60         int a,b;
61         scanf("%d%d",&a,&b);
62         add1(a,b);
63     } 
64     for(int i=1;i<=n;i++){
65         if(!dfn[i])tarjan(i);
66     }
67     for(int i=1;i<=n;i++){
68         for(int j=last[i];j;j=ee[j].next){
69             int tt=ee[j].to;
70             if(a[i]!=a[tt]){
71                 add2(a[i],a[tt]);//重建图 
72                 out[a[tt]]++;//统计出度 
73             }
74         }
75     }
76     int ans=0;
77     bool flag=0;
78     for(int i=1;i<=cnt;i++){
79         if(!flag&&out[i]==0&&v[i]==1&&pd(i)){
80             flag=1;//判断满足条件 
81         }
82         if(out[i]==0){
83             ans++;
84         }
85     }
86     if(flag)ans--;
87     printf("%.6lf",1.0-(double)ans/(double)n);
88 return 0;
89 }

OK,结束啦~~

 

 

 

posted @ 2019-02-21 10:53  惜时如金  阅读(592)  评论(0编辑  收藏  举报