0Pricing
DSA Interview Prep · درس

جدول المقررات I وII

مثّل المتطلبات السابقة للمقررات كرسم بياني موجّه، واستخدم الترتيب الطوبولوجي لتحديد إمكانية إكمال جميع المقررات وترتيب إكمالها.

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

نظرة عامة على المسألة

Course Schedule I (LeetCode 207): بالنظر إلى n مقررات وقائمة من أزواج prerequisites بالشكل [a, b]، حيث «يجب اجتياز b قبل a»، حدّدوا ما إذا كان بإمكانكم إكمال جميع المقررات. أما Course Schedule II (LeetCode 210)، فتُعيد الترتيب الفعلي لاجتياز المقررات، أو مصفوفة فارغة إذا استحال ذلك. تختزل المسألتان إلى فرز طوبولوجي على رسم بياني موجّه تكون فيه المتطلبات السابقة حواف.

نمذجة الرسم البياني

ابنوا رسمًا بيانيًا موجّهًا: لكل زوج من المتطلبات السابقة [a, b]، أضيفوا الحافة b → a، إذ إن «يجب أن يأتي b قبل a» تعني أن b يقود إلى a. احسبوا درجات الدخول لكل مقرر. المقرر ذو درجة الدخول 0 لا متطلبات سابقة له، ولذلك يمكن اجتيازه فورًا. تكون المسألة قابلة للحل إذا وفقط إذا لم توجد دورة في هذا الرسم البياني، أي لا توجد تبعية دائرية.

from collections import defaultdict

def build_graph(n, prerequisites):
    graph = defaultdict(list)
    in_degree = [0] * n
    for a, b in prerequisites:  # b must come before a
        graph[b].append(a)
        in_degree[a] += 1
    return graph, in_degree

graph, ind = build_graph(4, [[1,0],[2,0],[3,1],[3,2]])
print('In-degrees:', ind)   # [0, 1, 1, 2]
print('Graph edges:', dict(graph))

Course Schedule I: حل Kahn

استخدموا خوارزمية Kahn. إذا ساوى عدد المقررات التي عولجت n، فيمكن إكمال جميع المقررات. وإلا، تمنع تبعية دائرية إكمالها.

from collections import deque, defaultdict

def canFinish(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)
    count = 0
    
    while queue:
        course = queue.popleft()
        count += 1
        for nxt in graph[course]:
            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

Course Schedule II: إعادة الترتيب

الطريقة مماثلة لـ Course Schedule I، لكن اجمعوا ترتيب المقررات أثناء معالجتها. أعيدوا الترتيب إذا تضمن جميع المقررات، وإلا فأعيدوا قائمة فارغة.

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:
        course = queue.popleft()
        order.append(course)
        for nxt in graph[course]:
            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]]))

Course Schedule باستخدام DFS

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

from collections import defaultdict

def canFinish_dfs(numCourses, prerequisites):
    graph = defaultdict(list)
    for a, b in prerequisites:
        graph[b].append(a)
    
    # 0=unvisited, 1=in-progress, 2=done
    state = [0] * numCourses
    
    def has_cycle(course):
        if state[course] == 1: return True  # back edge
        if state[course] == 2: return False # already cleared
        state[course] = 1
        for nxt in graph[course]:
            if has_cycle(nxt):
                return True
        state[course] = 2
        return False
    
    return not any(has_cycle(i) for i in range(numCourses))

print(canFinish_dfs(2, [[1,0]]))        # True
print(canFinish_dfs(2, [[1,0],[0,1]])) # False

أهمية اتجاه الحواف

من الأخطاء الشائعة عكس اتجاه الحافة: إذا كان المتطلب السابق هو [a, b] بمعنى «b قبل a»، فأضيفوا الحافة b → a وليس a → b. يجب أن يعكس اتجاه الحافة تدفق التبعية: يشير السهم من العنصر الذي يجب إنجازه أولًا إلى العنصر الذي يعتمد عليه. عند استخدام الاتجاه الخاطئ، سينعكس اكتشاف الدورات والترتيب، ما يؤدي إلى نتائج غير صحيحة في المسائل التي تتضمن تبعيات متعددة.

Course Schedule III: الصيغة الجشعة

Course Schedule III (LeetCode 630) مسألة مختلفة: للمقررات مدد زمنية ومواعيد نهائية، وهدفكم تعظيم عدد المقررات التي تجتازونها. تُحل هذه المسألة بطريقة جشعة باستخدام كومة قصوى: خذوا دائمًا المقرر ذي الموعد النهائي الأبعد أولًا؛ وإذا أدى إضافة مقرر إلى تجاوز موعده النهائي، فاستبدلوه بأطول مقرر اجتزتموه حتى ذلك الحين، إذا كان ذلك المقرر أطول. هذه مسألة جشعة وليست مسألة فرز طوبولوجي، ما يوضح أهمية قراءة نص المسألة بعناية.

التعامل مع العقد المعزولة

المقررات التي لا متطلبات سابقة لها ولا مقررات تعتمد عليها هي عقد معزولة، إذ تبلغ درجة دخولها 0 ولا تملك حواف خارجة. تتعامل خوارزمية Kahn معها بصورة صحيحة، إذ تُضاف إلى الطابور وتُعالج فورًا. احرصوا على تهيئة درجات الدخول لجميع العقد من 0 إلى n-1، حتى للعقد التي لا تظهر في قائمة المتطلبات السابقة، وإلا فلن تتم معالجتها.

# Example: 4 courses, but only courses 0 and 1 have a prerequisite relationship
# Courses 2 and 3 are isolated - they should appear in the output
from collections import deque, defaultdict

def findOrder_isolated(numCourses, prerequisites):
    graph = defaultdict(list)
    in_degree = [0] * numCourses  # initialise ALL nodes
    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:
        c = queue.popleft(); order.append(c)
        for nxt in graph[c]:
            in_degree[nxt] -= 1
            if in_degree[nxt] == 0: queue.append(nxt)
    return order if len(order) == numCourses else []

print(findOrder_isolated(4, [[1,0]]))  # [0,1,2,3] or [2,3,0,1] etc.

زمن إكمال المقررات بالتوازي

Parallel Courses II: أوجدوا الحد الأدنى لعدد الفصول الدراسية اللازمة لاجتياز جميع المقررات، عندما يُسمح باجتياز k مقررات كحد أقصى في كل فصل، مع الالتزام بالمتطلبات السابقة. يتطلب ذلك معالجة خوارزمية Kahn على مستوى كل طبقة، مع استخدام برمجة ديناميكية بالأقنعة الثنائية لقيد اختيار k مقررات، وهي مسألة أصعب بكثير تجمع بين الفرز الطوبولوجي والبرمجة الديناميكية بالأقنعة الثنائية.

استراتيجية التواصل في المقابلة

عند مواجهة مسألة من نوع Course Schedule في مقابلة: (1) حدّدوا فورًا أنها مسألة فرز طوبولوجي أو اكتشاف دورات. (2) نمذجوا الرسم البياني مع توضيح اتجاه الحواف. (3) اختاروا خوارزمية Kahn ‏(BFS) للبساطة أو DFS للألفة. (4) تعاملوا مع حالة الدورة صراحةً. (5) اذكروا التعقيد الزمني O(V+E). يوضح هذا النهج المنظم امتلاككم مهارات منهجية في حل المسائل.

اختبار شامل

اختبروا الحلين على مجموعة من المدخلات للتحقق من صحتهما. يتعامل نهج Kahn بسلاسة مع الترتيبات الصالحة المتعددة، فأي ترتيب طوبولوجي صالح يُعد إجابة مقبولة لمسألة Course Schedule II.

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:
        c = queue.popleft(); order.append(c)
        for nxt in graph[c]:
            in_degree[nxt] -= 1
            if in_degree[nxt] == 0: queue.append(nxt)
    return order if len(order) == numCourses else []

print(findOrder(1, []))                    # [0]
print(findOrder(2, [[0,1]]))              # [1, 0]
print(findOrder(3, [[1,0],[2,1]]))        # [0, 1, 2]
print(findOrder(3, [[1,0],[0,1]]))        # [] cycle

اختبار سريع

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

مراجعة الدرس

تعلمتم في هذا الدرس أن مسألتي Course Schedule I وII تستخدمان الفرز الطوبولوجي مع الحافة b → a للمتطلب السابق [a, b]، وأن Course Schedule I تتحقق فقط من len(order) == n، بينما تعيد Course Schedule II الترتيب نفسه، وأن اكتشاف الدورات باستخدام DFS وثلاث حالات بديل صالح لنهج Kahn القائم على BFS. في الجزء التالي سنستكشف خوارزمية Kosaraju للمكوّنات شديدة الاتصال.

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

هل درس «جدول المقررات I وII» مجاني؟

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

ماذا ستتعلم في «جدول المقررات I وII»؟

مثّل المتطلبات السابقة للمقررات كرسم بياني موجّه، واستخدم الترتيب الطوبولوجي لتحديد إمكانية إكمال جميع المقررات وترتيب إكمالها. تتمرن على DSA Interview Prep مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.

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

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

كم من الوقت يستغرق درس «جدول المقررات I وII»؟

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

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

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

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

  1. خوارزمية Kahn: الترتيب الطوبولوجي باستخدام BFS
  2. الترتيب الطوبولوجي باستخدام DFS بترتيب ما بعد الزيارة
  3. جدول المقررات I وII
  4. المكوّنات شديدة الاتصال باستخدام Kosaraju
← العودة إلى DSA Interview Prep