子节点不剪枝模板代码
图的深度搜索(DFS)与BFS一样,也是三色标记,基本代码相似,只不过是把FIFO先进先出队列换成FILO先进后出栈。这与树的BFS/DFS是一样的,树也是BFS使用队列,DFS使用栈。阅读本文需要有树的BFS与DFS作为基础,这里我提供我写的树的BFS/DFS博文的链接。
但是既然用了三色,就有不一样的地方。三色的巧妙使用可以实现对递归的替换。我举个图的递归最通用的代码:
def dfs_vertex(self, node, visited):
if visited[node]:
self.repeat(node)
return
visited[node] = True
self.code1(node)
for neighbour in self.__edges[node]:
self.code2(node, neighbour)
self.dfs_vertex(neighbour, visited)
self.code3(node, neighbour)
self.code4(node)
def repeat(self, node):
print("重复: ", self.__vertices[node])
def code1(self, node):
print("code1: ", self.__vertices[node])
def code2(self, node, neighbour):
print("code2: ", self.__vertices[node], self.__vertices[neighbour])
def code3(self, node, neighbour):
print("code3: ", self.__vertices[node], self.__vertices[neighbour])
def code4(self, node):
print("code4: ", self.__vertices[node])
上面的递归有四处代码,如何替换为非递归代码,还能保持执行结果正确?以下图为例子:
执行结果如下:
code1: A
code2: A B
code1: B
code2: B C
code1: C
code2: C D
code1: D
code4: D
code3: C D
code4: C
code3: B C
code2: B D
重复: D
code3: B D
code4: B
code3: A B
code4: A
重复: B
重复: C
重复: D
可以看出由于A是根,所以A在头尾出现。如果我们要用栈替换这个递归。需要注意以下几点:
1. code3和code4是两个参数的,所以栈的节点是一个数据对
2. B和C都指向D,说明栈要支持重复放入,这时候三色的含义变了。白色还是未访问,但是灰色不是代表已经入栈,而是代表所有子节点已经入栈,黑色代表已经出栈。
3. 每个节点不是访问完马上出栈,而是所有子节点都访问完才出栈
4. 哪怕重复访问,code2和code3也要执行。
所以python代码如下:
def dfs_tricolor(self):
# 着色
colors = [UnweightedGraph.WHITE for _ in self.__vertices]
# d代表discover f代表finish,发现时间戳和完成时间戳
n = len(self.__vertices)
for root in range(n):
stack = [[None, root]]
while len(stack) > 0:
v = stack[-1]
parent = v[0]
this = v[1]
if colors[this] == UnweightedGraph.GRAY:
stack.pop()
self.code4(this)
if parent is not None:
self.code3(parent, this)
colors[this] = UnweightedGraph.BLACK
elif colors[this] == UnweightedGraph.BLACK:
if parent is not None:
self.code2(parent, this)
self.repeat(this)
if parent is not None:
self.code3(parent, this)
stack.pop()
else:
if parent is not None:
self.code2(parent, this)
self.code1(this)
for neighbor in reversed(self.__edges[this]):
stack.append([this, neighbor])
colors[this] = UnweightedGraph.GRAY
无递归代码测试结果:
code1: A
code2: A B
code1: B
code2: B C
code1: C
code2: C D
code1: D
code4: D
code3: C D
code4: C
code3: B C
code2: B D
重复: D
code3: B D
code4: B
code3: A B
code4: A
重复: B
重复: C
重复: D
与递归代码结果一模一样,完美复刻。
子节点剪枝模板代码
剪枝,就是访问邻居(或者子节点)时,判断一下。如下列python代码:
def dfs_cut(self):
visited = [False for _ in self.__vertices]
for root, _ in enumerate(self.__vertices):
self.dfs_cut_vertex(root, visited)
def dfs_cut_vertex(self, node, visited):
if visited[node]:
self.repeat(node)
return
visited[node] = True
self.code1(node)
for neighbour in self.__edges[node]:
if not visited[neighbour]:
self.code2(node, neighbour)
self.dfs_cut_vertex(neighbour, visited)
self.code3(node, neighbour)
self.code4(node)
以上面的例子运行结果就是:
code1: A
code2: A B
code1: B
code2: B C
code1: C
code2: C D
code1: D
code4: D
code3: C D
code4: C
code3: B C
code4: B
code3: A B
code4: A
重复: B
重复: C
重复: D
减少了三行输出,也就是 B → D B\to D B→D的路径被剪了。这种场景也可以使用三色标记。
code1: A
code2: A B
code1: B
code2: B C
code1: C
code2: C D
code1: D
code4: D
code3: C D
code4: C
code3: B C
code2: B D
重复: D
code3: B D
code4: B
code3: A B
code4: A
重复: B
重复: C
重复: D
修改的时候,千万不要以为是在入栈时剪枝,我试了这样做,没有作用。其实是在判断重复的代码段那里,判断如果没有parent,就什么都不做,这样才能剪去递归的前后代码。Python代码如下:
def dfs_tricolor_cut(self):
# 着色
colors = [UnweightedGraph.WHITE for _ in self.__vertices]
# d代表discover f代表finish,发现时间戳和完成时间戳
n = len(self.__vertices)
for root in range(n):
stack = [[None, root]]
while len(stack) > 0:
v = stack[-1]
parent = v[0]
this = v[1]
if colors[this] == UnweightedGraph.GRAY:
stack.pop()
self.code4(this)
if parent is not None:
self.code3(parent, this)
colors[this] = UnweightedGraph.BLACK
elif colors[this] == UnweightedGraph.BLACK:
if parent is None:
self.repeat(this)
stack.pop()
else:
if parent is not None:
self.code2(parent, this)
self.code1(this)
for neighbor in reversed(self.__edges[this]):
stack.append([this, neighbor])
colors[this] = UnweightedGraph.GRAY
DFS时间戳
在实际应用中,需要计算出各个节点的发现时间戳与完成时间戳,也就是在上述递归模板中的code1和code4部分分别记录时间戳。把模板替换下就完成了:
def dfs_tricolor_time(self):
# 着色
colors = [UnweightedGraph.WHITE for _ in self.__vertices]
# d代表discover f代表finish,发现时间戳和完成时间戳
time = 1
times = [{"d": 0, "f": 0} for _ in self.__vertices]
n = len(self.__vertices)
for root in range(n):
stack = [[None, root]]
while len(stack) > 0:
v = stack[-1]
parent = v[0]
this = v[1]
if colors[this] == UnweightedGraph.GRAY:
stack.pop()
times[this]['f'] = time
time += 1
colors[this] = UnweightedGraph.BLACK
elif colors[this] == UnweightedGraph.BLACK:
stack.pop()
else:
times[this]['d'] = time
time += 1
for neighbor in reversed(self.__edges[this]):
stack.append([this, neighbor])
colors[this] = UnweightedGraph.GRAY
return times
用上面的图测试,测试结果完全正确:
[{'d': 1, 'f': 8}, {'d': 2, 'f': 7}, {'d': 3, 'f': 6}, {'d': 4, 'f': 5}]
完整代码
# 无权图,主要有两个属性,因为图不太可能完全一一映射,所以,用稀疏矩阵
class UnweightedGraph:
# 定义几个静态的常量
WHITE = 0
GRAY = 1
BLACK = 2
def __init__(self):
self.__vertices = []
self.__edges = []
@property
def vertices(self):
return self.__vertices
@vertices.setter
def vertices(self, value):
self.__vertices = value
@property
def edges(self):
return self.__edges
@edges.setter
def edges(self, value):
self.__edges = value
def dfs(self):
visited = [False for _ in self.__vertices]
for root, _ in enumerate(self.__vertices):
self.dfs_vertex(root, visited)
def dfs_vertex(self, node, visited):
if visited[node]:
self.repeat(node)
return
visited[node] = True
self.code1(node)
for neighbour in self.__edges[node]:
self.code2(node, neighbour)
self.dfs_vertex(neighbour, visited)
self.code3(node, neighbour)
self.code4(node)
def repeat(self, node):
print("重复: ", self.__vertices[node])
def code1(self, node):
print("code1: ", self.__vertices[node])
def code2(self, node, neighbour):
print("code2: ", self.__vertices[node], self.__vertices[neighbour])
def code3(self, node, neighbour):
print("code3: ", self.__vertices[node], self.__vertices[neighbour])
def code4(self, node):
print("code4: ", self.__vertices[node])
def dfs_tricolor(self):
# 着色
colors = [UnweightedGraph.WHITE for _ in self.__vertices]
# d代表discover f代表finish,发现时间戳和完成时间戳
n = len(self.__vertices)
for root in range(n):
stack = [[None, root]]
while len(stack) > 0:
v = stack[-1]
parent = v[0]
this = v[1]
if colors[this] == UnweightedGraph.GRAY:
stack.pop()
self.code4(this)
if parent is not None:
self.code3(parent, this)
colors[this] = UnweightedGraph.BLACK
elif colors[this] == UnweightedGraph.BLACK:
if parent is not None:
self.code2(parent, this)
self.repeat(this)
if parent is not None:
self.code3(parent, this)
stack.pop()
else:
if parent is not None:
self.code2(parent, this)
self.code1(this)
for neighbor in reversed(self.__edges[this]):
stack.append([this, neighbor])
colors[this] = UnweightedGraph.GRAY
def to_dot_string(self):
str = 'digraph{\n'
for v in self.__vertices:
str += f'{v};\n'
for i in range(0, len(self.__edges)):
edges = self.__edges[i]
for e in edges:
str += f'{self.__vertices[i]}->{self.__vertices[e]};\n'
str += '}\n'
return str
def dfs_cut(self):
visited = [False for _ in self.__vertices]
for root, _ in enumerate(self.__vertices):
self.dfs_cut_vertex(root, visited)
def dfs_cut_vertex(self, node, visited):
if visited[node]:
self.repeat(node)
return
visited[node] = True
self.code1(node)
for neighbour in self.__edges[node]:
if not visited[neighbour]:
self.code2(node, neighbour)
self.dfs_cut_vertex(neighbour, visited)
self.code3(node, neighbour)
self.code4(node)
def dfs_tricolor_cut(self):
# 着色
colors = [UnweightedGraph.WHITE for _ in self.__vertices]
# d代表discover f代表finish,发现时间戳和完成时间戳
n = len(self.__vertices)
for root in range(n):
stack = [[None, root]]
while len(stack) > 0:
v = stack[-1]
parent = v[0]
this = v[1]
if colors[this] == UnweightedGraph.GRAY:
stack.pop()
self.code4(this)
if parent is not None:
self.code3(parent, this)
colors[this] = UnweightedGraph.BLACK
elif colors[this] == UnweightedGraph.BLACK:
if parent is None:
self.repeat(this)
stack.pop()
else:
if parent is not None:
self.code2(parent, this)
self.code1(this)
for neighbor in reversed(self.__edges[this]):
stack.append([this, neighbor])
colors[this] = UnweightedGraph.GRAY
def dfs_tricolor_time(self):
# 着色
colors = [UnweightedGraph.WHITE for _ in self.__vertices]
# d代表discover f代表finish,发现时间戳和完成时间戳
time = 1
times = [{"d": 0, "f": 0} for _ in self.__vertices]
n = len(self.__vertices)
for root in range(n):
stack = [[None, root]]
while len(stack) > 0:
v = stack[-1]
parent = v[0]
this = v[1]
if colors[this] == UnweightedGraph.GRAY:
stack.pop()
times[this]['f'] = time
time += 1
colors[this] = UnweightedGraph.BLACK
elif colors[this] == UnweightedGraph.BLACK:
stack.pop()
else:
times[this]['d'] = time
time += 1
for neighbor in reversed(self.__edges[this]):
stack.append([this, neighbor])
colors[this] = UnweightedGraph.GRAY
return times
完整测试代码
import unittest
from com.youngthing.graph.graph_dfs import UnweightedGraph
class MyTestCase(unittest.TestCase):
def create_dag(self):
vertices = ['A', 'B', 'C', 'D', ]
edges = [
[1, ], # 0
[2, 3], # 1
[3, ], # 2
[], # 3
]
graph = UnweightedGraph()
graph.vertices = vertices
graph.edges = edges
return graph
def test_dfs(self):
"""
输出结果
"""
# 定义一个图,测试BFS搜索
graph = self.create_dag()
print(graph.to_dot_string())
graph.dfs()
print("三色标记")
graph.dfs_tricolor()
def test_dfs_cut(self):
"""
输出结果
"""
# 定义一个图,测试BFS搜索
graph = self.create_dag()
print(graph.to_dot_string())
graph.dfs_cut()
print("三色标记")
graph.dfs_tricolor_cut()
def test_dfs_cut(self):
"""
输出结果
"""
# 定义一个图,测试BFS搜索
graph = self.create_dag()
print(graph.dfs_tricolor_time())
if __name__ == '__main__':
unittest.main()