يا جماعة الخير، السلام عليكم ورحمة الله. معكم أخوكم أبو عمر.
قبل عدة سنوات، كنت أعمل كمستشار تقني لشركة ناشئة في مجال الخدمات اللوجستية. كانت فكرتهم بسيطة ولامعة: تطبيق لتوصيل الطلبات في المدينة بأسرع وقت ممكن. تحمست للفكرة، وبدأنا العمل. كانت مهمتي الأساسية هي بناء “العقل” للنظام، أي الخوارزمية التي تحدد أفضل مسار للسائق من نقطة الاستلام إلى نقطة التسليم.
في البداية، قلت في نفسي: “الموضوع بسيط، ليش نعقّدها؟”. بنيت نظاماً يعتمد على ما نسميه “الخوارزمية الجشعة” (Greedy Algorithm). الفكرة كانت أن السائق عند كل تقاطع، يختار الطريق الأقصر الذي يليه مباشرةً. منطقي، مش هيك؟ казалось бы, логично, не так ли? (It seemed logical, didn’t it?). في الأيام الأولى، ومع عدد طلبات قليل، كانت الأمور تبدو على ما يرام. لكن مع نمو الشركة وزيادة تعقيد شبكة الطرق، بدأت الكارثة تتكشف.
بدأ السائقون يشتكون من أن النظام يوجههم إلى طرق طويلة بشكل غير منطقي. كانوا أحيانًا يدخلون في طرق فرعية قصيرة، ليكتشفوا أنها تؤدي إلى طريق رئيسي مزدحم يلتف حول نصف المدينة، بينما كان هناك طريق سريع مباشر لو أنهم تجاهلوا تلك “الفرصة” القصيرة في البداية. كانت الشركة تخسر وقوداً ووقتاً، والأهم من ذلك، ثقة العملاء. شعرت وقتها أنني بنيت متاهة رقمية، وأن مساراتي تتشابك في جحيم من الطرق الخاطئة التي تبدو “قصيرة” للوهلة الأولى. هنا، تذكرت محاضرة قديمة في الجامعة عن عالم هولندي عبقري اسمه إدسخر دايكسترا، وكانت تلك هي لحظة النجاة.
ما هي “الرسوم البيانية” (Graphs) أصلاً؟
قبل أن نغوص في حل دايكسترا، دعونا نرجع خطوة للوراء. لكي يفهم الكمبيوتر شبكة طرق أو أي شبكة أخرى، نحن بحاجة لتمثيلها بطريقة يفهمها. هذه الطريقة هي ما نسميه “الرسم البياني” أو Graph.
تخيل خريطة لعدة مدن. الرسم البياني يتكون ببساطة من شيئين:
- العُقد (Nodes/Vertices): وهي النقاط على الخريطة. في مثالنا، كل مدينة هي “عُقدة”. في شبكة كمبيوتر، كل جهاز هو “عُقدة”.
- الحواف (Edges): وهي الخطوط التي تصل بين هذه النقاط. في مثالنا، كل طريق بين مدينتين هو “حافة”.
لكن هذا ليس كل شيء. الطرق ليست متساوية؛ بعضها أطول من بعض، وبعضها يستغرق وقتاً أكثر بسبب الازدحام. هنا يأتي دور “الحواف الموزونة” (Weighted Edges)، حيث نعطي لكل حافة “وزناً” أو “تكلفة” (Cost). هذه التكلفة يمكن أن تكون مسافة (بالكيلومتر)، أو وقتاً (بالدقائق)، أو حتى تكلفة مالية (رسوم طريق). وهذا هو المفتاح لفهم مشكلتنا.
المشكلة: سحر “الطمع” الخادع
خوارزميتي الأولى كانت “جشعة” أو “طماعة” (Greedy). كانت تنظر فقط إلى الخطوة التالية المباشرة. إذا كان أمامي طريقان، واحد طوله 5 كم والآخر 7 كم، ستختار حتمًا الطريق ذا الـ 5 كم، دون أي اعتبار لما سيأتي بعده.
المشكلة أن هذا الطريق القصير قد يقودك إلى طريق طويل جداً لاحقاً. انظر لهذا المثال البسيط:
للوصول من النقطة A إلى النقطة D:
- المسار 1 (الجشع): A -> B (تكلفة 2) -> C (تكلفة 10) -> D (تكلفة 2). المجموع: 14. الخوارزمية الجشعة ستختار A -> B لأن تكلفتها (2) أقل من A -> C (التي تكلفتها 4).
- المسار 2 (الأمثل): A -> C (تكلفة 4) -> D (تكلفة 2). المجموع: 6.
الخوارزمية الجشعة قادتنا إلى مسار أطول بأكثر من الضعف! وهذا بالضبط ما كان يحدث في نظام التوصيل الخاص بي. كنا عالقين في “الأمثل محلياً” ونتجاهل “الأمثل عالمياً”.
المنقذ: دخول السيد إدسخر دايكسترا
في عام 1956، كان العالم الهولندي إدسخر دايكسترا يفكر في كيفية إيجاد أقصر طريق بين مدينتين في هولندا. في 20 دقيقة فقط وهو يحتسي القهوة مع زوجته، وضع أسس خوارزميته العبقرية التي لا نزال نستخدمها حتى اليوم في كل شيء تقريباً: من خرائط جوجل، إلى توجيه حزم البيانات على الإنترنت، وحتى في تصميم شبكات الذكاء الاصطناعي.
الفكرة الجوهرية لدايكسترا
فكرة دايكسترا ليست طماعة، بل هي “منهجية” و”متفائلة بحذر”. لا تتخذ قراراً وتنسى الباقي. بدلاً من ذلك، هي تحتفظ بقائمة من المسارات المحتملة وتستكشفها بذكاء. تقول الخوارزمية:
- ابدأ من نقطة البداية (المصدر). المسافة إليها هي صفر، والمسافة إلى كل النقاط الأخرى هي “لانهاية” (لأننا لم نصلها بعد).
- احتفظ بقائمة لـ “العُقد التي لم تتم زيارتها بعد”، واختر منها العُقدة ذات أقصر مسافة معروفة حالياً من المصدر.
- عند هذه العُقدة، انظر إلى كل جيرانها. لكل جار، احسب المسافة الجديدة (المسافة للوصول إلى العقدة الحالية + المسافة من العقدة الحالية إلى هذا الجار).
- إذا كانت هذه المسافة الجديدة أقصر من أي مسافة أخرى سجلناها لهذا الجار من قبل، قم بتحديثها.
- ضع علامة “تمت الزيارة” على العُقدة الحالية وأزلها من قائمة “لم تتم الزيارة”.
- كرر الخطوات من 2 إلى 5 حتى تتم زيارة كل العُقد أو حتى نصل إلى وجهتنا.
بهذه الطريقة، تضمن الخوارزمية أنها في كل خطوة، تكون المسافة التي سجلتها لأي عقدة تمت زيارتها هي بالفعل أقصر مسافة ممكنة للوصول إليها من المصدر. لا يوجد “يا ليتني اخترت الطريق الآخر!”.
مثال عملي مع الكود
هالحكي النظري جميل، لكن “الشغل بنعرفه من فعله”. خلينا نأخذ مثالاً عملياً ونكتب الكود. تخيل لدينا شبكة طرق بسيطة بين بعض المدن الفلسطينية (الأوزان هنا هي مجرد أرقام افتراضية للتوضيح).
- القدس -> أريحا (10)
- القدس -> رام الله (5)
- رام الله -> نابلس (15)
- أريحا -> نابلس (12)
- رام الله -> الخليل (20)
نريد إيجاد أقصر مسار من القدس إلى كل المدن الأخرى.
تمثيل الرسم البياني في بايثون
أولاً، سنمثل هذه الشبكة باستخدام قاموس (Dictionary) في بايثون:
graph = {
'القدس': {'أريحا': 10, 'رام الله': 5},
'أريحا': {'القدس': 10, 'نابلس': 12},
'رام الله': {'القدس': 5, 'نابلس': 15, 'الخليل': 20},
'نابلس': {'أريحا': 12, 'رام الله': 15},
'الخليل': {'رام الله': 20}
}
تطبيق خوارزمية دايكسترا
سنستخدم مكتبة heapq في بايثون، والتي توفر لنا بنية بيانات فعّالة جداً تسمى “طابور الأولوية” (Priority Queue) لاختيار العقدة ذات المسافة الأقل في كل مرة بكفاءة عالية.
import heapq
def dijkstra(graph, start_node):
# 1. التهيئة
# قاموس لتخزين أقصر مسافة من المصدر لكل عقدة
distances = {node: float('infinity') for node in graph}
distances[start_node] = 0
# طابور الأولوية لتخزين (المسافة, العقدة)
priority_queue = [(0, start_node)]
# قاموس لتتبع المسار
shortest_path_tree = {}
# 2. الحلقة الرئيسية
while priority_queue:
# استخراج العقدة ذات المسافة الأقصر
current_distance, current_node = heapq.heappop(priority_queue)
# إذا وجدنا مساراً أقصر بالفعل، نتجاهل هذا المسار
if current_distance > distances[current_node]:
continue
# 3. استكشاف الجيران
for neighbor, weight in graph[current_node].items():
distance = current_distance + weight
# 4. تحديث المسافة إذا وجدنا مساراً جديداً أقصر
if distance ".join(reversed(path))
print("nأقصر مسار من القدس إلى نابلس:")
print(get_path(path, 'القدس', 'نابلس'))
ماذا يفعل الكود؟
عند تشغيل هذا الكود، سيبدأ من “القدس” (المسافة 0). يرى أن لديه جارين: “أريحا” (مسافة 10) و”رام الله” (مسافة 5). سيختار “رام الله” لأنها الأقرب (5). من “رام الله”، يرى “نابلس” (5+15=20) و”الخليل” (5+20=25). الآن، أقرب عقدة غير مكتشفة هي “أريحا” (مسافتها 10 من البداية). من “أريحا”، يرى “نابلس” (10+12=22). انتظر! لقد سجلنا مسافة 20 للوصول إلى نابلس عبر رام الله. بما أن 20 ليست أقل من 22، فلن يقوم بتحديث المسار إلى نابلس. سيستمر هكذا حتى يجد أقصر مسار للجميع.
الناتج سيكون:
- القدس -> رام الله: 5
- القدس -> أريحا: 10
- القدس -> نابلس: 17 (عبر القدس -> أريحا -> نابلس، وليس عبر رام الله)
- القدس -> الخليل: 25 (عبر القدس -> رام الله -> الخليل)
لاحظ كيف أن الخوارزمية اكتشفت أن الطريق إلى نابلس عبر أريحا (10+12=22) هو أطول من الطريق عبر رام الله (5+15=20). لا، لحظة، هناك خطأ في حسابي اليدوي. دعنا نرى:
القدس -> رام الله (5) -> نابلس (15) = 20
القدس -> أريحا (10) -> نابلس (12) = 22
إذن المسار الأقصر هو عبر رام الله. الكود سيصل لهذه النتيجة الصحيحة. خطأي اليدوي يثبت أهمية وجود خوارزمية دقيقة!
تحديث بعد المراجعة: في مثالي الأول، كتبت أن المسافة من رام الله لنابلس 15، ومن أريحا لنابلس 12. دعنا نعدّل مثالنا قليلاً لنجعل الاختيار أكثر إثارة. لو كانت المسافة من رام الله لنابلس 15، ومن أريحا لنابلس 6.
الآن:
– المسار عبر رام الله: القدس -> رام الله (5) + رام الله -> نابلس (15) = 20.
– المسار عبر أريحا: القدس -> أريحا (10) + أريحا -> نابلس (6) = 16.
في هذه الحالة، ستكتشف دايكسترا أن المسار عبر أريحا (رغم أن الخطوة الأولى إليه أطول) هو الأقصر إجمالاً. هذا هو جمالها وقوتها.
متى لا تكون دايكسترا هي الحل؟
لكل بطل نقطة ضعف. نقطة ضعف دايكسترا هي “الأوزان السالبة” (Negative Weights). إذا كان لديك رسم بياني حيث الانتقال عبر حافة معينة يمنحك “رصيداً” بدلاً من أن يكلفك (أي الوزن سالب)، فإن منطق دايكسترا ينهار تماماً. هي تفترض أنه بمجرد زيارة عقدة، فقد وجدت أقصر طريق إليها. لكن مع الأوزان السالبة، قد تكتشف لاحقاً حلقة سالبة يمكن أن تقلل المسافة إلى ما لا نهاية. في هذه الحالات، نلجأ إلى خوارزميات أخرى مثل “Bellman-Ford” أو “Floyd-Warshall”.
نصائح من خبرة أبو عمر 🧔
نصيحة 1: اختر هياكل البيانات الصحيحة
سرعة خوارزمية دايكسترا تعتمد بشكل هائل على كيفية اختيارك “للعقدة التالية ذات المسافة الأقل”. استخدام مصفوفة بسيطة والبحث فيها في كل مرة سيجعل الخوارزمية بطيئة جداً (تعقيد O(V²)). استخدام “طابور الأولوية” (Min-Heap) يحسن الأداء بشكل جذري (إلى O(E log V))، وهذا هو المعيار في التطبيقات الحقيقية. لا تستهن بهذه النقطة!
نصيحة 2: دايكسترا ليست للخرائط فقط
أجمل ما في الخوارزميات هو قابليتها للتطبيق في مجالات مختلفة. استخدمت دايكسترا وما شابهها في:
- توجيه الشبكات (Network Routing): لإيجاد أسرع مسار لحزم البيانات عبر الإنترنت. هنا “التكلفة” قد تكون زمن الاستجابة (latency).
- تحليل الشبكات الاجتماعية: لحساب “درجات الانفصال” بين شخصين.
- تسلسل المهام (Dependency Resolution): إذا كانت المهمة B تعتمد على المهمة A، يمكنك نمذجة هذا كرسم بياني وإيجاد المسار “الأقل تكلفة” لإنجاز مشروع ما.
نصيحة 3: ارسمها قبل أن تبرمجها
قبل كتابة سطر كود واحد، أحضر ورقة وقلماً (أو افتح أي أداة رسم) وارسم الرسم البياني الخاص بمشكلتك. ضع العقد، والحواف، والأوزان. تتبع الخوارزمية يدوياً على مثال بسيط. هذه الدقائق القليلة ستوفر عليك ساعات من تصحيح الأخطاء لاحقاً، وستمنحك فهماً أعمق للمشكلة. زي ما بنحكي، “الفهم نص الشغل”.
الخلاصة: من المتاهة إلى الطريق الواضح 🗺️
العودة إلى تلك الشركة الناشئة، قمت باستبدال الخوارزمية الجشعة الساذجة بتطبيق متين لخوارزمية دايكسترا. النتائج كانت فورية ومذهلة. انخفض متوسط وقت التسليم بنسبة 30%، وتوقف السائقون عن الشكوى، والأهم أن النظام أصبح ذكياً وموثوقاً. لقد أنقذتني دايكسترا من جحيم الطرق الخاطئة، وعلمتني درساً مهماً في البرمجة والحياة: لا تنخدع بالحلول السهلة والسريعة. أحياناً، الطريق الذي يبدو أطول في البداية هو الذي يوصلك إلى وجهتك بأسرع ما يمكن. تعلم الخوارزميات ليس مجرد ترف فكري، بل هو امتلاك صندوق أدوات قوي لحل مشاكل العالم الحقيقي المعقدة. فاستثمروا في فهمها، وسترون كيف تفتح لكم آفاقاً جديدة في التفكير والبرمجة.
أتمنى لكم كل التوفيق في رحلتكم مع الخوارزميات. والسلام عليكم.