Come tracciare il percorso in una ricerca in ampiezza?


104

Come si traccia il percorso di una ricerca in ampiezza, in modo tale che nell'esempio seguente:

Se si cerca la chiave 11, restituisce l' elenco più breve collegando 1 a 11.

[1, 4, 7, 11]

6
In realtà era un vecchio incarico su cui stavo aiutando un amico mesi fa, basato sulla legge Kevin Bacon. La mia soluzione finale è stata molto sciatta, ho praticamente fatto un'altra ricerca Breadth-first per "riavvolgere" e tornare indietro. Non voglio trovare una soluzione migliore.
Christopher Markieta

21
Eccellente. Considero la rivisitazione di un vecchio problema nel tentativo di trovare una risposta migliore per essere un tratto ammirevole in un ingegnere. Ti auguro ogni bene per gli studi e la carriera.
Peter Rowell

1
Grazie per la lode, credo solo che se non lo imparo ora, mi troverò di nuovo ad affrontare lo stesso problema.
Christopher Markieta

Risposte:


194

Dovresti prima dare un'occhiata a http://en.wikipedia.org/wiki/Breadth-first_search .


Di seguito è riportata una rapida implementazione, in cui ho utilizzato un elenco di elenchi per rappresentare la coda dei percorsi.

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

Un altro approccio sarebbe mantenere una mappatura da ogni nodo al suo genitore e, quando si ispeziona il nodo adiacente, registrare il suo genitore. Quando la ricerca è terminata, è sufficiente eseguire il backtrace in base alla mappatura principale.

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

I codici precedenti si basano sul presupposto che non ci siano cicli.


2
Questo è eccellente! Il mio processo di pensiero mi ha portato a credere nella creazione di un qualche tipo di tabella o matrice, devo ancora imparare a conoscere i grafici. Grazie.
Christopher Markieta

Ho anche provato a utilizzare un approccio di tracciamento a ritroso anche se questo sembra molto più pulito. Sarebbe possibile creare un grafico se conoscessi solo l'inizio e la fine ma nessuno dei nodi intermedi? O anche un altro approccio oltre ai grafici?
Christopher Markieta

@ChristopherM Non sono riuscito a capire la tua domanda :(
qiao

1
È possibile adattare il primo algoritmo in modo che restituisca tutti i percorsi da 1 a 11 (supponendo che ce ne sia più di uno)?
Maria Ines Parnisari

1
Si consiglia di utilizzare collections.deque invece di un elenco. La complessità di list.pop (0) è O (n) mentre deque.popleft () è O (1)
Omar_0x80

23

Mi è piaciuta molto la prima risposta di qiao! L'unica cosa che manca qui è contrassegnare i vertici come visitati.

Perché dobbiamo farlo?
Immaginiamo che ci sia un altro nodo numero 13 connesso dal nodo 11. Ora il nostro obiettivo è trovare il nodo 13.
Dopo un po 'di corsa la coda sarà simile a questa:

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

Notare che ci sono DUE percorsi con il numero di nodo 10 alla fine.
Ciò significa che i percorsi dal nodo numero 10 verranno controllati due volte. In questo caso non sembra così male perché il nodo numero 10 non ha figli .. Ma potrebbe essere davvero cattivo (anche qui controlleremo quel nodo due volte senza motivo ..) Il
nodo numero 13 non è in quei percorsi in modo che il programma non ritorni prima di raggiungere il secondo percorso con il nodo numero 10 alla fine .. E lo ricontrolleremo ..

Ci manca solo un set per marcare i nodi visitati e non controllarli di nuovo ..
Questo è il codice di qiao dopo la modifica:

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)

L'output del programma sarà:

[1, 4, 7, 11, 13]

Senza i controlli inutili ..


6
Potrebbe essere utile usare collections.dequeper queuecome list.pop (0) incorrere in O(n)movimenti di memoria. Inoltre, per il bene dei posteri, se vuoi fare DFS basta impostare, path = queue.pop()nel qual caso la variabile si queuecomporta effettivamente come un file stack.
Sudhi

11

Codice molto semplice. Continui ad aggiungere il percorso ogni volta che scopri un nodo.

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

2
Trovo il tuo codice molto leggibile, rispetto ad altre risposte. Grazie mille!
Mitko Rusev

8

Ho pensato di provare a codificarlo per divertimento:

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 vuoi cicli puoi aggiungere questo:

for i, j in for_front: # allow cycles, add this code
    if i in graph:
        del graph[i]

dopo aver costruito next_for_front. Una domanda successiva, cosa succede se il grafico contiene loop? Ad esempio, se il nodo 1 avesse un bordo che si ricollega a se stesso? Cosa succede se il grafico ha più bordi che vanno tra due nodi?
robert king

1

Mi piacciono sia la prima risposta di @Qiao che l'aggiunta di @ Or. Per motivi di elaborazione un po 'meno vorrei aggiungere alla risposta di Or.

Nella risposta di @ Or tenere traccia del nodo visitato è fantastico. Possiamo anche consentire al programma di uscire prima di quanto lo sia attualmente. Ad un certo punto del ciclo for, current_neighbourdovrà essere il end, e una volta che ciò accade viene trovato il percorso più breve e il programma può tornare.

Modificherei il metodo come segue, prestando molta attenzione al ciclo 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)

L'output e tutto il resto sarà lo stesso. Tuttavia, l'elaborazione del codice richiederà meno tempo. Ciò è particolarmente utile su grafici più grandi. Spero che questo aiuti qualcuno in futuro.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.