Como rastrear o caminho em uma pesquisa em amplitude?

104

Como você rastreia o caminho de uma pesquisa em amplitude, de modo que no exemplo a seguir:

Se estiver procurando por chave 11, retorne a lista mais curta conectando de 1 a 11.

[1, 4, 7, 11]
Christopher Markieta
fonte
6
Na verdade, era uma tarefa antiga na qual eu estava ajudando um amigo meses atrás, com base na Lei Kevin Bacon. Minha solução final foi muito desleixada. Basicamente, fiz outra pesquisa de largura para "retroceder" e voltar atrás. Não quero encontrar uma solução melhor.
Christopher Markieta
21
Excelente. Penso em revisitar um velho problema na tentativa de encontrar uma resposta melhor para ser uma característica admirável em um engenheiro. Desejo-lhe felicidades nos estudos e na carreira.
Peter Rowell
1
Obrigado pelo elogio, só acredito que se não aprender agora, terei de enfrentar o mesmo problema novamente.
Christopher Markieta de

Respostas:

194

Você deve consultar http://en.wikipedia.org/wiki/Breadth-first_search primeiro.


Abaixo está uma implementação rápida, na qual usei uma lista de lista para representar a fila de caminhos.

# graph is in adjacent list representation
graph = {
        '1': ['2', '3', '4'],
        '2': ['5', '6'],
        '5': ['9', '10'],
        '4': ['7', '8'],
        '7': ['11', '12']
        }

def bfs(graph, start, end):
    # maintain a queue of paths
    queue = []
    # push the first path into the queue
    queue.append([start])
    while queue:
        # get the first path from the queue
        path = queue.pop(0)
        # get the last node from the path
        node = path[-1]
        # path found
        if node == end:
            return path
        # enumerate all adjacent nodes, construct a new path and push it into the queue
        for adjacent in graph.get(node, []):
            new_path = list(path)
            new_path.append(adjacent)
            queue.append(new_path)

print bfs(graph, '1', '11')

Outra abordagem seria manter um mapeamento de cada nó para seu pai e, ao inspecionar o nó adjacente, registrar seu pai. Quando a pesquisa for concluída, basta fazer o backtrace de acordo com o mapeamento pai.

graph = {
        '1': ['2', '3', '4'],
        '2': ['5', '6'],
        '5': ['9', '10'],
        '4': ['7', '8'],
        '7': ['11', '12']
        }

def backtrace(parent, start, end):
    path = [end]
    while path[-1] != start:
        path.append(parent[path[-1]])
    path.reverse()
    return path


def bfs(graph, start, end):
    parent = {}
    queue = []
    queue.append(start)
    while queue:
        node = queue.pop(0)
        if node == end:
            return backtrace(parent, start, end)
        for adjacent in graph.get(node, []):
            if node not in queue :
                parent[adjacent] = node # <<<<< record its parent 
                queue.append(adjacent)

print bfs(graph, '1', '11')

Os códigos acima são baseados no pressuposto de que não há ciclos.

qiao
fonte
2
Isto e excelente! Meu processo de pensamento me levou a acreditar na criação de algum tipo de tabela ou matriz, ainda não aprendi sobre gráficos. Obrigado.
Christopher Markieta
Também tentei usar uma abordagem de rastreamento de volta, embora isso pareça muito mais limpo. Seria possível fazer um gráfico se você conhecesse apenas o início e o fim, mas nenhum dos nós intermediários? Ou mesmo outra abordagem além de gráficos?
Christopher Markieta
@ChristopherM Não entendi sua pergunta :(
qiao
1
É possível adaptar o primeiro algoritmo para que ele retorne todos os caminhos de 1 a 11 (assumindo que haja mais de um)?
Maria Ines Parnisari,
1
É recomendado usar coleções.deque em vez de uma lista. a complexidade de list.pop (0) é O (n) enquanto deque.popleft () é O (1)
Omar_0x80
23

Gostei muito da primeira resposta de qiao! A única coisa que falta aqui é marcar os vértices como visitados.

Por que precisamos fazer isso?
Vamos imaginar que há outro nó de número 13 conectado a partir do nó 11. Agora, nosso objetivo é encontrar o nó 13.
Depois de um pouco de execução, a fila ficará assim:

[[1, 2, 6], [1, 3, 10], [1, 4, 7], [1, 4, 8], [1, 2, 5, 9], [1, 2, 5, 10]]

Observe que existem DOIS caminhos com o número de nó 10 no final.
O que significa que os caminhos do nó número 10 serão verificados duas vezes. Neste caso, não parece tão ruim porque o nó número 10 não tem nenhum filho .. Mas pode ser muito ruim (mesmo aqui vamos verificar esse nó duas vezes sem motivo ..) O
nó número 13 não está em esses caminhos para que o programa não retorne antes de chegar ao segundo caminho com o nó número 10 no final .. E vamos verificar novamente ..

Só falta um conjunto para marcar os nós visitados e não verificá-los novamente.
Este é o código de qiao após a modificação:

graph = {
    1: [2, 3, 4],
    2: [5, 6],
    3: [10],
    4: [7, 8],
    5: [9, 10],
    7: [11, 12],
    11: [13]
}


def bfs(graph_to_search, start, end):
    queue = [[start]]
    visited = set()

    while queue:
        # Gets the first path in the queue
        path = queue.pop(0)

        # Gets the last node in the path
        vertex = path[-1]

        # Checks if we got to the end
        if vertex == end:
            return path
        # We check if the current node is already in the visited nodes set in order not to recheck it
        elif vertex not in visited:
            # enumerate all adjacent nodes, construct a new path and push it into the queue
            for current_neighbour in graph_to_search.get(vertex, []):
                new_path = list(path)
                new_path.append(current_neighbour)
                queue.append(new_path)

            # Mark the vertex as visited
            visited.add(vertex)


print bfs(graph, 1, 13)

O resultado do programa será:

[1, 4, 7, 11, 13]

Sem as verificações desnecessárias ..

Ou Kazaz
fonte
6
Pode ser útil usar collections.dequepara queueas list.pop (0) incorrer em O(n)movimentos de memória. Além disso, para o bem da posteridade, se você quiser fazer DFS apenas defina path = queue.pop(), nesse caso, a variável queuerealmente atua como um stack.
Sudhi
11

Código muito fácil. Você continua acrescentando o caminho cada vez que descobre um nó.

graph = {
         'A': set(['B', 'C']),
         'B': set(['A', 'D', 'E']),
         'C': set(['A', 'F']),
         'D': set(['B']),
         'E': set(['B', 'F']),
         'F': set(['C', 'E'])
         }
def retunShortestPath(graph, start, end):

    queue = [(start,[start])]
    visited = set()

    while queue:
        vertex, path = queue.pop(0)
        visited.add(vertex)
        for node in graph[vertex]:
            if node == end:
                return path + [end]
            else:
                if node not in visited:
                    visited.add(node)
                    queue.append((node, path + [node]))
SeasonalShot
fonte
2
Acho seu código muito legível, em comparação com outras respostas. Muito obrigado!
Mitko Rusev
8

Pensei em tentar codificar isso por diversão:

graph = {
        '1': ['2', '3', '4'],
        '2': ['5', '6'],
        '5': ['9', '10'],
        '4': ['7', '8'],
        '7': ['11', '12']
        }

def bfs(graph, forefront, end):
    # assumes no cycles

    next_forefront = [(node, path + ',' + node) for i, path in forefront if i in graph for node in graph[i]]

    for node,path in next_forefront:
        if node==end:
            return path
    else:
        return bfs(graph,next_forefront,end)

print bfs(graph,[('1','1')],'11')

# >>>
# 1, 4, 7, 11

Se você quiser ciclos, pode adicionar isto:

for i, j in for_front: # allow cycles, add this code
    if i in graph:
        del graph[i]
robert king
fonte
depois de construir o next_for_front. Uma pergunta a seguir, e se o gráfico contiver loops? Por exemplo, se o nó 1 tinha uma aresta conectando-se de volta a si mesmo? E se o gráfico tiver várias arestas entre dois nós?
robert king
1

Eu gosto tanto da primeira resposta de @Qiao quanto da adição de @ Or. Para um pouco menos de processamento, gostaria de acrescentar à resposta de Or.

Na resposta de @ Or, manter o controle do nó visitado é ótimo. Também podemos permitir que o programa saia mais cedo do que está atualmente. Em algum ponto do loop for, o current_neighbourterá que ser o ende, quando isso acontecer, o caminho mais curto é encontrado e o programa pode retornar.

Eu modificaria o método conforme a seguir, preste atenção ao loop for

graph = {
1: [2, 3, 4],
2: [5, 6],
3: [10],
4: [7, 8],
5: [9, 10],
7: [11, 12],
11: [13]
}


    def bfs(graph_to_search, start, end):
        queue = [[start]]
        visited = set()

    while queue:
        # Gets the first path in the queue
        path = queue.pop(0)

        # Gets the last node in the path
        vertex = path[-1]

        # Checks if we got to the end
        if vertex == end:
            return path
        # We check if the current node is already in the visited nodes set in order not to recheck it
        elif vertex not in visited:
            # enumerate all adjacent nodes, construct a new path and push it into the queue
            for current_neighbour in graph_to_search.get(vertex, []):
                new_path = list(path)
                new_path.append(current_neighbour)
                queue.append(new_path)

                #No need to visit other neighbour. Return at once
                if current_neighbour == end
                    return new_path;

            # Mark the vertex as visited
            visited.add(vertex)


print bfs(graph, 1, 13)

A saída e tudo o mais serão os mesmos. No entanto, o código levará menos tempo para ser processado. Isso é especialmente útil em gráficos maiores. Espero que isso ajude alguém no futuro.

Darie Dorlus
fonte