Graph Representation, BFS ও DFS — সব graph algorithm-এর ভিত
Graphs: Representation, BFS and DFS
একই graph রাখার দুই উপায়ের খরচ আলাদা, ঘোরার দুই মৌলিক কৌশল — BFS queue দিয়ে layer ধরে shortest path, DFS stack দিয়ে cycle ও component — দুটোই O(V+E), কিন্তু representation choice constant আর feasibility দুটোই ঠিক করে।
আগে এটা বুঝি
আজ পর্যন্ত আপনার data structure-গুলো বিনীত ছিল — array-র element- দের দুই পাশে একজন প্রতিবেশী, tree-তে একজন parent। বাস্তব দুনিয়া এমন না: ফেসবুক friendship, রাস্তার junction, ওয়েব link, package dependency — যেকোনো কারো সাথে যেকোনো কারো সম্পর্ক হতে পারে, hierarchy নেই, শৃঙ্খল নেই। এই সবচেয়ে সাধারণ সম্পর্কের structure-টাই graph: vertex-দের সেট, আর মাঝের সংযোগ edge।
graph নিয়ে দুইটা মৌলিক কাজ — রাখা আর ঘোরা। রাখার দুই উপায় (adjacency list বনাম matrix) মেমরি, গতি, cache behavior সব ঠিক করে দেয়। ঘোরার দুই কৌশল — BFS (প্রস্থে ছড়ানো) আর DFS (গভীরে ঢোকা) — এর ওপর দাঁড়িয়ে আছে shortest path, MST, topological sort, SCC, এমনকি garbage collector-ও।
শেষে আমরা ১০⁵ node-এর graph বানিয়ে মাপব: representation বদলালে একই algorithm-এর সময় দশগুণ-পঞ্চাশগুণ বদলায়। ইঙ্গিত: big-O শুরু, hardware-ই শেষ কথা।
মূল ধারণা
Graph মানে কী
G = (V, E) — vertex-দের সেট V, vertex-pair-দের সেট E। Undirected
graph-এ (u,v) আর (v,u) এক (friendship); directed-তে u→v মানেই
v→u না (follow, dependency)। Weighted graph edge-এ সংখ্যা বহন করে —
পরের লেসনের বিষয়। দুটো সংখ্যা সর্বত্র ফিরবে: V, E। Dense graph
মানে E ≈ V², sparse মানে E ≈ V — আর বাস্তবের বড় graph (social,
web, road) প্রায় সবই ভীষণ sparse। এই fact-টাই পরের অংশের সবচেয়ে
গুরুত্বপূর্ণ ইনপুট।
Adjacency list — প্রতি vertex-এ প্রতিবেশী-তালিকা
# edges: 0-1, 0-2, 1-3, 3-4
adj = [[1, 2], [0, 3], [0], [1, 4], [3]]Space Θ(V+E) (undirected edge দুই প্রান্তে stored, মোট entry 2E)।
u-এর প্রতিবেশী ঘোরা Θ(deg(u)) — যতটুকু দরকার ততটুকুই। Edge আছে কি
না জানতে তালিকা ঝাড়তে হয় Θ(deg(u)) (hash-set দিলে O(1), constant
খরচে)।
Adjacency matrix — V×V grid
mat = [
[0, 1, 1, 0, 0],
[1, 0, 0, 1, 0],
[1, 0, 0, 0, 0],
[0, 1, 0, 0, 1],
[0, 0, 0, 1, 0],
]Space Θ(V²) — edge কম-বেশি জড়িত না। Edge query O(1) indexing; প্রতিবেশী iterate করতে পুরো row ঝাড়তে হয় Θ(V)।
Matrix জেতে যখন: graph genuinely dense (E≈V² হলে list-এর
2E-ও grid-এর কাছে), V ছোট (flat contiguous row = prefetcher-এর
স্বপ্ন), বা workload-ই হাজারবার reachability/edge query। List জেতে
sparse graph + traversal-heavy work-এ — ফাঁকা ঘর ঝাড়ার কোনো কাজই হয় না।
BFS — queue দিয়ে layer by layer
Source থেকে দূরত্ব ০ = source; ১ = তার প্রতিবেশী; ২ = তাদের না-দেখা প্রতিবেশী… layer discipline enforce করে FIFO queue (stacks-queues-deques লেসনের structure):
from collections import deque
def bfs(adj, src):
dist = [-1] * len(adj)
dist[src] = 0
q = deque([src])
while q:
u = q.popleft() # পুরনো (কাছের) layer আগে
for v in adj[u]:
if dist[v] == -1:
dist[v] = dist[u] + 1
q.append(v)
return distকেন dist-ই shortest? Invariant: queue-তে শুধু dist d আর d+1-এর vertex (FIFO-র জোরে কখনো d+2 ঢোকে না)। u dequeue-এর সময় তার না-দেখা প্রতিবেশীর true distance কম হতে পারে না — হলে সে আগের কোনো layer-এই ধরা পড়ত। প্রতিটা vertex একবার enqueue ⇒ dist চূড়ান্ত।
DFS — stack দিয়ে গভীরে
একটা পথে যত দূর সম্ভব, dead-end-এ পিছিয়ে বাকি পথ। Recursion করে, কিন্তু explicit stack production-এর পছন্দ (কারণ hood section-এ):
def dfs_components(adj):
comp = [-1] * len(adj)
cid = 0
for s in range(len(adj)):
if comp[s] != -1:
continue
comp[s] = cid
stack = [s]
while stack:
u = stack.pop() # LIFO — সদ্য-আবিষ্কৃত পথে আরও গভীরে
for v in adj[u]:
if comp[v] == -1:
comp[v] = cid
stack.append(v)
cid += 1
return compBFS আর DFS-এর পার্থক্য literally queue নাকি stack — skeleton এক। Queue dist দেয়, DFS structure দেয়: component, cycle, ordering (লেসন ১৬)।
Complexity — দুটোই O(V+E)
List-এ: প্রতিটা vertex একবার visit (visited-marking ছাড়া একই vertex বারবার queue/stack-এ ফিরে infinite growth!), প্রতিটা undirected edge দুইবার স্ক্যান — Θ(V+E)। Matrix-এ: প্রতি visit-এ পুরো row — Θ(V²), edge যত কমই হোক। Representation আর algorithm — দুইটা আলাদা সিদ্ধান্ত।
ভেতরে কী ঘটছে
Visited ভুললে যা হয়
Path 0−1−2−3-এ visited ছাড়া BFS: 0→1 queue-তে, 1-এর প্রতিবেশী 0
আবার queue-তে, 0 আবার 1-কে… — 0 আর 1 চিরকাল একে অপরকে খাওয়ায়, 2-3-এ
কোনোদিন পৌঁছায় না। Θ(V)-র একটা array infinite loop কিনে নেয় — পৃথিবীর
সবচেয়ে সস্তা insurance।
Undirected edge memory-তে দুইবার
add_edge(u,v) লেখে adj[u]+=v এবং adj[v]+=u — E edge =
2E entry। পরিণতি দুটো: edge-count গুনলে sum(len(l)) = 2E (ভাগ
২ করুন); আর DFS-এ প্রতিবেশী visited দেখাই cycle না — parent-exception
লাগে ঠিক এই duplication-এর জন্য। Directed-তে duplication নেই, তাই
একই code দুই জগতে ভিন্ন আচরণ করে — শুরুতেই সিদ্ধান্ত নিন।
Pointer chasing — list-এর hidden খরচ
Python-এর list-of-lists-এ ভেতরের list-গুলো heap-এ ছড়ানো — প্রতিটা প্রতিবেশী-visit pointer dereference, প্রায়ই fresh cache line, potential cache miss প্রতি edge-এ। Matrix row উল্টো: contiguous, sequential prefetch। C/C++-এ CSR format দিয়ে list-data-ও contiguous করা যায় — cache-aware-algorithms লেসনের বিষয়।
Recursion depth — call stack ছোঁড়া খেলা না
Default Python recursion limit ~1000; 10⁵-node chain-এ recursive DFS 10⁵ গভীরে ঢুকতে চায় — crash। Explicit stack heap-এ থাকে, সীমা RAM-এর সীমা। Production traversal-এ iterative DFS-ই মান — call stack ছোট, heap বড়; OS module-এ দেখা frame-সীমার গল্প এখানেও।
Directed nuances
Directed-এ adj[u] = out-neighbors; in-degree চাইলে reversed graph
(adjT) লাগে। Reachability দিক-নির্ভর: u⇒v মানে v⇒u না। Undirected
component এক BFS-এ পাওয়া যায়; directed-এ mutually-reachable আলাদা
concept — SCC, লেসন ১৬।
Cycle detection — কোন edge back?
Undirected: প্রতিবেশী v visited এবং v ≠ parent ⇒ back edge ⇒ cycle
(parent skip, কারণ u→parent edge-টাই উল্টো দেখা যাচ্ছে)। Directed:
parent-check যথেষ্ট না — u→v, v→u আলাদা edge, ২-cycle বৈধ! সমাধান
তিন-রঙা: white (অদেখা), gray (খোলা পথে আছে), black (শেষ)। Gray vertex-
এ যাওয়া edge-ই cycle — খোলা পথের সদস্যে ফেরার মানে লুপ টানটান।
উদাহরণ
হাতে trace — ৭ node-এর graph
0 ─── 1 ─── 2
│ │
3 ─── 4 5 ─── 6
edges: 0-1, 0-3, 1-2, 1-4, 3-4, 5-6BFS from 0
| Step | Dequeue | নতুন আবিষ্কৃত | Queue |
|---|---|---|---|
| init | – | 0 (d=0) | [0] |
| ১ | 0 | 1(1), 3(1) | [1, 3] |
| ২ | 1 | 2(2), 4(2) | [3, 2, 4] |
| ৩ | 3 | – (4 seen) | [2, 4] |
| ৪ | 2 | – | [4] |
| ৫ | 4 | – | [] |
dist = [0,1,2,1,2,-1,-1] — vertex 5,6 unreachable: আলাদা component।
এক BFS-ই connectivity + distance দুই খবর দিল।
DFS from 0 (explicit stack)
[0] pop 0 → push 1,3
[1,3] pop 3 → push 4
[1,4] pop 4 → (1,3 seen)
[1] pop 1 → push 2
[2] pop 2 → done — component 0 = {0,1,2,3,4}Loop s=5-এ নতুন component id ১: {5,6}। comp=[0,0,0,0,0,1,1]।
Cycle
Vertex 4-এ প্রতিবেশী 1: visited, parent না (4-এ এসেছি 3 থেকে) — back
edge। 1−4 tree-path 1→3→4-এর সাথে মিলে cycle: 1−3−4−1। Undirected-
এ cycle ⇔ DFS-এ non-parent visited প্রতিবেশী — পাওয়া গেছে, তাই cycle
আছে।
ছবিতে
dist 0 : (0)
/ \
dist 1 : (1) (3)
/ \ /
dist 2 : (2) (4)
unreachable: (5)—(6)0 d=1 ─┬─ 1 d=2 ─┬─ 2 d=3 f=4
│ └─ 4 d=5 ── 3 d=6 f=7
└──────────────────────┘
parenthesis: (0 (1 (2) (4 (3) )) ) f: 0=8
nest করা interval ⇒ ancestor; আধা-overlap অসম্ভব।- Traversalvisited + queue/stack — reachable সব vertex
- Shortest unweighted pathBFS dist array — পরের লেসনে weighted
- Components ও cyclesDFS comp label + back edge
- Ordering ও SCCfinish time + condensation — লেসন ১৬
নিজে চালিয়ে দেখুন
Representation বদলালে কী হয় — stopwatch দিয়ে
import random, time
from collections import deque
random.seed(42)
# ---------- অংশ ১: বড় sparse graph, list representation ----------
N, M = 100_000, 1_000_000
print(f"graph: {N:,} nodes, {M:,} edges")
edges = set()
while len(edges) < M:
u, v = random.randrange(N), random.randrange(N)
if u != v:
edges.add((min(u, v), max(u, v)))
adj = [[] for _ in range(N)]
for u, v in edges:
adj[u].append(v); adj[v].append(u)
def bfs_list(src):
dist = [-1] * N; dist[src] = 0; q = deque([src])
while q:
u = q.popleft(); du = dist[u]
for v in adj[u]:
if dist[v] == -1:
dist[v] = du + 1; q.append(v)
return dist
def dfs_count(src):
seen = bytearray(N); seen[src] = 1; c = 1; st = [src]
while st:
u = st.pop()
for v in adj[u]:
if not seen[v]:
seen[v] = 1; st.append(v); c += 1
return c
t0 = time.perf_counter(); dist = bfs_list(0); t_bfs = time.perf_counter() - t0
t0 = time.perf_counter(); c0 = dfs_count(0); t_dfs = time.perf_counter() - t0
print(f"BFS list: {t_bfs*1000:.1f} ms | reachable {sum(d!=-1 for d in dist):,}, max dist {max(dist)}")
print(f"DFS list: {t_dfs*1000:.1f} ms | component of 0: {c0:,}")
print(f"matrix would need {N*N:,} bytes (~{N*N/2**30:.0f} GiB) — বাসাবো না")
# ---------- অংশ ২: ছোট sparse graph-এ list vs matrix ----------
NS, MS = 5_000, 15_000
se = set()
while len(se) < MS:
u, v = random.randrange(NS), random.randrange(NS)
if u != v:
se.add((min(u, v), max(u, v)))
adjs = [[] for _ in range(NS)]
for u, v in se:
adjs[u].append(v); adjs[v].append(u)
mat = bytearray(NS * NS)
for u, v in se:
mat[u*NS+v] = 1; mat[v*NS+u] = 1
def bfs_m(src):
dist = [-1]*NS; dist[src] = 0; q = deque([src])
while q:
u = q.popleft(); row = mat[u*NS:(u+1)*NS]; du = dist[u]
for v in range(NS):
if row[v] and dist[v] == -1:
dist[v] = du + 1; q.append(v)
return dist
d_small_list = [-1]*NS
t0 = time.perf_counter()
q = deque([0]); d_small_list[0] = 0
while q:
u = q.popleft(); du = d_small_list[u]
for v in adjs[u]:
if d_small_list[v] == -1:
d_small_list[v] = du + 1; q.append(v)
tL = time.perf_counter() - t0
t0 = time.perf_counter(); bfs_m(0); tM = time.perf_counter() - t0
print(f"\nsparse ({NS} nodes, {MS} edges) BFS:")
print(f" adjacency list : {tL*1000:8.1f} ms")
print(f" adjacency matrix: {tM*1000:8.1f} ms ({tM/tL:.0f}x ধীর)")
# ---------- অংশ ৩: edge-existence query — matrix-র বৈধ জয় ----------
qs = [(random.randrange(NS), random.randrange(NS)) for _ in range(1_000_000)]
adj_set = [set(x) for x in adjs]
t0 = time.perf_counter()
hm = sum(1 for u, v in qs if mat[u*NS+v]); tMq = time.perf_counter() - t0
t0 = time.perf_counter()
hl = sum(1 for u, v in qs if v in adj_set[u]); tLq = time.perf_counter() - t0
assert hm == hl
print(f"১০ লাখ 'edge আছে?' query: matrix {tMq*1000:.0f} ms | hash-set {tLq*1000:.0f} ms")
# ---------- অংশ ৪: ছয়-ডিগ্রি খেলা ----------
hops = []
for s, t in [(random.randrange(N), random.randrange(N)) for _ in range(500)]:
seen = bytearray(N); seen[s] = 1; q = deque([s]); d = 0; ans = 99
while q and ans == 99:
d += 1
for _ in range(len(q)):
u = q.popleft()
for w in adj[u]:
if w == t:
ans = d; break
if not seen[w]:
seen[w] = 1; q.append(w)
if ans != 99:
break
if s != t and ans != 99:
hops.append(ans)
print(f"ছয়-ডিগ্রি খেলা: {len(hops)} pair reachable, "
f"গড় {sum(hops)/len(hops):.2f} hops — সবই ৬-এর মধ্যে")সাধারণ ফলাফলের pattern (laptop; সংখ্যা মেশিনভেদে বদলাবে):
graph: 100,000 nodes, 1,000,000 edges
BFS list: 205.0 ms | reachable 99,997, max dist 4
DFS list: 190.0 ms | component of 0: 99,997
matrix would need 10,000,000,000 bytes (~9 GiB) — বাসাবো না
sparse (5000 nodes, 15000 edges) BFS:
adjacency list : 3.0 ms
adjacency matrix: 1150.0 ms (383x ধীর)
১০ লাখ 'edge আছে?' query:
matrix O(1)/query : 285.0 ms
hash-set list : 505.0 ms
random pair: গড় দূরত্ব 2.31 hops (500 pair reachable, সবই ৬-এর মধ্যে)পড়ার নিয়ম: (১) complexity এক, wall-clock ৩৮০× আলাদা — sparse-এ matrix-BFS প্রতি vertex-এ পুরো row (৫০০০ ঘর) ঝাড়ে, list ঠিক ৩০ হাজার edge ছোঁয়। (২) তবু query-heavy workload-এ flat bytearray-র O(1) lookup hash-set-কে হারায় — matrix-র বৈধ জায়গা dense/query-heavy। (৩) ৯ GiB মানে feasibility নিজেই constraint। (৪) গড় degree ২০-র random graph ৪ layer-এ পুরো giant component ঢেকে ফেলে — Milgram-এর ৬-ডিগ্রি আসে বাস্তব social graph-এর ছোট degree + clustering থেকে; expansion-ই distance ঠিক করে।
একই BFS/DFS, একই O(V+E) — representation বদলালে বাস্তব সময় শত-গুণ বদলায়, আর ১০⁵ node-এ matrix মেমরিতেই অসম্ভব; তবু edge-query workload-এ matrix জেতে — choice কঠোর context-dependent।
নিজে বানান
Graph class — BFS path + components + directed/undirected cycle, এক ফাইলে
- Graph class — directed flag, add_edge, neighbors
- bfs_shortest_path — dist + parent দিয়ে আসল পথ reconstruct
- dfs_components — iterative stack labeling
- has_cycle — undirected-এ parent-check, directed-এ তিন-রঙা DFS
- নিচের ৭টা edge case-এ assert চালিয়ে প্রমাণ
পরের লেসনগুলো এই toolkit import করে চলবে — ~৮৫ লাইনে স্বয়ংসম্পূর্ণ।
from collections import deque
class Graph:
def __init__(self, n, directed=False):
self.n = n
self.directed = directed
self.adj = [[] for _ in range(n)]
def add_edge(self, u, v):
self.adj[u].append(v)
if not self.directed:
self.adj[v].append(u)
def bfs_shortest_path(self, src, dst):
"""unweighted shortest path (node list) বা None"""
if src == dst:
return [src]
dist = [-1] * self.n
parent = [-1] * self.n
dist[src] = 0
q = deque([src])
while q:
u = q.popleft()
for v in self.adj[u]:
if dist[v] == -1:
dist[v] = dist[u] + 1
parent[v] = u
if v == dst: # প্রথম ছোঁয়াই shortest
path = [dst]
while path[-1] != src:
path.append(parent[path[-1]])
return path[::-1]
q.append(v)
return None
def dfs_components(self):
comp = [-1] * self.n
cid = 0
for s in range(self.n):
if comp[s] != -1:
continue
comp[s] = cid
stack = [s]
while stack:
u = stack.pop()
for v in self.adj[u]:
if comp[v] == -1:
comp[v] = cid
stack.append(v)
cid += 1
return comp
def has_cycle(self):
if not self.directed:
# DSU trick (lesson 12 preview): edge-এর দুই প্রান্ত
# একই component-এ থাকলেই সেটা closing edge — cycle।
p = list(range(self.n))
def find(x):
while p[x] != x:
p[x] = p[p[x]]
x = p[x]
return x
for u in range(self.n):
for v in self.adj[u]:
if u < v:
ru, rv = find(u), find(v)
if ru == rv:
return True
p[ru] = rv
return False
WHITE, GRAY, BLACK = 0, 1, 2
color = [WHITE] * self.n
for s in range(self.n):
if color[s] != WHITE:
continue
color[s] = GRAY
stack = [(s, iter(self.adj[s]))]
while stack:
u, it = stack[-1]
advanced = False
for v in it:
if color[v] == GRAY:
return True # খোলা পথে ফেরত → cycle
if color[v] == WHITE:
color[v] = GRAY
stack.append((v, iter(self.adj[v])))
advanced = True
break
if not advanced:
color[u] = BLACK
stack.pop()
return False
if __name__ == '__main__':
g = Graph(6)
for u, v in [(0,1),(1,2),(2,3),(3,4),(4,5)]:
g.add_edge(u, v)
assert g.bfs_shortest_path(0, 5) == [0,1,2,3,4,5]
assert g.has_cycle() is False
assert len(set(g.dfs_components())) == 1
g2 = Graph(6) # 5 unreachable
for u, v in [(0,1),(1,2),(2,3),(3,4)]:
g2.add_edge(u, v)
assert g2.bfs_shortest_path(0, 5) is None
assert len(set(g2.dfs_components())) == 2
tri = Graph(3)
for u, v in [(0,1),(1,2),(2,0)]:
tri.add_edge(u, v)
assert tri.has_cycle() is True
dc = Graph(4, directed=True) # directed chain: cycle নেই
for u, v in [(0,1),(1,2),(2,3)]:
dc.add_edge(u, v)
assert dc.has_cycle() is False
two = Graph(2, directed=True) # ২-cycle — parent-check-এ ধরা পড়ে না
two.add_edge(0, 1); two.add_edge(1, 0)
assert two.has_cycle() is True
mix = Graph(7) # triangle + edge + 2 isolated
for u, v in [(0,1),(1,2),(2,0),(3,4)]:
mix.add_edge(u, v)
assert len(set(mix.dfs_components())) == 4
assert mix.has_cycle() is True
assert Graph(0).dfs_components() == []
assert Graph(1).has_cycle() is False
print('সব টেস্ট পাস — ৭/৭')নিজে বাড়ান: (১) bfs_levels(src) — layer-group ফেরত দেয়,
degrees-of-separation UI-র ভিত্তি; (২) directed has_cycle-কে cycle
path ফেরত দিতে শেখান (gray-stack snapshot) — লেসন ১৬-র warm-up;
(৩) CSR representation যোগ করে Python object-overhead কমে কত লাভ —
মেপুন।
বাস্তব সিস্টেমে
Social network। Facebook 2016-এ ১.৫৯ বিলিয়ন user-এ গড় দূরত্ব measure করেছিল: 3.57 — distributed-cluster BFS দিয়ে। LinkedIn-এর X-th-degree badge-ও এই dist array।
Web crawler। Googlebot মূলত BFS: seed URL queue, fetch, links re-enqueue — dedup visited set ছাড়া ইন্টারনেটের loop-এ আটকায়। Politeness delay priority queue upgrade করে, skeleton BFS-ই।
Garbage collection। mark-and-sweep-এর mark phase roots থেকে reachability — object-graph DFS; tri-color marking ঠিক আমাদের directed cycle-detection-এর রঙ।
Maze ও game AI। Grid maze = graph; shortest escape = BFS। Pac-Man ghost AI, chess move exploration — DFS-এর deep-path thinking + pruning।
Compiler/linker। Dead-code elimination: entry থেকে call-graph DFS,
অপৌঁছাদর বাদ; -ffunction-sections --gc-sections এই traversal।
যে ভুলগুলো সবাই করে
“BFS সবসময় shortest path দেয়।”
শুধু unweighted (বা equal-weight) graph-এ। Weighted-তে BFS edge-সংখ্যা minimize করে খরচ না — ১টা ১০০-km road আর ৩টা ১-km lane-এ BFS ৩-lane-কে ছোট বলবে। Weighted-এ Dijkstra — পরের লেসন।
“DFS-এর জন্য recursion-ই স্বাভাবিক ও সেরা।”
পড়তে সুন্দর, কিন্তু call stack সীমিত — Python-এ ~1000 frame; 10⁵-chain recursive DFS-এ crash, iterative-এ নির্বিঘ্ন। Production traversal-এ explicit stack-ই first choice।
“Adjacency matrix সহজ, তাই সেটাই ভালো।”
ছোট/dense graph-এ (V ≤ ~১০০০) matrix চমৎকার — flat memory, O(1) query। Sparse-এ space Θ(V²) আর traversal Θ(V²): V=10⁵-এ 10 GB + 10¹⁰ ঘর-ঝাড়া। Density আর query-pattern সিদ্ধান্ত করে, সরলতা না।
“দুটোই O(V+E), তাই সময় একই হবে।”
Big-O growth rate বলে, wall-clock না — আজকের experiment-এ ৩৮০× গ্যাপ। Complexity class দিয়ে algorithm বাছুন, measurement দিয়ে verify করুন।
“Connected আর strongly connected component একই।”
Connected undirected-র — দিক না-মেনে পৌঁছানো। Strongly connected directed-র
— উভয় দিকে পথ। Path digraph a→b→c weakly connected কিন্তু SCC তিনটা,
একটাও না। লেসন ১৬-র মূল বিষয় এই পার্থক্য।
বুঝেছেন কি না দেখুন
1Adjacency list আর matrix — কোন operation-এ কে জেতে?
স্মরণ
List: neighbor iteration — Θ(deg) vs matrix Θ(V); sparse-এ ফাঁকা ঘর ঝাড়াই নেই। Matrix: edge existence O(1) vs Θ(deg); space জেতে list (Θ(V+E)) যখন E ভীষণ ছোট V²।
2V=2000, E=6000 — দুই representation-এর stored-entry সংখ্যা? Matrix
বাঞ্ছনীয়?
প্রয়োগ
List: 2E = 12,000 entry। Matrix: V² = 4,000,000 ঘর — density 2E/V² = 0.003, ৯৯.৭% ফাঁকা। Traversal-এ ~৩০০× বাড়তি কাজ, মেমরি ~৪MB vs ~১০০KB — matrix না।
3BFS-এর queue-dist invariant ভাঙার উপায় কী, আর সেটাই কি DFS?
যুক্তি
Invariant FIFO discipline থেকে আসে — append সবসময় dequeue-এর dist+1, তাই dist non-decreasing ক্রমে pop হয়; ভাঙে না। Stack দিলেই ভাঙে — নতুন (দূরের) vertex পুরনোর আগে process হয়, distance-order চিরতরে যায়। অর্থাৎ shortest-path গ্যারান্টির মূল সম্পদ queue-র FIFO property।
4Roadmap app: X থেকে Y যাওয়া যায় কি না — প্রতি সেকেন্ডে ১০ হাজার yes/no
query। Design?
ডিজাইন
Query-pattern-ই রাজা: traversal একবার, query লাখবার। একবার DFS-এ component label করুন (Θ(V+E)), প্রতিটা query O(1): comp[X]==comp[Y]। Matrix লাগবেই না — reachability-কে component-id-তে নামানোই precompute-once pattern। Dynamic road-closure হলে union-find (lesson 12) incremental merge।
5Directed cycle detection-এ parent-check যথেষ্ট কেন না, তিন-রঙা কীভাবে
ধরে?
স্মরণ
Undirected-এ parent-edge-টাই উল্টো দেখা যায় (2E storage) — skip করলে বাকি visited প্রতিবেশী = সত্যিকারের অন্য পথ। Directed-তে u→p আর p→u স্বাধীন edge — ২-cycle বৈধ, skip করলে মিস। Gray = খোলা পথে উপস্থিত; খোলা পথের vertex-এ ফেরার edge-ই definition-অনুযায়ী directed cycle। Black-এ যাওয়া cross/forward edge cycle না।
এরপর কী
এরপর কী
Graph রাখলাম-ঘুরলাম, কিন্তু edge weightless ছিল। বাস্তবে রাস্তার দৈর্ঘ্য, packet delay, exchange rate — সব ভিন্ন। পরের লেসনে weighted দুনিয়া: Dijkstra কেন negative edge-এ ভাঙে (step-by-step counterexample), Bellman-Ford কীভাবে negative cycle শিকার করে arbitrage-খুঁজে, Floyd-Warshall DP-তে all-pairs, আর A* কীভাবে heuristic দিয়ে Dijkstra-কে goal-মুখী করে maze-এ অর্ধেক exploration বাঁচায়। আজকের Graph class আর dist-array intuition সরাসরি কাজে লাগবে।
আরও পড়ুন
- Introduction to Algorithms (CLRS), Chapter 22 — Elementary Graph Algorithms — Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, Clifford Stein · BFS/DFS-এর canonical analysis — O(V+E)-র প্রমাণসহ representation trade-off
- Algorithms, 4th Edition, Sections 4.1–4.2 — Robert Sedgewick, Kevin Wayne · Graph API ডিজাইন আর application-দর্শন — build section-এর চেতনা
- "An Experimental Study of the Small World Problem" (1969) — Jeffrey Travers, Stanley Milgram · ছয়-ডিগ্রি পরীক্ষণের মূল গবেষণা — six-degree game-এর পেছনের বিজ্ঞান