Como a implementação de DFS não recursiva existente fornecida nesta resposta parece estar quebrada, deixe-me fornecer uma que realmente funcione.
Eu escrevi isso em Python, porque acho que é bastante legível e organizado por detalhes de implementação (e porque tem a yield
palavra-chave útil para implementar geradores ), mas deve ser bastante fácil de portar para outras linguagens.
# 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
Este código mantém duas pilhas paralelas: uma contendo os nós anteriores no caminho atual e outra contendo o índice vizinho atual para cada nó na pilha de nós (para que possamos retomar a iteração através dos vizinhos de um nó quando o retirarmos a pilha). Eu poderia também ter usado uma única pilha de pares (nó, índice), mas imaginei que o método de duas pilhas seria mais legível e talvez mais fácil de implementar para usuários de outras linguagens.
Este código também usa um visited
conjunto separado , que sempre contém o nó atual e quaisquer nós na pilha, para me permitir verificar com eficiência se um nó já faz parte do caminho atual. Se a sua linguagem tiver uma estrutura de dados de "conjunto ordenado" que fornece operações push / pop semelhantes a pilha e consultas de associação eficientes, você pode usar isso para a pilha de nós e se livrar do visited
conjunto separado .
Alternativamente, se você estiver usando uma classe / estrutura mutável personalizada para seus nós, você pode simplesmente armazenar um sinalizador booleano em cada nó para indicar se ele foi visitado como parte do caminho de pesquisa atual. É claro que esse método não permite que você execute duas pesquisas no mesmo gráfico em paralelo, caso você queira fazer isso por algum motivo.
Aqui estão alguns códigos de teste que demonstram como a função fornecida acima funciona:
# 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)
Executar este código no gráfico de exemplo fornecido produz a seguinte saída:
A -> B -> C -> D
A -> B -> D
A -> C -> B -> D
A -> C -> D
Observe que, embora este gráfico de exemplo não seja direcionado (ou seja, todas as suas arestas vão para os dois lados), o algoritmo também funciona para gráficos direcionados arbitrários. Por exemplo, remover a C -> B
borda (removendo B
da lista de vizinhos de C
) produz a mesma saída, exceto para o terceiro caminho ( A -> C -> B -> D
), que não é mais possível.
Ps. É fácil construir gráficos para os quais algoritmos de pesquisa simples como este (e os outros fornecidos neste tópico) têm um desempenho muito fraco.
Por exemplo, considere a tarefa de encontrar todos os caminhos de A a B em um grafo não direcionado onde o nó inicial A tem dois vizinhos: o nó alvo B (que não tem outros vizinhos além de A) e um nó C que faz parte de um clique de n +1 nós, como este:
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"),
}
É fácil ver que o único caminho entre A e B é o direto, mas um DFS ingênuo iniciado no nó A perderá O ( n !) Tempo explorando inutilmente os caminhos dentro do clique, mesmo que seja óbvio (para um humano) que nenhum desses caminhos pode levar a B.
Também é possível construir DAGs com propriedades semelhantes, por exemplo, fazendo com que o nó inicial A conecte o nó de destino B e a dois outros nós C 1 e C 2 , ambos os quais se conectam aos nós D 1 e D 2 , ambos os quais se conectam a E 1 e E 2 e assim por diante. Para n camadas de nós organizados dessa forma, uma busca ingênua por todos os caminhos de A a B acabará perdendo O (2 n ) tempo examinando todos os possíveis becos sem saída antes de desistir.
É claro, a adição de um bordo para o nó de destino B a partir de um dos nós no bando (diferente de C), ou a partir da última camada do DAG, iria criar um exponencialmente grande número de possíveis caminhos de A a B, e um o algoritmo de busca puramente local não pode realmente dizer com antecedência se encontrará ou não tal borda. Assim, em certo sentido, a baixa sensibilidade de saída dessas pesquisas ingênuas se deve à falta de consciência da estrutura global do gráfico.
Embora existam vários métodos de pré-processamento (como a eliminação iterativa de nós folha, a procura de separadores de vértice de nó único, etc.) que poderiam ser usados para evitar alguns desses "becos sem saída em tempo exponencial", não conheço nenhum geral truque de pré-processamento que poderia eliminá-los em todos os casos. Uma solução geral seria verificar em cada etapa da pesquisa se o nó de destino ainda pode ser alcançado (usando uma subprocura) e retroceder mais cedo se não for - mas, infelizmente, isso tornaria a pesquisa significativamente mais lenta (na pior das hipóteses , proporcionalmente ao tamanho do gráfico) para muitos gráficos que não contêm esses becos sem saída patológicos.