เนื่องจากการใช้งาน DFS แบบไม่เรียกซ้ำที่มีอยู่ในคำตอบนี้ดูเหมือนจะใช้งานไม่ได้ฉันขอให้ฉันระบุสิ่งที่ใช้งานได้จริง
ฉันเขียนสิ่งนี้ใน Python เพราะฉันพบว่ารายละเอียดการใช้งานค่อนข้างอ่านได้และไม่กระจาย (และเนื่องจากมีyield
คีย์เวิร์ดที่ใช้งานง่ายสำหรับการใช้งานเครื่องกำเนิดไฟฟ้า ) แต่มันควรจะง่ายพอที่จะพอร์ตไปยังภาษาอื่น ๆ
# a generator function to find all simple paths between two nodes in a
# graph, represented as a dictionary that maps nodes to their neighbors
def find_simple_paths(graph, start, end):
visited = set()
visited.add(start)
nodestack = list()
indexstack = list()
current = start
i = 0
while True:
# get a list of the neighbors of the current node
neighbors = graph[current]
# find the next unvisited neighbor of this node, if any
while i < len(neighbors) and neighbors[i] in visited: i += 1
if i >= len(neighbors):
# we've reached the last neighbor of this node, backtrack
visited.remove(current)
if len(nodestack) < 1: break # can't backtrack, stop!
current = nodestack.pop()
i = indexstack.pop()
elif neighbors[i] == end:
# yay, we found the target node! let the caller process the path
yield nodestack + [current, end]
i += 1
else:
# push current node and index onto stacks, switch to neighbor
nodestack.append(current)
indexstack.append(i+1)
visited.add(neighbors[i])
current = neighbors[i]
i = 0
รหัสนี้เก็บสแต็กแบบขนานสองสแต็ก: อันหนึ่งมีโหนดก่อนหน้าในพา ธ ปัจจุบันและอีกอันที่มีดัชนีเพื่อนบ้านปัจจุบันสำหรับแต่ละโหนดในโหนดสแต็ก (เพื่อให้เราสามารถกลับมาทำซ้ำผ่านเพื่อนบ้านของโหนดเมื่อเราป๊อปกลับ กอง) ฉันสามารถใช้คู่ (โหนดดัชนี) สแต็กเดียวได้ดีพอ ๆ กัน แต่ฉันคิดว่าวิธีการสองสแต็กจะอ่านได้ง่ายกว่าและอาจใช้งานได้ง่ายกว่าสำหรับผู้ใช้ภาษาอื่น
รหัสนี้ยังใช้visited
ชุดแยกต่างหากซึ่งมีโหนดปัจจุบันและโหนดใด ๆ บนสแต็กเสมอเพื่อให้ฉันตรวจสอบได้อย่างมีประสิทธิภาพว่าโหนดนั้นเป็นส่วนหนึ่งของเส้นทางปัจจุบันหรือไม่ หากภาษาของคุณมีโครงสร้างข้อมูล "ชุดสั่งซื้อ" ที่ให้ทั้งการดำเนินการพุช / ป๊อปแบบสแต็กที่มีประสิทธิภาพและการสืบค้นข้อมูลสมาชิกที่มีประสิทธิภาพคุณสามารถใช้สิ่งนั้นสำหรับโหนดสแต็กและกำจัดvisited
ชุดแยก
หรือหากคุณใช้คลาส / โครงสร้างที่เปลี่ยนแปลงได้ที่กำหนดเองสำหรับโหนดของคุณคุณสามารถเก็บแฟล็กบูลีนไว้ในแต่ละโหนดเพื่อระบุว่ามีการเยี่ยมชมเป็นส่วนหนึ่งของเส้นทางการค้นหาปัจจุบันหรือไม่ แน่นอนว่าวิธีนี้จะไม่อนุญาตให้คุณทำการค้นหาสองครั้งบนกราฟเดียวกันพร้อมกันหากคุณต้องการทำเช่นนั้นด้วยเหตุผลบางประการ
นี่คือรหัสทดสอบบางส่วนที่แสดงให้เห็นว่าฟังก์ชันที่ระบุข้างต้นทำงานอย่างไร:
# test graph:
# ,---B---.
# A | D
# `---C---'
graph = {
"A": ("B", "C"),
"B": ("A", "C", "D"),
"C": ("A", "B", "D"),
"D": ("B", "C"),
}
# find paths from A to D
for path in find_simple_paths(graph, "A", "D"): print " -> ".join(path)
การรันโค้ดนี้บนกราฟตัวอย่างที่กำหนดจะสร้างผลลัพธ์ต่อไปนี้:
A -> B -> C -> ง
A -> B -> ง
A -> C -> B -> ง
ก -> ค -> ง
โปรดทราบว่าในขณะที่กราฟตัวอย่างนี้ไม่มีการกำหนดทิศทาง (เช่นขอบทั้งหมดไปทั้งสองทาง) อัลกอริทึมยังใช้กับกราฟที่กำหนดทิศทางโดยพลการ ตัวอย่างเช่นการลบC -> B
ขอบ (โดยการลบออกB
จากรายการเพื่อนบ้านของC
) ให้ผลลัพธ์เดียวกันยกเว้นพา ธ ที่สาม ( A -> C -> B -> D
) ซึ่งเป็นไปไม่ได้อีกต่อไป
ps เป็นเรื่องง่ายที่จะสร้างกราฟซึ่งอัลกอริทึมการค้นหาแบบง่ายเช่นนี้ (และอื่น ๆ ที่ระบุในเธรดนี้) ทำงานได้ไม่ดีนัก
ตัวอย่างเช่นพิจารณาภารกิจในการค้นหาเส้นทางทั้งหมดจาก A ถึง B บนกราฟที่ไม่ได้กำหนดทิศทางโดยที่โหนดเริ่มต้น A มีเพื่อนบ้านสองตัว: โหนดเป้าหมาย B (ซึ่งไม่มีเพื่อนบ้านอื่นนอกจาก A) และโหนด C ที่เป็นส่วนหนึ่งของกลุ่มของn +1 โหนดดังนี้:
graph = {
"A": ("B", "C"),
"B": ("A"),
"C": ("A", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O"),
"D": ("C", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O"),
"E": ("C", "D", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O"),
"F": ("C", "D", "E", "G", "H", "I", "J", "K", "L", "M", "N", "O"),
"G": ("C", "D", "E", "F", "H", "I", "J", "K", "L", "M", "N", "O"),
"H": ("C", "D", "E", "F", "G", "I", "J", "K", "L", "M", "N", "O"),
"I": ("C", "D", "E", "F", "G", "H", "J", "K", "L", "M", "N", "O"),
"J": ("C", "D", "E", "F", "G", "H", "I", "K", "L", "M", "N", "O"),
"K": ("C", "D", "E", "F", "G", "H", "I", "J", "L", "M", "N", "O"),
"L": ("C", "D", "E", "F", "G", "H", "I", "J", "K", "M", "N", "O"),
"M": ("C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "N", "O"),
"N": ("C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "O"),
"O": ("C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N"),
}
เป็นเรื่องง่ายที่จะเห็นว่าเส้นทางเดียวระหว่าง A และ B เป็นเส้นทางตรง แต่ DFS ที่ไร้เดียงสาที่เริ่มต้นจากโหนด A จะเสียเวลา O ( n !) ไปอย่างไร้ประโยชน์ในการสำรวจเส้นทางภายในกลุ่มแม้ว่าจะชัดเจน (สำหรับมนุษย์) ก็ตาม ไม่มีเส้นทางใดที่สามารถนำไปสู่ B ได้
นอกจากนี้เรายังสามารถสร้างDAG ที่มีคุณสมบัติคล้ายกันได้เช่นโดยให้โหนดเริ่มต้น A เชื่อมต่อโหนดเป้าหมาย B และอีกสองโหนด C 1และ C 2ซึ่งทั้งสองโหนดเชื่อมต่อกับโหนด D 1และ D 2ซึ่งทั้งสองเชื่อมต่อกับ E 1และ E 2และอื่น ๆ สำหรับnชั้นของโหนดที่จัดเรียงเช่นนี้การค้นหาอย่างไร้เดียงสาสำหรับเส้นทางทั้งหมดจาก A ถึง B จะทำให้เสียเวลา O (2 n ) ในการตรวจสอบจุดตายที่เป็นไปได้ทั้งหมดก่อนที่จะยอมแพ้
แน่นอนว่าการเพิ่มขอบให้กับโหนดเป้าหมาย B จากโหนดใดโหนดหนึ่งในกลุ่ม (นอกเหนือจาก C) หรือจากเลเยอร์สุดท้ายของ DAG จะสร้างเส้นทางที่เป็นไปได้จำนวนมากแบบทวีคูณจาก A ถึง B และ a อัลกอริธึมการค้นหาในพื้นที่อย่างแท้จริงไม่สามารถบอกล่วงหน้าได้ว่าจะพบขอบดังกล่าวหรือไม่ ดังนั้นในแง่หนึ่งความไวของผลลัพธ์ที่ไม่ดีของการค้นหาที่ไร้เดียงสาดังกล่าวเกิดจากการที่พวกเขาไม่รับรู้ถึงโครงสร้างทั่วโลกของกราฟ
แม้ว่าจะมีวิธีการก่อนการประมวลผลหลายวิธี (เช่นการกำจัดโหนดลีฟซ้ำ ๆ การค้นหาตัวคั่นจุดยอดโหนดเดียว ฯลฯ ) ที่สามารถใช้เพื่อหลีกเลี่ยง "จุดสิ้นสุดของเวลาเอกซ์โพเนนเชียล" บางส่วนได้ แต่ฉันไม่ทราบว่ามีทั่วไป เคล็ดลับการประมวลผลล่วงหน้าที่สามารถกำจัดได้ในทุกกรณี วิธีแก้ปัญหาทั่วไปคือการตรวจสอบในทุกขั้นตอนของการค้นหาว่ายังสามารถเข้าถึงโหนดเป้าหมายได้หรือไม่ (โดยใช้การค้นหาย่อย) และย้อนรอยก่อนหากไม่เป็นเช่นนั้น - แต่อนิจจานั่นจะทำให้การค้นหาช้าลงอย่างมาก (ที่แย่ที่สุด ตามสัดส่วนของขนาดของกราฟ) สำหรับกราฟจำนวนมากที่ไม่มีจุดตายทางพยาธิวิทยาดังกล่าว