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.

پێنج ئەرکی وەک تاقیکردنەوە لەسەر : سەرەتا و ئامانجی هەڕەمەکی، نووسراو و پشکنراو، نزیکترینی چەند ئامانجێک، سەردانی هەموو ئامانجەکان و گەیاندنی پاکەتێک لەسەر نەخشەی ڕێگاکان بە ، و ، هەریەکەیان بە کۆدی پاک و تاقیکراوە و یەکەم وەشانەکان چاک کراون.

18 min read16 خولەک خوێندنەوە · 13 stepsهەنگاو

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().

astar_grid.pyPYTHONType in a terminalلە تێرمیناڵدا بینووسە
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)
Explain the codeڕوونکردنەوەی کۆدەکە
Lineدێڕی 3-12

The Lab 4 maze: # is a wall, a space is an open cell.

تاقیگەی 4: # دیوارە، بۆشایی خانەیەکی کراوەیە.

Lineدێڕی 23-24

__eq__: two Point objects are equal when their row and column are equal. Without it, Point(4, 8) == Point(4, 8) is False, 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.

نموونەکە تەنیا کاتێک کار دەکات کە خودی ئەم فایلە جێبەجێ بکەیت، نەک کاتێک فایلێکی تر هاوردەی دەکات.

The Lab 4 maze with row numbers 0 to 6 and column numbers 0 to 11. The start (6, 0) is highlighted. The goal cells used in the tasks are marked: (0, 11), (4, 8), (2, 3) and (5, 5). The cell (5, 7) is crossed out: it is a wall, so a goal there can never be reached.لابیرینتی تاقیگەی 4 بە ژمارەی ڕیزەکانی 0 تا 6 و ژمارەی ستوونەکانی 0 تا 11. سەرەتای (6, 0) دیار کراوە. ئەو خانانەی لە ئەرکەکاندا دەبنە ئامانج نیشانە کراون: (0, 11)، (4, 8)، (2, 3) و (5, 5). خانەی (5, 7) خەتی بەسەردا هێنراوە: دیوارە، بۆیە ئامانجێک لەوێ هەرگیز ناگەیشترێت.
The maze of all the tasks, with (row, column) numbers. هەموو ئەرکەکان، بە ژمارەکانی (ڕیز، ستوون).CêIT
Quick checkپشکنینی خێراWhy does Point need __eq__ and __hash__ in these tasks?بۆچی Point لەم ئەرکانەدا پێویستی بە __eq__ و __hash__ هەیە؟

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() هەڵبژێرە، کە هەردوو سەرەکە لەخۆ دەگرێت. دووبارە هەوڵ بدەرەوە تا خانەکە کراوە دەبێت. ئامانج بە هەمان شێوە هەڵبژێرە و تا لەگەڵ سەرەتا یەکسان بێت دووبارەی بکەرەوە.

task_random.pyPYTHONType in a terminalلە تێرمیناڵدا بینووسە
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.")
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: دەپشکنێت.

Quick checkپشکنینی خێراWhich call can return 6, the last row of the maze?کام بانگکردن دەتوانێت 6 بگەڕێنێتەوە، دوایین ڕیزی ؟

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 بگرە بۆ دەقێک کە ژمارە نییە. ئەمانە هەمان ئەو پشکنینانەن کە لە خوێندنەوەی خانەیەک بە سەلامەتیدا هەن؛ لێرەدا لەسەر بەکار دەهێنرێن.

task_input.pyPYTHONType in a terminalلە تێرمیناڵدا بینووسە
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
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.

بپرسە، چارەسەر بکە، چاپ بکە و بپرسە ئایا دووبارە هەوڵ بدرێتەوە.

Quick checkپشکنینی خێراThe user types -1,3. Why must the program reject it, even though maze[-1] works in Python?بەکارهێنەر -1,3 دەنووسێت. بۆچی دەبێت ڕەتی بکاتەوە، هەرچەندە maze[-1] لە کار دەکات؟

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ـە هەرگیز زیاد ناخەمڵێنێت، بۆیە یەکەم ئامانج کە دەردەهێنرێت نزیکترینیانە.

task_nearest_goal.pyPYTHONType in a terminalلە تێرمیناڵدا بینووسە
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)
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 goals works 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)⁩.

Quick checkپشکنینی خێراA path has 17 cells and every step costs 1. How many steps, and what cost?ڕێگایەک ⁦17⁩ خانەی هەیە و هەر هەنگاوێک کۆستی 1ـە. چەند هەنگاو و چ کۆستێک؟

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)⁩ـی بەکار هێنا، کە دیوارە.

task_all_goals.pyPYTHONType in a terminalلە تێرمیناڵدا بینووسە
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()
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)⁩ گۆڕدراوە.

Quick checkپشکنینی خێراWhy did the first version fail?بۆچی یەکەم وەشان سەرنەکەوت؟

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 تاقی بکەینەوە:

task_best_order.pyPYTHONType in a terminalلە تێرمیناڵدا بینووسە
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")
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.

هەموو ڕیزبەندییەک تاقی بکەرەوە، هەنگاوەکان کۆ بکەرەوە و باشترینیان ڕابگرە.

The Traveling Salesman Problem: When Good Enough Beats PerfectReducible · Englishئینگلیزی · 378K viewsبینین · 15K likesلایک · Partبەشی 1:27–5:35 (ofلە 30:26)

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.

پرسیاری ڕاستەقینەی تاقیکردنەوەی نیوەی وەرز شارەکانی لەبری تۆڕی خانەکان بەکار هێنا. لیستێک لە ڕێگاکان سلێمانی، عەربەت، سەیدسادق و هەڵەبجە پێکەوە دەبەستێتەوە. لیستەکە دەکات بە (: شار ← شارە دراوسێکان)، بە گەڕانی قووڵایی خۆبانگکەرەوە ڕێگایەک دەدۆزێتەوە و بۆ هەر ڕێگایەک کە پاکەتەکە پێیدا دەڕوات دێڕێک چاپ دەکات.

parcel_dfs.pyPYTHONType in a terminalلە تێرمیناڵدا بینووسە
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")
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 new set() inside: every first call gets its own empty set. (A visited=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⁩» چاپ بکە.

Quick checkپشکنینی خێراWhat does find_path() return when the start is already the destination?find_path() چی دەگەڕێنێتەوە کاتێک سەرەتا هەر خودی مەبەستەکەیە؟

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ـەکەی هاوردە دەکات. لەبیرت بێت: ناوی شارەکان ڕاستەقینەن، بەڵام نەخشەکە بە ڕێژەی ڕاستەقینە نەکێشراوە و هەموو دوورییەکانی دەستکردن.

The practice map of eight towns with made-up road lengths: the cheapest route from Slemani to Halabja, through Arbat and Said Sadiq, is 72 km; the direct-looking route through Darbandikhan uses only two roads but is 100 km.نەخشەی ڕاهێنانی هەشت شار بە درێژیی دەستکردی ڕێگاکان: هەرزانترین ڕێگا لە سلێمانییەوە بۆ هەڵەبجە، بە عەربەت و سەیدسادقدا، 72 کمە؛ ڕێگاکەی بە دەربەندیخاندا تەنیا دوو ڕێگا بەکار دەهێنێت بەڵام 100 کمە.
The same practice map as in the A* lesson: real town names, but not to scale, and all distances are made up.هەمان نەخشەی ڕاهێنانی وانەی : ناوی شارەکان ڕاستەقینەن، بەڵام بە ڕێژەی ڕاستەقینە نەکێشراوە و هەموو دوورییەکان دەستکردن.CêIT
roads_bfs.pyPYTHONType in a terminalلە تێرمیناڵدا بینووسە
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")
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.

کیلۆمەترەکانی ڕێگایەک کۆ بکەرەوە، تەنیا بۆ بەراورد.

Quick checkپشکنینی خێراA courier is paid per road and does not care about kilometres. Which search fits?گەیەنەرێک بۆ هەر ڕێگایەک پارە وەردەگرێت و گوێ بە کیلۆمەتر نادات. کام گەڕان دەگونجێت؟

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

Common mistakesهەڵە باوەکان

Mistake: a shared default setهەڵە: کۆمەڵەیەکی بنەڕەتیی هاوبەش

A default value is created once, when the function is defined. If the default is a set, every call that uses the default shares the same set:

تەنیا یەک جار دروست دەکرێت، کاتێک دەناسێنرێت. ئەگەر بێت، هەموو ئەو بانگکردنانەی بەکاری دەهێنن هەمان بەکار دەهێنن:

default_trap.pyPYTHONType in a terminalلە تێرمیناڵدا بینووسە
graph = {"Slemani": ["Arbat"], "Arbat": ["Slemani", "Said Sadiq"],
         "Said Sadiq": ["Arbat", "Halabja"], "Halabja": ["Said Sadiq"]}


def find_path(graph, start, end, visited=set()):   # WRONG: one set for ALL calls
    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)
            if path:
                return [start] + path
    return None


print(find_path(graph, "Slemani", "Halabja"))   # first delivery
print(find_path(graph, "Slemani", "Halabja"))   # second delivery: same question!
Explain the codeڕوونکردنەوەی کۆدەکە
Lineدێڕی 5

visited=set(): this one set lives as long as the function.

visited=set(): ئەم بە قەد تەمەنی دەمێنێت.

Python Tutorial: Clarifying the Issues with Mutable Default ArgumentsCorey Schafer · Englishئینگلیزی · 39K viewsبینین · 1.5K likesلایک

Why a mutable default value such as a list is created only once and shared by every call, and how None fixes it.

بۆچی کە دەکرێت بگۆڕدرێت (وەک ) تەنها یەک جار دروست دەکرێت و هەموو تێیدا هاوبەشن، هەروەها چۆن None چاکی دەکاتەوە.

Whole video: the same bug as visited=set(), shown with a list, and the fix with None. The datetime example at the end shows that a default value is made only once.

هەموو ڤیدیۆکە: هەمان کە لە visited=set() هەیە، بەڵام بە پیشان دەدرێت، لەگەڵ چاککردنەکەی بە None. نموونەی datetime لە کۆتاییدا دەریدەخات کە تەنها یەک جار دروست دەکرێت.

Exercisesڕاهێنانەکان

ExerciseڕاهێنانReject a goal equal to the startڕەتکردنەوەی ئامانجێکی یەکسان بە سەرەتا

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وەڵامەکە پیشان بدە
PYTHON
    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 because Point has __eq__.

== ڕیز و ستوون بەراورد دەکات چونکە Point __eq__ـی هەیە.

ExerciseڕاهێنانThe same random task every timeهەمان ئەرکی هەڕەمەکی هەموو جارێک

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 زیاد بکە. هەمان تۆو هەمیشە هەمان زنجیرەی دەدات، بۆیە هەموو جێبەجێکردنێک هەمان سەرەتا و ئامانج هەڵدەبژێرێت. بۆ ئەرکە ڕاستەقینەکە دووبارە لای ببە.

The top of task_random.py, with the seed (the rest stays the same)سەرەوەی ⁦task_random.py⁩ لەگەڵ تۆوەکە (باقییەکەی وەک خۆی دەمێنێتەوە)task_random.pyPYTHON
import random
random.seed(1)          # the same "random" cells on every run
from astar_grid import MAZE, Point, is_open, solve
ExerciseڕاهێنانEight directionsهەشت ئاڕاستە

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)) بەکار بهێنە، کە کاتێک دیوار نییە وردە.

ExerciseڕاهێنانA new roadڕێگایەکی نوێ

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. Never eval().
  • 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 = دووری تا نزیکترین ئامانج.
  • هەموو ئامانجەکان: یەکەمجار بیانپشکنە، پاشان پارچە بە پارچە بگەڕێ؛ ڕیزبەندییە دراوەکە هەمیشە باشترین نییە.
  • نەخشەی ڕێگاکان: ڕێگایەک دەدۆزێتەوە، کەمترین ڕێگا، کەمترین کیلۆمەتر.
Review questionsپرسیارەکانی پێداچوونەوە
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زاراوە سەرەکییەکان

Next lessonوانەی داهاتوو

Hill Climbing and Its Problemsچیاهەڵەگڕی و کێشەکانی

References and further readingسەرچاوەکان و خوێندنەوەی زیاتر

This lecture is a first edition. The Kurdish text and terminology are waiting for review by Kurdish IT teachers.ئەم وانەیە چاپی یەکەمە. دەقە کوردییەکە و زاراوەکانی چاوەڕێی پێداچوونەوەی مامۆستایانی کوردی بواری IT ن.