文章目录
tarjan理解与模板
相关概念和变量
2. 时间戳
d
f
n
[
u
]
dfn[u]
dfn[u]:在
d
f
s
dfs
dfs遍历时,第一次遇到某个节点
u
u
u的时间。如下图红色记录每个节点的
d
f
n
dfn
dfn。
假设在遍历时从
1
1
1开始,那么
d
f
n
[
1
]
=
1
dfn[1]=1
dfn[1]=1,
1
1
1接下去遍历
2
2
2,则
d
f
n
[
2
]
=
2
dfn[2]=2
dfn[2]=2,
2
2
2接下去遍历
4
4
4,
d
f
n
[
4
]
=
3
dfn[4]=3
dfn[4]=3,
4
4
4接下去遍历
1
1
1,因为
1
1
1已经遍历过了,因此忽略
1
1
1,返回
4
4
4。
4
4
4接下去遍历
6
6
6,
d
f
n
[
6
]
=
4
dfn[6]=4
dfn[6]=4…
(非常无聊地查了
d
f
n
dfn
dfn是什么的缩写,
d
o
n
e
f
o
r
n
o
w
done \space for \space now
done for now ??可能是说到这里这个点已经遍历好了。)
3.
l
o
w
[
u
]
low[u]
low[u]:在
d
f
s
dfs
dfs遍历时,节点
u
u
u可以到达的节点中的最早时间,也可能是它自己的时间就是它可以到达的最早时间。
比如上图,到了节点
4
4
4,它的
d
f
n
[
4
]
=
3
dfn[4]=3
dfn[4]=3,但是它的
l
o
w
low
low值不是
3
3
3而是
1
1
1,因为
4
4
4可以通向
1
1
1,而
1
1
1的时间为
1
1
1比
3
3
3大,因此更新
l
o
w
[
4
]
=
1
low[4]=1
low[4]=1。而
6
6
6的
l
o
w
low
low值和它的
d
f
n
dfn
dfn值是相等的,因为它无法到达其他的节点,因此节点
6
6
6可以到达的节点中的最早时间就是它本身的时间戳。
这个
l
o
w
low
low值的含义我是这样理解的:先将图想象成一颗树(在还没有遇到环的时候),第一个遍历到的点就相当于树的根。那么所有点的
d
f
n
dfn
dfn与
l
o
w
low
low都是相等的,因为某个节点继续向孩子遍历,时间只会越来越大。一旦遇到环,那么当前的节点可能到达一个层数比自己还小的节点。
若将上面的图画成树,对于节点
4
4
4,原本在第三层,但是成环之后它又连向第一层,从而
l
o
w
low
low值减少,就像是攀亲戚一样,想攀就往越高的地方攀去以获得更小的
l
o
w
low
low值。
强连通分量和缩点
用一个栈来实现求强连通分量的功能。
要求某个强连通分量,擒贼先擒王,我们先找出这个强连通分量中出现时间最早的节点
f
a
fa
fa,将其压入栈中,这个点就作为这个强连通分量中其他节点的父亲;然后将这个强连通分量的所有节点都会压入栈中,同时更新这些节点的
l
o
w
low
low值;最后将所有节点都弹出栈并且将它们的父亲设置为
f
a
fa
fa。相当于整个强连通分量用
f
a
fa
fa来表示,缩成
f
a
fa
fa一个点。
详解版👇
vector<int>son[N];//存储每个点连向的点
int dfn[N],low[N],dfncnt,fa[N],st[N],top;
bool vis[N];//vis[i]为真 表示i节点在栈中
void tarjan(int u){
//更新时间戳 dfncnt为当前遍历的时间
dfn[u]=low[u]=++dfncnt;
vis[u]=true;//将当前u点压入栈中
st[++top]=u;
for(int i=0;i<son[u].size();i++){//遍历后继节点
int v=son[u][i];
/*如果后继节点还没有遍历过就去遍历它(注意 vis是用来标记是否在栈中
要判断是否遍历过需要用dfn 因为一旦dfn不为0就说明已经遍历过了*/
if(!dfn[v]){
tarjan(v);
low[u]=min(low[u],low[v]);
}
/*要当前的点还在栈中才能用dfn[v]更新low[u],
若当前点已经不在栈中,说明它已经找到了属于自己的强连通分量
不可能属于当前u节点所在的强连通分量了
*/
else if(vis[v])
low[u]=min(low[u],dfn[v]);
}
/* 满足dfn[u]==low[u],说明它出现时间最早,因此是它所在的强连通分量的fa。
只有找到整个强连通分量的fa才会对整个强连通分量进行缩点和将这些节点弹出栈,
因此至此已经遍历过并且不在栈中的点必然找到了属于自己的强连通分量
这里也是前面要用dfn[v]更新low[u]时需要判断vis[v]的根本原因
*/
if(dfn[u]==low[u])
do
{
int t=st[top];
vis[t]=false;
fa[t]=u;
}while(st[top--]!=u);
}
精简版👇
vector<int>son[N];
int dfn[N],low[N],dfncnt,fa[N],st[N],top;
bool vis[N];
void tarjan(int u){
dfn[u]=low[u]=++dfncnt;
vis[u]=true;
st[++top]=u;
for(int i=0;i<son[u].size();i++){
int v=son[u][i];
if(!dfn[v]){
tarjan(v);
low[u]=min(low[u],low[v]);
}
else if(vis[v])
low[u]=min(low[u],dfn[v]);
}
if(dfn[u]==low[u])
do
{
int t=st[top];
vis[t]=false;
fa[t]=u;
}while(st[top--]!=u);
}
习题
368. 银河 【差分约束 判正环】
题目链接
差分约束求最小值,相当于求结果大于等于某个值,最后答案形式为 dis >= ans,因此求最长路;即:求最大值则求最短路,求最小值则求最长路。
求最短路时化成:dis[v] - dis[u] <= w 连接 u->v的边
求最长路时化成: dis[v] - dis[u] >= w 连接 u->v的边
此题的边权都是非负的,求是否存在正环。因此只要缩点之后,若在一个连通分量里存在一条正边,则必然存在正环。但是枚举强连通分量里的边需要枚举每个点,复杂度太大。因此可【直接枚举每条正权边,看它们是否在同一个强连通分量里】来判断正环,复杂度为 O ( m ) O(m) O(m)。
#include <iostream>
#include <algorithm>
#include <cstring>
#include <cmath>
#include <queue>
#include<map>
#include<set>
#define LL long long
using namespace std;
const int N = 100000 + 10, M = 200000 + 10;
int n, m;
struct E{
int to, nxt, w;
}e[M], e_[M];
int head[N], cnt, head_[N], cnt_;
void add(int u, int v, int w)
{
e[++ cnt] = {v, head[u], w};
head[u] = cnt;
}
void add_(int u, int v, int w)
{
e_[++ cnt_] = {v, head_[u], w};
head_[u] = cnt_;
}
int low[N], dfn[N], dfn_cnt, c[N], scc_cnt, in[N];
bool vis[N];
vector<int> scc[N];
int stk[N], top;
int dis[N];
void tarjan(int u)
{
low[u] = dfn[u] = ++ dfn_cnt;
stk[++ top] = u, vis[u] = true;
for(int i = head[u]; i; i = e[i].nxt)
{
int v = e[i].to;
if(!dfn[v])
{
tarjan(v);
low[u] = min(low[v], low[u]);
}
else if(vis[v])
low[u] = min(low[u], dfn[v]);
}
if(low[u] == dfn[u])
{
++ scc_cnt;
int v;
do
{
v = stk[top --];
vis[v] = false;
scc[scc_cnt].push_back(v);
c[v] = scc_cnt;
}while(v != u);
}
}
void dfs(int u)
{
vis[u] = true;
for(int i = head_[u]; i; i = e_[i].nxt)
{
int v = e[i].to;
if(!vis[v]) dfs(v);
}
}
int main()
{
scanf("%d%d", &n, &m);
for(int i = 1; i <= m; i ++)
{
int op, a, b; scanf("%d%d%d", &op, &a, &b);
switch (op)
{
case 1: //a - b >= 0 b - a >=0
add(a, b, 0), add(b, a, 0);
break;
case 2:// a < b b > a b >= a + 1
add(a, b, 1);
break;
case 3: // a >= b
add(b, a, 0);
break;
case 4: // a > b a >= b + 1
add(b, a, 1);
break;
case 5: //a <= b b >= a
add(a, b, 0);
break;
default:
break;
}
}
for(int i = 1; i <= n; i ++)
if(!dfn[i]) tarjan(i);
for(int u = 1; u <= n; u ++)
{
for(int i = head[u]; i; i = e[i].nxt)
{
int v = e[i].to;
if(c[u] == c[v])
{
if(e[i].w > 0)
{
puts("-1");
return 0;
}
}
else
{
add_(c[u], c[v], e[i].w);
in[c[v]] ++;
}
}
}
//建新图
//拓扑排序
queue<int>q;
LL ans = 0;
for(int i = 1; i <= scc_cnt; i ++)
{
if(!in[i])
{
q.push(i); dis[i] = 1;
}
}
while(!q.empty())
{
int u = q.front(); q.pop();
for(int i = head_[u]; i; i = e_[i].nxt)
{
int v = e_[i].to;
dis[v] = max(dis[v], dis[u] + e_[i].w);
in[v] --;
if(!in[v]) q.push(v);
}
}
for(int i = 1; i <= scc_cnt; i ++)
ans += 1ll * scc[i].size() * dis[i];
printf("%lld", ans);
return 0;
}
P2341 [USACO03FALL / HAOI2006] 受欢迎的牛 G
#include<bits/stdc++.h>
using namespace std;
const int N=1e4+7,M=5E4+7;
vector<int>son[N];
set<int>ans;
int dfn[N],low[N],dfncnt,n,m,out[N],fa[N];
int st[N],top;
bool vis[N];
void tarjan(int u){
dfn[u]=low[u]=++dfncnt;
vis[u]=true;
st[++top]=u;
for(int i=0;i<son[u].size();i++){
int v=son[u][i];
if(!dfn[v]){
tarjan(v);
low[u]=min(low[u],low[v]);
}
else if(vis[v])
low[u]=min(low[u],dfn[v]);
}
if(dfn[u]==low[u])
do
{
int t=st[top];
vis[t]=false;
fa[t]=u;
}while(st[top--]!=u);
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++)fa[i]=i;
for(int i=1;i<=m;i++){
int u,v;cin>>u>>v;
son[u].push_back(v);
}
for(int i=1;i<=n;i++){
if(!dfn[i]){
tarjan(i);
}
}
for(int i=1;i<=n;i++){
for(int j=0;j<son[i].size();j++){
if(fa[i]==fa[son[i][j]])continue;
out[fa[i]]++;
}
}
for(int i=1;i<=n;i++){
if(!out[fa[i]])ans.insert(fa[i]);
}
if(ans.size()!=1){
cout<<0<<endl;
}
else {
int cnt=0,res=*ans.begin();
for(int i=1;i<=n;i++){
if(fa[i]==res)cnt++;
}
cout<<cnt<<endl;
}
return 0;
}
P2921 [USACO08DEC]Trick or Treat on the Farm G
- 因为每个点的出度只有一个,一旦形成一个环,这个环就不再有对外的出度,而只有从外面的点进来的入度。缩点之后用 r t rt rt代表整个强连通分量,同时将此强连通分量与外界的连边都迁移到 r t rt rt上。
- 因为每个点的出度只有一个,我们很容易联想到树形结构,因为在树中每个节点的父亲只有一个,因此 v v v指向 u u u可以将 u u u看作 v v v的父亲,从而建立一座森林。其中没有出度的节点,即没有父亲的节点,就可以作为一棵树的根,从每个根向下 d f s dfs dfs更新其孩子的可达节点数目。注意环必定是树的根,因为它也没有对外的出度。
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+7;
vector<int>son[N],set_rt[N];
bool vis[N],cir[N];
int low[N],fa[N],dfn[N],rt[N],dfncnt,top,stk[N],ans[N],n;
set<int>fas;
void tarjan(int u){
dfn[u]=low[u]=++dfncnt;
stk[++top]=u,vis[u]=true;
int v=fa[u];
if(v){
if(!dfn[v]){
tarjan(v);
low[u]=min(low[u],low[v]);
}
else if(vis[v])
low[u]=min(low[u],dfn[v]);
}
if(dfn[u]==low[u])
do{
int t=stk[top];
rt[t]=u;
vis[t]=false;
}while(stk[top--]!=u);
}
void dfs(int u,int cnt){
if(cir[u]&&rt[u]!=u)return;
//if(!cir[u]&&ans[u]>1)return;
ans[u]=cnt;
if(son[u].size()==0)return;
//cout<<u<<' '<<cnt<<endl;
for(int i=0;i<son[u].size();i++){
dfs(son[u][i],cnt+1);
}
}
int main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>fa[i];if(fa[i]==i)continue;
son[fa[i]].push_back(i);
}
for(int i=1;i<=n;i++){
if(!dfn[i])tarjan(i);
}
for(int i=1;i<=n;i++){
set_rt[rt[i]].push_back(i);
}
for(int i=1;i<=n;i++){
if(rt[i]==i)ans[i]=set_rt[i].size();
else {
ans[i]=set_rt[rt[i]].size();
for(int j=0;j<son[i].size();j++){
if(rt[son[i][j]]!=rt[i])son[rt[i]].push_back(son[i][j]);
}
son[i].clear();
}
}
for(int i=1;i<=n;i++){
if(ans[i]!=1)cir[i]=true;
}
for(int i=1;i<=n;i++){
if((cir[i]&&rt[i]==i)||(fa[i] == i))
dfs(i,ans[i]);
}
for(int i=1;i<=n;i++)
cout<<ans[i]<<endl;
return 0;
}
P2746 [USACO5.3]校园网Network of Schools
习题链接
计算内容是:
- 入度为0的点的数量。
- 将所有的点连接成一个强连通分量需要加多少边。
首先用tarjan将强连通分量进行缩点。缩点后成为几个不连通的DAG。所有点成为强连通分量,相当于从任意点出发都能到达每个点。对于入度为0的点,相当于没有点可以到达它,对于出度为0的点,相当于它无法到达别的点。因此只要将这两种点处理掉,从出度为0的点向入度为0的点连边,使得最终没有出度为0和入度为0的点即可,要添加的边就是两种点的点数更大值。
其实对于DAG图可以进行拓扑排序,将入度为0的点看成起点,将出度为0的点看成终点,那么从所有的起点出发,可以到达图中的每个点,自然也可以到达每个终点。
设一个终点为
e
d
ed
ed,其起点为
s
t
st
st,其他起点为
s
t
′
st'
st′,则添加一条
e
d
ed
ed到
s
t
′
st'
st′的连边,再添加
e
d
′
ed'
ed′到
s
t
st
st的连边…即将每个终点连向无法导向它的其他起点。
最后会剩下一些起点或者终点,而其他的点都已经成为强连通分量,如果剩余起点,就从强连通分量中任意一个点连向剩余起点;如果剩余终点,就从剩余终点连向强连通分量中任意一个点。
#include <iostream>
#include <algorithm>
#include <cstring>
#include <cmath>
#include <queue>
#include<map>
#include<set>
#define LL long long
using namespace std;
const int N = 110;
int n;
struct E{
int to, nxt;
}e[N * N], e_[N * N];
int head[N], cnt, head_[N], cnt_;
void add(int u, int v)
{
e[++ cnt] = {v, head[u]};
head[u] = cnt;
}
void add_(int u, int v)
{
e_[++ cnt_] = {v, head_[u]};
head_[u] = cnt_;
}
int low[N], dfn[N], dfn_cnt, c[N], scc_cnt, in[N], out[N], bl_cnt;
bool vis[N];
vector<int> scc[N];
int stk[N], top;
void tarjan(int u)
{
low[u] = dfn[u] = ++ dfn_cnt;
stk[++ top] = u, vis[u] = true;
for(int i = head[u]; i; i = e[i].nxt)
{
int v = e[i].to;
if(!dfn[v])
{
tarjan(v);
low[u] = min(low[v], low[u]);
}
else if(vis[v])
low[u] = min(low[u], dfn[v]);
}
if(low[u] == dfn[u])
{
++ scc_cnt;
int v;
do
{
v = stk[top --];
vis[v] = false;
scc[scc_cnt].push_back(v);
c[v] = scc_cnt;
}while(v != u);
}
}
void dfs(int u)
{
vis[u] = true;
for(int i = head_[u]; i; i = e_[i].nxt)
{
int v = e[i].to;
if(!vis[v]) dfs(v);
}
}
int main()
{
scanf("%d", &n);
for(int i = 1; i <= n; i ++)
{
int v;
while(scanf("%d", &v) && v) add(i, v);
}
for(int i = 1; i <= n; i ++)
if(!dfn[i]) tarjan(i);
//重新加边
for(int u = 1; u <= n; u ++)
{
for(int i = head[u]; i; i = e[i].nxt)
{
int v = e[i].to;
if(c[u] == c[v]) continue;
add_(c[u], c[v]);
in[c[v]] ++, out[c[u]] ++;
}
}
memset(vis, 0, sizeof vis);
int all_in = 0, all_out = 0;
for(int i = 1; i <= scc_cnt; i ++)
{
all_in += (in[i] == 0), all_out += (out[i] == 0);
}
int ans1 = all_in, ans2 = max(all_in, all_out);
if(scc_cnt == 1 && all_in == all_out && all_in == 1) ans2 = 0;
printf("%d\n%d", ans1, ans2);
return 0;
}