الگوریتم جستجوی اول عمق
در نظریهٔ گراف، جستجوی عمق اول (به انگلیسی: Depth-first Search، بهاختصار DFS) یک الگوریتم پیمایش گراف است که برای پیمایش یا جستجوی یک درخت یا یک گراف به کار میرود.
رده | الگوریتم جستجو |
---|---|
ساختمان داده | گراف (ساختار داده) |
کارایی بدترین حالت | for explicit graphs traversed without repetition, for implicit graphs with branching factor b searched to depth d |
پیچیدگی فضایی | if entire graph is traversed without repetition, O(longest path length searched)=for implicit graphs without elimination of duplicate nodes |
پیمایش گراف و پیمایش درخت |
---|
هرس آلفا بتا |
جستار وابسته |
برنامهریزی پویا |
استراتژی جستجوی عمق اول برای پیمایش گراف، همانطور که از نامش پیداست «جستجوی عمیقتر در گراف تا زمانی که امکان دارد» است.
چگونه کار میکند؟
الگوریتم از ریشه شروع میکند (در گرافها یا درختهای بدون ریشه راس دلخواهی به عنوان ریشه انتخاب میشود) و در هر مرحله همسایههای رأس جاری را از طریق یالهای خروجی رأس جاری به ترتیب بررسی کرده و به محض روبهرو شدن با همسایهای که قبلاً دیده نشده باشد، به صورت بازگشتی برای آن رأس به عنوان رأس جاری اجرا میشود. در صورتی که همهٔ همسایهها قبلاً دیده شده باشند، الگوریتم عقبگرد میکند و اجرای الگوریتم برای رأسی که از آن به رأس جاری رسیدهایم، ادامه مییابد. به عبارتی الگوریتم تا آنجا که ممکن است، به عمق بیشتر و بیشتر میرود و در مواجهه با بنبست عقبگرد میکند. این فرایند تا زمانی که همهٔ رأسهای قابل دستیابی از ریشه دیده شوند ادامه مییابد.
همچنین در مسائلی که حالات مختلف متناظر با رئوس یک گرافاند و حل مسئله مستلزم یافتن رأس هدف با خصوصیات مشخصی است، جستجوی عمق اول به صورت غیرخلاق عمل میکند. بدینترتیب که هر دفعه الگوریتم به اولین همسایهٔ یک رأس در گراف جستجو و در نتیجه هر دفعه به عمق بیشتر و بیشتر در گراف میرود تا به رأسی برسد که همهٔ همسایگانش دیده شدهاند که در حالت اخیر، الگوریتم به اولین رأسی بر میگردد که همسایهٔ داشته باشد که هنوز دیده نشده باشد. این روند تا جایی ادامه مییابد که رأس هدف پیدا شود یا احتمالاً همهٔ گراف پیمایش شود. البته پیادهسازی هوشمندانهٔ الگوریتم با انتخاب ترتیب مناسب برای بررسی همسایههای دیده نشدهٔ رأس جاری به صورتی که ابتدا الگوریتم به بررسی همسایهای بپردازد که به صورت موضعی و با انتخابی حریصانه به رأس هدف نزدیکتر است، امکانپذیر خواهد بود که معمولاً در کاهش زمان اجرا مؤثر است.
از دیدگاه عملی، برای اجرای الگوریتم، از یک پشته استفاده میشود. بدین ترتیب که هر بار با ورود به یک رأس دیده نشده، آن رأس را در پشته قرار میدهیم و هنگام عقبگرد رأس را از پشته حذف میکنیم؛ بنابراین در تمام طول الگوریتم اولین عنصر پشته رأس در حال بررسی است. جزئیات پیادهسازی در ادامه خواهد آمد.
وقتی در گرافهای بزرگی جستجو میکنیم که امکان ذخیرهٔ کامل آنها به علت محدودیت حافظه وجود ندارد، در صورتی که طول مسیر پیمایش شده توسط الگوریتم که از ریشه شروع شده، خیلی بزرگ شود، الگوریتم با مشکل مواجه خواهد شد. در واقع این راهحل ساده که «رئوسی را که تا به حال دیدهایم ذخیره کنیم» همیشه کار نمیکند. چرا که ممکن است حافظهٔ کافی برای این کار نداشته باشیم. البته این مشکل با محدود کردن عمق جستجو در هر بار اجرای الگوریتم حل میشود که در نهایت به الگوریتم جستجوی عمق اول عمیقکننده تکراری خواهد انجامید.
الگوریتم
پیمایش با انتخاب رأس به عنوان ریشه آغاز میشود. به عنوان یک رأس دیده شده برچسب میخورد. رأس دلخواه از همسایگان انتخاب شده و الگوریتم به صورت بازگشتی از به عنوان ریشه ادامه مییابد. از این پس در هر مرحله وقتی در رأسی مانند قرار گرفتیم که همهٔ همسایگانش دیده شدهاند، اجرای الگوریتم را برای آن رأس خاتمه میدهیم. حال اگر بعد از اجرای الگوریتم با ریشهٔ همهٔ همسایگان برچسب خورده باشند، الگوریتم پایان مییابد. در غیر این صورت رأس دلخواه از همسایگان را که هنوز برچسب نخورده انتخاب میکنیم و جستجو را به صورت بازگشتی از به عنوان ریشه ادامه میدهیم. این روند تامادامی که همهٔ همسایگان برچسب نخوردهاند ادامه مییابد.
البته پیمایش گراف برای تأمین هدفی صورت میگیرد. بر این اساس برای انعطافپذیر ساختن الگوریتم در قبال کاربردهای مختلف، دو نوع عملیات preWORK و postWORK را به همراهِ بازدید از هر رأس یا یال انجام میدهیم، که preWORK در زمان برچسب خوردنِ رأسِ در حال بازدید، و postWORK بعد از بررسی هر یالِ خروجی از رأسِ در حال بازدید انجام خواهد شد. هر دوی این عملیات وابسته به هدفِ استفاده از الگوریتم، مشخص خواهند شد.
الگوریتم بازگشتی جستجوی اول عمق به صورت زیر است. آرایه یک بعدی Visited تعیین میکند آیا راسی قبلاً ملاقات شدهاست یا خیر. اگر راس vi ملاقات شود Visited[i] برابر با یک میشود.
1 DFS (int v) 2 { 3 int w 4 Visited[v]:=1 5 For (each vertex w adjacent to v) 6 If (not visited[w]) then 7 DFS(w) 8 End if 9 End For 10 }
ترتیب ملاقات رئوس را DFN مینامند.
پیادهسازی
یک نسخهٔ بازگشتی الگوریتم به این قرار است:
1 Algorithm DFS(G,v) 2 Input: G=(V,E), v(a vetex of G) 3 Output: depends on the application 4 begin 5 mark v 6 perform preWORK on v 7 for all edge (v,w) do 8 if w is unmarked then 9 DFS(G,w) 10 perform postWORK on (v,w) 11 end
و یک نسخهٔ غیر بازگشتی الگوریتم به این قرار است:
1 Algorithm DFS(G,v) 2 Input: G=(V,E), v(a vertex of G) 3 Output: depends on the application 4 begin 5 mark v 6 perform preWORK on v 7 Edge = v.First 8 push v and Edge on the top of the stack 9 Parent = v 10 while the stack is not empty do 11 remove Edge from the top of the stack 12 while Edge is not nil do 13 Child = Edge->Vertex 14 if Child is unmarked then 15 mark Child 16 perform preWORK on Child 17 push Edge->Next to the top of the stack 18 Edge = Child.First 19 Parent = Child 20 push Parebtto the top of the stack 21 else 22 perform postWORK on (Parent,Child) 23 Edge = Edge->Next 24 remove Child from the top of the stack 25 if the stack is not empty then 26 let Edge and Parent be at the top of the stack 27 perform postWORK on (Parent,Child) 28 end
که v.First به معنی اولین یال متصل به v و Edge->Vertex به معنی رأس انتهای Edge و Edge->Next به معنی یال بعدی متصل به ابتدای Edge است.
پیچیدگی زمانی
مشخص است که الگوریتم، هر یال در گراف بدون جهت را دقیقاً دو بار (یک بار به به هنگام بررسی هر یک از دو انتها) و هر یال در گراف جهتدار را دقیقاً یک بار پیمایش میکند. همچنین هر رأس قابل دسترسی از ریشه دقیقاً یک بار بازدید خواهد شد. پس با فرض همبند بودن گراف و اینکه preWORK و postWORK در (1)O انجام شوند، پیچیدگی زمانی جستجوی عمق اول (|O(|V|+|E خواهد بود
تشخیص وجود دور
یکی از کاربردهای این الگوریتم تشخیص وجود دور است. در گراف ساده، برای این کار رویهٔ عادی جستجو عمق اول را در پیش میگیریم، تنها با این تفاوت که یک لیست نشانه و یک لیست پدر میسازیم که در ابتدا برای هر کدام از رئوس به ترتیب مقادیر نادرست (به انگلیسی: False) و اشارهگر تهی (به انگلیسی: Null) دارد، زیرا در ابتدای امر هیچکدام از رئوس دیده نشدهاند و پدر آنها در جنگل فراگیری که قرار است این الگوریتم بسازد، نامشخص است.
سپس در هر مرحله بررسی میشود که اگر رأس کنونی v که به وسیله رأس u پیدا شدهاست، قبلاً نشانهگذاری شده بود و خود v پدر u نبود، در این صورت یک یال بازگشت (به انگلیسی: Back Edge) پیدا شدهاست که نشان میدهد گراف دور دارد. در غیر این صورت رأس v نشانه گذاری شده و پدر آن رأس u میشود. در صورتی که تا پایان مراحل اجرا هیچ یال بازگشتای پیدا نشد، یعنی گراف بدون دور است.
در زیر پیادهسازی این روش به وسیله زبان پایتون آماده است.
class Graph:
def __init__(self, number_of_nodes):
self.number_of_nodes = number_of_nodes
self.node_neighbours = [[] for i in range(self.number_of_nodes)]
def get_input(self): # vertex indexes start from zero. (not one)
for i in range(len(self.node_neighbours)):
self.node_neighbours[i].extend(list(map(int, input().split()))) # get and add neighbour(s) of i-th vertex
def dfs(self, node_index, parent_index, mark_lst, parent_lst):
if mark_lst[node_index]:
if parent_index is not None and node_index != parent_lst[parent_index]:
# indicates there exist a back-edge to a previously seen vertex (node_index) which is not the parent
return True
return False
mark_lst[node_index], parent_lst[node_index] = True, parent_index
for neighbour_index in self.node_neighbours[node_index]:
if self.dfs(neighbour_index, node_index, mark_lst, parent_lst):
return True
return False
def find_cycle(self):
mark, parent = [False] * self.number_of_nodes, [None] * self.number_of_nodes
for i in range(self.number_of_nodes):
if not mark[i]:
if self.dfs(i, None, mark, parent):
return True
return False
کاربردها
- پیدا کردن مؤلفههای همبندی
- مرتبسازی موضعی (Topological Sort)
- پیدا کردن اجزای قویا همبند گراف جهتدار
- پیدا کردن مؤلفههای دوهمبند یالی و رأسی
- پیدا کردن پل
- الگوریتم انباشتن سیلابی[1]
- پیدا کردن دور در گراف[2]
- حل کردن معماهایی همچون ماز و آنهایی که نیاز به بررسی همه یا بخش بزرگی از حالات ممکن دارند.
- پیدا کردن گراف دوهمبند
پیوند به بیرون
در ویکیانبار پروندههایی دربارهٔ الگوریتم جستجوی اول عمق موجود است. |
- Open Data Structures - Section 12.3.2 - Depth-First-Search
- Depth-First Explanation and Example
- C++ Boost Graph Library: Depth-First Search
- Depth-First Search Animation (for a directed graph)
- Depth First and Breadth First Search: Explanation and Code
- QuickGraph, depth first search example for .Net
- Depth-first search algorithm illustrated explanation (Java and C++ implementations)
- YAGSBPL – A template-based C++ library for graph search and planning
- توضیح و مثالهایی از جستجوی عمق اول
- انیمیشن جستجوی عمق اول برای گرافهای جهتدار
- توضیح و پیادهسازی جستجوی عمق اول و جستجوی سطح اول
- Dfs Applet
منابع
- http://www.7khatcode.com/6431/الگوریتم-flood-filll?show=6431#q6431
- The Algorithm Design Manual 2008 ☀ISBN 978-1848000698
- Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, and Clifford Stein. Introduction to Algorithms, Second Edition. MIT Press and McGraw-Hill, 2001. ISBN 0-262-03293-7.
- Udi Manber. Introduction to Algorithms — A Creative Approach. MIT Press and Addison-Wesley, 1989. ISBN 0-201-12037-2.
- Donald E. Knuth. The Art Of Computer Programming Vol 1., Third Edition. Addison-Wesley, 1997. ISBN 0-201-89683-4.
- Douglas B. West. Introduction to Graph Theory, Second Edition. Prentice Hall, 2001. ISBN 0-13-014400-2.