Vì triển khai DFS không đệ quy hiện có được đưa ra trong câu trả lời này dường như bị hỏng, hãy để tôi cung cấp một cách thực sự hoạt động.
Tôi đã viết điều này bằng Python, vì tôi thấy nó khá dễ đọc và không gọn gàng bởi các chi tiết triển khai (và bởi vì nó có yield
từ khóa tiện dụng để triển khai trình tạo ), nhưng nó sẽ khá dễ dàng để chuyển sang các ngôn ngữ khác.
# a generator function to find all simple paths between two nodes in a
# graph, represented as a dictionary that maps nodes to their neighbors
def find_simple_paths(graph, start, end):
visited = set()
visited.add(start)
nodestack = list()
indexstack = list()
current = start
i = 0
while True:
# get a list of the neighbors of the current node
neighbors = graph[current]
# find the next unvisited neighbor of this node, if any
while i < len(neighbors) and neighbors[i] in visited: i += 1
if i >= len(neighbors):
# we've reached the last neighbor of this node, backtrack
visited.remove(current)
if len(nodestack) < 1: break # can't backtrack, stop!
current = nodestack.pop()
i = indexstack.pop()
elif neighbors[i] == end:
# yay, we found the target node! let the caller process the path
yield nodestack + [current, end]
i += 1
else:
# push current node and index onto stacks, switch to neighbor
nodestack.append(current)
indexstack.append(i+1)
visited.add(neighbors[i])
current = neighbors[i]
i = 0
Đoạn mã này duy trì hai ngăn xếp song song: một ngăn chứa các nút trước đó trong đường dẫn hiện tại và một chứa chỉ mục hàng xóm hiện tại cho mỗi nút trong ngăn xếp nút (để chúng ta có thể tiếp tục lặp lại các nút lân cận của nút khi chúng ta bật lại nó ngăn xếp). Tôi có thể đã sử dụng tốt như nhau một ngăn xếp các cặp (nút, chỉ mục), nhưng tôi nghĩ rằng phương pháp hai ngăn xếp sẽ dễ đọc hơn và có lẽ dễ triển khai hơn đối với người dùng các ngôn ngữ khác.
Mã này cũng sử dụng một visited
tập hợp riêng biệt , luôn chứa nút hiện tại và bất kỳ nút nào trên ngăn xếp, để cho phép tôi kiểm tra hiệu quả xem một nút đã là một phần của đường dẫn hiện tại hay chưa. Nếu ngôn ngữ của bạn tình cờ có cấu trúc dữ liệu "tập hợp có thứ tự" cung cấp cả hoạt động đẩy / bật lên giống ngăn xếp và truy vấn thành viên hiệu quả, bạn có thể sử dụng cấu trúc đó cho ngăn xếp nút và loại bỏ visited
tập hợp riêng biệt .
Ngoài ra, nếu bạn đang sử dụng một lớp / cấu trúc có thể thay đổi tùy chỉnh cho các nút của mình, bạn chỉ có thể lưu trữ cờ boolean trong mỗi nút để cho biết liệu nó đã được truy cập như một phần của đường dẫn tìm kiếm hiện tại hay chưa. Tất nhiên, phương pháp này sẽ không cho phép bạn chạy song song hai tìm kiếm trên cùng một biểu đồ, nếu vì lý do nào đó bạn muốn làm điều đó.
Dưới đây là một số mã kiểm tra chứng minh cách hoạt động của hàm được cung cấp ở trên:
# test graph:
# ,---B---.
# A | D
# `---C---'
graph = {
"A": ("B", "C"),
"B": ("A", "C", "D"),
"C": ("A", "B", "D"),
"D": ("B", "C"),
}
# find paths from A to D
for path in find_simple_paths(graph, "A", "D"): print " -> ".join(path)
Chạy mã này trên biểu đồ ví dụ đã cho sẽ tạo ra kết quả sau:
A -> B -> C -> D
A -> B -> D
A -> C -> B -> D
A -> C -> D
Lưu ý rằng, trong khi đồ thị ví dụ này là vô hướng (nghĩa là tất cả các cạnh của nó đi theo cả hai chiều), thuật toán cũng hoạt động đối với đồ thị có hướng tùy ý. Ví dụ: loại bỏ C -> B
cạnh (bằng cách xóa B
khỏi danh sách hàng xóm của C
) tạo ra cùng một đầu ra ngoại trừ đường dẫn thứ ba ( A -> C -> B -> D
), điều này không còn khả thi nữa.
Ps. Thật dễ dàng để xây dựng các biểu đồ mà các thuật toán tìm kiếm đơn giản như thuật toán này (và các thuật toán khác được đưa ra trong chủ đề này) hoạt động rất kém.
Ví dụ: hãy xem xét nhiệm vụ tìm tất cả các đường đi từ A đến B trên một đồ thị vô hướng trong đó nút bắt đầu A có hai lân cận: nút đích B (không có lân cận nào khác ngoài A) và nút C là một phần của một nhóm trong số n nút +1, như thế này:
graph = {
"A": ("B", "C"),
"B": ("A"),
"C": ("A", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O"),
"D": ("C", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O"),
"E": ("C", "D", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O"),
"F": ("C", "D", "E", "G", "H", "I", "J", "K", "L", "M", "N", "O"),
"G": ("C", "D", "E", "F", "H", "I", "J", "K", "L", "M", "N", "O"),
"H": ("C", "D", "E", "F", "G", "I", "J", "K", "L", "M", "N", "O"),
"I": ("C", "D", "E", "F", "G", "H", "J", "K", "L", "M", "N", "O"),
"J": ("C", "D", "E", "F", "G", "H", "I", "K", "L", "M", "N", "O"),
"K": ("C", "D", "E", "F", "G", "H", "I", "J", "L", "M", "N", "O"),
"L": ("C", "D", "E", "F", "G", "H", "I", "J", "K", "M", "N", "O"),
"M": ("C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "N", "O"),
"N": ("C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "O"),
"O": ("C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N"),
}
Dễ dàng nhận thấy rằng con đường duy nhất giữa A và B là con đường trực tiếp, nhưng một DFS ngây thơ bắt đầu từ nút A sẽ lãng phí O ( n !) Thời gian vô ích để khám phá các con đường bên trong bè phái, mặc dù rõ ràng là (đối với con người) không có con đường nào có thể dẫn đến B.
Người ta cũng có thể xây dựng các DAG với các thuộc tính tương tự, ví dụ bằng cách để nút bắt đầu A kết nối nút đích B và với hai nút khác C 1 và C 2 , cả hai đều kết nối với nút D 1 và D 2 , cả hai đều kết nối với E 1 và E 2 , v.v. Đối với n lớp nút được sắp xếp như thế này, một tìm kiếm ngây thơ cho tất cả các đường đi từ A đến B sẽ làm lãng phí O (2 n ) thời gian để kiểm tra tất cả các ngõ cụt có thể có trước khi từ bỏ.
Tất nhiên, việc thêm một cạnh vào nút đích B từ một trong những nút trong nhóm (không phải C) hoặc từ lớp cuối cùng của DAG, sẽ tạo ra một số lượng lớn các đường đi có thể từ A đến B theo cấp số nhân và thuật toán tìm kiếm cục bộ thuần túy thực sự không thể nói trước liệu nó có tìm thấy một cạnh như vậy hay không. Do đó, theo một nghĩa nào đó, độ nhạy đầu ra kém của các tìm kiếm ngây thơ như vậy là do họ thiếu nhận thức về cấu trúc toàn cục của biểu đồ.
Mặc dù có nhiều phương pháp tiền xử lý khác nhau (chẳng hạn như loại bỏ lặp đi lặp lại các nút lá, tìm kiếm các dấu phân tách đỉnh đơn nút, v.v.) có thể được sử dụng để tránh một số "kết thúc theo thời gian theo cấp số nhân", tôi không biết bất kỳ điều gì chung thủ thuật tiền xử lý có thể loại bỏ chúng trong mọi trường hợp. Một giải pháp chung là kiểm tra ở mọi bước tìm kiếm xem nút đích có còn có thể truy cập được hay không (sử dụng tìm kiếm phụ) và quay lại sớm nếu không - nhưng than ôi, điều đó sẽ làm chậm tìm kiếm đáng kể (tệ nhất là , tỷ lệ với kích thước của biểu đồ) đối với nhiều biểu đồ không chứa các đường cụt bệnh lý như vậy.