إيجاد الطريق الأفضل هي من التطبيقات الأقدم والأكثر شعبية في علوم الحاسوب، يمكنك عمليا إيجاد الطريق الأمثل من منطلق إلى مستقر بإضافة التكلفة التي يمكن تكون (مال، وقت.. الخ).
خوارزمية A* هي الخيار الأفضل والأكثر استخداماً في هذا النوع من المسائل، لنتابع في هذا المقال لماذا!؟
يتضمن هذا المقال:
- ماهي خوارزميات البحث؟
- لماذا خوارزمية A*؟
- مدخلات ومخرجات الخوارزمية
- تطبيق عملي
ماهي خوارزميات البحث؟
الانتقال من نقطة إلى أخرى هي عملية نحن كبشر نقوم بها يومياً، نحاول إيجاد أقصر طريق يمكننا من الوصول إلى وجهتنا الأمر الذي يجعل أمر السفر والتنقل مريح نسبياً، قديماً كنا نعتمد على تجريب الطرق وتسجيل النتائج لاختيار الأفضل بينها.
الآن نمتلك الخوارزميات والبيانات التي تساعدنا لمعرفة السبيل الأفضل واقعياً، فقط علينا إدخال الكلفة مهما تكن من مال أو وقت للخوارزمية والحصول على النتيجة بسرعة كبيرة، لقد تم تطوير الكثير من هذه الخوارزميات وA* هي الأكثر شعبية بينها.
لماذا خوارزمية A*؟
هي خوارزمية متطورة من خوارزميات البحث بالعرض أولاً (Breadth-First Search algorithm)، تتسم بالمثالية والكمالية.
يقصد بالمثالية أنها بالتأكيد سوف تخرج الطريق الأقل كلفة (تعطي الحل الأمثل) والكمالية أي أنها سوف توجد كافة الطرق المتوفرة التي تصل بين المنطلق والمستقر (كل الحلول المحققة لشرط).
اذاً هل هذا يجعل من A* الأفضل؟ في معظم الحالات نعم، لكنها أيضاً بطيئة وتحتاج إلى مساحة لحفظ الطرق المتوقعة، هذا الشيء يعطي الأفضلية لباقي الخوارزميات السريعة.
لماذا اختار A* بدلاً عن باقي الخوارزميات السريعة؟
انظر الصور المرفقة، لقد قمت بالمقارنة بين خوارزمية ديكسترا (Dijkstra) وخوارزمية A*.
نرى هنا ان ديسكترا تجد كل الطرق الممكنة دون علم أو معرفة أي منها أكثر ملائمة لحل المشكلة التي نواجهها، هذا يؤدي إلى عمل غير مستحسن و حسابات غير ضرورية.
في الصورة الثانية نجد أن خوارزمية الA* تصل للحل الأكثر مثالية يمكن اعتماده للوصول من منطلق إلى مستقر، فهي تعرف من كل نقطة أي الطرق أفضل للوصول إلى الوجهة.
مدخلات ومخرجات الخوارزمية
الآن وبعد أن علمنا لما اخترناها لنعرف قليلاً عن النظرية وأساسياتها لنتعلم كيف تعمل هذه الخوارزمية.
أي أنها توجد أقل كلفة من نقطة لأخرى، يوجد معادلة واحدة كافية وهي تمثل قلب وروح هذه الخوارزمية وهي:
F = G + H
ماهي هذه المتغيرات الثلاثة ولماذا هي مهمة جداً؟ لنتابع لمعرفة الجواب.
G هو كلفة الانتقال من عقدة إلى أخرى، هذا المتغير يختلف من عقدة إلى أخرى.
H هو التابع التقديري (الإرشادي) بين العقدة الحالية والعقدة الهدف، تكون هذه القيمة غير حقيقية ولكنها اقرب إلى الحقيقة المحسوبة بين المنطلق والمستقر (المصدر والوجهة).
F هو مجموع المتغيرين السابقين ويعبر عن أقل كلفة للانتقال من عقدة إلى القعدة التالية لها في الطريق الأمثل.
انظر الشكل التالي لزيادة الفهم:
افترض أنه لدينا بيان صغير بالرؤوس A,B,S,E حيث S هي المنطلق وE هي المستقر.
تذكر ان الكلفة بين المنطلق والمستقر هي دائماً صفر.
القيم الإرشادية هي:
A:4, B:5, S:5, E:0
لنستخدم المعادلة لحساب الطريق الأقصر للوصول أولاً للمنطلق:
f(S) = 0 + 5 = 5
الطريق من S إلى باقي الرؤوس:
f(S-A) = 1 + 4 = 5
f(S-B) = 2 + 5 = 7
لذا سوف نختار الطريق من S-A لأنه الأقل كلفة.
الطرق من A و B إلى المستقر:
f(S-A-E) = (1 + 13) + 0 = 14
f(S-B-E) = (2 + 5) + 0 = 7
بعد الحسابات، وجدنا أن B أوصلتنا للطريق الأمثل والأقل كلفة، هكذا نستخدم المعادلة للوصول إلى الطريق الأمثل.
بعد استيعابنا للمعادلة، لننتقل الآن لنرى كيف تعمل الخوارزمية:
بداية لننشئ لائحتين سوف تساعدنا في فهم الطريق نسميهما (Open) و (Closed).
- أضف عقدة بداية إلى لائحة.
- لكل عقد الجوار اوجد الأقل كلفة أي F.
- بدل إلى اللائحة الأخرى:
- لكل العقد المتاخمة للعقدة الحالية.
- إذا لا يمكن الوصول للعقدة تجاهلها وإلا:
- إذا العقدة ليست موجودة في اللائحة (Open) ضعها في القائمة واحسب F,H,G.
- إذا العقدة موجودة تحقق من ان الطريق الذي تقدمه أقل من الطريق الحالي واستبدله إذا كان كذلك.
- أوقف العمل عندما:
- تصل إلى المستقر (الوجهة).
- لم تتمكن من إيجاد الوجهة مرورا بكل الاحتمالات.
الكود المزيف (Pseudo-Code) لهذه الخوارزمية كما يلي:
let the openList equal empty list of nodes let the closedList equal empty list of nodes put the startNode on the openList (leave it's f at zero) while the openList is not empty let the currentNode equal the node with the least f value remove the currentNode from the openList add the currentNode to the closedList if currentNode is the goal You've found the end! let the children of the currentNode equal the adjacent nodes for each child in the children if child is in the closedList continue to beginning of for loop child.g = currentNode.g + distance between child and current child.h = distance from child to end child.f = child.g + child.h if child.position is in the openList's nodes positions if the child.g is higher than the openList node's g continue to beginning of for loop add the child to the openList
تطبيق عملي للخوارزمية
إنها خوارزمية رائعة لإيجاد الطريق الأفضل من منطلق إلى مستقر، سوف أعرض لكم كودين الأول لخوارزمية ديكسترا والثاني لخوارزميتنا.
لنبدأ بتطبيق خوارزمية ديكسترا :
def Dijkstra(maze, source): infinity = float('infinity') n = len(maze) dist = [infinity] * n previous = [infinity] * n dist = 0 Q = list(range(n)) while Q: u = min(Q, key=lambda n: dist[n]) Q.remove(u) if dist[u] == infinity: break for v in range(n): if maze[u][v] and (v in Q): alt = dist[u] + maze[u][v] if alt < dist[v]: dist[v] = alt previous[v] = u return dist, previous def display_solution(predecessor): cell = len(predecessor) - 1 while cell: print(cell, end='<') cell = predecessor[cell] print(0) maze = ((0, 0, 0, 0, 1, 0), (0, 0, 0, 0, 1, 0), (0, 0, 0, 0, 1, 0), (0, 0, 0, 0, 1, 0), (0, 0, 0, 0, 1, 0), (0, 0, 0, 0, 0, 0)) graph = ( (0, 1, 0, 0, 0, 0,), (1, 0, 1, 0, 1, 0,), (0, 1, 0, 0, 0, 1,), (0, 0, 0, 0, 1, 0,), (0, 1, 0, 1, 0, 0,), (0, 0, 1, 0, 0, 0,), ) values = Dijkstra(graph, 0) print(values) display_solution(values[1]) values = Dijkstra(maze, 0) print(values) display_solution(values[1])
الخرج للكود السابق:
([0, 1, 2, 3, 2, 3], [inf, 0, 1, 4, 1, 2]) 5<2<1<0 ([0, inf, inf, inf, 1, inf], [inf, inf, inf, inf, 0, inf]) 5display_solution(values[1]) File "C:/Users/AkashkumarJainS/PycharmProjects/A Star/dijkstra.py", line 26, in display_solution cell = predecessor[cell] TypeError: list indices must be integers or slices, not float
نجد أن ديكسترا نجحت في بيان وفشلت في آخر، حيث صادفها عائق أو لم يتوضح الطريق لها، فقد اعتبرت أغلب الطريق تصل الى اللانهاية مما أدى إلى إخراج خطأ في هذا البيان.
لنجرب نفس المدخلات في خوارزمية A*:
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 def astar(maze, start, end): start_node = Node(None, start) start_node.g = start_node.h = start_node.f = 0 end_node = Node(None, end) end_node.g = end_node.h = end_node.f = 0 open_list = [] closed_list = [] open_list.append(start_node) while len(open_list) > 0: current_node = open_list[0] current_index = 0 for index, item in enumerate(open_list): if item.f < current_node.f: current_node = item current_index = index open_list.pop(current_index) closed_list.append(current_node) if current_node == end_node: path = [] current = current_node while current is not None: path.append(current.position) current = current.parent return path[::-1] children = [] for new_position in [(0, -1), (0, 1), (-1, 0), (1, 0), (-1, -1), (-1, 1), (1, -1), (1, 1)]: node_position = (current_node.position[0] + new_position[0], current_node.position[1] + new_position[1]) if node_position[0] > (len(maze) - 1) or node_position[0] < 0 or node_position[1] > ( len(maze[len(maze) - 1]) - 1) or node_position[1] < 0: continue if maze[node_position[0]][node_position[1]] != 0: continue new_node = Node(current_node, node_position) children.append(new_node) for child in children: for closed_child in closed_list: if child == closed_child: continue child.g = current_node.g + 1 child.h = ((child.position[0] - end_node.position[0]) ** 2) + ( (child.position[1] - end_node.position[1]) ** 2) child.f = child.g + child.h for open_node in open_list: if child == open_node and child.g > open_node.g: continue open_list.append(child) def main(): maze = [[0, 0, 0, 0, 1, 0], [0, 0, 0, 0, 1, 0], [0, 0, 0, 0, 1, 0], [0, 0, 0, 0, 1, 0], [0, 0, 0, 0, 1, 0], [0, 0, 0, 0, 0, 0]] graph = [[0, 1, 0, 0, 0, 0], [1, 0, 1, 0, 1, 0], [0, 1, 0, 0, 0, 1], [0, 0, 0, 0, 1, 0], [0, 1, 0, 1, 0, 0], [0, 0, 1, 0, 0, 0] ] start = (0, 0) end = (5, 5) end1 = (5, 5) path = astar(maze, start, end) print(path) path1 = astar(graph, start, end1) print(path1) if __name__ == '__main__': main()
الخرج:
[(0, 0), (1, 1), (2, 2), (3, 3), (4, 3), (5, 4), (5, 5)] [(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5)]
نلاحظ أن A* تتعامل مع العائق لتصل إلى حل مقبول، تقود نفسها لتعطي الخرج المطلوب.
أتمنى أن تكون قد تعرفت بشكل جيد على خوارزمية A* آلية عملها وتطبيقها، لا تتردد بالسؤال، لا تتوقف عن التعلّم.
المصادر: