يا جماعة الخير، السلام عليكم ورحمة الله. اسمحولي اليوم أحكيلكم قصة صارت معي زمان، أيام ما كنت لسا ببداياتي في عالم البرمجة، وقبل ما يصير الذكاء الاصطناعي والشبكات العصبية هي “شغلانتي” الأساسية. في هذيك الأيام، كانت عيلتي تدير مشروع صغير لبيع زيت الزيتون والزعتر البلدي الأصيل. ومع اقتراب مواسم الأعياد، كانت الطلبات تنهال علينا من كل حدب وصوب، وكنت أنا المسؤول عن “لوجستيات” التوصيل.
بتذكر في يوم من الأيام، كان عندي قائمة فيها حوالي 15 عنوان لازم أوصلهم الطلبات. مسكت الخريطة الورقية (نعم، ورقية!) وقلم، وصرت أحاول أرسم “أحسن” طريق بعيني. قلت ببدأ من أبعد نقطة وبرجع، بعدين غيرت رأيي وقلت لأ، ببدأ بالأقرب. قضيت حوالي ساعة كاملة وأنا أخطط وأمسح، وفي الآخر طلعت بطريق “حسيت” إنه الأفضل. النتيجة؟ لفيت نص المدينة مرتين، استهلكت بنزين زيادة، وتأخرت على بعض الزبائن. رجعت آخر اليوم مهدود حيلي ومحفظتي شبه فاضية. يومها، قعدت مع حالي وقلت: “يا زلمة، إنت مبرمج! معقول ما في طريقة علمية، طريقة منطقية تحل هالمشكلة؟ مستحيل يكون الحل هو ‘بالحاسة السادسة’!”.
هذا الموقف البسيط كان هو الشرارة اللي خلتني أغوص في عالم الخوارزميات بشكل أعمق، وهناك، قابلت البطل الصامت اللي أنقذني من جحيم التخطيط العشوائي: خوارزمية ديكسترا.
ما هي الرسوم البيانية (Graphs)؟ وليش هي مهمة؟
قبل ما نحكي عن المنقذ “ديكسترا”، لازم نفهم طبيعة المشكلة اللي بنواجهها. الخريطة اللي كانت قدامي، مع المدن والشوارع، هي في عالم البرمجة إشي بنسميه “الرسم البياني” أو Graph. فهم هذا المفهوم هو مفتاح كل إشي.
مكونات الرسم البياني: العُقد والحواف (Nodes and Edges)
ببساطة شديدة، الرسم البياني بتكون من شغلتين:
- العُقد (Nodes أو Vertices): وهي النقاط أو الكيانات في شبكتنا. في قصة التوصيل، كل عنوان زبون، بالإضافة لموقعي (نقطة البداية)، هو عبارة عن “عُقدة”.
– الحواف (Edges): وهي الخطوط اللي بتوصل بين العُقد. في قصتنا، الشوارع اللي بتربط بين العناوين هي “الحواف”.
لما نمثل مشكلتنا بهي الطريقة، بتصير أوضح وأسهل للتعامل معها برمجياً. بطلت مجرد خريطة عشوائية، وصارت شبكة منظمة من النقاط والوصلات.
الرسوم البيانية الموزونة (Weighted Graphs)
هنا بتصير الأمور أكثر إثارة. مش كل الشوارع زي بعض، صح؟ في شارع طويل، وشارع قصير، وشارع فيه أزمة سير خانقة. لهيك، بنضيف “وزن” أو “تكلفة” (Weight/Cost) لكل حافة. هذا الوزن ممكن يمثل:
- المسافة (بالكيلومتر).
- الوقت اللازم لقطع المسافة.
- تكلفة الوقود.
- أي مقياس آخر بهمنا.
هيك، بصير هدفنا مش بس نلاقي أي مسار، بل نلاقي المسار ذو “أقل تكلفة إجمالية”. وهذا بالضبط ما صُممت خوارزمية ديكسترا لفعله.
فلنُرحب بالبطل: خوارزمية ديكسترا (Dijkstra’s Algorithm)
بعد ما جهزنا المسرح وفهمنا المشكلة، صار وقت ندخل البطل. خوارزمية ديكسترا، اللي سميت على اسم عالم الكمبيوتر الهولندي إدسخر ديكسترا، هي خوارزمية “طماعة” (Greedy Algorithm) هدفها إيجاد أقصر مسار بين عقدة بداية وكل العقد الأخرى في رسم بياني موزون (بشرط تكون كل الأوزان موجبة، ورح نحكي عن هاي النقطة بعدين).
الفكرة الأساسية ورا الخوارزمية ببساطة
تخيل حالك واقف في نقطة البداية (بيتك مثلاً). بتطلع على كل الطرق المباشرة اللي بتوصلك لأماكن تانية (جيرانك أو المحلات القريبة). بتختار “أقصر” طريق وبتروح على هذيك النقطة الجديدة. من هناك، بتعمل نفس الإشي: بتشوف كل الطرق الجديدة اللي انفتحت قدامك، وبتحسب المسافة الإجمالية من بيتك الأصلي لكل نقطة ممكن توصلها. بتضل تكرر هاي العملية، ودائماً بتختار الوجهة “الأقرب” إلك من بين كل الوجهات اللي لسا ما زرتها. مع كل خطوة، إنت بتبني بشكل تدريجي خريطة لأقصر المسارات من نقطة البداية لكل مكان تاني.
هاي هي روح الخوارزمية: “طمع” في اختيار الخطوة الأفضل حالياً، على أمل إنها توصلنا للحل الأمثل بالنهاية. وفي حالة ديكسترا، هذا الطمع مبرر ويؤدي للنتيجة الصحيحة دائماً.
كيف بتشتغل خوارزمية ديكسترا خطوة بخطوة؟
خلينا ناخد مثال عملي بسيط عشان الصورة توضح تماماً. تخيل عنا شبكة مدن بسيطة، وبدنا نلاقي أقصر طريق من المدينة ‘A’ لكل المدن التانية.

الرسم البياني يوضح 6 مدن (A, B, C, D, E, F) والمسافات (الأوزان) بينها.
الخطوة 0: التجهيزات الأولية: “يلا نجهز الطاولة”
قبل ما نبدأ، بنحتاج 3 أشياء:
- جدول المسافات: بنعمل جدول نسجل فيه أقصر مسافة معروفة حتى الآن من نقطة البداية (A) لكل مدينة. في البداية، المسافة لـ A هي 0، ولكل المدن التانية هي ما لا نهاية (∞)، لأنه لسا ما بنعرف أي طريق إلها.
- قائمة “لم تتم زيارته”: بنحط كل المدن في هاي القائمة.
- جدول “المسار السابق”: عشان نقدر بالاخر نرسم المسار، بنسجل لكل مدينة، من أي مدينة جيناها عشان نحصل على أقصر مسافة.
جدول البداية:
– المسافات: { A: 0, B: ∞, C: ∞, D: ∞, E: ∞, F: ∞ }
– لم تتم زيارته: { A, B, C, D, E, F }
الخطوة 1: نبدأ من ‘A’
- نختار العقدة “غير المزارة” ذات أقل مسافة. حالياً هي ‘A’ (مسافتها 0).
- ننظر لجيران ‘A’: هما ‘B’ و ‘C’.
- نحدّث مسافاتهم:
- المسافة إلى ‘B’ عبر ‘A’ هي 0 + 2 = 2. بما إن 2 < ∞، نحدّث مسافة 'B' لتصبح 2.
- المسافة إلى ‘C’ عبر ‘A’ هي 0 + 4 = 4. بما إن 4 < ∞، نحدّث مسافة 'C' لتصبح 4.
- نُخرج ‘A’ من قائمة “لم تتم زيارته”.
بعد الخطوة 1:
– المسافات: { A: 0, B: 2, C: 4, D: ∞, E: ∞, F: ∞ }
– لم تتم زيارته: { B, C, D, E, F }
الخطوة 2: ننتقل إلى ‘B’
- نختار العقدة “غير المزارة” ذات أقل مسافة. الآن هي ‘B’ (مسافتها 2).
- ننظر لجيران ‘B’: هما ‘A’ (تمت زيارته، نتجاهله)، ‘C’، و ‘D’.
- نحدّث مسافاتهم:
- المسافة إلى ‘C’ عبر ‘B’: هي مسافة ‘B’ + الوزن(B,C) = 2 + 1 = 3. بما إن 3 < 4 (المسافة القديمة لـ C)، نحدّث مسافة 'C' لتصبح 3.
- المسافة إلى ‘D’ عبر ‘B’: هي 2 + 7 = 9. بما إن 9 < ∞، نحدّث مسافة 'D' لتصبح 9.
- نُخرج ‘B’ من قائمة “لم تتم زيارته”.
بعد الخطوة 2:
– المسافات: { A: 0, B: 2, C: 3, D: 9, E: ∞, F: ∞ }
– لم تتم زيارته: { C, D, E, F }
وهكذا… نستمر في تكرار العملية. في كل مرة، نختار العقدة الأقرب التي لم تتم زيارتها، ونحدّث مسافات جيرانها إذا وجدنا مساراً أقصر. بالنهاية، سنحصل على جدول كامل بأقصر المسافات من ‘A’ إلى كل المدن الأخرى.
خلينا نترجم الحكي لكود (مثال بلغة Python)
الكلام النظري جميل، لكن “فرجيني الكود يا أبو عمر!”. إليكم مثال بسيط بلغة بايثون يوضح كيفية تطبيق الخوارزمية. سنستخدم مكتبة 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)]
# قاموس لتتبع المسار
previous_nodes = {node: None for node in graph}
while priority_queue:
# 2. اختيار العقدة غير المزارة ذات أقل مسافة
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
# إذا وجدنا مسارًا جديدًا أقصر إلى الجار
if distance < distances[neighbor]:
distances[neighbor] = distance
previous_nodes[neighbor] = current_node
heapq.heappush(priority_queue, (distance, neighbor))
return distances, previous_nodes
# تعريف الرسم البياني الخاص بنا كمثال
# graph = { 'A': {'B': 2, 'C': 4}, ... }
graph = {
'A': {'B': 2, 'C': 4},
'B': {'A': 2, 'C': 1, 'D': 7},
'C': {'A': 4, 'B': 1, 'E': 3},
'D': {'B': 7, 'F': 1},
'E': {'C': 3, 'F': 5},
'F': {'D': 1, 'E': 5}
}
# تشغيل الخوارزمية من النقطة 'A'
shortest_distances, path = dijkstra(graph, 'A')
print("أقصر المسافات من A:")
print(shortest_distances)
# مثال على استرجاع المسار إلى F
# F <- D <- B <- A
def get_path(previous_nodes, start_node, end_node):
path_list = []
current = end_node
while current is not None:
path_list.append(current)
current = previous_nodes[current]
path_list.reverse()
return path_list
print("nأقصر مسار من A إلى F:")
print(get_path(path, 'A', 'F'))
نصايح من خبرة “أبو عمر” العملية
تعلم الخوارزمية شيء، وتطبيقها في العالم الحقيقي شيء آخر. إليكم بعض النصائح من واقع التجربة:
ديكسترا مش دايماً الحل الأمثل
خوارزمية ديكسترا رائعة، لكن لها نقطة ضعف قاتلة: لا تعمل مع الأوزان السالبة. إذا كان هناك “شارع” في شبكتك تكلفته سالبة (كأنك تكسب مالاً عند المرور به)، ستدخل الخوارزمية في حلقة لا نهائية أو تعطي نتائج خاطئة لأنها ستظل تعود لهذا المسار السالب لتحسين النتيجة للأبد. في هذه الحالات، نلجأ لخوارزميات أخرى مثل “Bellman-Ford”.
الكفاءة هي اسم اللعبة
الطريقة التي تختار بها “أقرب عقدة لم تتم زيارتها” تؤثر بشكل هائل على أداء الخوارزمية. لو استخدمت قائمة عادية وبحثت فيها كل مرة، ستكون الخوارزمية بطيئة جداً مع الرسوم البيانية الكبيرة. استخدام “طابور الأولوية” (Priority Queue) كما في كود بايثون أعلاه هو الطريقة الاحترافية، ويحسن الأداء بشكل كبير جداً.
أبعد من مجرد خرائط
جمال ديكسترا أنها لا تقتصر على الخرائط والشوارع. يمكن تطبيقها في أي مكان يمكن تمثيله كشبكة موزونة:
- توجيه الشبكات (Network Routing): إيجاد أسرع مسار لحزم البيانات عبر الإنترنت من جهازك إلى الخادم.
- الشبكات الاجتماعية: حساب “أقصر طريق” بين شخصين في شبكة مثل لينكدإن (درجات الاتصال).
- تطوير الألعاب: حساب مسار شخصيات الذكاء الاصطناعي (NPCs) للوصول إلى اللاعب بأقصر طريق ممكن.
- البيولوجيا الحاسوبية: تحليل مسارات التفاعلات في الشبكات الأيضية.
الخلاصة: من الفوضى إلى الخوارزمية 💡
رحلتي بدأت من فوضى التخطيط اليدوي، من خريطة ورقية وقلم وتخمينات مكلفة. هذه الفوضى قادتني لأقدّر النظام والمنطق الذي تقدمه لنا الخوارزميات. ديكسترا لم تكن مجرد معادلات وكود بالنسبة لي، بل كانت أداة حقيقية حوّلت مشكلة معقدة ومحبطة إلى حل أنيق، فعال، وموثوق.
نصيحتي الأخيرة لكل مبرمج ومطور: لا تنظر إلى الخوارزميات وهياكل البيانات على أنها مجرد مادة أكاديمية مملة لاجتياز المقابلات الوظيفية. انظر إليها كصندوق أدواتك. كلما فهمت أدواتك بشكل أعمق، أصبحت أكثر قدرة على بناء حلول قوية ومبتكرة للمشاكل الحقيقية التي تواجهنا كل يوم، سواء في توصيل طلبات زيت الزيتون، أو في بناء الجيل القادم من تطبيقات الذكاء الاصطناعي. فكروا كمهندسين، وابحثوا عن الهيكل المنطقي في قلب كل فوضى.