Graph Theory — সংযোগের গণিত
Graph Theory
Vertex, edge, path, tree, connectivity — যে একটামাত্র কাঠামো দিয়ে network, dependency, compiler-এর control flow, filesystem আর distributed system সবই মডেল করা হয়।
আগে এটা বুঝি
এই ছয়টা জিনিসের মধ্যে কী মিল?
- ইন্টারনেটের router-গুলো
- আপনার
package.json-এর dependency - একটা function-এর ভেতরের
if/whilestructure - Filesystem-এর directory আর symlink
- Git-এর commit history
- Kubernetes-এ pod থেকে service-এ traffic
সবগুলোই graph — কিছু বস্তু, আর তাদের মধ্যে কিছু সংযোগ।
Graph theory-র শক্তি এখানেই: একটামাত্র বিমূর্ত কাঠামো, আর তার উপর প্রমাণিত theorem-গুলো সব প্রয়োগক্ষেত্রে একসাথে খাটে।
আপনি যখন শিখবেন “n vertex-এর tree-তে ঠিক n−1 edge”, তখন
সেটা একসাথে বলে দেবে: একটা spanning tree-তে কত link লাগবে,
একটা dependency tree-তে কত সম্পর্ক, একটা filesystem-এ কত
directory entry।
Relation-এর লেসনে আমরা graph-এর স্বাদ পেয়েছি। এবার পুরোটা।
মূল ধারণা
সংজ্ঞা ও পরিভাষা
একটা graph G = (V, E) — V হলো vertex-এর set,
E হলো edge-এর set।
| ধরন | Edge কী | উদাহরণ |
|---|---|---|
| Undirected | {u,v} — অসজ্জিত জোড়া | বন্ধুত্ব, রাস্তা |
| Directed (digraph) | (u,v) — সজ্জিত জোড়া | Twitter follow, dependency |
| Weighted | edge-এ একটা সংখ্যা | দূরত্ব, latency, খরচ |
| Multigraph | একাধিক edge একই জোড়ায় | দুই শহরের মধ্যে দুইটা রাস্তা |
| Simple | self-loop নেই, multi-edge নেই | সাধারণ ধরে নেওয়া হয় |
Degree — একটা vertex-এ কতগুলো edge লাগে।
Directed graph-এ দুইটা আলাদা: in-degree আর out-degree।
Handshake lemma
প্রতিটা edge দুইটা vertex-এর degree-তে ১ করে যোগ করে। তাই:
একটা তাৎক্ষণিক ফল: বিজোড় degree-র vertex সংখ্যা সবসময় জোড়।
কারণ যোগফল জোড় (2|E|), আর জোড় degree-গুলো যোগ করলে জোড় থাকে —
তাই বিজোড়গুলোর সংখ্যাও জোড় হতে হবে।
বাস্তব প্রয়োগ: একটা পার্টিতে যতজন মানুষ বিজোড় সংখ্যক লোকের সাথে হাত মিলিয়েছে, সেই সংখ্যাটা সবসময় জোড়।
CS-এ: একটা network topology-তে বিজোড় সংখ্যক link-ওয়ালা node-এর সংখ্যা জোড় হতেই হবে। Configuration-এ সেটা না মিললে আপনার data-তে ভুল আছে — একটা সস্তা sanity check।
Path, cycle, connectivity
| শব্দ | মানে |
|---|---|
| Walk | edge ধরে হাঁটা, পুনরাবৃত্তি অনুমোদিত |
| Path | vertex পুনরাবৃত্তি নেই এমন walk |
| Cycle | শুরু ও শেষ একই vertex, বাকি সব আলাদা |
| Connected | যেকোনো দুই vertex-এর মধ্যে path আছে |
| Component | সর্বোচ্চ connected উপ-graph |
Directed graph-এ দুই রকম connectivity:
- Strongly connected — যেকোনো
uথেকেvএবংvথেকেu - Weakly connected — দিক উপেক্ষা করলে connected
Connected দুইটা component Strongly connected
a ─── b a ─── b d a ──→ b
│ │ │ │ ↑ │
c ─── d c e └── c ←┘Tree — সবচেয়ে গুরুত্বপূর্ণ বিশেষ ক্ষেত্র
একটা tree হলো connected acyclic undirected graph।
সমতুল্য সংজ্ঞা — এই পাঁচটা একই জিনিস বলে:
n vertex-এর একটা graph G-এর জন্য, নিচের যেকোনো একটা সত্য
হলে বাকি সবগুলোও সত্য:
Gconnected এবং acyclicGconnected এবং ঠিকn−1edge আছেGacyclic এবং ঠিকn−1edge আছে- যেকোনো দুই vertex-এর মধ্যে ঠিক একটা path আছে
Gconnected, কিন্তু যেকোনো edge মুছলেই disconnect হয়ে যায়
সংজ্ঞা ৪ সবচেয়ে বেশি কাজে লাগে — “ঠিক একটা path” মানে কোনো অস্পষ্টতা নেই, কোনো redundancy নেই।
Tree-র edge সংখ্যা প্রমাণ
দাবি:
nvertex-এর একটা tree-তে ঠিকn−1edge।
প্রমাণ (n-এর উপর induction).
Base (n = 1): একটা vertex, শূন্য edge। 1 − 1 = 0 ✓
Step: ধরি n vertex-এর সব tree-তে n−1 edge।
n+1 vertex-এর একটা tree T নিন। প্রথমে দেখাই যে T-তে একটা
leaf (degree-১ vertex) আছে:
যদি সব vertex-এর degree ≥ 2 হতো, তাহলে একটা vertex থেকে হাঁটা
শুরু করে কখনো আটকে যেতাম না (প্রতিটা vertex-এ ঢোকার পর বেরোনোর
আরেকটা edge থাকত)। Vertex সংখ্যা সসীম, তাই কোনো vertex-এ দুইবার
পৌঁছাতেই হবে — অর্থাৎ cycle। কিন্তু tree acyclic। বিরোধ।
তাই একটা leaf v আছে। v আর তার একমাত্র edge মুছে ফেলুন।
যা থাকে সেটা n vertex-এর একটা tree (connected থাকে কারণ v
কোনো path-এর মাঝে ছিল না; acyclic থাকে কারণ edge মুছলে cycle
তৈরি হয় না)।
IH অনুযায়ী তাতে n−1 edge। যেটা মুছেছিলাম সেটা ফেরত যোগ করলে
n edge — অর্থাৎ (n+1) − 1 ✓ ∎
Bipartite graph
Vertex-দের দুইটা set-এ ভাগ করা যায় যেন প্রতিটা edge এক set থেকে অন্য set-এ যায় (একই set-এর ভেতরে কোনো edge নেই)।
Theorem: একটা graph bipartite যদি এবং কেবল যদি তাতে কোনো বিজোড় দৈর্ঘ্যের cycle নেই।
যাচাই করা সহজ — BFS দিয়ে ২-রঙে রাঙান:
from collections import deque
def is_bipartite(adj):
color = {}
for start in adj:
if start in color: continue
color[start] = 0
q = deque([start])
while q:
u = q.popleft()
for v in adj[u]:
if v not in color:
color[v] = 1 - color[u]
q.append(v)
elif color[v] == color[u]:
return False, None # বিজোড় cycle পাওয়া গেল
return True, colorCS-এ কোথায়:
- Matching — job ↔ worker, student ↔ course, ad ↔ slot
- Scheduling — task ↔ time slot
- Recommendation — user ↔ item (collaborative filtering)
- Conflict detection — দুই দলে ভাগ করা যায় কি না
Graph coloring
প্রতিটা vertex-এ একটা রঙ দিন যেন কোনো edge-এর দুই প্রান্তে
একই রঙ না থাকে। সর্বনিম্ন প্রয়োজনীয় রঙের সংখ্যাকে বলে
chromatic number χ(G)।
| Graph | χ |
|---|---|
| Tree (২+ vertex) | 2 |
| Bipartite | 2 |
| Cycle (জোড় দৈর্ঘ্য) | 2 |
| Cycle (বিজোড় দৈর্ঘ্য) | 3 |
Complete graph Kₙ | n |
| Planar graph | ≤ 4 (four color theorem) |
χ(G) বের করা NP-complete — এমনকি χ ≤ 3 কি না সেটাও।
তবু এটা প্রতিদিন ব্যবহার হয়:
আরো প্রয়োগ: exam scheduling (একই সময়ে conflicting exam নয়), frequency assignment (পাশাপাশি tower-এ একই frequency নয়), Sudoku (৯-coloring সমস্যা)।
ভেতরে কী ঘটছে
Representation — trade-off টা বাস্তব
দুইটা প্রধান উপায়:
Adjacency matrix
n × n boolean (বা weight) matrix।
M = [[0]*n for _ in range(n)]
M[u][v] = 1Adjacency list
প্রতিটা vertex-এর প্রতিবেশীদের তালিকা।
adj = {u: [v1, v2, ...] for u in vertices}| Operation | Matrix | List |
|---|---|---|
has_edge(u,v) | O(1) | O(deg u) |
| প্রতিবেশী ঘোরা | O(n) | O(deg u) |
| Edge যোগ | O(1) | O(1) |
| Edge মোছা | O(1) | O(deg u) |
| Memory | O(n²) | O(n + m) |
| BFS/DFS | O(n²) | O(n + m) |
সিদ্ধান্তের নিয়ম:
- Sparse (
m ≈ n, বেশিরভাগ বাস্তব graph) → list - Dense (
m ≈ n²) → matrix - Matrix operation দরকার (reachability, path counting) → matrix
সংখ্যা দিয়ে দেখুন: একটা social network-এ ১০ লক্ষ user, গড়ে ২০০ বন্ধু।
| Memory | |
|---|---|
| Matrix (bit) | 10¹² bit = ১২৫ GB |
| List (৮-byte id) | 2 × 10⁸ × 8 = ১.৬ GB |
৭৮ গুণ পার্থক্য — আর matrix-টা RAM-এ আঁটবেই না।
Traversal — BFS আর DFS
দুইটাই O(V + E), কিন্তু ভিন্ন কাজে লাগে।
from collections import deque
def bfs(adj, start):
"""স্তরে স্তরে — unweighted shortest path দেয়"""
dist = {start: 0}
parent = {start: None}
q = deque([start])
while q:
u = q.popleft()
for v in adj.get(u, ()):
if v not in dist:
dist[v] = dist[u] + 1
parent[v] = u
q.append(v)
return dist, parent
def dfs(adj, start):
"""যত গভীরে যায় — cycle, topological sort, component"""
seen, order, stack = set(), [], [start]
while stack:
u = stack.pop()
if u in seen: continue
seen.add(u); order.append(u)
for v in reversed(adj.get(u, ())):
if v not in seen:
stack.append(v)
return orderপার্থক্যটা শুধু data structure-এ: queue বনাম stack। এক লাইন বদলালে সম্পূর্ণ ভিন্ন আচরণ।
| BFS | DFS | |
|---|---|---|
| Data structure | Queue | Stack (বা recursion) |
| Memory | O(width) | O(depth) |
| Shortest path (unweighted) | ✓ | ✗ |
| Cycle detection | সম্ভব | স্বাভাবিক |
| Topological sort | Kahn-এ | ✓ |
| Connected component | ✓ | ✓ |
| Bipartite check | ✓ | ✓ |
- BFS on a graphগাণিতিক algorithm
- ping / traceroutenetwork reachability
- Garbage collector-এর mark phasereachable object খোঁজা
- Package resolverdependency ঘোরা
- Web crawlerlink ধরে হাঁটা
- Filesystem-এ finddirectory tree
- Compiler-এর dataflowCFG-তে fixed point
Euler path — যেখান থেকে graph theory-র জন্ম
১৭৩৬ সালে Königsberg শহরে সাতটা সেতু ছিল। প্রশ্ন: প্রতিটা সেতু ঠিক একবার পার হয়ে হাঁটা সম্ভব?
Euler প্রমাণ করলেন: না — আর সেই প্রমাণেই graph theory-র জন্ম।
Euler-এর যুক্তি: আপনি যখন একটা এলাকায় ঢোকেন, বেরোতেও হয়। তাই প্রতিটা মধ্যবর্তী এলাকার degree জোড় হতে হবে। শুধু শুরু আর শেষের বিন্দু বিজোড় হতে পারে।
Königsberg-এ চারটা এলাকার সবগুলোর degree বিজোড় (৩, ৩, ৩, ৫)। দুইটার বেশি বিজোড় — তাই অসম্ভব।
Theorem:
- Euler circuit (শুরুতে ফেরা) আছে ⟺ সব degree জোড়
- Euler path আছে ⟺ ঠিক ০ বা ২টা বিজোড় degree
Hamiltonian path (প্রতিটা vertex ঠিক একবার) দেখতে একই রকম,
কিন্তু NP-complete। Euler path O(E)-তে হয়।
দুইটা প্রায়-অভিন্ন প্রশ্ন, একটা সহজ একটা কঠিন — এই বৈপরীত্যটা complexity theory-র একটা ক্লাসিক উদাহরণ। Level 13-এ ফিরব।
উদাহরণ
একটা বাস্তব সমস্যাকে graph-এ রূপান্তর
সমস্যা: আপনার CI pipeline-এ ৫০টা test suite আছে। কিছু suite একই database fixture ব্যবহার করে, তাই একসাথে চালানো যায় না। সর্বনিম্ন কত ধাপে সব test চালানো যাবে?
রূপান্তর:
- Vertex = test suite
- Edge = দুইটা suite conflict করে (একই fixture)
- রঙ = কোন ধাপে চলবে
- উত্তর = chromatic number
এটা graph coloring — NP-complete। কিন্তু:
১. Greedy heuristic প্রায়ই যথেষ্ট:
def greedy_coloring(adj, order=None):
"""Welsh-Powell: বেশি degree আগে"""
order = order or sorted(adj, key=lambda v: -len(adj[v]))
color = {}
for v in order:
used = {color[u] for u in adj[v] if u in color}
c = 0
while c in used: c += 1
color[v] = c
return colorGreedy সর্বোচ্চ Δ + 1 রঙ ব্যবহার করে (Δ = সর্বোচ্চ degree) —
আর প্রায়ই তার চেয়ে কম।
২. কাঠামো কাজে লাগান। Conflict graph যদি bipartite হয়,
২ ধাপেই হয়ে যায় — আর সেটা O(V+E)-তে যাচাই করা যায়।
৩. সমস্যাটা বদলান। Fixture গুলো আলাদা করলে (প্রতিটা suite-এর নিজস্ব database schema) conflict graph edge-শূন্য হয়ে যায় — সব সমান্তরালে চলে।
Small-world আর scale-free network
বাস্তব network-গুলো random graph-এর মতো নয় — তাদের দুইটা বৈশিষ্ট্য প্রায় সর্বজনীন।
১. Small-world — গড় path দৈর্ঘ্য আশ্চর্যজনকভাবে ছোট,
O(log n)।
Milgram-এর “six degrees of separation” পরীক্ষা (১৯৬৭)। Facebook-এর ২০১৬-র বিশ্লেষণে ১.৫৯ বিলিয়ন user-এর গড় দূরত্ব পাওয়া গেছে ৩.৫৭।
২. Scale-free — degree distribution power law মানে:
অর্থাৎ কয়েকটা vertex-এর বিশাল degree (hub), আর বেশিরভাগের খুব কম।
পরিণতি — নিরাপত্তার দিক থেকে দ্বিমুখী:
| আক্রমণ | প্রভাব |
|---|---|
| Random node ব্যর্থতা | network প্রায় অক্ষত থাকে |
| Hub-লক্ষ্য করে আক্রমণ | দ্রুত ভেঙে পড়ে |
Internet-এর AS-level topology scale-free। তাই random router fail করলে কিছু হয় না, কিন্তু কয়েকটা বড় IXP বা tier-1 provider গেলে বড় বিভ্রাট।
২০২১-এর Facebook outage-এ ঠিক এটাই ঘটেছিল — কয়েকটা backbone router-এর config একসাথে ভুল হওয়ায় পুরো AS ইন্টারনেট থেকে বিচ্ছিন্ন হয়ে গিয়েছিল।
Planarity আর four color theorem
একটা graph planar যদি একে সমতলে edge না কেটে আঁকা যায়।
Euler’s formula (connected planar graph-এ):
F = face (অঞ্চল), বাইরের অসীম অঞ্চল সহ।
একটা দরকারি ফল: simple planar graph-এ (V ≥ 3):
তাই K₅ (৫ vertex, ১০ edge) planar নয়: 10 > 3(5) − 6 = 9।
Four color theorem (১৯৭৬): যেকোনো planar graph ৪ রঙে রাঙানো যায়।
এটা প্রথম প্রধান theorem যা computer-এর সাহায্যে প্রমাণিত — ১৯৩৬টা ক্ষেত্র যাচাই করতে হয়েছিল। গণিতবিদদের মধ্যে বিতর্ক হয়েছিল: যে প্রমাণ কোনো মানুষ সম্পূর্ণ যাচাই করতে পারে না, সেটা কি প্রমাণ?
২০০৫-এ Georges Gonthier পুরো প্রমাণটা Coq-এ formalize করেন — এখন একটা proof assistant পুরোটা যাচাই করতে পারে।
CS-এ planarity: VLSI chip layout (তার কাটাকাটি না করে), graph drawing, আর কিছু algorithm planar graph-এ দ্রুততর হয় (planar separator theorem)।
নিজে চালিয়ে দেখুন
নিজের মেশিনের network graph দেখুন
# একটা path দেখুন
traceroute -m 30 example.com 2>/dev/null || tracepath example.com
# একাধিক গন্তব্যে গিয়ে hop সংখ্যা তুলনা করুন
for host in google.com github.com wikipedia.org bbc.co.uk \
example.com cloudflare.com; do
hops=$(traceroute -m 30 -q 1 -w 1 "$host" 2>/dev/null | wc -l)
printf "%-20s %s hops\n" "$host" "$((hops - 1))"
doneসাধারণত ৮–২০ hop — বিশ্বের যেকোনো প্রান্তে। এটাই small-world
ধর্ম: কোটি কোটি router, কিন্তু path দৈর্ঘ্য O(log n)।
Graph-টা নিজে বানান:
import subprocess, re
from collections import defaultdict
HOSTS = ["google.com", "github.com", "wikipedia.org",
"cloudflare.com", "bbc.co.uk", "stackoverflow.com"]
edges = set()
paths = {}
for host in HOSTS:
try:
out = subprocess.run(
["traceroute", "-m", "20", "-q", "1", "-w", "1", host],
capture_output=True, text=True, timeout=90).stdout
except Exception:
continue
hops = []
for line in out.splitlines()[1:]:
m = re.search(r'\((\d+\.\d+\.\d+\.\d+)\)', line)
hops.append(m.group(1) if m else None)
paths[host] = hops
prev = "SELF"
for h in hops:
if h:
edges.add((prev, h))
prev = h
# ── বিশ্লেষণ ────────────────────────────────────────────────
adj = defaultdict(set)
for u, v in edges:
adj[u].add(v); adj[v].add(u)
print(f"vertex : {len(adj)}")
print(f"edge : {len(edges)}")
print(f"গড় degree: {2*len(edges)/len(adj):.2f}")
print("\nসবচেয়ে বেশি degree-র node (hub):")
for node, nbrs in sorted(adj.items(), key=lambda x: -len(x[1]))[:5]:
print(f" {node:>18} degree {len(nbrs)}")
print("\nপ্রতিটা গন্তব্যের path দৈর্ঘ্য:")
for host, hops in paths.items():
print(f" {host:>20} {len([h for h in hops if h])} hop")
# Handshake lemma যাচাই
total_deg = sum(len(v) for v in adj.values())
print(f"\nHandshake: Σdeg = {total_deg}, 2|E| = {2*len(edges)} "
f"{'মিলেছে' if total_deg == 2*len(edges) else 'গরমিল'}")
odd = sum(1 for v in adj.values() if len(v) % 2)
print(f"বিজোড় degree-র vertex: {odd} ({'জোড় সংখ্যা' if odd % 2 == 0 else 'ভুল!'})")লক্ষ্য করুন প্রথম কয়েকটা hop সব গন্তব্যের জন্য একই — আপনার router, ISP-র gateway। এরাই আপনার local graph-এর hub, আর এটাই scale-free কাঠামোর একটা ঝলক।
Internet একটা বাস্তব graph, আর traceroute দিয়ে তার একটা path সরাসরি দেখা যায় — small-world ধর্মটা নিজের চোখে যাচাই করা যায়।
Representation-এর memory ও গতি মাপুন
import sys, time, random
from collections import deque
def build_matrix(n, edges):
M = [bytearray(n) for _ in range(n)]
for u, v in edges:
M[u][v] = M[v][u] = 1
return M
def build_list(n, edges):
adj = [[] for _ in range(n)]
for u, v in edges:
adj[u].append(v); adj[v].append(u)
return adj
def bfs_matrix(M, s):
n = len(M); dist = [-1]*n; dist[s] = 0
q = deque([s])
while q:
u = q.popleft()
row = M[u]
for v in range(n):
if row[v] and dist[v] \< 0:
dist[v] = dist[u]+1; q.append(v)
return dist
def bfs_list(adj, s):
dist = [-1]*len(adj); dist[s] = 0
q = deque([s])
while q:
u = q.popleft()
for v in adj[u]:
if dist[v] \< 0:
dist[v] = dist[u]+1; q.append(v)
return dist
def mem(obj, depth=1):
total = sys.getsizeof(obj)
if depth and isinstance(obj, (list, tuple)):
total += sum(sys.getsizeof(x) for x in obj)
return total
N = 2000
print(f"n = {N}\n")
print(f"{'avg deg':>8} {'edges':>9} {'density':>9} "
f"{'mat MB':>8} {'list MB':>9} {'mat ms':>8} {'list ms':>9}")
print("─" * 68)
random.seed(1)
for avg_deg in [2, 5, 20, 100, 500, 1000]:
m = N * avg_deg // 2
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)))
edges = list(edges)
M = build_matrix(N, edges)
adj = build_list(N, edges)
t0 = time.perf_counter(); bfs_matrix(M, 0); t_m = (time.perf_counter()-t0)*1000
t0 = time.perf_counter(); bfs_list(adj, 0); t_l = (time.perf_counter()-t0)*1000
density = 2*len(edges)/(N*(N-1))
print(f"{avg_deg:>8} {len(edges):>9,} {density:>8.2%} "
f"{mem(M)/1e6:>8.2f} {mem(adj)/1e6:>9.2f} {t_m:>8.1f} {t_l:>9.1f}")সাধারণ ফলাফল:
avg deg edges density mat MB list MB mat ms list ms
────────────────────────────────────────────────────────────────────
2 2,000 0.10% 4.11 0.15 380.2 1.8
5 5,000 0.25% 4.11 0.20 381.5 3.9
20 20,000 1.00% 4.11 0.48 382.1 14.2
100 100,000 5.00% 4.11 1.72 384.7 68.3
500 499,748 25.00% 4.11 7.34 389.1 332.6
1000 996,857 49.86% 4.11 14.21 392.4 651.8তিনটা পর্যবেক্ষণ:
১. Matrix-এর সময় density-নিরপেক্ষ — সবসময় O(n²),
কারণ প্রতিটা row-এর সব n টা ঘর দেখতে হয়, edge থাক বা না থাক।
২. List-এর সময় edge সংখ্যার সমানুপাতিক — O(n + m)।
৩. ভারসাম্য উল্টে যায় প্রায় ৩০% density-তে। তার নিচে list জেতে, উপরে matrix।
বাস্তব graph প্রায় সবসময় sparse (density \< 1%), তাই
adjacency list প্রায় সবসময় সঠিক পছন্দ।
N বাড়িয়ে ৫০০০ করে দেখুন — matrix-এর memory 4× বাড়বে
(O(n²)), list-এর প্রায় 2.5× (O(n+m))।
Matrix বনাম list-এর তাত্ত্বিক trade-off বাস্তবে নাটকীয় — আর density কোথায় ভারসাম্য উল্টে দেয় সেটা measure করা যায়।
নিজে বানান
Graph Library — শূন্য থেকে
- একটা Graph class লিখুন যা directed ও undirected দুটোই সামলায়
- BFS, DFS, connected component, cycle detection যোগ করুন
- Bipartite check ও greedy coloring যোগ করুন
- Handshake lemma ও tree-র ধর্ম দিয়ে নিজের implementation যাচাই করুন
from collections import defaultdict, deque
class Graph:
def __init__(self, directed=False):
self.directed = directed
self.adj = defaultdict(set)
self._edges = set()
# ── নির্মাণ ──────────────────────────────────────────────
def add_vertex(self, v):
self.adj[v] # defaultdict touch
return self
def add_edge(self, u, v):
self.adj[u].add(v)
self.adj[v] # v-ও যেন থাকে
if not self.directed:
self.adj[v].add(u)
self._edges.add((u, v) if self.directed else (min(u,v,key=str), max(u,v,key=str)))
return self
@property
def V(self): return len(self.adj)
@property
def E(self): return len(self._edges)
def degree(self, v):
return len(self.adj[v])
# ── মৌলিক ধর্ম ───────────────────────────────────────────
def handshake_holds(self):
"""Σ deg(v) == 2|E| — undirected graph-এ"""
if self.directed: return None
return sum(len(n) for n in self.adj.values()) == 2 * self.E
def odd_degree_count(self):
return sum(1 for v in self.adj if self.degree(v) % 2)
# ── Traversal ────────────────────────────────────────────
def bfs(self, start):
dist, parent = {start: 0}, {start: None}
q = deque([start])
while q:
u = q.popleft()
for v in sorted(self.adj[u], key=str):
if v not in dist:
dist[v] = dist[u] + 1
parent[v] = u
q.append(v)
return dist, parent
def path(self, a, b):
_, parent = self.bfs(a)
if b not in parent: return None
out, cur = [], b
while cur is not None:
out.append(cur); cur = parent[cur]
return list(reversed(out))
def dfs(self, start):
seen, order, stack = set(), [], [start]
while stack:
u = stack.pop()
if u in seen: continue
seen.add(u); order.append(u)
for v in sorted(self.adj[u], key=str, reverse=True):
if v not in seen: stack.append(v)
return order
# ── Connectivity ─────────────────────────────────────────
def components(self):
seen, comps = set(), []
for v in sorted(self.adj, key=str):
if v in seen: continue
c = self.dfs(v)
seen.update(c)
comps.append(sorted(c, key=str))
return comps
def is_connected(self):
return len(self.components()) <= 1
# ── Cycle ────────────────────────────────────────────────
def find_cycle(self):
if self.directed:
WHITE, GRAY, BLACK = 0, 1, 2
color = {v: WHITE for v in self.adj}
parent = {}
def visit(u):
color[u] = GRAY
for v in self.adj[u]:
if color[v] == GRAY:
cyc, cur = [v], u
while cur != v:
cyc.append(cur); cur = parent[cur]
cyc.append(v)
return list(reversed(cyc))
if color[v] == WHITE:
parent[v] = u
r = visit(v)
if r: return r
color[u] = BLACK
return None
for v in self.adj:
if color[v] == WHITE:
r = visit(v)
if r: return r
return None
else:
parent = {}
seen = set()
def visit(u, p):
seen.add(u)
for v in self.adj[u]:
if v == p: continue
if v in seen:
cyc, cur = [v], u
while cur != v and cur is not None:
cyc.append(cur); cur = parent.get(cur)
cyc.append(v)
return list(reversed(cyc))
parent[v] = u
r = visit(v, u)
if r: return r
return None
for v in sorted(self.adj, key=str):
if v not in seen:
r = visit(v, None)
if r: return r
return None
def is_tree(self):
"""পাঁচটা সমতুল্য সংজ্ঞার দুইটা দিয়ে যাচাই"""
return (not self.directed
and self.is_connected()
and self.E == self.V - 1
and self.find_cycle() is None)
# ── Bipartite ও coloring ─────────────────────────────────
def bipartition(self):
color = {}
for s in sorted(self.adj, key=str):
if s in color: continue
color[s] = 0
q = deque([s])
while q:
u = q.popleft()
for v in self.adj[u]:
if v not in color:
color[v] = 1 - color[u]; q.append(v)
elif color[v] == color[u]:
return None # বিজোড় cycle
return color
def greedy_coloring(self):
"""Welsh–Powell — বেশি degree আগে"""
order = sorted(self.adj, key=lambda v: (-self.degree(v), str(v)))
color = {}
for v in order:
used = {color[u] for u in self.adj[v] if u in color}
c = 0
while c in used: c += 1
color[v] = c
return color
def report(self, name):
print(f"\n── {name} " + "─" * max(0, 46 - len(name)))
print(f" V = {self.V}, E = {self.E}")
if not self.directed:
print(f" handshake (Σdeg = 2|E|) : {self.handshake_holds()}")
print(f" বিজোড় degree vertex : {self.odd_degree_count()} "
f"({'জোড় — ঠিক' if self.odd_degree_count() % 2 == 0 else 'ভুল!'})")
print(f" connected : {self.is_connected()}")
print(f" component : {len(self.components())}")
print(f" tree : {self.is_tree()}")
bp = self.bipartition()
print(f" bipartite : {bp is not None}")
col = self.greedy_coloring()
print(f" greedy রঙ লেগেছে : {max(col.values())+1} "
f"(Δ+1 = {max(self.degree(v) for v in self.adj)+1} -এর সীমার মধ্যে)")
cyc = self.find_cycle()
print(f" cycle : {cyc if cyc else 'নেই'}")
# ── উদাহরণ ──────────────────────────────────────────────────
tree = Graph()
for u, v in [("a","b"), ("a","c"), ("b","d"), ("b","e"), ("c","f")]:
tree.add_edge(u, v)
tree.report("একটা tree")
cyc = Graph()
for u, v in [("a","b"), ("b","c"), ("c","d"), ("d","a")]:
cyc.add_edge(u, v)
cyc.report("৪-cycle (জোড়)")
odd = Graph()
for u, v in [("a","b"), ("b","c"), ("c","a")]:
odd.add_edge(u, v)
odd.report("ত্রিভুজ (বিজোড় cycle)")
k5 = Graph()
for i in range(5):
for j in range(i+1, 5):
k5.add_edge(i, j)
k5.report("K5 (complete)")
dag = Graph(directed=True)
for u, v in [("parse","check"), ("check","opt"), ("opt","gen"), ("lint","gen")]:
dag.add_edge(u, v)
dag.report("Build DAG")
bad = Graph(directed=True)
for u, v in [("a","b"), ("b","c"), ("c","a")]:
bad.add_edge(u, v)
bad.report("চক্র সহ digraph")
print("\npath a → f :", tree.path("a", "f"))
print("dist :", tree.bfs("a")[0])নিজে বাড়ান:
- Weighted edge যোগ করুন এবং Dijkstra লিখুন
- Kruskal দিয়ে minimum spanning tree — Union-Find ব্যবহার করে (relations-এর লেসনের data structure)
- Directed graph-এ strongly connected component (Tarjan বা Kosaraju)
is_planar()-এর একটা আংশিক check:E ≤ 3V − 6যাচাই করুন- Random graph generator লিখুন (Erdős–Rényi) আর degree distribution plot করুন — তারপর preferential attachment (Barabási–Albert) দিয়ে scale-free বানিয়ে তুলনা করুন
বাস্তব সিস্টেমে
Graph যেখানে যেখানে
Internet routing। BGP প্রতিটা autonomous system-কে একটা vertex ধরে path vector protocol চালায়। রুট নির্বাচন মানে graph-এ path বাছা — কিন্তু policy সহ, শুধু দৈর্ঘ্য নয়। Level 7-এ দেখব।
Compiler-এর control flow graph। প্রতিটা basic block একটা vertex, প্রতিটা jump একটা edge। Dominator tree, loop detection, dead code elimination — সবই graph algorithm। Level 5-এ বানাব।
Garbage collection। Object graph-এ root থেকে reachability। Mark phase আক্ষরিকভাবে একটা BFS/DFS। Cycle থাকলে reference counting ব্যর্থ হয় — এটাই tracing GC দরকার হওয়ার কারণ।
Filesystem। Directory tree একটা tree — কিন্তু hard link আর
symlink যোগ করলে সাধারণ graph হয়ে যায়। find -এ -L দিলে
symlink follow হয়, আর তখন cycle-এ পড়া সম্ভব — তাই
find -এ loop detection আছে।
Build system। make, Bazel — DAG-এ topological sort।
আগের লেসনে দেখেছি।
Database query plan। একটা query plan একটা DAG। Join order নির্বাচন = সেই DAG-এর আকার বাছা।
Distributed system-এর causality। Event-দের happens-before graph একটা DAG। Vector clock সেই graph-এর তথ্য encode করে।
Neural network। একটা computation graph। Backpropagation হলো সেই graph-এ reverse topological order-এ chain rule প্রয়োগ। PyTorch-এর autograd আক্ষরিকভাবে একটা DAG বানায় আর উল্টো দিকে হাঁটে।
Social network। Facebook-এর graph API, Twitter-এর follow graph। Friend suggestion = common neighbor গোনা। Community detection = graph clustering।
Recommendation। User–item bipartite graph। Collaborative filtering মানে সেই graph-এ random walk বা matrix factorization।
Version control। Git-এর commit DAG — আগের লেসনে দেখেছি।
Kubernetes। Service mesh-এর traffic graph, owner reference-এর DAG (garbage collection-এর জন্য), আর network policy graph।
যে ভুলগুলো সবাই করে
“Tree আর graph আলাদা দুইটা data structure।”
Tree হলো graph-এর একটা বিশেষ শ্রেণি — connected এবং acyclic।
তাই প্রতিটা graph algorithm tree-তেও চলে (আর প্রায়ই সরল হয়ে যায়)।
উল্টোদিকে, tree-র জন্য লেখা algorithm সাধারণ graph-এ ভেঙে পড়ে — কারণ tree-তে কোনো cycle নেই, তাই “visited” track করার দরকার হয় না।
def sum_tree(node):
if not node: return 0
return node.val + sum_tree(node.left) + sum_tree(node.right)
# graph-এ চালালে cycle-এ অসীম recursionবাস্তব bug: একটা “tree” ধরে নেওয়া কাঠামোয় যদি কেউ একটা extra reference যোগ করে দেয় (JSON-এ circular reference, ORM-এ bidirectional relation), তখন serialization বা traversal infinite loop-এ পড়ে।
এই কারণেই json.dumps() circular reference-এ ValueError
ছোঁড়ে, আর ভালো serializer-এ visited set থাকে।
“Adjacency matrix সবসময় দ্রুত, কারণ O(1) lookup।”
has_edge(u,v) O(1) — সত্যি। কিন্তু সেটাই একমাত্র operation নয়।
বেশিরভাগ graph algorithm-এর মূল operation হলো “u-এর সব প্রতিবেশী ঘোরা”:
- Matrix-এ:
O(n)— সবnটা ঘর দেখতে হয় - List-এ:
O(deg u)— শুধু প্রকৃত প্রতিবেশী
Sparse graph-এ deg u ≈ 5 আর n = 10⁶ হলে পার্থক্য ২ লক্ষ গুণ।
আর তাই BFS/DFS:
- Matrix:
O(n²) - List:
O(n + m)
উপরের experiment-এ আমরা এটা measure করেছি — sparse graph-এ list ২০০ গুণ দ্রুত ছিল।
Matrix কখন ভালো:
- Dense graph (
m ≈ n²) nছোট (\< 1000)- Matrix operation দরকার — যেমন
M^kদিয়েk-দৈর্ঘ্যের path গোনা - SIMD/GPU-তে matrix multiplication কাজে লাগানো যায়
“Shortest path মানেই Dijkstra।”
Dijkstra একটা নির্দিষ্ট শর্তে কাজ করে: সব edge weight অ-ঋণাত্মক।
| পরিস্থিতি | সঠিক algorithm | জটিলতা |
|---|---|---|
| Unweighted | BFS | O(V+E) |
| অ-ঋণাত্মক weight | Dijkstra | O(E log V) |
| ঋণাত্মক weight | Bellman–Ford | O(VE) |
| সব জোড়ার মধ্যে | Floyd–Warshall | O(V³) |
| Heuristic আছে | A* | ব্যবহারিকভাবে দ্রুত |
| DAG | topological sort + relax | O(V+E) |
Unweighted graph-এ Dijkstra ব্যবহার করা সাধারণ ভুল — BFS একই উত্তর দেয়, অনেক দ্রুত, priority queue ছাড়াই।
ঋণাত্মক weight কেন Dijkstra ভাঙে: Dijkstra ধরে নেয় একবার একটা node finalize হলে তার দূরত্ব আর কমবে না। ঋণাত্মক edge থাকলে পরে একটা ছোট path পাওয়া যেতে পারে।
ঋণাত্মক weight কোথায় বাস্তব: currency arbitrage (লাভকে ঋণাত্মক cost ধরে), কিছু scheduling সমস্যা, আর network flow-এর residual graph।
Level 6-এ আমরা এই পাঁচটাই implement করব আর measure করব।
“Graph algorithm গুলো বড় graph-এ চলে না।”
অনেকগুলো চমৎকার scale করে।
BFS, DFS, connected component, topological sort — সবই O(V+E),
অর্থাৎ linear। বিলিয়ন-edge graph-এ চলে।
Facebook-এর ২০১৬-র বিশ্লেষণে ১.৫৯ বিলিয়ন user-এর গড় দূরত্ব হিসাব করা হয়েছিল — HyperLogLog-ভিত্তিক আনুমানিক পদ্ধতিতে, কিন্তু graph traversal-এর উপরেই।
যেগুলো scale করে না:
| সমস্যা | জটিলতা |
|---|---|
| All-pairs shortest path | O(V³) — ১০,০০০ vertex-এ অসম্ভব |
| Graph coloring (optimal) | NP-complete |
| Hamiltonian path | NP-complete |
| Maximum clique | NP-complete |
| Subgraph isomorphism | NP-complete |
কৌশল: approximation (greedy coloring), sampling (random walk), অথবা সমস্যাটার বিশেষ কাঠামো কাজে লাগানো (planar, bipartite, tree)।
আর distributed graph processing framework আছে — Pregel, GraphX, Giraph — যা বিলিয়ন-vertex graph-এ vertex-centric computation চালায়।
বুঝেছেন কি না দেখুন
1একটা graph-এ ৭টা vertex আছে যাদের degree যথাক্রমে
3, 3, 3, 3, 3, 3, 3। এমন graph কি সম্ভব?
প্রয়োগ
3, 3, 3, 3, 3, 3, 3। এমন graph কি সম্ভব?না।
Handshake lemma:
কিন্তু 21 বিজোড়, আর 2|E| সবসময় জোড়। বিরোধ। ∎
সাধারণ নিয়ম: বিজোড় degree-র vertex সংখ্যা অবশ্যই জোড় হতে হবে। এখানে ৭টা vertex-ই বিজোড় degree-র — ৭ বিজোড়, তাই অসম্ভব।
সংশোধন: ৬টা vertex-এর প্রতিটার degree ৩ হলে সম্ভব
(6 × 3 = 18 = 2 × 9, অর্থাৎ ৯টা edge)। যেমন K₃,₃ বা
একটা prism graph।
এই check-টা কোথায় কাজে লাগে:
একটা network topology config parse করার পর এটা একটা সস্তা sanity check:
def validate_topology(degrees):
total = sum(degrees)
if total % 2:
raise ValueError(f"degree যোগফল {total} বিজোড় — config-এ ভুল আছে")
odd = sum(1 for d in degrees if d % 2)
assert odd % 2 == 0
return total // 2 # edge সংখ্যাকোনো graph না বানিয়েই ভুল ধরা পড়ে।
Erdős–Gallai theorem আরো এগিয়ে যায়: একটা degree sequence
কখন realizable সেটার সম্পূর্ণ শর্ত দেয়। Handshake শুধু
প্রয়োজনীয় শর্ত, যথেষ্ট নয় — যেমন (3, 1, 1, 1) যোগফল জোড়
কিন্তু তবু অসম্ভব simple graph-এ।
2প্রমাণ করুন: n ≥ 2 vertex-এর যেকোনো tree-তে অন্তত দুইটা leaf
(degree-১ vertex) আছে।
যুক্তি
n ≥ 2 vertex-এর যেকোনো tree-তে অন্তত দুইটা leaf
(degree-১ vertex) আছে।প্রমাণ (contradiction).
ধরি একটা tree T-তে n ≥ 2 vertex আছে কিন্তু সর্বোচ্চ একটা
leaf।
তাহলে অন্তত n − 1 টা vertex-এর degree ≥ 2।
Degree-র যোগফল:
(সবচেয়ে ভালো ক্ষেত্রে: n−1 টা vertex-এর degree ২, আর একটা leaf-এর ১)
Handshake lemma অনুযায়ী:
Tree-তে |E| = n − 1 (আগে প্রমাণিত), তাই:
কিন্তু আমরা পেয়েছিলাম ≥ 2n − 1।
মিথ্যা। বিরোধ। ∎
বিকল্প প্রমাণ (constructive) — অনেক বেশি অন্তর্দৃষ্টিপূর্ণ:
T-এর একটা দীর্ঘতম path নিন: v₀ → v₁ → … → vₖ
(k ≥ 1 যেহেতু n ≥ 2 এবং connected)।
দাবি: v₀ আর vₖ দুটোই leaf।
ধরুন v₀-এর degree ≥ 2। তাহলে v₁ ছাড়াও আরেকটা প্রতিবেশী
u আছে।
- যদি
upath-এ না থাকে → path বাড়ানো যেত (u → v₀ → … → vₖ), কিন্তু আমরা দীর্ঘতম নিয়েছিলাম। বিরোধ। - যদি
upath-এ থাকে →v₀থেকেuপর্যন্ত path আর নতুন edge মিলে একটা cycle। কিন্তু tree acyclic। বিরোধ।
তাই v₀ leaf। একই যুক্তিতে vₖ leaf। ∎
তাৎপর্য: এই leaf-এর অস্তিত্বই বহু tree algorithm-এর ভিত্তি — আপনি সবসময় একটা leaf খুঁজে সরিয়ে ছোট tree-তে নামতে পারবেন।
- Tree-র edge সংখ্যার induction প্রমাণে এটাই ব্যবহার করেছি
- Huffman coding leaf থেকে শুরু করে উপরে ওঠে
- Tree DP প্রায়ই leaf থেকে root-এর দিকে চলে
- Topological sort-এর Kahn’s algorithm একই ধারণার DAG-রূপ
3আপনার microservice architecture-এ ২০টা service। আপনি একটা
“blast radius” metric চান — একটা service fail করলে কতগুলো
প্রভাবিত হবে। এটা কীভাবে graph সমস্যা হিসেবে মডেল করবেন?
ডিজাইন
মডেল:
- Vertex = service
- Edge
A → B=A,B-র উপর নির্ভর করে (Bfail করলেAপ্রভাবিত)
Blast radius of X = যেসব service X-এ পৌঁছাতে পারে
= reverse graph-এ X থেকে reachable set
def blast_radius(deps, service):
"""deps: {service: [যাদের উপর নির্ভর করে]}"""
reverse = defaultdict(set)
for s, targets in deps.items():
for t in targets:
reverse[t].add(s) # t fail → s প্রভাবিত
seen, q = set(), deque([service])
while q:
u = q.popleft()
for v in reverse[u]:
if v not in seen:
seen.add(v); q.append(v)
return seenএকটা BFS — O(V + E)।
আরো দরকারি metric গুলো:
| Metric | Graph প্রশ্ন |
|---|---|
| Blast radius | reverse reachability |
| Critical service | সবচেয়ে বড় blast radius |
| Single point of failure | যার blast radius = সব |
| Fragility | in-degree (কতজন এর উপর নির্ভর করে) |
| Coupling | out-degree (কতগুলোর উপর নির্ভর করে) |
| Layer violation | topological order লঙ্ঘনকারী edge |
| Articulation point | যে vertex মুছলে graph disconnect হয় |
শেষটা বিশেষভাবে দরকারি — articulation point (বা cut vertex)
খোঁজার জন্য Tarjan-এর O(V+E) algorithm আছে। এগুলোই আপনার
প্রকৃত single point of failure।
তিনটা সূক্ষ্মতা যা প্রায়ই বাদ পড়ে:
১. Hard বনাম soft dependency। A, B-র উপর নির্ভর করে —
কিন্তু B fail করলে A কি পুরো বন্ধ হয়, নাকি degraded mode-এ
চলে? Edge-এ একটা weight বা label দিন।
edges = {("api", "auth"): "hard",
("api", "recommendations"): "soft"}Soft edge blast radius-এ গোনা উচিত নয় (বা কম ওজনে)।
২. লুকানো dependency। আপনার service graph-এ DNS, নেই? Certificate authority? Cloud provider-এর metadata service? এগুলো প্রায় সব service-এর dependency, আর প্রায়ই diagram-এ থাকে না — অথচ এগুলোই বাস্তব outage-এর কারণ হয়।
৩. Transitive weight। A → B → C -তে C fail করলে A
প্রভাবিত, কিন্তু হয়তো B-র caching-এর কারণে কম।
Probability দিয়ে মডেল করা যায়:
ব্যবহারিক সুপারিশ:
- Service manifest থেকে graph auto-generate করুন (distributed tracing data সবচেয়ে নির্ভরযোগ্য উৎস — declared নয়, actual call)
- CI-তে cycle detection চালান
- Blast radius একটা dashboard metric বানান
- উচ্চ blast radius-এর service-এ বেশি SLO, বেশি redundancy, staged rollout
Level 12-এ আমরা distributed tracing থেকে এই graph বানাব।
4একটা adjacency matrix M-এর জন্য, M²-এর (i,j) ঘরে কী থাকে?
আর M^k-এ?
প্রয়োগ
M-এর জন্য, M²-এর (i,j) ঘরে কী থাকে?
আর M^k-এ?Matrix গুণের সংজ্ঞা:
M boolean (0/1) হলে, M[i][k] × M[k][j] = 1 তখনই যখন
i → k এবং k → j — অর্থাৎ i থেকে j-তে k হয়ে
একটা ২-দৈর্ঘ্যের path আছে।
যোগফলটা সব সম্ভাব্য মধ্যবর্তী k-এর উপর, তাই:
সাধারণভাবে:
লক্ষ্য করুন “walk”, “path” নয় — vertex পুনরাবৃত্তি অনুমোদিত।
প্রয়োগ:
১. ত্রিভুজ গোনা। trace(M³)/6 = undirected graph-এ ত্রিভুজের
সংখ্যা।
কারণ (M³)ᵢᵢ = i থেকে i-তে ৩-দৈর্ঘ্যের walk = i জড়িত
ত্রিভুজ। প্রতিটা ত্রিভুজ ৩টা vertex-এ গোনা হয়, আর প্রতিটা
২ দিকে — তাই 6 দিয়ে ভাগ।
Social network-এ ত্রিভুজ গোনা = clustering coefficient মাপা = “আমার বন্ধুরা কি একে অপরের বন্ধু?”
২. Reachability। Boolean semiring-এ (+ → OR, × → AND):
কারণ n vertex-এর graph-এ যেকোনো path সর্বোচ্চ n−1 দৈর্ঘ্যের।
৩. Shortest path। Min-plus semiring-এ (+ → min, × → +):
এটাই Floyd–Warshall-এর matrix রূপ, আর একই কারণে
repeated squaring দিয়ে O(n³ log n)-এ all-pairs shortest
path করা যায়।
৪. PageRank। Transition matrix-এর বারবার গুণ, যা eigenvector power iteration। Google-এর মূল algorithm।
৫. Markov chain। M যদি transition probability matrix হয়,
তাহলে M^k = k ধাপ পরের distribution।
5কেন BFS unweighted graph-এ shortest path দেয় কিন্তু DFS দেয় না?
DFS-কে shortest path দিতে বাধ্য করা যায় কি?
যুক্তি
BFS কেন কাজ করে:
BFS স্তরে স্তরে অগ্রসর হয় — প্রথমে দূরত্ব ১-এর সব vertex, তারপর দূরত্ব ২, ইত্যাদি।
Invariant: যখন একটা vertex প্রথমবার queue-তে ঢোকে, তখন তার দূরত্ব চূড়ান্ত।
প্রমাণ (induction on distance).
Base: dist(s) = 0, স্পষ্ট।
Step: ধরি দূরত্ব ≤ d-এর সব vertex সঠিক দূরত্বে আবিষ্কৃত।
Queue-তে এখন শুধু দূরত্ব d ও d+1-এর vertex আছে (FIFO ধর্ম)।
দূরত্ব d+1-এর একটা vertex v নিন। তার একটা প্রতিবেশী u
আছে যার দূরত্ব d। u প্রক্রিয়াকৃত হলে v আবিষ্কৃত হবে,
আর dist[v] = dist[u] + 1 = d+1 ✓ ∎
DFS কেন ব্যর্থ:
DFS একটা শাখায় যতদূর সম্ভব যায়। তাই একটা vertex-এ পৌঁছাতে পারে একটা দীর্ঘ ঘুরপথে, আর সেটা “visited” চিহ্নিত করে দেয়। পরে ছোট path পাওয়া গেলেও আর update হয় না।
a
/ \
b c
| |
d |
\ /
ea → e-এর shortest path হলো a → c → e (দৈর্ঘ্য ২)।
DFS যদি b আগে দেখে: a → b → d → e (দৈর্ঘ্য ৩) দিয়ে e
পৌঁছাবে আর visited করে দেবে। ভুল উত্তর।
DFS-কে বাধ্য করা যায়?
হ্যাঁ, কিন্তু দাম দিয়ে।
বিকল্প ১ — visited না রেখে সব path দেখুন:
def all_paths_dfs(adj, u, target, path=None, best=None):
path = (path or []) + [u]
if u == target:
return path if not best or len(path) \< len(best) else best
for v in adj[u]:
if v not in path: # শুধু বর্তমান path-এ চেক
best = all_paths_dfs(adj, v, target, path, best)
return bestসঠিক, কিন্তু exponential — সব path enumerate করে।
বিকল্প ২ — Iterative Deepening DFS (IDDFS):
def iddfs(adj, s, t, max_depth):
def dls(u, t, depth, path):
if u == t: return path
if depth == 0: return None
for v in adj[u]:
if v not in path:
r = dls(v, t, depth-1, path + [v])
if r: return r
return None
for d in range(max_depth + 1):
r = dls(s, t, d, [s])
if r: return r
return Noneগভীরতা ১, ২, ৩… করে বারবার DFS চালান। প্রথম যেটা পায় সেটাই shortest।
খরচ: উপরের স্তরগুলো বারবার visit হয়। কিন্তু branching
factor b হলে মোট কাজ:
অর্থাৎ শেষ স্তরটাই খরচের প্রায় পুরোটা — পুনরাবৃত্তির overhead ধ্রুবক গুণক মাত্র।
IDDFS কেন ব্যবহার হয়:
| BFS | IDDFS | |
|---|---|---|
| সময় | O(b^d) | O(b^d) |
| Memory | O(b^d) | O(d) |
| Shortest path | ✓ | ✓ |
Memory-টাই আসল সুবিধা। BFS-কে পুরো frontier রাখতে হয়, যা exponential হতে পারে। IDDFS শুধু বর্তমান path রাখে।
তাই বিশাল search space-এ (chess, puzzle solver, game AI) IDDFS প্রামাণ্য — আর তার heuristic-সহ সংস্করণ IDA* বহু puzzle solver-এর ভিত্তি।
Level 6-এ আমরা এগুলো implement করব।
এরপর কী
Graph theory শেষ করে আমরা এখন Level 0-এর শেষ চারটা বিষয়ে যাব, আর এগুলো সরাসরি Level 1 ও Level 2-এর সেতু:
- Boolean algebra — propositional logic-এরই বীজগণিতিক রূপ, কিন্তু এবার circuit-এর ভাষায়। K-map, minimization, আর কেন NAND দিয়ে সব হয়।
- Number systems ও modular arithmetic — base conversion, GCD, মৌলিক সংখ্যা, আর RSA-র গাণিতিক ভিত্তি।
- Linear algebra — vector, matrix, transformation; graphics, ML আর PageRank-এর ভাষা।
- Asymptotic notation —
O,Θ,Ωআনুষ্ঠানিকভাবে, আর কখন এই বিমূর্তকরণ মিথ্যা বলে।
এরপর Level 1 — যেখানে আমরা bit-এর জগতে নামব।
আরও পড়ুন
- Introduction to Graph Theory — Douglas West · প্রামাণ্য পাঠ্যবই
- Mathematics for Computer Science, Chapters 11–12 — Lehman, Leighton, Meyer