مقدمة: يوم أن ضاع “أبو أحمد” في تطبيقي!
يا هلا فيكم يا جماعة، معكم أخوكم أبو عمر.
قبل كم سنة، كنت شغال على تطبيق لشركة ناشئة فكرته كانت بسيطة وذكية: تطبيق ملاحة داخلية للمولات والأسواق الكبيرة. تخيل معي سوق ضخم مثل أسواقنا الشعبية القديمة، بس بشكل عصري، والمستخدم بده يروح من محل العطارة لمحل الأقمشة بأسرع طريق ممكن. الفكرة كانت رائعة على الورق، وبدينا الشغل بكل حماس.
النسخة الأولى من التطبيق كانت شغالة، لكن بعد إطلاقها التجريبي، بلشت توصلنا شكاوى غريبة. مستخدمين بحكوا إنه التطبيق بوخذهم في “لفات طويلة مالهاش داعي”. أسوأ موقف كان لما اتصل فينا مدير المول بحكيلي إنه في زبون كبير في السن، خلينا نسميه “أبو أحمد”، ضل يلف نص ساعة عشان يوصل من موقف السيارات للصيدلية، مع إنه المسافة المفروض ما تاخذ أكثر من 5 دقائق! التطبيق أخذه في مسار غريب عبر منطقة المطاعم وبعدها منطقة ألعاب الأطفال… باختصار، بهدله وبهدلنا معه.
في هداك اليوم، حسيت بإحباط كبير. الخوارزمية اللي كنت بستخدمها (كانت نسخة معدلة من خوارزمية البحث بالعمق أولاً – DFS) كانت بتلاقي مسار، أي مسار، لكنها ما كانت “ذكية” كفاية لتعرف إنه في مسار أفضل وأقصر. كانت مثل اللي بسوق سيارة وعيونه مغمضة، بمشي وبس. هون تذكرت محاضرة قديمة في الجامعة عن خوارزميات إيجاد المسار الذكية، ولمع في بالي اسم: A* (A-Star). كانت هي اللحظة اللي تغير فيها كل شيء، وأنقذت المشروع من كابوس المسارات غير الفعالة. خلوني أحكيلكم كيف.
ما هي خوارزمية A*؟ ببساطة، هي “البحث الذكي”
لو بدنا نوصف خوارزمية A* بكلمتين، فهي: “بحث مُستنير” (Informed Search). على عكس الخوارزميات “العمياء” مثل BFS (البحث بالعرض أولاً) أو DFS (البحث بالعمق أولاً) اللي بتبحث في كل الاتجاهات الممكنة تقريبًا، خوارزمية A* عندها “حاسة سادسة”. هي بتستخدم طريقة ذكية عشان “تخمّن” أي المسارات واعدة أكثر وتستكشفها أولاً.
هذا الذكاء كله بيجي من معادلة بسيطة لكن عبقرية:
f(n) = g(n) + h(n)
شكلها بخوف؟ ولا يهمكم، بسيطة جداً. خلينا نفصصها حبة حبة:
g(n): التكلفة الفعلية للوصول (المشوار اللي مشيته)
هذا الجزء هو الأسهل. g(n) هي التكلفة الحقيقية للمسار من نقطة البداية إلى النقطة الحالية (اللي بنسميها العقدة ‘n’).
- مثال تطبيقي: في تطبيق المول تبعنا، لو كل متر بنمشيه هو تكلفة “1”، فـ
g(n)هي ببساطة المسافة اللي قطعها المستخدم فعليًا من مدخل المول لحد ما وصل للنقطة ‘n’ الحالية. لو مشي 100 متر، بتكونg(n) = 100.
h(n): التكلفة المُقدّرة للوصول (المشوار اللي ضايل، تخمينًا)
هون بيجي السحر كله! h(n) هي دالة التخمين أو “الحدس” (Heuristic). وظيفتها إنها تقدّر التكلفة من النقطة الحالية ‘n’ إلى نقطة الهدف النهائية. هذا مجرد تخمين، لكن لازم يكون تخمين “ذكي ومتفائل”.
- مثال تطبيقي: كيف ممكن نخمن المسافة من مكانك الحالي في المول للصيدلية؟ أبسط طريقة هي حساب “المسافة الجوية” أو الخط المستقيم بين النقطتين (As the crow flies). أكيد ما رح تقدر تمشي عبر الحيطان، لكن هذا التخمين بعطينا فكرة ممتازة عن مدى قربك من الهدف. هذا هو الحدس.
نصيحة من أبو عمر: نجاح خوارزمية A* بيعتمد بشكل كلي على جودة الدالة
h(n). القاعدة الذهبية هي أن تكون الدالة “مقبولة” (Admissible)، يعني لا تبالغ أبدًا في تقدير التكلفة الحقيقية. لازم تخمينها يكون دائمًا أقل من أو يساوي التكلفة الفعلية للوصول للهدف. المسافة الجوية مثال ممتاز لأنها أقصر مسافة ممكنة، فمستحيل تكون أطول من المسار الحقيقي المتعرج.
f(n): التكلفة الإجمالية المُقدّرة (الخلاصة)
ببساطة، الخوارزمية بتجمع التكلفة الفعلية g(n) مع التكلفة المُقدّرة h(n). النقطة (أو العقدة) اللي عندها أقل قيمة f(n) هي اللي بتبدو “واعدة” أكثر، فالخوارزمية بتقرر تستكشفها أولاً.
بهذه الطريقة، A* بتوازن بين المسارات القصيرة اللي قطعتها بالفعل، وبين المسارات اللي بتبدو أقرب للهدف. ما بتنجذب للمسارات اللي صارت طويلة جدًا (g(n) عالية)، ولا بتنجذب لمسارات بعيدة عن الهدف (h(n) عالية).
مثال عملي: كيف يجد “أبو أحمد” الصيدلية باستخدام A*؟
لنتخيل خريطة المول تبعنا على شكل شبكة بسيطة. ‘S’ هي نقطة البداية (موقف السيارات)، ‘E’ هي الهدف (الصيدلية)، والمربعات السوداء هي جدران أو عوائق.
+---+---+---+---+---+
| S | | | | |
+---+---+---+---+---+
| |███| |███| |
+---+---+---+---+---+
| |███| | | E |
+---+---+---+---+---+
| | | | | |
+---+---+---+---+---+
الخوارزمية بتشتغل هيك:
- تبدأ من S: تنظر إلى المربعات المجاورة لها (فوق، تحت، يمين، يسار).
- تحسب f(n) لكل جار:
- للمربع اللي على يمين S:
g(n)= 1 (لأننا تحركنا خطوة واحدة).h(n)= نحسب المسافة المستقيمة منه إلى E (لنقل أنها 4).f(n)= 1 + 4 = 5.
- للمربع اللي تحت S:
g(n)= 1.h(n)= نحسب المسافة المستقيمة منه إلى E (لنقل أنها 5).f(n)= 1 + 5 = 6.
- للمربع اللي على يمين S:
- تختار الأفضل: الخوارزمية ترى أن المربع اللي على اليمين (f=5) واعد أكثر من اللي تحت (f=6)، فتتحرك إليه وتضعه في قائمة “المسار الحالي”.
- تكرر العملية: من المربع الجديد، تعيد حساب f(n) لجيرانه الجدد، ودائمًا تختار المسار صاحب أقل قيمة f(n) من بين كل المسارات المتاحة التي لم تستكشفها بعد.
لاحظ كيف أنها لم تستكشف المسار السفلي لأنه “بدا” أبعد عن الهدف، وهذا بفضل دالة الحدس h(n). ستستمر الخوارزمية في سلوك المسارات التي تقربها من الهدف بشكل عام، وتتجنب بذكاء الجدران، حتى تصل إلى ‘E’ بأقصر مسار ممكن. هذا بالضبط ما كان يحتاجه “أبو أحمد”!
شوية كود: تطبيق A* بلغة بايثون
الكلام النظري حلو، بس إحنا المبرمجين بنحب نشوف الكود. إليكم مثال مبسط جدًا لخوارزمية A* بلغة بايثون. هذا الكود ليس للإنتاج مباشرة، لكنه يوضح الفكرة الأساسية بشكل ممتاز.
import heapq
# دالة الحدس (Heuristic): هنا نستخدم مسافة مانهاتن
def heuristic(a, b):
# a و b هما زوج من الإحداثيات (x, y)
return abs(a[0] - b[0]) + abs(a[1] - b[1])
def a_star(grid, start, goal):
# الجيران الممكن التحرك إليهم (أعلى، أسفل، يمين، يسار)
neighbors = [(0, 1), (0, -1), (1, 0), (-1, 0)]
close_set = set() # مجموعة العقد التي تم تقييمها
came_from = {} # قاموس لتتبع المسار
# g_score هو التكلفة من البداية إلى العقدة الحالية
g_score = {start: 0}
# f_score هو التكلفة الإجمالية المتوقعة (g_score + heuristic)
f_score = {start: heuristic(start, goal)}
# قائمة الأولوية (Priority Queue) للعقد التي سيتم تقييمها
# هيك بنضمن إنه دايماً بنختار العقدة اللي الها أقل f_score
open_heap = []
heapq.heappush(open_heap, (f_score[start], start))
while open_heap:
# احصل على العقدة في القائمة المفتوحة التي لديها أقل f_score
current = heapq.heappop(open_heap)[1]
# إذا وصلنا للهدف، قم بإعادة بناء المسار وإرجاعه
if current == goal:
path = []
while current in came_from:
path.append(current)
current = came_from[current]
path.append(start)
return path[::-1] # نعكس المسار ليكون من البداية للنهاية
close_set.add(current)
for i, j in neighbors:
neighbor = current[0] + i, current[1] + j
# نحسب التكلفة المبدئية للمسار إلى الجار
# هنا نفترض أن تكلفة كل خطوة هي 1
tentative_g_score = g_score[current] + 1
# تحقق من أن الجار داخل حدود الشبكة وأنه ليس عائقًا
if 0 <= neighbor[0] < len(grid) and 0 <= neighbor[1] = g_score[neighbor]:
continue
# هذا هو أفضل مسار حتى الآن. سجله!
came_from[neighbor] = current
g_score[neighbor] = tentative_g_score
f_score[neighbor] = g_score[neighbor] + heuristic(neighbor, goal)
# أضف الجار إلى القائمة المفتوحة ليتم تقييمه
if neighbor not in [i[1] for i in open_heap]:
heapq.heappush(open_heap, (f_score[neighbor], neighbor))
return False # لم يتم العثور على مسار
# --- مثال للاستخدام ---
# 0 = ممر, 1 = جدار
grid = [
[0, 0, 0, 0, 0],
[0, 1, 0, 1, 0],
[0, 1, 0, 0, 0],
[0, 0, 0, 1, 0],
[0, 0, 0, 1, 0]
]
start_node = (0, 0)
goal_node = (2, 4)
path = a_star(grid, start_node, goal_node)
print("المسار الأفضل هو:", path)
نصائح عملية من خبرة أبو عمر
بعد ما استخدمت A* في مشاريع كثيرة، من الألعاب لتطبيقات الخرائط واللوجستيات، تعلمت كم شغلة “من الكيس” زي ما بحكوها. تفضلوا:
- اهتم بهياكل البيانات (Data Structures): استخدام قائمة عادية للبحث عن العقدة ذات أقل
f_scoreفي كل مرة هو أمر كارثي من ناحية الأداء. استخدام “قائمة الأولوية” (Priority Queue/Heap) هو الحل الأمثل والأسرع، زي ما عملنا في كود البايثون باستخدام مكتبةheapq. - دالة الحدس (Heuristic) هي فن وعلم: في الشبكات المربعة، “مسافة مانهاتن” (Manhattan Distance) غالبًا ما تكون أفضل من “المسافة الإقليدية” (Euclidean Distance) إذا كان مسموحًا لك فقط بالحركة أفقيًا وعموديًا. اختر الدالة التي تعكس طبيعة الحركة في مشكلتك.
- A* ليست دائمًا الحل: إذا كانت كل تكاليف الحركة متساوية ولا تحتاج لدالة حدس (يعني كل الطرق زي بعض)، فإن خوارزمية BFS ستعطيك أقصر مسار وبكود أبسط. أما إذا كنت تريد إيجاد أقصر مسار من نقطة واحدة إلى كل النقاط الأخرى، فخوارزمية “دايكسترا” (Dijkstra) هي خيارك الأفضل. A* تتألق عندما يكون لديك نقطة بداية ونقطة نهاية محددتان.
- العالم الحقيقي أكثر تعقيدًا: في تطبيقي، لم تكن التكلفة مجرد مسافة. أضفنا عوامل أخرى مثل “ازدحام الممرات” في أوقات معينة. يمكنك تعديل تكلفة الحركة بين عقدتين (الـ “1” اللي افترضناها في الكود) لتكون ديناميكية وتعكس هذه العوامل. هذا ما يجعل A* مرنة وقوية جدًا.
الخلاصة: من متاهة إلى طريق سريع 🚀
في النهاية، بعد ما طبقنا خوارزمية A* في تطبيق المول، كانت النتائج مذهلة. الشكاوى اختفت، وتقييمات التطبيق ارتفعت. “أبو أحمد” وكل المستخدمين صاروا يوصلوا لوجهتهم بأسرع وأذكى الطرق. الدرس اللي تعلمته كان أعمق من مجرد كود وخوارزمية.
الدرس هو أن اختيار الأداة الصحيحة للمشكلة هو نصف الحل. أحيانًا، الحل “اللي بمشي الحال” ليس كافيًا، وخصوصًا عندما تتعلق التجربة بالمستخدم. خوارزمية A* لم تكن مجرد سطور من الكود، بل كانت الفلسفة التي حولت تطبيقي من متاهة محبطة إلى مرشد ذكي وموثوق.
نصيحتي لكم يا جماعة: لا تخافوا من الخوارزميات والنظريات. افهموا المبدأ الأساسي وراءها، وجربوا تطبيقها على مشاكل بسيطة. كل خوارزمية هي قصة، أداة، وسلاح في ترسانتك كمطور. وكلما زادت أسلحتك، أصبحت قادرًا على مواجهة تحديات أكبر وأكثر تعقيدًا. يلا، شدوا حيلكم!