Tarjan \text{Tarjan} Tarjan 与连通性
补充(很重要):
关于tarjan的一些大问题:
tarjan中low数组的一个问题:
if(!dfn[v]){
tarjan(v);
low[u] = min(low[u], low[v]);
} else low[u] = min(low[u], dfn[v]);
这里如果v是u的父亲,那么low[u]就能被dfn[v]更新。
如果求强连通分量和割点没什么问题,但在求割边的时候就有问题。
割边的判断法则是 low[v]>dfn[fa v],但如果v能被fa v更新时,则low[v]至少=dfn[fa v],就会出错。
所以在tarjan时要不要加上v==fa?continue这种话?
答:使用网络瘤存边用的成对变换,在边上打标记不去走。(thanks @z_z_y)
DFS \text{DFS} DFS 生成树
有向图的 DFS \text{DFS} DFS 生成树有 2 2 2 类边:
- 树边:每次搜索找到一个没有访问过的节点就形成一条树边
- 非树边:分为返祖边、横叉边、前向边,差别为边连接的两个节点是否有祖孙关系。
Tarjan \text{Tarjan} Tarjan
在 Tarjan \text{Tarjan} Tarjan 算法中维护了以下两个数组:
- d f n u dfn_u dfnu: DFS \text{DFS} DFS 时节点 u u u 被遍历的顺序。
- l o w u low_u lowu: u u u 能够回溯到的最早的已经在栈中的节点。计算方法: l o w u = min ( min v ∈ S u b t r e e u { l o w v } , min t ∈ S u b t r e e u , 有 边 ( t , v ) { l o w v } ) low_u=\min(\min\limits_{v\in Subtree_u}\{low_v\},\min\limits_{t\in Subtree_u,有边(t,v)}\{low_v\}) lowu=min(v∈Subtreeumin{lowv},t∈Subtreeu,有边(t,v)min{lowv})。
按照 DFS \text{DFS} DFS 序搜索,对于搜索到的一条边 ( u , v ) (u,v) (u,v) 有:
- v v v 未被访问,对 v v v 进行 DFS \text{DFS} DFS,回溯时用 l o w v low_v lowv 更新 l o w u low_u lowu。
- v v v 被访问过,在栈中:用 d f n v dfn_v dfnv 更新 l o w u low_u lowu。
- v v v 被访问过,不在栈中: v v v 已经搜索完毕,不用管。
框架
以下为 Tarjan \text{Tarjan} Tarjan 算法框架,针对不同的问题会添加不同语句。
const int N = 1e6 + 10;
int dfn[N], low[N], dfncnt, st[N], in_st[N], top;
vector<int> G[N];
void tarjan(int u){
dfn[u] = low[u] = ++ dfncnt;
st[++top] = u, in_st[u] = 1;
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(in_st[v]) {
low[u] = min(low[u], dfn[v]);
}
}
}
//code in main():
for(int i = 1; i <= n; ++ i) if(!dfn[i]) tarjan(i);
常见模型
强连通分量
有向图 G G G 强连通: G G G 中任意两个节点连通。
强连通分量( Strongly Connected Components,SCC ) \text{Strongly Connected Components,SCC}) Strongly Connected Components,SCC):极大的强连通子图。
对于一个强连通图,有且只有一个 u u u 使得 d f n u = l o w u dfn_u = low_u dfnu=lowu,即该强连通分量中第一个被遍历到的点。
因此,在回溯过程中,判断 d f n u = l o w u dfn_u = low_u dfnu=lowu 是否成立,若成立则栈中 u u u 及其上方节点为一个 S C C SCC SCC。时间复杂度 O ( n + m ) O(n+m) O(n+m)。
const int N = 1e6 + 10;
int dfn[N], low[N], dfncnt, st[N], in_st[N], top;
int scc[N], scc_sz[N], scc_cnt;
vector<int> G[N];
void tarjan(int u){
dfn[u] = low[u] = ++ dfncnt;
st[++top] = u, in_st[u] = 1;
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(in_st[v]) {
low[u] = min(low[u], dfn[v]);
}
}
if(dfn[u] == low[u]){
++ scc_cnt;
while(st[top] != u){
scc[st[top]] = scc_cnt, ++ scc_sz[st[top]];
in_st[st[top]] = 0, -- top;
}
scc[st[top]] = scc_cnt, ++ scc_sz[st[top]];
in_st[st[top]] = 0, -- top;
}
}
例题:Luogu P2341 [USACO03FALL][HAOI2006]受欢迎的牛 G
给一张有向图,求图中所有满足 ∏ i = 1 , i ≠ u n [ 存 在 一 条 从 i 到 u 的 路 径 ] = 1 \prod\limits_{i=1,i\not=u}^n [存在一条从 i 到 u 的路径]=1 i=1,i=u∏n[存在一条从i到u的路径]=1 的节点个数。
1 ≤ n ≤ 1 0 4 , 1 ≤ m ≤ 5 ∗ 1 0 4 1\leq n \leq 10^4, 1\leq m \leq 5*10^4 1≤n≤104,1≤m≤5∗104。
答案为该图中唯一一个出度为 0 0 0 的强连通分量的节点数。
强连通分量-缩点
将一个强连通分量缩成一个点进行计算。
注意:连接二点强连通分量时需注意此两点是否在同一强连通分量!
for(int i = 1; i <= n; ++ i)
for(int j = 0; j < G[i].size(); ++ j)
if(scc[G[i][j]] != scc[i])//
++ ind[scc[G[i][j]]], ++ oud[scc[i]];
例题:Luogu P3387 【模板】缩点
给一张点带权有向图,求一条路径,使得路径经过的点权值和最大(重复经过的点只算一次)。
1 ≤ n ≤ 1 0 4 , 1 ≤ m ≤ 1 0 5 1\leq n \leq 10^4, 1\leq m \leq 10^5 1≤n≤104,1≤m≤105。
把这张图的所有强连通分量进行缩点,形成一张 D A G DAG DAG,每个节点的点权为对应强连通分量所有点点权和。之后在这张 D A G DAG DAG 上跑最长路即可。
例题:Luogu P2812 校园网络【[USACO]Network of Schools加强版】
给一张有向图,求两个问题:
- 至少选几个点开始搜索能使整个图都能被遍历到;
- 至少加几条边才能是整个图变成强连通图。
1 ≤ n ≤ 1 0 4 , 1 ≤ m ≤ 5 ∗ 1 0 6 1\leq n \leq 10^4, 1\leq m \leq 5*10^6 1≤n≤104,1≤m≤5∗106。
对于问题1,求出缩点后入度为 0 0 0 的节点数;对于问题2,求出缩点后入度为 0 0 0 的节点数和出度为 0 0 0 的节点数的最小值。
注意:如果整个图已经是一个强连通图,则问题2特判为 0 0 0。
2-SAT \text{2-SAT} 2-SAT 问题
有 n n n 个变量 a 1 , 2 , . . . , n a_{1,2,...,n} a1,2,...,n,每个变量都是 bool \text{bool} bool 类型,现有 m m m 个要求,每个要求形如:若 a i a_i ai 为 p p p,则 a j a_j aj 一定为 q q q( p , q ∈ { 0 , 1 } p,q\in\{0,1\} p,q∈{0,1})。
把每个变量拆成两个点 a i , t r u e , a i , f a l s e a_{i,true},a_{i,false} ai,true,ai,false。对于每个要求,连接 a i , p − > a j , q , a j , p − > a i , q a_{i,p}->a_{j,q},a_{j,p}->a_{i,q} ai,p−>aj,q,aj,p−>ai,q,即若 a i = p a_i=p ai=p 则 a j = q a_j=q aj=q。此时对最终的图进行强连通分量缩点。每一个强连通分量中的点值都是一样的。
若 a i , t r u e , a i , f a l s e a_{i,true},a_{i,false} ai,true,ai,false 在同一强连通分量中,则无解。否则 a i a_i ai 值取深度大的那个(若用 Tarjan \text{Tarjan} Tarjan 求强连通分量,则是强连通分量编号小的那一个)。
for(int i = 1; i <= n*2; ++ i) if(!dfn[i]) tarjan(i);
for(int i = 1; i <= n; ++ i){
if(scc[i] == scc[i+n]){
puts("IMPOSSIBLE");
return 0;
}
}
puts("POSSIBLE");
for(int i = 1; i <= n; ++ i){
printf("%d ", scc[i] < scc[i+n]);
}
割点 / 割顶
对于一个无向图,如果把一个点删除后这个图的极大连通分量数增加了,那么这个点就是这个图的割点(又称割顶)。
判定方法:
- 若 u u u 不是搜索树的根节点,则 u u u 是割点当且仅当存在一个 u u u 的子节点 v v v 使得 d f n u ≤ l o w v dfn_u\leq low_v dfnu≤lowv。
- 若 u u u 是搜索树的根节点,则 u u u 是割点需存在两个 u u u 的子节点 v 1 , v 2 v_1,v_2 v1,v2 满足 d f n u ≤ l o w v 1 , l o w v 2 dfn_u\leq low_{v_1},low_{v_2} dfnu≤lowv1,lowv2。
这时候可能有个问题:若 u u u 是搜索树的根节点,那么 d f n u = 1 dfn_u=1 dfnu=1, u u u 所有的子节点都满足上述条件!所以只要有 2 2 2 个或以上儿子的根节点就一定是割点吗?肯定不是的,因为这里的儿子节点指搜索树上该节点的儿子,而非原图上的儿子。
const int N = 1e6 + 10;
int dfn[N], low[N], dfncnt, root;
vector<int> G[N];
bool iscut[N];
void tarjan(int u){
dfn[u] = low[u] = ++ dfncnt;
for(int flg = 0, i = 0; i < G[u].size(); ++ i){
int v = G[u][i];
if(!dfn[v]){
tarjan(v), low[u] = min(low[u], low[v]);
if(low[v] >= dfn[u]){
++ flg;
if(u != root || flg > 1) iscut[u] = true;
}
} else low[u] = min(low[u], dfn[v]);
}
}
//code in main():
for(int i = 1; i <= n; ++ i) if(!dfn[i]) root = i, tarjan(i);
割边 / 桥
对于一个无向图,如果把一条边删除后这个图的极大连通分量数增加了,那么这条边就是这个图的割边(又称桥)。
判定方法:
- 搜索树上存在一个 u u u 和 u u u 的子节点 v v v 满足 d f n u < l o w v dfn_u<low_v dfnu<lowv,则 ( u , v ) (u,v) (u,v) 为割边。
由于遍历的是无向图,所以从 u u u 出发时一定能遍历到 f a u fa_u fau。根据 l o w low low 的计算方法,不能用 d f n f a u dfn_{fa_u} dfnfau 更新 l o w u low_u lowu;但是若 ( u , f a u ) (u,fa_u) (u,fau) 有重边,则可以通过其他的边使用 d f n f a u dfn_{fa_u} dfnfau 更新 l o w u low_u lowu(其他的边不在搜索树上)。此时不能使用记录 f a fa fa 的方法。
一个好的解决方法:将无向图的每一条边看做双向边记录在链式前向星的 2 k , 2 k + 1 2k,2k+1 2k,2k+1 位置,并在递归时记录“递归进入每一个节点的边的编号”(链式前向星中存储的下标位置)。若沿着编号为 i i i 的边进入节点 u u u,则忽略从 u u u 出发的编号为 i xor 1 i\text{~xor~}1 i xor 1 的边。
边双连通分量
概念:
边双连通图:不存在割边的无向图。
边双连通分量( e-DCC \text{e-DCC} e-DCC):一个无向图的极大边双连通子图。
求法:
求出无向图中所有桥,把桥都删除后图分成若干连通块,每个连通块是一个 e-DCC \text{e-DCC} e-DCC。
边双连通分量缩点后形成森林。
点双连通分量
概念:
点双连通图:不存在割点的无向图
点双连通分量( v-DCC \text{v-DCC} v-DCC):一个无向图的极大点双连通子图。
定理:
一张无向图是“点双连通图”,当且仅当满足一下两个条件其一:
- 图中顶点数 ≤ 2 \leq 2 ≤2;
- 图中任意两点都同时包含在至少一个简单环中。
割边不属于任何 e-DCC \text{e-DCC} e-DCC,割点可能属于多个 v-DCC \text{v-DCC} v-DCC。
求法:
在 Tarjan \text{Tarjan} Tarjan 进行过程中维护一个栈:
- 当一个节点第一次被访问时入栈。
- 当割点判定法则 d f n u ≤ l o w v dfn_u \leq low_v dfnu≤lowv 成立时,无论 u u u 是否为根,执行:从栈顶不断弹出节点,直至 v v v 被弹出。此时所有弹出的节点与 u u u 一起构成一个 v-DCC \text{v-DCC} v-DCC。
const int N = 1e6 + 10;
int dfn[N], low[N], dfncnt, st[N], top, root;
vector<int> v_DCC[N]; int cnt;
vector<int> G[N];
void tarjan(int u){
dfn[u] = low[u] = ++ dfncnt;
st[++top] = u;
if(u == root && G[u].size() == 0){
v_DCC[++cnt].push_back(u);
return;
}
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]);
if(low[v] >= dfn[u]){
++ cnt; int t;
do { t = st[top--]; v_DCC[cnt].push_back(t);
} while(t != v);
v_DCC[cnt].push_back(u);
}
} else low[u] = min(low[u], dfn[v]);
}
}
//code in main():
for(int i = 1; i <= n; ++ i) if(!dfn[i]) root = i, tarjan(i);
点双连通分量缩点:
设图中共有 p p p 个割点和 t t t 个 v-DCC \text{v-DCC} v-DCC,建立一张包含 p + t p+t p+t 个节点的新图,把每个 v − D C C v-DCC v−DCC 和每个割点都作为新图中的节点,并在每个割点与包含它的所有 $\text{v-DCC} 连边,形成森林。