Foundationপ্রথম নীতি থেকে
LEVEL 0লেসন ১২/১৬কঠিন১ ঘণ্টা ১০ মিনিট

Graph Theory — সংযোগের গণিত

Graph Theory

Vertex, edge, path, tree, connectivity — যে একটামাত্র কাঠামো দিয়ে network, dependency, compiler-এর control flow, filesystem আর distributed system সবই মডেল করা হয়।

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

  • Graph-এর মৌলিক পরিভাষা ও ধরনগুলো সঠিকভাবে ব্যবহার করতে পারবেন
  • Adjacency matrix ও adjacency list-এর trade-off বুঝে সঠিকটা বাছতে পারবেন
  • Handshake lemma ও degree-ভিত্তিক যুক্তি প্রয়োগ করতে পারবেন
  • Tree-র সমতুল্য সংজ্ঞাগুলো প্রমাণ করতে পারবেন
  • একটা বাস্তব সমস্যাকে graph সমস্যায় রূপান্তর করতে পারবেন

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

আগে এটা বুঝি

এই ছয়টা জিনিসের মধ্যে কী মিল?

  • ইন্টারনেটের router-গুলো
  • আপনার package.json-এর dependency
  • একটা function-এর ভেতরের if/while structure
  • 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
Weightededge-এ একটা সংখ্যাদূরত্ব, latency, খরচ
Multigraphএকাধিক edge একই জোড়ায়দুই শহরের মধ্যে দুইটা রাস্তা
Simpleself-loop নেই, multi-edge নেইসাধারণ ধরে নেওয়া হয়

Degree — একটা vertex-এ কতগুলো edge লাগে।

Directed graph-এ দুইটা আলাদা: in-degree আর out-degree

Handshake lemma

প্রতিটা edge দুইটা vertex-এর degree-তে ১ করে যোগ করে। তাই:

vVdeg(v)=2E\sum_{v \in V} \deg(v) = 2|E|

একটা তাৎক্ষণিক ফল: বিজোড় degree-র vertex সংখ্যা সবসময় জোড়

কারণ যোগফল জোড় (2|E|), আর জোড় degree-গুলো যোগ করলে জোড় থাকে — তাই বিজোড়গুলোর সংখ্যাও জোড় হতে হবে।

বাস্তব প্রয়োগ: একটা পার্টিতে যতজন মানুষ বিজোড় সংখ্যক লোকের সাথে হাত মিলিয়েছে, সেই সংখ্যাটা সবসময় জোড়।

CS-এ: একটা network topology-তে বিজোড় সংখ্যক link-ওয়ালা node-এর সংখ্যা জোড় হতেই হবে। Configuration-এ সেটা না মিললে আপনার data-তে ভুল আছে — একটা সস্তা sanity check।

Path, cycle, connectivity

শব্দমানে
Walkedge ধরে হাঁটা, পুনরাবৃত্তি অনুমোদিত
Pathvertex পুনরাবৃত্তি নেই এমন 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 ←┘
একই vertex সেট, ভিন্ন edge — connectivity সম্পূর্ণ আলাদা।

Tree — সবচেয়ে গুরুত্বপূর্ণ বিশেষ ক্ষেত্র

একটা tree হলো connected acyclic undirected graph।

সমতুল্য সংজ্ঞা — এই পাঁচটা একই জিনিস বলে:

n vertex-এর একটা graph G-এর জন্য, নিচের যেকোনো একটা সত্য হলে বাকি সবগুলোও সত্য:

  1. G connected এবং acyclic
  2. G connected এবং ঠিক n−1 edge আছে
  3. G acyclic এবং ঠিক n−1 edge আছে
  4. যেকোনো দুই vertex-এর মধ্যে ঠিক একটা path আছে
  5. G connected, কিন্তু যেকোনো edge মুছলেই disconnect হয়ে যায়

সংজ্ঞা ৪ সবচেয়ে বেশি কাজে লাগে — “ঠিক একটা path” মানে কোনো অস্পষ্টতা নেই, কোনো redundancy নেই।

Tree-র edge সংখ্যা প্রমাণ

দাবি: n vertex-এর একটা tree-তে ঠিক n−1 edge।

প্রমাণ (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, color

CS-এ কোথায়:

  • 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
Bipartite2
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] = 1

Adjacency list

প্রতিটা vertex-এর প্রতিবেশীদের তালিকা।

adj = {u: [v1, v2, ...] for u in vertices}
OperationMatrixList
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)
MemoryO(n²)O(n + m)
BFS/DFSO(n²)O(n + m)
কোনটা বাছবেন তা edge density আর কোন operation বেশি হবে তার উপর নির্ভর করে।

সিদ্ধান্তের নিয়ম:

  • 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। এক লাইন বদলালে সম্পূর্ণ ভিন্ন আচরণ।

BFSDFS
Data structureQueueStack (বা recursion)
MemoryO(width)O(depth)
Shortest path (unweighted)
Cycle detectionসম্ভবস্বাভাবিক
Topological sortKahn-এ
Connected component
Bipartite check
একটা graph traversal কোথায় কোথায় চলছে
  1. BFS on a graphগাণিতিক algorithm
  2. ping / traceroutenetwork reachability
  3. Garbage collector-এর mark phasereachable object খোঁজা
  4. Package resolverdependency ঘোরা
  5. Web crawlerlink ধরে হাঁটা
  6. Filesystem-এ finddirectory tree
  7. 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 color

Greedy সর্বোচ্চ Δ + 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 মানে:

P(deg=k)kγ,γ23P(\deg = k) \propto k^{-\gamma}, \qquad \gamma \approx 2\text{–}3

অর্থাৎ কয়েকটা 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-এ):

VE+F=2V - E + F = 2

F = face (অঞ্চল), বাইরের অসীম অঞ্চল সহ।

একটা দরকারি ফল: simple planar graph-এ (V ≥ 3):

E3V6E \le 3V - 6

তাই 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)।

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

EXPERIMENT

নিজের মেশিনের network graph দেখুন

Linux / macOS· ১৫ মিনিট
# একটা 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 ধর্মটা নিজের চোখে যাচাই করা যায়।

EXPERIMENT

Representation-এর memory ও গতি মাপুন

Python 3· ১৫ মিনিট
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 বাড়বে (O(n²)), list-এর প্রায় 2.5× (O(n+m))।

এটা কী প্রমাণ করে

Matrix বনাম list-এর তাত্ত্বিক trade-off বাস্তবে নাটকীয় — আর density কোথায় ভারসাম্য উল্টে দেয় সেটা measure করা যায়।

নিজে বানান

BUILD IT

Graph Library — শূন্য থেকে

Python · ●●●○○
  1. একটা Graph class লিখুন যা directed ও undirected দুটোই সামলায়
  2. BFS, DFS, connected component, cycle detection যোগ করুন
  3. Bipartite check ও greedy coloring যোগ করুন
  4. 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])

নিজে বাড়ান:

  1. Weighted edge যোগ করুন এবং Dijkstra লিখুন
  2. Kruskal দিয়ে minimum spanning tree — Union-Find ব্যবহার করে (relations-এর লেসনের data structure)
  3. Directed graph-এ strongly connected component (Tarjan বা Kosaraju)
  4. is_planar() -এর একটা আংশিক check: E ≤ 3V − 6 যাচাই করুন
  5. 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জটিলতা
UnweightedBFSO(V+E)
অ-ঋণাত্মক weightDijkstraO(E log V)
ঋণাত্মক weightBellman–FordO(VE)
সব জোড়ার মধ্যেFloyd–WarshallO(V³)
Heuristic আছেA*ব্যবহারিকভাবে দ্রুত
DAGtopological sort + relaxO(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 pathO(V³) — ১০,০০০ vertex-এ অসম্ভব
Graph coloring (optimal)NP-complete
Hamiltonian pathNP-complete
Maximum cliqueNP-complete
Subgraph isomorphismNP-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 কি সম্ভব?

প্রয়োগ

না।

Handshake lemma: deg(v)=2E\sum \deg(v) = 2|E|

7×3=217 \times 3 = 21

কিন্তু 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) আছে।

যুক্তি

প্রমাণ (contradiction).

ধরি একটা tree T-তে n ≥ 2 vertex আছে কিন্তু সর্বোচ্চ একটা leaf।

তাহলে অন্তত n − 1 টা vertex-এর degree ≥ 2

Degree-র যোগফল: deg(v)2(n1)+1=2n1\sum \deg(v) \ge 2(n-1) + 1 = 2n - 1

(সবচেয়ে ভালো ক্ষেত্রে: n−1 টা vertex-এর degree ২, আর একটা leaf-এর ১)

Handshake lemma অনুযায়ী: deg(v)=2E\sum \deg(v) = 2|E|

Tree-তে |E| = n − 1 (আগে প্রমাণিত), তাই: deg(v)=2(n1)=2n2\sum \deg(v) = 2(n-1) = 2n - 2

কিন্তু আমরা পেয়েছিলাম ≥ 2n − 1

2n22n1    212n - 2 \ge 2n - 1 \implies -2 \ge -1

মিথ্যা। বিরোধ। ∎

বিকল্প প্রমাণ (constructive) — অনেক বেশি অন্তর্দৃষ্টিপূর্ণ:

T-এর একটা দীর্ঘতম path নিন: v₀ → v₁ → … → vₖ (k ≥ 1 যেহেতু n ≥ 2 এবং connected)।

দাবি: v₀ আর vₖ দুটোই leaf।

ধরুন v₀-এর degree ≥ 2। তাহলে v₁ ছাড়াও আরেকটা প্রতিবেশী u আছে।

  • যদি u path-এ না থাকে → path বাড়ানো যেত (u → v₀ → … → vₖ), কিন্তু আমরা দীর্ঘতম নিয়েছিলাম। বিরোধ।
  • যদি u path-এ থাকে → 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-র উপর নির্ভর করে (B fail করলে 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 গুলো:

MetricGraph প্রশ্ন
Blast radiusreverse reachability
Critical serviceসবচেয়ে বড় blast radius
Single point of failureযার blast radius = সব
Fragilityin-degree (কতজন এর উপর নির্ভর করে)
Couplingout-degree (কতগুলোর উপর নির্ভর করে)
Layer violationtopological 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 দিয়ে মডেল করা যায়:

P(A প্রভাবিত)=1path(1P(path fail))P(A \text{ প্রভাবিত}) = 1 - \prod_{\text{path}} (1 - P(\text{path fail}))

ব্যবহারিক সুপারিশ:

  1. Service manifest থেকে graph auto-generate করুন (distributed tracing data সবচেয়ে নির্ভরযোগ্য উৎস — declared নয়, actual call)
  2. CI-তে cycle detection চালান
  3. Blast radius একটা dashboard metric বানান
  4. উচ্চ blast radius-এর service-এ বেশি SLO, বেশি redundancy, staged rollout

Level 12-এ আমরা distributed tracing থেকে এই graph বানাব।

4

একটা adjacency matrix M-এর জন্য, -এর (i,j) ঘরে কী থাকে? আর M^k-এ?

প্রয়োগ

Matrix গুণের সংজ্ঞা:

(M2)ij=kMikMkj(M^2)_{ij} = \sum_{k} M_{ik} \cdot M_{kj}

M boolean (0/1) হলে, M[i][k] × M[k][j] = 1 তখনই যখন i → k এবং k → j — অর্থাৎ i থেকে j-তে k হয়ে একটা ২-দৈর্ঘ্যের path আছে।

যোগফলটা সব সম্ভাব্য মধ্যবর্তী k-এর উপর, তাই:

(M2)ij=i থেকে j-তে ঠিক ২ দৈর্ঘ্যের walk-এর সংখ্যা(M^2)_{ij} = i \text{ থেকে } j \text{-তে ঠিক ২ দৈর্ঘ্যের walk-এর সংখ্যা}

সাধারণভাবে:

(Mk)ij=i থেকে j-তে ঠিক k দৈর্ঘ্যের walk-এর সংখ্যা(M^k)_{ij} = i \text{ থেকে } j \text{-তে ঠিক } k \text{ দৈর্ঘ্যের walk-এর সংখ্যা}

লক্ষ্য করুন “walk”, “path” নয় — vertex পুনরাবৃত্তি অনুমোদিত।

প্রয়োগ:

১. ত্রিভুজ গোনা। trace(M³)/6 = undirected graph-এ ত্রিভুজের সংখ্যা।

কারণ (M³)ᵢᵢ = i থেকে i-তে ৩-দৈর্ঘ্যের walk = i জড়িত ত্রিভুজ। প্রতিটা ত্রিভুজ ৩টা vertex-এ গোনা হয়, আর প্রতিটা ২ দিকে — তাই 6 দিয়ে ভাগ।

Social network-এ ত্রিভুজ গোনা = clustering coefficient মাপা = “আমার বন্ধুরা কি একে অপরের বন্ধু?”

২. Reachability। Boolean semiring-এ (+ → OR, × → AND):

MM2Mn1=transitive closureM \vee M^2 \vee \cdots \vee M^{n-1} = \text{transitive closure}

কারণ n vertex-এর graph-এ যেকোনো path সর্বোচ্চ n−1 দৈর্ঘ্যের।

৩. Shortest path। Min-plus semiring-এ (+ → min, × → +):

(Mk)ij=সর্বোচ্চ k edge-এর shortest path(M^{\otimes k})_{ij} = \text{সর্বোচ্চ } k \text{ edge-এর shortest path}

এটাই 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-তে এখন শুধু দূরত্ব dd+1-এর vertex আছে (FIFO ধর্ম)।

দূরত্ব d+1-এর একটা vertex v নিন। তার একটা প্রতিবেশী u আছে যার দূরত্ব du প্রক্রিয়াকৃত হলে v আবিষ্কৃত হবে, আর dist[v] = dist[u] + 1 = d+1 ✓ ∎

DFS কেন ব্যর্থ:

DFS একটা শাখায় যতদূর সম্ভব যায়। তাই একটা vertex-এ পৌঁছাতে পারে একটা দীর্ঘ ঘুরপথে, আর সেটা “visited” চিহ্নিত করে দেয়। পরে ছোট path পাওয়া গেলেও আর update হয় না।

    a
   / \
  b   c
  |   |
  d   |
   \ /
    e

a → 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 হলে মোট কাজ:

i=0dbibd+1b1=O(bd)\sum_{i=0}^{d} b^i \approx \frac{b^{d+1}}{b-1} = O(b^d)

অর্থাৎ শেষ স্তরটাই খরচের প্রায় পুরোটা — পুনরাবৃত্তির overhead ধ্রুবক গুণক মাত্র।

IDDFS কেন ব্যবহার হয়:

BFSIDDFS
সময়O(b^d)O(b^d)
MemoryO(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 notationO, Θ, Ω আনুষ্ঠানিকভাবে, আর কখন এই বিমূর্তকরণ মিথ্যা বলে।

এরপর Level 1 — যেখানে আমরা bit-এর জগতে নামব।

আরও পড়ুন