强连通分量
写在前面:我是一只蒟蒻
今天,我们来介绍一下强连通分量这个玩意儿
有向图的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
输出格式:
第一行:单独一个整数,表示明星奶牛的数量
输入输出样例
3 3 1 2 2 1 2 3
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 认识 y(y 不一定认识 x,例如President同志) 。
注:原文zz敏感内容已替换
输出格式:仅包含一行一个实数,保留小数点后面 666 位,表示最大概率。
输入输出样例
5 4 1 2 1 3 1 4 1 5
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,结束啦~~