يا جماعة الخير، خلوني أحكيلكم قصة صارت معي قبل كم سنة. كنا شغالين على تطبيق توصيل، وكان كل اعتمادنا على الخرائط وتوجيه السائقين. في البداية، استخدمنا خوارزمية بسيطة لإيجاد أقصر طريق، وكنا مفكرين حالنا “أبو العُرّيف” وإنه الموضوع سهل. بس المصيبة بلشت تبين شوي شوي.
صارت توصلنا شكاوى غريبة من السائقين: “التطبيق بحكيلي ألف من شارع مسدود!”، “ليش وداني على طريق ترابي مع إني بقدر أروح من الأوتوستراد؟”، “المسار اللي أعطاني إياه التطبيق أطول بـ 15 دقيقة من الطريق اللي بعرفه!”. بصراحة، كان الوضع محرج ومُحبط. كنا في جحيم حقيقي من المسارات غير المنطقية، والمصداقية تبعتنا صارت على المحك. في ليلة من الليالي، وأنا قاعد بقلّب في الكود والخرائط اللي صارت تشبه معكرونة السباغيتي، صرخت على زميلي: “يا زلمة، لازم نلاقي حل! تطبيقنا صار أضحوكة!”. ومن هنا، بدأت رحلتنا مع المنقذ: خوارزمية A*.
المشكلة: لماذا كانت خوارزمياتنا “غبية”؟
لفهم عظمة A*، لازم نفهم وين كانت مشكلتنا. في البداية، اعتمدنا على خوارزميات مثل البحث بأولوية العرض (BFS). هذه الخوارزمية ممتازة لإيجاد أقصر مسار من حيث “عدد الخطوات”، لكنها لا تفهم “تكلفة” هذه الخطوات.
تخيل أنك تبحث عن طريق بين نقطتين في مدينة. خوارزمية BFS ستعامل الطريق السريع المفتوح (خطوة واحدة طويلة) بنفس أهمية الدخول في زقاق ضيق ومزدحم (خطوة واحدة قصيرة). النتيجة؟ قد تختار لك مساراً يمر بعشرة أزقة ضيقة بدلاً من السير لـ 5 دقائق على طريق سريع، فقط لأن عدد “التقاطعات” أو “الخطوات” أقل. كانت خوارزميتنا عمياء، تبحث في كل الاتجاهات بنفس الأولوية، مثل شخص أعمى يتلمس طريقه في غرفة مزدحمة، مما يستهلك وقتاً وذاكرة هائلين ويعطي نتائج غير واقعية.
المنقذ: خوارزمية A* (نجمة ألفا) واللمسة السحرية
خوارزمية A* (تلفظ A-Star) هي خوارزمية بحث عن مسار ذكية للغاية. هي ليست عمياء مثل سابقاتها، بل لديها “بصيرة” أو “حدس” يمكنها من اتخاذ قرارات أفضل. هذا الحدس يأتي من معادلة بسيطة ولكنها عبقرية:
f(n) = g(n) + h(n)
قد تبدو المعادلة معقدة، لكنها في الحقيقة بسيطة جداً. خلينا نفصصها حبة حبة:
ما هي g(n): تكلفة المسار الفعلي
المتغير g(n) يمثل التكلفة الفعلية للوصول من نقطة البداية إلى النقطة الحالية (n). هذه هي المسافة التي قطعناها بالفعل. إنها الجزء المعروف والمؤكد من الرحلة. في تطبيق الخرائط، يمكن أن تكون هذه المسافة بالكيلومترات، أو الوقت المستغرق لقطعها مع الأخذ في الاعتبار السرعات المحددة.
ما هي h(n): تكلفة المسار المتوقع (الحدس)
هنا يكمن سحر A*! المتغير h(n) هو دالة الكلفة الحدسية (Heuristic Function). إنها “أفضل تخمين” أو تقدير للتكلفة من النقطة الحالية (n) إلى نقطة النهاية. هذا التخمين لا يجب أن يكون دقيقاً 100%، ولكنه يعطي الخوارزمية فكرة عن الاتجاه الذي يجب أن تسلكه.
على سبيل المثال، أبسط أشكال الحدس هو “المسافة الجوية المستقيمة” (as the crow flies) بين النقطة الحالية والهدف. من البديهي أنك لن تستطيع السفر عبر المباني، لكن هذا التقدير يوجه بحثك نحو الهدف بشكل عام، بدلاً من البحث في الاتجاه المعاكس تماماً.
ما هي f(n): التكلفة الإجمالية المقدرة
ببساطة، f(n) هي مجموع الجزأين: التكلفة الفعلية التي قطعناها (g) + التكلفة التقديرية المتبقية (h). خوارزمية A* في كل خطوة، تختار النقطة التي لديها أقل قيمة لـ f(n). بهذا، هي توازن بذكاء بين المسار الذي سلكته بالفعل والمسار الذي تعتقد أنه الأفضل للمستقبل.
كيف تفكر خوارزمية A*: مثال عملي
تخيل أنك في شبكة مربعات (Grid) وتريد الانتقال من المربع الأخضر (بداية) إلى المربع الأحمر (نهاية)، مع وجود جدار (أسود) لا يمكن عبوره.

تستخدم A* قائمتين رئيسيتين:
- القائمة المفتوحة (Open List): تحتوي على المربعات التي لم نزرها بعد ولكننا نفكر في زيارتها. نرتبها حسب قيمة f(n) الأصغر.
- القائمة المغلقة (Closed List): تحتوي على المربعات التي زرناها بالفعل وقمنا بتقييمها. لن نعود إليها مرة أخرى.
الخطوات بشكل مبسط:
- تبدأ من نقطة البداية، تضعها في القائمة المفتوحة.
- تختار المربع صاحب أقل قيمة f(n) من القائمة المفتوحة (في البداية سيكون هو نقطة البداية نفسها).
- تنقل هذا المربع من القائمة المفتوحة إلى القائمة المغلقة.
- تفحص كل جيرانه الصالحين (الذين يمكن المشي إليهم).
- لكل جار:
- إذا كان هو نقطة النهاية، مبروك! لقد وجدت المسار.
- وإلا، احسب له قيم g و h و f.
- إذا كان هذا الجار ليس في القائمة المفتوحة، أضفه. إذا كان موجوداً بالفعل، تحقق إذا كان هذا المسار الجديد إليه أفضل (g أقل)، وقم بتحديثه.
- كرر من الخطوة 2 حتى تجد النهاية أو تصبح القائمة المفتوحة فارغة (مما يعني عدم وجود مسار).
هذا المزيج بين استكشاف المسارات الواعدة (بفضل h) وتتبع التكلفة الحقيقية (بفضل g) هو ما يجعل A* تتجنب بذكاء الطرق الطويلة غير المنطقية وتتجه مباشرة نحو الحل الأمثل.
من النظرية إلى الكود: مثال بايثون بسيط
طبعاً، الكلام النظري جميل، لكن “فرجيني شطارتك بالكود يا أبو عمر”. إليكم مثال مبسط جداً بلغة بايثون لتوضيح الفكرة. هذا ليس كوداً جاهزاً للإنتاج، ولكنه يوضح المبدأ الأساسي.
# This is a very simplified example to illustrate the concept.
# In a real-world scenario, you'd use more robust data structures.
import heapq # To implement the priority queue for the Open List
class Node:
def __init__(self, parent=None, position=None):
self.parent = parent
self.position = position
self.g = 0
self.h = 0
self.f = 0
def __eq__(self, other):
return self.position == other.position
# For the priority queue (heapq)
def __lt__(self, other):
return self.f 0:
# Get the current node (the one with the lowest f value)
current_node_tuple = heapq.heappop(open_list)
current_node = current_node_tuple[1]
# Add to closed list
closed_list.add(current_node.position)
# Found the goal
if current_node == end_node:
path = []
current = current_node
while current is not None:
path.append(current.position)
current = current.parent
return path[::-1] # Return reversed path
# Generate children (neighbors)
for new_position in [(0, -1), (0, 1), (-1, 0), (1, 0)]: # Neighbors (up, down, left, right)
node_position = (current_node.position[0] + new_position[0], current_node.position[1] + new_position[1])
# Make sure within range and walkable
if node_position[0] > (len(grid) - 1) or node_position[0] (len(grid[0]) - 1) or node_position[1] = open_node.g:
break
else:
heapq.heappush(open_list, (child.f, child))
return None # Return None if no path is found
# Example Usage
grid = [[0, 0, 0, 0, 1, 0],
[0, 1, 0, 0, 0, 0],
[0, 0, 1, 0, 1, 0],
[0, 0, 0, 0, 1, 0],
[0, 0, 0, 0, 0, 0]] # 1 is a wall
start = (0, 0)
end = (4, 5)
path = astar(grid, start, end)
print(path) # Output: [(0, 0), (0, 1), (0, 2), (1, 2), (2, 3), (3, 3), (4, 3), (4, 4), (4, 5)]
نصائح أبو عمر العملية
بعد ما طبقنا الخوارزمية، تحسنت النتائج بشكل دراماتيكي. لكن الطريق ما كان مفروش بالورود، وتعلمنا كم شغلة مهمة من خبرتنا العملية:
- جودة الحدس (Heuristic) هي كل إشي: دالة الحدس هي قلب A* النابض. إذا كانت دالة الحدس تبعتك سيئة (مثلاً، تعطي تقديرات مبالغ فيها جداً)، ممكن A* ما تلاقي لك أقصر طريق. القاعدة الذهبية: يجب أن يكون الحدس “مقبولاً” (Admissible)، أي أنه لا يبالغ في تقدير التكلفة الحقيقية أبداً. الحدس المثالي هو الذي يقترب من التكلفة الحقيقية قدر الإمكان دون أن يتجاوزها.
- ليست دائماً الأسرع: على الخرائط الصغيرة، قد لا تلاحظ فرقاً كبيراً في الأداء. لكن على الخرائط الضخمة جداً (مثل خريطة بلد كامل)، يمكن أن تستهلك A* ذاكرة كبيرة جداً بسبب القائمة المفتوحة. هناك نسخ محسنة من A* مثل IDA* أو Jump Point Search تتعامل مع هذه المشاكل.
- العالم الحقيقي متغير: في تطبيقات التوصيل، الخريطة ليست ثابتة. هناك زحمة سير، حوادث، طرق مغلقة. A* الكلاسيكية تجد مساراً بناءً على صورة ثابتة. في الواقع، تحتاج إلى تحديث “تكلفة” المسارات باستمرار (مثلاً، شارع مزدحم تصبح تكلفة المرور فيه أعلى) وإعادة حساب المسار عند اللزوم.
الخلاصة: من جحيم الفوضى إلى جمال المنطق
التحول من الخوارزميات البسيطة إلى A* كان بمثابة نقلة نوعية. انتقلنا من تقديم مسارات تثير سخرية المستخدمين إلى بناء نظام توجيه يثقون به ويعتمدون عليه. لم تكن A* مجرد خوارزمية، بل كانت درساً في أهمية “الذكاء” في حل المشاكل. هي تعلمنا أن الجمع بين البيانات المؤكدة (ما قطعناه بالفعل) والتقدير الذكي للمستقبل (الحدس) هو أقوى طريقة للوصول إلى أهدافنا، سواء في الكود أو في الحياة.
نصيحتي لكل مبرمج ومطور: لا تحفظوا الخوارزميات، بل افهموا فلسفتها. عندما تفهم “لماذا” تعمل A* بهذا الشكل، ستتمكن من تكييفها وتطويرها لحل مشاكل لم تكن لتتخيلها. بالتوفيق يا جماعة! 😊