Tarjan算法

简介: Tarjan算法

在介绍算法之前,首先引入时间戳和追溯点的概念。

时间戳: dfn[u]表示 u结点深度优先遍历的序号。

追溯点: low[u]表示 u结点或u的子孙能通过非父子边追溯到的dfn最小的结点序号。即回到最早的过去

例如,在深度优先搜索中,每个点的时间戳和追溯点求解过程如下。

初始时,dfn[u]=low[u], 如果该结点的邻接点未被访问,则一直深度优先遍历,1- -2-3-5-6- -4, 此时4的邻接点1已被访问,且1不是4的父节点,4的父节点是6 (深度优先搜索树上的父结点)。


20210312164122200.png

那么4号结点能回到最早的过去是1号结点( dfn=1 ),因此low[4]=min(low[4],dfn[1])=1。返回时,更新路径上所有祖先结点的low值,因为子孙能回到的追溯点,其祖先也能回到

如图所示。

20210312164230734.png


1.无向图的桥判定法则:·


无向边(x.y)是桥,当且仅当搜索树上存在x的一个子节点y满足:

low[y] > dfm[x]

就是说,孩子的low值比自己的dfn.值大,则该结点到这个孩子的边为桥。下图中,边(5,7),5的子节点7,满足low[7]>dfmn[5],因此该边为桥。


20210312164532453.png

实验代码

#include<iostream>
#include<cstring>
using namespace std;
const int maxn = 1000+5;
int n,m;
int head[maxn],cnt;
struct Edge{
  int to,next;
}e[maxn<<1];
int low[maxn],dfn[maxn],num;
void add(int u,int v){
  e[++cnt].next = head[u];
  e[cnt].to = v;
  head[u] = cnt;
} 
void tarjan(int u,int fa)//u当前节点  fa:u的父节点 
{
  dfn[u]=low[u]=++num;//结点的dfn和low 进行初始化 
  for(int i=head[u];i;i=e[i].next)
  {
    int v=e[i].to;//v:u的临界点 也是子节点 
    if(v==fa)//如果v是u的父节点,则直接结束. 因为判断桥时,要的是当前节点和其子节点.. 
      continue;
    if(!dfn[v])//v未访问过 
    {
      tarjan(v,u); //进行深度优先遍历                        
      low[u]=min(low[u],low[v]);//更新父节点的low值,孩子能到达的,父亲一定也能到达. 
      if(low[v]>dfn[u])//如果孩子结点的Low值 > 父节点的dfn则说明u是桥. 
        cout<<u<<"---"<<v<<"是桥"<<endl; 
    }
    else//v已被访问, 且u-->v  所以 更新u的low值. 
      low[u]=min(low[u],dfn[v]);
  }
}
void init(){
  memset(head,0,sizeof(head));
  memset(low,0,sizeof(low));
  memset(dfn,0,sizeof(dfn));
  cnt = num = 0;
} 
int main(){
  while(cin>>n>>m){
    init();
    int u,v;
    while(m--){
      cin>>u>>v;
      add(u,v);
      add(v,u);
    }
    for(int i = 1; i <=n; i++){
      if(!dfn[i]){// 防止不是连通图的情况. 
        tarjan(i,0);
      }
    }
  }
}

2.无向图的割点割点判定法则


若x不是根结点,则x是割点,当且仅当搜索树上存在x的一.个子节点y,满足:

low[y]≥dfn[x]

若x是根结点,则x是割点,当且仅当搜索树上存在至少两个子节点,满足上述条件。就是说,如果不是根,孩子的low值大于等于自己的dfn值,该结点就是割点;如果是根,至少需要两个孩子满足条件。


①.当x不是根节点时:

下图中,5的子节点7,满足low[7]>dfn[5],因此5是割点。


20210312164801136.png

②.当x是根节点时,两个节点不存在割点.只有三个及其以上(也就是根节点至少有两个子节点)时,才可能有割点.第二幅图中,根节点的两个子节点满足,则1是割点.

20210312164813770.png

最后来解释下为啥判断条件里面有个 = ?

很简单,从下图看,2,3是割点,而dfn[ 3 ] = low[ 3 ] ,所以得加上等号…哈哈

20210513211300922.png

实验代码


#include<iostream>
#include<cstring>
using namespace std;
const int maxn=1000+5;
int n,m;
int head[maxn],cnt,root;
struct Edge
{
  int to,next;
}e[maxn<<1];
int low[maxn],dfn[maxn],num;
void add(int u,int v)
{
  e[++cnt].next=head[u];
  e[cnt].to=v;
  head[u]=cnt;  
}
void tarjan(int u,int fa)
{
  dfn[u]=low[u]=++num;
  int count=0;//用于计数 ,因为跟节点必须有两个节点而且子节点的low>= 根节点才说明根节点是割点. 
  for(int i=head[u];i;i=e[i].next)//访问当前节点u的临界点. 
  {
    int v=e[i].to;
    if(v==fa) 
      continue;
    if(!dfn[v])
    {
      tarjan(v,u);
      low[u]=min(low[u],low[v]);
      if(low[v]>=dfn[u])
      {
        count++;
        if(u!=root||count>1)//如果不是跟则,割点已找到.如果是根,则应该判断count是否>=2.. 
          cout<<u<<"是割点"<<endl; 
      } 
    }
    else
      low[u]=min(low[u],dfn[v]);
  }
}
void init()
{
  memset(head,0,sizeof(head));
  memset(low,0,sizeof(low));
  memset(dfn,0,sizeof(dfn));
  cnt=num=0;
}
int main()
{
  while(cin>>n>>m)
  {
    init();
    int u,v;
    while(m--)
    {
      cin>>u>>v;
      add(u,v);
      add(v,u);
    }
    for(int i=1;i<=n;i++)
      if(!dfn[i])//从哪个节点开始,那个节点就是根. 
      {
        root=i;
        tarjan(i,0);
       } 
  }
  return 0;
}


3.有向图的强连通分量


算法步骤:

1)深度优先遍历结点, 第一次访问结点x时,将x入栈,且dfn[x]=low[x]=++num;

2)遍历 x的所有邻接点y,


若y没被访问,则递归访问y,返回时更新low[x]=min(low[x]low[y]);

若y已被访问且在栈中,则令low[x]=min(low[x],low[y]);

x 回溯之前,判断如果low[x]=dfn[x], 则从找中不断弹出结点,直到x出栈停止。

弹出的结点就是一一个连通分量。


20210312165107297.png

202103121651203.png

为啥连通判断条件时low[x]=dfn[x]呢?

因为对于一个连通分量来说,里面所有的low值都是相同的,都等于这个连通分量最开始搜索的那个点的low和dfn.在这个连通分量中只有该点满足low=dfn其他的都是dfn>low.回溯到这里则可以从stack中弹出刚才遍历的那些点了.

实验代码

#include<iostream>
#include<cstring>
#include<stack>
using namespace std;
const int maxn=1000+5;
int n,m;
int head[maxn],cnt;
stack<int>s;//开一个栈 
bool ins[maxn];
struct Edge
{
  int to,next;
}e[maxn<<1];
int low[maxn],dfn[maxn],num;
void add(int u,int v)
{
  e[++cnt].next=head[u];
  e[cnt].to=v;
  head[u]=cnt;  
}
void tarjan(int u)
{
  low[u]=dfn[u]=++num;
  ins[u]=true;
  s.push(u);
  for(int i=head[u];i;i=e[i].next)
  {
    int v=e[i].to;
    if(!dfn[v])//如果没有被访问过,则进行深搜访问. 
    {
      tarjan(v);
      low[u]=min(low[u],low[v]);//维护父节点. 
    }
    else if(ins[v])//如果访问过而且在栈中. 因为v先访问过了,所以dfn会比较小(因为在入栈时,low和dfn相同,所以min里面dfn[v]和low[v]都行...). 
      low[u]=min(low[u],low[v]);
  }//u的所有邻接点已访问完毕. 
  if(low[u]==dfn[u])//连通分量之间交接的点的low和dfn相同. 
  {
    int v;
    cout<<"连通分量:";
    do
    {
      v=s.top();
      s.pop();
      cout<<v<<" ";
      ins[v]=false;//当前节点已出栈,标记发生改变 
    }while(v!=u);//一直弹出直到u 
    cout<<endl;
  }
}
void init()
{
  memset(head,0,sizeof(head));
  memset(low,0,sizeof(low));
  memset(dfn,0,sizeof(dfn));
  memset(ins,0,sizeof(ins));
  cnt=num=0;
}
int main()
{
  while(cin>>n>>m)
  {
    init();
    int u,v;
    while(m--)
    {
      cin>>u>>v;
      add(u,v);
    }
    for(int i=1;i<=n;i++)
      if(!dfn[i])
        tarjan(i);
  }
  return 0;
}
/*
5 8
1 3
1 2
3 5
3 4
3 2
4 5
4 1
5 1
*/ 
/*
7 9
1 2
2 3
3 1
2 4
4 7
7 4
4 5
5 6
*/ 

运行结果:

20210513223317316.png

20210513223537355.png

注意:有向图的强连通分量和无向图的连通分量是完全不同的概念,Tarjan算法也大不一样.对于无向图,连通分量的标号可以用low直接表示,但对于有向图则完全不可以.在无向图中连通分量的个数就是low的最大值,而在强连通分量中不是.


相关文章
|
7月前
|
算法 vr&ar Windows
Tarjan算法求LCA(最近公共祖先)
Tarjan算法求LCA(最近公共祖先)
46 0
|
安全 算法 Go
有向图的强联通分量(SCC)Tarjan算法
有向图的强联通分量(SCC)Tarjan算法
203 0
|
算法
图论——强连通分量:Tarjan算法。
在有向图G中,如果两个定点u,v间存在一条u到v的路径,也存在一条v到u的路径,则称u,v是强连通的。 若有向图G的任意两点都强联通,则称G是一个强联通图。 非强连通图的极大强连通子图称为强连通分量。   这里,极大强连通子图可以理解为一个子图是强连通图,且它的任意子图都不是强联通。
2649 0
|
算法 C++ BI
图论——强连通分量:Tarjan算法——练习1
上一次我们详细介绍了强连通分量的Tarjan算法,今天呢,我们来做一些习题来巩固Tarjan算法,毕竟它十分重要。 Tarjan算法详解 上面是上一次的详解,在做题时可供参考。 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~                               练习一般采用洛谷题库。
1311 0
|
算法 数据建模
【POJ 1236 Network of Schools】强联通分量问题 Tarjan算法,缩点
题目链接:http://poj.org/problem?id=1236 题意:给定一个表示n所学校网络连通关系的有向图。现要通过网络分发软件,规则是:若顶点u,v存在通路,发给u,则v可以通过网络从u接收到。
1190 0
|
算法 存储
【HDU 4547 CD操作】LCA问题 Tarjan算法
题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=4547 题意:模拟DOS下的cd命令,给出n个节点的目录树以及m次查询,每个查询包含一个当前目录cur和一个目标目录tar,返回从cur切换到tar所要使用的cd命令次数: 注意这里的cd命令是简化版,只能进行如下两种操作:   1.
979 0
|
算法 计算机视觉 存储
【HDU 2586 How far away?】LCA问题 Tarjan算法
题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=2586 题意:给出一棵n个节点的无根树,每条边有各自的权值。给出m个查询,对于每条查询返回节点u到v的最短路径的权值和,按查询顺序输出结果。
1061 0
|
算法
【POJ 1330 Nearest Common Ancestors】LCA问题 Tarjan算法
题目链接:http://poj.org/problem?id=1330 题意:给定一个n个节点的有根树,以及树中的两个节点u,v,求u,v的最近公共祖先。 数据范围:n [2, 10000] 思路:从树根出发进行后序深度优先遍历,设置vis数组实时记录是否已被访问。
1261 0
|
18天前
|
算法
基于WOA算法的SVDD参数寻优matlab仿真
该程序利用鲸鱼优化算法(WOA)对支持向量数据描述(SVDD)模型的参数进行优化,以提高数据分类的准确性。通过MATLAB2022A实现,展示了不同信噪比(SNR)下模型的分类误差。WOA通过模拟鲸鱼捕食行为,动态调整SVDD参数,如惩罚因子C和核函数参数γ,以寻找最优参数组合,增强模型的鲁棒性和泛化能力。
|
4天前
|
供应链 算法 调度
排队算法的matlab仿真,带GUI界面
该程序使用MATLAB 2022A版本实现排队算法的仿真,并带有GUI界面。程序支持单队列单服务台、单队列多服务台和多队列多服务台三种排队方式。核心函数`func_mms2`通过模拟到达时间和服务时间,计算阻塞率和利用率。排队论研究系统中顾客和服务台的交互行为,广泛应用于通信网络、生产调度和服务行业等领域,旨在优化系统性能,减少等待时间,提高资源利用率。