Karena implementasi DFS non-rekursif yang ada yang diberikan dalam jawaban ini tampaknya rusak, izinkan saya memberikan yang benar-benar berfungsi.
Saya telah menulis ini dengan Python, karena saya merasa cukup mudah dibaca dan tidak berantakan oleh detail implementasi (dan karena memiliki yield
kata kunci yang berguna untuk mengimplementasikan generator ), tetapi seharusnya cukup mudah untuk di-port ke bahasa lain.
# 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
Kode ini mempertahankan dua tumpukan paralel: satu berisi node sebelumnya di jalur saat ini, dan satu lagi berisi indeks tetangga saat ini untuk setiap node dalam tumpukan node (sehingga kita dapat melanjutkan iterasi melalui tetangga node saat kita memunculkannya kembali tumpukan). Saya bisa saja menggunakan satu tumpukan (node, indeks) pasangan dengan sama baiknya, tetapi saya pikir metode dua tumpukan akan lebih mudah dibaca, dan mungkin lebih mudah diimplementasikan untuk pengguna bahasa lain.
Kode ini juga menggunakan visited
set terpisah , yang selalu berisi node saat ini dan node apa pun di stack, agar saya dapat memeriksa secara efisien apakah node sudah menjadi bagian dari jalur saat ini. Jika bahasa Anda kebetulan memiliki struktur data "kumpulan berurutan" yang menyediakan operasi push / pop mirip tumpukan yang efisien dan kueri keanggotaan yang efisien, Anda dapat menggunakannya untuk tumpukan node dan membuang visited
kumpulan terpisah .
Alternatifnya, jika Anda menggunakan kelas / struktur kustom yang bisa berubah untuk node Anda, Anda bisa menyimpan bendera boolean di setiap node untuk menunjukkan apakah itu telah dikunjungi sebagai bagian dari jalur pencarian saat ini. Tentu saja, metode ini tidak akan membiarkan Anda menjalankan dua pencarian pada grafik yang sama secara paralel, jika Anda karena suatu alasan ingin melakukannya.
Berikut beberapa kode uji yang menunjukkan cara kerja fungsi yang diberikan di atas:
# 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)
Menjalankan kode ini pada grafik contoh yang diberikan menghasilkan keluaran sebagai berikut:
A -> B -> C -> D
A -> B -> D
A -> C -> B -> D
A -> C -> D
Perhatikan bahwa, meskipun grafik contoh ini tidak diarahkan (semua tepinya mengarah ke dua arah), algoritme juga berfungsi untuk grafik berarah sembarang. Misalnya, menghapus C -> B
tepi (dengan menghapus B
dari daftar tetangga C
) menghasilkan keluaran yang sama kecuali untuk jalur ketiga ( A -> C -> B -> D
), yang tidak lagi memungkinkan.
Ps. Sangat mudah untuk membuat grafik dimana algoritma pencarian sederhana seperti ini (dan algoritma lainnya yang diberikan dalam utas ini) berkinerja sangat buruk.
Misalnya, pertimbangkan tugas untuk menemukan semua jalur dari A ke B pada grafik yang tidak diarahkan di mana node awal A memiliki dua tetangga: node target B (yang tidak memiliki tetangga selain A) dan node C yang merupakan bagian dari sebuah klik dari n +1 node, seperti ini:
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"),
}
Sangat mudah untuk melihat bahwa satu-satunya jalur antara A dan B adalah jalur langsung, tetapi DFS naif yang dimulai dari node A akan membuang waktu O ( n !) Dengan sia-sia untuk menjelajahi jalur dalam klik, meskipun jelas (bagi manusia) bahwa tak satu pun dari jalur itu yang mungkin mengarah ke B.
Seseorang juga dapat membangun DAG dengan properti serupa, misalnya dengan memiliki simpul awal A menghubungkan simpul target B dan ke dua simpul lain C 1 dan C 2 , keduanya terhubung ke simpul D 1 dan D 2 , keduanya terhubung ke E 1 dan E 2 , dan seterusnya. Untuk n lapisan node yang diatur seperti ini, pencarian naif untuk semua jalur dari A ke B akan menghabiskan O (2 n ) untuk memeriksa semua kemungkinan jalan buntu sebelum menyerah.
Tentu saja, menambahkan tepi ke node target B dari salah satu node dalam klik (selain C), atau dari lapisan terakhir DAG, akan membuat sejumlah besar kemungkinan jalur secara eksponensial dari A ke B, dan Algoritma pencarian lokal murni tidak dapat benar-benar memberi tahu sebelumnya apakah ia akan menemukan keunggulan seperti itu atau tidak. Jadi, dalam arti tertentu, sensitivitas keluaran yang buruk dari penelusuran naif tersebut disebabkan oleh kurangnya kesadaran mereka tentang struktur global grafik.
Meskipun ada berbagai metode preprocessing (seperti menghilangkan node daun secara berulang, mencari pemisah simpul simpul tunggal, dll.) Yang dapat digunakan untuk menghindari beberapa "jalan buntu waktu eksponensial" ini, saya tidak tahu ada yang umum trik preprocessing yang bisa menghilangkannya dalam semua kasus. Solusi umum adalah memeriksa di setiap langkah pencarian apakah node target masih dapat dijangkau (menggunakan sub-pencarian), dan mundur lebih awal jika tidak - tapi sayangnya, itu akan sangat memperlambat pencarian (paling buruk , sebanding dengan ukuran grafik) untuk banyak grafik yang tidak mengandung jalan buntu patologis tersebut.