خوارزمية Kahn: الترتيب الطوبولوجي باستخدام BFS
احسب درجات الدخول لجميع العقد، وأدرج العقد ذات درجة الدخول الصفرية في الطابور، ثم عالج الطابور لإنتاج ترتيب طوبولوجي مع اكتشاف الدورات.
خوارزمية Kahn: الترتيب الطوبولوجي باستخدام BFS درس مجاني في Coding Interview Prep على CoddyKit. هذا هو الدرس 1 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في Coding Interview Prep، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة Coding Interview Prep 4 دروس في المجموع.
ما الترتيب الطوبولوجي؟
الترتيب الطوبولوجي للرسم البياني الموجّه غير الدوري (DAG) هو ترتيب لعقده بحيث تعني كل حافة موجهة u → v أن u تأتي قبل v في الترتيب. ويمثل ذلك ترتيب تنفيذ صالحاً للمهام التي لها تبعيات، مثل أنظمة البناء أو جدولة المقررات أو إدارة الحزم. لا تملك ترتيبات طوبولوجية صالحة إلا الرسوم البيانية من نوع DAG؛ إذ تجعل الدورة ذلك مستحيلاً.
خوارزمية Kahn: الفكرة الأساسية
خوارزمية Kahn هي أسلوب قائم على BFS لإجراء الترتيب الطوبولوجي. وتتمثل الفكرة الأساسية في أن العقدة ذات الدرجة الداخلة 0 (أي التي لا تملك متطلبات مسبقة) يمكن وضعها أولاً في الترتيب. بعد وضعها، أزلها وأنقص الدرجة الداخلة لجيرانها. وتصبح العقد الجديدة ذات الدرجة الداخلة الصفرية متاحة. كرر ذلك حتى تُوضَع جميع العقد أو تُكتشف دورة (أي تبقى عقد ذات درجة داخلة غير صفرية).
حساب الدرجة الداخلة
ابدأ ببناء قائمة التجاور وحساب الدرجة الداخلة (عدد الحواف الواردة) لكل عقدة. وتكون العقد ذات الدرجة الداخلة 0 هي نقاط البداية، إذ لا تبعيات لديها. بالنسبة إلى رسم بياني حوافه هي [(0,1),(0,2),(1,3),(2,3)]، تكون الدرجات الداخلة كما يلي: 0→0، 1→1، 2→1، 3→2. تبدأ العقدة 0 وحدها بدرجة داخلة تساوي 0.
from collections import deque, defaultdict
def compute_in_degree(n, edges):
in_degree = [0] * n
graph = defaultdict(list)
for u, v in edges:
graph[u].append(v)
in_degree[v] += 1
return graph, in_degree
graph, ind = compute_in_degree(4, [(0,1),(0,2),(1,3),(2,3)])
print('In-degrees:', ind) # [0, 1, 1, 2]تطبيق خوارزمية Kahn
أدرج جميع العقد ذات الدرجة الداخلة الصفرية في طابور. عالج كل عقدة: أضفها إلى النتيجة، ثم أنقص الدرجة الداخلة لكل جار وأدرجه في الطابور إذا وصلت درجته إلى 0. إذا كانت قائمة النتيجة تحتوي على عقد أقل من عدد عقد الرسم البياني، فهناك دورة — إذ تعذر إخراج بعض العقد من الطابور.
from collections import deque, defaultdict
def kahn_topological_sort(n, edges):
graph = defaultdict(list)
in_degree = [0] * n
for u, v in edges:
graph[u].append(v)
in_degree[v] += 1
queue = deque(i for i in range(n) if in_degree[i] == 0)
order = []
while queue:
node = queue.popleft()
order.append(node)
for nxt in graph[node]:
in_degree[nxt] -= 1
if in_degree[nxt] == 0:
queue.append(nxt)
if len(order) == n:
return order # valid topological sort
return [] # cycle detected
print(kahn_topological_sort(4, [(0,1),(0,2),(1,3),(2,3)]))اكتشاف الدورات باستخدام Kahn
توفر خوارزمية Kahn اكتشافاً مجانياً للدورات: إذا كان len(order) < n، فهذا يعني أن بعض العقد لم تُضف إلى الطابور مطلقاً لأن درجتها الداخلة لم تصل إلى 0 — وهي جزء من دورة. وهذا أنظف من الاحتفاظ بمصفوفة زيارة ملوّنة. أعد قائمة فارغة للإشارة إلى وجود دورة.
# Cyclic graph: 0->1->2->0
edges_cycle = [(0,1),(1,2),(2,0)]
result = kahn_topological_sort(3, edges_cycle)
print(result) # [] (cycle detected)
# Acyclic graph
edges_dag = [(0,1),(1,2)]
result = kahn_topological_sort(3, edges_dag)
print(result) # [0, 1, 2]التعقيد الزمني وتعقيد المساحة
تعالج خوارزمية Kahn كل عقدة مرة واحدة (إذ تُخرج من الطابور مرة واحدة) وكل حافة مرة واحدة (إذ تُنقص الدرجة الداخلة مرة واحدة). التعقيد الزمني: O(V + E). أما المساحة فهي O(V + E) لقائمة التجاور ومصفوفة الدرجات الداخلة، إضافة إلى O(V) للطابور. وهذا مثالي، إذ يجب عليك على الأقل قراءة جميع العقد والحواف لإنتاج ترتيب صالح.
الترتيب الطوبولوجي الأصغر معجمياً
ينتج استخدام خوارزمية Kahn مع كومة صغرى بدلاً من الطابور الترتيب الطوبولوجي الأصغر معجمياً. استبدل deque بـ heapq: أدخل (node)، وعالج دائماً أصغر عقدة متاحة أولاً. ويضمن ذلك الحصول على أصغر ترتيب صالح معجمياً من بين جميع الترتيبات الطوبولوجية الممكنة.
import heapq
from collections import defaultdict
def kahn_lex_order(n, edges):
graph = defaultdict(list)
in_degree = [0] * n
for u, v in edges:
graph[u].append(v)
in_degree[v] += 1
heap = [i for i in range(n) if in_degree[i] == 0]
heapq.heapify(heap)
order = []
while heap:
node = heapq.heappop(heap)
order.append(node)
for nxt in graph[node]:
in_degree[nxt] -= 1
if in_degree[nxt] == 0:
heapq.heappush(heap, nxt)
return order if len(order) == n else []
print(kahn_lex_order(6, [(5,2),(5,0),(4,0),(4,1),(2,3),(3,1)]))تطبيق: جدولة المقررات I
Course Schedule (LeetCode 207): معطى عدد n من المقررات ومتطلباتها السابقة، هل يمكنك إكمال جميع المقررات؟ مثّل المتطلبات السابقة بحواف موجهة، وتحقق مما إذا كان يوجد ترتيب طوبولوجي صالح (أي لا توجد دورة). أعد True إذا أنتجت خوارزمية Kahn ترتيباً طوله n، وFalse إذا اكتُشفت دورة.
from collections import deque, defaultdict
def canFinish(numCourses, prerequisites):
graph = defaultdict(list)
in_degree = [0] * numCourses
for a, b in prerequisites: # b must be taken before a
graph[b].append(a)
in_degree[a] += 1
queue = deque(i for i in range(numCourses) if in_degree[i] == 0)
count = 0
while queue:
node = queue.popleft()
count += 1
for nxt in graph[node]:
in_degree[nxt] -= 1
if in_degree[nxt] == 0:
queue.append(nxt)
return count == numCourses
print(canFinish(2, [[1,0]])) # True
print(canFinish(2, [[1,0],[0,1]])) # False (cycle)تطبيق: جدولة المقررات II
Course Schedule II (LeetCode 210): أعد الترتيب الفعلي الذي ينبغي اتباعه لأخذ المقررات. الأمر مماثل لما سبق، لكن أعد قائمة order بدلاً من قيمة منطقية. إذا وُجدت دورة، فأعد قائمة فارغة. ويُستخدم ناتج Kahn مباشرةً بوصفه الإجابة.
from collections import deque, defaultdict
def findOrder(numCourses, prerequisites):
graph = defaultdict(list)
in_degree = [0] * numCourses
for a, b in prerequisites:
graph[b].append(a)
in_degree[a] += 1
queue = deque(i for i in range(numCourses) if in_degree[i] == 0)
order = []
while queue:
node = queue.popleft()
order.append(node)
for nxt in graph[node]:
in_degree[nxt] -= 1
if in_degree[nxt] == 0:
queue.append(nxt)
return order if len(order) == numCourses else []
print(findOrder(4, [[1,0],[2,0],[3,1],[3,2]]))جدولة المهام المتوازية
استخدام أكثر تقدماً: معطاة مهام ذات تبعيات، أوجد الحد الأدنى لعدد «الجولات» اللازمة إذا كان يمكن تنفيذ المهام التي لا تبعيات لديها بالتوازي. عالج خوارزمية Kahn على مستوى كل طبقة (على نحو مشابه للمعالجة على مستوى BFS): أدرج جميع العقد ذات الدرجة الداخلة الصفرية، وعالج الطابور الحالي بأكمله بوصفه جولة واحدة، ثم أدرج العقد التي تحررت حديثاً بوصفها الجولة التالية. احسب عدد الجولات.
from collections import deque, defaultdict
def min_rounds(n, edges):
graph = defaultdict(list)
in_degree = [0] * n
for u, v in edges:
graph[u].append(v)
in_degree[v] += 1
queue = deque(i for i in range(n) if in_degree[i] == 0)
rounds = 0
while queue:
rounds += 1
for _ in range(len(queue)): # process current level
node = queue.popleft()
for nxt in graph[node]:
in_degree[nxt] -= 1
if in_degree[nxt] == 0:
queue.append(nxt)
return rounds
print(min_rounds(4, [(0,2),(1,2),(2,3)])) # 3الترتيب الطوبولوجي والبرمجة الديناميكية على رسوم DAG
يتيح الترتيب الطوبولوجي استخدام البرمجة الديناميكية على رسوم DAG: عالج العقد وفق الترتيب الطوبولوجي، وعند حساب dp[v] تكون جميع قيم dp[u] للعقد السابقة نهائية بالفعل. يجمع ذلك بين الترتيب الطوبولوجي والبرمجة الديناميكية لحل مسائل مثل أطول مسار في رسم DAG، أو أقل تكلفة للوصول إلى جميع العقد، أو أكبر ربح من سلسلة تبعيات. ويضمن الترتيب حساب قيمة البرمجة الديناميكية لكل عقدة مرة واحدة بالضبط، بعد اكتمال جميع تبعياتها.
from collections import deque, defaultdict
def longest_path_dag(V, edges):
graph = defaultdict(list)
in_degree = [0] * V
for u, v, w in edges:
graph[u].append((v, w))
in_degree[v] += 1
queue = deque(i for i in range(V) if in_degree[i] == 0)
dp = [0] * V
while queue:
u = queue.popleft()
for v, w in graph[u]:
dp[v] = max(dp[v], dp[u] + w)
in_degree[v] -= 1
if in_degree[v] == 0: queue.append(v)
return max(dp)
print(longest_path_dag(4, [(0,1,3),(0,2,2),(1,3,4),(2,3,1)])) # 7تحقق سريع
اختبر مدى فهمك لمفاهيم هياكل البيانات والخوارزميات — الاستعداد لمقابلات البرمجة — التي تناولها هذا الدرس.
مراجعة الدرس
تعلمت في هذا الدرس أن: خوارزمية Kahn تحسب الترتيب الطوبولوجي بإزالة العقد ذات الدرجة الداخلة الصفرية تكرارياً باستخدام BFS، وأن اكتشاف الدورات مجاني — فإذا كان len(order) < n فهناك دورة، وأن استبدال الطابور بكومة صغرى يعطي أصغر ترتيب طوبولوجي معجمياً. سنتناول تالياً الترتيب الطوبولوجي القائم على DFS باستخدام الترتيب اللاحق، بوصفه بديلاً عن Kahn.
الأسئلة الشائعة
هل درس «خوارزمية Kahn: الترتيب الطوبولوجي باستخدام BFS» مجاني؟
نعم — نص درس «خوارزمية Kahn: الترتيب الطوبولوجي باستخدام BFS» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة Coding Interview Prep، انتقل إلى CoddyKit PRO. تتضمن دورة Coding Interview Prep 4 دروس في المجموع.
ماذا ستتعلم في «خوارزمية Kahn: الترتيب الطوبولوجي باستخدام BFS»؟
احسب درجات الدخول لجميع العقد، وأدرج العقد ذات درجة الدخول الصفرية في الطابور، ثم عالج الطابور لإنتاج ترتيب طوبولوجي مع اكتشاف الدورات. تتمرن على Coding Interview Prep مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.
هل أحتاج إلى خبرة سابقة لأبدأ Coding Interview Prep؟
لا تُشترط خبرة سابقة. Coding Interview Prep على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 1 من أصل 4.
كم من الوقت يستغرق درس «خوارزمية Kahn: الترتيب الطوبولوجي باستخدام BFS»؟
معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.
هل يمكنني كتابة وتشغيل أكواد في درس Coding Interview Prep هذا؟
نعم. كل درس في Coding Interview Prep يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.
جميع الدروس في هذه الدورة
- خوارزمية Kahn: الترتيب الطوبولوجي باستخدام BFS
- الترتيب الطوبولوجي باستخدام DFS بترتيب ما بعد الزيارة
- جدول المقررات I وII
- المكوّنات شديدة الاتصال باستخدام Kosaraju