搞懂了一直搞不懂的地方,趁着热乎赶紧记下来。
参看资料:
https://blog.csdn.net/qq_34374664/article/details/77488976
https://blog.csdn.net/jeryjeryjery/article/details/52829142?locationNum=4&fps=1
https://blog.csdn.net/qq_36614777/article/details/81083705
https://blog.csdn.net/qq_36288976/article/details/79382878
基础知识:
1》强连通:在一个有向图G里,设两个点 a、b ;若a 到 b 存在一条通路,且 b 到 a 亦存在一条通路,我们就称这两个顶点(a,b)强连通。
2》连通图:在一个有向图G中,若任意两个点都强连通,我们就称这个图为强连通图。
3》强连通分量:在一个有向图G中,存在一个子图,若该子图 任意两点 都满足强连通,我们就叫这个子图叫做 强连通分量 。
4》搜索树:https://blog.csdn.net/sodacoco/article/details/86488033。
Tarjan算法:
Tarjan算法是基于对图深度优先搜索的算法,每个强连通分量为搜索树中的一棵子树。
搜索时,把当前搜索树中未处理的节点加入一个堆栈,回溯时可以判断 栈顶 到 栈中的节点 是否为一个强连通分量。
Tarjan算法有点类似于 基于后序的深度遍历搜索 和并查集的组合,充分利用 回溯 来解决问题。
1》在Tarjan算法中为每个节点 i 维护了以下几个变量:
DFN[i]:深度优先搜索遍历时节点 i 被搜索的次序【时间戳】。
low[i]:节点 i 能够回溯到 的 最早位于 栈中的节点【即栈中节点的最小时间戳】。
flag[i]:标记几点 i 是否在栈中。
2》Tarjan算法的运行过程:【认真看一遍比找很多资料草草看效果好的多】
1> 首先就是按照深度优先搜索算法搜索的次序对图中所有的节点进行搜索。
2> 在搜索过程中,对于任意节点u和与其相连的节点v,根据节点v是否在栈中来进行不同的操作:
* 节点v不在栈中,即节点v还没有被访问过,则继续对v进行深度搜索。
* 节点v已经在栈中,即已经被访问过,则判断节点 v的DFN值 和节点 u的low值 的大小来 更新节点u的low值 。如果节点v的 DFN值要小于节点u的low值,根据low值的定义(能够回溯到的最早的已经在栈中的节点),我们需要用DFN值来更新u 的low值【DFN是时间戳,不会改变,可以更新的只有low,即该点可以回溯到的 所有点的最小时间戳 】。
3> 在回溯过程中,对于任意节点u用其子节点v(其实不能算是子节点,只是在深度遍历的过程中,v是在u之后紧挨着u的节点)的 low值来更新节点u的low值。因为节点v能够回溯到的已经在栈中的节点,节点u也一定能够回溯到。因为存在从u到v的直接路径,所以v能够到的节点u也一定能够到。
4> 对于一个连通图,我们很容易想到,在该连通图中有且仅有一个节点u的DFN值和low值相等。该节点一定是在深度遍历的过程中,该连通图中第一个被访问过的节点,因为它的DFN值和low值最小,不会被该连通图中的其他节点所影响。
【PS:为什么仅有一个节点的DFN和low值相等。假设有两个节点的DFN值和low值相等,由于这两个节点的DFN值一定不相同 (DFN值的定义就是深度遍历时被访问的先后次序),所以两个的low值也绝对不相等。由于位于同一个连通图中,所以两个节点必定相互可达,那么两者的low值一定会被另外一个所影响(要看谁的low值更小),所以不可能存在两对DFN值和low值相等的节点。
所以我们在回溯的过程中就能够通过判断节点的low值和DFN值是否相等来判断是否已经找到一个子连通图。由于该连通图中的DFN值和low值相等的节点是该连通图中第一个被访问到的节点,又根据栈的特性(先压入 栈的节点在栈的更里面),则该节点在最里面。所以能够通过不停的弹栈,直到弹出该DFN值和low值相同的节点来弹出该连通图中所有的节点。】
PS:图示【来源:https://blog.csdn.net/jeryjeryjery/article/details/52829142】
实现代码:
#include<iostream>
#include<cstring>
using namespace std;
int DFN[105]; //记录在做dfs时节点的搜索次序
int low[105]; //记录节点能够找到的最先访问的祖先的记号
int count=1; //标记访问次序,时间戳
int stack[105]; //压入栈中
int top=-1;
int flag[105]; //标记节点是否已经在栈中
int number=0;
int j;
int matrix[105][105]={{0,1,1,0,0,0},{0,0,0,1,0,0},{0,0,0,1,1,0},{1,0,0,0,0,1},{0,0,0,0,0,1},{0,0,0,0,0,0}};
int length; //图的长度
void tarjan(int u){
DFN[u]=low[u]=count++; //初始化两个值,自己为能找到的最先访问的祖先
stack[++top]=u;
flag[u]=1; //标记为已经在栈中
for(int v=0;v<length;v++){
if(matrix[u][v]){
if(!DFN[v]){ //如果点i没有被访问过
tarjan(v); //递归访问
if(low[v]<low[u])
low[u]=low[v]; //更新能找的到祖先
}
else{ //如果访问过了,并且该点的DFN更小,则
if(DFN[v]<low[u]&&flag[v]) //flag[v]这个判断条件很重要,这样可以避免已经确定在其他联通图的v,因为u到v的单向边而影响到u的low
low[u]=DFN[v]; //也就是已经确定了的联通图要剔除掉,剔除的办法就是判断其还在栈中,因为已经确定了的连通图的点
} //flag在下面的do while中已经设为0了(即已经从栈中剔除了)
}
}
//往后回溯的时候,如果发现DFN和low相同的节点,就可以把这个节点之后的节点全部弹栈,构成连通图
if(DFN[u]==low[u]){
number++; //记录连通图的数量
do{
j=stack[top--]; //依次取出,直到u
cout<<j<<" ";
flag[j]=0; //设置为不在栈中
}while(j!=u);
cout<<endl;
}
}
int main(){
memset(DFN,0,sizeof(DFN)); //数据的初始化
memset(low,0,sizeof(low));
memset(flag,0,sizeof(flag));
length=6;
tarjan(0);
cout<<endl;
for(int i=0;i<6;i++){
cout<<"DFN["<<i<<"]:"<<DFN[i]<<" low["<<i<<"]:"<<low[i]<<endl;
}
return 0;
}
解决问题:
1》给定一个有向图,输出该图所有的强连通分量。
input:
6 8
1 3
1 2
2 4
3 4
3 5
4 6
4 1
5 6
output:
6
5
3 4 2 1
2》代码:
#include<cstdio>
#include<algorithm>
#include<string.h>
using namespace std;
struct node {
int v,next;
}edge[1001];
int DFN[1001],LOW[1001];
int stack[1001],heads[1001],visit[1001],cnt,tot,index;
void add(int x,int y)
{
edge[++cnt].next=heads[x];
edge[cnt].v = y;
heads[x]=cnt;
return ;
}
void tarjan(int x)//代表第几个点在处理。递归的是点。
{
DFN[x]=LOW[x]=++tot;// 新进点的初始化。
stack[++index]=x;//进站
visit[x]=1;//表示在栈里
for(int i=heads[x];i!=-1;i=edge[i].next)
{
if(!DFN[edge[i].v]) {//如果没访问过
tarjan(edge[i].v);//往下进行延伸,开始递归
LOW[x]=min(LOW[x],LOW[edge[i].v]);//递归出来,比较谁是谁的儿子/父亲,就是树的对应关系,涉及到强连通分量子树最小根的事情。
}
else if(visit[edge[i].v ]){ //如果访问过,并且还在栈里。
LOW[x]=min(LOW[x],DFN[edge[i].v]);//比较谁是谁的儿子/父亲。就是链接对应关系
}
}
if(LOW[x]==DFN[x]) //发现是整个强连通分量子树里的最小根。
{
do{
printf("%d ",stack[index]);
visit[stack[index]]=0;
index--;
}while(x!=stack[index+1]);//出栈,并且输出。
printf("\n");
}
return ;
}
int main()
{
memset(heads,-1,sizeof(heads));
int n,m;
scanf("%d%d",&n,&m);
int x,y;
for(int i=1;i<=m;i++)
{
scanf("%d%d",&x,&y);
add(x,y);
}
for(int i=1;i<=n;i++)
if(!DFN[i]) tarjan(i);//当这个点没有访问过,就从此点开始。防止图没走完
return 0;
}