强连通分量:
对于有向连通图,如果任意两点之间都能到达,则称为强连通图。如果对于有向图的一个子图是强连通的,则称为强连通子图;
极大的强连通子图称为强连通分量。一个有向图可以有多个强连通分量,一个点也算是强连通分量。强连通分量的术语是strongly connnected components,简称SCC
tarjan算法的基础是深度优先搜索,用两个数组low和dfn,和一个栈。low数组是一个标记数组,记录该点所在的强连通子图所在搜索子树的根节点的dfn值,dfn数组记录搜索到该点的时间,也就是第几个搜索这个点的。根据以下几条规则,经过搜索遍历该图和对栈的操作,我们就可以得到该有向图的强连通分量。
算法规则:
1.数组的初始化:当首次搜索到点p时,Dfn与Low数组的值都为到该点的时间。
2.堆栈:每搜索到一个点,将它压入栈顶。
3.当点p有与点p’相连时,如果此时(时间为dfn[p]时)p’不在栈中,p的low值为两点的low值中较小的一个。
4.当点p有与点p’相连时,如果此时(时间为dfn[p]时)p’在栈中,p的low值为p的low值和p’的dfn值中较小的一个。
5.每当搜索到一个点经过以上操作后(也就是子树已经全部遍历)的low值等于dfn值,则将它以及在它之上的元素弹出栈。这些出栈的元素组成一个强连通分量。
6.继续搜索(或许会更换搜索的起点,因为整个有向图可能分为两个不连通的部分),直到所有点被遍历。
算法伪代码:
tarjan(u) {
DFN[u]=Low[u]=++Index // 为节点u设定次序编号和Low初值 Stack.push(u) // 将节点u压入 栈中 for each (u, v) in E // 枚举每一条边 if (!dfn[v]) // 如果节点v未被访问过
{
tarjan(v) // 继续向下找
Low[u] = min(Low[u], Low[v])
}
else if (v in S) // 如果节点v还在栈内
Low[u] = min(Low[u], DFN[v])
if (DFN[u] == Low[u]) // 如果节点u是强连通分量的根
do{
v = S.pop // 将v退栈,为该强连通分量中一个顶点 }while(u == v); }
应用例子:(OJ 1484 popular cows) ( 值得深入学习一下,包括重新建图)
题意:有N只牛,输入a,b的话,则说明b关注a,而且关注有传递性。例如c关注b,且b关注a,则c也关注a。题目问有多少只奶牛能被其他所有的奶牛关注。
把题目的模型转换:N个顶点的有向图,有M条边。求一共有多少个点,满足这样的条件:所有其它的点都可以到达这个点。这个点满足条件的充要条件是:这个点是树中唯一的出度为0的点。
先求强连通分量,然后可以把强连通分量缩成一个点,因为,在强连通分量中的任意两个点可以到达,所有的点具有相同的性质,即它们分别能到达的点集都是相同的,能够到达它们的点集也是相同的。然后就重新构图,缩点后的图是没有强连通分量的,否则,可将环上所有点也缩成一个点,与极大强联通分量矛盾。所以只要找出度为0的点,并且出度为0的点只有1个 ,如果出度为0的点有多个的话,问题则无解。 代码:
#include<cstdio>
#include<cstring>
using namespace std;
const int adj=10010;
const int edg=50010
struct node
{
int v;
int next;
};
node edge[edg];
node edge1[edg];
int low[adj],dfn[adj],Stack[adj];
int first[adj],first1[adj],fuck[adj];
bool ins[adj];
int n,m,temp,cnt,top,count;
int cnt_size[adj],belong[adj],outdegree[adj];
void creat(int u,int v)
{
edge1[count].next=first1[u];
edge1[count].v=v;
first1[u]=count++;
}
void tarjan(int u)
{
int i,v;
dfn[u]=low[u]=++temp;
Stack[top++]=u;
ins[u]=true;
for(i=first[u]; i!=-1; i=edge[i].next)
{
v=edge[i].v;
if(!dfn[v])
{
tarjan(v);
if(low[u]>low[v])
low[u]=low[v];
}
else if(ins[v])
{
if(low[u]>dfn[v])
low[u]=dfn[v];
}
}
if(dfn[u]==low[u])
{
int j;
do
{
top--;
j=Stack[top];
ins[j]=false;
cnt_size[cnt]++;
belong[j]=cnt;
}
while(u!=j);
cnt++;
}
}
int main()
{
int i,j,k,v,sum,num;
int e=0;
scanf("%d%d",&n,&m);
memset(first,-1,sizeof(first));
for(k=0; k<m; k++)
{
scanf("%d%d",&i,&j);
edge[e].v=j;
edge[e].next=first[i];
first[i]=e;
e++;
}
memset(dfn,0,sizeof(dfn));
memset(ins,false,sizeof(ins));
temp=cnt=top=0;
memset(cnt_size,0,sizeof(cnt_size));
memset(low,0,sizeof(low));
for(i=1; i<=n; i++) //求强连通分量
{
if(!dfn[i])
tarjan(i);
}
memset(first1,-1,sizeof(first1));
count=0;
for(i=1; i<=n; i++) //重新构造图
{
for(j=first[i]; j!=-1; j=edge[j].next)
{
v=edge[j].v;
if(belong[i]!=belong[v])
creat(belong[i],belong[v]);
}
}
memset(outdegree,0,sizeof(outdegree));
for(i=0; i<cnt; i++) //求每个节点的出度
{
for(j=first1[i]; j!=-1; j=edge1[j].next)
outdegree[i]++;
}
sum=num=0;
for(i=0; i<cnt; i++)
{
if(outdegree[i]==0) //求节点为0的个数
{
sum=cnt_size[i];
num++;
}
}
if(num==1)
printf("%d\n",sum);
else
printf("0\n");
return 0;
}
加一段有注释的代码:
vector<int>g[N];
int n,m;
/*
*instack记录节点u是否在栈中
*dfn记录节点u第一次被访问是的时间戳
*low记录与节点u和u的子树节点中最早的时间戳
*belong记录每个节点属于的强连通分量编号
*/
int instack[N],low[N],dfn[N],stack[N],belong[N];
int times,top,cnt;
void tarjan(int u)
{
int v;
dfn[u] = low[u] = ++times;
instack[u] = 1;
stack[++top] = u;
for(int i = 0 ; i < g[u].size() ; i ++)
{
v = g[u][i];
if(!dfn[v])//节点v还没有被访问
{
tarjan(v);//递归访问每个子树
low[u] = min(low[u],low[v]);//更新每个子树中最早访问的时间戳
}
//u与v相连,但v已经被访问过了,且还在栈中
//更新子树节点最早的时间戳
else if(instack[v] && dfn[v] < low[u])
low[u] = dfn[v];
}
//第一访问的时间戳和最早时间戳相等,说明节点u是强连通分量的根
if(dfn[u] == low[u])
{
cnt++;
do
{
v = stack[top--];
instack[v] = 0;
belong[v] = cnt;
cout<<v<<" ";
}
while(u != v);
cout<<endl;
}
}
双联通分量:
对于无向连通图,如果任意两点之间都有多于一条的路径,则称为双连通图。对于无向图的一个子图是双连通的,则称为双连通子图;
极大的双连通子图称为双连通分量。一个无向图可以有多个双连通分量,一个点也算是双连通分量。双连通分量的术语是biconnected components,简称为BCC
下面是双连通分量的模板代码(转载):
//----------DCC------------------
int dfn[MAXNODE],low[MAXNODE],index;//dfn记录各点被访问次序,low是追溯到DCC的根节点
//的dfn的值,当根节点的某个直接儿子节点的low值大于或等于根节点的dfn的值时,就可以从
//栈中取值了,直到取到根节点为止时一个DCC
int stack[MAXNODE],top;//栈:用深搜搜索节点并依次存储各个节点—以便于找到DCC(即当
//发现环时就是一个DCC,用low标记的,从该栈中取值取到该根节点为止)
int id_dcc[MAXNODE],cnt_dcc;//id_dcc:名副其实即DCC的id(编号),存储各节点的所在的
//编号(就是你给他们编的号,从1-n)cnt_dcc就是编号下标!
int father[MAXNODE];//由于求DCC是在一个无向连通图中,即为双向的图,该father就是为了
//防止某一节点又访问上一个节点(上一个节点搜出该节点)
void DFS_DCC(int cur)
{
int next; //next为cur节点下的节点
dfn[cur]=low[cur]=++index;
stack[++top]=cur;
for(Node *p=G[cur];p;p=p->next)
{
next=p->num;
if(!dfn[next])
{
father[next]=cur; //额,可以不用。。
DFS_DCC(next);
if(low[next]<low[cur]) //更新low使每一个DCC中的low的值==根节点low值
low[cur]=low[next];
if(low[next]>=dfn[cur])//当发现节点cur的儿子节点next的low值>=dfn[cur]
{ //则就要取栈,即是时候取出DCC了。为什么?因为不
cnt_dcc++; //这样就不对了^_^!(具体原因,自己举几个例子try)
do
{
next=stack[top--];
id_dcc[next]=cnt_dcc;
}while(next!=cur);
top++; //这里为什么要++因为连着DCC是有共同节点的(举例try)
//不++肯定要出错!也许我这code跟别人不一样,其实
//思想都一样,只是具体实现的code有小小的差异罢了
}
}
else if(next!=father[cur] && dfn[next]<low[cur])
{
//呃呃呃!其实不用这个father数组都可以了,只需
//dfn[next]<low[cur]就行了
low[cur]=dfn[next];
}
}
}
void solve()
{
index=top=cnt_dcc=0;
memset(dfn,0,sizeof(dfn));//初始化各下标及dfn
DFS_DCC(1);//由于是无向连通图,只需深搜一个节点就都可以都到了
}