0Pricing
Coding Interview Prep · درس

DFS: المكوّنات المتصلة والملء التلقائي

طبّق DFS لعدّ المكوّنات المتصلة، وحل number-of-islands على شبكة ثنائية الأبعاد، ونفّذ flood fill لمعالجة الصور

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

تعريف المكوّنات المتصلة

المكوّن المتصل في الرسم البياني غير الموجه هو مجموعة قصوى من الرؤوس، بحيث يوجد مسار بين كل زوج من الرؤوس في المجموعة. ويمكن أن يحتوي الرسم البياني الواحد على عدة مكوّنات منفصلة. ويُعد العثور على المكوّنات المتصلة أساساً للعديد من مسائل الرسوم البيانية؛ إذ يمكن اختزال التجميع والدمج وعدّ الجزر وتوحيد الحسابات إلى هذه العملية الأساسية.

from collections import defaultdict

# Graph with 3 components: {0,1,2}, {3,4}, {5}
graph = defaultdict(list)
for u, v in [(0,1),(0,2),(1,2),(3,4)]:
    graph[u].append(v)
    graph[v].append(u)
# Node 5 is isolated (no edges)
for node in [0,1,2,3,4,5]:
    if node not in graph:
        graph[node] = []

# We need DFS or BFS from each unvisited node
# to discover all components
print('Graph has nodes 0-5 with components: {0,1,2}, {3,4}, {5}')

عدّ المكوّنات المتصلة باستخدام DFS

كرّروا على جميع العقد. ولكل عقدة لم تتم زيارتها، ابدؤوا DFS لوضع علامة على جميع العقد القابلة للوصول منها باعتبارها زِيرت. ويمثل كل بدء لـDFS اكتشاف مكوّن جديد. عدّوا مرات بدء DFS لتحصلوا على عدد المكوّنات. تعمل هذه الخوارزمية بزمن O(V + E) بشكل صحيح سواء أكان الرسم البياني متصلاً أم لا.

from collections import defaultdict

def count_components(n, edges):
    graph = defaultdict(list)
    for u, v in edges:
        graph[u].append(v)
        graph[v].append(u)

    visited = set()
    count = 0

    def dfs(node):
        visited.add(node)
        for nb in graph[node]:
            if nb not in visited:
                dfs(nb)

    for node in range(n):
        if node not in visited:
            dfs(node)
            count += 1

    return count

print(count_components(6, [(0,1),(0,2),(1,2),(3,4)]))  # 3
print(count_components(5, [(0,1),(1,2),(3,4)]))          # 2

عدد الجزر

تُعدّ مسألة Number of Islands (LeetCode #200) المسألة النموذجية للمكوّنات المتصلة على شبكة ثنائية الأبعاد. تنتمي كل خلية قيمتها '1' إلى جزيرة، وتشكل الخلايا المتجاورة التي قيمتها '1' (أعلى/أسفل/يمين/يسار) الجزيرة نفسها. عدّوا عدد الجزر المميزة باستخدام DFS: كرّروا على جميع الخلايا، وعند العثور على خلية قيمتها '1' ولم تتم زيارتها، ابدؤوا DFS لوضع علامة على جميع خلايا '1' المتصلة بها (أي تنفيذ ملء انتشاري)، ثم زيدوا العدّاد.

def num_islands(grid):
    if not grid:
        return 0
    rows, cols = len(grid), len(grid[0])
    count = 0

    def dfs(r, c):
        if r < 0 or r >= rows or c < 0 or c >= cols:
            return
        if grid[r][c] != '1':
            return
        grid[r][c] = '#'  # mark visited in-place
        dfs(r+1,c); dfs(r-1,c)
        dfs(r,c+1); dfs(r,c-1)

    for r in range(rows):
        for c in range(cols):
            if grid[r][c] == '1':
                dfs(r, c)
                count += 1
    return count

grid = [['1','1','0','0','0'],
        ['1','1','0','0','0'],
        ['0','0','1','0','0'],
        ['0','0','0','1','1']]
print(num_islands(grid))  # 3

خوارزمية الملء الانتشاري

تستبدل خوارزمية Flood Fill (LeetCode #733) جميع الخلايا المتصلة التي لها لون بداية محدد بلون جديد، تماماً مثل أداة دلو الطلاء في محررات الصور. استخدموا DFS: بدءاً من البكسل المصدر، أعيدوا تلوين جميع الجيران الذين يطابقون اللون الأصلي بشكل متكرر. وتتمثل الحالة الحدّية الأساسية في أن يكون لون خلية البداية مساوياً للون الجديد؛ عندها أعيدوا النتيجة فوراً لتجنّب الاستدعاء الذاتي اللانهائي.

def flood_fill(image, sr, sc, new_color):
    original = image[sr][sc]
    if original == new_color:
        return image  # edge case: same color, nothing to do
    rows, cols = len(image), len(image[0])

    def dfs(r, c):
        if r < 0 or r >= rows or c < 0 or c >= cols:
            return
        if image[r][c] != original:
            return
        image[r][c] = new_color
        dfs(r+1,c); dfs(r-1,c)
        dfs(r,c+1); dfs(r,c-1)

    dfs(sr, sc)
    return image

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

أقصى مساحة لجزيرة

أقصى مساحة لجزيرة (LeetCode #695) يوسّع فكرة عدّ الجزر: أعد حجم أكبر جزيرة من بين جميع الجزر. أثناء اجتياح المنطقة باستخدام DFS، احسب الخلايا التي تحددها. تُرجع DFS حجم الجزيرة الحالية، وتتتبع القيمة القصوى بين جميع الجزر. هذه إضافة بسيطة إلى نمط المكوّنات المتصلة.

def max_area_of_island(grid):
    if not grid:
        return 0
    rows, cols = len(grid), len(grid[0])
    max_area = 0

    def dfs(r, c):
        if r < 0 or r >= rows or c < 0 or c >= cols:
            return 0
        if grid[r][c] != 1:
            return 0
        grid[r][c] = 0  # mark visited
        return (1 + dfs(r+1,c) + dfs(r-1,c) +
                dfs(r,c+1) + dfs(r,c-1))

    for r in range(rows):
        for c in range(cols):
            if grid[r][c] == 1:
                max_area = max(max_area, dfs(r, c))
    return max_area

grid = [[0,0,1,0,0,0,0,1,0,0,0,0,0],
        [0,0,0,0,0,0,0,1,1,1,0,0,0],
        [0,1,1,0,1,0,0,0,0,0,0,0,0],
        [0,1,0,0,1,1,0,0,1,0,1,0,0]]
print(max_area_of_island(grid))  # 6

تدفّق المياه إلى المحيطين الهادئ والأطلسي

تدفّق المياه إلى المحيطين الهادئ والأطلسي (LeetCode #417) يطلب تحديد الخلايا التي يمكن أن تتدفق منها المياه إلى المحيط الهادئ (حافتا الأعلى واليسرى) والمحيط الأطلسي (حافتا الأسفل واليمنى). بدلاً من محاكاة تدفق المياه إلى أسفل، استخدم DFS عكسيًا: تخيّل تدفق المياه صعودًا من المحيطين. نفّذ عمليتي DFS — واحدة من حدود المحيط الهادئ، وأخرى من حدود المحيط الأطلسي — واجمع الخلايا التي يمكن الوصول إليها. ويكون التقاطع هو الإجابة.

def pacific_atlantic(heights):
    rows, cols = len(heights), len(heights[0])
    pac = set(); atl = set()

    def dfs(r, c, visited, prev_h):
        if (r,c) in visited or r < 0 or r >= rows or c < 0 or c >= cols:
            return
        if heights[r][c] < prev_h:
            return  # water can't flow uphill in reverse
        visited.add((r,c))
        for dr, dc in [(0,1),(0,-1),(1,0),(-1,0)]:
            dfs(r+dr, c+dc, visited, heights[r][c])

    for r in range(rows):
        dfs(r, 0, pac, heights[r][0])           # Pacific left
        dfs(r, cols-1, atl, heights[r][cols-1]) # Atlantic right
    for c in range(cols):
        dfs(0, c, pac, heights[0][c])            # Pacific top
        dfs(rows-1, c, atl, heights[rows-1][c]) # Atlantic bottom

    return sorted(pac & atl)  # intersection

print(pacific_atlantic([[1,2,2,3,5],[3,2,3,4,4],[2,4,5,3,1],[6,7,1,4,5],[5,1,1,2,4]]))

خوارزمية DFS التكرارية للمكوّنات المتصلة

استخدم DFS تكرارية (مع مكدس صريح) لتجنّب حد الاستدعاء التكراري في Python عند التعامل مع الشبكات الكبيرة. الإصدار التكراري مكافئ لـ DFS التكرارية، لكنه يستخدم مكدسًا بدلاً من مكدس الاستدعاءات. ادفع العقدة الابتدائية إلى المكدس، ثم أخرج عقدة، وعلّمها بأنها زِيرت، وادفع جيرانها الذين لم تتم زيارتهم. يتيح ذلك التعامل بأمان مع شبكات يصل حجمها إلى ملايين الخلايا، بينما قد تتسبب DFS التكرارية في تجاوز سعة المكدس.

def count_components_iterative(n, edges):
    from collections import defaultdict
    graph = defaultdict(list)
    for u, v in edges:
        graph[u].append(v)
        graph[v].append(u)

    visited = set()
    count = 0

    for start in range(n):
        if start in visited:
            continue
        # Iterative DFS
        stack = [start]
        while stack:
            node = stack.pop()
            if node in visited:
                continue
            visited.add(node)
            for nb in graph[node]:
                if nb not in visited:
                    stack.append(nb)
        count += 1

    return count

print(count_components_iterative(6, [(0,1),(0,2),(1,2),(3,4)]))  # 3

المناطق المحاطة

المناطق المحاطة (LeetCode #130) تلتقط جميع مناطق 'O' المحاطة بالكامل بحدود من 'X'. لا تُلتقط المنطقة إذا لامست أي خلية من خلايا 'O' فيها حافة اللوحة. تكمن الحيلة في أنه بدلاً من البحث عن المناطق المحاطة مباشرةً، ننفّذ DFS من جميع خلايا 'O' الواقعة على الحدود، ونعلّم كل ما يمكن الوصول إليه بأنه آمن. ثم نعكس القيم: فجميع خلايا 'O' المتبقية محاطة وتصبح 'X'، بينما تُستعاد الخلايا الآمنة إلى 'O'.

def solve(board):
    if not board:
        return
    rows, cols = len(board), len(board[0])

    def dfs(r, c):
        if r < 0 or r >= rows or c < 0 or c >= cols:
            return
        if board[r][c] != 'O':
            return
        board[r][c] = 'S'  # safe: connected to border
        dfs(r+1,c); dfs(r-1,c)
        dfs(r,c+1); dfs(r,c-1)

    # Mark border-connected O's as safe
    for r in range(rows):
        dfs(r, 0); dfs(r, cols-1)
    for c in range(cols):
        dfs(0, c); dfs(rows-1, c)

    # Flip: surrounded O -> X, safe S -> O
    for r in range(rows):
        for c in range(cols):
            if board[r][c] == 'O': board[r][c] = 'X'
            elif board[r][c] == 'S': board[r][c] = 'O'

board = [['X','X','X','X'],['X','O','O','X'],
         ['X','X','O','X'],['X','O','X','X']]
solve(board)
print([board[1][1], board[3][1]])  # X, O

عدّ الجزر الفرعية

عدّ الجزر الفرعية (LeetCode #1905) يبحث عن الجزر في grid2 المحاطة بالكامل داخل جزيرة في grid1. نفّذ DFS بدءًا من كل خلية '1' في grid2: تكون الجزيرة جزيرة فرعية إذا كانت كل خلية تزورها أيضًا تساوي '1' في grid1. تكمن الحيلة في زيارة جميع خلايا الجزيرة، حتى نعلّمها بأنها استُكشفت، مع تتبّع ما إذا كانت كل هذه الخلايا تساوي '1' في grid1 أيضًا. لا تتوقف مبكرًا عند أول '0' في grid1، وإلا فلن تضع علامة الاستكشاف على الخلايا الأخرى من الجزيرة نفسها.

def count_sub_islands(grid1, grid2):
    rows, cols = len(grid2), len(grid2[0])

    def dfs(r, c):
        if r < 0 or r >= rows or c < 0 or c >= cols:
            return True
        if grid2[r][c] != 1:
            return True
        grid2[r][c] = 0  # mark visited
        is_sub = grid1[r][c] == 1  # this cell must be in grid1
        is_sub = dfs(r+1,c) and is_sub  # note: AND not short-circuit OR
        is_sub = dfs(r-1,c) and is_sub
        is_sub = dfs(r,c+1) and is_sub
        is_sub = dfs(r,c-1) and is_sub
        return is_sub

    count = 0
    for r in range(rows):
        for c in range(cols):
            if grid2[r][c] == 1 and dfs(r, c):
                count += 1
    return count

print(count_sub_islands([[1,1,1],[1,0,1],[1,1,1]],
                         [[1,1,1],[1,0,1],[1,1,1]]))  # 1

DFS أم BFS للمكوّنات المتصلة

تجد كل من DFS وBFS جميع المكوّنات المتصلة بشكل صحيح، وبالتعقيد نفسه: زمن O(V + E) ومساحة O(V). تكون DFS أبسط عند تنفيذها تكراريًا في مسائل المكوّنات المتصلة، بينما يُفضَّل BFS عندما تحتاج أيضًا إلى معلومات أقصر مسار. في مسائل الشبكات، تكون DFS أكثر ملاءمة لذاكرة التخزين المؤقت لأنها تتعمق في اتجاه واحد قبل التراجع، فتصل إلى مواقع الذاكرة المتجاورة بالتتابع.

# DFS advantages for connected components:
# - Simpler recursive implementation
# - Lower constant factor for small graphs
# - Can restore grid state during backtracking (if needed)

# BFS advantages:
# - Finds shortest path while traversing
# - Better for wide, shallow graphs (avoids deep recursion)
# - Multi-source initialisation is natural

# Same asymptotic complexity: O(V + E) time, O(V) space
# Grid (m rows, n cols): O(mn) time and space
print('DFS and BFS: same O(V+E) complexity for component counting')

الجزر مع قيود: الأشكال والمحيطات

محيط الجزيرة (LeetCode #463) يحسب المحيط الكلي للجزيرة الوحيدة في شبكة. لكل خلية يابسة ('1')، أضف 4 إلى المحيط، ثم اطرح 2 لكل خلية يابسة مجاورة (بسبب الحواف المشتركة). لا تتطلب هذه الطريقة القائمة على الصيغة، ذات التعقيد O(mn)، استخدام DFS، لكن فهم أنها مكافئة لـ DFS تحسب حواف الحدود يعزّز الصلة بين مسائل الشبكات والتفكير القائم على الرسوم البيانية.

def island_perimeter(grid):
    rows, cols = len(grid), len(grid[0])
    perimeter = 0
    for r in range(rows):
        for c in range(cols):
            if grid[r][c] == 1:
                perimeter += 4  # start with 4 sides
                # Subtract shared edges with adjacent land cells
                if r > 0 and grid[r-1][c] == 1:
                    perimeter -= 2  # shared top edge
                if c > 0 and grid[r][c-1] == 1:
                    perimeter -= 2  # shared left edge
    return perimeter

grid = [[0,1,0,0],[1,1,1,0],[0,1,0,0],[1,1,0,0]]
print(island_perimeter(grid))  # 16

اختبار سريع

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

مراجعة الدرس

تعلّمت في هذا الدرس: المكوّنات المتصلة باستخدام DFS مع تتبّع الخلايا التي تمت زيارتها، وعدد الجزر وملء المنطقة كتطبيقين أساسيين على شبكة ثنائية الأبعاد، وأنماط متقدمة مثل DFS العكسية من الحدود (المناطق المحاطة) وتعدد عمليات DFS مع تتبّع القيود (الجزر الفرعية). بعد ذلك سنتناول اكتشاف الدورات في الرسوم البيانية الموجّهة وغير الموجّهة.

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

هل درس «DFS: المكوّنات المتصلة والملء التلقائي» مجاني؟

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

ماذا ستتعلم في «DFS: المكوّنات المتصلة والملء التلقائي»؟

طبّق DFS لعدّ المكوّنات المتصلة، وحل number-of-islands على شبكة ثنائية الأبعاد، ونفّذ flood fill لمعالجة الصور تتمرن على Coding Interview Prep مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.

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

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

كم من الوقت يستغرق درس «DFS: المكوّنات المتصلة والملء التلقائي»؟

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

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

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

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

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