Course Schedule
هناك numCourses مقررًا، مرقمة من 0 إلى numCourses-1. كل زوج [a, b] في prerequisites يعني أنه عليك إكمال المقرر b قبل أن تبدأ المقرر a. أعد true إذا كان هناك ترتيب يتيح لك إكمال جميع المقررات، وfalse إذا لم يوجد.
الدالة
- numCoursesinteger
- عدد المقررات الدراسية
- prerequisitesinteger-2d-array
- الأزواج [a, b]، حيث يعني كل زوج أن المقرر b يسبق المقرر a
- تُرجعboolean
- صحيح إذا أمكن إكمال جميع المقررات، وخطأ خلاف ذلك
القيود
1 ≤ numCourses ≤ 1051 ≤ prerequisites.length ≤ 5000- كل زوج
[a, b]يحقق0 ≤ a, b < numCourses. - لا يظهر أي زوج أكثر من مرة.
- قد يُسمّي الزوج المقرر نفسه مرتين،
[a, a]. يحتاج هذا المقرر إلى نفسه أولًا، لذا لا يمكن أخذه أبدًا.
أمثلة
- المدخلات
- numCourses = 4prerequisites = [[1, 0], [2, 1], [3, 1]]
- المخرجات
- true
- الشرح
- المقرر 0 ليس له متطلبات سابقة، لذا تأخذه أولًا. وهذا يتيح لك أخذ المقرر 1، الذي يتيح بدوره أخذ المقررين 2 و3، لذا فإن الترتيب 0، 1، 2، 3 مناسب.
- المدخلات
- numCourses = 3prerequisites = [[0, 2], [2, 1], [1, 0]]
- المخرجات
- false
- الشرح
- تنتظر الدورة 0 الدورة 2، وتنتظر الدورة 2 الدورة 1، وتنتظر الدورة 1 الدورة 0. تنتظر الدورات الثلاث بعضها بعضًا في حلقة، لذا لا يمكن لأيٍّ منها أن تبدأ أولًا.
سؤال إضافي
يمكن أخذ أي عدد من المقررات في فصل دراسي واحد، ما دامت المتطلبات السابقة لكل مقرر قد أُنجزت في فصول دراسية سابقة. ما أقل عدد من الفصول الدراسية اللازمة لإكمال جميع المقررات؟
تلميحات
افتحها واحدًا تلو الآخر. كل تلميح يكشف أكثر قليلًا.
ارسم كل مقرر كنقطة، وكل زوج
[a, b]كسهم منbإلىa. ما الشكل في هذا الرسم الذي يجعل إتمام المقررات مستحيلاً؟حلقة من الأسهم. كل مقرر دراسي في حلقة ينتظر مقررًا آخر من الحلقة نفسها، لذا لا يمكن لأيٍّ منها أن يبدأ أولًا أبدًا. والسؤال هو ما إذا كان الرسم البياني يحتوي على دورة.
احسب عدد المتطلبات السابقة التي لا يزال كل مقرر ينتظرها. ابدأ قائمة انتظار بالمقررات التي يبلغ عددها 0، وفي كل مرة تختار فيها مقررًا، قلّل عدد كل مقرر ينتظره. إذا لم يصل إلى قائمة الانتظار عدد من المقررات أقل من
numCourses، فهناك دورة.
الحل
ارسم سهمًا من b إلى a لكل زوج [a, b]. يمكن إنهاء كل مقرر إذا وفقط إذا لم تتضمن هذه الشبكة، التي تحتوي على V = numCourses عقدة وE = prerequisites.length سهمًا، أي دورة. تتحقق خوارزمية Kahn من ذلك بالطريقة التي يخطط بها الطالب: واصل دراسة مقرر استوفيت متطلباته السابقة، ثم تحقق مما إذا كان قد تم أخذ كل مقرر.
احصل على كل دورة مجانية، جولة بعد جولة
صحيحة، لكنها لا تنتهي في أكبر الاختبارات
الفكرة
في كل جولة، خذ كل مقرر لم يُؤخذ بعد وكانت متطلباته السابقة قد أُخذت كلها. توقّف عندما لا تأخذ الجولة أي مقرر، وأجب بـ true إذا أُخذت كل المقررات. لا تخلو الجولة من أخذ أي مقرر إلا عندما يكون كل مقرر متبقٍ بانتظار مقرر متبقٍ آخر، وهذا يعني وجود دورة.
تعيد كل جولة قراءة كل زوج وكل مقرر، ومع ذلك قد تأخذ مقررًا واحدًا فقط. سلسلة من 5,001 مقرر ضمن 100,000 مقرر تحتاج إلى أكثر من 5,000 جولة: نحو 5 × 10^8 عملية تحقق.
الخوارزمية
- علِّم على كل مقرر بأنه غير مُنجز.
- علِّم على المقرر بأنه محظور إذا منحه زوجٌ ما متطلبًا سابقًا غير مُنجز.
- خذ كل مقرر لم يُنجز وليس محظورًا.
- إذا لم تأخذ الجولة أي مقرر، فتوقّف؛ وإلا فارجع إلى الخطوة 2.
- أعِد true إذا كانت جميع المقررات مُنجزة.
def canFinish(numCourses, prerequisites):
taken = [False] * numCourses
count = 0
while True:
# A course is blocked while one of its prerequisites is not taken.
blocked = [False] * numCourses
for course, before in prerequisites:
if not taken[before]:
blocked[course] = True
# Take every course that is free this round.
progress = False
for c in range(numCourses):
if not taken[c] and not blocked[c]:
taken[c] = True
count += 1
progress = True
if not progress:
break
return count == numCoursesالبحث بالعمق أولاً بثلاث حالات
الفكرة
يكتشف البحث بالعمق أولًا وجود دورة من خلال تتبّع المسار الذي يسلكه. أعطِ كل مقرر إحدى ثلاث حالات: لم تتم زيارته، على المسار، منتهٍ. اتبع الأسهم من مقرر إلى المقررات التي تنتظره. حدّد المقرر بأنه على المسار عند دخوله، وبأنه منتهٍ عند استكشاف جميع أسهمه.
السهم المؤدي إلى مقرر على المسار يُغلق حلقة: أعد false. أما السهم المؤدي إلى مقرر منتهٍ فهو آمن، لأن كل ما يمكن الوصول إليه انطلاقًا منه قد تم التحقق منه ولا يحتوي على دورة، لذا فإن الوصول إلى مقرر مرتين لا يشكّل بحد ذاته دورة. يُدخَل كل مقرر مرة واحدة ويُتّبَع كل سهم مرة واحدة: O(V + E).
الخوارزمية
- أنشئ، لكل مقرر، قائمة بالمقررات التي تنتظره.
- لكل مقرر لم تتم زيارته، علّمه على المسار وأضِفه إلى المكدس.
- انظر إلى أعلى المكدس. إذا لم يتبقَّ له سهم، فعلّمه على أنه مكتمل وأخرجه من المكدس؛ وإلا فاتبع سهمه التالي.
- إذا كان السهم يؤدي إلى مقرر على المسار، فأعِد false. وإذا كان يؤدي إلى مقرر لم تتم زيارته، فعلّم ذلك المقرر على المسار وأضِفه إلى المكدس.
- عندما تكتمل جميع المقررات، أعِد true.
def canFinish(numCourses, prerequisites):
unlocks = [[] for _ in range(numCourses)]
for course, before in prerequisites:
unlocks[before].append(course)
# 0 = not visited, 1 = on the current path, 2 = done, no cycle below it
state = [0] * numCourses
# next_edge[c] counts the edges out of c the search has already followed.
next_edge = [0] * numCourses
for start in range(numCourses):
if state[start] != 0:
continue
# A stack of our own instead of recursion: a chain of 5,000
# courses would go 5,000 calls deep.
state[start] = 1
stack = [start]
while stack:
course = stack[-1]
if next_edge[course] == len(unlocks[course]):
state[course] = 2
stack.pop()
continue
nxt = unlocks[course][next_edge[course]]
next_edge[course] += 1
if state[nxt] == 1:
return False # an edge back to the current path closes a cycle
if state[nxt] == 0:
state[nxt] = 1
stack.append(nxt)
return Trueخوارزمية كان
الفكرة
تصبح المقررة متاحة في لحظة واحدة: عندما تُؤخذ آخر مقررة مطلوبة لها. لذا احتفظ بدرجة الدخول لكل مقررة، أي عدد المتطلبات السابقة التي ما زالت تنتظرها، وابدأ بطابور يضم كل مقررة عددها 0. أخذ مقررة يخفض العدد لكل مقررة تنتظرها، والمقررة التي يصل عددها إلى 0 تنضم إلى الطابور. في المثال الأول تبدأ الأعداد بـ 0، 1، 1، 1: أخذ 0 يتيح 1، وأخذ 1 يتيح 2 و3. تدخل كل مقررة الطابور مرة واحدة، ويخفض كل زوج عددًا مرة واحدة: O(V + E).
إذا فرغ الطابور بينما المقررة a لم تُؤخذ، فعددها أكبر من 0، لذا فإن أحد متطلباتها السابقة لم يُؤخذ أيضًا، وهكذا. لا بد أن تعود هذه المسيرة إلى مقررة سبق المرور بها: أي دورة. وعلى العكس، تنتظر كل مقررة في دورة مقررة أخرى فيها، لذا لا تبدأ أي منها أولًا. والترتيب الذي تغادر به المقررات الطابور هو جدول صالح.
الخوارزمية
- لكل زوج [a, b]، أضف a إلى قائمة المقررات التي تنتظر b، وأضف 1 إلى الدرجة الداخلة لـ a.
- ضع كل مقرر درجته الداخلة 0 في قائمة انتظار.
- أخرج مقررًا من قائمة الانتظار واحتسبه. خفّض الدرجة الداخلة لكل مقرر ينتظره، وأضف كل مقرر تصل درجته إلى 0 إلى قائمة الانتظار.
- عندما تصبح قائمة الانتظار فارغة، أعد ما إذا كان العدد يساوي
numCourses.
from collections import deque
def canFinish(numCourses, prerequisites):
# unlocks[b] lists the courses that wait for b.
# indegree[a] counts the prerequisites of a that are not taken yet.
unlocks = [[] for _ in range(numCourses)]
indegree = [0] * numCourses
for course, before in prerequisites:
unlocks[before].append(course)
indegree[course] += 1
# Every course with no prerequisites can be taken right away.
queue = deque(c for c in range(numCourses) if indegree[c] == 0)
taken = 0
while queue:
course = queue.popleft()
taken += 1
for nxt in unlocks[course]:
indegree[nxt] -= 1
if indegree[nxt] == 0:
queue.append(nxt)
# A course on a cycle never gets down to 0, so it is never taken.
return taken == numCourses
أخطاء شائعة وحالات حدّية
- الخلط في الاتجاه. تعني
[a, b]أن b يأتي أولًا: يتجه السهم من b إلى a، وتزداد درجة الدخول لـ a. - نسيان المقررات التي لا تظهر في أي زوج. عند
numCourses = 5ومع الزوج الوحيد[4, 3]، يجب أيضًا أن تبدأ المقررات 0 و1 و2 في قائمة الانتظار. - استخدام حالتين بدلًا من ثلاث في البحث بالعمق أولًا. في المعيّن 0 → 1، 0 → 2، 1 → 3، 2 → 3، يُزار المقرر 3 مرتين ويبدو الأمر كأنه دورة إذا كنت تتعقب «التي شوهدت» فقط.
- الاستدعاء التكراري على سلسلة من 5,000 مقرر: 5,000 استدعاء عميقًا، متجاوزًا الحد الافتراضي في Python البالغ 1,000.
- إرجاع true عند إفراغ قائمة الانتظار من دون التحقق من أخذ
numCoursesمقررًا. سيُرجع المثال الثاني true.
أسئلة شائعة3
ما هو التعقيد الزمني لمسألة جدولة المقررات؟
O(V + E)، حيث إن V هو عدد المقررات وE هو عدد الأزواج، باستخدام خوارزمية كاهن أو البحث بالعمق أولاً. يُقرأ كل زوج مرة واحدة، ويُعالَج كل مقرر مرة واحدة. تتطلب القوائم والعدادات مساحة O(V + E).
هل ينبغي استخدام BFS أم DFS لجدول المقررات الدراسية؟
كلاهما يعمل في زمن O(V + E). لا داعي للقلق بشأن عمق الاستدعاء التعاودي في خوارزمية كان، وهي النسخة التي تستخدم البحث بعرض الشجرة، كما أنها تمنحك ترتيبًا صالحًا للمقررات. ويُعد البحث بالعمق مع ثلاث حالات الخيار الطبيعي عندما يتعين عليك أيضًا الإبلاغ عن الدورة، لأن المقررات الموجودة في مكدسه تكوّنها.
ما هو الترتيب الطوبولوجي؟
ترتيبٌ لعُقد رسم بياني موجّه بحيث يشير كل سهم إلى الأمام؛ وهنا، ترتيبٌ يأتي فيه كل متطلب سابق قبل المقرر الذي يحتاج إليه. يوجد هذا الترتيب بالضبط عندما لا تحتوي الرسوم البيانية على دورة، والترتيب الذي يأخذ به Kahn's algorithm المقررات هو أحد هذه الترتيبات. يسأل Course Schedule عمّا إذا كان مثل هذا الترتيب موجودًا.
مسائل مشابهة
مسائل تعتمد على الأفكار نفسها. حلّ اثنتين أو ثلاث منها يثبّت النمط.
def canFinish(numCourses, prerequisites):
# اكتب الكود هنااختبار #1
اختبار #2
الوسائط
numCourses = 4 prerequisites = [[1, 0], [2, 1], [3, 1]]
الإخراج المتوقع
true