0Pricing
Coding Interview Prep · درس

BFS: أقصر مسار والاجتياز حسب المستويات

استخدم BFS للعثور على أقصر مسار في رسم بياني غير موزون، وحل word-ladder مستوىً تلو الآخر، واستنسخ رسمًا بيانيًا باستخدام خريطة تجزئة

BFS: أقصر مسار والاجتياز حسب المستويات درس مجاني في Coding Interview Prep على CoddyKit. هذا هو الدرس 2 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في Coding Interview Prep، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة Coding Interview Prep 4 دروس في المجموع.

BFS وأقصر مسار في الرسوم البيانية غير الموزونة

تعثر BFS على أقصر مسار (أي أقل عدد من الحواف) في الرسم البياني غير الموزون، لأنها تستكشف العقد بترتيب تزايد المسافة من المصدر. وعندما تصل BFS إلى عقدة للمرة الأولى، تكون قد وصلت إليها عبر أقصر مسار ممكن. ولا تنطبق هذه الخاصية على DFS. أما في الرسوم البيانية الموزونة ذات الأوزان غير السالبة، فاستخدموا خوارزمية Dijkstra بدلاً من ذلك، إذ تتعامل BFS ضمنياً مع جميع الحواف على أن وزنها 1.

from collections import deque, defaultdict

def shortest_path(graph, start, end):
    if start == end:
        return 0
    visited = {start}
    queue = deque([(start, 0)])  # (node, distance)
    while queue:
        node, dist = queue.popleft()
        for neighbour in graph[node]:
            if neighbour == end:
                return dist + 1
            if neighbour not in visited:
                visited.add(neighbour)
                queue.append((neighbour, dist + 1))
    return -1  # no path found

graph = defaultdict(list)
for u, v in [(0,1),(1,2),(2,3),(0,3),(1,4)]:
    graph[u].append(v); graph[v].append(u)
print(shortest_path(graph, 0, 3))  # 1 (direct edge)
print(shortest_path(graph, 0, 4))  # 2 (0->1->4)

تتبّع أقصر مسار الفعلي

لإعادة بناء المسار الفعلي (وليس طوله فقط)، احتفظوا بقاموس للعقد الأب يسجّل كيفية الوصول إلى كل عقدة. وعند الوصول إلى الوجهة، تتبّعوا خريطة الآباء من النهاية إلى البداية، ثم اعكسوا النتيجة. يضيف ذلك مساحة O(V) لخريطة الآباء، لكنه يوفّر المسار الكامل بزمن O(path_length) بعد اكتمال BFS.

from collections import deque, defaultdict

def shortest_path_with_route(graph, start, end):
    parent = {start: None}
    queue = deque([start])
    while queue:
        node = queue.popleft()
        if node == end:
            break
        for nb in graph[node]:
            if nb not in parent:
                parent[nb] = node
                queue.append(nb)
    if end not in parent:
        return []  # no path
    # Reconstruct path by tracing back
    path = []
    node = end
    while node is not None:
        path.append(node)
        node = parent[node]
    return path[::-1]  # reverse

graph = defaultdict(list)
for u, v in [(0,1),(1,2),(2,3),(0,4),(4,3)]:
    graph[u].append(v); graph[v].append(u)
print(shortest_path_with_route(graph, 0, 3))  # [0, 4, 3] or [0, 1, 2, 3]

Word Ladder: BFS على رسم بياني ضمني

تطلب مسألة Word Ladder (LeetCode #127) إيجاد الحد الأدنى من تغييرات الأحرف المفردة لتحويل كلمة بداية إلى كلمة نهاية، على أن تكون كل كلمة وسيطة موجودة في قاموس. وهذه مسألة BFS على رسم بياني ضمني، حيث تمثل الكلمات العقد، وتصل الحواف بين الكلمات التي تختلف بحرف واحد. ولّدو جميع التغييرات التي تغيّر حرفاً واحداً، وتحققوا مما إذا كانت موجودة في مجموعة الكلمات. تضمن BFS إيجاد تسلسل التحويل الأدنى.

from collections import deque

def word_ladder(begin_word, end_word, word_list):
    word_set = set(word_list)
    if end_word not in word_set:
        return 0
    queue = deque([(begin_word, 1)])
    visited = {begin_word}
    while queue:
        word, steps = queue.popleft()
        for i in range(len(word)):
            for c in 'abcdefghijklmnopqrstuvwxyz':
                new_word = word[:i] + c + word[i+1:]
                if new_word == end_word:
                    return steps + 1
                if new_word in word_set and new_word not in visited:
                    visited.add(new_word)
                    queue.append((new_word, steps + 1))
    return 0

print(word_ladder('hit', 'cog', ['hot','dot','dog','lot','log','cog']))  # 5

اجتياز المستويات: تتبّع المسافة

يجمع اجتياز المستويات العقد وفقاً لمسافتها من المصدر، وهو مفيد مباشرةً للمسائل التي تحتاج إلى معالجة كل مستوى على حدة. تتبّعوا المسافة إما بتخزينها في عنصر الطابور على شكل الصف (node, dist)، أو باستخدام تقنية حجم الطابور (سجّلوا حجم الطابور قبل كل مستوى، وعالجوا هذا العدد من العقد بالضبط، ثم زيدوا عدّاد المستوى). ويعطي الأسلوبان النتائج نفسها.

from collections import deque, defaultdict

def bfs_levels(graph, start):
    levels = {}
    visited = {start}
    queue = deque([start])
    dist = 0
    while queue:
        # Process all nodes at current distance
        for _ in range(len(queue)):
            node = queue.popleft()
            levels[node] = dist
            for nb in graph[node]:
                if nb not in visited:
                    visited.add(nb)
                    queue.append(nb)
        dist += 1
    return levels

graph = defaultdict(list)
for u, v in [(0,1),(0,2),(1,3),(2,3),(3,4)]:
    graph[u].append(v); graph[v].append(u)
print(bfs_levels(graph, 0))  # {0:0, 1:1, 2:1, 3:2, 4:3}

استنساخ الرسم البياني

تنشئ مسألة Clone Graph (LeetCode #133) نسخة عميقة من رسم بياني متصل وغير موجه. استخدموا BFS وخريطة تجزئة تربط العقد الأصلية بنسخها. عند زيارة عقدة للمرة الأولى، أنشئوا نسختها وأضيفوها إلى الخريطة. وعند معالجة الجيران، ابحثوا عن نسخهم أو أنشئوها، ثم صِلوا الحواف. وتؤدي خريطة التجزئة غرضين: تتبّع العقد التي تمت زيارتها وربط العقد الأصلية بنسخها.

from collections import deque

class Node:
    def __init__(self, val=0, neighbors=None):
        self.val = val
        self.neighbors = neighbors if neighbors is not None else []

def clone_graph(node):
    if not node:
        return None
    old_to_new = {node: Node(node.val)}
    queue = deque([node])
    while queue:
        curr = queue.popleft()
        for nb in curr.neighbors:
            if nb not in old_to_new:
                old_to_new[nb] = Node(nb.val)
                queue.append(nb)
            old_to_new[curr].neighbors.append(old_to_new[nb])
    return old_to_new[node]

# Build a simple graph: 1 -- 2 -- 3 -- 4 -- 1
n1 = Node(1); n2 = Node(2); n3 = Node(3); n4 = Node(4)
n1.neighbors = [n2, n4]; n2.neighbors = [n1, n3]
n3.neighbors = [n2, n4]; n4.neighbors = [n3, n1]
cloned = clone_graph(n1)
print(cloned.val, [n.val for n in cloned.neighbors])  # 1 [2, 4]

BFS ثنائية الاتجاه

تبدأ BFS ثنائية الاتجاه من المصدر والوجهة في الوقت نفسه، وتوسّع مستوى واحداً في كل مرة من كل طرف. وعندما تلتقي الجبهتان، يكون قد تم العثور على أقصر مسار. وفي الرسوم البيانية الكبيرة، يقلّل ذلك مساحة البحث من O(b^d) إلى O(2 * b^(d/2))، حيث b هو معامل التفرّع وd هو طول المسار، مما يحقق تحسناً كبيراً في الرسوم البيانية شديدة الترابط مثل Word Ladder مع القواميس الكبيرة.

from collections import defaultdict

def word_ladder_bidir(begin, end, word_list):
    word_set = set(word_list)
    if end not in word_set:
        return 0
    front, back = {begin}, {end}
    visited = {begin, end}
    steps = 1
    while front and back:
        # Always expand the smaller frontier
        if len(front) > len(back):
            front, back = back, front
        next_front = set()
        for word in front:
            for i in range(len(word)):
                for c in 'abcdefghijklmnopqrstuvwxyz':
                    nw = word[:i] + c + word[i+1:]
                    if nw in back:  # frontiers met!
                        return steps + 1
                    if nw in word_set and nw not in visited:
                        visited.add(nw)
                        next_front.add(nw)
        front = next_front
        steps += 1
    return 0

print(word_ladder_bidir('hit','cog',['hot','dot','dog','lot','log','cog']))  # 5

BFS من النوع 0-1 للرسوم البيانية الموزونة

تتعامل 0-1 BFS مع الرسوم البيانية التي تقتصر أوزان حوافها على 0 أو 1. وبدلاً من استخدام طابور عادي، استخدموا طابوراً مزدوج النهاية: أضيفوا إلى الخلف للحواف ذات الوزن 1 (المستوى التالي)، وإلى الأمام للحواف ذات الوزن 0 (المستوى نفسه). يوفّر ذلك حساب أقصر المسارات بزمن O(V + E)، وهو أسرع من زمن Dijkstra البالغ O((V+E) log V) عندما تكون الأوزان ثنائية. ويشيع استخدامه في مسائل الشبكات التي تكون فيها بعض الحركات مجانية بينما تكلّف الحركات الأخرى 1.

from collections import deque

def zero_one_bfs(graph, start, n):
    # graph: list of (neighbour, weight) where weight is 0 or 1
    dist = [float('inf')] * n
    dist[start] = 0
    dq = deque([start])
    while dq:
        node = dq.popleft()
        for nb, w in graph[node]:
            if dist[node] + w < dist[nb]:
                dist[nb] = dist[node] + w
                if w == 0:
                    dq.appendleft(nb)   # same level
                else:
                    dq.append(nb)       # next level
    return dist

# Simple test:
graph = [[(1, 0), (2, 1)],   # node 0: free to 1, cost 1 to 2
         [(3, 1)],            # node 1: cost 1 to 3
         [(3, 0)],            # node 2: free to 3
         []]
print(zero_one_bfs(graph, 0, 4))  # [0, 0, 1, 1]

الجدران والبوابات (BFS متعددة المصادر)

تملأ مسألة Walls and Gates كل غرفة فارغة بالمسافة إلى أقرب بوابة. استخدموا BFS متعددة المصادر: هيّئوا الطابور بجميع البوابات (ذات القيمة 0) في الوقت نفسه، ثم وسّعوا البحث إلى الخارج. وتُضبط قيمة كل خلية على المستوى الذي يتم الوصول إليها فيه للمرة الأولى. ويكون هذا الحل بزمن O(mn) أكثر كفاءة من تشغيل BFS من كل غرفة فارغة بشكل منفصل، وهو ما يستغرق O(m²n²).

from collections import deque

def walls_and_gates(rooms):
    if not rooms:
        return
    rows, cols = len(rooms), len(rooms[0])
    INF = float('inf')
    queue = deque()
    # Multi-source: all gates at distance 0
    for r in range(rows):
        for c in range(cols):
            if rooms[r][c] == 0:  # gate
                queue.append((r, c))
    dirs = [(0,1),(0,-1),(1,0),(-1,0)]
    while queue:
        r, c = queue.popleft()
        for dr, dc in dirs:
            nr, nc = r+dr, c+dc
            if 0<=nr<rows and 0<=nc<cols and rooms[nr][nc]==INF:
                rooms[nr][nc] = rooms[r][c] + 1
                queue.append((nr, nc))

rooms = [[float('inf'),-1,0,float('inf')],
         [float('inf'),float('inf'),float('inf'),-1],
         [float('inf'),-1,float('inf'),-1],
         [0,-1,float('inf'),float('inf')]]
walls_and_gates(rooms)
print(rooms[0][0], rooms[1][1])  # 3, 2

BFS في لعبة الثعابين والسلالم

تُعدّ Snakes and Ladders (LeetCode #909) مسألة إيجاد أقصر مسار باستخدام BFS على شبكة مرقّمة. مثّلوا اللوحة كرسم بياني غير موزون، حيث يمكنكم التحرك من أي مربع بمقدار 1 إلى 6، وقد تهبطون على ثعبان أو سلّم ينقلكم فوراً إلى مربع آخر. تعثر BFS على أقل عدد من رميات النرد. ويتمثل التحدي الأساسي في التحويل بين الموضع أحادي البعد وإحداثيات اللوحة ثنائية الأبعاد، مع مراعاة تخطيط boustrophedon (اتجاه الصفوف المتناوب).

from collections import deque

def snakes_and_ladders(board):
    n = len(board)
    def get_board(pos):
        r, c = divmod(pos - 1, n)
        if r % 2 == 1: c = n - 1 - c  # alternating direction
        return board[n - 1 - r][c]

    visited = {1}
    queue = deque([(1, 0)])
    while queue:
        pos, moves = queue.popleft()
        for dice in range(1, 7):
            next_pos = pos + dice
            if next_pos > n * n:
                break
            val = get_board(next_pos)
            if val != -1:
                next_pos = val  # snake or ladder
            if next_pos == n * n:
                return moves + 1
            if next_pos not in visited:
                visited.add(next_pos)
                queue.append((next_pos, moves + 1))
    return -1

print('BFS models game as an unweighted shortest-path problem')

تعقيد BFS وتحسيناتها

التعقيد الزمني لـBFS هو O(V + E)، لأن كل رأس يُضاف إلى الطابور مرة واحدة، وتُفحص كل حافة عدداً ثابتاً من المرات. أما التعقيد المكاني فهو O(V) لمجموعة العقد التي تمت زيارتها وللطابور. وفي رسوم الشبكات البيانية، تكون V = m*n وE = 4*m*n (إذ لكل خلية 4 جيران)، ولذلك تكون BFS على الشبكة بزمن O(mn). والتحسين الأساسي هو استخدام مجموعة للتحقق من الزيارة (بحث بزمن O(1)) بدلاً من قائمة (بحث بزمن O(n)). ضعوا علامة الزيارة عند الإضافة إلى الطابور، لا عند الإزالة منه.

# BFS on a graph with V vertices and E edges:
# Time:  O(V + E) -- each vertex and edge visited once
# Space: O(V)     -- visited set + queue

# BFS on an m x n grid:
# V = m*n cells
# E <= 4*m*n edges (4 directions, max)
# Time:  O(m*n)
# Space: O(m*n)

# Common pitfalls:
# 1. Marking visited on dequeue (not enqueue) -> same node queued multiple times
# 2. Using a list for visited -> O(n) membership check -> O(V*E) total
# 3. Not handling disconnected graph -> BFS from single source misses components
print('O(V+E) time, O(V) space -- mark visited on enqueue')

أقرب 0 في مصفوفة ثنائية

تجد مسألة 01 Matrix (LeetCode #542) المسافة من كل خلية إلى أقرب 0. وتوفّر BFS متعددة المصادر، التي تبدأ من جميع الخلايا التي تحتوي على 0 في الوقت نفسه، الحل الأمثل بزمن O(mn). هيّئوا الطابور بجميع الخلايا التي تحتوي على 0 وبمسافة 0، واضبطوا مسافة جميع الخلايا التي تحتوي على 1 على ما لا نهاية. وتنشر BFS المسافات إلى الخارج انطلاقاً من الخلايا التي تحتوي على 0، فتضبط مسافة كل خلية تحتوي على 1 عند الوصول إليها للمرة الأولى، وهو ما يضمن أن المسافة هي الأقصر.

from collections import deque

def update_matrix(mat):
    rows, cols = len(mat), len(mat[0])
    dist = [[float('inf')] * cols for _ in range(rows)]
    queue = deque()
    for r in range(rows):
        for c in range(cols):
            if mat[r][c] == 0:
                dist[r][c] = 0
                queue.append((r, c))
    dirs = [(0,1),(0,-1),(1,0),(-1,0)]
    while queue:
        r, c = queue.popleft()
        for dr, dc in dirs:
            nr, nc = r+dr, c+dc
            if 0<=nr<rows and 0<=nc<cols:
                if dist[r][c] + 1 < dist[nr][nc]:
                    dist[nr][nc] = dist[r][c] + 1
                    queue.append((nr, nc))
    return dist

mat = [[0,0,0],[0,1,0],[1,1,1]]
result = update_matrix(mat)
for row in result: print(row)  # [[0,0,0],[0,1,0],[1,2,1]]

تحقق سريع

اختبروا فهمكم لمفاهيم Data Structures & Algorithms — Coding Interview Prep الواردة في هذا الدرس.

مراجعة الدرس

تعلّمتم في هذا الدرس استخدام BFS لإيجاد أقصر المسارات في الرسوم البيانية غير الموزونة، مع تتبّع الآباء لإعادة بناء المسار، وWord Ladder بوصفها مثالاً أساسياً على BFS في رسم بياني ضمني، وBFS ثنائية الاتجاه للرسوم البيانية الكبيرة، وBFS متعددة المصادر للمسائل التي تتضمن نقاط بداية متعددة. في الخطوة التالية، سنطبّق DFS على المكوّنات المتصلة والملء الانتشاري.

الأسئلة الشائعة

هل درس «BFS: أقصر مسار والاجتياز حسب المستويات» مجاني؟

نعم — نص درس «BFS: أقصر مسار والاجتياز حسب المستويات» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة Coding Interview Prep، انتقل إلى CoddyKit PRO. تتضمن دورة Coding Interview Prep 4 دروس في المجموع.

ماذا ستتعلم في «BFS: أقصر مسار والاجتياز حسب المستويات»؟

استخدم BFS للعثور على أقصر مسار في رسم بياني غير موزون، وحل word-ladder مستوىً تلو الآخر، واستنسخ رسمًا بيانيًا باستخدام خريطة تجزئة تتمرن على Coding Interview Prep مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.

هل أحتاج إلى خبرة سابقة لأبدأ Coding Interview Prep؟

لا تُشترط خبرة سابقة. Coding Interview Prep على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 2 من أصل 4.

كم من الوقت يستغرق درس «BFS: أقصر مسار والاجتياز حسب المستويات»؟

معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.

هل يمكنني كتابة وتشغيل أكواد في درس Coding Interview Prep هذا؟

نعم. كل درس في Coding Interview Prep يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.

جميع الدروس في هذه الدورة

  1. تمثيلات الرسوم البيانية وإعداد الاجتياز
  2. BFS: أقصر مسار والاجتياز حسب المستويات
  3. DFS: المكوّنات المتصلة والملء التلقائي
  4. اكتشاف الدورات في الرسوم الموجهة وغير الموجهة
← العودة إلى Coding Interview Prep