仅供部落成员学习使用
Tarjan算法简介
Tarjan算法是基于对图深度优先搜索的算法,定义DFN(u)为节点的次序编号(时间戳),Low(u)为u或u的子树能够追溯到的最早的节点的次序号
C++代码,采用链式前向星存图
#include<bits/stdc++.h>
using namespace std;
struct node{
int to, nxt;
}e[M];
int n, m, hd[N], dfn[N], low[N], cnt, tot;
void add(int u, int v)
{
e[++cnt].to = v;
e[cnt].nxt = hd[u];
hd[u] = cnt;
}
void tarjan(int u)
{
dfn[u] = low[u] = ++tot;
for(int j = hd[u]; j; j = e[j].nxt)
{
int v = e[j].to; // v是u的子节点
if(!dfn[v])
{
tarjan(v);
if(low[u] > low[v]) low[u] = low[v];
}
else
{
//这里要格外注意, 有的代码写成了下行注释写法
//此写法在求割点的时候会出错,文章下面有分析 ???
// if(low[u] > low[v]) low[u] = low[v];
if(low[u] > dfn[v]) low[u] = dfn[v];
}
}
}
int main()
{
cin >> n >> m;
int u, v;
for(int i = 1; i <= m; i++)
{
scanf("%d%d", &u, &v);
add(u, v);
}
for(int i = 1; i <= n; i++)
if(!dfn[i]) tarjan(i);
return 0;
}
Tarjan求割点
在无向连通图中,如果将其中一个点以及所有连接该点的边去掉,图就不再连通,那么这个点就叫做割点。
如何利用Tarjan求割点
case1:非root节点且dfn[x] <= low[x的儿子]
对于边(u, v),如果dfn[u]<=low[v],即v即其子树能够回溯到的最早的点,最早也只能是u,要到u前面就需要u的回边或u的父子边。也就是说这时如果把u去掉,u的回边和父子边都会消失,那么v最早能够回溯到的最早的点,已经到了u后面,无法到达u前面的顶点了,此时u就是割点。
case2:root节点 儿子个数>=2
这里要解释一下右边的图形,因为A,B之间有路径连接,所以dfs的顺序为root->A->B,所以root只有一个儿子,root非割点
为什么root要单独考虑呢,下图中Tarjan跑完后所有点的low都为1
low[root] == dfn[root],满足case1,但显然root非割点,所以需要单独考虑
详细代码
#include<bits/stdc++.h>
using namespace std;
const int N = 20001, M = 100001;
int n, m, hd[N], cnt;
int dfn[N], low[N], tot, num;
bool iscut[N];
struct node{
int to, nxt;
}e[M * 2]; // 注意,因为是无向图,所以边的数量是M*2
void add(int u, int v){
e[++cnt].to = v;
e[cnt].nxt = hd[u];
hd[u] = cnt;
}
void tarjan(int root, int u){
dfn[u] = low[u] = ++tot;
int child = 0;
for(int j = hd[u]; j; j = e[j].nxt){
int v = e[j].to;
if(!dfn[v]){
tarjan(root, v);
low[u] = min(low[u], low[v]);
if(root == u) child++;
//不能这里统计割点的数量,会重复 , dfn[u] <= low[v1], dfn[u] <= low[v2].....
//u点会被重复计算多次
if((root == u && child > 1) || (root != u && dfn[u] <= low[v]))
iscut[u] = true;
}
//这里必须是dfn[v], 不能是low[v]解释见下图
else low[u] = min(low[u], dfn[v]);
}
}
int main()
{
cin >> n >> m;
int x, y;
for(int i = 1; i <= m; i++)
scanf("%d %d", &x, &y), add(x, y), add(y, x);
for(int i = 1; i <= n; i++)
if(!dfn[i]) tarjan(i, i);
for(int i = 1; i <= n; i++)
if(iscut[i]) num++;
cout << num << endl;
for(int i = 1; i <= n; i++)
if(iscut[i]) cout << i << " ";
return 0;
}
注释解释,为什么不能是low[u] = min(low[u], low[v])呢
割点模板题
b站邋遢大哥233 视频详讲
Tarjan求割边(桥)
在无向连通图中,如果将其中一条边删除,图就不再连通,那么这条边就叫做割边。
割边要注意无向图的边只能由父——>儿,不能儿——>父, 否则
在low[u] = min(low[u], dfn[v])的作用下,任何dfn[父] >= low[儿]
我们在用链式前向星存边的时候,边号从2开始计算,那么无向图的双向边的编号就是x和 x^1, ^1的作用是对偶数加1,奇数减1
#include<bits/stdc++.h>
using namespace std;
const int N = 20001, M = 100001;
int n, m, hd[N], cnt;
int dfn[N], low[N], tot, num;
bool iscut[N];
struct node{
int to, nxt;
}e[M * 2]; // 注意,因为是无向图,所以边的数量是M*2
void add(int u, int v){
e[++cnt].to = v;
e[cnt].nxt = hd[u];
hd[u] = cnt;
}
void tarjan(int u, int eid){
dfn[u] = low[u] = ++tot;
for(int j = hd[u]; j; j = e[j].nxt){
int v = e[j].to;
if(!dfn[v]){
tarjan(v, j);
low[u] = min(low[u], low[v]);
if (low[v] > dfn[u])
bridge[i] = bridge[i ^ 1] = true;
}
//保证不是指向父节点
else if(i != (eid ^ 1))low[u] = min(low[u], dfn[v]);
}
}
int main()
{
cin >> n >> m;
int x, y;
cnt = 1;
for(int i = 1; i <= m; i++)
scanf("%d %d", &x, &y), add(x, y), add(y, x);
for(int i = 1; i <= n; i++)
if(!dfn[i]) tarjan(i, 0);
for (int i = 2; i < cnt; i += 2)
if (bridge[i])
printf("%d %d\n", e[i ^ 1].to, e[i].to);
return 0;
}
Tarjan求强连通分量
强连通:有向图,任意两点双向互通
强连通分量:有向图的极大强连通子图,极大的概念指即一个有向图,分成很多部分,每一部分内部都可以互相抵达,而不同部分之间不能互相到达或者只能单向到达.
具体代码
#include<bits/stdc++.h>
using namespace std;
const int N = 20001, M = 100001;
int n, m, hd[N], cnt;
int dfn[N], low[N], S[N], beLong[N], top, tot, scNum;
bool inStack[N];
struct node{
int to, nxt;
}e[M * 2]; // 注意,因为是无向图,所以边的数量是M*2
void add(int u, int v){
e[++cnt].to = v;
e[cnt].nxt = hd[u];
hd[u] = cnt;
}
//新增一个栈,inStack[], 一个栈顶下标top
//新增一个标记数组beLong[],记录各个顶点属于哪一个强连通分量, scNum强连通分量的标号
void tarjan(int u){
dfn[u] = low[u] = ++tot;
inStack[u] = true; //标记u点已经在栈里了
S[++top] = u;
for(int j = hd[u]; j; j = e[j].nxt){
int v = e[j].to;
if(!dfn[v]){
tarjan(v);
low[u] = min(low[u], low[v]);
}
//已经确定的强连通分量里的点不能更新当前点
//避免出现两个强连通分量之间有单向边的情况,见下图
else if(inStack[v])low[u] = min(low[u], dfn[v]);
}
//当u的所有子孙节点都搜索完成后
if(dfn[u] == low[u]){
//strong connectivity 强连通分量的标号
scNum++;
int v;
do{
v = S[top--];
inStack[v] = false;
beLong[v] = scNum;
}while(v != u);
}
}
int main()
{
cin >> n >> m;
int x, y;
cnt = 1;
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);
for(int i = 1; i <= n; i++)
cout << beLong[i] << " ";
return 0;
}
else if(inStack[v])low[u] = min(low[u], dfn[v]),如果不加inStack[v]的限制,会出现下图情况,
dfs后所有点的dfn和low都是1,导致3,4点组成的强连通分量未被找出
参考洛谷资料
双连通分量(biconnected component)
Tarjan求边双(E-BCC)
不存在割边(桥)的无向连通图称为边双连通图
在无向图中,如果无论删去那条边都不能使得u和v不连通,则称u和v边双连通
即u到v的路径上无必经边
求解:Tarjan求出割边,标记好割边,再dfs染色一遍(不使用割边)
Trajan求割边
void dfs(int u)
{
beLong[u] = eNum;//染色
for(int j = hd[u]; j; j = e[j].nxt){
{
int v = e[j].to;
if (bridge[j]) // 跳过割边
continue;
if(!beLong[v]) dfs(v);
}
}
for(int i = 1; i <= n; i++){
if(!beLong[i]) dfs(i);
}
P4214 [CERC2015]Juice Junctions
P2860 [USACO06JAN]Redundant Paths G
Tarjan求点双(V-BCC)
在无向图中,如果无论删去那个点(非u点和v点)都不能使得u和v不连通,则称u和v点双连通
即u到v的路径上无必经点(u,v除外)
注意:两个点和一条边构成的图也是BCC
无向连通图中割点一定属于至少两个BCC,非割点只属于一个BCC
割点就算相邻也会属于至少两个BCC;BCC间的交点都是割点,所以非割点只属于一个BCC
解决方法:
到一个结点就将该结点入栈,回溯时若目标结点low值不小于当前结点dfn值就出栈直到目标结点(目标结点也出栈),将出栈结点和当前结点存入BCC,对于每个BCC,它在DFS树中最先被发现的点一定是割点或DFS树的树根
证明:割点是BCC间的交点,故割点在BCC的边缘,且BCC间通过割点连接,所以BCC在DFS树中最先被发现的点是割点;特殊情况是对于开始DFS的点属于的BCC,其最先被发现的点就是DFS树的树根
上面的结论等价于每个BCC都在其最先被发现的点(一个割点或树根)的子树中
这样每发现一个BCC(low[v]>=dfn[u]),就将该子树出栈,并将该子树和当前结点(割点或树根)加入BCC中。
#include<bits/stdc++.h>
using namespace std;
const int N = 20001, M = 100001;
int n, m, hd[N], cnt;
int dfn[N], low[N], S[N], top, tot, bNum;
vector <int> bcc [N];
struct node{
int to, nxt;
}e[M * 2]; // 注意,因为是无向图,所以边的数量是M*2
void add(int u, int v){
e[++cnt].to = v;
e[cnt].nxt = hd[u];
hd[u] = cnt;
}
//新增bcc 可变数组 vector<int>bcc[N];
void tarjan(int u) {
dfn[u] = low[u] = ++tot;
S[++top] = u;
for(int j = hd[u]; j; j = e[j].nxt){
int v = e[j].to;
if(!dfn[v]) {
tarjan(v);
low[u] = min(low[u], low[v]);
if(low[v] >= dfn[u]){ //u是割点或者是根
bNum++;
int v;
do{
v = S[top--];
bcc[bNum].push_back(v);
}while(u != v);
//割点属于多个点双联通分量,所以要把割点重新放回去
S[++top] = v;
//割点什么时候能出栈呢, 只有遍历到另一个割点的时候
}
} else {
low[u] = min(low[u], dfn[v]);
}
}
}
int main()
{
cin >> n >> m;
int x, y;
cnt = 1;
for(int i = 1; i <= m; i++)
// 单向边
scanf("%d %d", &x, &y), add(x, y), add(y, x);
for(int i = 1; i <= n; i++)
if(!dfn[i]) tarjan(i);
for(int i = 1; i <= bNum; i++){
for(int j = 0; j < bcc[i].size(); j++)
cout << bcc[i][j] << " ";
cout << endl;
}
return 0;
}
dfs时不越过割点,即可求解点双连通图
SP2878 KNIGHTS - Knights of the Round Table
P3225 [HNOI2012]矿场搭建
P5058 [ZJOI2004]嗅探器
Tarjan缩点
就是 tarjan求出的所有强连通分量都变成点(集合或共享某些信息),这样有向有环图就变成有向无环图(DAG)
#include<bits/stdc++.h>
using namespace std;
const int N = 20001, M = 100001;
int n, m, hd[N], cnt;
int a[N], dfn[N], low[N], S[N], beLong[N], top, tot;
bool inStack[N], exist[N];
struct node{
int u, to, nxt;
}e[M * 2]; // 注意,因为是无向图,所以边的数量是M*2
void add(int u, int v){
e[++cnt].to = v;
e[cnt].u = u;
e[cnt].nxt = hd[u];
hd[u] = cnt;
}
//新增一个栈,inStack[], 一个栈顶下标top
//新增一个标记数组beLong[],记录各个顶点属于哪一个强连通分量, scNum强连通分量的标号
void tarjan(int u){
dfn[u] = low[u] = ++tot;
inStack[u] = true; //标记u点已经在栈里了
S[++top] = u;
for(int j = hd[u]; j; j = e[j].nxt){
int v = e[j].to;
if(!dfn[v]){
tarjan(v);
low[u] = min(low[u], low[v]);
}
//已经确定的强连通分量里的点不能更新当前点
//避免出现两个强连通分量之间有单向边的情况,见下图
else if(inStack[v])low[u] = min(low[u], dfn[v]);
}
//当u的所有子孙节点都搜索完成后
if(dfn[u] == low[u]){
//strong connectivity 强连通分量的标号
int v;
exist[u] = true;
do{
v = S[top--];
inStack[v] = false;
beLong[v] = u;
//合并强连通分量的权值
a[u] += a[v];
}while(v != u);
//最后多算了一次a[u] + a[u]
a[u] /= 2;
}
}
int main()
{
cin >> n >> m;
int x, y;
cnt = 1;
for(int i = 1; i <= m; i++)
// 单向边
scanf("%d %d", &x, &y), add(x, y);
// 输入每个点的权值
for(int i = 1; i <= n; i++)
scanf("%d", &a[i]);
for(int i = 1; i <= n; i++)
if(!dfn[i]) tarjan(i);
//重新建图
cnt = 0;
for(int j = 1; j <= m; j++){
//缩点后会存在重边,一般来说没有影响
//如果属于两个不同的缩点,则创建新边
if(beLong[e[j].u] != beLong[e[j].to]){
add(beLong[e[j].u], beLong[e[j].to]);
}
}
return 0;
}