OI模板 Tarjan与连通性

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(vSubtreeumin{lowv},tSubtreeu,(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=un[iu]=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 1n104,1m5104

答案为该图中唯一一个出度为 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 1n104,1m105

把这张图的所有强连通分量进行缩点,形成一张 D A G DAG DAG,每个节点的点权为对应强连通分量所有点点权和。之后在这张 D A G DAG DAG 上跑最长路即可。

例题:Luogu P2812 校园网络【[USACO]Network of Schools加强版】

给一张有向图,求两个问题:

  1. 至少选几个点开始搜索能使整个图都能被遍历到;
  2. 至少加几条边才能是整个图变成强连通图。

1 ≤ n ≤ 1 0 4 , 1 ≤ m ≤ 5 ∗ 1 0 6 1\leq n \leq 10^4, 1\leq m \leq 5*10^6 1n104,1m5106

对于问题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 dfnulowv
  • 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} dfnulowv1,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):一个无向图的极大点双连通子图。

定理:

一张无向图是“点双连通图”,当且仅当满足一下两个条件其一:

  1. 图中顶点数 ≤ 2 \leq 2 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 dfnulowv 成立时,无论 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 vDCC 和每个割点都作为新图中的节点,并在每个割点与包含它的所有 $\text{v-DCC} 连边,形成森林。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值