Course Schedule
Có numCourses khóa học, được đánh số từ 0 đến numCourses-1. Mỗi cặp [a, b] trong prerequisites có nghĩa là bạn phải hoàn thành khóa học b trước khi bắt đầu khóa học a. Trả về true nếu có thứ tự giúp bạn hoàn thành tất cả các khóa học, và false nếu không có thứ tự nào như vậy.
Hàm
- numCoursesinteger
- số lượng khóa học
- prerequisitesinteger-2d-array
- các cặp [a, b], mỗi cặp có nghĩa là khóa học b đứng trước khóa học a
- Trả vềboolean
- true nếu có thể hoàn thành mọi khóa học, false nếu không
Ràng buộc
1 ≤ numCourses ≤ 1051 ≤ prerequisites.length ≤ 5000- Mỗi cặp
[a, b]thỏa mãn0 ≤ a, b < numCourses. - Không có cặp nào xuất hiện hai lần.
- Một cặp có thể ghi tên cùng một khóa học hai lần,
[a, a]. Khóa học đó cần chính nó trước, nên không thể học được.
Ví dụ
- Đầu vào
- numCourses = 4prerequisites = [[1, 0], [2, 1], [3, 1]]
- Đầu ra
- true
- Giải thích
- Khóa học 0 không có điều kiện tiên quyết, nên bạn học khóa đó trước. Nhờ vậy, khóa học 1 không còn điều kiện tiên quyết, rồi đến khóa học 2 và 3, vì thế thứ tự 0, 1, 2, 3 là phù hợp.
- Đầu vào
- numCourses = 3prerequisites = [[0, 2], [2, 1], [1, 0]]
- Đầu ra
- false
- Giải thích
- Khóa học 0 chờ 2, 2 chờ 1 và 1 chờ 0. Cả ba chờ nhau trong một vòng lặp, nên không ai có thể đi trước.
Câu hỏi mở rộng
Bạn có thể học bao nhiêu khóa học tùy ý trong một học kỳ, miễn là bạn đã hoàn thành các khóa học tiên quyết trong những học kỳ trước. Cần ít nhất bao nhiêu học kỳ để học hết mọi khóa học?
Gợi ý
Mở từng gợi ý một. Mỗi gợi ý tiết lộ thêm một chút.
Hãy biểu diễn mỗi khóa học bằng một chấm và mỗi cặp
[a, b]bằng một mũi tên từbđếna. Hình dạng nào trong sơ đồ đó sẽ khiến bạn không thể hoàn thành?Một vòng lặp gồm các mũi tên. Mỗi khóa học trong vòng lặp đều chờ một khóa học khác trong cùng vòng lặp, vì vậy không khóa học nào có thể bắt đầu trước. Câu hỏi đặt ra là đồ thị có chu trình hay không.
Đếm số môn học tiên quyết mà mỗi khóa học vẫn đang chờ. Bắt đầu một hàng đợi với những khóa học có số lượng bằng 0, và mỗi lần lấy một khóa học ra, giảm số lượng của mọi khóa học đang chờ môn đó. Nếu chưa đến
numCourseskhóa học được đưa vào hàng đợi, thì có một chu trình.
Lời giải
Vẽ một mũi tên b → a cho mỗi cặp [a, b]. Có thể hoàn thành mọi khóa học khi và chỉ khi đồ thị này, với V = numCourses nút và E = prerequisites.length mũi tên, không có chu trình. Thuật toán Kahn kiểm tra theo cách một học viên lên kế hoạch: tiếp tục học một khóa học có các điều kiện tiên quyết đã hoàn thành, rồi xem liệu có thể học hết mọi khóa học hay không.
Tham gia mọi khóa học miễn phí, hết vòng này đến vòng khác
Đúng, nhưng không chạy xong với các test lớn nhất
Ý tưởng
Trong mỗi vòng, hãy lấy mọi khóa học chưa được lấy mà tất cả khóa học tiên quyết của chúng đã được lấy. Dừng lại khi một vòng không lấy được khóa học nào, rồi trả về true nếu đã lấy mọi khóa học. Một vòng không lấy được khóa học nào chỉ xảy ra khi mỗi khóa học còn lại đều chờ một khóa học còn lại khác, nghĩa là có một chu trình.
Mỗi vòng đọc lại mọi cặp và mọi khóa học, nhưng có thể chỉ lấy một khóa học. Một chuỗi gồm 5,001 khóa học trong tổng số 100,000 khóa học cần hơn 5,000 vòng: khoảng 5 × 10^8 lần kiểm tra.
Thuật toán
- Đánh dấu mọi khóa học là chưa học.
- Đánh dấu một khóa học là bị chặn nếu có cặp nào đó cho biết khóa học đó có một môn tiên quyết chưa học.
- Học mọi khóa học chưa học và không bị chặn.
- Nếu vòng này không học khóa nào, hãy dừng lại; nếu không, quay lại bước 2.
- Trả về true nếu đã học mọi khóa học.
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 == numCoursesTìm kiếm theo chiều sâu với ba trạng thái
Ý tưởng
Thuật toán tìm kiếm theo chiều sâu tìm chu trình bằng cách theo dõi đường đi mà nó đang duyệt. Gán cho mỗi khóa học một trong ba trạng thái: chưa thăm, đang trên đường đi, đã hoàn tất. Đi theo các mũi tên từ một khóa học đến những khóa học phụ thuộc vào nó. Đánh dấu một khóa học là đang trên đường đi khi bạn bắt đầu duyệt khóa học đó, và là đã hoàn tất khi đã duyệt hết các mũi tên của khóa học.
Mũi tên dẫn đến một khóa học đang trên đường đi sẽ khép lại một vòng lặp: trả về false. Mũi tên dẫn đến một khóa học đã hoàn tất là an toàn, vì mọi thứ có thể đi đến từ khóa học đó đều đã được kiểm tra và không có chu trình. Vì vậy, một khóa học được đến nhiều lần tự nó không tạo thành chu trình. Mỗi khóa học được duyệt một lần và mỗi mũi tên được đi theo một lần: O(V + E).
Thuật toán
- Lập danh sách các khóa học đang chờ từng khóa học.
- Với mỗi khóa học chưa được truy cập, đánh dấu khóa học đó trên đường đi và đẩy nó vào ngăn xếp.
- Xem phần tử trên cùng của ngăn xếp. Nếu không còn mũi tên nào, đánh dấu phần tử đó là đã hoàn tất rồi lấy nó ra khỏi ngăn xếp; nếu không, đi theo mũi tên tiếp theo.
- Nếu mũi tên dẫn đến một khóa học trên đường đi, trả về false. Nếu mũi tên dẫn đến một khóa học chưa được truy cập, đánh dấu khóa học đó trên đường đi và đẩy nó vào ngăn xếp.
- Khi mọi khóa học đều hoàn tất, trả về 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 TrueThuật toán Kahn
Ý tưởng
Một khóa học trở nên có thể học khi điều kiện tiên quyết cuối cùng của khóa học đó được hoàn thành. Vì vậy, hãy theo dõi bậc vào (in-degree) của mỗi khóa học, tức số điều kiện tiên quyết mà khóa học đó vẫn đang chờ, rồi tạo hàng đợi gồm tất cả khóa học có số đếm bằng 0. Khi học một khóa học, số đếm của mỗi khóa học đang chờ khóa học đó sẽ giảm đi, và khóa học nào có số đếm giảm còn 0 sẽ được thêm vào hàng đợi. Trong ví dụ đầu tiên, các số đếm ban đầu là 0, 1, 1, 1: học khóa học 0 sẽ mở khóa khóa học 1, còn học khóa học 1 sẽ mở khóa khóa học 2 và 3. Mỗi khóa học vào hàng đợi một lần và mỗi cặp chỉ làm giảm một số đếm một lần: O(V + E).
Nếu hàng đợi trống trong khi khóa học a chưa được học, số đếm của khóa học đó lớn hơn 0, vậy nên một điều kiện tiên quyết của a cũng chưa được hoàn thành, và cứ tiếp tục như vậy. Lần theo các điều kiện này chắc chắn sẽ quay lại một khóa học đã gặp: đó là một chu trình. Ngược lại, mỗi khóa học trong chu trình đều chờ một khóa học khác trong chu trình, nên không khóa học nào có thể được học trước. Thứ tự các khóa học rời khỏi hàng đợi tạo thành một lịch học hợp lệ.
Thuật toán
- Với mỗi cặp [a, b], thêm a vào danh sách các khóa học đang chờ b, rồi tăng bậc vào của a thêm 1.
- Đưa mọi khóa học có bậc vào bằng 0 vào hàng đợi.
- Lấy một khóa học khỏi hàng đợi và đếm khóa học đó. Giảm bậc vào của mọi khóa học đang chờ khóa học đó, rồi thêm mỗi khóa học có bậc vào bằng 0 vào hàng đợi.
- Khi hàng đợi trống, trả về kết quả cho biết số lượng có bằng
numCourseshay không.
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
Lỗi thường gặp và trường hợp biên
- Nhầm chiều.
[a, b]có nghĩa là b đứng trước: mũi tên đi từ b đến a và bậc vào của a tăng lên. - Quên các khóa học không xuất hiện trong cặp nào. Với
numCourses = 5và cặp duy nhất là[4, 3], các khóa học 0, 1 và 2 cũng phải được đưa vào hàng đợi ngay từ đầu. - Chỉ dùng hai trạng thái thay vì ba trong tìm kiếm theo chiều sâu. Trong hình thoi 0 → 1, 0 → 2, 1 → 3, 2 → 3, khóa học 3 được đi đến hai lần và trông giống một chu trình nếu bạn chỉ theo dõi trạng thái "đã thấy".
- Đệ quy trên một chuỗi gồm 5.000 khóa học: độ sâu 5.000 lời gọi, vượt quá giới hạn mặc định 1.000 của Python.
- Trả về true khi hàng đợi trống mà không kiểm tra xem đã học đủ
numCourseskhóa học chưa. Ví dụ thứ hai sẽ trả về true.
Câu hỏi thường gặp3
Độ phức tạp thời gian của bài toán Course Schedule là bao nhiêu?
O(V + E), trong đó V là số khóa học và E là số cặp, khi dùng thuật toán của Kahn hoặc tìm kiếm theo chiều sâu. Mỗi cặp được đọc một lần và mỗi khóa học được xử lý một lần. Các danh sách và số lượng cần O(V + E) bộ nhớ.
Bạn nên dùng BFS hay DFS cho bài toán lập lịch khóa học?
Cả hai đều chạy trong thời gian O(V + E). Thuật toán Kahn, phiên bản tìm kiếm theo chiều rộng, không cần lo về độ sâu đệ quy và cho bạn thứ tự khóa học hợp lệ. Tìm kiếm theo chiều sâu với ba trạng thái là lựa chọn tự nhiên khi bạn cũng cần báo cáo chu trình, vì các khóa học trên ngăn xếp của nó tạo thành chu trình đó.
Thứ tự sắp xếp topo là gì?
Một thứ tự của các nút trong đồ thị có hướng sao cho mọi mũi tên đều hướng về phía trước; ở đây, đó là thứ tự mà mỗi môn học tiên quyết đứng trước môn học cần đến nó. Thứ tự này tồn tại khi và chỉ khi đồ thị không có chu trình, và thứ tự mà thuật toán Kahn lấy các môn học là một ví dụ. Course Schedule hỏi liệu có tồn tại thứ tự như vậy hay không.
Bài tập tương tự
Các bài tập dùng cùng ý tưởng. Giải hai hoặc ba bài là bạn sẽ nhớ được mẫu này.
def canFinish(numCourses, prerequisites):
# Viết code ở đâyKiểm thử #1
Kiểm thử #2
Đối số
numCourses = 4 prerequisites = [[1, 0], [2, 1], [3, 1]]
Đầu ra mong đợi
true