Foundationপ্রথম নীতি থেকে
LEVEL 6লেসন ১৩/২৪কঠিন১ ঘণ্টা

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 দুটোই ঠিক করে।

এই লেসন শেষে আপনি পারবেন

  • Graph-কে adjacency list আর adjacency matrix — দুইভাবেই represent করে space (E+V বনাম V²), neighbor-iteration cost আর edge-query cost তুলনা করতে পারবেন
  • কোন অবস্থায় matrix জেতে — dense graph, ছোট V, repeated reachability query — সংখ্যা দিয়ে বিশ্লেষণ করে representation বাছতে পারবেন
  • BFS queue দিয়ে layer-by-layer চালিয়ে unweighted shortest path আর level structure বের করতে পারবেন, আর কেন dist-ই shortest — invariant দিয়ে বোঝাতে পারবেন
  • Iterative DFS চালিয়ে discovery/finish concept, back-edge-based cycle detection আর connected components বের করতে পারবেন
  • ১০⁵ node-এর random graph-এ representation choice বাস্তব runtime কতটা বদলায় — নিজের চালানো timing টেবিল দিয়ে দেখাতে পারবেন

আগে যা বোঝা থাকা দরকার

আগে এটা বুঝি

আজ পর্যন্ত আপনার 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 comp

BFS আর 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-6

BFS from 0

StepDequeueনতুন আবিষ্কৃতQueue
init–0 (d=0)[0]
১01(1), 3(1)[1, 3]
২12(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)
BFS = পানিতে ঢিল: wavefront সমান গতিতে সবদিকে; প্রতিটা স্তর এক distance layer। Unreachable অংশে wavefront কখনো পৌঁছায় না।
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 অসম্ভব।
DFS = সুতা গুটিয়ে maze: discovery (d) ঢোকার সময়, finish (f) ফেরার — interval-গুলো নিখুঁতভাবে nest করে। Nesting-ই ancestor-test, topo sort, Kosaraju-র ভিত্তি।
এক traversal শিখে কী খুলে যায়
  1. Traversalvisited + queue/stack — reachable সব vertex
  2. Shortest unweighted pathBFS dist array — পরের লেসনে weighted
  3. Components ও cyclesDFS comp label + back edge
  4. Ordering ও SCCfinish time + condensation — লেসন ১৬

নিজে চালিয়ে দেখুন

EXPERIMENT

Representation বদলালে কী হয় — stopwatch দিয়ে

macOS / Linux (Python 3.10+)· ১৫–২০ মিনিট
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।

নিজে বানান

BUILD IT

Graph class — BFS path + components + directed/undirected cycle, এক ফাইলে

Python · ●●○○○
  1. Graph class — directed flag, add_edge, neighbors
  2. bfs_shortest_path — dist + parent দিয়ে আসল পথ reconstruct
  3. dfs_components — iterative stack labeling
  4. has_cycle — undirected-এ parent-check, directed-এ তিন-রঙা DFS
  5. নিচের ৭টা 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 তিনটা, একটাও না। লেসন ১৬-র মূল বিষয় এই পার্থক্য।

বুঝেছেন কি না দেখুন

1

Adjacency list আর matrix — কোন operation-এ কে জেতে?

স্মরণ

List: neighbor iteration — Θ(deg) vs matrix Θ(V); sparse-এ ফাঁকা ঘর ঝাড়াই নেই। Matrix: edge existence O(1) vs Θ(deg); space জেতে list (Θ(V+E)) যখন E ভীষণ ছোট V²।

2

V=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 না।

3

BFS-এর 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।

4

Roadmap 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।

5

Directed 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-এর পেছনের বিজ্ঞান