010 — Course Schedule¶
Source¶
- Original source: Classic capstone interview problem (cycle detection / topological sort)
- Platform: LeetCode — https://leetcode.com/problems/course-schedule/
- Topic: graphs + hashing (adjacency)
- Difficulty: Medium
- Pattern: Cycle detection via topological sort (Kahn's BFS on indegree)
- Basic idea inherited from: এটা একটা cross-topic capstone — এখানে মেশে 09 graphs (prerequisite-গুলোকে একটা directed graph ধরে, cycle আছে কি না / topological order সম্ভব কি না দেখা) আর 05 hashing (adjacency list-টা একটা dictionary/
defaultdictদিয়ে O(1)-এ গড়া ও পড়া)। দুই tool জোড়া লাগলেই এই problem।
1. Problem in simple words¶
তোমাকে numCoursesটা কোর্স (0 থেকে numCourses-1) আর কিছু prerequisite জোড়া দেওয়া। প্রতিটা জোড়া [a, b]-র মানে: কোর্স a করার আগে কোর্স b করতেই হবে। প্রশ্ন: এমন কোনো ক্রম আছে কি, যাতে সব কোর্স শেষ করা যায়? (মানে কোনো চক্রাকার নির্ভরতা — "A-র আগে B, B-র আগে A" — নেই তো?) সম্ভব হলে True, নাহলে False।
numCourses = 2, prerequisites = [[1, 0]]
মানে: 1-এর আগে 0 -> 0 তারপর 1 -> সম্ভব -> True
numCourses = 2, prerequisites = [[1, 0], [0, 1]]
মানে: 1-এর আগে 0, আবার 0-এর আগে 1 -> চক্র! -> False
2. Which basic idea does this inherit from?¶
এই problem দুটো আগের chapter-এর tool জোড়া দেয়:
- 09 graphs থেকে: prerequisite-গুলোকে একটা directed graph ধরা (edge
b -> aমানে "b শেষ হলে তবে a-র দিকে যাওয়া যায়")। তখন "সব কোর্স করা সম্ভব?" = "graph-এ cycle নেই তো?" = "একটা topological order বের করা যায় কি?" - 05 hashing থেকে: graph-টা একটা adjacency list (
defaultdict(list)) দিয়ে রাখা — কোন node থেকে কোথায় edge যায়, তা O(1)-এ পড়া।
একা graph-চিন্তা দেয় cycle/topo framing; একা hashing দেয় adjacency-র দ্রুত গঠন। দুটো মিললে O(V+E)।
3. What is the hidden math or logic?¶
লুকানো logic: সব কোর্স করা সম্ভব ঠিক তখনই যখন directed graph-টা acyclic (DAG)। একটা DAG-এ সবসময় অন্তত একটা node থাকে যার কোনো অসম্পূর্ণ prerequisite নেই (indegree 0); সেটা শেষ করে সরিয়ে দিলে আবার নতুন indegree-0 node বেরোয়। সব node এভাবে সরাতে পারলে cycle নেই; কোনো এক পর্যায়ে আর indegree-0 না পেলে বাকিগুলো একটা cycle-এ আটকা — অসম্ভব।
4. Which data structure is needed?¶
- একটা adjacency list (
defaultdict(list), 05 hashing) — graph-এর edge রাখতে। - একটা indegree array — প্রতিটা node-এর কতগুলো অসম্পূর্ণ prerequisite।
- একটা queue (
deque) — Kahn's BFS-এ indegree-0 node ধরে রাখতে।
5. Why this data structure fits¶
Topological sort-এ বারবার দুটো কাজ লাগে: (1) "এই node থেকে কোথায় কোথায় edge যায়" দ্রুত জানা, (2) "এই মুহূর্তে কোন কোন node-এর indegree 0" ধরে রাখা। adjacency list (hashmap) প্রথমটা O(1)-এ দেয়; indegree array + queue দ্বিতীয়টা সহজ করে। এই combination-ই Kahn-এর algorithm-কে O(V+E)-এ চালায়। তাই graph (09) + hashing (05) এখানে নিখুঁত।
6. Real-life analogy¶
ভাবো তুমি জামাকাপড় পরছ — মোজার আগে জুতো পরা যায় না, শার্টের আগে কোট না। তুমি প্রথমে সেই জিনিসগুলো পরো যেগুলোর কোনো "আগে-শর্ত" নেই (অন্তর্বাস, মোজা)। একটা পরে ফেললে যেসব জিনিসের শর্ত পূরণ হলো (এবার জুতো পরা যায়), সেগুলো "তৈরি" তালিকায় ঢোকে। সব পরা শেষ হলে ভালো; কিন্তু যদি "A-র আগে B, B-র আগে A" এমন গিঁট থাকে, তুমি কখনোই শুরু করতে পারবে না — সেটাই cycle।
7. Visual explanation¶
numCourses=4, prereqs [[1,0],[2,0],[3,1],[3,2]] — Kahn's BFS:
edge (b->a): 0->1, 0->2, 1->3, 2->3
indegree: [0, 1, 1, 2] # node 0-এর কোনো শর্ত নেই
queue=[0] নাও 0; processed=1; 1,2-র indegree-- -> [0,0,0,2]; queue=[1,2]
queue=[1,2] নাও 1; processed=2; 3-র indegree-- -> [_,_,0,1]; queue=[2]
queue=[2] নাও 2; processed=3; 3-র indegree-- -> [_,_,_,0]; queue=[3]
queue=[3] নাও 3; processed=4; queue খালি
processed (4) == numCourses (4) -> cycle নেই -> True
8. Example input and output¶
Input : numCourses=2, prerequisites=[[1,0]] -> Output: True
Input : numCourses=2, prerequisites=[[1,0],[0,1]] -> Output: False (চক্র)
Edge case 1 (কোনো prereq নেই): numCourses=3, [] -> Output: True
Edge case 2 (একটা কোর্স): numCourses=1, [] -> Output: True
Edge case 3 (নিজের উপর নির্ভর): numCourses=1, [[0,0]] -> Output: False
9. Brute force thinking¶
প্রথম সরল চিন্তা: প্রতিটা কোর্স থেকে DFS করে দেখি কোনো পথ ঘুরে নিজের কাছে ফিরে আসে কি না (cycle খোঁজা)। প্রতিটা DFS-এ বর্তমান পথের node গুলো একটা "recursion stack"-এ রেখে, আবার সেই node-এ পৌঁছালে cycle।
for প্রতিটা course c:
if dfs(c) cycle পায়:
return False
return True
# dfs: c-কে "চলমান পথে" চিহ্নিত করো, প্রতিবেশীতে যাও;
# চলমান node আবার পেলে cycle; শেষ হলে চিহ্ন তুলে নাও
10. Why brute force is slow¶
সাবধানে visited/finished চিহ্ন না রাখলে একই subtree বারবার ঘুরে DFS O(V·(V+E)) বা আরও খারাপ হয়ে যায়। ঠিকভাবে memo করলে DFS-ও O(V+E)-এ নামে — কিন্তু তিন রঙের state (unvisited / in-progress / done) সামলানো bug-প্রবণ। Kahn's BFS (indegree) একই O(V+E) আরও সরল ও কম-ভুলের পথে দেয়, আর বাড়তি হিসেবে একটা valid order-ও দেয়।
11. Key observation¶
মূল observation: প্রশ্নটা আসলে "graph-এ cycle আছে কি?" — আর একটা DAG-এ সবসময় indegree-0 node দিয়ে শুরু করে সব node সরানো যায়। যদি সব node সরাতে পারি (processed == numCourses), cycle নেই। কোনো পর্যায়ে আর indegree-0 না পেলে, বাকিগুলো cycle-এ আটকা। এই এক চিন্তাই সমাধান।
12. Optimized intuition¶
Adjacency list (b -> a) আর indegree array বানাও। indegree-0 সব node queue-তে দাও (এদের আগে কোনো শর্ত নেই)। queue থেকে এক এক node নাও, "processed" গুনো, আর তার প্রতিবেশীদের indegree কমাও; কারো indegree 0 হলে queue-তে দাও। শেষে যদি processed == numCourses, তাহলে সব করা সম্ভব (True), নাহলে cycle (False)।
13. Thinking tweak¶
মোচড়: "কোন ক্রমে কোর্স করব খুঁজব" ভাবার বদলে ভাবো "শুধু জিজ্ঞেস করি — সব node কি indegree-0 দিয়ে এক এক করে সরানো যায়?" topological-feasibility একটা সহজ indegree-counting-এ গলে যায়; পুরো order বের করারও দরকার নেই, শুধু গুনলেই হয়।
14. Step-by-step dry run¶
Input numCourses=3, prereqs [[1,0],[2,1]] (edge 0->1, 1->2):
- adjacency:
{0:[1], 1:[2]}; indegree[0, 1, 1]; queue[0]; processed 0 - নাও
0: processed 1;1-র indegree → 0; queue[1] - নাও
1: processed 2;2-র indegree → 0; queue[2] - নাও
2: processed 3; queue খালি - processed (3) == numCourses (3) → return
True
15. Python solution¶
from collections import deque, defaultdict
def can_finish(num_courses, prerequisites):
adj = defaultdict(list) # b -> [a, ...]: b শেষ হলে a-রা এক ধাপ এগোয়
indegree = [0] * num_courses
for a, b in prerequisites: # a-র আগে b
adj[b].append(a)
indegree[a] += 1
queue = deque(c for c in range(num_courses) if indegree[c] == 0)
processed = 0
while queue:
node = queue.popleft()
processed += 1 # এই কোর্স "করা গেল"
for nxt in adj[node]:
indegree[nxt] -= 1 # একটা শর্ত পূরণ হলো
if indegree[nxt] == 0:
queue.append(nxt)
return processed == num_courses # সব করা গেলে cycle নেই
# ---- tests ----
assert can_finish(2, [[1, 0]]) is True
assert can_finish(2, [[1, 0], [0, 1]]) is False # সরাসরি চক্র
assert can_finish(3, []) is True # কোনো শর্ত নেই
assert can_finish(1, []) is True # একটা কোর্স
assert can_finish(1, [[0, 0]]) is False # নিজের উপর নির্ভর = চক্র
assert can_finish(4, [[1,0],[2,0],[3,1],[3,2]]) is True
assert can_finish(3, [[0,1],[1,2],[2,0]]) is False # ত্রিভুজ চক্র
print("সব test pass!")
16. Line-by-line code explanation¶
adj = defaultdict(list)— adjacency list, নতুন key আপনাআপনি খোলে (05 hashing)।for a, b in prerequisites—a-র আগেb, তাই edgeb -> aযোগ করি আরa-র indegree বাড়াই।queue = deque(... indegree[c] == 0)— যেসব কোর্সের কোনো শর্ত নেই, তারা প্রথমে তৈরি।processed += 1— queue থেকে নেওয়া মানে এই কোর্স সফলভাবে করা গেল, গুনে রাখি।for nxt in adj[node]— যাদের এই node শর্ত ছিল, তাদের indegree কমাও।if indegree[nxt] == 0: queue.append(nxt)— সব শর্ত পূরণ হলে সেই কোর্স এবার তৈরি।return processed == num_courses— সব কোর্স process হলে DAG (সম্ভব); কম হলে কিছু cycle-এ আটকা (অসম্ভব)।
17. Output walkthrough¶
numCourses=2, [[1,0]]-এ indegree [0,1], queue [0]; 0 নিয়ে processed 1, 1-র indegree 0 হয়ে queue-তে; 1 নিয়ে processed 2; 2 == 2 → True — assert pass। [[1,0],[0,1]]-এ দুটো node-ই indegree 1, queue শুরুতেই খালি, processed 0 ≠ 2 → False। ত্রিভুজ চক্র, self-loop, খালি-prereq case-ও সঠিক। শেষে print: সব test pass!।
18. Time complexity¶
O(V + E) — প্রতিটা node একবার queue-তে ঢোকে-বেরোয় (V), প্রতিটা edge একবার relax হয় (E)।
19. Space complexity¶
O(V + E) — adjacency list সব edge রাখে (E), indegree array আর queue O(V)।
20. Common mistakes¶
- edge-এর দিক উল্টে দেওয়া (
a -> bবনামb -> a) —[a,b]মানে "a-র আগে b", তাই edgeb -> a; উল্টালে cycle-যুক্তি ভেঙে যায়। - শেষে
processed == num_coursesচেক না করে শুধু queue খালি কি না দেখা — cycle-এ আটকা node গুলো কখনো queue-তে ঢোকেই না, তাই গোনাটাই আসল test। - self-loop (
[0,0]) ভুলে যাওয়া — এটা একটা cycle,Falseহওয়া উচিত; indegree-পদ্ধতি এটা আপনাআপনি ধরে। - disconnected node (কোনো prereq নেই) ভুলে যাওয়া — এদের indegree 0, শুরুতেই queue-তে দিতে হয়।
21. Which basic problem this inherits from¶
ভিত্তি: directed graph-এ cycle detection / topological sort (09 graphs, ../../09-graphs/topological-sort.md) + adjacency list-এর জন্য hashmap (05 hashing, ../../05-hashing/)। সাধারণ graph traversal-ই এখানে indegree-গণনার মোড়কে।
22. Similar harder problems¶
- Course Schedule II (শুধু সম্ভব কি না না, আসল order ফেরত দাও) — https://leetcode.com/problems/course-schedule-ii/
- Alien Dictionary (অক্ষরের topological order) — https://leetcode.com/problems/alien-dictionary/
- Minimum Height Trees (indegree-ছাঁটাইয়ের আত্মীয়) — https://leetcode.com/problems/minimum-height-trees/
- Parallel Courses (level-by-level topo) — https://leetcode.com/problems/parallel-courses/
23. Pattern learned¶
Topological sort / cycle detection (Kahn's BFS): prerequisite-কে directed graph ধরে, indegree-0 node দিয়ে শুরু করে সব node সরানো যায় কি না গুনে — "নির্ভরতা নিয়ে সব কাজ করা সম্ভব?"-ধরনের problem সমাধান। DFS-three-color দিয়েও একই কাজ হয়।
24. Final summary¶
ভবিষ্যতের আমাকে: "prerequisite / dependency / ordering সম্ভব কি না" শুনলেই topological sort (cycle detection) মনে করবে। মূল চাল — directed graph বানাও, indegree-0 থেকে BFS, শেষে processed == n হলে DAG। edge-এর দিক আর শেষের গোনা — এই দুটোতেই বেশি ভুল হয়, তাই সাবধান। order চাইলে Course Schedule II-তে শুধু processed-ক্রমটা জমিয়ে রাখলেই হয়।
মনে রাখার নিয়ম (legal): সব নিজের ভাষায় লেখা; problem statement copy করা হয়নি; official link শুধু attribution-এর জন্য; "asked by Google/Amazon" দাবি নেই — এটা একটা common interview pattern।