จะติดตามเส้นทางในการค้นหาแบบกว้าง - แรกได้อย่างไร?


104

คุณติดตามเส้นทางของ Breadth-First Search ได้อย่างไรในตัวอย่างต่อไปนี้:

หากค้นหาคีย์11ให้ส่งคืนรายการที่สั้นที่สุดที่เชื่อมต่อ 1 ถึง 11

[1, 4, 7, 11]

6
จริงๆแล้วมันเป็นงานเก่าที่ฉันช่วยเพื่อนคนหนึ่งเมื่อหลายเดือนก่อนตามกฎหมายของเควินเบคอน วิธีแก้ปัญหาสุดท้ายของฉันมันเลอะเทอะมากโดยพื้นฐานแล้วฉันทำการค้นหาแบบกว้างเป็นอันดับแรกเพื่อ "ย้อนกลับ" และย้อนกลับ ฉันไม่ต้องการหาทางออกที่ดีกว่านี้
Christopher Markieta

21
ยอดเยี่ยม. ฉันพิจารณาทบทวนปัญหาเก่าเพื่อพยายามหาคำตอบที่ดีกว่าเพื่อเป็นลักษณะที่น่าชื่นชมในวิศวกร ขอให้คุณมีความสุขในการเรียนและอาชีพการงาน
Peter Rowell

1
ขอบคุณสำหรับคำชมฉันเชื่อว่าถ้าฉันไม่เรียนรู้ตอนนี้ฉันจะต้องเจอกับปัญหาเดิมอีกครั้ง
Christopher Markieta

คำตอบ:


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
ยอดเยี่ยมมาก! กระบวนการคิดของฉันทำให้ฉันเชื่อในการสร้างตารางหรือเมทริกซ์บางประเภทฉันยังไม่ได้เรียนรู้เกี่ยวกับกราฟ ขอบคุณ.
Christopher Markieta

ฉันยังลองใช้วิธีการย้อนกลับแม้ว่าจะดูสะอาดกว่ามาก เป็นไปได้ไหมที่จะสร้างกราฟถ้าคุณรู้เพียงจุดเริ่มต้นและจุดสิ้นสุด แต่ไม่มีโหนดใดที่อยู่ระหว่าง หรือแม้กระทั่งแนวทางอื่นนอกจากกราฟ?
Christopher Markieta

@ChristopherM ฉันไม่เข้าใจคำถามของคุณ :(
qiao

1
เป็นไปได้หรือไม่ที่จะปรับอัลกอริทึมแรกเพื่อให้ส่งคืนเส้นทางทั้งหมดตั้งแต่ 1 ถึง 11 (สมมติว่ามีมากกว่าหนึ่ง)
Maria Ines Parnisari

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สำหรับqueueเป็น list.pop (0) ก่อให้เกิดO(n)การเคลื่อนไหวของหน่วยความจำ นอกจากนี้เพื่อประโยชน์ของลูกหลานหากคุณต้องการทำ DFS เพียงแค่ตั้งค่าpath = queue.pop()ในกรณีนี้ตัวแปรqueueจะทำหน้าที่เหมือนไฟล์stack.
Sudhi

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
ฉันพบว่ารหัสของคุณอ่านง่ายมากเมื่อเทียบกับคำตอบอื่น ๆ ขอบคุณมาก!
Mitko Rusev

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 มี edge เชื่อมกลับเข้าหาตัวเอง? จะเกิดอะไรขึ้นถ้ากราฟมีหลายขอบระหว่างสองโหนด?
robert king

1

ฉันชอบทั้งคำตอบแรกของ @Qiao และการเพิ่มของ @ หรือ เพื่อประโยชน์ในการประมวลผลที่น้อยลงเล็กน้อยฉันต้องการเพิ่มคำตอบของ Or

คำตอบของ In @ Or การติดตามโหนดที่เยี่ยมชมนั้นยอดเยี่ยมมาก นอกจากนี้เรายังสามารถอนุญาตให้โปรแกรมออกเร็วกว่าที่เป็นอยู่ได้ ในบางจุดของการวนซ้ำcurrent_neighbourจะต้องเป็นendและเมื่อสิ่งนั้นเกิดขึ้นจะพบเส้นทางที่สั้นที่สุดและโปรแกรมสามารถย้อนกลับได้

ฉันจะแก้ไขวิธีการดังต่อไปนี้ให้ความสนใจกับ for loop

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)

ผลลัพธ์และทุกอย่างจะเหมือนกัน อย่างไรก็ตามโค้ดจะใช้เวลาในการประมวลผลน้อยลง สิ่งนี้มีประโยชน์อย่างยิ่งกับกราฟขนาดใหญ่ ฉันหวังว่าสิ่งนี้จะช่วยใครบางคนในอนาคต

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.