Relations — নির্ভরতা, সমতুল্যতা আর ক্রম
Relations, Equivalence and Order
Equivalence relation জিনিসকে শ্রেণিতে ভাগ করে, partial order নির্ভরতা সাজায়, আর transitive closure বলে কোথা থেকে কোথায় পৌঁছানো যায় — তিনটাই প্রতিদিনের সফটওয়্যারে।
আগে এটা বুঝি
এই তিনটা প্রশ্ন একই ধরনের:
- “এই দুইটা commit কি একই বাগ ঠিক করে?”
- “এই package install করতে আগে কী কী লাগবে?”
- “এই microservice থেকে কি ঐ database-এ পৌঁছানো যায়?”
তিনটাই সম্পর্ক নিয়ে প্রশ্ন — জিনিসগুলো নিজে নয়, তাদের মধ্যকার সংযোগ নিয়ে।
গত লেসনে আমরা set শিখেছি: “কী কী আছে”। আজ শিখব relation: “কে কার সাথে কীভাবে যুক্ত”।
আর একটা চমৎকার ব্যাপার হলো — মাত্র চারটা ধর্ম (reflexive, symmetric,
antisymmetric, transitive) দিয়ে relation-দের যে শ্রেণিবিভাগ হয়,
সেটাই ব্যাখ্যা করে দেয় কেন == আর \< এত আলাদা আচরণ করে, কেন
dependency cycle বিপজ্জনক, আর কেন Git-এর history একটা DAG।
মূল ধারণা
Relation কী
গত লেসনে দেখেছি Cartesian product:
একটা relation হলো সেই product-এর একটা উপসেট:
(a, b) ∈ R লেখার বদলে সাধারণত লেখা হয় a R b।
উদাহরণ: A = {1, 2, 3}, আর R = “ছোট” সম্পর্ক:
R = { (1,2), (1,3), (2,3) }A × A-তে ৯টা জোড়া সম্ভব ছিল; R তার মধ্যে ৩টা বেছে নিয়েছে।
তিনভাবে একই জিনিস দেখা
একটা relation R on A = {a, b, c}:
১. জোড়ার set:
R = { (a,b), (b,c), (a,c) }২. Matrix:
a b c
a [0 1 1]
b [0 0 1]
c [0 0 0]৩. Directed graph:
a ──→ b ──→ c
└──────────↗তিনটা রূপই সমান দরকারি, আর কোনটা ব্যবহার করবেন তা নির্ভর করে কী হিসাব করতে চান তার উপর:
| প্রশ্ন | সেরা রূপ |
|---|---|
a R b কি? | matrix বা hash set — O(1) |
a-এর সব প্রতিবেশী? | adjacency list |
| Transitive closure? | matrix (Warshall) বা graph (DFS) |
| Memory কম চাই? | sparse হলে adjacency list |
Level 6-এ graph representation নিয়ে বিস্তারিত দেখব।
চারটা মৌলিক ধর্ম
R একটা relation on set A। চারটা প্রশ্ন করুন:
| ধর্ম | সংজ্ঞা | সাধারণ ভাষায় |
|---|---|---|
| Reflexive | ∀a (a R a) | প্রত্যেকে নিজের সাথে সম্পর্কিত |
| Symmetric | ∀a,b (a R b → b R a) | সম্পর্ক দুইমুখী |
| Antisymmetric | ∀a,b (a R b ∧ b R a → a = b) | দুইমুখী শুধু নিজের সাথে |
| Transitive | ∀a,b,c (a R b ∧ b R c → a R c) | চেইন সংক্ষিপ্ত হয় |
কয়েকটা পরিচিত relation যাচাই করি:
| Relation | Refl | Sym | Antisym | Trans |
|---|---|---|---|---|
= (সমান) | ✅ | ✅ | ✅ | ✅ |
≤ | ✅ | ❌ | ✅ | ✅ |
\< | ❌ | ❌ | ✅ | ✅ |
⊆ (subset) | ✅ | ❌ | ✅ | ✅ |
| “একই বাড়িতে থাকে” | ✅ | ✅ | ❌ | ✅ |
| “বন্ধু” (সামাজিক) | ❌ | ✅ | ❌ | ❌ |
| “সরাসরি নির্ভর করে” | ❌ | ❌ | ✅ | ❌ |
শেষ দুইটা লক্ষ্য করুন:
- বন্ধুত্ব transitive নয় — আমার বন্ধুর বন্ধু আমার বন্ধু নাও হতে পারে
- সরাসরি নির্ভরতা transitive নয় —
A → B → CমানেAসরাসরিC-র উপর নির্ভর করে না (যদিও পরোক্ষভাবে করে)
Equivalence relation — জিনিসকে শ্রেণিতে ভাগ করা
একটা relation equivalence relation যদি সেটা reflexive, symmetric আর transitive — তিনটাই হয়।
স্মরণসূত্র: RST।
উদাহরণ:
=— সবচেয়ে মৌলিক- “একই সপ্তাহের দিনে জন্ম”
a ≡ b (mod n)— modular সমতা- “একই connected component-এ” (graph-এ)
- “একই git commit থেকে এসেছে”
সবচেয়ে গুরুত্বপূর্ণ theorem
Theorem: একটা set-এর উপর equivalence relation আর সেই set-এর partition — এই দুইটা একই জিনিস।
Partition মানে set-কে এমন কিছু অ-খালি, পরস্পর-বিচ্ছিন্ন অংশে ভাগ করা যাদের union পুরো set।
কেন এরা একই? প্রতিটা equivalence relation একটা partition বানায়:
আর প্রতিটা partition একটা equivalence relation বানায়: “একই অংশে আছে”।
উদাহরণ — mod 3:
ℤ কে ভাগ করে তিনটা class-এ:
[0] = { …, −6, −3, 0, 3, 6, … }
[1] = { …, −5, −2, 1, 4, 7, … }
[2] = { …, −4, −1, 2, 5, 8, … }তিনটা class পরস্পর-বিচ্ছিন্ন, আর একসাথে পুরো ℤ ঢাকে। ✓
Partial order — নির্ভরতার গণিত
একটা relation partial order যদি সেটা reflexive, antisymmetric আর transitive হয়।
Symmetric-এর জায়গায় antisymmetric — এই একটা বদলই সবকিছু পাল্টে দেয়। Equivalence “একরকম” বলে; partial order “আগে-পরে” বলে।
সাধারণত ≼ চিহ্ন ব্যবহার হয়। উদাহরণ:
≤সংখ্যায়⊆set-এ- “ভাগ করে” (
a | b) পূর্ণসংখ্যায় - “task A, task B-এর আগে হতে হবে”
- Type hierarchy — “
A,B-এর subtype”
কেন “partial”?
কারণ কিছু জোড়া অতুলনীয় (incomparable) হতে পারে।
⊆ -এর উদাহরণ: {1,2} আর {2,3} — কোনোটাই অন্যটার subset নয়।
এরা তুলনীয় নয়।
যদি প্রতিটা জোড়া তুলনীয় হয়, তখন সেটা total order (বা linear
order)। সংখ্যায় ≤ total; set-এ ⊆ শুধু partial।
Hasse diagram
Partial order আঁকার সংক্ষিপ্ত উপায় — reflexive আর transitive edge বাদ দিয়ে শুধু “সরাসরি উপরে” সম্পর্কগুলো আঁকা।
{1,2,3}-এর power set, ⊆ অনুসারে:
{1,2,3}
/ | \
{1,2} {1,3} {2,3}
| \ / \ / |
| \/ \/ |
| /\ /\ |
{1} {2} {3}
\ | /
\ | /
∅∅ ⊆ {1,2,3} -ও সত্য, কিন্তু আঁকা হয়নি — transitivity থেকে বোঝা যায়।
ভেতরে কী ঘটছে
Transitive closure — “কোথায় পৌঁছানো যায়”
একটা relation R-এর transitive closure R⁺ হলো সবচেয়ে ছোট
transitive relation যা R-কে ধারণ করে।
সহজ ভাষায়: a R⁺ b মানে a থেকে b-তে এক বা একাধিক ধাপে
পৌঁছানো যায়।
উদাহরণ — package নির্ভরতা:
R (সরাসরি নির্ভরতা):
app → express
express→ body-parser
body-parser → bytes
R⁺ (সব নির্ভরতা):
app → express, body-parser, bytes
express → body-parser, bytes
body-parser → bytesnpm ls আপনাকে R দেখায়; npm ls --all কার্যত R⁺।
Warshall’s algorithm
Matrix রূপে transitive closure বের করার সবচেয়ে সরল উপায়:
def transitive_closure(M):
"""M: n×n boolean matrix. In-place-এ R⁺ বানায়।"""
n = len(M)
for k in range(n): # মধ্যবর্তী node
for i in range(n):
if not M[i][k]: # i → k না থাকলে বাদ
continue
for j in range(n):
if M[k][j]:
M[i][j] = True
return MO(n³)। যুক্তিটা সুন্দর: বাইরের loop-টা বলে “শুধু 0..k node-গুলো
মধ্যবর্তী হিসেবে ব্যবহার করলে কোথায় পৌঁছানো যায়” — আর এটা k-এর
উপর induction।
DAG আর topological sort
একটা directed graph DAG (Directed Acyclic Graph) যদি তাতে কোনো চক্র না থাকে।
মূল theorem:
একটা directed graph-এর topological sort আছে যদি এবং কেবল যদি সেটা একটা DAG।
কেন cycle থাকলে হয় না: যদি A → B → C → A চক্র থাকে, তাহলে
A-কে B-এর আগে, B-কে C-এর আগে, আর C-কে A-এর আগে রাখতে হবে।
অর্থাৎ A-কে A-এর আগে রাখতে হবে। অসম্ভব।
Kahn’s algorithm:
from collections import deque
def topo_sort(nodes, edges):
"""edges: dict node → নির্ভরশীলদের list (node আগে হতে হবে)"""
indeg = {n: 0 for n in nodes}
for u in nodes:
for v in edges.get(u, []):
indeg[v] += 1
ready = deque(n for n in nodes if indeg[n] == 0)
order = []
while ready:
u = ready.popleft()
order.append(u)
for v in edges.get(u, []):
indeg[v] -= 1
if indeg[v] == 0:
ready.append(v)
if len(order) != len(nodes):
cyclic = [n for n in nodes if indeg[n] > 0]
raise ValueError(f"চক্র আছে, এই node-গুলো জড়িত: {cyclic}")
return orderO(V + E)।
একাধিক বৈধ ক্রম — আর সেটাই parallelism
A → C
B → C
C → Dবৈধ ক্রম: A B C D বা B A C D — দুটোই ঠিক।
কারণ A আর B অতুলনীয় — তাদের মধ্যে কোনো নির্ভরতা নেই। তাই
তারা সমান্তরালে চালানো যায়।
- Makefile-এর নির্ভরতাকোন target আগে
- make -j8অতুলনীয় target সমান্তরালে
- Package manager (apt, cargo)install ক্রম
- Compiler-এর instruction schedulingস্বাধীন instruction পুনর্বিন্যাস
- CPU-র out-of-order executiondependency graph দেখে চালায়
- Memory orderingকোন write কোনটার আগে দৃশ্যমান
লক্ষ্য করুন — সবচেয়ে নিচের তিনটা স্তরে CPU নিজেই একটা partial order নিয়ে কাজ করছে। Out-of-order execution মানে CPU instruction-দের dependency graph বানিয়ে যেগুলো অতুলনীয় সেগুলো সমান্তরালে চালায়। Level 3 আর Level 11-এ আমরা সেটা দেখব।
Cycle detection — বাস্তবে সবচেয়ে দরকারি
Topological sort ব্যর্থ হলে চক্র আছে। কিন্তু কোন চক্র? ব্যবহারকারীকে সেটা বলতে পারা জরুরি।
def find_cycle(nodes, edges):
WHITE, GRAY, BLACK = 0, 1, 2
color = {n: WHITE for n in nodes}
parent = {}
def dfs(u):
color[u] = GRAY
for v in edges.get(u, []):
if color[v] == GRAY: # back edge → চক্র
cycle, cur = [v], u
while cur != v:
cycle.append(cur)
cur = parent[cur]
cycle.append(v)
return list(reversed(cycle))
if color[v] == WHITE:
parent[v] = u
found = dfs(v)
if found: return found
color[u] = BLACK
return None
for n in nodes:
if color[n] == WHITE:
c = dfs(n)
if c: return c
return Noneতিনটা রঙের অর্থ:
- WHITE — এখনো দেখা হয়নি
- GRAY — বর্তমান DFS path-এ আছে (recursion stack-এ)
- BLACK — সম্পূর্ণ প্রক্রিয়াকৃত
একটা GRAY node-এ পৌঁছানো মানে back edge — অর্থাৎ চক্র।
উদাহরণ
Union-Find — equivalence class ব্যবস্থাপনা
Equivalence relation-এর সবচেয়ে ব্যবহারিক data structure।
সমস্যা: গতিশীলভাবে জিনিস “একই দলে” ঘোষণা করা হচ্ছে, আর জিজ্ঞেস করা হচ্ছে “এই দুইটা কি একই দলে?”
class UnionFind:
def __init__(self, n):
self.parent = list(range(n)) # শুরুতে প্রত্যেকে নিজের class-এ
self.rank = [0] * n
self.count = n # কতগুলো আলাদা class
def find(self, x):
"""x-এর equivalence class-এর প্রতিনিধি"""
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x]) # path compression
return self.parent[x]
def union(self, a, b):
"""a আর b কে একই class-এ আনুন"""
ra, rb = self.find(a), self.find(b)
if ra == rb:
return False # আগে থেকেই একই class
if self.rank[ra] \< self.rank[rb]:
ra, rb = rb, ra
self.parent[rb] = ra
if self.rank[ra] == self.rank[rb]:
self.rank[ra] += 1
self.count -= 1
return True
def same(self, a, b):
return self.find(a) == self.find(b)তিনটা RST ধর্মই এখানে স্বতঃসিদ্ধভাবে রক্ষিত:
- Reflexive —
same(a, a)সবসময় True, কারণfind(a) == find(a) - Symmetric —
same(a,b)আরsame(b,a)একই হিসাব - Transitive —
aআরbএকই root-এ,bআরcএকই root-এ, তাইaআরc-ও একই root-এ
Data structure-টা গাণিতিক ধর্মগুলো গঠনগতভাবে নিশ্চিত করে — আলাদা করে যাচাই করতে হয় না।
Complexity: path compression + union by rank দিয়ে amortized
O(α(n)), যেখানে α হলো inverse Ackermann function। বাস্তব সব
input-এর জন্য α(n) ≤ 4 — কার্যত ধ্রুবক।
Version control-এর relation
Git-এর commit history একটা DAG — আর এটা বহু জিনিস ব্যাখ্যা করে।
C ─── D
/ \
A ─── B F (F একটা merge commit)
\ /
E ────R = “সরাসরি parent”। R⁺ = “পূর্বপুরুষ”।
এখন প্রশ্নগুলো relation-এর ভাষায়:
| Git প্রশ্ন | Relation প্রশ্ন |
|---|---|
git merge-base D E | সর্বনিম্ন সাধারণ পূর্বপুরুষ |
git log A..B | B-এর পূর্বপুরুষ বিয়োগ A-এর পূর্বপুরুষ |
| “এই commit কি release-এ আছে?” | commit R⁺ release? |
| Fast-forward সম্ভব? | একটা কি অন্যটার পূর্বপুরুষ? |
কেন cycle অসম্ভব: প্রতিটা commit-এর hash তার parent-এর hash অন্তর্ভুক্ত করে। একটা commit নিজের পূর্বপুরুষ হতে হলে তার hash নিজেকে অন্তর্ভুক্ত করতে হতো — যা hash function-এর সংজ্ঞায় অসম্ভব।
গঠনগতভাবে DAG নিশ্চিত — Git-কে আলাদা করে cycle check করতে হয় না। এটা cryptographic hash-এর একটা সুন্দর পার্শ্বফল। Level 10-এ আমরা দেখব blockchain-ও ঠিক এই নীতিতেই চলে।
Antisymmetry ভাঙলে কী হয়
ধরুন আপনি একটা task scheduler লিখছেন, আর কেউ ভুল করে লিখল:
build:
needs: [test]
test:
needs: [build]build ≼ test এবং test ≼ build, কিন্তু build ≠ test।
Antisymmetry ভেঙে গেল — এটা আর partial order নয়।
ফলাফল: topological sort অসম্ভব, indeg কখনো শূন্য হয় না, কোনো task
কখনো চলে না।
ভালো tool এই অবস্থাটা ধরে ফেলে আর চক্রটা দেখায়:
Error: circular dependency detected
build → test → buildখারাপ tool infinite loop-এ পড়ে বা deadlock করে।
নিজে চালিয়ে দেখুন
নিজের প্রজেক্টের dependency graph দেখুন
Node.js প্রজেক্টে:
# সরাসরি নির্ভরতা — এটাই R
cat package.json | python3 -c "import json,sys; d=json.load(sys.stdin); print(len(d.get('dependencies',{})))"
# transitive closure — এটাই R⁺
ls node_modules | wc -l
# গভীরতা দেখুন
npm ls --all --depth=99 2>/dev/null | head -40সাধারণত ৫টা সরাসরি dependency থেকে ৩০০+ package আসে।
R ছোট, R⁺ বিশাল।
Python-এ:
pip install pipdeptree
pipdeptree --graph-output dot > deps.dot
# চক্র থাকলে pipdeptree সতর্ক করে
pipdeptree --warn failযেকোনো ভাষায় — নিজে হিসাব করুন:
import json, subprocess
from collections import defaultdict, deque
def load_npm_tree():
out = subprocess.run(["npm", "ls", "--all", "--json"],
capture_output=True, text=True).stdout
return json.loads(out)
def flatten(node, edges, name=None):
deps = node.get("dependencies", {})
for child, sub in deps.items():
if name:
edges[name].add(child)
flatten(sub, edges, child)
return edges
edges = flatten(load_npm_tree(), defaultdict(set), "ROOT")
# transitive closure — BFS দিয়ে
def reachable(start):
seen, q = set(), deque([start])
while q:
u = q.popleft()
for v in edges.get(u, ()):
if v not in seen:
seen.add(v); q.append(v)
return seen
direct = edges["ROOT"]
allof = reachable("ROOT")
print(f"সরাসরি নির্ভরতা (R) : {len(direct)}")
print(f"সব নির্ভরতা (R⁺) : {len(allof)}")
print(f"বিস্তার অনুপাত : {len(allof)/max(len(direct),1):.1f}×")Transitive closure একটা বিমূর্ত ধারণা নয় — আপনার প্রতিটা প্রজেক্টের ভেতরে একটা বিশাল DAG আছে, আর তার আকার প্রায়ই বিস্ময়কর।
Cycle detection — নিজে ভাঙুন
from collections import deque
def topo_sort(edges):
nodes = set(edges) | {v for vs in edges.values() for v in vs}
indeg = {n: 0 for n in nodes}
for u in edges:
for v in edges[u]:
indeg[v] += 1
ready = deque(sorted(n for n in nodes if indeg[n] == 0))
order = []
while ready:
u = ready.popleft()
order.append(u)
for v in sorted(edges.get(u, [])):
indeg[v] -= 1
if indeg[v] == 0:
ready.append(v)
if len(order) != len(nodes):
stuck = sorted(n for n in nodes if indeg[n] > 0)
raise ValueError(f"চক্র আছে — আটকে থাকা node: {stuck}")
return order
# ── বৈধ DAG ─────────────────────────────────────────────────
build = {
"parse": ["typecheck"],
"typecheck": ["optimize"],
"optimize": ["codegen"],
"codegen": ["link"],
"lint": ["link"], # lint স্বাধীন — parse-এর সাথে সমান্তরাল
}
print("ক্রম:", topo_sort(build))
# ── কোনগুলো সমান্তরালে চালানো যায়? ──────────────────────────
def parallel_layers(edges):
nodes = set(edges) | {v for vs in edges.values() for v in vs}
indeg = {n: 0 for n in nodes}
for u in edges:
for v in edges[u]:
indeg[v] += 1
layers, remaining = [], dict(indeg)
while remaining:
layer = sorted(n for n, d in remaining.items() if d == 0)
if not layer:
raise ValueError("চক্র")
layers.append(layer)
for n in layer:
del remaining[n]
for v in edges.get(n, []):
if v in remaining:
remaining[v] -= 1
return layers
print("\nসমান্তরাল স্তর:")
for i, layer in enumerate(parallel_layers(build), 1):
print(f" ধাপ {i}: {layer}")
# ── এবার একটা চক্র ঢোকান ────────────────────────────────────
broken = dict(build)
broken["link"] = ["parse"] # ⚠ চক্র!
try:
topo_sort(broken)
except ValueError as e:
print("\nধরা পড়ল:", e)Output:
ক্রম: ['lint', 'parse', 'typecheck', 'optimize', 'codegen', 'link']
সমান্তরাল স্তর:
ধাপ 1: ['lint', 'parse']
ধাপ 2: ['typecheck']
ধাপ 3: ['optimize']
ধাপ 4: ['codegen']
ধাপ 5: ['link']
ধরা পড়ল: চক্র আছে — আটকে থাকা node: ['codegen', 'link', 'optimize', 'parse', 'typecheck']লক্ষ্য করুন ধাপ ১-এ lint আর parse একসাথে — কারণ তারা
অতুলনীয়। এই একটা তথ্যই make -j কে কাজ করতে দেয়।
এবার নিজে চেষ্টা করুন:
- এমন একটা graph বানান যেখানে ৩টা স্তরে মোট ১০টা task আছে — speedup কত হবে?
find_cycleযোগ করুন যা শুধু “চক্র আছে” না বলে কোন পথে চক্র সেটা দেখায়- এমন dependency লিখুন যাতে কোনো parallelism সম্ভব নয় (একটা সরলরেখা)
একটা partial order-এ antisymmetry ভাঙলে topological sort অসম্ভব হয়ে যায় — আর ভালো tooling সেটা স্পষ্ট বার্তা দিয়ে জানায়, চুপচাপ ঝুলে থাকে না।
নিজে বানান
Relation Analyzer
- একটা relation-এর চারটা ধর্ম যাচাই করার function লিখুন
- সেই ধর্ম দেখে relation-টা কী ধরনের তা শ্রেণিবদ্ধ করুন
- Equivalence relation হলে equivalence class বের করুন
- Partial order হলে topological sort করুন
- Transitive closure হিসাব করুন এবং তুলনা করুন
from itertools import product
from collections import defaultdict, deque
class Relation:
def __init__(self, elements, pairs):
self.E = list(elements)
self.R = set(pairs)
def holds(self, a, b):
return (a, b) in self.R
# ── চারটা ধর্ম ────────────────────────────────────────────
def is_reflexive(self):
return all(self.holds(a, a) for a in self.E)
def is_irreflexive(self):
return not any(self.holds(a, a) for a in self.E)
def is_symmetric(self):
return all(self.holds(b, a) for a, b in self.R)
def is_antisymmetric(self):
return all(a == b for a, b in self.R if self.holds(b, a))
def is_transitive(self):
return all(
self.holds(a, c)
for a, b in self.R
for c in self.E
if self.holds(b, c)
)
# ── শ্রেণিবিভাগ ───────────────────────────────────────────
def classify(self):
r, s, a, t = (self.is_reflexive(), self.is_symmetric(),
self.is_antisymmetric(), self.is_transitive())
kinds = []
if r and s and t: kinds.append("Equivalence relation")
if r and a and t: kinds.append("Partial order")
if r and a and t and self._is_total(): kinds.append("Total order")
if self.is_irreflexive() and a and t: kinds.append("Strict partial order")
if not kinds: kinds.append("(কোনো বিশেষ শ্রেণি নয়)")
return kinds
def _is_total(self):
return all(self.holds(a, b) or self.holds(b, a)
for a, b in product(self.E, repeat=2))
# ── Equivalence class ────────────────────────────────────
def classes(self):
if not (self.is_reflexive() and self.is_symmetric() and self.is_transitive()):
raise ValueError("equivalence relation নয়")
seen, out = set(), []
for a in self.E:
if a in seen: continue
cls = sorted(b for b in self.E if self.holds(a, b), key=str)
seen.update(cls)
out.append(cls)
return out
# ── Transitive closure (Warshall) ────────────────────────
def closure(self):
idx = {e: i for i, e in enumerate(self.E)}
n = len(self.E)
M = [[False]*n for _ in range(n)]
for a, b in self.R:
M[idx[a]][idx[b]] = True
for k in range(n):
for i in range(n):
if not M[i][k]: continue
for j in range(n):
if M[k][j]: M[i][j] = True
pairs = {(self.E[i], self.E[j])
for i in range(n) for j in range(n) if M[i][j]}
return Relation(self.E, pairs)
# ── Topological sort ─────────────────────────────────────
def topo(self):
adj = defaultdict(list)
indeg = {e: 0 for e in self.E}
for a, b in self.R:
if a == b: continue # reflexive edge উপেক্ষা
adj[a].append(b); indeg[b] += 1
q = deque(sorted((e for e in self.E if indeg[e] == 0), key=str))
order = []
while q:
u = q.popleft(); order.append(u)
for v in sorted(adj[u], key=str):
indeg[v] -= 1
if indeg[v] == 0: q.append(v)
if len(order) != len(self.E):
raise ValueError("চক্র আছে — topological sort অসম্ভব")
return order
def report(self, name):
print(f"\n── {name} " + "─" * max(0, 46 - len(name)))
print(f" reflexive : {self.is_reflexive()}")
print(f" irreflexive : {self.is_irreflexive()}")
print(f" symmetric : {self.is_symmetric()}")
print(f" antisymmetric : {self.is_antisymmetric()}")
print(f" transitive : {self.is_transitive()}")
print(f" → {', '.join(self.classify())}")
try:
print(f" equivalence class: {self.classes()}")
except ValueError:
pass
try:
print(f" topological order: {self.topo()}")
except ValueError as e:
print(f" topo: {e}")
c = self.closure()
print(f" |R| = {len(self.R)}, |R⁺| = {len(c.R)}")
# ── উদাহরণ ──────────────────────────────────────────────────
E = [1, 2, 3, 4]
Relation(E, {(a, b) for a in E for b in E if a == b}
).report("সমতা (=)")
Relation(E, {(a, b) for a in E for b in E if a <= b}
).report("কম-বা-সমান (≤)")
Relation(E, {(a, b) for a in E for b in E if a \< b}
).report("কম (\<)")
Relation(E, {(a, b) for a in E for b in E if a % 2 == b % 2}
).report("একই জোড়/বিজোড় (mod 2)")
Relation(E, {(a, b) for a in E for b in E if b % a == 0}
).report("ভাগ করে (a | b)")
# একটা নির্ভরতা graph
tasks = ["parse", "typecheck", "optimize", "codegen", "lint", "link"]
deps = {("parse","typecheck"), ("typecheck","optimize"),
("optimize","codegen"), ("codegen","link"), ("lint","link")}
Relation(tasks, deps).report("build নির্ভরতা")
# চক্র সহ
Relation(tasks, deps | {("link","parse")}).report("চক্র সহ")নিজে বাড়ান:
is_total()কে আরো দক্ষ করুন — বর্তমানেO(n²)জোড়া দেখে- Hasse diagram ছাপুন — reflexive আর transitive edge বাদ দিয়ে
closure()কে DFS দিয়ে লিখুন এবং sparse graph-এ Warshall-এর সাথে সময় তুলনা করুন- একটা relation দিলে সেটা equivalence relation বানাতে সবচেয়ে কম কতগুলো জোড়া যোগ করতে হবে বের করুন (reflexive-symmetric-transitive closure)
- Union-Find দিয়ে
classes()লিখুন এবং বড় input-এ তুলনা করুন
বাস্তব সিস্টেমে
Relation কোথায় কোথায়
Build system। Make, Bazel, Gradle, Ninja — প্রতিটার কেন্দ্রে একটা
DAG আর topological sort। make -j8 কাজ করে কারণ partial order-এ
অতুলনীয় target সমান্তরালে চালানো যায়।
Package manager। apt, cargo, npm, pip — dependency
resolution মানে একটা DAG বানিয়ে topological sort করা। চক্র থাকলে
error, না থাকলে install ক্রম।
Spreadsheet। Excel-এর প্রতিটা cell একটা node, প্রতিটা formula একটা edge। Recalculation হলো topological sort। আর “circular reference” error হলো cycle detection।
Compiler। Instruction scheduling — কোন instruction কোনটার উপর নির্ভর করে তার DAG বানিয়ে পুনর্বিন্যাস। Register allocation-এ interference graph। Dataflow analysis-এ partial order-এর উপর fixed point।
CPU-র out-of-order execution। Hardware নিজেই একটা dependency graph রাখে (reorder buffer + reservation station) আর অতুলনীয় instruction সমান্তরালে চালায়। Level 3-এ দেখব।
Type system। Subtyping একটা partial order। Dog ≼ Animal,
কিন্তু Dog আর Cat অতুলনীয়। Type checker এই order-এ চলাচল করে।
Git। Commit DAG। merge-base হলো সর্বনিম্ন সাধারণ পূর্বপুরুষ।
rebase হলো DAG-এর একটা অংশ পুনর্লিখন।
Kubernetes। Init container-এর ক্রম, resource-এর owner reference — সবই DAG। Garbage collection owner reference-এর transitive closure ধরে চলে।
Distributed system-এ causality। Lamport-এর happens-before
(→) একটা strict partial order। দুইটা event অতুলনীয় মানে তারা
concurrent। Level 9-এ এটাই vector clock-এর ভিত্তি।
Memory model। C++ আর Java-র memory model বলে কোন write কোন read-এর কাছে দৃশ্যমান — সেটা একটা partial order (happens-before)। Level 11-এ দেখব।
যে ভুলগুলো সবাই করে
“'বন্ধুর বন্ধু আমার বন্ধু' — তাই বন্ধুত্ব transitive।”
সামাজিক বন্ধুত্ব transitive নয়, আর এই ভুল ধারণাটা সফটওয়্যারে বাস্তব bug তৈরি করে।
Access control-এ ভাবুন: A বিশ্বাস করে B-কে, B বিশ্বাস করে C-কে।
তাহলে A কি C-কে বিশ্বাস করে?
- যদি হ্যাঁ ধরেন — তাহলে transitive trust, আর একটা compromised account থেকে পুরো নেটওয়ার্কে ছড়াতে পারে
- যদি না ধরেন — তাহলে প্রতিটা সম্পর্ক আলাদা করে যাচাই করতে হয়
PGP-র web of trust ইচ্ছাকৃতভাবে সীমিত transitivity ব্যবহার করে — trust depth-এর একটা সীমা থাকে। কারণ পূর্ণ transitive closure নিলে একটা খারাপ key পুরো নেটওয়ার্ক দূষিত করত।
নকশাগত প্রশ্ন: আপনার সিস্টেমের relation-গুলো transitive হওয়া উচিত কি না — এটা একটা সচেতন সিদ্ধান্ত হতে হবে, ঘটনাচক্রে নয়।
“Partial order মানে 'অসম্পূর্ণ' বা 'ত্রুটিপূর্ণ' order।”
“Partial” মানে অসম্পূর্ণ নয় — মানে কিছু জোড়া তুলনীয় নয়। আর সেটা প্রায়ই একটা সুবিধা, ত্রুটি নয়।
A আর B-এর মধ্যে নির্ভরতা না থাকা মানে তারা সমান্তরালে চলতে পারে।
Total order আরোপ করলে সেই সুযোগটা নষ্ট হতো।
Partial order: A ∥ B → C দুইটা core ব্যবহার করা যায়
Total order: A → B → C সবসময় একটা coreDistributed system-এ এটা আরো গুরুত্বপূর্ণ: দুইটা event concurrent হওয়া মানে তাদের মধ্যে কোনো causal সম্পর্ক নেই, তাই আলাদা node-এ স্বাধীনভাবে ঘটতে পারে। জোর করে total order আরোপ করা (যেমন একটা global sequencer দিয়ে) কাজ করে, কিন্তু সেটাই bottleneck হয়ে যায়।
Partial order = concurrency-র গাণিতিক রূপ।
“Transitive closure হিসাব করা সবসময় ভালো — সব তথ্য পাওয়া যায়।”
প্রায়ই না, দুইটা কারণে।
১. আকার বিস্ফোরিত হয়। n node-এর একটা chain-এ R-এ n−1 টা
edge, কিন্তু R⁺-এ n(n−1)/2 টা। ১০০০ node মানে ৯৯৯ থেকে ৫ লক্ষ।
২. তথ্য হারায়। R⁺ বলে “পৌঁছানো যায়”, কিন্তু “সরাসরি” আর
“পরোক্ষ”-এর পার্থক্য মুছে দেয়।
R : app → express → body-parser
R⁺ : app → express, app → body-parser, express → body-parserR⁺ দেখে আপনি বুঝবেন না body-parser একটা transitive dependency —
মনে হবে সরাসরি। অথচ এই পার্থক্যটাই গুরুত্বপূর্ণ: সরাসরি dependency
আপনি বেছে নিয়েছেন, transitive-টা আপনার উপর চাপিয়ে দেওয়া হয়েছে।
উল্টো operation-টাও দরকারি: transitive reduction — সবচেয়ে কম edge রেখে একই reachability। DAG-এ এটা অনন্য, আর Hasse diagram আসলে সেটাই।
npm ls (reduction) আর npm ls --all (closure) — দুটোই দরকার,
ভিন্ন প্রশ্নের জন্য।
“`==` যেকোনোভাবে define করা যায়, এটা শুধু একটা convention।”
না — == কে equivalence relation হতেই হবে, নাহলে ভাষার
data structure ভেঙে পড়ে।
তিনটা ধর্ম না মানলে কী হয়:
Reflexive ভাঙলে (a != a):
float('nan') == float('nan') # Falseএটাই কেন NaN কে list.index() দিয়ে খুঁজে পাওয়া যায় না, আর কেন
NaN-যুক্ত sort অদ্ভুত আচরণ করে।
Symmetric ভাঙলে (a == b কিন্তু b != a):
class Weird:
def __eq__(self, o): return isinstance(o, int)
Weird() == 5 # True
5 == Weird() # Python fallback করে, কিন্তু সবসময় নয়in, dict lookup — সব অপ্রত্যাশিত আচরণ করবে।
Transitive ভাঙলে (a == b, b == c, কিন্তু a != c):
set -এ কোনটা রাখা হবে তা insertion order-এর উপর নির্ভর করবে।
একই ডেটা, ভিন্ন ফল।
Java-র Comparable documentation স্পষ্টভাবে বলে দেয় contract-টা,
আর সেটা ভাঙলে TreeMap-এর আচরণ undefined। Rust-এ Eq trait
আক্ষরিকভাবে বলে “এটা একটা equivalence relation” (আর PartialEq
সেই গ্যারান্টি দেয় না — এজন্যই float শুধু PartialEq)।
বুঝেছেন কি না দেখুন
1A = {1,2,3,4,5,6} -এর উপর relation R = {(a,b) : a আর b -এর একই ভাজক সংখ্যা}। এটা কি equivalence relation? হলে equivalence class
বের করুন।
প্রয়োগ
A = {1,2,3,4,5,6} -এর উপর relation R = {(a,b) : a আর b -এর একই ভাজক সংখ্যা}। এটা কি equivalence relation? হলে equivalence class
বের করুন।প্রথমে ভাজক সংখ্যা গুনি:
| n | ভাজক | সংখ্যা |
|---|---|---|
| 1 | 1 | 1 |
| 2 | 1,2 | 2 |
| 3 | 1,3 | 2 |
| 4 | 1,2,4 | 3 |
| 5 | 1,5 | 2 |
| 6 | 1,2,3,6 | 4 |
তিনটা ধর্ম যাচাই:
- Reflexive —
a-এর ভাজক সংখ্যাa-এর ভাজক সংখ্যার সমান ✓ - Symmetric — সমতা symmetric ✓
- Transitive — সমতা transitive ✓
তিনটাই ✓ — হ্যাঁ, equivalence relation।
Equivalence class:
[1] = {1} — ১টা ভাজক
[2] = {2, 3, 5} — ২টা ভাজক (মৌলিক সংখ্যা)
[4] = {4} — ৩টা ভাজক
[6] = {6} — ৪টা ভাজকPartition যাচাই: চারটা class পরস্পর-বিচ্ছিন্ন, আর union = {1,2,3,4,5,6} ✓
একটা সাধারণ pattern: যেকোনো function f : A → B স্বয়ংক্রিয়ভাবে
একটা equivalence relation বানায়:
একে বলে kernel of f। এখানে f(n) = ভাজক সংখ্যা।
এটা প্রোগ্রামিং-এ সর্বত্র:
from itertools import groupby
data = sorted(items, key=f)
for key, group in groupby(data, key=f):
... # প্রতিটা group একটা equivalence classGROUP BY clause আক্ষরিকভাবে একটা equivalence relation দিয়ে
partition করছে। SQL-এর নকশাটা এই গণিতের সরাসরি প্রতিফলন।
আর লক্ষ্য করুন [2] = {2,3,5} ঠিক মৌলিক সংখ্যাগুলো — কারণ
“ঠিক দুইটা ভাজক” হলো মৌলিকতার সংজ্ঞা।
2কেন \< (কঠোরভাবে কম) partial order নয়, যদিও সেটা antisymmetric আর
transitive?
যুক্তি
\< (কঠোরভাবে কম) partial order নয়, যদিও সেটা antisymmetric আর
transitive?কারণ \< reflexive নয় — a \< a কখনো সত্য নয়।
Partial order-এর সংজ্ঞায় reflexive লাগে, তাই \< সেই সংজ্ঞা মানে না।
\< কে বলে strict partial order — irreflexive, transitive,
আর (স্বয়ংক্রিয়ভাবে) asymmetric।
দুইটার সম্পর্ক একের সাথে এক:
| থেকে | তে | কীভাবে |
|---|---|---|
≤ (partial order) | \< (strict) | a \< b ⟺ a ≤ b ∧ a ≠ b |
\< (strict) | ≤ | a ≤ b ⟺ a \< b ∨ a = b |
তাই কোনটা ব্যবহার করবেন সেটা সুবিধার প্রশ্ন, গভীর পার্থক্য নয়।
কিন্তু প্রোগ্রামিং-এ পার্থক্যটা বাস্তব।
C++-এর std::sort একটা strict weak ordering দাবি করে, অর্থাৎ
comparator-টা irreflexive হতে হবে — cmp(a, a) অবশ্যই false।
এই নিয়ম ভাঙলে কী হয়:
// ভুল — reflexive comparator
std::sort(v.begin(), v.end(), [](int a, int b) { return a <= b; });a <= a → true। libstdc++-এর introsort তখন array-র সীমার বাইরে
পড়তে পারে — segfault বা memory corruption, কোনো warning ছাড়াই।
কারণ sort-এর ভেতরের partition loop while (cmp(*i, pivot)) ++i;
এমন লেখা যেখানে pivot নিজেই sentinel হিসেবে কাজ করে। Comparator
reflexive হলে pivot-এ এসে loop থামে না।
একটা গাণিতিক ধর্ম লঙ্ঘন → memory safety bug। এটাই কারণ Level 0-এ এই ধর্মগুলো শেখা।
3আপনার একটা microservice আর্কিটেকচার আছে যেখানে service-রা পরস্পরকে
call করে। কোন relation ধর্মটা আপনি জোর করে আরোপ করতে চাইবেন,
আর কেন?
ডিজাইন
Acyclicity — অর্থাৎ call graph-টা একটা DAG হতে হবে।
সমতুল্যভাবে: “A, B-কে call করে” relation-এর transitive closure antisymmetric হতে হবে।
চক্র থাকলে যেসব সমস্যা:
| সমস্যা | ব্যাখ্যা |
|---|---|
| Cascading failure | A → B → A — একটা ধীর হলে দুটোই আটকে যায় |
| Deploy ক্রম নেই | কোনটা আগে deploy করবেন? |
| Deadlock | Synchronous call-এ resource waiting cycle |
| Retry storm | চক্রে retry ঘুরতে থাকে, load বহুগুণ |
| বোঝা কঠিন | কোনো “স্তর” নেই, সব সবার সাথে যুক্ত |
২০২১-এর Facebook outage (৬ ঘণ্টা, বিশ্বব্যাপী) ছিল আংশিকভাবে এই শ্রেণির সমস্যা: BGP configuration ভুলে DNS server অগম্য হলো, কিন্তু ভেতরের tool-গুলো ঠিক করার জন্য সেই DNS-এর উপরই নির্ভরশীল ছিল। পুনরুদ্ধারের পথে একটা চক্র।
কীভাবে আরোপ করবেন:
১. Layered architecture — service-দের স্তরে ভাগ করুন, আর নিয়ম করুন call শুধু নিচের দিকে যাবে:
Layer 3: API gateway, BFF
Layer 2: domain service
Layer 1: platform service (auth, config)
Layer 0: datastoreLayer সংখ্যা একটা total order, আর “শুধু নিচে call” নিয়মটা গঠনগতভাবে চক্র অসম্ভব করে দেয় — ঠিক যেমন lock ordering deadlock প্রতিরোধ করে।
২. CI-তে যাচাই — service manifest থেকে call graph বানিয়ে প্রতিটা PR-এ cycle detection চালান। চক্র ঢুকলে build fail।
৩. চক্র সত্যিই লাগলে asynchronous করুন — direct call-এর বদলে event/queue ব্যবহার করুন। তখন call graph-এ edge থাকে না; নির্ভরতাটা সময়ে বিচ্ছিন্ন হয়ে যায়।
সমস্যা: A --sync--> B --sync--> A চক্র
সমাধান: A --sync--> B
B --event--> queue --> A চক্র নেই৪. যেখানে চক্র অনিবার্য, সেখানে circuit breaker — চক্র ভাঙা না গেলে অন্তত cascading failure আটকান।
একটা সূক্ষ্মতা: পুরোপুরি acyclic architecture সবসময় ব্যবহারিক নয়। লক্ষ্য হওয়া উচিত — চক্র থাকলে সেটা জেনেশুনে, নথিভুক্ত, আর async দিয়ে ভাঙা। দুর্ঘটনাক্রমে চক্র তৈরি হওয়াটাই আসল বিপদ।
Level 9 (distributed systems) আর Level 12 (cloud)-এ আমরা এগুলো বিস্তারিত দেখব।
4n node-এর একটা DAG-এ transitive closure-এ সর্বোচ্চ কতগুলো edge
থাকতে পারে? কোন গঠনে সেটা হয়?
প্রয়োগ
n node-এর একটা DAG-এ transitive closure-এ সর্বোচ্চ কতগুলো edge
থাকতে পারে? কোন গঠনে সেটা হয়?সর্বোচ্চ:
কেন এটাই সীমা: DAG-এ কোনো চক্র নেই, তাই যেকোনো জোড়া (a, b)-র
জন্য সর্বোচ্চ একটা দিকে পথ থাকতে পারে (দুইদিকেই থাকলে চক্র হতো)।
আর n node-এর জোড়া সংখ্যা C(n,2)।
কোন গঠনে অর্জিত হয়: একটা total order — সরলরেখা।
1 → 2 → 3 → 4 → 5মূল edge: ৪টা। Transitive closure: (1,2),(1,3),(1,4),(1,5),(2,3), (2,4),(2,5),(3,4),(3,5),(4,5) = ১০টা = C(5,2) ✓
অনুপাতটা লক্ষ্য করুন:
| n | R (chain) | R⁺ | বৃদ্ধি |
|---|---|---|---|
| 5 | 4 | 10 | 2.5× |
| 100 | 99 | 4,950 | 50× |
| 1,000 | 999 | 499,500 | 500× |
| 10,000 | 9,999 | ~50M | 5,000× |
O(n) থেকে O(n²) — এই কারণেই transitive closure precompute করা
প্রায়ই খারাপ ধারণা।
বাস্তব প্রভাব — SQL-এ ancestor query:
-- বিকল্প ১: closure table (precomputed R⁺)
CREATE TABLE ancestor (descendant int, ancestor int, depth int);
-- দ্রুত query, কিন্তু O(n²) storage আর প্রতিটা insert-এ O(depth) row
-- বিকল্প ২: recursive CTE (চাহিদামতো হিসাব)
WITH RECURSIVE anc AS (
SELECT parent_id FROM node WHERE id = 42
UNION ALL
SELECT n.parent_id FROM node n JOIN anc a ON n.id = a.parent_id
)
SELECT * FROM anc;
-- O(1) storage overhead, কিন্তু প্রতিটা query-তে traversalTrade-off: closure table read-heavy workload-এ ভালো (read দ্রুত, write ধীর), recursive CTE write-heavy-তে ভালো।
আর তৃতীয় একটা বিকল্প আছে — nested set model বা materialized
path — যা O(n) storage-এ O(1) ancestor check দেয়, কিন্তু
tree পুনর্বিন্যাস ব্যয়বহুল করে।
Level 8-এ আমরা hierarchical data-র এই তিনটা মডেল তুলনা করব।
5Distributed system-এ “happens-before” (→) একটা strict partial order।
দুইটা event a আর b অতুলনীয় হওয়ার মানে কী, আর এর ব্যবহারিক
তাৎপর্য কী?
যুক্তি
→) একটা strict partial order।
দুইটা event a আর b অতুলনীয় হওয়ার মানে কী, আর এর ব্যবহারিক
তাৎপর্য কী?a আর b অতুলনীয় মানে না a → b, না b → a — অর্থাৎ তারা
concurrent।
Lamport-এর সংজ্ঞায় a → b হয় তিনভাবে:
- একই process-এ
a,b-এর আগে ঘটেছে aএকটা message পাঠানো,bসেটা গ্রহণ- Transitivity
কোনোটাই না হলে দুইটা event concurrent।
ব্যবহারিক তাৎপর্য — চারটা:
১. কোনো “সঠিক” ক্রম নেই। দুইজন পর্যবেক্ষক দুই ক্রমে দেখতে পারে, আর দুজনেই ঠিক। “কোনটা আগে হয়েছিল” প্রশ্নটার উত্তর নেই।
২. Wall clock দিয়ে সমাধান হয় না। দুইটা machine-এর ঘড়ি NTP দিয়ে sync করলেও কয়েক মিলিসেকেন্ড পার্থক্য থাকে। তাই timestamp দেখে “এটা আগে” বলা অনির্ভরযোগ্য।
Google Spanner এই সমস্যা সমাধান করে TrueTime দিয়ে — atomic clock
আর GPS ব্যবহার করে uncertainty interval (ε) হিসাব করে, তারপর
commit-এর সময় ε অপেক্ষা করে যাতে ক্রম নিশ্চিত হয়। হার্ডওয়্যার
দিয়ে গণিতের সমস্যা সমাধান — ব্যয়বহুল কিন্তু কার্যকর।
৩. Conflict সম্ভব। দুইটা concurrent write একই key-তে হলে কোনটা রাখবেন? সিদ্ধান্তগুলো:
| কৌশল | আচরণ |
|---|---|
| Last-write-wins | timestamp দেখে — ডেটা হারায় |
| Multi-value | দুটোই রাখুন, application সিদ্ধান্ত নিক (Dynamo) |
| CRDT | merge function এমন যে conflict-ই হয় না |
৪. Vector clock এই order-টা ধরে রাখে। Lamport timestamp (একটা সংখ্যা) শুধু total order দেয় — concurrent আর causal আলাদা করতে পারে না। Vector clock (প্রতি node-এ একটা counter) partial order-টাই encode করে:
V(a) \< V(b) → a happens-before b
V(b) \< V(a) → b happens-before a
কোনোটাই না → concurrentগভীর পয়েন্ট: distributed system-এ event-দের প্রকৃত কাঠামো একটা partial order। যেকোনো total order (একটা global log, একটা sequence number) সেই কাঠামোর উপর একটা কৃত্রিম আরোপ — যা সুবিধাজনক কিন্তু coordination-এর দাম দিয়ে কেনা।
আর সেই coordination-ই latency আর availability-র উপর সীমা টানে — CAP theorem-এর গোড়ায় এই গাণিতিক সত্যটাই আছে।
Level 9-এ আমরা vector clock নিজে implement করব আর Raft-এর safety proof-এ এই partial order ব্যবহার করব।
এরপর কী
Relation আমাদের শিখিয়েছে জিনিসগুলোর মধ্যকার সম্পর্ক কীভাবে গঠন করে।
পরের লেসনে আসছে functions — যা relation-এরই একটা বিশেষ শ্রেণি (প্রতিটা input-এর ঠিক একটা output)। সেখান থেকে আসবে injective, surjective, bijective — আর সেই ধারণাগুলো সরাসরি ব্যাখ্যা করবে কেন hash collision অনিবার্য, কেন lossless compression সব ফাইল ছোট করতে পারে না, আর কেন একটা encoding “invertible” হওয়া মানে কী।
আরও পড়ুন
- Discrete Mathematics and Its Applications, §9.1–9.6 — Kenneth Rosen
- A Relational Model of Data for Large Shared Data Banks — E. F. Codd (1970) · যে পেপার থেকে আধুনিক database-এর জন্ম