Як простежити шлях в пошуку за шириною до першого?


104

Як простежити шлях пошуку за шириною до першого, наприклад, у наступному прикладі:

Якщо ви шукаєте ключ 11, поверніть найкоротший список, що з'єднує 1 до 11.

[1, 4, 7, 11]

6
Насправді це було старе завдання, якому я багато місяців тому допомагав другові, грунтуючись на законі про Кевіна Бекона. Моє остаточне рішення було дуже неохайним, я в основному здійснив черговий пошук "Ширини", щоб "перемотати назад" і повернути назад. Я не хочу знайти кращого рішення.
Крістофер Маркієта

21
Відмінно. Я вважаю переглядом старої проблеми, щоб спробувати знайти кращу відповідь, щоб бути чудовою рисою інженера. Бажаю вам добра у навчанні та кар’єрі.
Пітер Роуелл

1
Дякую за похвалу, я просто вірю, що якщо я цього не вивчу зараз, я знову зіткнуся з тією ж проблемою.
Крістофер Маркієта

Відповіді:


194

Вам слід спочатку подивитися на http://en.wikipedia.org/wiki/Breadth-first_search .


Нижче наводиться швидка реалізація, в якій я використовував список списку для представлення черги шляхів.

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

Іншим підходом було б підтримання відображення від кожного вузла до його батьківського, і при огляді сусіднього вузла запишіть його батьківський. Коли пошук виконано, просто відхиліться відповідно до батьківського відображення.

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

Вищеописані коди ґрунтуються на припущенні, що циклів немає.


2
Це чудово! Мій думковий процес змусив мене повірити у створення певного типу таблиці чи матриці, я ще не повинен дізнатися про графіки. Дякую.
Крістофер Маркієта

Я також спробував застосувати підхід до відстеження, хоча це здається набагато чистішим. Чи можна було б скласти графік, якщо ви знаєте лише початок і кінець, але жоден з вузлів між ними? Або навіть інший підхід, крім графіків?
Крістофер Маркієта

@ChristopherM Я не зміг зрозуміти ваше запитання :(
qiao

1
Чи можливо адаптувати перший алгоритм так, щоб він повертав усі шляхи від 1 до 11 (якщо вважати, що їх більше)?
Марія Інес Парнісарі

1
Рекомендується використовувати collection.deque замість списку. складність list.pop (0) - O (n), а deque.popleft () - O (1)
Omar_0x80

23

Мені дуже сподобалась перша відповідь qiao! Тут бракує лише позначення вершин як відвіданих.

Навіщо нам це робити?
Давайте уявимо, що є ще один вузол №13, підключений до вузла 11. Тепер наша мета - знайти вузол 13.
Після невеликого запуску черга буде виглядати так:

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

Зверніть увагу, що Є два ДВІ контури з номером 10 на кінці.
Що означає, що шляхи від вузла №10 перевірятимуться двічі. У цьому випадку це виглядає не так вже й погано, оскільки у вузлі №10 немає дітей .. Але це може бути дуже погано (навіть тут ми перевіримо цей вузол двічі без причини.)
Номер вузла 13 не знаходиться ці шляхи, тому програма не повернеться, перш ніж дійти до другого шляху з номером 10 в кінці. І ми повторно перевіримо його ..

Все, що нам бракує, це набір для позначення відвідуваних вузлів і не перевірки їх знову.
Це код qiao після модифікації:

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)

Вихід програми буде:

[1, 4, 7, 11, 13]

Без повторних перевірок.


6
Це може бути корисно використовувати collections.dequeдля queuelist.pop (0), які здійснюють O(n)рухи пам'яті. Також, для нащадків, якщо ви хочете зробити DFS, просто встановіть, path = queue.pop()у цьому випадку змінна queueнасправді діє як stack.
Суджі

11

Дуже простий код. Ви продовжуєте додавати шлях щоразу, коли виявляєте вузол.

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
Я вважаю ваш код дуже читабельним, порівняно з іншими відповідями. Велике спасибі!
Мітько Русєв

8

Я думав, що спробую це кодувати для розваги:

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

Якщо ви хочете циклів, ви можете додати це:

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

після того, як ви побудували next_for_front. Наступне запитання, що робити, якщо графік містить петлі? Наприклад, якщо у вузла 1 був край, що з'єднується назад до себе? Що робити, якщо графік має кілька ребер, що йдуть між двома вузлами?
Роберт Кінг

1

Мені подобається перша відповідь @Qiao і додаток @ Or. Заради трохи меншої обробки я хотів би додати відповідь Ор.

У @ Або відповіді відстеження відвіданого вузла чудово. Ми також можемо дозволити програмі вийти раніше, ніж зараз є. У якийсь момент у циклі for для циклу current_neighbourдоведеться статиend , і як тільки це станеться, знайдеться найкоротший шлях і програма може повернутися.

Я б модифікував метод як слід, зверну пильну увагу на цикл 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)

Вихід та все інше будуть однаковими. Однак на обробку коду знадобиться менше часу. Це особливо корисно для великих графіків. Я сподіваюся, що це допоможе комусь у майбутньому.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.