Tarjan
Tarjan,全名Robert Tarjan,美国计算机科学家,创造了很多算法。
Tarjan算法就是以他命名的,可求解强连通分量、割点和桥。
强连通分量
有向图强连通分量:在有向图G中,如果两个顶点vi,vj间(vi>vj)有一条从vi到vj的有向路径,同时还有一条从vj到vi的有向路径,则称两个顶点强连通(strongly connected)。如果有向图G的每两个顶点都强连通,称G是一个强连通图。有向图的极大强连通子图,称为强连通分量(strongly connected components)。
求解强连通分量、割点、桥的Tarjan算法都差不多,都是通过DFS来找的。
在Tarjan算法中,有几个非常重要的变量。
dfn[i] : 表示第i个顶点在dfs中第几个访问(时间戳)
low[i] : 表示第i个顶点可以回溯到的最早访问的顶点
sta : 一个栈表示当前所有可能能构成是强连通分量的点
ins[i] : 表示第i个顶点是否在栈内
当然很难懂先看伪代码:
void Tarjan(int u):
low[u] = dfn[u] = ++ind;
ins[u] = 1;
stack.push(i);
for v in <u, v>:
if(v is not visited):
Tarjan(v)
low[u] = min(low[u], low[v]);
else if(v in stack):
low[u] = min(low[u], dfn[v]);
//表示这是一个强连通分量
if(low[u] == dfn[u]):
int p;
cnt++;
do:
p = stack.top;
stack.pop;
ins[p] = 0;
belong[p] = cnt;
while(p != u)
当然这伪代码一点也不伪,还是先看看图解吧(图解是参考别人的,嘻嘻)
以这有向图为例
然后从顶点1开始dfs来到下图:
然后到了顶点5的时候不能到dfs其他点了,并且并且low[5] = dfn[5],说明存在强连通分量。把栈中的元素依次出栈,直到遇到了顶点5。这里只出栈了顶点5,说明顶点5单独成为一个强连通分量。
此时dfs回退到顶点3
此时顶点3和顶点5的情况一样,顶点3单独成为一个强连通分量.
回退到顶点2,继续dfs到顶点4
这时候顶点4,dfs到顶点1,发现顶点1已经遍历过了,所以更新顶点4的low为1.
顶点4dfs完了,回退到顶点2,更新顶点2的low = 1,然后回退到顶点1。顶点1也结束了dfs了,并且low[1] = dfn[1]。说明有一个强连通分量。依次退栈直到得到顶点1.
这时候顶点1、 2、 4成为一个强连通分量。
好了,算法就结束了。
模板如下:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define inf 0x3f3f3f3f
typedef pair<int, int> P;
const int maxn = 5e5+5;
const ll mod = 1e9+7;
using namespace std;
vector<int> g[maxn];
int low[maxn], dfn[maxn], sta[maxn], ins[maxn], belong[maxn];//belong是说明他是哪个点的强连通分量
int cnt, ind, tot; //cnt:强连通分量的数量, ind:时间戳, tot:sta的top
void init()
{
memset(ins, 0, sizeof(ins));
memset(belong, 0, sizeof(belong));
memset(dfn, 0, sizeof(dfn));
cnt = ind = tot = 0;
}
//求强连通分量
void Tarjan(int u)
{
low[u] = dfn[u] = ++ind;
ins[u] =1;
sta[++tot] = u;
for(int i=0;i<g[u].size();i++)
{
int v = g[u][i];
if(!dfn[v])
{
Tarjan(v);
low[u] = min(low[u],low[v]);
}
else if(ins[v])
{
low[u] = min(low[u],low[v]);
}
}
int p;
if(low[u] == dfn[u])
{
++cnt;
do
{
p = sta[tot--];
belong[p] = cnt;
ins[p] = 0;
}while(p!=u);
}
}
/*
5 5
1 2
2 3
2 4
3 5
4 1
*/
int main()
{
int n,m;
cin>>n>>m;
for(int i=0;i<m;i++)
{
int a,b;
cin>>a>>b;
g[a].push_back(b);
}
init();
//依次tarjan,就可以避免输入的图不是连通图的情况,若只tarjan(1),非连通图有误。
for (int i=1;i<=n;i++)
if (!dfn[i]) Tarjan(i);
for(int i=1;i<=n;i++)
cout<<belong[i]<<" ";
return 0;
}
输入上面图解的例子,可得:
割点
在一个无向图中,如果删除一个顶点及与它相关联的边之后,图的连通分量增多了,则称这一个点为割点。
判断是否割点有两种情况:
1.对于根节点来说,只要它至少有两颗子树,那么根节点一定是割点。对于其它顶点来说则麻烦一些。
2.回想一下Tarjan算法中low的意义,表示该顶点能够回溯到最早访问的顶点的序号。对于一个顶点来说,在它之前访问的一个顶点是该顶点的父亲,在它父亲之前的是它的祖先,若该顶点能够不通过父亲就回溯到祖先的话,那么说明该顶点的父亲不是割点。所以,若一个顶点的所有儿子都能够不通过它自己就能回溯到祖先顶点,则该顶点不是割点,否则就是割点。
所以对于边<u, v>来说,若low[v] >= dfn[u],则u是割点。
对于上图来说,如果是2和3是割点。
模板如下:
#include<bits/stdc++.h>
#include<iostream>
#include<cstdio>
#include<stdlib.h>
#include<string>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
#define inf 0x3f3f3f3f
#define INF 0x3f3f3f3f3f3f3f3f
#define IO ios::sync_with_stdio(0)
typedef pair<int, int> P;
const int maxn = 2e5+5;
const ll mod = 1e9+7;
using namespace std;
vector<int> g[maxn];
// iscut[i]: 若顶点i是割点,则为1,反之为0
int low[maxn], dfn[maxn], iscut[maxn];
int ind;
int ans= 0 ;
void init()
{
memset(dfn, 0, sizeof(dfn));
memset(iscut, 0, sizeof(iscut));
ind = 0;
}
// pa为u的父节点,初始时Tarjan(i, i)
void Tarjan(int u, int pa)
{
int cnt = 0; //用来记录子树的数量
low[u] = dfn[u] = ++ind;
for(int i=0;i<g[u].size();i++)
{
int v = g[u][i];
if(!dfn[v])//未被访问过
{
Tarjan(v, u);
low[u] = min(low[u], low[v]);
// 若low[v]>=dfn[u],并且u不是根节点,则u是割点
if(low[v] >= dfn[u] && pa!=u)
iscut[u] = 1;
// 若u是根节点,则cnt++
if(u == pa)
cnt++;
}
//else if(v != pa) //若v不等于父节点
low[u] = min(low[u], dfn[v]);
}
if(cnt>=2 && u==pa) //根节点子树数量大于等于2,则为割点
iscut[u] = 1;
}
/*
5 5
1 2
2 3
2 4
3 5
4 1
*/
int main()
{
IO;
int n,m;
cin>>n>>m;
for(int i=0;i<m;i++)
{
int a,b;
cin>>a>>b;
g[a].push_back(b);
g[b].push_back(a);
}
init();
for(int i=1;i<=n;i++)
if(dfn[i]==0)
Tarjan(i,i);
for(int i=1;i<=n;i++)
if(iscut[i]) ans++;
cout<<ans<<endl;
for(int i=1;i<=n;i++)
if(iscut[i])
cout<<i<<" ";
return 0;
}
桥
桥也叫割边,对于边e来说,若去掉了e,图的连通分量增多了,则说明e为桥。
找割边和找割点类似。对于边<u, v>来说,若low[v] > dfn[u], 则<u, v>是桥。
所以上图,边<2, 3>和<3, 5>都是桥。
- 模板:待补充。
此外,tarjan还可以用来缩点。
Tarjan缩点
缩点一般用于有向图,简单来说就是:把图中的一个强联通分量直接缩成一个点
Tarjan缩点之后,图就变成了有向无环图(DAG),大致可以想象成单一有向的线性序列,比如
然后DAG图的一些性质:
性质(结合上面这个DAG图):
①如果超级点入度为0,并且图中只有一个入度为0的超级点,那么显然他可以到达其他任何点,而不能被任何点到达。
②如果超级点出度为0,并且图中只有一个出度为0的超级点,那么显然他可以被任何其他点到达,而不能到达任何点
也是这类题目的一些解题思路了
而上面图解中的例子
就可以缩成这样的图:
也可以缩成这样:
这种重新构图,从1开始标点,当然了要看具体题目的情况。
这里给出,洛谷P3387【模板】缩点的题解,就不要在意一些傻逼注释…
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define inf 0x3f3f3f3f
typedef pair<int, int> P;
const int maxn = 1e5+5;
const ll mod = 1e9+7;
#define IO ios::sync_with_stdio(0)
using namespace std;
vector<int> g[maxn];
int val[maxn],low[maxn], dfn[maxn], sta[maxn], ins[maxn], belong[maxn];
int cnt, ind, tot; //cnt:强连通分量的数量, ind:时间戳, tot:sta的top
int sd[maxn];//缩点点后在哪个点
int in[maxn];//入度
int dist[maxn];
vector<int> sdg[maxn];
int n,m;
void init()
{
memset(ins, 0, sizeof(ins));
memset(belong, 0, sizeof(belong));
memset(dfn, 0, sizeof(dfn));
cnt = ind = tot = 0;
}
//求强连通分量
void Tarjan(int u)
{
low[u] = dfn[u] = ++ind;
ins[u] =1;
sta[++tot] = u;
for(int i=0;i<g[u].size();i++)
{
int v = g[u][i];
if(!dfn[v])
{
Tarjan(v);
low[u] = min(low[u],low[v]);
}
else if(ins[v])
{
low[u] = min(low[u],low[v]);
}
}
int p;
if(low[u] == dfn[u])
{
++cnt;
int y;
while(y=sta[tot--])
{
sd[y] = u;
ins[y] = 0;
if(u==y) break;
val[u]+=val[y];
//cout<<"asd"<<endl;
}
//这行代码会加了两次
// do
// {
// p = sta[tot--];
// sd[p] =u;
// val[u]+=val[p];
// cout<<"asd"<<endl;
// //belong[p] = cnt;
// ins[p] = 0;
// }while(p!=u);
}
}
int tuopu()
{
queue <int> q;
//int tot=0;
for (int i=1;i<=n;i++)
if (sd[i]==i&&!in[i])
{
q.push(i);
dist[i]=val[i];
//cout<<i<<" "<<dist[i]<<endl;
}
while(!q.empty())
{
int k=q.front();q.pop();
for(int i=0;i<sdg[k].size();i++)
{
int v = sdg[k][i];
dist[v] = max(dist[v],dist[k]+val[v]);
in[v]--;
if(in[v]==0) q.push(v);
}
}
int ans=0;
for (int i=1;i<=n;i++)
ans=max(ans,dist[i]);
return ans;
}
/*
5 5
1 2 3 4 5
1 2
2 3
2 4
3 5
4 1
*/
int main()
{
IO;
cin>>n>>m;
for (int i=1;i<=n;i++)
cin>>val[i];
for(int i=0;i<m;i++)
{
int a,b;
cin>>a>>b;
g[a].push_back(b);
}
init();
for (int i=1;i<=n;i++)
if (!dfn[i]) Tarjan(i);
// for(int i=1;i<=n;i++)
// cout<<belong[i]<<" ";
// for(int i=1;i<=n;i++)
// cout<<sd[i]<<" ";
//缩点,建图
for(int i=1;i<=n;i++)
for(int j=0;j<g[i].size();j++)
{
int x = sd[i],y=sd[g[i][j]];
//cout<<x<<" "<<y<<endl;
if(x!=y)
{
sdg[x].push_back(y);
in[y]++;
}
}
cout<<tuopu()<<endl;
return 0;
}
sdg就是缩点后的图。
这题缩点后需要拓扑排序,稍详细的题解下篇博客再说。
然后给出一些缩点的练习题,也是洛谷的:
受欢迎的牛
消息扩散
间谍网络
下篇博客给出我的垃圾题解。
本博客参考的一些博客有:
http://elitedj.me/2019/10/01/Tarjan%E6%B1%82%E8%A7%A3%E5%BC%BA%E8%BF%9E%E9%80%9A%E5%88%86%E9%87%8F%E3%80%81%E5%89%B2%E7%82%B9%E3%80%81%E6%A1%A5/#more
https://blog.csdn.net/m0_38033475/article/details/80081253
https://www.cnblogs.com/stxy-ferryman/p/7779347.html