اكتشاف الدورات في الرسوم الموجهة وغير الموجهة
اكتشف الدورات في الرسوم غير الموجهة بتتبع الآباء، وفي الرسوم الموجهة بترميز ألوان DFS للحالات الثلاث: الأبيض والرمادي والأسود
اكتشاف الدورات في الرسوم الموجهة وغير الموجهة درس مجاني في 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 يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.
جميع الدروس في هذه الدورة
- تمثيلات الرسوم البيانية وإعداد الاجتياز
- BFS: أقصر مسار والاجتياز حسب المستويات
- DFS: المكوّنات المتصلة والملء التلقائي
- اكتشاف الدورات في الرسوم الموجهة وغير الموجهة