Оскільки існуюча нерекурсивна реалізація 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)
Запуск цього коду на наведеному прикладі графіка дає такий результат:
A -> B -> C -> D
A -> B -> D
A -> C -> B -> D
A -> C -> D
Зауважимо, що, хоча цей приклад графа непрямий (тобто всі його краї йдуть обома напрямками), алгоритм також працює для довільно спрямованих графіків. Наприклад, видалення C -> B
краю (видалення B
зі списку сусідів C
) дає такий же вихід, за винятком третього шляху ( A -> C -> B -> D
), який вже неможливий.
Пс. Побудувати графіки, для яких такі прості алгоритми пошуку, як цей (та інші, наведені в цій темі), працюють дуже погано.
Наприклад, розглянемо завдання знайти всі шляхи від А до В на непрямому графіку, де у початкового вузла А є два сусіди: цільовий вузол В (у якого немає інших сусідів, крім А) і вузол С, який є частиною кліки з 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 !) Час, марно досліджуючи шляхи всередині кліки, хоча очевидно (для людини), що жоден із цих шляхів не може призвести до Б.
Можна також сконструювати DAG з подібними властивостями, наприклад, маючи стартовий вузол A, що з'єднує цільовий вузол B, та два інші вузли C 1 і C 2 , обидва з яких підключаються до вузлів D 1 і D 2 , обидва з яких з'єднуються з E 1 і Е 2 тощо. Для n шарів вузлів, розташованих так, наївний пошук усіх шляхів від А до В закінчиться витрачати час (2 n ), вивчаючи всі можливі тупики перед відмовою.
Звичайно, додаючи край до цільового вузла B від одного з вузлів в кліці (крім З), або з останнього шару DAG, буде створювати експоненціально велике число можливих шляхів від A до B, а суто локальний алгоритм пошуку насправді не може заздалегідь сказати, знайде він такий край чи ні. Таким чином, у певному сенсі погана чутливість виводу таких наївних пошуків пояснюється їх недостатньою обізнаністю про глобальну структуру графіка.
Хоча існують різні методи попередньої обробки (такі як ітераційне усунення листкових вузлів, пошук одновузлових роздільників вершин тощо), які можуть бути використані, щоб уникнути деяких із цих «тупикових експоненціально-часових кінців», я не знаю жодного загального хитрість попередньої обробки, яка могла б усунути їх у всіх випадках. Загальним рішенням було б перевірити на кожному кроці пошуку, чи цільовий вузол все ще доступний (за допомогою під-пошуку), і відкликати його рано, якщо це не так - але на жаль, це значно сповільнить пошук (в гіршому випадку (пропорційно розміру графіка) для багатьох графіків, які не містять таких патологічних тупиків.