图的连通性代码模板(python)

图的连通性代码模板

  • 关于 e − D C C e-DCC eDCC v − D C C v-DCC vDCC 缩点的应用可能结合倍增 L C A LCA LCA 算法,多次回答无向图上两点之间的必经边、必经点的询问。

T a r j a n Tarjan Tarjan算法与无向图连通性

时间戳

在图的深度优先遍历中,按照每个结点第一次被访问的顺序,依次给予 N N N 个结点 1 1 1~ N N N 的整数标记,该标记就被称为“时间戳”,记为 d f n [ x ] dfn[x] dfn[x]

搜索树

在无向连通图中任选一个结点进行 d f s dfs dfs ,每个结点只访问一次。所有发生递归的边 ( x , y ) (x,y) (x,y) 构成一棵树,我们把它称为“无向连通图的搜索树”。一般的无向连通图的各个连通块的搜索树构成无向图的搜索森林。

追溯值

返祖边:搜索树上的一个点连向其祖先结点的边

横插边:搜索树上一个点连向它另一条支链上的点的边(只存在于有向图)

l o w [ x ] low[x] low[x] 定义为当前点及其子树的仅通过一条返祖边能连到 d f n dfn dfn 值最小的点

( u , v ) (u,v) (u,v) 是树边: l o w [ u ] = m i n ( l o w [ u ] , l o w [ v ] ) low[u] = min(low[u],low[v]) low[u]=min(low[u],low[v])

( u , v ) (u,v) (u,v) 是返祖边: l o w [ u ] = m i n ( l o w [ u ] , d f n [ v ] ) low[u] = min(low[u],dfn[v]) low[u]=min(low[u],dfn[v])

什么样的点是割点?

  1. 有多个儿子的根结点。
  2. 子树中不存在跨越自己连向上方的返祖边

什么样的边是桥(割边)?

  • ( u , v ) (u,v) (u,v) 是树边,且下侧的点及其子树中不存在连向上侧的返祖边,即搜索树上一结点 x x x 及其子结点 y y y 满足: d f n [ x ] ≤ l o w [ y ] dfn[x] ≤ low[y] dfn[x]low[y]

T a r j a n Tarjan Tarjan 求割点代码

def tarjan(x):
    global cnt # 当前时间戳
    cnt += 1
    dfn[x] = low[x] = cnt
    flag = 0  # 子树中不满足条件的点的数量
    for y,w in g[x]:
        if not dfn[y]:
            tarjan(y)
            low[x] = min(low[x], low[y]) # 树边更新low值
            if low[y] >= dfn[x]: # 返祖边在x下侧,只要有x就是割点
                flag += 1
                if (x != root or flag > 1): boo[x] = 1 # 如果是root,还要保证儿子数大于1
        else: low[x] = min(low[x], dfn[y]) # 非树边更新 low值

n,m = map(int, input().split())
g = [[] for _ in range(n+1)]
for _ in range(m):
    x,y = map(int, input().split())
    if x == y: continue
    g[x].append(y)
    g[y].append(x)

cnt = 0  # 用于记录当前时间戳
dfn, boo = [0]*(n+1), [0]*(n+1) # dfn:时间戳, boo:是否为割点
low = [i for i in range(n+1)]
for i in range(1,n+1):
    if not dfn[i]: 
        root = i
    	tarjan(i)

T a r j a n Tarjan Tarjan 求桥(割边)代码

这是一个求无向图桥的代码,因为是无向图所以从每个点 x x x 出发都能到其父亲结点 f a fa fa。根据 l o w low low 的计算方法, ( x , f a ) (x,fa) (x,fa) 是树上的边,且 f a fa fa 不是 x x x 的子结点,故不能用 l o w [ f a ] 来更新 l o w [ x ] low[fa]来更新low[x] low[fa]来更新low[x],但是如果只记录每个结点的父亲结点,则不能处理重边的情况,有重边时,重边也属于 x x x 的返祖边,故可以用 d f n [ f a ] dfn[fa] dfn[fa] 来更新 l o w [ x ] low[x] low[x]。一个比较好的处理方法是利用“成对变换”的技巧,当沿着编号为 i i i 的边递归进入了结点 x x x 则忽略从 x x x 出发编号为 i i i^ 1 1 1 边,通过其他边计算 l o w [ x ] low[x] low[x] 即可

def add(x,y,z):
    global tot
    tot += 1
    ver[tot],edge[tot] = y,z
    nxt[tot],head[x] = head[x],tot
    
def tarjan(x, in_edge):
    global cnt  # 在solve里面用 nonlocal cnt
    cnt += 1
    dfn[x] = low[x] = cnt
    i = head[x]
    while i:
        y = ver[i]
        if not dfn[y]:
            tarjan(y,i)
            low[x] = min(low[x], low[y])
            if low[y] > dfn[x]: # (x,y) 为桥
                bridge[i] = bridge[i^1] = 1
        elif i != (in_edge^1): # 忽略反向边
            low[x] = min(low[x],dfn[y])
        i = nxt[i]
    
n,m = map(int, input().split())
head = [0]*(n+1)
ver,edge,nxt = [0]*(2*m+2),[0]*(2*m+2),[0]*(2*m+2)
tot = 1

for i in range(m):
    x,y,z = map(int, input().split())
    add(x,y,z)
    add(y,x,z)

bridge = [0]*(2*m+2)
dfn,low = [0]*(n+1), [i for i in range(n+1)]
cnt = 0
for i in range(1,n+1):
    if not dfn[i]:tarjan(i,0)
for i in range(2,tot+1,2):
    if bridge[i]: print(ver[i^1],ver[i])

边双连通分量( e − D C C e-DCC eDCC)

​ 把割边删了之后,每一个连通块都是一个边双连通分量,可以用 c c c 数组记录结点属于的便双连通分量的编号。

e − D C C 缩点 e-DCC缩点 eDCC缩点

​ 把每个 e − D C C e-DCC eDCC 看成一个结点,把桥 ( x , y ) (x,y) (x,y) 看成连接 c [ x ] c[x] c[x] c [ y ] c[y] c[y] 的无向边,会产生一棵树(若原来的无向图不连通,则产生森林),python代码如下。

import sys
input = sys.stdin.readline

def add(x,y,z):
    global tot
    tot += 1
    ver[tot],edge[tot] = y,z
    nxt[tot],head[x] = head[x], tot

def tarjan(x, in_edge):
    global cnt  # 在solve里面用 nonlocal cnt
    cnt += 1
    dfn[x] = low[x] = cnt
    i = head[x]
    while i:
        y = ver[i]
        if not dfn[y]: # 是树边
            tarjan(y,i)
            low[x] = min(low[x], low[y])
            if low[y] > dfn[x]: # (x,y) 为桥
                bridge[i] = bridge[i^1] = 1
        elif i != (in_edge^1): # 忽略反向边
            low[x] = min(low[x], dfn[y])
        i = nxt[i]

def dfs(x):  
    """类似找连通块的方法对 e-DCC 染色"""
    c[x] = dcc
    i = head[x]
    while i:
        y = ver[i]
        if c[y] or bridge[i]: continue
        dfs(y)
        i = nxt[i]

n,m = map(int, input().split())
head = [0]*(n+1)
ver,nxt,edge = [0]*(2*m+2),[0]*(2*m+2),[0]*(2*m+2)
tot = 1
for i in range(m):
    x,y,z = map(int, input().split())
    add(x,y,z)
    add(y,x,z)

bridge = [0]*(m*2+1)
dfn,low = [0]*(n+1),[i for i in range(n+1)]
cnt = 0
for i in range(1,n+1):
    if not dfn[i]: tarjan(i, 0)

c,dcc = [0]*(n+1), 0
for i in range(1,n+1):
    if not c[i]:
        dcc += 1
        dfs(i)

点双连通分量( v − D C C v-DCC vDCC)

v − D C C v-DCC vDCC

​ 这里需要注意的是割点可能属于多个点双连通分量,为了求出点双连通分量,需要在 T a r j a n Tarjan Tarjan 算法的过程中维护一个栈,并按照如下方法维护栈中元素。

  1. 当一个结点第一次被访问时,把该结点入栈。
  2. 当割点判定法则中的条件 d f n [ x ] ≤ l o w [ y ] dfn[x]≤low[y] dfn[x]low[y] 成立时,无论 x x x 是否为根,都要:
    • 1)从栈顶不断弹出结点,直至结点 y y y 被弹出。
    • 2)刚才弹出的所有结点与结点 x x x 共同构成一个 v − D C C v-DCC vDCC

v − D C C v-DCC vDCC缩点

v − D C C v-DCC vDCC 的缩点比 e − D C C e-DCC eDCC 的缩点复杂——因为一个割点可能属于多个 v − D C C v-DCC vDCC。设图有 p p p 个割点和 t t t v − D C C v-DCC vDCC。我们建立一张包含 p + t p+t p+t 个结点的新图,把每个 v − D C C v-DCC vDCC每个割点都作为新图中的结点,并在每个割点和包含它的所有 v − D C C v-DCC vDCC 之间连边。这张新图其实是一张树或森林。

python代码如下:

import sys

def tarjan(x):
    global cnt,tot # 当前时间戳
    cnt += 1
    dfn[x] = low[x] = cnt
    st.append(x)
    if x == root and len(g[x]) == 0:
        tot += 1
        dcc[tot].append(x)
        return
    flag = 0  # 子树中不满足条件的点的数量
    for y in g[x]:
        if not dfn[y]:
            tarjan(y)
            low[x] = min(low[x], low[y]) # 树边更新low值
            if low[y] >= dfn[x]: # 返祖边在x下侧,只要有x就是割点
                flag,tot= flag + 1, tot + 1
                if (x != root or flag > 1): boo[x] = 1 # 如果是root,还要保证儿子数大于1
                while st[-1] != y:
                    dcc[tot].append(st.pop())
                dcc[tot].append(st.pop())
                dcc[tot].append(x)
        else: low[x] = min(low[x], dfn[y]) # 非树边更新 low值

n,m = map(int, input().split())
g = [[] for _ in range(n+1)]
for _ in range(m):
    x,y = map(int, input().split())
    if x == y: continue
    g[x].append(y)
    g[y].append(x)

"""求v-DCC"""
cnt,tot = 0,0  # 用于记录当前时间戳, dcc编号
dfn, boo = [0]*(n+1), [0]*(n+1) # dfn:时间戳, boo:是否为割点
low = [i for i in range(n+1)]
st,dcc = [],[[] for i in range(n+1)]  # stack, v-DCC
for i in range(1,n+1):
    if not dfn[i]:
        root = i
        tarjan(i)

"""v-DCC 缩点"""
cnt = tot
new_id = [0]*(n+1)
for i in range(1,n+1):
    if boo[i]:  # 给每个割点一个新的编号(从 cnt+1 开始)
        cnt += 1
        new_id[i] = cnt
gg = [[] for _ in range(cnt + 1)]
c = [0]*(n+1) # v-DCC 染色
for i in range(1,tot+1):
    for j in range(len(dcc[i])):
        x = dcc[i][j]
        if boo[x]:
            gg[i].append(new_id[x])
            gg[new_id[x]].append(i)
        else: c[x] = i  # 除割点外,其他点仅属于1个 v-DCC

有向图的连通性

一些概念

​ 给定有向图 G ( V , E ) G(V,E) G(V,E),若存在 r ∈ V r∈V rV,满足从 r r r 出发能够到达 V V V 中所有的点则称 G G G 是一个“流图”,记为 ( G , r ) (G,r) (G,r),其中 r r r 为流图的源点。流图中每条有向边 ( x , y ) (x,y) (x,y) 必然是以下四种之一:1、树边 2、前向边 3、返祖边(后向边) 4、横插边。

​ 有向图的极大强连通子图称为“强连通分量” ( S C C ) (SCC) (SCC)

1、 K o s a r a j u Kosaraju Kosaraju 算法

​ 对原图进行一次 d f s dfs dfs,分成若干棵树,再以第一次搜索的出栈顺序对原图的返图进行 d f s dfs dfs A A A A A A 能到的是在同一个连通分量中。

2、 T a r j a n Tarjan Tarjan 算法与有向图的强连通分量

强连通分量 S C C SCC SCC 以及 S C C SCC SCC 缩点

​ 在这里 T a r j a n Tarjan Tarjan 算法的的基本思路是对于每一个点,尽量找到能与他构成环的所有结点。为了找到通过“后向边”和“横插边”构成的环, T a r j a n Tarjan Tarjan 算法在 d f s dfs dfs 的同时维护一个栈,当访问到结点 x x x 时,栈中需要保存以下两类结点:

  1. 搜索树上 x x x 的祖先结点,记为集合 a n c ( x ) anc(x) anc(x)。设 y ∈ a n c ( x ) y∈anc(x) yanc(x)。若存在后向边 ( x , y ) (x,y) (x,y),则 ( x , y ) (x,y) (x,y) y y y x x x 的路径一定形成环。
  2. 已经访问过并且存在一条路径到达 a n c ( x ) anc(x) anc(x) 的结点。设 z z z 是这样一个点,从 z z z 出发存在一条路径到达 y ∈ a n c ( x ) y∈anc(x) yanc(x)。若存在横插边 ( x , z ) (x,z) (x,z),则 ( x , z ) 、 z 到 y 的路径、 y 到 x 的路径 (x,z)、z到y的路径、y到x的路径 (x,z)zy的路径、yx的路径 形成一个环。

综上所述,栈中结点就是能与从 x x x 出发的“后向边”和“横插边”形成环的结点。

​ 设 s u b t r e e ( x ) subtree(x) subtree(x) 表示流图的搜索树中以 x x x 为根的子树。 l o w [ x ] low[x] low[x] 定义为从 s u b t r e e ( x ) subtree(x) subtree(x) 出发的有向边能连接到的还在栈中 d f n dfn dfn 值最小的点。根据定义, T a r j a n Tarjan Tarjan 算法计算“追溯值” l o w [ x ] low[x] low[x] 的步骤如下:

  1. 当结点 x x x 第一次被访问时,把 x x x 入栈,初始化 l o w [ x ] = d f n [ x ] low[x] = dfn[x] low[x]=dfn[x]
  2. 扫描 x x x 的每条出边 ( x , y ) (x,y) (x,y):
    • y y y 没被访问过,则说明 ( x , y ) (x,y) (x,y) 是树边,递归访问 y y y,从 y y y 回溯后,令 l o w [ x ] = m i n ( l o w [ x ] , l o w [ y ] ) low[x] = min(low[x],low[y]) low[x]=min(low[x],low[y])
    • y y y 被访问过,并且 y y y 在栈中,则令 l o w [ x ] = m i n ( l o w [ x ] , d f n [ y ] ) low[x] = min(low[x],dfn[y]) low[x]=min(low[x],dfn[y])
  3. x x x 回溯之前,判断是否有 l o w [ x ] = = d f n [ x ] low[x] == dfn[x] low[x]==dfn[x]。若成立,则不断从栈中弹出结点,直至 x x x 出栈。 ( l o w [ x ] = = d f n [ x ] 意味着 x 就是这个强连通分量中在搜索树上最高的点 ) (low[x] == dfn[x] 意味着x就是这个强连通分量中在搜索树上最高的点) (low[x]==dfn[x]意味着x就是这个强连通分量中在搜索树上最高的点)

python code:

import sys
input = sys.stdin.readline

def tarjan(x):
    global cnt,tot # 当前时间戳
    cnt += 1
    dfn[x] = low[x] = cnt
    st.append(x)
    ins[x] = 1
    for y in g[x]:
        if not dfn[y]:
            tarjan(y)
            low[x] = min(low[x], low[y])
        elif ins[y]:
            low[x] = min(low[x],dfn[y])
    if dfn[x] == low[x]:
        tot += 1
        while st[-1] != x:
            y = st.pop()
            ins[y] = 0
            c[y] = tot
            scc[tot].append(y)
        y = st.pop()
        ins[y] = 0
        c[y] = tot, scc[tot].append(y)
        
cnt,tot = 0,0  # 用于记录当前时间戳, scc编号
n,m = map(int, input().split())
g = [[] for _ in range(n+1)]
for _ in range(m):
    x,y = map(int, input().split())
    if x == y: continue
    g[x].append(y)
    g[y].append(x)

"""求SCC"""
dfn,c = [0]*(n+1),[0]*(n+1) # dfn:时间戳, c:SCC编号
low = [i for i in range(n+1)]
scc = [[] for _ in range(n+1)]
st,ins = [],[0]*(n+1) # stack, if in stack or not
for i in range(1,n+1):
    if not dfn[i]: tarjan(i)

"""SCC缩点"""
gc = [[] for _ in range(tot+1)]
for x in range(1,n+1):
    for y in g[x]:
        if c[x] == c[y]: continue
        gc[c[x]].append(c[y])
  • 20
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

FarawayTraveler_Yuch

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值