爱在心中 vijos-1626 jdoj-1588
题目大意:给你n个点和m条有向边,求出大于一的强连通分量的个数以及是否存在唯一的强连通分量使得这个分量可以被任意点到达。如果存在,则排序输出这个强联通分量里的点,如果不存在或大于1个,则输出-1。
注释:n<=1000,m<=10000
想法:咳咳,又是一个新知识点,别问我博主为什么最近狂学知识点...就因为一个LCA啊!(其实是受刺激了)。好,我们今天来说一下tarjan算法。
所谓tarjan,就是在多项式时间复杂度内求出强连通分量的算法。显然,这题是一道裸题(这题的题目描述不是这样的啊?!炒鸡恶心的)。我在这里简略的说一下tarjan算法。我们用deep数组和low数组来记录,其中,low数组表示时间戳,deep数组表示第几个被搜到。然后,我们tarjan的过程就是dfs。对于一个刚刚被搜到的节点i,我把它扔进栈里,紧接着我来枚举这个点的出边。如果枚举到了一个栈里的点,证明那个栈里的点可以联通到i且i有一条边联想栈中节点。所以,i和栈里的点在同一个强联通分量里。那么,我们怎么记录呢?我们用时间戳来记录。显然这个时候,i到那个栈里的点之间的点都可以通过i连向栈里的点的边相互联通,所以,他们之间的点的都隶属于同一个强联通分量里。那么,我们就用那个栈里的最开始与i联通的点为这个强联通分量的标志。但是,我们将i连向的点与i当做同一个强联通分量当且仅当我们已经枚举完i的所有出边,即可。最后,我们用f[i]来表示一个强连通分量里的所有点,其中f[i][0]表示这个强联通分量里面点的个数。
最后,我们来理解一下这个好看的版子(鸣谢panxf老师)... ...
void tarjan(int x) { z[++top]=x;v[x]=1;inz[x]=1;// v[ ] 表示是否被处理过,inz[ ]表示是否在栈中。 deep[x]=low[x]=++tot; //deep[ ],low[ ],如上所述。 for(int i=1;i<=n;i++) //枚举邻接矩阵中的x的每一条出边。 if(a[x][i]==1) { if(v[i]==0) tarjan(i),low[x]=min(low[x],low[i]); //没有被访问过,更新low[x] else if(inz[i]==1) low[x]=min(low[x],deep[i]); //如果在栈中,更新low[x] } if(deep[x]==low[x]) //当x的出边都处理过后,如果deep[x]==low[x] { ans++; //记录最大连通分量的个数。 int t; //t记录栈顶元素 do //do{ }while(); 先执行再判断。 { t=z[top--],inz[t]=0; //取出栈顶 f[ans][++f[ans][0]]=t; //记录最大连通分量的集合元素。 }while(t!=x); } }
然后,我们来说明一下这道题。我们发现第一问统计答案显然是简单的,我们来看一下第二问。我们先将每一个强联通分量缩成一个大的点,每一个所隶属的强连通分量我们用inwhat来表示,所以,最后的强连通分量的个数不是anss,而是ans。然后,我们暴力枚举所有的边,来枚举缩完点之后的这个图的情况。紧接着,我们来证明一个事情:如果一个点是在这道题中满足题意者,那么它所必须要满足的条件是出度为0且这张图中只有这一个强联通分量的出度为0。为什么?我们来尝试证明:
首先,如果这个点的出度不是0且满足题意:我们假设这个点是a,有一条出边指向了b。那么此时,如果a满足题意,则所有的点又可以指向或间接指向a,又因为a指向b,所以所有点有除了a的点都可以先指向a再指向b,且a直接指向b,所以,b也满足题意,与题中只有一个点满足题意矛盾,所以,一个点满足题意必要条件是这个点的出度是0。
其次,我们来证明如果存在多个点出度是0也不满足题意。对于这样的两个点a和b,出度都是0。仍然采用反证法:如果a满足题意,那么b指向a,但是b并没有任何出边,所以b不可能指向任何点,所以b不可能指向或间接指向a,所以,矛盾。故此,证毕。
此时,显然我们可以直接枚举所有的强连通分量,以至于特判和输出即可。
最后,附上丑陋的代码... ...
1 #include <iostream> 2 #include <cstdio> 3 // #include <cstring> 4 #include <algorithm> 5 #define N 1010 6 using namespace std; 7 int z[N],top,deep[N],low[N],tot,ans,a[N][N],f[N][N]; 8 int inwhat[N]; 9 int chu[N]; 10 int x[N],y[N]; 11 bool v[N],inz[N]; 12 bool isv[N]; 13 int n; 14 void tarjan(int x) 15 { 16 z[++top]=x;v[x]=inz[x]=1; 17 deep[x]=low[x]=++tot; 18 for(int i=1;i<=n;i++) 19 { 20 if(a[x][i]) 21 { 22 if(!v[i]) tarjan(i),low[x]=min(low[x],low[i]); 23 else if(inz[i]) low[x]=min(low[x],deep[i]); 24 } 25 } 26 if(deep[x]==low[x]) 27 { 28 ans++; 29 int t; 30 do{ 31 t=z[top--],inz[t]=0; 32 f[ans][++f[ans][0]]=t; 33 inwhat[t]=ans; 34 }while(t!=x); 35 } 36 } 37 int main() 38 { 39 int m; 40 scanf("%d%d",&n,&m); 41 for(int i=1;i<=m;i++) 42 { 43 scanf("%d%d",&x[i],&y[i]); 44 a[x[i]][y[i]]=1; 45 //a[y][x]=1; 46 isv[x[i]]=1;//表示x[i]这个点有出边 47 } 48 for(int i=1;i<=n;i++) 49 { 50 if(isv[i]&&!v[i])//这样可以使得遍历整个图 51 { 52 tarjan(i); 53 } 54 } 55 int anss=0; 56 for(int i=1;i<=ans;i++)//这是第一问,f[i][0]表示第i个强连通分量的点的个数 57 { 58 if(f[i][0]!=1) anss++; 59 } 60 printf("%d\n",anss); 61 for(int i=1;i<=m;i++) 62 { 63 if(inwhat[x[i]]!=inwhat[y[i]]) 64 chu[inwhat[x[i]]]++;//这步很重要,我们计算一个强连通分量的出边是不可以计算强联通分量之内的边 65 } 66 /*for(int i=1;i<=ans;i++) 67 { 68 for(int j=1;j<=f[i][0];j++) 69 { 70 printf("%d ",f[i][j]); 71 } 72 puts(""); 73 } 74 for(int i=1;i<=ans;i++) printf("chu[%d]=%d\n",i,chu[i]);*/ 75 int cnt=0;//记录有多少出度为0的边。 76 int count=1;//记录出度为0的边是几。 77 for(int i=1;i<=ans;i++) 78 { 79 if(chu[i]==0) cnt++,count=i; 80 } 81 if(cnt>1) printf("-1\n"); 82 else 83 { 84 if(f[count][0]==1)//如果只有一个点,是不满足题意的 85 { 86 printf("-1"); 87 return 0; 88 } 89 sort(f[count]+1,f[count]+f[count][0]+1);//排序 90 for(int i=1;i<=f[count][0];i++) printf("%d ",f[count][i]); 91 } 92 return 0; 93 }
小结:tarjan可以解决很多问题,也是一种遍历的思想,在处理LCA是比较实用。
错误:1.枚举强连通分量的出度是不要枚举强连通分量之内的边的情况。
2.题目中的描述是解题的关键。
3.开始想的是floyd,结果发现过不去,告诉我们做题要注意数据范围。