BFS — Breadth-First Search¶
Inherits from: the queue. Data structure: graph + queue। Thinking tweak: layer by layer expand করো — কোনো node-এ প্রথমবার পৌঁছানোই shortest path (যখন প্রতিটা edge-এর cost সমান)।
Inheritance-টা, খুলে বলা¶
BFS তোমার হাতে আগে থেকেই আছে। ../04-stack-and-queue/-এ শিখেছ, একটা queue জিনিসগুলো arrival order-এ process করে। BFS এর বেশি কিছুই না:
Start node queue-তে ঢোকে। তারপর চিরকাল: front pop করো, তার unseen neighbors enqueue করো। Queue first-in-first-out বলে, distance d-এর সবকিছু পুরোপুরি process হয় distance d+1-এর কিছু ছোঁয়ার আগেই। Tree-র level-order traversal (../07-trees/) আসলে এমন এক graph-এ BFS ছিল যার ঘটনাক্রমে কোনো cycle ছিল না — এখানে একমাত্র নতুন উপাদান হলো visited set, কারণ graph পেছনে loop করতে পারে।
যে একটা theorem তোমাকে defend করতে পারতেই হবে¶
Unweighted graph-এ, BFS কোনো node-কে প্রথমবার যখন discover করে, তখনই সে তার একটা shortest path পেয়ে গেছে।
কেন — সরল কথায়: BFS nodes discover করে strictly non-decreasing distance order-এ — আগে distance 0-এর সব, তারপর distance 1-এর সব, তারপর 2, এভাবে। তাই কোনো node-এর যদি BFS-এর পাওয়া পথের চেয়ে ছোট কোনো route থাকত, সেই ছোট route-এর endpoint আগের কোনো wave-এই discover হয়ে যেত। হয়নি। Contradiction। শেষ।
এজন্যই "minimum number of moves/steps/swaps/transformations" problem মানেই BFS problem, ব্যস — যখন প্রতিটা move-এর cost সমান।
Visited-set discipline¶
সবচেয়ে common BFS bug:
# WRONG: mark when popped
node = q.popleft()
if node in seen: # node may already sit in the queue 5 times
continue
seen.add(node)
# RIGHT: mark when pushed
for nxt in adj[node]:
if nxt not in seen:
seen.add(nxt) # claimed the moment it joins the queue
q.append(nxt)
Mark-on-pop-ও শেষমেশ terminate করে, কিন্তু queue একই node অনেকবার ধরে রাখতে পারে — এই অপচয় O(V+E)-কে আরও অনেক খারাপ কিছুতে নিয়ে যেতে পারে, আর "queue-তে একবারই আছি" ধরে নেওয়া যেকোনো logic-কে নষ্ট করে। অভ্যাসটা পাকা করো: seen.add থাকে q.append-এর ঠিক পাশে।
আরও দুটো discipline point:
collections.dequeব্যবহার করো;list.pop(0)pop প্রতি O(n) আর চুপিচুপি BFS-কে quadratic বানিয়ে দেয়।- Start node
seen-এ যায় loop-এর আগেই। এটা ভুললে কোনো cycle start-কে re-discover করতে পারে।
Levels ধরে distance — দুটো equivalent style¶
Style 1: node-এর সাথে distance store করো। "এক target-এর distance"-এর জন্য সবচেয়ে clean।
from collections import deque
def bfs_dist(adj, start):
dist = {start: 0}
q = deque([start])
while q:
node = q.popleft()
for nxt in adj[node]:
if nxt not in dist: # dist doubles as the visited set
dist[nxt] = dist[node] + 1
q.append(nxt)
return dist
Style 2: প্রতিটা outer iteration-এ পুরো একটা layer process করো। যখন level number-টাই answer ("কত minutes / rounds / waves") — তখন সবচেয়ে clean।
def bfs_levels(adj, start):
seen = {start}
frontier = [start]
level = 0
while frontier:
next_frontier = []
for node in frontier: # everything at distance `level`
for nxt in adj[node]:
if nxt not in seen:
seen.add(nxt)
next_frontier.append(nxt)
frontier = next_frontier
level += 1
return level - 1 # distance of the farthest node
দুটোই O(V + E)। যেটা problem-এর জন্য বেশি ভালো পড়া যায়, সেটাই নাও।
আসল path-টা reconstruct করতে (শুধু তার length না), discovery-র সময় parent[nxt] = node store করো, তারপর goal থেকে parents ধরে পেছনে হাঁটো — CSES Labyrinth-এর মতো maze problem-এ লাগে।
Multi-source BFS: একসাথে কয়েকটা পাথর ফেলো¶
Question-এর shape: "প্রতিটা cell-এর জন্য, nearest X-এর distance" বা "spread-টা সবকিছুতে পৌঁছাতে কতক্ষণ লাগে?" (rotting oranges, আগুন ছড়ানো, nearest exit)।
Trick-টা প্রায় অপমানজনক রকম সহজ: queue-টা শুরু করো ALL sources দিয়ে, distance 0-তে। ঢেউগুলো প্রতিটা source থেকে একসাথে ছড়ায়, আর যেখানে ঢেউ মেলে, প্রতিটা node রাখে যে ঢেউ তাকে আগে ছুঁয়েছে তার distance — automatically nearest source।
def multi_source_bfs(adj, sources):
dist = {s: 0 for s in sources}
q = deque(sources) # the only change: many starts
while q:
node = q.popleft()
for nxt in adj[node]:
if nxt not in dist:
dist[nxt] = dist[node] + 1
q.append(nxt)
return dist
Mental model: পুকুরে একটা পাথরের বদলে এক মুঠো ছুড়ে দিলে। একই ঢেউ, একই code, seeding-এর একটা extra লাইন। Classic uses: Rotting Oranges, 01 Matrix।
(Equivalent framing: একটা fake super-source বানাও, যেটা free edges দিয়ে সব real source-এর সাথে জোড়া, তারপর সেখান থেকে plain BFS চালাও।)
Grid as graph¶
বেশিরভাগ interview BFS grid-এ ঘটে, যেখানে graph টা implicit:
def grid_bfs(grid, start_r, start_c):
R, C = len(grid), len(grid[0])
dist = {(start_r, start_c): 0}
q = deque([(start_r, start_c)])
while q:
r, c = q.popleft()
for dr, dc in ((-1,0), (1,0), (0,-1), (0,1)):
nr, nc = r + dr, c + dc
if (0 <= nr < R and 0 <= nc < C # inside the grid
and grid[nr][nc] != "#" # not a wall
and (nr, nc) not in dist): # not yet discovered
dist[(nr, nc)] = dist[(r, c)] + 1
q.append((nr, nc))
return dist
পাকা করার মতো grid habits:
- Direction list-টা একবারই লেখো:
((-1,0),(1,0),(0,-1),(0,1)); চারটা diagonal যোগ করো শুধু problem বললে। - Index করার আগে bounds check — Python-এর negative index out-of-bounds read-কে crash না করিয়ে চুপিচুপি wrap করে দেয়।
- Grid বড় হলে booleans-এর একটা
visited2D list, tuples-এর set-এর চেয়ে দ্রুত; শেখার জন্য দুটোই ঠিক আছে। - কখনো কখনো grid-টাই graph না: Bus Routes-এ nodes হলো routes, stops না। Code করার আগে সবসময় জিজ্ঞেস করো, "আমার states কী?"
কখন BFS, কখন DFS¶
| You need | Use | Why |
|---|---|---|
| Shortest path / fewest moves (unweighted) | BFS | distance-order discovery is the whole theorem |
| "How many rounds until everything is reached" | BFS (multi-source) | rounds = levels |
| Just reachability / visit everything | either | both are O(V+E); pick the one you type faster |
| Connected components | either | same cost; DFS is often shorter to write |
| Cycle detection, ordering, structural facts | DFS | finish times and the recursion tree carry the info |
| Very deep graph, recursion limit a risk | BFS or iterative DFS | no call-stack depth problem |
| Memory worry on a bushy, shallow graph | DFS | BFS frontier can hold a whole (huge) level |
পকেটে রাখার এক-লাইনের summary: distance matter করলে BFS; structure matter করলে DFS; শুধু reachability হলে যেকোনোটা।
BFS-এর trigger words¶
"Shortest", "minimum number of steps/moves/operations", "fewest transformations", "level", "nearest", "spread/infect/rot minute by minute", "wave", "all cells' distance to the closest X"।
Representative problems¶
- Rotting Oranges — grid-এ multi-source BFS; answer হলো level count।
- Word Ladder — এক অক্ষর দূরের শব্দদের implicit graph-এ BFS।
- 01 Matrix — প্রতিটা zero থেকে multi-source BFS।
- CSES Labyrinth — grid BFS plus parent-pointer path reconstruction।
- Is Graph Bipartite? — layer by layer BFS coloring (even layers এক রঙ, odd layers অন্যটা)।
Proof সহ reference treatment: cp-algorithms BFS। পরের file: dfs.md — অন্য personality-টা। তারপর shortest-path.md, যেখানে BFS তার queue-টা heap দিয়ে বদলে বড় হয়ে Dijkstra হয়ে যায়।