Comment tracer le chemin dans une recherche en largeur d'abord?

104

Comment tracer le chemin d'une recherche en largeur d'abord, comme dans l'exemple suivant:

Si vous recherchez une clé 11, renvoyez la liste la plus courte reliant 1 à 11.

[1, 4, 7, 11]
Christopher Markieta
la source
6
C'était en fait une ancienne mission pour laquelle j'aidais un ami il y a des mois, basée sur la loi de Kevin Bacon. Ma solution finale était très bâclée, j'ai essentiellement fait une autre recherche en largeur d'abord pour «rembobiner» et revenir en arrière. Je ne veux pas trouver une meilleure solution.
Christopher Markieta
21
Excellent. Je considère que revisiter un vieux problème pour tenter de trouver une meilleure réponse est un trait admirable chez un ingénieur. Je vous souhaite bonne chance dans vos études et votre carrière.
Peter Rowell
1
Merci pour les éloges, je crois juste que si je ne l'apprends pas maintenant, je serai de nouveau confronté au même problème.
Christopher Markieta

Réponses:

194

Vous devriez d'abord regarder http://en.wikipedia.org/wiki/Breadth-first_search .


Voici une implémentation rapide, dans laquelle j'ai utilisé une liste de liste pour représenter la file d'attente des chemins.

# 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')

Une autre approche consisterait à maintenir un mappage de chaque nœud à son parent et, lors de l'inspection du nœud adjacent, à enregistrer son parent. Lorsque la recherche est terminée, effectuez simplement un retour arrière en fonction du mappage parent.

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')

Les codes ci-dessus sont basés sur l'hypothèse qu'il n'y a pas de cycles.

qiao
la source
2
C'est excellent! Mon processus de réflexion m'a amené à croire en la création d'un type de tableau ou de matrice, je n'ai pas encore appris sur les graphiques. Je vous remercie.
Christopher Markieta
J'ai également essayé d'utiliser une approche de traçage arrière bien que cela semble beaucoup plus propre. Serait-il possible de faire un graphique si vous ne connaissez que le début et la fin mais aucun des nœuds intermédiaires? Ou même une autre approche en plus des graphiques?
Christopher Markieta
@ChristopherM Je n'ai pas compris votre question :(
qiao
1
Est-il possible d'adapter le premier algorithme pour qu'il renvoie tous les chemins de 1 à 11 (en supposant qu'il y en ait plus d'un)?
Maria Ines Parnisari
1
Il est recommandé d'utiliser collections.deque au lieu d'une liste. La complexité de list.pop (0) est O (n) tandis que deque.popleft () est O (1)
Omar_0x80
23

J'ai beaucoup aimé la première réponse de qiao! La seule chose qui manque ici est de marquer les sommets comme visités.

Pourquoi devons-nous le faire?
Imaginons qu'il y ait un autre nœud numéro 13 connecté à partir du nœud 11. Maintenant, notre objectif est de trouver le nœud 13.
Après un peu de course, la file d'attente ressemblera à ceci:

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

Notez qu'il y a DEUX chemins avec le numéro de nœud 10 à la fin.
Ce qui signifie que les chemins du nœud numéro 10 seront vérifiés deux fois. Dans ce cas, cela n'a pas l'air si mal car le nœud numéro 10 n'a pas d'enfants .. Mais cela pourrait être vraiment mauvais (même ici, nous vérifierons ce nœud deux fois sans raison ..) Le
nœud numéro 13 n'est pas dans ces chemins afin que le programme ne revienne pas avant d'atteindre le deuxième chemin avec le noeud numéro 10 à la fin .. Et nous le revérifierons.

Il ne nous manque qu'un ensemble pour marquer les nœuds visités et ne pas les vérifier à nouveau.
Voici le code de qiao après la modification:

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)

La sortie du programme sera:

[1, 4, 7, 11, 13]

Sans les revérifications inutiles.

Ou Kazaz
la source
6
Il peut être utile d'utiliser collections.dequepour queueas list.pop (0) engendre O(n)des mouvements de mémoire. De plus, dans un souci de postérité, si vous voulez faire DFS, définissez simplement path = queue.pop()dans quel cas la variable queueagit en fait comme un stack.
Sudhi
11

Code très simple. Vous continuez à ajouter le chemin chaque fois que vous découvrez un nœud.

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]))
Saisonnier
la source
2
Je trouve votre code très lisible, comparé à d'autres réponses. Merci beaucoup!
Mitko Rusev
8

J'ai pensé essayer de coder ceci pour le plaisir:

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

Si vous voulez des cycles, vous pouvez ajouter ceci:

for i, j in for_front: # allow cycles, add this code
    if i in graph:
        del graph[i]
Robert King
la source
après avoir construit le fichier next_for_front. Une question de suivi, et si le graphique contient des boucles? Par exemple, si le nœud 1 avait un bord se reconnectant à lui-même? Que faire si le graphe a plusieurs arêtes entre deux nœuds?
robert king
1

J'aime à la fois la première réponse de @Qiao et l'ajout de @ Or. Pour un peu moins de traitement, je voudrais ajouter à la réponse d'Or.

Dans la réponse de @ Or, le suivi du nœud visité est excellent. Nous pouvons également permettre au programme de se terminer plus tôt qu'il ne l'est actuellement. À un moment donné dans la boucle for, il current_neighbourdevra être le end, et une fois que cela se produit, le chemin le plus court est trouvé et le programme peut revenir.

Je modifierais la méthode comme suit, faites très attention à la boucle 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)

La sortie et tout le reste seront les mêmes. Cependant, le code prendra moins de temps à traiter. Ceci est particulièrement utile sur les graphiques plus grands. J'espère que cela aidera quelqu'un à l'avenir.

Darie Dorlus
la source