Tarjan算法详解
来源:互联网 发布:甄子丹功夫怎么样知乎 编辑:程序博客网 时间:2024/05/23 00:27
MZX大佬授课DAY2上午
tarjan是用来解决图的割边割点问题以及有向图的强连通分量(缩点)的问题的。
割边
割边是图论算法中一类很常见的问题:
定义
在一个连通图G中,假设有一条边e,去掉e后图G不再连通,那么e就是G的一条割边。换句话说,G是连通图,G-e不是连通图。
暴力算法
最暴力最暴力的算法就是每次都去掉一条边,然后进行dfs深度优先遍历。要进行n次dfs深度优先遍历。这显然效率是很低很低的。
这个时候一个叫Tarjan的男人站了出来。
tarjan算法求割边
tarjan算法是以dfs深度优先遍历算法为基础的。也就是说tarjan是对dfs的一个最优性剪枝。
在tarjan中,我们需要对每一个顶点维护两个值,dfn值和low值。
我们在dfs深度优先遍历的时候根据dfs遍历的顺序可以画出一颗dfs树(有n-1条边,这n-1条边称为树边,其余称为非树边)。我们dfs遍历的顺序编号就是dfn值,我们可以这样理解:dfn值就是一个时间戳,dfn[i]=1,就代表第i号结点是第1个被遍历到的。low值是某个节点通过非树边能够回溯到的dfn值最小的点。比如说low[8]=4;我们就能知道,8号点能通过非树边回溯到4号点。
这有什么用呢?
我们任意连接dfn上的一条非树边,发现有这样一个性质:总是祖孙结点相连的。这样的边称为返祖边。我们可以知道,如果一个点有返祖边与dfn值更小的点相连,那么它的父亲结点肯定不是割点(好好思考下,想通了再往下面看)。所以我们可以得出一个结论:在图中,如果存在一条树边(x,y),而且x是y的父亲,存在low[y]>dfn[x],那么(x,y)是割边。
先看一个例题
luogu炸铁路
分析
这道题目的大意就是要炸掉一条铁路,让这个交通系统变成两部分,这明显是一个裸的割边题目。我们这边拿这道题目作为一个模板题来讲解。
code
#include<bits/stdc++.h>#define maxn 200#define maxm 5200*2using namespace std;inline int read(){ int num=0; char c; bool flag=false; for(;c>'9'||c<'0';c=getchar()) if(c=='-') flag=true; for(;c>='0'&&c<='9';num=num*10+c-48,c=getchar()); return flag?-num:num;}//快读/*这个代码风格……我已经被大佬们带掉了怎么说呢,就是使用namespace这个东西有啥好处呢,就是可以使整个代码的结构变得很清晰不信你看*/namespace graph//这部分是跟图论就有关系了{ int n,m,head[maxn],top=0; struct Hydra_ { int dot_order,next_location; }a[maxm];//邻接表 void insert(int x,int y) { top++; a[top].dot_order=y; a[top].next_location=head[x]; head[x]=top; }//在邻接表中插入边 void init() { n=read(); m=read(); for(int i=1;i<=m;i++) { int x=read(); int y=read(); insert(x,y); insert(y,x); } }//读入}using namespace graph;//一定别忘了写这句namespace Tarjan//接下来是tarjan{ struct edge { int from,to; }ans[maxm];//这是用来存储答案的一个边表 int dfn[maxn],low[maxn],tot=0,num; void hy(int u,int fa) //dfs函数,u代表当前结点,fa代表当前结点的父亲 { dfn[u]=low[u]=++tot; for(int i=head[u];i;i=a[i].next_location) //遍历链表,我们需要遍历的是所以非u,fa的边 { int v=a[i].dot_order; if(v==fa)continue; /*所以这里如果v就是u的父亲, 那么就是u,fa这条边了*/ if(!dfn[v]) //如果没有被访问过 { hy(v,u); //先访问递归 low[u]=min(low[u],low[v]); //就用v的low值更新u的low值。 } else//如果被访问过了 low[u]=min(low[u],dfn[v]); //那就用v的dfn值更新u的low值 if(dfn[u]<low[v]) //这里是判断是否是割边,这是 { ++num; ans[num].from=min(u,v); ans[num].to=max(u,v); //把这条割边存进边表 } } }}using namespace Tarjan;bool mycmp(edge a,edge b){ return a.from<b.from||(a.from==b.from&&a.to<b.to);}//给边表排序的函数int main(){ init(); for(int i=1;i<=n;i++) if(!dfn[i]) hy(i,0); //这里是个坑,这是为了避免不连通的情况,坑了我很久啊 sort(ans+1,ans+1+num,mycmp); for(int i=1;i<=num;i++) printf("%d %d\n",ans[i].from,ans[i].to); return 0;}
割点
割点是图论算法中另外一个非常常见的问题。
定义
类比割边的定义,割点就是,在连通图G中,去掉了点a,让这个图不再连通,那么就是割点。也就是G是连通图,G-a不是连通图。
暴力算法
这类题目的暴力算法一样,就是把所有跟这个点有关系的边全部去掉,再dfs。太慢了。
tarjan求割点
tarjan求割点的方法和求割边的方式类同,都需要维护两个值:low值和dfn值。意义也基本相同。我们仍然构造一颗dfs树,如果一个点有一个儿子能通过非树边到达其他非子孙结点,那么这个点就不是割点。我们可以说,如果存在一条边(x,y),x是y的父亲。如果low[y]<=dfn[x]那么说明x是割点。
需要一提的是,由于有向图都是单向边,所以非树边不一定是返祖边。可以连到兄弟子树的结点的边也是可以让这个结点不是割点的。最后要强调的是,只要有一个儿子结点满足上面的关系,那么这个点就是割点。因为至少有一个点被他阻断了呀。
思考一下根节点,根节点要怎么样才能是割点呢:度>=2。
再看一个例题
[扭曲锣鼓 割点模板题(https://www.luogu.org/problem/show?pid=3388)
分析
裸模板题!!tarjan上。
code
#include<bits/stdc++.h>#define maxn 100100using namespace std;inline int read(){ int num=0; char c; bool flag=false; for(;c>'9'||c<'0';c=getchar()) if(c=='-') flag=true; for(;c>='0'&&c<='9';num=num*10+c-48,c=getchar()); return flag?-num:num;}//快读/*还是那个套路。namespace*/namespace graph{ int n,m,head[maxn],top=0; struct WE { int dot_order,next_location; }a[maxn*2];//邻接表 void insert(int x,int y) { top++; a[top].dot_order=y; a[top].next_location=head[x]; head[x]=top; }//插入 void init() { n=read(); m=read(); for(int i=1;i<=m;i++) { int x=read(); int y=read(); insert(x,y); insert(y,x); } }//读入数据}using namespace graph;namespace Tarjan//tarjan算法{ int dfn[maxn],low[maxn],father[maxn]; /* dfn、low的意义见上。father表示这个点 在哪个集合里面。为了搞多个连通块的问题。 */ bool ans[maxn];//用来标记某点是不是割点 int tot=0; void hy(int x)//深度优先遍历 { int degree=0; dfn[x]=low[x]=++tot;//初始化dfn和low相等 for(int i=head[x];i;i=a[i].next_location) //遍历邻接表 { int y=a[i].dot_order; if(!dfn[y]) { father[y]=father[x]; //他俩有边相连,所以在一个连通块里面 hy(y); //继续遍历 low[x]=min(low[x],low[y]); //用y的low值来更新x的low值 if(low[y]>=dfn[x]&&x!=father[x]) ans[x]=true; //判断是否为割点(非根节点的情况) if(x==father[x]) degree++; //如果它是根节点,父亲是自己,那么度+1 } low[x]=min(low[x],dfn[y]); // 用y的dfn值更新x的low值 } if(x==father[x]&°ree>=2) ans[x]=true; //如果它是根节点,而且度数>=2,那么它是割点 }}using namespace Tarjan;int main(){ init(); for(int i=1;i<=n;i++) father[i]=i; //默认父亲是自己 memset(ans,false,sizeof(ans)); for(int i=1;i<=n;i++) if(!dfn[i])hy(i); //为了解决多个连通块的问题 int num=0; for(int i=1;i<=n;i++) if(ans[i])num++; //统计割点数量 printf("%d\n",num); for(int i=1;i<=n;i++) if(ans[i])printf("%d ",i); //打印割点编号 return 0;}
强连通分量和缩点
强连通分量是有向图中的一类很常见的问题。弄得我痛不欲生!!!
定义
有向图中一个连通分量如果满足这样的性质:其中任意两个点都是能够互相通过路径到达的,而且整个分量没有出度,那么这个连通分量就是强连通分量。
暴力算法
我给恩撒。
我才不写暴力呢。
优秀的tarjan
tarjan求强连通分量是需要一个栈来维护一个被遍历到的序列,这要碰到dfn[I]=low[I]的就把所有的它以上进栈的全部弹出,这就是个强连通分量了。这个推导的话我推荐一个blog
大佬博客。关于强连通分量的证明
例题
luogu消息扩散
分析
不多说了
统计强连通分量个数。缩点。统计入度为0的强连通分量。
code
#include<bits/stdc++.h>#define maxn 100100#define maxm 500200*2using namespace std;inline int read(){ int num=0; bool flag=true; char c; for(;c>'9'||c<'0';c=getchar()) if(c=='-') flag=false; for(;c>='0'&&c<='9';num=num*10+c-48,c=getchar()); return flag ? num : -num;}namespace graph{ int n,m,head[maxn],top1; struct RNG { int dot_order,next_location; }a[maxm]; bool tr[maxm]; struct edge { int x,y; }e[maxm]; void insert(int x,int y) { top1++; a[top1].dot_order=y; a[top1].next_location=head[x]; head[x]=top1; } void init() { n=read(); m=read(); for(int i=1;i<=m;i++) { e[i].x=read(); e[i].y=read(); insert(e[i].x,e[i].y); } }}using namespace graph; namespace Tarjan{ int stack[maxm],dfn[maxn],low[maxn],tot; bool flag[maxn]; int w[maxm]; int top=0,number=0; void hy(int x) { dfn[x]=low[x]=++tot; stack[++top]=x; flag[x]=true; for(int i=head[x];i;i=a[i].next_location) { int y=a[i].dot_order; if(!dfn[y]) { hy(y); low[x]=min(low[x],low[y]); } else if(dfn[y]<low[x]&&flag[y]) low[x]=dfn[y]; } if(dfn[x]==low[x]) { number++; int r; do { r=stack[top--]; flag[r]=false; w[r]=n+number; }while(x!=r);/*跟上面的代码不一样对的地方,如果碰到一个low和dfn相等的点,就弹出所有的栈中比它后入栈的点作为一个强连通分量*/ } }}using namespace Tarjan;int main(){ init(); for(int i=1;i<=n;i++) if(!dfn[i]) { top=0; hy(i); } int ans=0; for(int i=1;i<=m;i++) { if(!tr[w[e[i].y]]&&w[e[i].x]!=w[e[i].y]) { tr[w[e[i].y]]=true; ans++; } } printf("%d",number-ans); return 0; }
- tarjan算法详解
- tarjan算法详解
- Tarjan算法详解
- Tarjan算法详解
- Tarjan算法详解
- Tarjan算法详解
- Tarjan算法详解
- Tarjan 算法超级详解
- tarjan算法的详解
- Tarjan算法详解
- Tarjan算法模板 详解
- 强连通分量-tarjan算法模板详解
- 【ACM】tarjan算法详解【强连通分量】
- 强连通算法--Tarjan个人理解+详解
- ★★★★Tarjan算法详解
- tarjan算法
- Tarjan算法
- tarjan算法
- Sql语句Convert函数获取时间格式的一种用法
- android 判断进程是否处于前台
- Excel单元格首位数字为“0”不显示的问题
- hihoCoder之KMP算法
- VHDL 疑难解答
- Tarjan算法详解
- 第四组-2017.10.31
- 大数据基础设施建设需要得到重视 | 记清华大数据“应用·创新”讲座
- 数据湖架构—读书笔记[2]--数据的生命周期
- java多线程学习体系
- win10安装XGBoost,遇到XGBoostLibraryNotFound错误
- LeetCode基础-排序
- 20171030
- 20171030