Course Schedule
Jest numCourses kursów ponumerowanych od 0 do numCourses-1. Każda para [a, b] w prerequisites oznacza, że musisz ukończyć kurs b, zanim rozpoczniesz kurs a. Zwróć true, jeśli istnieje kolejność, w której możesz ukończyć każdy kurs, a false, jeśli taka kolejność nie istnieje.
Funkcja
- numCoursesinteger
- liczba kursów
- prerequisitesinteger-2d-array
- pary [a, b], z których każda oznacza, że kurs b poprzedza kurs a
- Zwracaboolean
- true, jeśli można ukończyć każdy kurs, w przeciwnym razie false
Ograniczenia
1 ≤ numCourses ≤ 1051 ≤ prerequisites.length ≤ 5000- Każda para
[a, b]spełnia warunek0 ≤ a, b < numCourses. - Żadna para nie pojawia się dwukrotnie.
- A para może dwukrotnie wskazywać ten sam kurs,
[a, a]. Ten kurs wymaga najpierw ukończenia samego siebie, więc nigdy nie można go ukończyć.
Przykłady
- Wejście
- numCourses = 4prerequisites = [[1, 0], [2, 1], [3, 1]]
- Wyjście
- true
- Wyjaśnienie
- Kurs 0 nie ma żadnych wymagań wstępnych, więc realizujesz go jako pierwszy. Dzięki temu odblokowuje się kurs 1, a kurs 1 odblokowuje zarówno 2, jak i 3, więc kolejność 0, 1, 2, 3 jest prawidłowa.
- Wejście
- numCourses = 3prerequisites = [[0, 2], [2, 1], [1, 0]]
- Wyjście
- false
- Wyjaśnienie
- Kurs 0 czeka na 2, kurs 2 czeka na 1, a kurs 1 czeka na 0. Te trzy kursy czekają na siebie nawzajem w pętli, więc żaden z nich nie może być tym, który weźmiesz jako pierwszy.
+20 ukrytych testów przy wysłaniu
Pytanie dodatkowe
W jednym semestrze można zrealizować dowolną liczbę kursów, pod warunkiem że wymagania wstępne każdego kursu zostały spełnione w poprzednich semestrach. Jaka jest najmniejsza liczba semestrów potrzebna do zrealizowania wszystkich kursów?
Podpowiedzi
Otwieraj je po kolei. Każda zdradza trochę więcej.
Przedstaw każdy kurs jako kropkę, a każdą parę
[a, b]jako strzałkę odbdoa. Jaki kształt na tym rysunku uniemożliwiłby ukończenie kursu?Pętla strzałek. Każdy kurs w pętli czeka na inny kurs z tej samej pętli, więc żaden z nich nie może być pierwszy. Pytanie brzmi, czy graf zawiera cykl.
Policz, na ile wymaganych wcześniej kursów każdy kurs nadal czeka. Umieść w kolejce kursy, których licznik wynosi 0, a za każdym razem, gdy pobierzesz jeden z nich, zmniejsz licznik każdego kursu, który na niego czeka. Jeśli do kolejki trafi mniej niż
numCourseskursów, oznacza to, że istnieje cykl.
Rozwiązanie
Zamień pary w graf skierowany z V = numCourses węzłami i E = prerequisites.length krawędziami, po jednej strzałce b → a dla każdej pary [a, b]. Każdy kurs można ukończyć wtedy i tylko wtedy, gdy graf nie zawiera cyklu. Algorytm Kahna rozstrzyga to tak, jak zaplanowałby to student: kontynuuj realizowanie kursów, których wszystkie wymagania wstępne są już spełnione, i sprawdź, czy najpierw skończą się kursy, czy możliwości wyboru.
Bierz udział w każdym bezpłatnym kursie, runda po rundzie
Poprawne, ale nie kończy się na największych testach
Intuicja
Zaplanuj to tak, jak zrobiłby to uczeń. W każdej rundzie sprawdź każdy kurs, którego jeszcze nie ukończyłeś. Jeśli wszystkie jego wymagania wstępne są spełnione, ukończ go. Powtarzaj, aż w którejś rundzie nie ukończysz żadnego kursu. Jeśli do tego czasu ukończysz wszystkie kursy, odpowiedź brzmi true.
Dlaczego runda, w której nie udało się ukończyć żadnego kursu, oznacza false: każdy pozostały kurs ma wymaganie wstępne, które również nie zostało spełnione. Zacznij od dowolnego pozostałego kursu i za każdym razem przechodź do jednego z jego niespełnionych wymagań wstępnych. Nie zabraknie Ci kolejnych kroków, a kursów jest tylko określona liczba, więc wrócisz do kursu, który już odwiedziłeś. To cykl, a kursy w nim będą na siebie czekać bez końca.
Ta metoda jest poprawna, ale w każdej rundzie ponownie sprawdza każdą parę i każdy kurs, a w jednej rundzie można ukończyć zaledwie jeden kurs. Łańcuch 5,001 kursów, z których każdy wymaga ukończenia poprzedniego, zajmuje ponad 5,000 rund; wśród 100,000 kursów to około 5 × 10^8 sprawdzeń, z których prawie wszystkie dotyczą kursów, których status się nie zmienił.
Algorytm
- Oznacz każdy kurs jako nieukończony.
- Oznacz kurs jako zablokowany, jeśli któraś para wskazuje dla niego nieukończony warunek wstępny.
- Wybierz każdy kurs, który nie jest ani ukończony, ani zablokowany.
- Jeśli w tej rundzie niczego nie wybrano, zatrzymaj się; w przeciwnym razie wróć do kroku 2.
- Zwróć true, jeśli każdy kurs został ukończony.
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 == numCoursesPrzeszukiwanie w głąb z trzema stanami
Intuicja
Cykl to ścieżka, która wraca do miejsca, w którym się zaczęła. Wyszukiwanie w głąb znajduje cykl, zapamiętując, które kursy znajdują się na aktualnie przemierzanej ścieżce. Przypisz każdemu kursowi jeden z trzech stanów: nieodwiedzony, na bieżącej ścieżce i ukończony.
Przechodź od kursu wzdłuż jego strzałek do kursów, które na niego czekają. Oznacz kurs jako „na ścieżce”, gdy na niego wejdziesz, a jako „ukończony”, gdy sprawdzisz wszystkie wychodzące z niego strzałki i wrócisz. Strzałka prowadząca do kursu znajdującego się na ścieżce oznacza, że zatoczyłeś krąg: zwróć false. Strzałka prowadząca do ukończonego kursu jest bezpieczna, ponieważ sprawdzono już wszystko, co jest z niego osiągalne, i nie ma tam cyklu, więc ją pomijasz. Każdy kurs jest odwiedzany raz, a każda strzałka jest śledzona raz.
Dwa stany nie wystarczą. W rombie 0 → 1, 0 → 2, 1 → 3, 2 → 3 wyszukiwanie dociera do kursu 3 po raz drugi przez 2, ale do tego czasu kurs 3 jest już ukończony, a nie znajduje się na ścieżce, więc nie ma cyklu. Tylko strzałka prowadząca z powrotem na bieżącą ścieżkę zamyka pętlę.
Zapisz wyszukiwanie, używając własnego stosu oraz pozycji następnej niezbadanej strzałki dla każdego kursu. Wersja rekurencyjna jest krótsza, ale łańcuch 5,000 kursów wymagałby 5,000 wywołań w głąb.
Algorytm
- Zbuduj dla każdego kursu listę kursów, które na niego czekają.
- Dla każdego kursu, który nie został odwiedzony, oznacz go na ścieżce i umieść na stosie.
- Spójrz na szczyt stosu. Jeśli nie ma już żadnej strzałki, oznacz go jako ukończony i zdejmij ze stosu; w przeciwnym razie podążaj za następną strzałką.
- Jeśli strzałka prowadzi do kursu znajdującego się na ścieżce, zwróć false. Jeśli prowadzi do kursu, który nie został odwiedzony, oznacz ten kurs na ścieżce i umieść go na stosie.
- Gdy wszystkie kursy będą ukończone, zwróć 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 TrueAlgorytm Kahna
Intuicja
Rundy w pierwszym podejściu marnują czas na ponowne sprawdzanie kursów, które się nie zmieniły. Kurs staje się dostępny tylko w jednym momencie: gdy zostanie zaliczone jego ostatnie wymaganie wstępne. Dlatego policz, na ile wymagań wstępnych każdy kurs nadal czeka — to jego stopień wejściowy. Gdy zaliczasz kurs, zmniejsz licznik każdego kursu, który na niego czeka. Gdy licznik spadnie do 0, oznacza to, że kurs jest już dostępny, więc dodajesz go do kolejki.
Na początku umieść w kolejce wszystkie kursy, których licznik wynosi 0, a potem pobieraj z niej kursy, aż będzie pusta. W pierwszym przykładzie początkowe liczniki dla kursów od 0 do 3 wynoszą 0, 1, 1, 1. Zaliczenie kursu 0 zmniejsza licznik kursu 1 do 0; zaliczenie kursu 1 zmniejsza liczniki kursów 2 i 3 do 0; wszystkie cztery kursy zostają zaliczone, więc odpowiedzią jest true. Każdy kurs trafia do kolejki co najwyżej raz, a każda para zmniejsza jeden licznik raz, więc złożoność wynosi O(V + E).
Dlaczego pozostawiony kurs oznacza cykl: jeśli kolejka się opróżni, zanim kurs a zostanie zaliczony, jego licznik jest większy od 0, więc jedno z jego wymagań wstępnych, b, również nie zostało zaliczone. To samo dotyczy kursu b i tak dalej. Przechodząc od kursu do niezliczonego wymagania wstępnego, nigdy się nie zatrzymasz, więc w końcu trafisz ponownie na ten sam kurs, co oznacza cykl. W drugim przykładzie początkowy licznik żadnego kursu nie wynosi 0, kolejka jest więc pusta, a żaden z trzech kursów nie zostaje zaliczony.
W drugą stronę działa to tak samo: kurs należący do cyklu czeka na inny kurs z tego samego cyklu, więc jego licznik nie może spaść do 0, zanim tamten kurs nie zostanie zaliczony, a żaden z nich nie zostanie zaliczony jako pierwszy. A zatem „wszystkie kursy zostały zaliczone” i „nie ma cyklu” oznaczają to samo. Dodatkową korzyścią jest to, że kolejność, w jakiej kursy opuszczają kolejkę, stanowi prawidłowy harmonogram.
Algorytm
- Dla każdej pary [a, b] dodaj a do listy kursów, które czekają na b, i zwiększ stopień wejściowy a o 1.
- Umieść w kolejce każdy kurs o stopniu wejściowym równym 0.
- Wyjmij kurs z kolejki i dolicz go do liczby. Zmniejsz stopień wejściowy każdego kursu, który na niego czeka, i dodaj do kolejki każdy kurs, którego stopień wejściowy osiągnie 0.
- Gdy kolejka będzie pusta, zwróć informację, czy liczba jest równa
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
Pułapki i przypadki brzegowe
Większość błędów wynika z pomylenia kierunku pary, zbyt rygorystycznego sprawdzania cykli lub pominięcia kursów, które nie występują w żadnej parze.
- Pomylenie kierunku.
[a, b]oznacza, że b jest pierwszy, więc strzałka prowadzi z b do a, a stopień wejściowy a zwiększa się o 1. Zbudowanie list w jednym kierunku i zliczanie stopni wejściowych w przeciwnym kierunku psuje algorytm. - Pominięcie kursów, które nie występują w żadnej parze. Przy
numCourses = 5i jedynej parze[4, 3]kursy 0, 1 i 2 nadal się liczą. Umieść w kolejce każdy kurs o stopniu wejściowym 0, a nie tylko te, które pojawiły się w parze. - Kurs, który jest własnym wymaganiem wstępnym,
[2, 2]. To cykl długości jeden: jego stopień wejściowy nigdy nie osiąga 0, a odpowiedź brzmi false. - Dwa stany zamiast trzech w przeszukiwaniu w głąb. W rombie 0 → 1, 0 → 2, 1 → 3, 2 → 3 kurs 3 jest osiągany dwukrotnie, co wygląda jak cykl, jeśli śledzisz tylko elementy „odwiedzone”. Pętlę zamyka tylko strzałka prowadząca z powrotem do bieżącej ścieżki.
- Rekurencja w przypadku długich łańcuchów. Łańcuch 5,000 kursów wymaga 5,000 wywołań w głąb, przekraczając domyślny limit Python wynoszący 1,000.
- Zwracanie true po opróżnieniu kolejki bez porównania liczby zaliczonych kursów z
numCourses.
Najczęstsze pytania4
Jaka jest złożoność czasowa problemu Course Schedule?
O(V + E), gdzie V to liczba kursów, a E to liczba par, przy użyciu algorytmu Kahna lub przeszukiwania w głąb. Tworzenie list odczytuje każdą parę raz, każdy kurs trafia do kolejki najwyżej raz, a każda para zmniejsza jeden licznik raz. Listy i liczniki zajmują O(V + E) pamięci.
Dlaczego kurs pozostały w algorytmie Kahna oznacza, że istnieje cykl?
Kurs pozostaje tylko wtedy, gdy jego licznik nigdy nie osiągnął 0, więc co najmniej jeden z jego warunków wstępnych również pozostaje. Podążaj za tym oczekiwaniem od kursu do kursu: każdy krok prowadzi do kolejnego pozostałego kursu, a ponieważ kursów jest skończenie wiele, ścieżka musi wrócić do kursu, który już napotkała. Odcinek między tymi dwoma odwiedzinami jest cyklem.
Czy do harmonogramu kursów użyć BFS czy DFS?
Oba działają w czasie O(V + E). Algorytm Kahna, czyli wersja z przeszukiwaniem wszerz, nie wymaga martwienia się o głębokość rekurencji i od razu podaje prawidłową kolejność kursów. Przeszukiwanie w głąb z trzema stanami jest równie szybkie i stanowi naturalny wybór, gdy trzeba również zgłosić cykl, ponieważ tworzą go kursy znajdujące się na stosie.
Czym jest sortowanie topologiczne?
Uporządkowanie wierzchołków grafu skierowanego, w którym każda strzałka wskazuje do przodu; tutaj jest to kolejność kursów, w której każdy kurs wymagany wcześniej poprzedza kurs, który go wymaga. Taka kolejność istnieje dokładnie wtedy, gdy graf nie zawiera cyklu, a kolejność, w jakiej algorytm Kahna wybiera kursy, jest jedną z takich kolejności. Course Schedule pyta, czy istnieje porządek topologiczny.
Podobne zadania
Zadania oparte na tych samych pomysłach. Rozwiązanie dwóch lub trzech utrwala schemat.
Python
def canFinish(numCourses, prerequisites):
# Napisz kod tutajPrzypadek 1
Przypadek 2
Wejście
numCourses = 4 prerequisites = [[1, 0], [2, 1], [3, 1]]
Oczekiwane
true