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 לא נלקח, וכן הלאה. המסלול הזה חייב לחזור לבקר בקורס: זהו מעגל. ולהפך, כל קורס שנמצא במעגל ממתין לקורס אחר שנמצא בו, ולכן אף אחד מהם אינו יכול להילקח ראשון. הסדר שבו הקורסים יוצאים מהתור הוא לוח זמנים תקף.
אלגוריתם
- עבור כל זוג [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, גרסת החיפוש לרוחב, אינו מצריך דאגה לעומק הרקורסיה ומחזיר לך סדר תקין של קורסים. חיפוש לעומק עם שלושה מצבים הוא הבחירה הטבעית כשצריך גם לדווח על המעגל, כי הקורסים במחסנית שלו יוצרים אותו.
מהו מיון טופולוגי?
סדר של הצמתים בגרף מכוון שבו כל חץ מצביע קדימה; כאן, סדר שבו כל דרישת קדם מופיעה לפני הקורס שזקוק לה. סדר כזה קיים בדיוק כאשר אין בגרף מעגל, והסדר שבו האלגוריתם של Kahn לוקח קורסים הוא אחד כזה. השאלה Course Schedule שואלת אם קיים סדר כזה.
בעיות דומות
בעיות שמשתמשות באותם רעיונות. פתרון של שתיים או שלוש מהן מקבע את הדפוס.
def canFinish(numCourses, prerequisites):
# כתבו כאן קודבדיקה #1
בדיקה #2
ארגומנטים
numCourses = 4 prerequisites = [[1, 0], [2, 1], [3, 1]]
הפלט הצפוי
true