最近几天准备根据VJ上面kuangbin带你飞专题的题把tarjan算法学习一下,这一篇是根据POJ1236来学习通过tarjan算法查找强连通分量
这个blog对于tarjan算法的讲解很详细,先贴在这里:http://www.lydshy.com/wordpress/92 和 http://www.lydshy.com/wordpress/115
下面是我自己的一些理解
[有向图强连通分量]
在有向图G中,如果两个顶点间至少存在一条路径,称两个顶点强连通(strongly connected)。如果有向图G的每两个顶点都强连通,称G是一个强连通图。非强连通图有向图的极大强连通子图,称为强连通分量(strongly connected components),简记为SCC。
5号节点能追溯到的dfn最小的节点是2好,其low为2,所以low[5]=2;而且每次搜索都把当前节点压入栈中,当搜索了当前节点中所有的点之后,如果low[k]==dfn[k],则栈中从k到栈顶的所有元素属于一个连通分量
以下算法流程是摘抄自开头的博客
接下来是对算法流程的演示。
从节点1开始DFS,把遍历到的节点加入栈中。搜索到节点u=6时,DFN[6]=LOW[6],找到了一个强连通分量。退栈到u=v为止,{6}为一个强连通分量。
返回节点5,发现DFN[5]=LOW[5],退栈后{5}为一个强连通分量。
返回节点3,继续搜索到节点4,把4加入堆栈。发现节点4向节点1有后向边,节点1还在栈中,所以LOW[4]=1。节点6已经出栈,(4,6)是横叉边,返回3,(3,4)为树枝边,所以LOW[3]=LOW[4]=1。
继续回到节点1,最后访问节点2。访问边(2,4),4还在栈中,所以LOW[2]=DFN[4]=5。返回1后,发现DFN[1]=LOW[1],把栈中节点全部取出,组成一个连通分量{1,3,4,2}。
至此,算法结束。经过该算法,求出了图中全部的三个强连通分量{1,3,4,2},{5},{6}。
可以发现,运行Tarjan算法的过程中,每个顶点都被访问了一次,且只进出了一次堆栈,每条边也只被访问了一次,所以该算法的时间复杂度为O(N+M)。
以下是POJ1236的题目和题解
You are to write a program that computes the minimal number of schools that must receive a copy of the new software in order for the software to reach all schools in the network according to the agreement (Subtask A). As a further task, we want to ensure that by sending the copy of new software to an arbitrary school, this software will reach all schools in the network. To achieve this goal we may have to extend the lists of receivers by new members. Compute the minimal number of extensions that have to be made so that whatever school we send the new software to, it will reach all other schools (Subtask B). One extension means introducing one new member into the list of receivers of one school.
5 2 4 3 0 4 5 0 0 0 1 0Sample Output
1 2
题目大意:有一些学校可以相互传输(单向)信息,现在给出其联系图,让你求出要使所有人都收到信息,最少要发送几个信息,和最少增加几条线路可以使只发送一条信息就能使所有人都收到
题解:第一个答案是所有入度为0的强连通分量的个数,第二个答案是所有入度为0和所有出度为0的连通分量数中的最大值的个数,这一点画个简易的图就可以看出来了
下面是代码
#include<stdio.h>
#include<iostream>
#include<string.h>
#include<algorithm>
using namespace std;
#define MAX 105
int head[MAX];
int u[MAX*MAX];
int v[MAX*MAX];
int _next[MAX*MAX];
int low[MAX];
int dfn[MAX];//可以代替vis【MAX】
int cnt[MAX];//每一个点所属的联通分量的标号
int re;//联通分量个数
int stack[MAX];
int top;
int in[MAX];
int out[MAX];
void dfs(int k,int num){
dfn[k]=low[k]=num;
stack[++top]=k;
for(int i=head[k];i!=-1;i=_next[i]){
int e=v[i];
if(!dfn[e]){
dfs(e,++num);
low[k]=min(low[k],low[e]);
}
else if(dfn[e]) low[k]=min(low[k],dfn[e]);//这里为什么是min(low,dfn)之后要画图解释
}
if(dfn[k]==low[k]){ //dfn==low 出栈
re++;
do{
cnt[stack[top]]=re;
}while(stack[top--]!=k);//之前因为--放到循环里面导致出错 习惯让栈顶空着了····
}
}
int main(){
int n;
while(~scanf("%d",&n)){
int t;
int c=0;
memset(head,-1,sizeof(head));
memset(dfn,0,sizeof(dfn));
memset(in,0,sizeof(in));
memset(out,0,sizeof(out));
top=0;
re=0;
for(int i=1;i<=n;i++){
while(scanf("%d",&t)&&t){
u[c]=i;
v[c]=t;
_next[c]=head[i];
head[i]=c++;
}
}
for(int i=1;i<=n;i++){
if(!dfn[i]) dfs(i,1);
}
int ans1=0,ans2=0;
for(int i=0;i<c;i++){
if(cnt[u[i]]!=cnt[v[i]]){
in[cnt[v[i]]]++;
out[cnt[u[i]]]++;
}
}
for(int i=1;i<=re;i++){
if(!in[i]) ans1++;
if(!out[i]) ans2++;
}
if(re==1) printf("1\n0\n");
else printf("%d\n%d\n",ans1,max(ans1,ans2));
}
}
之前也看到过一些代码,里面对于low[v]的更新不区分是树枝边还是后向边(指向在当前栈中的点的边),一律都是low[u]=min(low[v],low[u]),这两种的区别我现在还不是非常的清楚,但是好像在找割点和桥的时候是有很大区别的,我在这里推荐树枝边和后向边分开来更新,就是我的代码中的更新方法,如果之后我有更深的理解会进行补充