有向图的强连通分量
一.定义
给定一张有向图。若对于任意两个节点x,y 既存在从x->y的路径,也存在从y->x的路径,则称该有向图为“强连通图”。
有向图的极大连通子图被称为强连通分量。
二.强连通分量的求法。
1.Tarjan算法
基于 dfs 的一种算法,每一个强连通分量为其搜索树的一棵子树,并且需要用到 栈 的数据结构。
tarjan 中两个重要的数组 dfn[] (时间戳,搜索时的序号) , low[] (表示当前能回溯到的最小的时间戳), 初始化 dfn[] = low[] = ++ cnt;
运算过程:
(1). main 函数里面用 for 循环 查找 ,若dfn[]为0,则进行 tarjan 算法。
(2). 设 v[] 数组表示是否进栈,设 栈 sta[];
(3). 对于每个搜索的 x ,对其相连的边进行搜索,若没有,递归运行,若已经入栈,说明形成了 环 ,更新 low 值;、
(4). 不断深搜的过程中,若出边的遍历完了,进行回溯,不断比较 low 值,取最小值 , 若dfn[] == low[] ,则找到了 一个强连通分量的根,然后对栈进行弹出操作,直到x 被弹出栈;
code
1 inline void tarjan(int x){ 2 dfn[x]=++ind; 3 low[x]=ind; 4 s[++top]=x; 5 v[x]=1; 6 for(int i=head[x];i;i=next[i]){ 7 int y=ver[i]; 8 if(!dfn[y]){ 9 tarjan(y); 10 low[x]=min(low[y],low[x]); 11 } 12 else if(v[y]==1){ 13 low[x]=min(low[x],dfn[y]); 14 } 15 } 16 if(low[x]==dfn[x]){ 17 int y; 18 do{ 19 y=s[top--]; 20 v[y]=0; 21 }while(y != x); 22 } 23 }
2.Kosaraju 算法
算法流程:
(1)存图时需要再存一个反图 ;
(2) 先在原图上 进行 dfs ,不断访问节点,在回溯前进行后序遍历标号
(3) 第二次进行dfs 时,需用到反图,以标号最大的顶点为起点进行遍历,遍历完后,由dfs 遍历过的顶点集合就是一个强连通分量。
p.s: 要对集合进行分类标记,每找到一个新的起点就要对 标记值进行自增;
code
1 int k=0; 2 inline void dfs(int x){ 3 v[x]=1; 4 for(int i=0;i<g[x].size();i++){ 5 if(!v[g[x][i]]) dfs(g[x][i]); 6 } 7 vs.push_back(x); 8 } 9 inline void rdfs(int x){ 10 v[x]=1; 11 fa[x]=k; 12 for(int i=0;i<rg[x].size();i++){ 13 if(!v[rg[x][i]]) rdfs(rg[x][i]); 14 } 15 } 16 inline void work(){ 17 for(int i=1;i<=n;i++){ 18 if(!v[i]) dfs(i); 19 } 20 memset(v,0,sizeof(v)); 21 for(int i=vs.size()-1;i>=0;i--){ 22 if(!v[vs[i]]) { 23 k++; 24 rdfs(vs[i]); 25 } 26 } 27 }
三.例题e.g.
No.1.受欢迎的牛[HAOI2006](来源:洛谷)
题目描述
每头奶牛都梦想成为牛棚里的明星。被所有奶牛喜欢的奶牛就是一头明星奶牛。所有奶牛都是自恋狂,每头奶牛总是喜欢自己的。奶牛之间的“喜欢”是可以传递的——如果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
思路:
看图
可以得出, b,c,d 为强连通分量,可知,分量中任意一个点被喜欢,则分量中所有点都会被喜欢,所以将图中的所有强连通分量看作一个点,则图变成一个有向无环图,且成为明星 的牛一定是出度为零的点 。
code
1 #include <bits/stdc++.h> 2 using namespace std; 3 4 const int N=50001 * 2; 5 int n,m,k=0; 6 int head[N],next[N],ver[N],tot; 7 int f[N],d[N],out[N],an[N]; 8 9 int find(){ 10 int ans=0; 11 for(int i=1;i<=k;i++){ 12 for(int j=head[f[i]];j;j=next[j]){ 13 int y=ver[j]; 14 if(!d[y]) ans++; 15 } 16 } 17 return ans; 18 } 19 20 inline void add(int x,int y){ 21 ver[++tot]=y; 22 next[tot]=head[x]; 23 head[x]=tot; 24 } 25 26 int s[N],top=0; 27 int v[N]; 28 int dfn[N],low[N],ind=0; 29 int belong[N],cnt=0; 30 31 inline void tarjan(int x){ 32 dfn[x]=++ind; 33 low[x]=ind; 34 s[++top]=x; 35 v[x]=1; 36 for(int i=head[x];i;i=next[i]){ 37 int y=ver[i]; 38 if(!dfn[y]){ 39 tarjan(y); 40 low[x]=min(low[y],low[x]); 41 } 42 else if(v[y]==1){ 43 low[x]=min(low[x],dfn[y]); 44 } 45 } 46 if(low[x]==dfn[x]){ 47 ++cnt; 48 int y; 49 do{ 50 y=s[top--]; 51 v[y]=0; 52 f[++k]=y; 53 d[y]=1; 54 an[cnt]++; 55 }while(y!=x); 56 out[cnt]=find(); 57 k=0; 58 memset(d,0,sizeof(d)); 59 } 60 } 61 62 inline int read(){//快读 63 int ans=0,f=1; 64 char s=getchar(); 65 while(!isdigit(s)) f*=(s=='-')? -1:1,s=getchar(); 66 do ans=(ans<<1)+(ans<<3)+(s^48),s=getchar(); 67 while(isdigit(s)); 68 return ans*f; 69 } 70 71 int main(){ 72 n=read(),m=read(); 73 for(int i=1;i<=m;i++){ 74 int a=read(),b=read(); 75 add(a,b); 76 } 77 for(int i=1;i<=n;i++) 78 if(!dfn[i]) tarjan(i); 79 int s=0,ans; 80 for(int i=1;i<=cnt;i++){ 81 if(!out[i]) s++,ans=i; 82 } 83 if(s==1) printf("%d",an[ans]); 84 else printf("0"); 85 return 0; 86 }
No.2 稳定婚姻(来源:洛谷)
题目描述
我国的离婚率连续7年上升,今年的头两季,平均每天有近5000对夫妇离婚,大城市的离婚率上升最快,有研究婚姻问题的专家认为,是与简化离婚手续有关。
25岁的姗姗和男友谈恋爱半年就结婚,结婚不到两个月就离婚,是典型的“闪婚闪离”例子,而离婚的导火线是两个人争玩电脑游戏,丈夫一气之下,把电脑炸烂。
有社会工作者就表示,80后求助个案越来越多,有些是与父母过多干预有关。而根据民政部的统计,中国离婚五大城市首位是北京,其次是上海、深圳,广州和厦门,那么到底是什么原因导致我国成为离婚大国呢?有专家分析说,中国经济急速发展,加上女性越来越来越独立,另外,近年来简化离婚手续是其中一大原因。
——以上内容摘自第一视频门户
现代生活给人们施加的压力越来越大,离婚率的不断升高已成为现代社会的一大问题。而其中有许许多多的个案是由婚姻中的“不安定因素”引起的。妻子与丈夫吵架后,心如绞痛,于是寻求前男友的安慰,进而夫妻矛盾激化,最终以离婚收场,类似上述的案例数不胜数。
我们已知n对夫妻的婚姻状况,称第i对夫妻的男方为Bi,女方为Gi。若某男Bi与某女Gj曾经交往过(无论是大学,高中,亦或是幼儿园阶段,i≠j),则当某方与其配偶(即Bi与Gi或Bj与Gj)感情出现问题时,他们有私奔的可能性。不妨设Bi和其配偶Gi感情不和,于是Bi和Gj旧情复燃,进而Bj因被戴绿帽而感到不爽,联系上了他的初恋情人Gk……一串串的离婚事件像多米诺骨牌一般接踵而至。若在Bi和Gi离婚的前提下,这2n个人最终依然能够结合成n对情侣,那么我们称婚姻i为不安全的,否则婚姻i就是安全的。
给定所需信息,你的任务是判断每对婚姻是否安全。
输入格式
第一行为一个正整数n,表示夫妻的对数;
以下n行,每行包含两个字符串,表示这n对夫妻的姓名(先女后男),由一个空格隔开;
第n+2行包含一个正整数m,表示曾经相互喜欢过的情侣对数;
以下m行,每行包含两个字符串,表示这m对相互喜欢过的情侣姓名(先女后男),由一个空格隔开。
输出格式
输出文件共包含n行,第i行为“Safe”(如果婚姻i是安全的)或“Unsafe”(如果婚姻i是不安全的)。
输入输出样例
2 Melanie Ashley Scarlett Charles 1 Scarlett Ashley
Safe Safe
2 Melanie Ashley Scarlett Charles 2 Scarlett Ashley Melanie Charles
Unsafe Unsafe
说明/提示
对于20%的数据,n≤20;
对于40%的数据,n≤100,m≤400;
对于100%的数据,所有姓名字符串中只包含英文大小写字母,大小写敏感,长度不大于8,保证每对关系只在输入文件中出现一次,输入文件的最后m行不会出现未在之前出现过的姓名,这2n个人的姓名各不相同,1≤n≤4000,0≤m≤20000。
思路:
我们可以将题目中的男女关系进行建图,然后求出强连通分量,判断夫妻之间是否在同一强连通分量中,若在,则不安全,反之则安全。
可以将 夫妻 关系 建为 female -> male ; 将 情人 关系建为 male-> female;
p.s. 由于原题中需要输入字符串 (string), 我们自己建立映射条件极为不方便 ,于是可以调用数据结构 map :map <string, int> g 进行映射
因为一共2*n个人 所以建图时 第 i 对夫妻,female 为 i 号节点 ,male 为i+n 号;
code
1 #include <bits/stdc++.h> 2 using namespace std; 3 4 const int N=30001; 5 map <string, int> g; 6 int n,m; 7 int head[N],next[N],ver[N],tot; 8 9 inline void add(int x,int y){ 10 ver[++tot]=y; 11 next[tot]=head[x]; 12 head[x]=tot; 13 } 14 15 int s[N],top=0; 16 int v[N]; 17 int dfn[N],low[N],ind=0; 18 int belong[N],cnt=0; 19 20 inline void tarjan(int x){ 21 dfn[x]=++ind; 22 low[x]=ind; 23 s[++top]=x; 24 v[x]=1; 25 for(int i=head[x];i;i=next[i]){ 26 int y=ver[i]; 27 if(!dfn[y]){ 28 tarjan(y); 29 low[x]=min(low[y],low[x]); 30 } 31 else if(v[y]==1){ 32 low[x]=min(low[x],dfn[y]); 33 } 34 } 35 if(low[x]==dfn[x]){ 36 ++cnt; 37 do{ 38 belong[s[top]]=cnt; 39 v[s[top]]=0; 40 }while(s[top--]!=x); 41 } 42 } 43 44 inline int read(){ 45 int ans=0,f=1; 46 char s=getchar(); 47 while(!isdigit(s)) f*=(s=='-')? -1:1,s=getchar(); 48 do ans=(ans<<1)+(ans<<3)+(s^48),s=getchar(); 49 while(isdigit(s)); 50 return ans*f; 51 } 52 53 int main(){ 54 n=read(); 55 string fe,ma; 56 for(int i=1;i<=n;i++){ 57 cin>>fe>>ma; 58 g[fe]=i; 59 g[ma]=i+n; 60 add(i,i+n); 61 } 62 m=read(); 63 for(int i=1;i<=m;i++){ 64 cin>>fe>>ma; 65 add(g[ma],g[fe]); 66 } 67 for(int i=1;i<=n*2;i++) if(dfn[i]==0) tarjan(i); 68 for(int i=1;i<=n;i++){ 69 if(belong[i]==belong[i+n]) printf("Unsafe "); 70 else printf("Safe "); 71 } 72 return 0; 73 }
No.3 消息扩散(来源:洛谷)
题目描述
有n个城市,中间有单向道路连接,消息会沿着道路扩散,现在给出n个城市及其之间的道路,问至少需要在几个城市发布消息才能让这所有n个城市都得到消息。
输入格式
第一行两个整数n,m表示n个城市,m条单向道路。
以下m行,每行两个整数b,e表示有一条从b到e的道路,道路可以重复或存在自环。
输出格式
一行一个整数,表示至少要在几个城市中发布消息。
输入输出样例
5 4 1 2 2 1 2 3 5 1
2
说明/提示
【数据范围】
对于20%的数据,n≤200;
对于40%的数据,n≤2,000;
对于100%的数据,n≤100,000,m≤500,000.
【限制】
时间限制:1s,内存限制:256M
【注释】
样例中在4,5号城市中发布消息。
思路:
题目中有重边和自环,那就判断不是重边与自环才建图,然后求强连通分量,入度为0 的顶点个数即为answer;
code
1 #include<bits/stdc++.h> 2 using namespace std; 3 const int N = 100005; 4 const int M = 500005; 5 int x,y,n,m,pointnum,tot,Ind,top,ans; 6 int head[N],nxt[M],to[M]; 7 int low[N],dfn[N],s[N]; 8 int belong[N],rd[N]; 9 bool in[N]; 10 inline int read() { 11 int ans=0,f=1; 12 char s=getchar(); 13 while(!isdigit(s)) f*=(s=='-')? -1:1,s=getchar(); 14 do ans=(ans<<1)+(ans<<3)+(s^48),s=getchar(); 15 while(isdigit(s)); 16 return ans*f; 17 } 18 19 inline void add(int x,int y) { 20 nxt[++tot]=head[x]; 21 head[x]=tot; 22 to[tot]=y; 23 } 24 25 inline void dfs(int x) { 26 low[x]=dfn[x]=++Ind; 27 s[++top]=x; 28 in[x]=true; 29 for(int i=head[x]; i; i=nxt[i]) { 30 int v=to[i]; 31 if(!dfn[v]) { 32 dfs(v); 33 low[x]=min(low[x],low[v]); 34 } else if(in[v]) low[x]=min(low[x],dfn[v]); 35 } 36 if(low[x]==dfn[x]) { 37 ++pointnum; 38 int j=-1; 39 while(j!=x) { 40 j=s[top--]; 41 belong[j]=pointnum; 42 in[j]=false; 43 } 44 } 45 } 46 47 int main() { 48 n=read(),m=read(); 49 for(int i=1; i<=m; i++) { 50 x=read(),y=read(); 51 if(x!=y) add(x,y); 52 } 53 for(int i=1; i<=n; i++) if(!dfn[i]) dfs(i); 54 for(int i=1; i<=n; i++) 55 for(int e=head[i]; e; e=nxt[e]) 56 if(belong[i]!=belong[to[e]]) 57 rd[belong[to[e]]]++; 58 for(int i=1; i<=pointnum; i++) 59 if(!rd[i]) ans++; 60 cout<<ans<<endl; 61 return 0; 62 }
No.4 间谍网络(来源:洛谷)
题目描述
由于外国间谍的大量渗入,国家安全正处于高度的危机之中。如果A间谍手中掌握着关于B间谍的犯罪证据,则称A可以揭发B。有些间谍收受贿赂,只要给他们一定数量的美元,他们就愿意交出手中掌握的全部情报。所以,如果我们能够收买一些间谍的话,我们就可能控制间谍网中的每一分子。因为一旦我们逮捕了一个间谍,他手中掌握的情报都将归我们所有,这样就有可能逮捕新的间谍,掌握新的情报。
我们的反间谍机关提供了一份资料,包括所有已知的受贿的间谍,以及他们愿意收受的具体数额。同时我们还知道哪些间谍手中具体掌握了哪些间谍的资料。假设总共有n个间谍(n不超过3000),每个间谍分别用1到3000的整数来标识。
请根据这份资料,判断我们是否有可能控制全部的间谍,如果可以,求出我们所需要支付的最少资金。否则,输出不能被控制的一个间谍。
输入格式
第一行只有一个整数n。
第二行是整数p。表示愿意被收买的人数,1≤p≤n。
接下来的p行,每行有两个整数,第一个数是一个愿意被收买的间谍的编号,第二个数表示他将会被收买的数额。这个数额不超过20000。
紧跟着一行只有一个整数r,1≤r≤8000。然后r行,每行两个正整数,表示数对(A, B),A间谍掌握B间谍的证据。
输出格式
如果可以控制所有间谍,第一行输出YES,并在第二行输出所需要支付的贿金最小值。否则输出NO,并在第二行输出不能控制的间谍中,编号最小的间谍编号。
输入输出样例
3 2 1 10 2 100 2 1 3 2 3
YES 110
4 2 1 100 4 200 2 1 2 3 4
NO 3
思路:
不难发现有两种情况:
(1). 此间谍没人能够揭发他,也不能够贿赂他,那么直接输出他的编号;
(2). 1) 没有环,则资金为没有入度那个点的钱数;
2)有环,则在环中找最小值;
code:
1 #include<bits/stdc++.h> 2 using namespace std; 3 struct ss{ 4 int next,to; 5 };ss data[200010]; 6 const int inf=1e9+7; 7 int n,q,timeclock,p,top,cnt,ans,r; 8 int dfn[200010],low[200010],s[200010],instack[200010],next[200010],head[200010]; 9 int belong[200010],money[200010],sum[200010],size[200010],rd[200010]; 10 void add(int a,int b) 11 { 12 data[++p].next=head[a]; 13 data[p].to=b; 14 head[a]=p; 15 } 16 void tarjan(int a) 17 { 18 dfn[a]=low[a]=++timeclock; 19 instack[a]=1; 20 s[++top]=a; 21 for(int i=head[a];i;i=data[i].next) 22 { 23 int v=data[i].to; 24 if(!dfn[v]) 25 { 26 tarjan(v); 27 low[a]=min(low[a],low[v]); 28 } 29 else 30 if(instack[v]) 31 low[a]=min(low[a],dfn[v]); 32 } 33 if(dfn[a]==low[a]) 34 { 35 cnt++; 36 while(s[top+1]!=a) 37 { 38 belong[s[top]]=cnt; 39 instack[s[top]]=0; 40 size[cnt]++; 41 sum[cnt]=min(sum[cnt],money[s[top]]); 42 top--; 43 } 44 } 45 } 46 int main() 47 { 48 scanf("%d",&n); 49 for(int i=1;i<=n;i++) 50 money[i]=1e9+7; 51 for(int i=1;i<=n;i++) 52 sum[i]=1e9; 53 scanf("%d",&q); 54 for(int i=1;i<=q;i++) 55 { 56 int u,mo; 57 scanf("%d%d",&u,&mo); 58 money[u]=mo; 59 } 60 scanf("%d",&r); 61 for(int i=1;i<=r;i++) 62 { 63 int u,v; 64 scanf("%d%d",&u,&v); 65 add(u,v); 66 } 67 for(int i=1;i<=n;i++) 68 if(!dfn[i]&&money[i]!=inf) 69 tarjan(i); 70 for(int i=1;i<=n;i++) 71 if(!dfn[i]) 72 { 73 printf("NO "); 74 printf("%d ",i); 75 return 0; 76 } 77 78 for(int i=1;i<=n;i++) 79 for(int j=head[i];j;j=data[j].next) 80 if(belong[i]!=belong[data[j].to]) 81 { 82 rd[belong[data[j].to]]++; 83 } 84 printf("YES "); 85 for(int i=1;i<=cnt;i++) 86 { 87 if(!rd[i]) 88 { 89 ans+=sum[i]; 90 } 91 } 92 printf("%d ",ans); 93 return 0; 94 }