Làm thế nào để theo dõi đường dẫn trong Tìm kiếm theo chiều rộng-Đầu tiên?


104

Làm cách nào để bạn theo dõi đường dẫn của Tìm kiếm theo chiều rộng-Đầu tiên, chẳng hạn như trong ví dụ sau:

Nếu đang tìm kiếm khóa 11, hãy trả về danh sách ngắn nhất nối từ 1 đến 11.

[1, 4, 7, 11]

6
Đó thực sự là một nhiệm vụ cũ mà tôi đã giúp một người bạn vào tháng trước, dựa trên Luật Kevin Bacon. Giải pháp cuối cùng của tôi rất cẩu thả, về cơ bản tôi đã thực hiện một tìm kiếm Breadth-first khác để "tua lại" và quay lại. Tôi không thể tìm ra một giải pháp tốt hơn.
Christopher Markieta

21
Thông minh. Tôi coi việc xem lại một vấn đề cũ trong nỗ lực tìm ra câu trả lời tốt hơn là một đặc điểm đáng ngưỡng mộ ở một kỹ sư. Chúc các bạn thành công trong học tập và sự nghiệp.
Peter Rowell

1
Cảm ơn vì lời khen ngợi, tôi chỉ tin rằng nếu tôi không học nó bây giờ, tôi sẽ phải đối mặt với vấn đề tương tự một lần nữa.
Christopher Markieta

Câu trả lời:


194

Trước tiên, bạn nên xem tại http://en.wikipedia.org/wiki/Breadth-first_search .


Dưới đây là một triển khai nhanh chóng, trong đó tôi đã sử dụng một danh sách danh sách để đại diện cho hàng đợi các đường dẫn.

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

Một cách tiếp cận khác sẽ là duy trì một ánh xạ từ mỗi nút đến nút cha của nó và khi kiểm tra nút liền kề, hãy ghi lại nút cha của nó. Khi tìm kiếm xong, chỉ cần truy xuất ngược theo ánh xạ gốc.

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

Các mã trên dựa trên giả định rằng không có chu kỳ.


2
Thật tuyệt vời! Quá trình suy nghĩ của tôi khiến tôi tin tưởng vào việc tạo ra một số loại bảng hoặc ma trận, tôi vẫn chưa tìm hiểu về đồ thị. Cảm ơn bạn.
Christopher Markieta

Tôi cũng đã thử sử dụng phương pháp truy tìm ngược mặc dù cách này có vẻ sạch hơn nhiều. Có thể tạo một biểu đồ nếu bạn chỉ biết điểm bắt đầu và điểm kết thúc nhưng không có nút nào ở giữa? Hoặc thậm chí một cách tiếp cận khác ngoài đồ thị?
Christopher Markieta

@ChristopherM Tôi không hiểu câu hỏi của bạn :(
qiao

1
Có thể điều chỉnh thuật toán đầu tiên để nó sẽ trả về tất cả các đường dẫn từ 1 đến 11 (giả sử có nhiều hơn một) không?
Maria Ines Parnisari

1
Bạn nên sử dụng collection.deque thay vì danh sách. list.pop (0) 's phức tạp là O (n) trong khi deque.popleft () là O (1)
Omar_0x80

23

Tôi thích câu trả lời đầu tiên của qiao rất nhiều! Điều duy nhất còn thiếu ở đây là đánh dấu các đỉnh là đã thăm.

Tại sao chúng ta cần làm điều đó?
Hãy tưởng tượng rằng có một nút số 13 khác được kết nối từ nút 11. Bây giờ mục tiêu của chúng ta là tìm nút 13.
Sau khi chạy một chút, hàng đợi sẽ trông như thế này:

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

Lưu ý rằng có HAI đường dẫn với nút số 10 ở cuối.
Có nghĩa là các đường dẫn từ nút số 10 sẽ được kiểm tra hai lần. Trong trường hợp này, nó trông không tệ lắm vì nút số 10 không có con nào .. Nhưng nó có thể thực sự tồi tệ (thậm chí ở đây chúng tôi sẽ kiểm tra nút đó hai lần mà không có lý do gì ..)
Nút số 13 không ở trong những đường dẫn đó để chương trình sẽ không quay lại trước khi đến đường dẫn thứ hai có nút số 10 ở cuối..Và chúng tôi sẽ kiểm tra lại ..

Tất cả những gì chúng tôi còn thiếu là một bộ để đánh dấu các nút đã truy cập và không phải kiểm tra lại chúng ..
Đây là mã của qiao sau khi sửa đổi:

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)

Đầu ra của chương trình sẽ là:

[1, 4, 7, 11, 13]

Nếu không có sự kiểm tra lại ma thuật ..


6
Nó có thể là hữu ích để sử dụng collections.dequecho queuenhư list.pop (0) phải chịu O(n)phong trào nhớ. Ngoài ra, vì lợi ích của hậu thế, nếu bạn muốn làm DFS, chỉ cần đặt path = queue.pop()trong trường hợp đó biến queuethực sự hoạt động giống như a stack.
Sudhi

11

Mã rất dễ dàng. Bạn tiếp tục thêm đường dẫn mỗi khi bạn phát hiện ra một nút.

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
Tôi thấy mã của bạn rất dễ đọc, so với các câu trả lời khác. Cảm ơn rât nhiều!
Mitko Rusev

8

Tôi nghĩ tôi sẽ thử viết mã này cho vui:

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

Nếu bạn muốn chu kỳ, bạn có thể thêm cái này:

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

sau khi bạn đã xây dựng next_for_front. Một câu hỏi tiếp theo, điều gì sẽ xảy ra nếu biểu đồ chứa các vòng lặp? Ví dụ: nếu nút 1 có một cạnh kết nối trở lại chính nó? Điều gì sẽ xảy ra nếu đồ thị có nhiều cạnh đi giữa hai nút?
robert king

1

Tôi thích cả câu trả lời đầu tiên của @Qiao và phần bổ sung của @ Or. Để tiện cho việc xử lý bớt một chút, tôi muốn thêm vào câu trả lời của Or.

Trong câu trả lời của @ Or, việc theo dõi nút đã truy cập là rất tốt. Chúng tôi cũng có thể cho phép chương trình thoát sớm hơn so với hiện tại. Tại một thời điểm nào đó trong vòng lặp for, giá trị current_neighboursẽ phải là end, và khi điều đó xảy ra, con đường ngắn nhất sẽ được tìm thấy và chương trình có thể trả về.

Tôi sẽ sửa đổi phương thức như sau, hãy chú ý đến vòng lặp 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)

Đầu ra và mọi thứ khác sẽ giống nhau. Tuy nhiên, mã sẽ mất ít thời gian hơn để xử lý. Điều này đặc biệt hữu ích trên các đồ thị lớn hơn. Tôi hy vọng điều này sẽ giúp ai đó trong tương lai.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.