Algorithm

思路:当访问一个节点的邻居的时候,立即访问邻居的邻居。当一个节点的邻居都被访问完后,回溯到有邻居没有访问的节点。

image-20250315122821525

Example

image-20250315122729226 dfs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class Node:
def __init__(self, value):
self.value = value
self.color = 0 # 0-white 1-gray 2-black
self.d = float('inf')
self.f = float('inf')
self.p = None


nodes = {n: Node(n) for n in ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I']}
G = {nodes['A']: [nodes['B'], nodes['C'], nodes['D']],
nodes['B']: [nodes['A'], nodes['E'], nodes['F']],
nodes['C']: [nodes['A'], nodes['D'], nodes['G']],
nodes['D']: [nodes['A'], nodes['C'], nodes['G'], nodes['H']],
nodes['E']: [nodes['B'], nodes['F'], nodes['I']],
nodes['F']: [nodes['B'], nodes['E'], nodes['G']],
nodes['G']: [nodes['C'], nodes['D'], nodes['F']],
nodes['H']: [nodes['D']],
nodes['I']: [nodes['E']]}


def DFS_visit(graph, start, time=-1):
# initialize: done by init Node
time += 1
start.d = time
start.color = 1
for v in graph[start]:
if v.color == 0:
v.color = 1
v.p = start
time = DFS_visit(graph, v, time)
time += 1
start.f = time
start.color = 2
return time

DFS_visit(G,nodes['I'])

for n in G.keys():
print(f"{n.value},{n.d},{n.f}")

Application

1.回环检测(返回是否有回环)

思路:

  1. 一个树(连通有向无环图)一定包含V1V-1条边
  2. 如果一个图的边少于V1V-1,则不连通
  3. 如果一个图的边多于V1V-1,则有环

Algorithm:

  1. run BFS/DFS 找到所有的连通图
  2. 对每个连通图统计边的数量EE
  3. 如果EVE\geq V,则表明有环

2.回环检测(返回回环)

tree edge, back edge, and cross edge

  • tree edge: BFS或DFS生成的树
  • back edge: 连接一个点和其ancestor(不包括parent)的边
  • cross edge: 连接的两个节点之间没有ancestor/descendent关系(i.e.下图中,6的ancestor为1,5,descendent为7,8,因此3和6之间没有ancestor/descendent关系)

algorithms - Difference between cross edges and forward edges in a DFT -  Computer Science Stack Exchange

proposition:对于无向图,DFS中没有cross edge。

proof:考虑边edge(u,v){\rm edge}(u,v),不妨设uuvv更早发现。如果有一条连接uuvv,那么uuvv的ancestor,则这条边为tree edge或back edge。

proposition,对于无向图,BFS中没有back edge。

proof:考虑边edge(u,v){\rm edge}(u,v),不妨设uuvv更早发现。如果有一条连接uuvv,则uuvv的parent,这条边为tree edge。

DFS for cycle detection

image-20250315205418008

在进行DFS时,如果有一条边连接当前节点uu和一些已经发现的节点(且不是父节点),denote as vv,那么这条边就是back edge,此时找到cycle。由于我们知道这个cycle一定是从当前节点的ancestor开始的,因此只需要不断找parent直到找到vv

注:bfs也可以

3. Bridge Detection

bridge:如果删除一条边,图变得不连通了,则这条边就是bridge

算法1. 移除一条边,然后使用DFS/BFS检测图是否连通,重复EE次,复杂度O(E2)\mathcal{O}(E^2)

observation

  1. back edge不是bridge:back bridge在环里
  2. 对于一个从uuvv的tree edge,这条边是bridge的充分必要条件是:没有back edge连接vv的descendant(包括vv)和uu的ancestor(包括uu)。例如下图,即使去掉了edge(u,v){\rm edge}(u,v),还有一条edge(u,t){\rm edge}(u,t)使图连通
1
2
3
4
5
6
7
8
flowchart TD
a((a))---b((b))
b --- u((u))
u --- v((v))
v --- s((s))
v --- t((t))
t --- u
linkStyle 5 stroke:#d11a2d,stroke-width:2px;

Tarjan Algorithm

思路:找到DFS tree,对于每个edge(u,v){\rm edge}(u,v),检测是否存在“跨越”edge(u,v){\rm edge}(u,v)的back edge

具体而言:记录u.stu.{\rm st}u.lowu.{\rm low}
其中u.stu.{\rm st}是该节点最初被访问的时刻
u.lowu.{\rm low}是以下三个值中最小的一个:

  1. u.stu.{\rm st}
  2. t.stt.{\rm st},其中edge(u,t){\rm edge}(u,t)是一个back edge
  3. v.lowv.{\rm low},其中vvuu的child

一个tree edge是bridge当且仅当 v.low>u.stv.{\rm low}>u.{\rm st}

image-20250315235905207

关于为什么判断桥的语句在if中是因为if中处理的是tree edge,而else if中处理的是back edge。桥必然在tree edge中。

example

Critical Connections in a Network - LeetCode

image-20250316001826036

很菜,但总之过了就行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class Node:
def __init__(self, value: int):
self.value = value
self.color = 0
self.st = float("inf")
self.low = float("inf")
self.p = None


class Solution:
def criticalConnections(self, n: int, connections: List[List[int]]) -> List[List[int]]:
graph = self.convert_to_graph(connections)
result = []
for u in graph.keys():
if u.color == 0:
self.DFS_visit(graph, u, 0, result)
return result

def DFS_visit(self, graph, u, t, result):
u.color = 1
t += 1
u.st = t
u.low = t
for v in graph[u]:
if v.color == 0:
v.p = u
t = self.DFS_visit(graph, v, t, result)
u.low = min(u.low, v.low)
if v.low > u.st:
result.append([u.value, v.value])
elif v != u.p:
u.low = min(u.low, v.st)
u.color = 2
return t

def convert_to_graph(self, connections):
node_dict = {}
graph = {}
for u, v in connections:
if u not in node_dict:
node_dict[u] = Node(u)
graph[node_dict[u]] = []
if v not in node_dict:
node_dict[v] = Node(v)
graph[node_dict[v]] = []
graph[node_dict[u]].append(node_dict[v])
graph[node_dict[v]].append(node_dict[u])
return graph

4.Topological Sort

拓扑排序是一种对有DAG的顶点进行线性排序的算法。这种排序满足以下条件:对于图中的每一条有向边 (u, v),顶点 u 在排序中总是位于顶点 v 的前面。换句话说,拓扑排序反映了图中顶点之间的依赖关系。

imageonline-co-gifimage

Observations:DAG一定包含至少一个入度为0的点。

algorithm

  1. 输出一个入度为0的节点uu
  2. 从图中移除uu和所有从uu出发的边edge(u,v){\rm edge}(u,v)
  3. 如果图非空,回到步骤1
image-20250317112525683

每个节点只经过一次,对每个节点统计从该节点出发的边(vVoutdegree=E\sum_{v\in V}{\rm out-degree}=E),因此时间复杂度:O(V+E)\mathcal{O}(V+E)

使用DFS实现 Topological Sort

image-20250317112932234

  1. 对于任意一个节点运行DFS
  2. 按照finish time 倒序输出