图的各种边和割点计算
如果对图进行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)