图的各种边和割点计算
如果对图进行DFS,按照访问节点的各个顺序,那么可以将边进行分类。其中无向图包括 1. 树边(tree-edge) 和 2. 反向边(backward-edge). 有向图相比无向图多了两类: 3. 正向边(forward-edge) 和 4. 交叉边(cross-edge). 关于这些边如何定义,可以参考任何一本算法和数据结构的书。
我这里参考的书籍是 《[数据结构与算法分析_Java语言描述(第2版)].韦斯》,甚至代码里面的例子都是书里面的。
class State:
def __init__(self, n):
self.ts = 0
self.in_ts = [-1] * n
self.out_ts = [-1] * n
self.ps_ts = [-1] * n
self.parents = [-1] * n
def enter(self, v):
self.in_ts[v] = self.ts
self.ts += 1
def exit(self, v):
self.out_ts[v] = self.ts
self.ts += 1
def process(self, v):
self.ps_ts[v] = self.ts
self.ts += 1
def set_parent(self, v, p):
self.parents[v] = p
def get_parent(self, v):
return self.parents[v]
def undi_dfs(G: Graph):
n = len(G)
tree_edges = []
back_edges = []
state = State(n)
def fn(v):
state.enter(v)
state.process(v)
mat = G.mat
for v2 in range(n):
if mat[v][v2] != 0:
# 如果没有访问的话,那么记录下parent关系,这个在判断是否为回边的时候需要去除
if state.in_ts[v2] == -1:
state.set_parent(v2, v)
tree_edges.append((v, v2))
fn(v2)
# 如果 v->v2, 但是v2访问的时间更早并且不是父子关系的话,那么认为是回边
elif state.get_parent(v) != v2 and state.in_ts[v] > state.in_ts[v2]:
assert (state.out_ts[v2] == -1)
back_edges.append((v, v2))
state.exit(v)
fn(0)
return state, tree_edges, back_edges
def di_dfs(G: Graph, ss):
n = len(G)
tree_edges = []
back_edges = []
fwd_edges = []
cross_edges = []
state = State(n)
def fn(v):
state.enter(v)
state.process(v)
mat = G.mat
for v2 in range(n):
if mat[v][v2] != 0:
# 如果没有访问的话,那么记录下parent关系,这个在判断是否为回边的时候需要去除
if state.in_ts[v2] == -1:
state.set_parent(v2, v)
tree_edges.append((v, v2))
fn(v2)
# 如果 v->v2, 但是v2访问的时间更早并且不是父子关系的话,那么认为是回边
# 并且要求这个时候v2还没有访问完成
elif state.get_parent(v) != v2 and \
state.in_ts[v] > state.in_ts[v2] and \
state.out_ts[v2] == -1:
back_edges.append((v, v2))
else:
assert (state.out_ts[v2] != -1)
# 如果v2访问完成时间是大于v访问初始时间,那么就是正向边
if state.out_ts[v2] > state.in_ts[v]:
fwd_edges.append((v, v2))
else:
cross_edges.append((v, v2))
state.exit(v)
for s in ss:
fn(s)
return state, tree_edges, back_edges, fwd_edges, cross_edges
如果一个图可以一次DFS将所有节点遍历到,那么这个图就是联通的(connected). 对于一个连通图来说,这个图中存在某个节点,如果将这个节点删除(以及它上面的边) 的话,那么这个图就变成非连通的,那么这个节点就是割点。计算割点的时候使用到了反向边(backward-edge)这个概念。割点满足的条件也好理解:如果一个节点,它下面 的孩子如果没有办法达到这个节点的父节点的话,那么这个节点就是一个割点。那么怎么定义子节点到父节点呢?这就是一个反向边。此外根节点是一个特例。
class State:
def __init__(self, n):
self.num = [-1] * n
self.parent = [-1] * n
self.low = [-1] * n
self.counter = 0
def assign_number(self, v):
self.low[v] = self.counter
self.num[v] = self.counter
self.counter += 1
def find_articulation_nodes(G: Graph):
# low[v] = min(num[v], min(low[x] for x in children), min(num[x] for (v, x) in back_edges)
# low的含义是这个节点向上回溯,能关联到序号最低的节点是什么
# 如果某个节点v,它其中一个孩子x, low[x] <= num[v]的话,说明这个x没有办法
# 连接到序号更低的节点,那么v节点就是一个割点
n = len(G)
res = set()
state = State(n)
def fn(v):
state.assign_number(v)
for t in range(n):
if G.mat[v][t] != 0:
# tree edges.
if state.num[t] == -1:
state.parent[t] = v
fn(t)
if state.low[t] >= state.num[v]:
# v is articulation node.
res.add(v)
state.low[v] = min(state.low[v], state.low[t])
# back edges.
elif state.parent[v] != t and state.num[v] > state.num[t]:
state.low[v] = min(state.low[v], state.num[t])
start = 0
fn(start)
count = 0
for t in range(n):
if G.mat[start][t] != 0:
count += 1
if count >= 2:
res.add(start)
break
return list(res)