Поскольку существующая нерекурсивная реализация DFS, приведенная в этом ответе, кажется неработающей, позвольте мне предоставить ту, которая действительно работает.
Я написал это на Python, потому что нахожу его довольно читаемым и не загроможденным деталями реализации (а также потому, что в нем есть удобный yield
ключевое слово для реализации генераторов ), но его должно быть довольно легко перенести на другие языки.
# 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
Этот код поддерживает два параллельных стека: один содержит более ранние узлы в текущем пути, а другой содержит индекс текущего соседа для каждого узла в стеке узлов (так что мы можем возобновить итерацию по соседям узла, когда мы вытолкнем его обратно. стек). Я мог бы с таким же успехом использовать один стек пар (узел, индекс), но я решил, что метод с двумя стеками будет более читабельным и, возможно, более легким для реализации для пользователей других языков.
В этом коде также используется отдельный visited
набор, который всегда содержит текущий узел и все узлы в стеке, чтобы я мог эффективно проверить, является ли узел уже частью текущего пути. Если ваш язык имеет структуру данных «упорядоченного набора», которая обеспечивает как эффективные, подобные стеку, операции push / pop и эффективные запросы членства, вы можете использовать ее для стека узлов и избавиться от отдельного visited
набора.
В качестве альтернативы, если вы используете настраиваемый изменяемый класс / структуру для своих узлов, вы можете просто сохранить логический флаг в каждом узле, чтобы указать, был ли он посещен как часть текущего пути поиска. Конечно, этот метод не позволит вам выполнить два поиска на одном и том же графике параллельно, если вы по какой-то причине захотите это сделать.
Вот тестовый код, демонстрирующий, как работает приведенная выше функция:
# 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)
Запуск этого кода на приведенном примере графа дает следующий результат:
А -> В -> С -> D
А -> Б -> D
А -> С -> В -> D
А -> С -> D
Обратите внимание, что хотя этот пример графа неориентированный (то есть все его ребра идут в обе стороны), алгоритм также работает для произвольных ориентированных графов. Например, удаление C -> B
края (путем удаления B
из списка соседей C
) дает тот же результат, за исключением третьего пути ( A -> C -> B -> D
), который больше невозможен.
Ps. Легко построить графики, для которых простые алгоритмы поиска, подобные этому (и другим, приведенным в этом потоке), работают очень плохо.
Например, рассмотрим задачу поиска всех путей от A до B на неориентированном графе, где у начального узла A есть два соседа: целевой узел B (у которого нет других соседей, кроме A) и узел C, который является частью клики из n +1 узлов, например:
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"),
}
Легко видеть, что единственный путь между A и B является прямым, но наивная DFS, запущенная с узла A, будет тратить O ( n !) Времени на бесполезное исследование путей внутри клики, хотя (для человека) очевидно, что ни один из этих путей не может привести к B.
Можно также создать группы DAG с аналогичными свойствами, например, если начальный узел A соединит целевой узел B и два других узла C 1 и C 2 , оба из которых подключаются к узлам D 1 и D 2 , оба из которых подключаются к E 1 и E 2 и так далее. Для n слоев узлов, расположенных таким образом, наивный поиск всех путей от A до B приведет к потере O (2 n ) времени на изучение всех возможных тупиков, прежде чем отказаться.
Конечно, добавляя край к целевому узлу B от одного из узлов в клике (кроме С), или из последнего слоя DAG, будет создавать экспоненциально большое число возможных путей от A до B, а чисто локальный алгоритм поиска не может заранее сказать, найдет он такое ребро или нет. Таким образом, в некотором смысле низкая чувствительность к выходным данным таких наивных поисков связана с отсутствием понимания глобальной структуры графа.
Хотя существуют различные методы предварительной обработки (такие как итеративное удаление листовых узлов, поиск одноузловых разделителей вершин и т. Д.), Которые можно использовать, чтобы избежать некоторых из этих «тупиков экспоненциального времени», я не знаю ни одного общего трюк с предварительной обработкой, который может устранить их во всех случаях. Общим решением было бы проверять на каждом этапе поиска, доступен ли целевой узел (с помощью подпоиска), и рано возвращаться, если это не так, но, увы, это значительно замедлит поиск (в худшем случае пропорционально размеру графа) для многих графов, не содержащих таких патологических тупиков.