Chapterبەش 4 · Lectureوانە 4.7
Search Practice: Midterm-Style Tasksڕاهێنانی گەڕان: ئەرکی وەک تاقیکردنەوەی نیوەی وەرز
Five exam-style tasks built on the A* maze program: random start and goal, typed and checked input, the nearest of several goals, visiting all goals, and delivering a parcel on a road map with DFS, BFS and A*, each with clean tested Python and the first versions' bugs fixed.
پێنج ئەرکی وەک تاقیکردنەوە لەسەر : سەرەتا و ئامانجی هەڕەمەکی، نووسراو و پشکنراو، نزیکترینی چەند ئامانجێک، سەردانی هەموو ئامانجەکان و گەیاندنی پاکەتێک لەسەر نەخشەی ڕێگاکان بە ، و ، هەریەکەیان بە کۆدی پاک و تاقیکراوە و یەکەم وەشانەکان چاک کراون.
What you will learnچی فێر دەبیت
- Reuse one A* module and change only the part a question asks for.
- Generate random cells and read typed cells safely, with full validation.
- Search for the nearest of several goals and visit several goals in turn.
- Model a road map as a graph and choose DFS, BFS or A* for the question asked.
- Find and fix typical bugs: a goal on a wall, negative indexes, shared defaults, cells counted as steps.
- یەک دووبارە بەکار بهێنیت و تەنیا ئەو بەشە بگۆڕیت کە پرسیارەکە داوای دەکات.
- خانەی هەڕەمەکی دروست بکەیت و خانەی نووسراو بە سەلامەتی بخوێنیتەوە، بە تەواوەوە.
- بۆ نزیکترینی چەند ئامانجێک بگەڕێیت و سەردانی چەند ئامانجێک بە نۆرە بکەیت.
- نەخشەی ڕێگاکان وەک نیشان بدەیت و بۆ پرسیارەکە ، یان هەڵبژێریت.
- بدۆزیتەوە و چاکیان بکەیت: ئامانج لەسەر دیوار، ، کە هاوبەشن، ژماردنی خانە وەک هەنگاو.
Exam tasks: "what if they ask…?"ئەرکەکانی تاقیکردنەوە: «ئەگەر داوا بکەن…؟»
Before the midterm the class collected typical exam questions: what if they ask us to…? Each one changes one small part of the A* maze program from A* search: where the start and goal come from, how many goals there are, or what the program prints. This lesson solves each task in clean, tested Python and fixes the bugs of the first versions.
پێش تاقیکردنەوەی نیوەی وەرز پۆلەکە پرسیارە باوەکانی تاقیکردنەوەیان کۆ کردەوە: ئەگەر داوامان لێ بکەن…؟ هەریەکەیان بەشێکی بچووک لە دەگۆڕێت لە گەڕانی A*: خاڵی سەرەتا و ئامانج لە کوێوە دێن، چەند ئامانج هەن، یان چی چاپ دەکات. ئەم وانەیە هەر ئەرکێک بە کۆدی پاک و تاقیکراوە چارەسەر دەکات و یەکەم وەشانەکان ڕاست دەکاتەوە.
| Taskئەرک | What changesچی دەگۆڕێت |
|---|---|
| 1. Random start and goal1. سەرەتا و ئامانجی هەڕەمەکی | Where the start and goal come fromسەرەتا و ئامانج لە کوێوە دێن |
| 2. Start and goal typed by the user2. سەرەتا و ئامانج بە دەستی بەکارهێنەر | Reading and checking inputخوێندنەوە و پشکنینی |
| 3. The nearest of several goals3. نزیکترینی چەند ئامانجێک | The goal test and the heuristic و |
| 4. Visit all goals4. سەردانی هەموو ئامانجەکان | A loop around the search بە دەوری گەڕانەکەدا |
| 5. Deliver a parcel on a road map5. گەیاندنی پاکەتێک لەسەر نەخشەی ڕێگاکان | A graph of towns instead of a grid لە شارەکان لەبری تۆڕی خانەکان |
The base program: astar_grid.pyبەرنامە بنەڕەتییەکە: astar_grid.py
Save the A* program as astar_grid.py once. Every task below imports from it, so it must be in the same folder. Compared with the Lab 4 code it has three small additions: __eq__ and __hash__ in Point, and a helper is_open().
یەک جار وەک astar_grid.py پاشەکەوت بکە. هەموو ئەرکەکانی خوارەوە لێیەوە هاوردە دەکەن، بۆیە دەبێت لە هەمان بوخچەدا بێت. بەراورد بە کۆدی تاقیگەی 4، سێ زیادکردنی بچووکی هەیە: __eq__ و __hash__ لە Pointدا و یارمەتیدەری is_open().
import heapq
WALL = "#"
MAZE = [
"# ",
"# ######### ",
"# # # ",
"# # ##### # ",
"# # # ",
"### # ##### ",
" # ",
]
class Point:
def __init__(self, x, y):
self.x = x # row
self.y = y # column
def __repr__(self):
return f"({self.x}, {self.y})"
def __eq__(self, other): # two points are equal if row and column match
return self.x == other.x and self.y == other.y
def __hash__(self): # so points can go in sets and dictionary keys
return hash((self.x, self.y))
def is_open(maze, p):
"""True if p is inside the maze and not a wall."""
return 0 <= p.x < len(maze) and 0 <= p.y < len(maze[0]) and maze[p.x][p.y] != WALL
def manhattan(a, b):
return abs(a.x - b.x) + abs(a.y - b.y)
def solve(maze, start, end):
"""A* from start to end. Returns the path (a list of points) or None."""
count = 0
frontier = [(manhattan(start, end), count, start)]
g = {start: 0}
parent = {start: None}
closed = set()
while frontier:
_, _, current = heapq.heappop(frontier)
if current in closed:
continue
if current == end:
return backtrack(parent, current)
closed.add(current)
for dx, dy in [(-1, 0), (0, -1), (1, 0), (0, 1)]:
nxt = Point(current.x + dx, current.y + dy)
if is_open(maze, nxt) and g[current] + 1 < g.get(nxt, float("inf")):
g[nxt] = g[current] + 1
parent[nxt] = current
count += 1
heapq.heappush(frontier, (g[nxt] + manhattan(nxt, end), count, nxt))
return None
def backtrack(parent, end):
path = []
while end is not None:
path.append(end)
end = parent[end]
return path[::-1]
if __name__ == "__main__":
path = solve(MAZE, Point(6, 0), Point(0, 11))
print("Path:", path)
print("Cells:", len(path), "| steps:", len(path) - 1)Path: [(6, 0), (6, 1), (6, 2), (6, 3), (5, 3), (4, 3), (4, 2), (4, 1), (3, 1), (2, 1), (1, 1), (0, 1), (0, 2), (0, 3), (0, 4), (0, 5), (0, 6), (0, 7), (0, 8), (0, 9), (0, 10), (0, 11)] Cells: 22 | steps: 21
Explain the codeڕوونکردنەوەی کۆدەکە
- Lineدێڕی 3-12
The Lab 4 maze:
#is a wall, a space is an open cell.تاقیگەی 4:
#دیوارە، بۆشایی خانەیەکی کراوەیە.- Lineدێڕی 23-24
__eq__: twoPointobjects are equal when their row and column are equal. Without it,Point(4, 8) == Point(4, 8)isFalse, because Python compares the objects themselves.__eq__: دووPointیەکسانن کاتێک ڕیز و ستوونیان یەکسان بێت. بەبێ ئەمە،Point(4, 8) == Point(4, 8)دەبێت بەFalse، چونکە خودی بەراورد دەکات.- Lineدێڕی 26-27
__hash__: lets a point be a dictionary key or a set member by value (sets and tuples).__hash__: ڕێگە دەدات خاڵێک ببێت بە یان ئەندامی بە پێی نرخەکەی (کۆمەڵەکان و تاپڵەکان).- Lineدێڕی 30-32
One test for "inside the maze and not a wall". It also rejects negative numbers, which Python would otherwise read from the end of a list.
یەک پشکنین بۆ «لە ناو و دیوار نییە». ڕەت دەکاتەوە، کە ئەگەر نا لە کۆتایی لیستەکەوە دەیانخوێنێتەوە.
- Lineدێڕی 39-60
A* as in Lab 4, with dictionaries for g and parent and a set for CLOSED. It returns the path, or
None.وەک تاقیگەی 4، بە بۆ g و باوک و بۆ CLOSED. ڕێگاکە دەگەڕێنێتەوە، یان
None.- Lineدێڕی 71-74
The demo runs only when you run this file itself, not when another file imports it.
نموونەکە تەنیا کاتێک کار دەکات کە خودی ئەم فایلە جێبەجێ بکەیت، نەک کاتێک فایلێکی تر هاوردەی دەکات.
Task 1: a random start and goal every timeئەرکی 1: سەرەتا و ئامانجی هەڕەمەکی هەموو جارێک
Pick a random row and a random column with random.randint(), which includes both ends. Try again until the cell is open. Pick the goal the same way, and repeat while it equals the start.
ڕیزێکی هەڕەمەکی و ستوونێکی هەڕەمەکی بە random.randint() هەڵبژێرە، کە هەردوو سەرەکە لەخۆ دەگرێت. دووبارە هەوڵ بدەرەوە تا خانەکە کراوە دەبێت. ئامانج بە هەمان شێوە هەڵبژێرە و تا لەگەڵ سەرەتا یەکسان بێت دووبارەی بکەرەوە.
import random
from astar_grid import MAZE, Point, is_open, solve
def random_open_point(maze):
while True: # try until we hit an open cell
p = Point(random.randint(0, len(maze) - 1), # randint includes both ends
random.randint(0, len(maze[0]) - 1))
if is_open(maze, p):
return p
start = random_open_point(MAZE)
end = random_open_point(MAZE)
while end == start: # the goal must be another cell
end = random_open_point(MAZE)
print(f"Random start: {start}, random goal: {end}")
path = solve(MAZE, start, end)
if path:
print("Path:", path)
print("Steps:", len(path) - 1)
else:
print("No path between these two cells.")Random start: (6, 0), random goal: (0, 1) Path: [(6, 0), (6, 1), (6, 2), (6, 3), (5, 3), (4, 3), (4, 2), (4, 1), (3, 1), (2, 1), (1, 1), (0, 1)] Steps: 11 Random start: (6, 11), random goal: (2, 8) Path: [(6, 11), (6, 10), (6, 9), (6, 8), (6, 7), (6, 6), (6, 5), (5, 5), (4, 5), (4, 6), (4, 7), (4, 8), (4, 9), (3, 9), (2, 9), (2, 8)] Steps: 15 Random start: (4, 1), random goal: (1, 11) Path: [(4, 1), (3, 1), (2, 1), (1, 1), (0, 1), (0, 2), (0, 3), (0, 4), (0, 5), (0, 6), (0, 7), (0, 8), (0, 9), (0, 10), (0, 11), (1, 11)] Steps: 15
These are three real runs, but the cells are random: every run picks a new start and goal, so your start, goal and path will be different from these. To get the same cells on every run while testing, see the exercise "The same random task every time".
ئەمانە سێ جێبەجێکردنی ڕاستەقینەن، بەڵام خانەکان هەڕەمەکین: هەموو جێبەجێکردنێک سەرەتا و ئامانجێکی نوێ هەڵدەبژێرێت، بۆیە سەرەتا، ئامانج و ڕێگای تۆ لەمانە جیاواز دەبن. بۆ ئەوەی لە کاتی تاقیکردنەوەدا هەموو جارێک هەمان خانەکان وەربگریت، ڕاهێنانی «هەمان ئەرکی هەڕەمەکی هەموو جارێک» ببینە.
Explain the codeڕوونکردنەوەی کۆدەکە
- Lineدێڕی 5-10
Keep drawing cells until one is open.
randint(0, len(maze) - 1)gives 0 to 6: both ends included.تا خانەیەکی کراوە دەردەچێت، خانە بکێشە.
randint(0, len(maze) - 1)0 تا 6 دەدات: هەردوو سەرەکە لەخۆ دەگرێت.- Lineدێڕی 13-16
The goal must be a different cell.
!=and==work on points because of__eq__.ئامانج دەبێت خانەیەکی جیاواز بێت.
!=و==بەهۆی__eq__ـەوە لەسەر خاڵەکان کار دەکەن.- Lineدێڕی 18-24
Solve with A* from the base program and print the path and the number of steps.
بە چارەسەری بکە و ڕێگا و ژمارەی هەنگاوەکان چاپ بکە.
Every open cell of this maze can reach every other one (all 49 open cells are connected), so a path always exists. In another maze the program would print "No path" for some pairs; that is why it checks if path:.
هەموو خانەیەکی کراوەی ئەم دەتوانێت بگاتە هەموو ئەوانی تر (هەموو 49 خانە کراوەکە پێکەوە بەستراون)، بۆیە هەمیشە ڕێگایەک هەیە. لە تردا بۆ هەندێک جووت «No path» چاپ دەکات؛ بۆیە if path: دەپشکنێت.
Task 2: the user types the start and the goalئەرکی 2: بەکارهێنەر سەرەتا و ئامانج دەنووسێت
User input must be checked twice: is it in the right format (two whole numbers), and is it a valid cell (inside the maze and not a wall)? Use int(), never eval() (why not eval), and catch ValueError for text that is not a number. These are the same checks as in reading a cell safely; here they are applied to the A* maze.
دەبێت دوو جار بپشکنرێت: ئایا شێوەکەی دروستە (دوو ) و ئایا خانەیەکی دروستە (لە ناو و دیوار نییە)؟ int() بەکار بهێنە، هەرگیز eval() نا (بۆچی eval نا) و ValueError بگرە بۆ دەقێک کە ژمارە نییە. ئەمانە هەمان ئەو پشکنینانەن کە لە خوێندنەوەی خانەیەک بە سەلامەتیدا هەن؛ لێرەدا لەسەر بەکار دەهێنرێن.
from astar_grid import MAZE, Point, is_open, solve
def ask_point(prompt, maze):
while True:
text = input(prompt)
try:
x, y = map(int, text.split(",")) # "6,0" -> 6 and 0 (never eval!)
except ValueError:
print("Please type row,column - for example 6,0")
continue
p = Point(x, y)
if is_open(maze, p): # inside the maze and not a wall
return p
print("That cell is outside the maze or a wall. Try again.")
for row in MAZE:
print("|" + row + "|")
while True:
start = ask_point("Start (row,column): ", MAZE)
end = ask_point("Goal (row,column): ", MAZE)
path = solve(MAZE, start, end)
if path:
print("Path:", path)
print("Steps:", len(path) - 1)
else:
print("The goal cannot be reached from the start.")
if input("Try again? (yes/no): ").strip().lower() != "yes":
break|# | |# ######### | |# # # | |# # ##### # | |# # # | |### # ##### | | # | Start (row,column): 0,0 That cell is outside the maze or a wall. Try again. Start (row,column): -1,3 That cell is outside the maze or a wall. Try again. Start (row,column): a Please type row,column - for example 6,0 Start (row,column): 6,0 Goal (row,column): 0,1 Path: [(6, 0), (6, 1), (6, 2), (6, 3), (5, 3), (4, 3), (4, 2), (4, 1), (3, 1), (2, 1), (1, 1), (0, 1)] Steps: 11 Try again? (yes/no): yes Start (row,column): 1,11 Goal (row,column): 1,11 Path: [(1, 11)] Steps: 0 Try again? (yes/no): no
(0, 0) is a wall, (−1, 3) is outside the maze, "a" is not a number. A start equal to the goal gives a path of one cell and 0 steps.
(0, 0) دیوارە، (−1, 3) لە دەرەوەی ، «a» ژمارە نییە. سەرەتایەک کە یەکسان بێت بە ئامانج ڕێگایەکی یەک خانەیی و 0 هەنگاو دەدات.
Explain the codeڕوونکردنەوەی کۆدەکە
- Lineدێڕی 6-8
Read one line and split it at the comma: "6,0" → 6 and 0. Spaces around the numbers are allowed.
یەک دێڕ بخوێنەوە و لە فاریزەکەدا لەتی بکە: «6,0» ← 6 و 0. بۆشایی بە دەوری ژمارەکاندا ڕێگەپێدراوە.
- Lineدێڕی 9-11
"a" or "6" alone raises
ValueError: show the right format and ask again.«a» یان «6» بە تەنیا
ValueErrorدروست دەکەن: شێوە دروستەکە نیشان بدە و دووبارە بپرسە.- Lineدێڕی 12-15
The number format is fine; now check the cell. Negative numbers and walls are rejected.
شێوەی ژمارەکان دروستە؛ ئێستا خانەکە بپشکنە. و دیوارەکان ڕەت دەکرێنەوە.
- Lineدێڕی 21-31
Ask, solve, print, and ask whether to try again.
بپرسە، چارەسەر بکە، چاپ بکە و بپرسە ئایا دووبارە هەوڵ بدرێتەوە.
Task 3: several goals, go to the nearestئەرکی 3: چەند ئامانجێک، بڕۆ بۆ نزیکترینیان
Two changes turn A* into a nearest-goal search. The goal test: stop when the point taken out is any goal. The heuristic: use the Manhattan distance to the nearest goal, min(...) over all goals. This h never overestimates, so the first goal taken out is the nearest one.
دوو گۆڕانکاری دەکەن بە گەڕان بۆ نزیکترین ئامانج. : کاتێک خاڵی دەرهێنراو هەر ئامانجێک بوو بوەستە. : تا نزیکترین ئامانج بەکار بهێنە، min(...) بەسەر هەموو ئامانجەکاندا. ئەم hـە هەرگیز زیاد ناخەمڵێنێت، بۆیە یەکەم ئامانج کە دەردەهێنرێت نزیکترینیانە.
import heapq
from astar_grid import MAZE, Point, is_open, manhattan, backtrack
def solve_nearest(maze, start, goals):
"""A* that stops at the first goal it selects: the nearest one."""
def h(p): # distance to the NEAREST goal
return min(manhattan(p, goal) for goal in goals)
count = 0
frontier = [(h(start), count, start)]
g = {start: 0}
parent = {start: None}
closed = set()
while frontier:
_, _, current = heapq.heappop(frontier)
if current in closed:
continue
if current in goals: # any goal ends the search
return backtrack(parent, current), g[current]
closed.add(current)
for dx, dy in [(-1, 0), (0, -1), (1, 0), (0, 1)]:
nxt = Point(current.x + dx, current.y + dy)
if is_open(maze, nxt) and g[current] + 1 < g.get(nxt, float("inf")):
g[nxt] = g[current] + 1
parent[nxt] = current
count += 1
heapq.heappush(frontier, (g[nxt] + h(nxt), count, nxt))
return None, None
start = Point(6, 0)
goals = {Point(0, 11), Point(4, 8)}
path, cost = solve_nearest(MAZE, start, goals)
print("Nearest goal:", path[-1])
print("Path:", path)
print("Cells on the path:", len(path))
print("Steps:", len(path) - 1, "| cost:", cost)Nearest goal: (4, 8) Path: [(6, 0), (6, 1), (6, 2), (6, 3), (5, 3), (4, 3), (3, 3), (2, 3), (2, 4), (2, 5), (2, 6), (2, 7), (2, 8), (2, 9), (3, 9), (4, 9), (4, 8)] Cells on the path: 17 Steps: 16 | cost: 16
The nearer goal is (4, 8): 17 cells, 16 steps. Because each step costs 1, the cost is also 16.
ئامانجە نزیکترەکە (4, 8)ـە: 17 خانە، 16 هەنگاو. چونکە هەر هەنگاوێک کۆستی 1ـە، کۆستەکەش 16ـە.
Explain the codeڕوونکردنەوەی کۆدەکە
- Lineدێڕی 7-8
h(p) = the smallest Manhattan distance from p to any goal.
h(p) = بچووکترین لە pـەوە تا هەر ئامانجێک.
- Lineدێڕی 19-20
current in goalsworks because points have__eq__and__hash__. The cost g of that goal is returned too.current in goalsکار دەکات چونکە خاڵەکان__eq__و__hash__یان هەیە. کۆستی gـی ئەو ئامانجەش دەگەڕێنرێتەوە.- Lineدێڕی 32-33
Two goals: the far corner (0, 11) and the cell (4, 8).
دوو ئامانج: گۆشە دوورەکە (0, 11) و خانەی (4, 8).
Task 4: visit all the goalsئەرکی 4: سەردانی هەموو ئامانجەکان بکە
Run A* once for each goal: from the start to goal 1, from goal 1 to goal 2, and so on. Join the pieces, without repeating the point where two pieces meet. But first check every goal: the first version of this task used the goal (5, 7), which is a wall.
بۆ هەر ئامانجێک یەک جار جێبەجێ بکە: لە سەرەتاوە بۆ ئامانجی 1، لە ئامانجی 1ـەوە بۆ ئامانجی 2 و بەم شێوەیە. پارچەکان پێکەوە ببەستە، بەبێ دووبارەکردنەوەی ئەو خاڵەی دوو پارچە تێیدا بەیەک دەگەن. بەڵام یەکەمجار هەموو ئامانجێک بپشکنە: یەکەم وەشانی ئەم ئەرکە ئامانجی (5, 7)ـی بەکار هێنا، کە دیوارە.
from astar_grid import MAZE, Point, is_open, solve
def visit_all_goals(maze, start, goals):
for goal in goals: # check the goals BEFORE searching
if not is_open(maze, goal):
print(f"Goal {goal} is a wall or outside the maze.")
return None
route = [start]
current = start
for goal in goals: # the goals in the given order
path = solve(maze, current, goal)
if path is None:
print(f"No path from {current} to {goal}.")
return None
route += path[1:] # path[0] is already in the route
current = goal
return route
start = Point(6, 0)
for goals in ([Point(0, 11), Point(2, 3), Point(5, 7)],
[Point(0, 11), Point(2, 3), Point(5, 5)]):
print("Goals:", goals)
route = visit_all_goals(MAZE, start, goals)
if route:
print("Route:", route)
print("Total steps:", len(route) - 1)
print()Goals: [(0, 11), (2, 3), (5, 7)] Goal (5, 7) is a wall or outside the maze. Goals: [(0, 11), (2, 3), (5, 5)] Route: [(6, 0), (6, 1), (6, 2), (6, 3), (5, 3), (4, 3), (4, 2), (4, 1), (3, 1), (2, 1), (1, 1), (0, 1), (0, 2), (0, 3), (0, 4), (0, 5), (0, 6), (0, 7), (0, 8), (0, 9), (0, 10), (0, 11), (0, 10), (0, 9), (0, 8), (0, 7), (0, 6), (0, 5), (0, 4), (0, 3), (0, 2), (0, 1), (1, 1), (2, 1), (3, 1), (4, 1), (4, 2), (4, 3), (3, 3), (2, 3), (2, 4), (2, 5), (2, 6), (2, 7), (2, 8), (2, 9), (3, 9), (4, 9), (4, 8), (4, 7), (4, 6), (4, 5), (5, 5)] Total steps: 52
With (5, 7) the program stops at once with a clear message. With (5, 5) the route has 52 steps: up to (0, 11), back along row 0 to (2, 3), then to (5, 5).
بە (5, 7) دەستبەجێ بە پەیامێکی ڕوون دەوەستێت. بە (5, 5) ڕێگاکە 52 هەنگاوە: بەرەو سەرەوە تا (0, 11)، بە درێژایی ڕیزی 0 دەگەڕێتەوە تا (2, 3)، پاشان بۆ (5, 5).
Explain the codeڕوونکردنەوەی کۆدەکە
- Lineدێڕی 5-8
Check all goals before any search. A wall can never be reached, so searching for it only wastes time.
پێش هەر گەڕانێک هەموو ئامانجەکان بپشکنە. دیوار هەرگیز ناگەیشترێت، بۆیە گەڕان بۆی تەنیا کات بەفیڕۆ دەدات.
- Lineدێڕی 11-18
Search piece by piece.
path[1:]skips the first point, which is already the end of the route so far.پارچە بە پارچە بگەڕێ.
path[1:]یەکەم خاڵ تێدەپەڕێنێت، کە هەر کۆتایی ڕێگاکەی تا ئێستایە.- Lineدێڕی 21-29
The goals of the first version, then the same goals with the wall replaced by the open cell (5, 5).
ئامانجەکانی یەکەم وەشان، پاشان هەمان ئامانجەکان کە دیوارەکە بە خانە کراوەکەی (5, 5) گۆڕدراوە.
Task 4: visit all the goalsئەرکی 4: سەردانی هەموو ئامانجەکان بکە
Extra: the best order of the goalsزیادە: باشترین ڕیزبەندیی ئامانجەکان
The task visits the goals in the given order, which is not the shortest tour. With three goals there are only 3 × 2 × 1 = 6 orders, so we can simply try them all with itertools.permutations:
ئەرکەکە سەردانی ئامانجەکان بەو ڕیزبەندییەی دراوە دەکات، کە کورتترین گەشت نییە. بە سێ ئامانج تەنیا 3 × 2 × 1 = 6 ڕیزبەندی هەیە، بۆیە دەتوانین بە سادەیی هەموویان بە itertools.permutations تاقی بکەینەوە:
from itertools import permutations
from astar_grid import MAZE, Point, solve
def steps(a, b):
return len(solve(MAZE, a, b)) - 1
start = Point(6, 0)
goals = [Point(0, 11), Point(2, 3), Point(5, 5)]
best = None
for order in permutations(goals): # 3 goals -> 3 x 2 x 1 = 6 orders
total = 0
current = start
for goal in order:
total += steps(current, goal)
current = goal
print(order, total)
if best is None or total < best[0]:
best = (total, order)
print("Best order:", best[1], "with", best[0], "steps")((0, 11), (2, 3), (5, 5)) 52 ((0, 11), (5, 5), (2, 3)) 47 ((2, 3), (0, 11), (5, 5)) 38 ((2, 3), (5, 5), (0, 11)) 33 ((5, 5), (0, 11), (2, 3)) 51 ((5, 5), (2, 3), (0, 11)) 51 Best order: ((2, 3), (5, 5), (0, 11)) with 33 steps
The given order costs 52 steps; the best order, (2, 3) → (5, 5) → (0, 11), needs only 33.
ڕیزبەندییە دراوەکە 52 هەنگاو دەکات؛ باشترین ڕیزبەندی، (2, 3) ← (5, 5) ← (0, 11)، تەنیا 33ی پێویستە.
Explain the codeڕوونکردنەوەی کۆدەکە
- Lineدێڕی 5-6
The number of steps between two points, using A*.
ژمارەی هەنگاوەکان لە نێوان دوو خاڵدا، بە .
- Lineدێڕی 12-20
Try every order, add up the steps, keep the best.
هەموو ڕیزبەندییەک تاقی بکەرەوە، هەنگاوەکان کۆ بکەرەوە و باشترینیان ڕابگرە.
What the travelling salesman problem is and why trying every order of the cities quickly becomes impossible.
چییە و بۆچی تاقیکردنەوەی هەموو ڕیزبەندییەکانی شارەکان زوو دەبێتە کارێکی مەحاڵ.
Watch 1:27-5:35: the problem and why brute force over every order explodes.
لە 1:27 تا 5:35 سەیر بکە: کێشەکە و بۆچی تاقیکردنەوەی هەموو ڕیزبەندییەکان (brute force) زۆر بە خێرایی لە توانادا نامێنێت.
Task 5 (midterm exam): deliver a parcel on a road mapئەرکی 5 (تاقیکردنەوەی نیوەی وەرز): گەیاندنی پاکەتێک لەسەر نەخشەی ڕێگاکان
The real midterm question used towns instead of a grid. A list of roads connects Slemani, Arbat, Said Sadiq and Halabja. The program turns the list into a graph (a dictionary: town → neighbouring towns), finds a path with a recursive depth-first search and prints one line for each road the parcel takes.
پرسیاری ڕاستەقینەی تاقیکردنەوەی نیوەی وەرز شارەکانی لەبری تۆڕی خانەکان بەکار هێنا. لیستێک لە ڕێگاکان سلێمانی، عەربەت، سەیدسادق و هەڵەبجە پێکەوە دەبەستێتەوە. لیستەکە دەکات بە (: شار ← شارە دراوسێکان)، بە گەڕانی قووڵایی خۆبانگکەرەوە ڕێگایەک دەدۆزێتەوە و بۆ هەر ڕێگایەک کە پاکەتەکە پێیدا دەڕوات دێڕێک چاپ دەکات.
roads = [["Slemani", "Arbat"], ["Arbat", "Said Sadiq"], ["Said Sadiq", "Halabja"]]
def roads_to_graph(roads):
graph = {}
for town1, town2 in roads:
if town1 not in graph:
graph[town1] = []
if town2 not in graph:
graph[town2] = []
graph[town1].append(town2) # a road works both ways
graph[town2].append(town1)
return graph
def find_path(graph, start, end, visited=None):
if visited is None: # a NEW set for every first call
visited = set()
if start == end:
return [start]
visited.add(start)
for neighbour in graph[start]:
if neighbour not in visited:
path = find_path(graph, neighbour, end, visited) # recursion
if path:
return [start] + path
return None
def deliver_parcel(graph, start, destination):
path = find_path(graph, start, destination)
if path is None:
print("No path found!")
else:
for i in range(len(path) - 1):
print(f"Moving from {path[i]} to {path[i + 1]}")
graph = roads_to_graph(roads)
print(graph)
deliver_parcel(graph, "Slemani", "Halabja"){'Slemani': ['Arbat'], 'Arbat': ['Slemani', 'Said Sadiq'], 'Said Sadiq': ['Arbat', 'Halabja'], 'Halabja': ['Said Sadiq']}
Moving from Slemani to Arbat
Moving from Arbat to Said Sadiq
Moving from Said Sadiq to HalabjaThe first line is the graph dictionary; then one line per road.
دێڕی یەکەم ؛ پاشان دێڕێک بۆ هەر ڕێگایەک.
Explain the codeڕوونکردنەوەی کۆدەکە
- Lineدێڕی 1
Each road is a pair of towns.
هەر ڕێگایەک جووتێک شارە.
- Lineدێڕی 4-13
Build the graph. A road works both ways, so each town is added to the other's list.
دروست بکە. ڕێگا بە هەردوو ئاڕاستەدا کار دەکات، بۆیە هەر شارێک بۆ لیستی ئەوی تر زیاد دەکرێت.
- Lineدێڕی 16-18
visited=None, then a newset()inside: every first call gets its own empty set. (Avisited=set()default would be shared by all calls; see the mistakes below.)visited=None، پاشانset()ـێکی نوێ لە ناوەوە: هەر یەکەم بانگکردنێک بەتاڵەکەی خۆی وەردەگرێت. (visited=set()لە نێوان هەموو بانگکردنەکاندا هاوبەش دەبێت؛ هەڵەکانی خوارەوە ببینە.)- Lineدێڕی 19-20
Base case: already at the destination.
حاڵەتی بنەڕەتی: پێشتر لە مەبەستەکەداین.
- Lineدێڕی 22-26
Recursion: try each unvisited neighbour; if it reaches the end, put the current town in front of its path.
: هەر دراوسێیەکی سەردان نەکراو تاقی بکەرەوە؛ ئەگەر گەیشتە کۆتایی، شاری ئێستا بخە پێش ڕێگاکەی.
- Lineدێڕی 30-36
Print "Moving from A to B" for each road of the path.
بۆ هەر ڕێگایەکی سەر ڕێگاکە «Moving from A to B» چاپ بکە.
Task 5 (midterm exam): deliver a parcel on a road mapئەرکی 5 (تاقیکردنەوەی نیوەی وەرز): گەیاندنی پاکەتێک لەسەر نەخشەی ڕێگاکان
Fewest roads or fewest kilometres?کەمترین ڕێگا یان کەمترین کیلۆمەتر؟
On a bigger map, depth-first search finds a path, not the best one. Two questions have two different answers: fewest roads is found by breadth-first search with a queue (collections.deque, as in the maze solver), and fewest kilometres by A* (or uniform-cost search). The program below uses the practice map of the A* lesson and imports its town_astar.py. Remember: the town names are real, but the map is not to scale and all its distances are made up.
لەسەر نەخشەیەکی گەورەتر، ڕێگایەک دەدۆزێتەوە، نەک باشترینیان. دوو پرسیار دوو وەڵامی جیاوازیان هەیە: کەمترین ڕێگا بە گەڕانی پانایی و دەدۆزرێتەوە (collections.deque، وەک لە چارەسەرکەری لابیرینت) و کەمترین کیلۆمەتر بە (یان ). خوارەوە نەخشەی ڕاهێنانی وانەی گەڕانی A* بەکار دەهێنێت و town_astar.pyـەکەی هاوردە دەکات. لەبیرت بێت: ناوی شارەکان ڕاستەقینەن، بەڵام نەخشەکە بە ڕێژەی ڕاستەقینە نەکێشراوە و هەموو دوورییەکانی دەستکردن.
from collections import deque
from town_astar import ROADS, STRAIGHT_LINE, roads_to_graph, a_star
def fewest_roads(graph, start, goal):
"""Breadth-first search: the path with the fewest roads."""
frontier = deque([start]) # a FIFO queue
parent = {start: None} # also our 'reached' set
while frontier:
town = frontier.popleft() # the oldest town first
if town == goal:
path = []
while town is not None:
path.append(town)
town = parent[town]
return path[::-1]
for nxt in graph[town]:
if nxt not in parent:
parent[nxt] = town
frontier.append(nxt)
return None
def km(graph, path):
return sum(graph[a][b] for a, b in zip(path, path[1:]))
graph = roads_to_graph(ROADS)
path = fewest_roads(graph, "Slemani", "Halabja")
print("BFS:", " -> ".join(path), "|", len(path) - 1, "roads |", km(graph, path), "km")
cost, path, _ = a_star(graph, STRAIGHT_LINE, "Slemani", "Halabja")
print("A*: ", " -> ".join(path), "|", len(path) - 1, "roads |", cost, "km")BFS: Slemani -> Darbandikhan -> Halabja | 2 roads | 100 km A*: Slemani -> Arbat -> Said Sadiq -> Halabja | 3 roads | 72 km
BFS finds 2 roads but 100 km; A* finds 72 km with 3 roads. "Shortest" depends on what you count.
2 ڕێگا بەڵام 100 کم دەدۆزێتەوە؛ 72 کم بە 3 ڕێگا دەدۆزێتەوە. «کورتترین» بەستراوەتەوە بەوەی چی دەژمێریت.
Explain the codeڕوونکردنەوەی کۆدەکە
- Lineدێڕی 2
Reuse the roads, the heuristic and
a_star()from the A* lesson's file.ڕێگاکان، و
a_star()لە فایلەکەی وانەی دووبارە بەکار بهێنەوە.- Lineدێڕی 7-8
A FIFO queue and a parent dictionary, which is also the set of reached towns.
FIFO و باوکەکان، کە کۆمەڵەی شارە گەیشتراوەکانیشە.
- Lineدێڕی 10
popleft()takes the oldest town: breadth-first order.popleft()کۆنترین شار وەردەگرێت: ڕیزبەندیی .- Lineدێڕی 24-25
Add up the kilometres of a path, just to compare.
کیلۆمەترەکانی ڕێگایەک کۆ بکەرەوە، تەنیا بۆ بەراورد.
Common mistakesهەڵە باوەکان
| Mistakeهەڵە | What happensچی ڕوو دەدات | Fixچارەسەر |
|---|---|---|
| A goal on a wallئامانجێک لەسەر دیوار | A* searches the whole maze and fails. هەموو دەگەڕێت و سەرناکەوێت. | Check every goal with is_open() first.یەکەمجار هەموو ئامانجێک بە is_open() بپشکنە. |
eval(input())eval(input()) | The user can run any Python code.بەکارهێنەر دەتوانێت هەر کۆدێکی جێبەجێ بکات. | int() inside try … except ValueError.int() لە ناو try … except ValueError. |
Checking only x < len(maze)تەنیا پشکنینی x < len(maze) | −1 is accepted and means the last row.−1 قبوڵ دەکرێت و واتای دوایین ڕیزە. | 0 <= x < len(maze)0 <= x < len(maze) |
Point without __eq__ and __hash__Point بەبێ __eq__ و __hash__ | current == end is never true: the base program searches forever.current == end هەرگیز ڕاست نابێت: بۆ هەمیشە دەگەڕێت. | Add both methods.هەردوو زیاد بکە. |
visited=set() as a defaultvisited=set() وەک | The second call finds no path.بانگکردنی دووەم هیچ ڕێگایەک نادۆزێتەوە. | visited=None, then set() inside.visited=None، پاشان set() لە ناوەوە. |
| Printing cells as "steps"چاپکردنی خانەکان وەک «هەنگاو» | Every answer is one too big.هەموو وەڵامێک یەک زیاترە. | steps = len(path) - 1هەنگاوەکان = len(path) - 1 |
Exercisesڕاهێنانەکان
Change task_input.py so that the goal cannot be the same cell as the start. The program should say so and ask for the goal again.
task_input.py بگۆڕە بۆ ئەوەی ئامانج نەتوانێت هەمان خانەی سەرەتا بێت. دەبێت ئەمە بڵێت و دووبارە داوای ئامانج بکات.
Show answerوەڵامەکە پیشان بدە
start = ask_point("Start (row,column): ", MAZE)
end = ask_point("Goal (row,column): ", MAZE)
while end == start:
print("The goal must be a different cell.")
end = ask_point("Goal (row,column): ", MAZE)Explain the codeڕوونکردنەوەی کۆدەکە
- Lineدێڕی 3-5
==compares row and column becausePointhas__eq__.==ڕیز و ستوون بەراورد دەکات چونکەPoint__eq__ـی هەیە.
When you test Task 1, it helps to get the same "random" cells on every run. Which one line makes that happen, and where does it go?
کاتێک ئەرکی 1 تاقی دەکەیتەوە، باشە لە هەموو جێبەجێکردنێکدا هەمان خانە «هەڕەمەکی»یەکان وەربگریت. کام یەک دێڕ ئەمە دەکات و لە کوێ دادەنرێت؟
Show answerوەڵامەکە پیشان بدە
Add random.seed(1) (any fixed number) right after import random. The same seed always gives the same sequence of random numbers, so every run picks the same start and goal. Remove it again for the real task.
random.seed(1) (هەر ژمارەیەکی جێگیر) ڕاستەوخۆ دوای import random زیاد بکە. هەمان تۆو هەمیشە هەمان زنجیرەی دەدات، بۆیە هەموو جێبەجێکردنێک هەمان سەرەتا و ئامانج هەڵدەبژێرێت. بۆ ئەرکە ڕاستەقینەکە دووبارە لای ببە.
import random
random.seed(1) # the same "random" cells on every run
from astar_grid import MAZE, Point, is_open, solveRandom start: (6, 1), random goal: (2, 1) Path: [(6, 1), (6, 2), (6, 3), (5, 3), (4, 3), (4, 2), (4, 1), (3, 1), (2, 1)] Steps: 8
Your teacher allows diagonal moves too, each costing 1. Which two parts of astar_grid.py must change, and why is Manhattan distance no longer a safe heuristic?
مامۆستاکەت ڕێگەی جووڵەی لاریش دەدات، هەریەکەیان بە کۆستی 1. کام دوو بەشی astar_grid.py دەبێت بگۆڕێن و بۆچی چیتر بێمەترسی نییە؟
Show answerوەڵامەکە پیشان بدە
Add the four diagonal moves (-1, -1), (-1, 1), (1, -1), (1, 1) to the directions list, and change the heuristic. One diagonal step now covers one row and one column, so Manhattan distance can say 2 for a cell that is 1 step away: it overestimates, and A* may miss the shortest path. Use max(abs(dx), abs(dy)) instead, which is exact when there are no walls.
چوار جووڵە لارەکە (-1, -1), (-1, 1), (1, -1), (1, 1) بۆ لیستی ئاڕاستەکان زیاد بکە و بگۆڕە. یەک هەنگاوی لار ئێستا یەک ڕیز و یەک ستوون دەبڕێت، بۆیە دەشێت بۆ خانەیەک کە 1 هەنگاو دوورە بڵێت 2: زیاد دەخەمڵێنێت و لەوانەیە لەدەست بدات. لەبری ئەوە max(abs(dx), abs(dy)) بەکار بهێنە، کە کاتێک دیوار نییە وردە.
A new road Arbat–Halabja of 50 km is built on the practice map. Without running the code: how many roads and kilometres will breadth-first search report now, and what will A* report?
ڕێگایەکی نوێی عەربەت–هەڵەبجە بە درێژیی 50 کم لەسەر نەخشەی ڕاهێنان دروست دەکرێت. بەبێ جێبەجێکردنی کۆدەکە: ئێستا چەند ڕێگا و کیلۆمەتر ڕادەگەیەنێت و چی ڕادەگەیەنێت؟
Hintئاماژە
Slemani–Arbat is 24 km.
سلێمانی–عەربەت 24 کمە.
Show answerوەڵامەکە پیشان بدە
Two routes now use only 2 roads: Slemani–Darbandikhan–Halabja (100 km) and Slemani–Arbat–Halabja (24 + 50 = 74 km). Breadth-first search returns the one it reaches first; it does not compare kilometres. In this program Arbat is Slemani's first neighbour, so BFS reports Slemani → Arbat → Halabja: 2 roads, 74 km. A* still returns the 72 km route through Said Sadiq, because 72 < 74 (checked by adding the road to ROADS).
ئێستا دوو گەشت هەن کە هەریەکەیان تەنیا 2 ڕێگا بەکار دەهێنێت: سلێمانی–دەربەندیخان–هەڵەبجە (100 کم) و سلێمانی–عەربەت–هەڵەبجە (24 + 50 = 74 کم). ئەوەیان دەگەڕێنێتەوە کە یەکەمجار پێی دەگات؛ کیلۆمەتر بەراورد ناکات. لەم عەربەت یەکەم دراوسێی سلێمانییە، بۆیە سلێمانی ← عەربەت ← هەڵەبجە ڕادەگەیەنێت: 2 ڕێگا، 74 کم. هێشتا گەشتە 72 کمییەکە بە سەیدسادقدا دەگەڕێنێتەوە، چونکە 72 < 74 (بە زیادکردنی ڕێگاکە بۆ ROADS پشکنراوە).
Summaryپوختە
- Put the search in one module (
astar_grid.py) and change only what the question asks. - Give
Point__eq__and__hash__so points work in==, sets and dictionaries. - Random cells:
random.randint()includes both ends; draw again until the cell is open. - Typed cells:
int()+try/except ValueError+0 <= x < rows+ not a wall. Nevereval(). - Nearest goal: stop at the first goal taken out; h = distance to the nearest goal.
- All goals: check them first, then search piece by piece; the given order is not always the best.
- Road maps: DFS finds a path, BFS the fewest roads, A* the fewest kilometres.
- گەڕانەکە لە یەک دابنێ (
astar_grid.py) و تەنیا ئەوە بگۆڕە کە پرسیارەکە داوای دەکات. __eq__و__hash__بدە بەPointبۆ ئەوەی خاڵەکان لە==، و کار بکەن.- خانەی هەڕەمەکی:
random.randint()هەردوو سەرەکە لەخۆ دەگرێت؛ دووبارە بکێشە تا خانەکە کراوە دەبێت. - خانەی نووسراو:
int()+try/except ValueError+0 <= x < rows+ دیوار نەبێت. هەرگیزeval()نا. - نزیکترین ئامانج: لە یەکەم ئامانجی دەرهێنراودا بوەستە؛ h = دووری تا نزیکترین ئامانج.
- هەموو ئامانجەکان: یەکەمجار بیانپشکنە، پاشان پارچە بە پارچە بگەڕێ؛ ڕیزبەندییە دراوەکە هەمیشە باشترین نییە.
- نەخشەی ڕێگاکان: ڕێگایەک دەدۆزێتەوە، کەمترین ڕێگا، کەمترین کیلۆمەتر.
How do you turn A* into a nearest-goal search?چۆن گەڕانی A* دەکەیت بە گەڕان بۆ نزیکترین ئامانج؟
Stop when the point taken out is in the set of goals, and use h(p) = the smallest Manhattan distance from p to any goal.
کاتێک خاڵی دەرهێنراو لە ئامانجەکاندا بوو بوەستە و h(p) = بچووکترین لە pـەوە تا هەر ئامانجێک بەکار بهێنە.
Why check the goals before searching?بۆچی پێش گەڕان ئامانجەکان دەپشکنیت؟
A goal on a wall or outside the maze can never be reached. Without the check, A* explores the whole maze before it gives up.
ئامانجێک لەسەر دیوار یان لە دەرەوەی هەرگیز ناگەیشترێت. بەبێ پشکنین، هەموو دەپشکنێت پێش ئەوەی واز بهێنێت.
What is the difference between "Path length" and "Steps"?جیاوازیی نێوان «Path length» و «Steps» چییە؟
Path length counts cells, including the start. Steps = cells − 1. With step cost 1, the cost equals the steps.
Path length خانەکان دەژمێرێت، لەگەڵ سەرەتا. هەنگاوەکان = خانەکان − 1. بە 1، کۆست یەکسانە بە هەنگاوەکان.
Lesson complete!وانەکە تەواو بوو!
Well done. Review the key terms, then go on to the next lesson.دەستخۆش. سەیرێکی زاراوە سەرەکییەکان بکەرەوە، پاشان بڕۆ بۆ وانەی داهاتوو.
Key termsزاراوە سەرەکییەکان
Hill Climbing and Its Problemsچیاهەڵەگڕی و کێشەکانی
Related lecturesوانە پەیوەندیدارەکان
- Heuristics and Greedy Best-First Searchپێوەرەکان و گەڕانی چاوچنۆکانە
- Solving a Maze with DFS and BFSچارەسەرکردنی لابیرینت بە DFS و BFS
- Depth-Limited, Uniform-Cost, Iterative Deepening and Bidirectional Searchگەڕانی قووڵایی سنووردار، گەڕانی کەمترین کۆست، گەڕانی قووڵکردنەوەی پلەبەپلە و گەڕانی دووئاڕاستە
References and further readingسەرچاوەکان و خوێندنەوەی زیاتر
- Python documentation: random (randint, seed)
- Python documentation: collections.deque
- Python tutorial: default argument values (evaluated only once)
- Python documentation: eval() (never use it on user input)
- Harvard CS50 AI, Project 0: Degrees (shortest path with breadth-first search)
- Red Blob Games: Introduction to the A* Algorithm
This lecture is a first edition. The Kurdish text and terminology are waiting for review by Kurdish IT teachers.ئەم وانەیە چاپی یەکەمە. دەقە کوردییەکە و زاراوەکانی چاوەڕێی پێداچوونەوەی مامۆستایانی کوردی بواری IT ن.