گراف یعنی گرهها و یالهایی که آنها را به هم وصل میکنند، و برخلاف درخت، مسیرها اجازه دارند برگردند. سادهترین شکل نوشتنش در پایتون یک دیکشنری است که هر کلید یک گره است و مقدارش فهرست همسایهها:
{"tajrish": ["gheytarieh"], "gheytarieh": ["tajrish", "shahid-sadr"]}- یال دوطرفه یعنی هر دو سر باید همدیگر را در فهرستشان داشته باشند
- یال یکطرفه یعنی فقط یکی از آن دو سطر نوشته میشود
روی یک نقشه شش ایستگاهی که آخری به اولی برمیگردد، پیمایش سطحاول با یک مجموعه دیدهشدهها ۶ گره را میبیند و تمام میشود. همان پیمایش بدون آن مجموعه، وقتی شمارنده را روی دو هزار متوقف کردیم هنوز در حال کار بود. تفاوت یک سطر است: if w in seen: continue. پیمایش گراف بدون مجموعه دیدهشدهها روی حلقه پایان نمیگیرد، و این نه یک بهینهسازی است و نه یک احتیاط؛ شرط درستی است.
حلقه چیزی است که درخت نمیتواند داشته باشد
پیمایش عمقاول روی یک درخت هیچوقت خودش را تکرار نمیکند، چون هر گره یک والد دارد و راه برگشتی نیست. همان کد روی گراف بینهایت میچرخد. برای همین است که هر کدی که از درخت به گراف منتقل میشود باید یک مجموعه بگیرد، و برای همین است که این باگ همیشه بعد از تغییر ساختار داده پیدا میشود نه هنگام نوشتنش. تفاوت این دو شکل در سلسلهمراتب، و گشتن در آن بدون خواندن همهاش است.
دو پیمایش نامدار، و کاری که هرکدام واقعا میکنند: سطحاول با یک صف پیش میرود و اولین باری که به گرهای میرسد از کوتاهترین مسیر رسیده، پس برای کمترین تعداد ایستگاه همیشه همین درست است. عمقاول با یک پشته پیش میرود و کوتاهترین مسیر را تضمین نمیکند؛ کاری که خوب انجام میدهد رسیدن به همه گرهها و پیدا کردن اجزای جدا از هم است.
مسیر را نگه دارید، نه فقط اینکه رسیدید
پیمایشی که فقط True برمیگرداند نصف کار را کرده. با یک دیکشنری کنارش که برای هر گره ثبت میکند از کدام گره به آن رسیدید، در پایان میشود از مقصد رو به عقب راه رفت و مسیر کامل را ساخت. همان دیکشنری هم جای مجموعه دیدهشدهها را میگیرد، چون هر گرهی که در آن هست یک بار دیده شده است.
اگر یالها وزن داشته باشند، مثلا زمان بهجای تعداد ایستگاه، سطحاول دیگر جواب درست نمیدهد و ظرفی که لازم میشود صف اولویت است: آخرین ورودی اول، اولین ورودی اول، و جای هرکدام.
جایی که کار گیر میکند، دادهای است که فکر میکنید درخت است
هر جدولی که یک ستون شناسه والد دارد در نگاه اول درخت است. در عمل، همان جدول بعد از چند سال ویرایش دستی یک حلقه پیدا میکند: دستهای که والدش نوه خودش شده، معمولا با یک جابهجایی در پنل مدیریت که هیچ بررسیای نداشت. کدی که منو را میسازد تا آن روز درست کار میکرد و از آن روز سرور را بیصدا اشغال میکند، چون هیچ خطایی نمیدهد و فقط تمام نمیشود. اگر دادهتان از دست آدم میآید، پیمایش را از همان اول با مجموعه دیدهشدهها بنویسید، حتی وقتی مطمئنید درخت است.
ابزار، و سالی که این حرف درست است
در سال ۲۰۲۶ برای گرافهای کوچک، دیکشنری و collections.deque در کتابخانه استاندارد کافیاند و رایگان؛ صف پیمایش سطحاول دقیقا همان ظرفی است که از یک سر اضافه و از سر دیگر برداشته میشود. برای گرافهای واقعی، networkx بسته جاافتاده است و کوتاهترین مسیر و اجزای همبند و تشخیص حلقه را آماده دارد؛ متنباز است اما باید از مخزن نصب شود، و در جایی که نصب وابستگی خودش یک مرحله جداست، همان صف دستی گاهی انتخاب درستتری است.
پرسش بعدی این است که وقتی گراف بزرگ شد، هزینه این پیمایش چطور با تعداد گره و یال بالا میرود، که شمردن کار، وقتی ورودی بزرگ میشود جوابش را دارد. و همان مجموعه دیدهشدهها که همه چیز به آن بسته است، از جستوجو با کلید، و عضویت در زمان ثابت میآید.