Tarjan(原理、应用)
目录
- Tarjan
- 一、算法介绍
- 二、原理
- 三、应用
- 1、求强连通分量
- 例1 [[POJ 3180]](http://poj.org/problem?id=3180) The Cow Prom
- 例2 [[POJ 2186]](http://poj.org/problem?id=2186)受欢迎的牛
- 2、求割点
- 例题 [[洛谷 3388]](https://www.luogu.org/problem/P3388) 割点(割顶)
- 3、求桥(割边)
- 例题 [[HDU 4378]](http://acm.hdu.edu.cn/showproblem.php?pid=4738) Caocao's Bridges
Tarjan
首先记录说明一下图论中的常用的概念
- 无向图中,如果任意两个顶点之间都能够连通,则称此无向图为连通图
- 有向图中,若任意两个顶点 ViV_iVi 和VjV_jVj,满足从 $V_i $到 VjV_jVj 以及从 VjV_jVj 到 ViV_iVi 都连通,也就是都含有至少一条通路,则称此有向图为强连通图
- 若无向图不是连通图,但图中存储某个子图符合连通图的性质,则称该子图为连通分量
- 非强连通图有向图的极大强连通子图(最大的子图),称为强连通分量
- 若一个无向图中的去掉任意一个节点(一条边)都不会改变此图的连通性,即不存在割点(桥),则称作点(边)双连通图
- 在无向连通图中,如果将其中一个点以及所有连接该点的边去掉,图就不再连通或者连通分枝数增加,那么这个点就叫做割点
- 桥(割边)——指的是一条边,就是如果没有这条边,图的连通分量就会增加
- 没有圈的连通图叫树,树的边数恰好是顶点数减1,没有圈的非连通图叫做森林
一、算法介绍
TarjanTarjanTarjan是一种由RobertTarjanRobert TarjanRobertTarjan提出的求解有向图强连通分量的线性时间的算法。通常可以用来求强连通分量、双连通分量、缩点、割点、割边等问题。
例如上图中顶点1、2、31、2、31、2、3组成的子图就是这个有向图的强连通分量
TarjanTarjanTarjan算法可以找出这样的强连通分量,TarjanTarjanTarjan算法是基于对整个有向图的dfsdfsdfs进行的,将整个图作为一棵搜索树,图中的强连通分量作为搜索树的子树。
对于上图,很容易看出强连通分量,但是要让计算机找出来,那么肯定要做好相应的标记,如果对于这张图进行dfsdfsdfs遍历,假设从1开始搜索(用链式前向星存),搜索到的边如下
1−>2−>4−>51->2->4->51−>2−>4−>5、2−>3−>12- >3->12−>3−>1,
可以发现,只有333到111这条边搜索到了已经走过的顶点111,如果按照走过的顶点的先后顺序来表示时间,那么关系如下
顶点 | 访问时间(第几个访问到的) |
---|---|
1 | 1 |
2 | 2 |
3 | 5 |
4 | 3 |
5 | 4 |
很明显,如果从一个点出发不断遍历,发现有一个能够走回之前已经走过的点,说明形成了一个环,环上的点能够互相访问,环上的所有点都是强连通的
二、原理
上面举了一个例子,基于这一点,很容易想到需要维护一个访问时间顺序的数组,知道了访问顺序,那么怎么找出其中和这个点构成的所有强连通分量呢?同样需要一个数组来维护,在搜索每一个点的时候,如果发现这个点已经被访问过,说明形成了环,这时候就可以将当前的点进行更新,更新成已经访问过的点的顺序。因此TarjanTarjanTarjan中重要的两个数组需要维护好
low[N];//low[i]的值表示i能够回溯的最小的祖先
dfn[N];//表示时间戳,dfn[i]的值表示第几个访问到i节点的
由于是递归实现的,明显数组lowlowlow是靠着回溯的时候更新的,而dfndfndfn是靠着递进去的时候更新的
struct Edge {int to, next;
}edge[M * 2];
void add_Edge(int u, int v) {edge[++cnt].to = v;edge[cnt].next = head[u];head[u] = cnt;
}
void Tarjan(int x) { //表示当前的顶点xdfn[x] = low[x] = ++c_time; //更新时间戳for(int i = head[x]; i != -1; i = edge[i].next) { //访问所有x能够一步到达的顶点,用v表示int v = edge[i].to;if(!dfn[v]) { //如果顶点v没有访问过,就继续找下去Tarjan(v);low[x] = min(low[x], low[v]);//v是由x走过去的,因此x是v的祖先,如果有环,则可能low[v]小于low[x],因此要更新low[x]}low[x] = min(low[x], dfn[v]);//当v已经被访问过,出现了环,当前x则更新到小的那个节点}
}
上图中更新结果
dfn[1]=1、dfn[2]=2、dfn[3]=5、dfn[4]=3、dfn[5]=4dfn[1] = 1、dfn[2] = 2、dfn[3] = 5、 dfn[4] = 3、 dfn[5] = 4dfn[1]=1、dfn[2]=2、dfn[3]=5、dfn[4]=3、dfn[5]=4
low[1]=1、low[2]=1、low[3]=1、low[4]=3、low[5]=4low[1] = 1、low[2] = 1、low[3] = 1、low[4] = 3、low[5] = 4low[1]=1、low[2]=1、low[3]=1、low[4]=3、low[5]=4
使用Tarjan的时候,如果不能保证可以一遍搜完整个图,那么使用方式如下
for(int i = 1; i <= n; i++)if(!dfn[i])Tarjan(i);
三、应用
1、求强连通分量
强连通分量需要引入栈来进行记录,每次进入递归的时候都进栈。考虑这样一种情况,当前的节点xxx在更新完之后,如果dfn[x]=low[x]dfn[x] = low[x]dfn[x]=low[x]说明,x以及它的所有子节点构成强连通分量,因此在这个时候需要把xxx节点后面进栈的节点和xxx全部弹出,这些节点都是和xxx强连通的
void Tarjan(int x) {dfn[x] = low[x] = ++c_time;stack[++t] = x; //进栈vst[x] = 1; //表示顶点x在栈中for(int i = head[x]; i != -1; i = edge[i].next) {int v = edge[i].to;if(!dfn[v]) {Tarjan(v);low[x] = min(low[x], low[v]);}else if(vst[v]) //如果v在栈中,并且已经访问或,则肯定要更新low[x]low[x] = min(low[x], dfn[v]);}if(dfn[x] == low[x]) { //出现强连通分量子树的最小根int cur;do {cur = stack[t--]; //弹栈vst[cur] = 0; //标记出栈cout << cur << " ";}while(x != cur);cout << "\n";}
}
例1 [POJ 3180] The Cow Prom
题意:给出nnn个点mmm条边,求出有向图中所有大于111的强连通分量个数
5 4
2 4
3 5
1 2
4 1
1
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <cctype>
#define N 10005
#define M 50010
using namespace std;
inline int read() {int x = 0, f = 1; char c = getchar();while(!isdigit(c)) {if(c == '-') f = -1; c = getchar();}while(isdigit(c)) {x = (x << 3) + (x << 1) + c - 48; c = getchar();}return f * x;
}
struct Edge{int to, next;
}edge[M * 2];int n, m, cnt, c_time, t, ans;
int head[N], dfn[N], low[N], stack[N];
bool vst[N];
inline void addEdge(int u, int v) {edge[++cnt].to = v;edge[cnt].next = head[u];head[u] = cnt;
}void Tarjan(int x) {dfn[x] = low[x] = ++ c_time;stack[++t] = x;vst[x] = 1;for(int i = head[x]; i != -1; i = edge[i].next) {int v = edge[i].to;if(!dfn[v]) {Tarjan(v);low[x] = min(low[x], low[v]);}else if(vst[v]) {low[x] = min(low[x], dfn[v]);}}int now = 0;if(dfn[x] == low[x]) { //找到所有以x为根的强连通分量int cur;do{cur = stack[t--];vst[cur] = 0;now ++;}while(cur != x);}if(now > 1) ans ++;
}int main() {int u, v;memset(head, -1, sizeof(head));n = read(), m = read();for(int i = 1; i <= m; i++) {u = read(), v = read();addEdge(u, v);}for(int i = 1; i <= n; i++)if(!dfn[i]) Tarjan(i);cout << ans;return 0;
}
例2 [POJ 2186]受欢迎的牛
3 3
1 2
2 1
2 3
1
#include <iostream>
#include <cctype>
#include <algorithm>
#include <cstring>
#include <cstdio>
#define M 50050
#define N 10050
using namespace std;inline int read() {int x = 0, f = 1; char c = getchar();while(!isdigit(c)) {if(c == '-') f = -1; c = getchar();}while(isdigit(c)){x = (x << 3) + (x << 1) + c - 48; c = getchar();}return f * x;
}
struct Edge{int to, next;
}edge[M * 2];
//color表示染色标记的数组,sum[i]表示标记为i的图中顶点的数量,r数组表示出度
int head[N], low[N], dfn[N], color[N], sum[N], stack[N], r[N];
//c_time 表示时间戳,t用来记录栈中的元素个数,rs用来记录染色的数量,也就是连通分量的数量
int cnt, n, m, c_time, t, rs;
bool vst[N];inline void add_Edge(int u, int v) {edge[++cnt].to = v;edge[cnt].next = head[u];head[u] = cnt;
}void Tarjan(int x) {dfn[x] = low[x] = ++ c_time;stack[++t] = x; vst[x] = 1;for(int i = head[x]; i != -1; i = edge[i].next) {int u = edge[i].to;if(!dfn[u]) {Tarjan(u);low[x] = min(low[x], low[u]);} else if (vst[u]) {low[x] = min(low[x], dfn[u]);}}if(dfn[x] == low[x]) {int cur;rs ++; //连通分量的数量增加do{cur = stack[t--];color[cur] = rs; //染色,标记sum[rs] ++; //记录该标记下的点数量vst[cur] = 0;}while(cur != x);}
}int main () {memset(head, -1, sizeof(head));int u, v, judge = 0, loc;n = read(), m = read();for(int i = 1; i <= m; i++) {u = read(), v = read();add_Edge(u, v);}for(int i = 1; i <= n; i++)if(!dfn[i]) Tarjan(i);for(int i = 1; i <= n; i++) {for(int j = head[i]; j != -1; j = edge[j].next) {int ve = edge[j].to;if(color[i] != color[ve]) { //如果顶点i和ve不是同一连通分量,那么顶点i的标记出度+1(因为有缩点)r[color[i]] ++;}}}for(int i = 1; i <= rs; i++) { //遍历DAGif(r[i] == 0) { //出度为0judge ++;loc = i; //记录标记}}if(judge == 1) cout << sum[loc]; //输出带有这种标记的顶点数量else cout << 0;return 0;
}
2、求割点
割点:在无向联通图 G=(V,E)G=(V,E)G=(V,E)中: 若对于x∈Vx∈Vx∈V, 从图中删去节点xxx以及所有与xxx关联的边之后,GGG分裂成两个或两个以上不相连的子图, 则称xxx为GGG的割点。
割边(桥):在一个无向图中,如果有一个边集合,删除这个边集合以后,图的连通分量增多,就称这个边集为割边集合,如果某个割边集合只含有一条边 X(也即{X}是一个边集合),那么X称为一个割边,也叫做桥。
根据定义来看,割点为3、43、43、4,桥为d、ed、ed、e,有两种情况会出现割点
- 当对于点xxx存在儿子节点yyy,使得dfn[x]≤low[y]dfn[x] \leq low[y]dfn[x]≤low[y]则xxx一定是割点
- 如果根节点有222个及以上的儿子,那么它也是割点(特判)
例题 [洛谷 3388] 割点(割顶)
下面mmm行每行输入x,yx,yx,y表示xxx到yyy有一条边
6 7
1 2
1 3
1 4
2 5
3 5
4 5
5 6
1
5
#include <iostream>
#include <cstdio>
#include <cctype>
#include <cstring>
#define N 20005
#define M 100005
using namespace std;inline int read() {int x = 0, f = 1; char c = getchar();while(!isdigit(c)) {if(c == '-') f = -1; c = getchar();}while(isdigit(c)) {x = x * 10 + c - 48; c = getchar();}return f * x;
}struct Edge {int to, next;
}edge[M * 2];int s_clock, cnt, n, m;
int dfn[N], low[N], head[N];
//low[i]表示i能回溯到的最小祖先,dfn[i]表示时间戳,也就是第几个访问到的
bool vst[N], cut[N];inline void add_edge(int u, int v) {edge[++cnt].to = v;edge[cnt].next = head[u];head[u] = cnt;
}
void init() {memset(head, -1, sizeof(head));
}
void Tarjan(int x, int root) {int r = 0; //用来判断根节点是否是割点dfn[x] = low[x] = ++s_clock; //更新时间戳for(int i = head[x]; i != -1; i = edge[i].next) {int u = edge[i].to;if(!dfn[u]) {dfn[u] = 1;Tarjan(u, root);low[x] = min(low[x], low[u]); // ****if(dfn[x] <= low[u] && x != root) cut[x] = 1; //判断割点,当前节点x的子节点u能回溯的最小祖先小于当前节点的时间戳,说明一定没有往回的路,那么当前节点一定是一个割点if(x == root) r ++; //最终回溯到了根节点} low[x] = min(low[x], dfn[u]); // ****}if(x == root && r > 1) cut[root] = 1; //如果有2条及以上的路 能回到根节点,那么根节点也是割点
}int main() {init();int u, v, ans = 0;n = read(), m = read();for(int i = 1; i <= m; i++) {u = read(), v = read();add_edge(u, v);add_edge(v, u);}for(int i = 1; i <= n; i++)if(!dfn[i]) Tarjan(i, i);for(int i = 1; i <= n; i++) //统计割点的个数if(cut[i]) ans ++;cout << ans << "\n";for(int i = 1; i <= n; i++) //输出割点if(cut[i]) cout << i << " ";return 0;
}
3、求桥(割边)
割边(桥):在一个无向图中,如果有一个边集合,删除这个边集合以后,图的连通分量增多,就称这个边集为割边集合,如果某个割边集合只含有一条边 X(也即{X}是一个边集合),那么X称为一个割边,也叫做桥。
和求割点的方法类似,桥的判断如下
- uuu的子节点是vvv,当且仅当dfn[u]<low[v]dfn[u] < low[v]dfn[u]<low[v]时,(u,v)(u,v)(u,v)是桥
但是由于是无向图,可能会有重边的情况,为了统一处理,可以利用链式前向星存边的特性,同一条边的序号一定是相邻的,因此在更新low[x]low[x]low[x]的时候,需要判断当前边是否和上一条边相同
void Tarjan(int x, int fa) {low[x] = dfn[x] = ++c_time;for(int i = head[x]; i != -1; i = edge[i].next) {int u = edge[i].to;if(!dfn[u]) {Tarjan(u, i);low[x] = min(low[x], low[u]);if(dfn[x] < low[u]) ans++;}else if((i + 1) / 2 != (fa + 1) / 2) low[x] = min(low[x], dfn[u]); //不是同一条边}
}
例题 [HDU 4378] Caocao’s Bridges
对于每个测试用例,输出周瑜必须发送的最小士兵号码才能完成任务。如果周瑜无法成功,请输出-1代替。
3 3
1 2 7
2 3 4
3 1 4
3 2
1 2 7
2 3 4
0 0
-1
4
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cctype>
#include <algorithm>
#define N 1005
#define M 1000005
#define INF 0x7fffffff
using namespace std;inline void read(int &x) {x = 0; int f = 1; char c = getchar();while(!isdigit(c)) {if(c == '-') f = -1; c = getchar();}while(isdigit(c)){x = (x << 3) + (x << 1) + c - 48; c = getchar();}x *= f;
}struct Edge{int to, next, w;
}edge[M * 2];int head[N], low[N], dfn[N], f[N];
int cnt, c_time, ans, n, m;inline void init() {memset(head, -1, sizeof(head));memset(low, 0, sizeof(low));memset(dfn, 0, sizeof(dfn));for(int i = 1; i <= n; i ++)f[i] = i;cnt = 0;c_time = 0;ans = INF;
}inline void addEdge(int u, int v, int cost) {edge[++cnt].to = v;edge[cnt].w = cost;edge[cnt].next = head[u];head[u] = cnt;
}inline int find(int x) {if(x == f[x]) return x;else return f[x] = find(f[x]);
}
inline void unite(int x, int y) {int u = find(x);int v = find(y);f[u] = v;
}
inline bool IsSame(int x, int y) {int u = find(x);int v = find(y);if(u == v) return true;else return false;
}
inline void Tarjan(int x, int fa) {dfn[x] = low[x] = ++c_time;for(int i = head[x]; i != -1; i = edge[i].next) {int u = edge[i].to;if(!dfn[u]) {Tarjan(u, i);low[x] = min(low[x], low[u]);if(dfn[x] < low[u]) ans = min(ans, edge[i].w);//更新桥的最小权值}else if((i + 1) / 2 != (fa + 1) / 2) low[x] = min(low[x], dfn[u]);}
}
int main() {int u, v, cost, flag;while(1) {read(n), read(m);if(n == 0 && m == 0) break;flag = 0;init();for(int i = 1; i <= m; i ++) {read(u), read(v), read(cost);unite(u, v);addEdge(u, v, cost);addEdge(v, u, cost); }for(int i = 1; i < n; i++) //判断图是否是连通的if(!IsSame(i, i+1)) {printf("0\n");flag = 1;break;}if(flag) continue;Tarjan(1, 0);if(ans == INF) ans = -1; //没有桥的情况if(ans == 0) ans++; //桥上没有人的情况printf("%d\n", ans);}return 0;
}
Tarjan(原理、应用)相关推荐
- 有向图的强连通分量--Tarjan算法---代码分析
本文就是做个代码分析,顺便说下理解. 一.预备知识: 需要知道什么是: 回边.前向边.交叉边 二.上代码: #include<algorithm> #define NIL -1using ...
- Tarjan 算法详解
一个神奇的算法,求最大连通分量用O(n)的时间复杂度,真实令人不可思议.转自 废话少说,先上题目 题目描述: 给出一个有向图G,求G连通分量的个数和最大连通分量. 输入: n,m,表示G有n个点,m条 ...
- 有向图强连通分量tarjan算法
转自:http://www.byvoid.com/blog/scc-tarjan/ http://blog.csdn.net/geniusluzh/article/details/6601514 在有 ...
- POJ2186-Popular Cows(流行的奶牛)【tarjan,强连通分量,图论】
正题 题目链接 大意 有n头奶牛,奶牛们会认为有些奶牛很受欢迎,受欢迎会互相传递,如:如果A认为B很受欢迎,而B认为C受欢迎,那么A也会认为C是受欢迎的.然后求每一个奶牛都认为受欢迎的奶牛数量. 解题 ...
- 图论 —— 图的连通性 —— Tarjan 求双连通分量
[概念] 1.双连通分量:对于一个无向图,其边/点连通度大于1,满足任意两点之间,能通过两条或两条以上没有任何重复边的路到达的图,即删掉任意边/点后,图仍是连通的 2.分类: 1)点双连通图:点连通度 ...
- 图论 —— 图的连通性 —— Tarjan 求割点与桥
[概念] 1.割点 1)割点:删除某点后,整个图变为不连通的两个部分的点 2)割点集合:在一个无向图中删除该集合中的所有点,能使原图变成互不相连的连通块的点的集合 3)点连通度:最小割点集合点数 如上 ...
- 强连通基础与例题(Kosaraju算法与Tarjan算法)
目录 Kosaraju算法 Tarjan算法 例题 A:HDU-1269 迷宫城堡 B:HDU-2767 Proving Equivalences C:HDU-1827 Summer Holiday ...
- Tarjan算法小结1——SCC
引入 许多最短单源路径算法,如Dijkstra,SPFA, floyd, Bellman-Ford等,在运用时只能给出指定点到任意点的最短距离,抑或是给出图中是否有环的信息,并不能准确确定环的个数.包 ...
- 洛谷·[POI2005]SKA-Piggy Banks 小猪存钱罐【Tarjan 并查集
初见安~这里是传送门:洛谷P3420 题目描述3 Byteazar the Dragon has NN piggy banks. Each piggy bank can either be opene ...
最新文章
- windows系统中hosts文件位置
- 支持向量机(SVM)PPT
- html vue分页,Vue.js bootstrap前端实现分页和排序
- 【数据结构】树状数组
- tde数据库加密_如何在TDE加密的数据库上配置SQL Server镜像
- 第三方侧滑菜单SlidingMenu在android studio中的使用
- IOS音频1:之采用四种方式播放音频文件(一)AudioToolbox AVFoundation OpenAL AUDIO QUEUE...
- KL散度、JS散度以及交叉熵对比
- freeSHHd+puttygen搭建Sftp
- python 基础代谢率计算_【Python 19】BMR计算器3.0(字符串分割与格式化输出)
- 计算机没有安装鼠标和键盘驱动,鼠标不能用如何安装驱动程序-使用键盘安装鼠标驱动的方法 - 河东软件园...
- Android之简洁天气
- 台式机win10关闭fn热键_笔记本fn键,小编告诉你笔记本fn键怎么取消
- [OpenCV实战]23 使用OpenCV获取高动态范围成像HDR
- 粒子群课设_粒子群算法(人工智能结课论文)
- I.MX6Q(TQIMX6Q)--资料汇总
- 中国历史各王朝的知识点总结记忆
- Pytorch:CycleGAN代码中nn.Sequential(*module)处错误:list is not a Module subclass
- 百家号自媒体文章出现哪些因素会不推荐?
- 详谈redis命令之集合(SET)