Coding Interview Prep · درس

اكتشاف الدورات في الرسوم الموجهة وغير الموجهة

اكتشف الدورات في الرسوم غير الموجهة بتتبع الآباء، وفي الرسوم الموجهة بترميز ألوان DFS للحالات الثلاث: الأبيض والرمادي والأسود

الدرس 4 من 413 خطوة

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

لماذا يهم اكتشاف الدورات

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

from collections import defaultdict

# Undirected cycle: A-B-C-A (triangle)
undirected = defaultdict(list)
for u, v in [('A','B'),('B','C'),('C','A')]:
    undirected[u].append(v)
    undirected[v].append(u)

# Directed cycle: A->B->C->A
directed = defaultdict(list)
for u, v in [('A','B'),('B','C'),('C','A')]:
    directed[u].append(v)  # one direction only

# Key difference:
# Undirected: edge A-B appears as both A->B and B->A
# Must track parent to distinguish cycle from back-edge to parent
print('Undirected and directed cycles need different detection')

اكتشاف الدورات في الرسوم البيانية غير الموجّهة باستخدام DFS

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

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

    visited = set()

    def dfs(node, parent):
        visited.add(node)
        for nb in graph[node]:
            if nb not in visited:
                if dfs(nb, node):  # recurse with current as parent
                    return True
            elif nb != parent:     # visited and not parent = CYCLE
                return True
        return False

    for node in range(n):
        if node not in visited:
            if dfs(node, -1):  # -1 = no parent for root
                return True
    return False

print(has_cycle_undirected(4, [(0,1),(1,2),(2,3),(3,1)]))  # True
print(has_cycle_undirected(3, [(0,1),(1,2)]))               # False

اكتشاف دورة في رسم بياني غير موجّه باستخدام BFS

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

from collections import deque, defaultdict

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

    visited = set()

    for start in range(n):
        if start in visited:
            continue
        visited.add(start)
        parent = {start: -1}
        queue = deque([start])
        while queue:
            node = queue.popleft()
            for nb in graph[node]:
                if nb not in visited:
                    visited.add(nb)
                    parent[nb] = node
                    queue.append(nb)
                elif parent[node] != nb:  # visited and not parent = CYCLE
                    return True
    return False

print(has_cycle_bfs_undirected(4, [(0,1),(1,2),(2,0)]))  # True

الدورة في رسم بياني موجّه: لماذا يفشل تتبّع الأصل

في الرسم البياني الموجّه، لا يكفي تتبّع الأصل. لنفترض وجود A→C وB→C: للعقدة C أصلان، لكن لا توجد دورة. يستخدم الأسلوب الصحيح تلوينًا ذا ثلاث حالات: أبيض (لم تتم زيارته)، ورمادي (موجود في مسار أو مكدس DFS الحالي)، وأسود (تمت معالجته بالكامل). توجد دورة إذا صادفنا عقدة رمادية أثناء DFS، ما يعني أننا وجدنا حافة عائدة إلى سلف في المسار الحالي.

# Three-state DFS coloring:
# WHITE (0): not yet visited
# GRAY  (1): currently being visited (in DFS stack)
# BLACK (2): fully visited (all descendants processed)

# Why parent fails for directed graphs:
# A -> C  (no cycle)
# B -> C  (no cycle)
# If we DFS from A, mark C gray
# Then DFS from B finds C is gray -- but this is NOT a cycle!
# C is gray from A's path, not B's path.
# Parent tracking only works when the back-edge goes to the IMMEDIATE parent.
print('Directed graph: use 3-state coloring (white/gray/black)')

اكتشاف الدورات في رسم بياني موجّه باستخدام DFS ذات الحالات الثلاث

استخدم مصفوفة state[] بقيم 0 (أبيض/لم تتم زيارته)، و1 (رمادي/في المكدس)، و2 (أسود/انتهت معالجته). ابدأ DFS، وعلّم العقدة بالرمادي عند الدخول إليها وبالأسود عند الخروج منها. إذا وصلت DFS في أي وقت إلى عقدة رمادية، فقد عُثر على حافة عائدة، أي توجد دورة. أما إذا وصلت إلى عقدة سوداء، فهذا المسار قد استُكشف بالكامل من قبل ولا يحتوي على دورة، لذا تخطَّه.

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

    state = [0] * n  # 0=white, 1=gray, 2=black

    def dfs(node):
        state[node] = 1  # mark gray (in stack)
        for nb in graph[node]:
            if state[nb] == 1:  # gray = back edge = CYCLE
                return True
            if state[nb] == 0:  # white = unvisited
                if dfs(nb):
                    return True
        state[node] = 2  # mark black (fully processed)
        return False

    for node in range(n):
        if state[node] == 0:
            if dfs(node):
                return True
    return False

print(has_cycle_directed(4, [(0,1),(1,2),(2,0),(2,3)]))  # True (0->1->2->0)
print(has_cycle_directed(3, [(0,1),(1,2)]))               # False

جدولة المقررات: دورة في DAG

جدولة المقررات (LeetCode #207) تسأل عما إذا كان بالإمكان إكمال جميع المقررات عند إعطاء المتطلبات السابقة. مثّل المقررات بالعقد، والمتطلبات السابقة بحواف موجّهة. يمكن إكمال جميع المقررات إذا وفقط إذا كان الرسم البياني DAG (أي لا يحتوي على دورات). استخدم اكتشاف الدورات باستخدام DFS ذات الحالات الثلاث: إذا عُثر على دورة، فأعد False؛ وإلا فأعد True.

from collections import defaultdict

def can_finish(num_courses, prerequisites):
    graph = defaultdict(list)
    for a, b in prerequisites:
        graph[b].append(a)  # b is prerequisite for a: b -> a

    state = [0] * num_courses

    def dfs(course):
        if state[course] == 1: return False  # cycle!
        if state[course] == 2: return True   # already verified
        state[course] = 1  # mark as in-progress
        for next_course in graph[course]:
            if not dfs(next_course):
                return False
        state[course] = 2  # mark as done
        return True

    return all(dfs(i) for i in range(num_courses) if state[i] == 0)

print(can_finish(2, [[1,0]]))        # True: take 0 then 1
print(can_finish(2, [[1,0],[0,1]]))  # False: circular dependency

اكتشاف الدورات باستخدام خوارزمية Kahn (BFS)

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

from collections import defaultdict, deque

def has_cycle_kahn(n, edges):
    graph = defaultdict(list)
    in_degree = [0] * n
    for u, v in edges:
        graph[u].append(v)
        in_degree[v] += 1

    # Start with all zero in-degree nodes
    queue = deque(i for i in range(n) if in_degree[i] == 0)
    processed = 0
    while queue:
        node = queue.popleft()
        processed += 1
        for nb in graph[node]:
            in_degree[nb] -= 1
            if in_degree[nb] == 0:
                queue.append(nb)

    return processed != n  # if not all processed, cycle exists

print(has_cycle_kahn(4, [(0,1),(1,2),(2,0),(2,3)]))  # True
print(has_cycle_kahn(3, [(0,1),(1,2)]))               # False

العثور على الدورة: جمع عقد الدورة

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

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

    state = [0] * n
    path = []  # current DFS path
    cycle = []

    def dfs(node):
        state[node] = 1
        path.append(node)
        for nb in graph[node]:
            if state[nb] == 1:  # back edge -> found cycle
                start = path.index(nb)
                cycle.extend(path[start:])
                return True
            if state[nb] == 0 and dfs(nb):
                return True
        path.pop()
        state[node] = 2
        return False

    for i in range(n):
        if state[i] == 0 and dfs(i):
            break
    return cycle

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

العثور على الحالات الآمنة في النهاية

العثور على الحالات الآمنة في النهاية (LeetCode #802) يسأل عن العقد التي تؤدي في النهاية إلى عقدة نهائية (لا حواف صادرة لها) من دون الوقوع في دورة. تكون العقدة «آمنة» إذا أدت جميع المسارات المنطلقة منها إلى عقد نهائية. استخدم DFS ذات الحالات الثلاث: فالعقد السوداء (التي عولجت بالكامل من دون اكتشاف دورة) آمنة. أما العقد التي تشكّل جزءًا من دورة أو تؤدي إليها فليست آمنة.

def eventual_safe_nodes(graph):
    n = len(graph)
    state = [0] * n  # 0=unvisited, 1=visiting, 2=safe

    def dfs(node):
        if state[node] == 1:  # currently visiting = cycle
            return False
        if state[node] == 2:  # already verified safe
            return True
        state[node] = 1  # mark as visiting
        for nb in graph[node]:
            if not dfs(nb):
                return False  # leads to cycle, not safe
        state[node] = 2  # mark as safe
        return True

    return [i for i in range(n) if dfs(i)]

# [[1,2],[2,3],[5],[0],[5],[],[]] means:
# 0->[1,2], 1->[2,3], 2->[5], 3->[0] (cycle!), 4->[5], 5->[], 6->[]
print(eventual_safe_nodes([[1,2],[2,3],[5],[0],[5],[],[]]))
# [2, 4, 5, 6]

الاتصال الزائد في رسم بياني غير موجّه

الاتصال الزائد (LeetCode #684) يعثر على الحافة التي تُنشئ دورة عند إضافتها إلى رسم بياني غير موجّه لا يحتوي على دورات. يمكن حل ذلك باستخدام اكتشاف الدورات عبر DFS، لكن الحل الأنظف يستخدم Union-Find (DSU): عالج الحواف واحدةً تلو الأخرى؛ فإذا كان الطرفان متصلين بالفعل (في المكوّن نفسه)، فإن الحافة الحالية تنشئ دورة، وهي الإجابة. يوفّر DSU تعقيدًا قدره O(alpha(n)) لكل عملية، أي O(1) عمليًا.

def find_redundant_connection(edges):
    n = len(edges)
    parent = list(range(n + 1))
    rank = [0] * (n + 1)

    def find(x):
        if parent[x] != x:
            parent[x] = find(parent[x])  # path compression
        return parent[x]

    def union(x, y):
        px, py = find(x), find(y)
        if px == py:
            return False  # already connected = cycle!
        if rank[px] < rank[py]: px, py = py, px
        parent[py] = px
        if rank[px] == rank[py]: rank[px] += 1
        return True

    for u, v in edges:
        if not union(u, v):
            return [u, v]  # this edge creates the cycle
    return []

print(find_redundant_connection([[1,2],[1,3],[2,3]]))  # [2,3]
print(find_redundant_connection([[1,2],[2,3],[3,4],[1,4],[1,5]]))  # [1,4]

ملخص: استراتيجيات اكتشاف الدورات

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

# Cycle detection summary:
# Graph type  | Algorithm            | Complexity
# ------------|----------------------|-----------
# Undirected  | DFS + parent track   | O(V + E)
# Undirected  | Union-Find (DSU)     | O(E * alpha(V))
# Directed    | DFS 3-state (W/G/B)  | O(V + E)
# Directed    | Kahn's BFS topo sort | O(V + E)

# When to choose:
# Online (edges added one at a time): Union-Find
# Need topological order too: Kahn's BFS
# Need cycle nodes identified: 3-state DFS with path stack
# Simple existence check: any of the above
print('Always clarify directed vs undirected before coding')

اختبار سريع

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

مراجعة الدرس

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

البدء مجانًا

تعلم Coding Interview Prep مع معلم ذكاء اصطناعي — مجانًا

اكتب وقم بتشغيل أكوادك الفعلية في المتصفح، واحصل على مساعدة فورية من معلم ذكاء اصطناعي متاح 24/7، واستمر من حيث توقفت على الويب أو في التطبيق.

الدورات
90
الدروس
360

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

هل درس «اكتشاف الدورات في الرسوم الموجهة وغير الموجهة» مجاني؟

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

ماذا ستتعلم في «اكتشاف الدورات في الرسوم الموجهة وغير الموجهة»؟

اكتشف الدورات في الرسوم غير الموجهة بتتبع الآباء، وفي الرسوم الموجهة بترميز ألوان DFS للحالات الثلاث: الأبيض والرمادي والأسود تتمرن على Coding Interview Prep مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.

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

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

كم من الوقت يستغرق درس «اكتشاف الدورات في الرسوم الموجهة وغير الموجهة»؟

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

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

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

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

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