এই পর্বে যা শিখবেন
- Strongly connected component আর condensation DAG-এর সংজ্ঞা, আর condensation কেন কখনো cycle রাখতে পারে না তার প্রমাণ
- Kosaraju-র দুই পাস হাতে trace করা, আর finish-এর ক্রম কেন কাজ করে তা যুক্তি দিয়ে বলা
- Tarjan-এর অ্যালগরিদমে disc, low আর stack ধাপে ধাপে চালানো, C++ ও Python-এ নিজে লেখা
- 2-SAT-কে implication graph-এ রূপান্তর করে SCC দিয়ে সমাধান করা আর মান বের করা
- Bridge আর articulation point-এর low-link শর্ত, root-এর বিশেষ নিয়ম আর multi-edge সামলানো, O(V + E)-তে
১ · একমুখী রাস্তার শহর — গিয়ে কি ফিরে আসা যায়?
পুরান ঢাকার অনেক গলি একমুখী। রিকশা নিয়ে আপনি এক মোড় থেকে আরেক মোড়ে সহজেই চলে গেলেন, কিন্তু ফেরার সময় দেখলেন সেই গলিতে উল্টো দিকে ঢোকা নিষেধ। তখন প্রশ্ন: অন্য গলি ঘুরে ফেরা কি আদৌ সম্ভব? ট্রাফিক বিভাগের কাছে প্রশ্নটি আরও বড়: শহরের কোন কোন মোড়ের দল এমন, যাদের ভেতরে যেকোনো মোড় থেকে যেকোনো মোড়ে যাওয়া আর ফিরে আসা যায়?
গ্রাফের ভাষায় মোড়গুলো vertex, একমুখী রাস্তাগুলো দিকসহ edge — এমন গ্রাফকে বলে directed graph. u থেকে edge ধরে ধরে v-তে পৌঁছানো গেলে বলি v, u থেকে reachable. Undirected গ্রাফে (পর্ব ৩৯) এই সম্পর্ক দুদিকেই একসাথে সত্য, কিন্তু directed গ্রাফে নয়: u থেকে v-তে যাওয়া গেলেও v থেকে u-তে ফেরার পথ না-ও থাকতে পারে।
u আর v পরস্পর reachable (লিখি u ↔ v), যদি u থেকে v-তে আর v থেকে u-তে — দুদিকেই পথ থাকে। যে vertex-গুলো একে অন্যের সাথে পরস্পর reachable, তাদের সবচেয়ে বড় দলটিকে বলে strongly connected component (SCC). পুরো গ্রাফ একটিই SCC হলে গ্রাফটি strongly connected.
“↔” সম্পর্কটি একটি equivalence relation: প্রত্যেকে নিজের সাথে পরস্পর reachable (শূন্য দৈর্ঘ্যের পথ), সম্পর্কটি প্রতিসম, আর u ↔ v ও v ↔ w হলে দুটি পথ জুড়ে u ↔ w. তাই পর্ব ৩৩-এর DSU-এর মতোই vertex-গুলো পরস্পর-বিচ্ছিন্ন দলে ভাগ হয় — প্রতিটি vertex ঠিক একটি SCC-তে, আর যার কোনো সঙ্গী নেই, সে একাই একটি SCC.
এই পর্ব জুড়ে আমাদের সঙ্গী একটি কল্পিত মানচিত্র: সাতটি মোড় (0 থেকে 6) আর নয়টি একমুখী রাস্তা।
দেখুন: 1 থেকে 3-এ যাওয়া যায় (সরাসরি রাস্তা), কিন্তু 3 থেকে আর কখনো 1-এ ফেরা যায় না — B থেকে বেরোনোর একমাত্র রাস্তা 5 → 6, আর 6 একটি কানাগলি। তাই 1 আর 3 আলাদা SCC-তে। প্রতিটি vertex থেকে আলাদা করে BFS বা DFS চালিয়ে কে কার কাছে যায় তা দেখে SCC বের করা যায়, কিন্তু তাতে সময় লাগে O(V · (V + E)). এই পর্বের দুটি অ্যালগরিদম — Kosaraju আর Tarjan — একই কাজ করে মাত্র O(V + E)-এ, অর্থাৎ গ্রাফটি একবার-দুবার পড়তে যতক্ষণ লাগে ততক্ষণে। দুটিই দাঁড়িয়ে আছে পর্ব ৪১-এর DFS-এর ওপর।
পুরো পর্বে গ্রাফ রাখা হবে adjacency list-এ: প্রতিটি vertex-এর জন্য তার বের-হওয়া রাস্তাগুলোর তালিকা। আমাদের মানচিত্রে: 0: 1, 1: 2, 3, 2: 0, 6, 3: 4, 4: 5, 5: 3, 6, 6: (কিছু নেই). DFS প্রতিবেশীদের এই ক্রমেই দেখবে — ডায়াগ্রামের প্রতিটি ধাপ এই ক্রমের ওপর নির্ভর করে।
২ · Condensation — প্রতিটি SCC-কে একটি বিন্দু বানালে একটি DAG
প্রতিটি SCC-কে একটি বড় বিন্দুতে সংকুচিত করুন। দুই SCC X আর Y-এর মধ্যে X → Y edge রাখুন, যদি মূল গ্রাফে X-এর কোনো vertex থেকে Y-এর কোনো vertex-এ রাস্তা থাকে। যা পাওয়া গেল, তার নাম condensation graph. আমাদের মানচিত্রে:
A = {0,1,2} ───────────→ B = {3,4,5}
│ (1→3) │
│ │ (5→6)
└──────(2→6)──→ C = {6} ←┘
দাবি: যেকোনো directed গ্রাফের condensation একটি DAG (directed acyclic graph). প্রমাণ: ধরুন condensation-এ একটি cycle আছে: X1 → X2 → … → Xk → X1, k ≥ 2. তাহলে X1-এর যেকোনো vertex থেকে cycle ধরে X2-এর যেকোনো vertex-এ যাওয়া যায় (প্রতিটি SCC-র ভেতরে সবাই সবার কাছে পৌঁছায়), আর cycle-এর বাকি অংশ ধরে আবার X1-এ ফেরা যায়। অর্থাৎ X1 আর X2-এর vertex-গুলো পরস্পর reachable — তারা একই SCC-তে থাকার কথা। কিন্তু SCC মানে সবচেয়ে বড় দল, তাই এটি স্ববিরোধ। ∎
এই ছোট প্রমাণটির ফল বড়। যেকোনো directed গ্রাফ আসলে দুই স্তরের: ভেতরে কয়েকটি “ঘন” SCC, যার ভেতরে সবাই সবার কাছে যায়; আর বাইরে তাদের মধ্যে একটি DAG, যেখানে ফেরার পথ নেই। DAG-এর ওপর topological sort (পর্ব ৪২) চলে, DP চলে, সবচেয়ে লম্বা পথও সহজে বের হয়। তাই অনেক কঠিন directed-গ্রাফ সমস্যার সমাধান একই ছাঁচে: SCC বের করুন → condensation বানান → DAG-এ কাজ করুন। যেমন, “কোনো মোড় থেকে শুরু করে সর্বোচ্চ কয়টি আলাদা দোকানে যাওয়া যায়, একই মোড়ে বারবার আসা চলবে” — একটি SCC-তে ঢুকলে তার সব দোকান ঘোরা যায়, তাই প্রতিটি SCC-র দোকান যোগ করে condensation DAG-এ সবচেয়ে ভারী পথ খুঁজলেই উত্তর।
Condensation-এর দুই ধরনের বিন্দু বিশেষ গুরুত্বপূর্ণ। Source SCC: যেখানে বাইরে থেকে কোনো edge ঢোকে না (আমাদের A). Sink SCC: যেখান থেকে কোনো edge বের হয় না (আমাদের C). আমাদের মানচিত্রে 6-এ পৌঁছানো একটি রিকশা আর কোথাও যেতে পারে না — sink. দুটি অ্যালগরিদমই আসলে এই source আর sink-কে চতুরভাবে খুঁজে বের করে।
৩ · Kosaraju — দুইবার DFS, দ্বিতীয়বার উল্টো রাস্তায়
মূল ভাবনাটি এই: যদি আমরা একটি sink SCC-র কোনো vertex থেকে DFS শুরু করতে পারি, তাহলে DFS শুধু সেই SCC-র ভেতরেই ঘুরবে — বেরোনোর রাস্তা নেই — আর ঠিক সেই SCC-টিকেই দেখাবে। তারপর সেটি সরিয়ে রাখলে পরের sink খুঁজে আবার একই কাজ। সমস্যা একটাই: sink SCC-র vertex কোনটি, তা আগে থেকে জানি না।
এখানে আসে DFS-এর finish time: একটি vertex-এর সব প্রতিবেশী দেখা শেষ করে যে মুহূর্তে DFS তার থেকে ফিরে আসে। যে vertex সবচেয়ে দেরিতে শেষ হয়, সে সবসময় একটি source SCC-তে থাকে (প্রমাণ একটু পরে)। Source চাই না, sink চাই — তাই চালাকি: সব রাস্তার দিক উল্টে দিন। উল্টানো গ্রাফকে বলে transpose graph GT. GT-এর SCC-গুলো G-এর SCC-র হুবহু সমান (প্রতিটি পথ উল্টে দিলেও পরস্পর reachable থাকে), কিন্তু G-এর source SCC হয়ে যায় GT-এর sink. এটিই Kosaraju-র অ্যালগরিদম:
Pseudocode — Kosaraju (0-based)
order = [] // pass 1: finish order on G
for each vertex u: if not seen[u]: dfs1(u)
dfs1(u): seen[u] = true
for v in adj[u]: if not seen[v]: dfs1(v)
order.append(u) // u finishes now
build GT: for each edge u -> v add v -> u // pass 2 on the reversed graph
for u in reversed(order): // latest finish first
if comp[u] == none: c = c + 1; dfs2(u, c)
dfs2(u, c): comp[u] = c
for v in radj[u]: if comp[v] == none: dfs2(v, c)আমাদের মানচিত্রে দুই পাস চালিয়ে দেখুন:
| পাস | |
| এই ধাপে | |
| SCC-র সংখ্যা |
- প্রথম পাস শুরু. G-তে DFS, vertex 0 থেকে; প্রতিবেশী দেখা হবে adjacency list-এর ক্রমে। এখনো কেউ শেষ হয়নি।
- প্রথম finish. 0 → 1 → 2. 2-এর প্রথম প্রতিবেশী 0 আগেই দেখা, তাই পরেরটি: 2 → 6. 6 থেকে কোনো রাস্তা নেই — 6 সবার আগে শেষ। খেয়াল করুন, 6 একটি sink SCC, কিন্তু “আগে শেষ হওয়া” দিয়ে sink চেনা যায় না — পরের ধাপে 2-ও শেষ হবে, যে sink নয়।
- 2 শেষ, 1 এগোয়. 2-এর সব প্রতিবেশী দেখা, 2 শেষ। ফিরে 1-এর পরের প্রতিবেশী 3: 3 → 4 → 5. 5 থেকে 3 আর 6 দুটোই আগে দেখা।
- প্রথম পাস শেষ. 5, 4, 3 পরপর শেষ, তারপর 1, তারপর 0. Finish-এর পুরো ক্রম
6 2 5 4 3 1 0. সবচেয়ে দেরিতে শেষ হয়েছে 0 — আর 0 আছে source SCC A-তে। দ্বিতীয় পাস চলবে উল্টো ক্রমে:0 1 3 4 5 2 6. - উল্টো গ্রাফ. প্রতিটি রাস্তার দিক উল্টে GT বানানো হলো (যেমন 1 → 3 হয়ে গেল 3 → 1). SCC একই থাকে, কিন্তু এখন A হয়ে গেছে একটি sink — GT-তে A থেকে বেরোনোর কোনো রাস্তা নেই।
- প্রথম SCC. ক্রমের প্রথম vertex 0. GT-তে 0 → 2 → 1, আর 1 → 0 আগেই দেখা। A থেকে বেরোনোর রাস্তা নেই, তাই DFS ঠিক {0, 2, 1}-এ থেমে গেল — প্রথম SCC.
- দ্বিতীয় SCC. ক্রমের পরের 1 আগেই নেওয়া। তারপর 3: GT-তে 3 → 1 আছে, কিন্তু 1 ইতিমধ্যে A-র — নেওয়া হয় না। 3 → 5 → 4, তারপর 4 → 3 দেখা। দ্বিতীয় SCC {3, 5, 4}.
- তৃতীয় SCC. 4, 5, 2 আগেই নেওয়া। 6: GT-তে 6 → 2 আর 6 → 5, দুজনই নেওয়া। তৃতীয় SCC {6}. মোট দুটি DFS — একবার G-তে, একবার GT-তে — আর SCC বেরিয়েছে condensation-এর topological ক্রমে: A, B, C.
কেন কাজ করে — finish time-এর মূল লেমা
একটি SCC X-এর জন্য f(X) = X-এর সব vertex-এর মধ্যে সবচেয়ে বড় finish time. লেমা: G-তে দুটি আলাদা SCC X আর Y, আর X থেকে Y-তে একটি edge থাকলে f(X) > f(Y).
প্রমাণ: দুই ক্ষেত্র। (ক) DFS প্রথমে X-এ ঢোকে, ধরুন vertex x-এ। তখন X আর Y-এর সব vertex অদেখা, আর x থেকে সবাই reachable (X-এর ভেতরে, তারপর X → Y edge ধরে Y-তে). DFS-এর একটি মৌলিক বৈশিষ্ট্য (পর্ব ৪১-এর “white path” নীতি): x শেষ হওয়ার আগেই x থেকে অদেখা পথে যাওয়া যায় এমন সবাই দেখা ও শেষ হয়। তাই x সবার পরে শেষ হয়, f(X) = x-এর finish > f(Y). (খ) DFS প্রথমে Y-তে ঢোকে। Y থেকে X-এ কোনো পথ নেই — থাকলে X → Y edge-এর সাথে মিলে X আর Y একই SCC হতো। তাই Y-র সব vertex শেষ হয়ে যায় X-এর কেউ দেখা হওয়ার আগেই, f(Y) < f(X). ∎
এবার দ্বিতীয় পাস। সবচেয়ে বড় finish-এর vertex যে SCC-তে, ধরুন X, তার f(X) সবার বড় — তাই লেমা অনুযায়ী G-তে কোনো SCC থেকে X-এ edge ঢোকে না: X একটি source. GT-তে তাই X থেকে কোনো edge বের হয় না, আর GT-তে DFS ঠিক X-কেই দেখায়। পরের DFS যে SCC থেকে শুরু হয়, তার থেকে GT-তে বের-হওয়া edge শুধু এমন SCC-তে যায় যাদের f আরও বড় — অর্থাৎ যারা আগেই নেওয়া হয়ে গেছে। Induction-এ প্রতিটি দ্বিতীয়-পাসের DFS tree ঠিক একটি SCC. ∎
সবচেয়ে প্রচলিত ভুল: “সবচেয়ে আগে শেষ হওয়া vertex sink SCC-তে থাকে” — এটি সত্য নয়. ওপরের ডায়াগ্রামে দ্বিতীয় শেষ হওয়া vertex 2, যে source SCC A-তে! লেমাটি শুধু সবচেয়ে বড় finish time সম্পর্কে নিশ্চয়তা দেয়। তাই G-তে ছোট finish থেকে শুরু করার “শর্টকাট” ভুল উত্তর দেয়; দুই পাস আর উল্টো গ্রাফ — দুটোই জরুরি।
জটিলতা: দুটি DFS, প্রতিটি প্রতিটি vertex আর edge একবার করে ছোঁয় — O(V + E). GT বানাতেও O(V + E). বাড়তি জায়গা O(V + E) (GT-র জন্য) আর O(V) (seen, order, comp). Adjacency matrix-এ রাখলে প্রতিটি DFS O(V²).
৪ · Tarjan — এক DFS-এ low-link আর একটি stack
Kosaraju সুন্দর, কিন্তু গ্রাফটি দুবার পড়ে আর একটি উল্টো কপি বানায়। ১৯৭২ সালে Robert Tarjan একটিমাত্র DFS-এ SCC বের করার পদ্ধতি দেন। এর জন্য DFS-এর দুটি সংখ্যা লাগে:
- disc[u]: DFS কত নম্বরে u-তে প্রথম পৌঁছাল (0, 1, 2, …)।
- low[u]: u-র DFS tree-এর subtree থেকে tree edge ধরে নেমে, তারপর একটি অ-tree edge দিয়ে এখনো stack-এ থাকা কোন vertex-এ পৌঁছানো যায় — তাদের মধ্যে সবচেয়ে ছোট disc.
সাথে একটি stack: DFS যে vertex-এ ঢোকে, তাকে stack-এ রাখে; কেউ বের হয় শুধু তখন, যখন তার পুরো SCC পাওয়া যায়। মূল পর্যবেক্ষণ: প্রতিটি SCC-র যে vertex-এ DFS সবার আগে ঢোকে, সে-ই সেই SCC-র “মাথা” (root), আর SCC-র বাকি সবাই DFS tree-তে তার নিচে। মাথাকে চেনার উপায় — low[u] == disc[u]: u-র subtree থেকে u-র চেয়ে পুরোনো কোনো stack-এর vertex-এ ফেরা যায় না। তখন stack-এ u আর তার ওপরে যারা আছে, তারাই u-র SCC.
Pseudocode — Tarjan's SCC (0-based, timer starts at 0)
dfs(u):
disc[u] = low[u] = timer++; push u; onStack[u] = true
for v in adj[u]:
if disc[v] == none: dfs(v); low[u] = min(low[u], low[v]) // tree edge
else if onStack[v]: low[u] = min(low[u], disc[v]) // edge into the open SCC
// else: v already belongs to a finished SCC, ignore the edge
if low[u] == disc[u]: // u is the head of an SCC
pop w until w == u, each popped w gets onStack[w] = false -> one SCConStack-এর শর্তটিই অ্যালগরিদমের প্রাণ। v যদি আগেই দেখা কিন্তু এখন stack-এ না থাকে, তাহলে v একটি শেষ হয়ে যাওয়া SCC-র সদস্য — সেখান থেকে u-তে ফেরার পথ নেই (থাকলে v-র SCC তখনো শেষ হতো না)। এমন edge (প্রায়ই একটি cross edge) low-কে ছোট করলে দুটি আলাদা SCC ভুল করে জুড়ে যেত। একই মানচিত্রে চালিয়ে দেখুন:
| এই ধাপের ঘটনা | |
| পাওয়া SCC |
- নেমে যাওয়া. DFS 0 থেকে 1, তারপর 2-তে। প্রত্যেকে ঢোকার সময় disc = low = timer পায় (0, 1, 2) আর stack-এ ওঠে।
- পেছনে ফেরার রাস্তা. 2 → 0: 0 আগেই দেখা আর এখনো stack-এ — এটি একটি back edge. low[2] = min(2, disc[0]) = 0: “2-এর subtree থেকে 0-তে ফেরা যায়”.
- প্রথম SCC. 2 → 6: নতুন vertex, disc[6] = low[6] = 3. 6-এর কোনো রাস্তা নেই, তাই low[6] == disc[6] — 6 নিজের SCC-র মাথা। Stack থেকে 6 পর্যন্ত pop: {6}. 2-এ ফিরে low[2] = min(0, low[6]) = 0 — বদলায় না।
- দ্বিতীয় শাখা. 2 শেষ (low[2] = 0 ≠ disc[2] = 2, তাই 2 stack-এই থাকে)। 1-এ ফিরে low[1] = min(1, 0) = 0. তারপর 1 → 3 → 4 → 5, disc 4, 5, 6.
- দুটি ভিন্ন edge. 5 → 3: 3 stack-এ, তাই low[5] = min(6, 4) = 4. 5 → 6: 6 দেখা, কিন্তু stack-এ নেই — তার SCC আগেই শেষ, তাই edge-টি উপেক্ষা। এখানে low[5] = 3 (disc[6]) বসালে B আর A ভুল করে মিশে যেত।
- B পাওয়া গেল. 4-এ ফিরে low[4] = min(5, 4) = 4; 3-এ ফিরে low[3] = min(4, 4) = 4 = disc[3]. 3 মাথা: stack থেকে 5, 4, 3 pop — দ্বিতীয় SCC {5, 4, 3}.
- A পাওয়া গেল. 1-এ ফিরে low[1] = min(0, low[3] = 4) = 0. 0-তে ফিরে low[0] = 0 = disc[0]: বাকি 2, 1, 0 pop — তৃতীয় SCC. SCC বেরিয়েছে C, B, A ক্রমে — condensation-এর উল্টো topological ক্রমে।
কেন ঠিক — একটি অনানুষ্ঠানিক প্রমাণ
(১) একটি SCC-র সবাই এক সাথে বের হয়। SCC S-এর প্রথম দেখা vertex h. DFS-এর white path নীতিতে S-এর বাকি সবাই h-এর subtree-তে আসে, আর h শেষ হওয়ার আগে কেউ stack থেকে বের হয় না, যদি না তাদের নিজেদের low == disc হয়। কিন্তু S-এর কোনো r ≠ h-এর জন্য low[r] < disc[r]: r থেকে S-এর ভেতরে h-এ ফেরার পথ আছে, আর সেই পথের প্রথম যে edge r-এর subtree থেকে বেরোয়, তা stack-এ থাকা কোনো পুরোনো vertex-এ যায়। (২) h-কে চেনা যায়: h-এর subtree থেকে h-এর চেয়ে পুরোনো stack-এর vertex-এ পৌঁছানো গেলে সেই vertex আর h পরস্পর reachable হতো, অর্থাৎ h S-এর প্রথম দেখা vertex নয় — স্ববিরোধ; তাই low[h] = disc[h]. (৩) h-এর ওপরে stack-এ যারা আছে, তারা h-এর subtree-র এমন vertex যাদের SCC এখনো শেষ হয়নি — তারা h-এ ফিরতে পারে, তাই S-এর সদস্য। ∎
জটিলতা: প্রতিটি vertex একবার push, একবার pop; প্রতিটি edge একবার দেখা — O(V + E) সময়। বাড়তি জায়গা O(V): disc, low, onStack, stack আর recursion-এর গভীরতা — উল্টো গ্রাফ লাগে না।
১৯৭২ সালে SIAM Journal on Computing-এ Robert Tarjan-এর প্রবন্ধ “Depth-first search and linear graph algorithms” প্রকাশিত হয় — সেখানেই এই SCC অ্যালগরিদম, আর undirected গ্রাফের biconnected component-এর রৈখিক সময়ের পদ্ধতি। S. Rao Kosaraju দুই-পাসের পদ্ধতিটি দেন ১৯৭৮ সালে, কিন্তু প্রকাশ করেননি; Aho, Hopcroft ও Ullman তাঁদের বইয়ে তাঁর নামে এটি তুলে ধরেন। স্বাধীনভাবে Micha Sharir ১৯৮১ সালে একই পদ্ধতি প্রকাশ করেন — data flow analysis-এর প্রয়োগসহ — তাই কিছু বইয়ে এর নাম Kosaraju–Sharir অ্যালগরিদম।
৫ · কোড আর জটিলতা — Tarjan-এর SCC, C++ ও Python-এ
ওপরের মানচিত্রটিই কোডে: adjacency list হুবহু একই, তাই আউটপুট ডায়াগ্রামের সাথে মিলিয়ে দেখতে পারবেন। প্রতিটি SCC যে ক্রমে stack থেকে বের হয়, সেই ক্রমেই ছাপা হবে, শেষে disc আর low অ্যারে।
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
vector<vector<int>> adj = {{1}, {2, 3}, {0, 6}, {4}, {5}, {3, 6}, {}}; // one-way streets: u -> v
int n = adj.size(), timer = 0;
vector<int> disc(n, -1), low(n), st;
vector<bool> onStack(n);
vector<vector<int>> comps;
void dfs(int u) {
disc[u] = low[u] = timer++;
st.push_back(u); onStack[u] = true;
for (int v : adj[u]) {
if (disc[v] == -1) { dfs(v); low[u] = min(low[u], low[v]); } // tree edge
else if (onStack[v]) low[u] = min(low[u], disc[v]); // edge into the open SCC
}
if (low[u] == disc[u]) { // u is the head of an SCC
comps.emplace_back();
int w;
do { w = st.back(); st.pop_back(); onStack[w] = false; comps.back().push_back(w); } while (w != u);
}
}
void show(const char* label, const vector<int>& a) {
cout << label; for (int x : a) cout << ' ' << x; cout << '\n';
}
int main() {
for (int u = 0; u < n; ++u) if (disc[u] == -1) dfs(u);
for (size_t i = 0; i < comps.size(); ++i) { cout << "SCC " << i + 1 << ':'; show("", comps[i]); }
show("disc:", disc);
show("low: ", low);
}আউটপুট:
SCC 1: 6
SCC 2: 5 4 3
SCC 3: 2 1 0
disc: 0 1 2 4 5 6 3
low: 0 0 0 4 4 4 3
তিনটি SCC ঠিক ডায়াগ্রামের ক্রমে: আগে sink {6}, তারপর {5, 4, 3}, শেষে source {2, 1, 0}. disc অ্যারেতে দেখুন 6-এর disc 3 — DFS 2 থেকে সরাসরি 6-এ গিয়েছিল, 3-এ পৌঁছানোর আগেই। low অ্যারেতে প্রতিটি SCC-র সবার low একই — তাদের মাথার disc (A-র 0, B-র 4, C-র 3) — এই উদাহরণে তাই হয়েছে, কিন্তু সবসময় এমন হবে এমন নিশ্চয়তা নেই; নিশ্চিত শুধু মাথার low == disc. do … while লুপটি u নিজেকেও pop করে তারপর থামে। প্রতিটি vertex আর edge একবার — O(V + E).
from itertools import count
adj = [[1], [2, 3], [0, 6], [4], [5], [3, 6], []] # one-way streets: u -> v
n = len(adj)
disc, low = [-1] * n, [0] * n
on_stack, stack, comps = [False] * n, [], []
tick = count()
def dfs(u):
disc[u] = low[u] = next(tick)
stack.append(u)
on_stack[u] = True
for v in adj[u]:
if disc[v] == -1: # tree edge
dfs(v)
low[u] = min(low[u], low[v])
elif on_stack[v]: # edge into the SCC still being built
low[u] = min(low[u], disc[v])
if low[u] == disc[u]: # u is the head of an SCC
comp = []
while True:
w = stack.pop()
on_stack[w] = False
comp.append(w)
if w == u:
break
comps.append(comp)
for u in range(n):
if disc[u] == -1:
dfs(u)
for i, comp in enumerate(comps, 1):
print(f"SCC {i}:", *comp)
print("disc:", *disc)
print("low: ", *low)আউটপুট C++-এর সাথে হুবহু এক:
SCC 1: 6
SCC 2: 5 4 3
SCC 3: 2 1 0
disc: 0 1 2 4 5 6 3
low: 0 0 0 4 4 4 3
itertools.count() একটি ক্রমিক সংখ্যার উৎস: প্রতিবার next(tick) 0, 1, 2, … দেয় — global timer আর global ঘোষণার ঝামেলা থাকে না। এখানে recursion নিরাপদ, কারণ গ্রাফ ছোট। কিন্তু DFS-এর গভীরতা V পর্যন্ত যেতে পারে, আর Python-এর recursion সীমা সাধারণত ১০০০ — লাখখানেক vertex-এর একটি লম্বা শিকলে এই কোড RecursionError দেবে। তখন একটি explicit stack দিয়ে iterative DFS লিখুন, অথবা sys.setrecursionlimit বাড়ানোর সাথে threading.stack_size দিয়ে বড় stack-এর thread-এ চালান।
| বিষয় | Kosaraju | Tarjan |
|---|---|---|
| DFS-এর সংখ্যা | ২টি (G-তে, তারপর GT-তে) | ১টি |
| সময় | O(V + E) | O(V + E) |
| বাড়তি জায়গা | O(V + E) — উল্টো গ্রাফ | O(V) |
| SCC বের হওয়ার ক্রম | condensation-এর topological ক্রম | উল্টো topological ক্রম |
| বুঝতে ও লিখতে | সহজ — শুধু দুটি সাধারণ DFS | low আর onStack-এর সূক্ষ্মতা আছে |
| একই ধারণা আর কোথায় | — | bridge, articulation point (অংশ ৭) |
৬ · প্রয়োগ — 2-SAT, বৃত্তাকার নির্ভরতা আর বন্ধুদের দল
2-SAT — শর্ত মেনে হ্যাঁ/না ঠিক করা
তিন বন্ধু — রাফি (a), মিম (b), তানিম (c) — কে কে একটি পিকনিকে যাবে, তা ঠিক করতে হবে। শর্তগুলো:
- রাফি বা মিম, অন্তত একজন যাবে:
(a ∨ b) - রাফি গেলে তানিমও যাবে:
(¬a ∨ c) - মিম আর তানিম একসাথে যাবে না:
(¬b ∨ ¬c) - তানিম আর রাফি একসাথে যাবে না:
(¬c ∨ ¬a)
প্রতিটি চলক true (যাবে) বা false (যাবে না). x বা ¬x — প্রতিটিকে বলে একটি literal. শর্তগুলো AND দিয়ে জোড়া, প্রতিটি শর্তে দুটি literal-এর OR — এই রূপের নাম CNF, আর প্রতি শর্তে ঠিক দুটি literal হলে সমস্যাটি 2-SAT. সাধারণ SAT, এমনকি প্রতি শর্তে তিনটি literal-এর 3-SAT-ও NP-complete (পর্ব ৬৪) — কিন্তু 2-SAT রৈখিক সময়ে সমাধান হয়, SCC দিয়ে।
চাবিকাঠি: একটি শর্ত (x ∨ y) আসলে দুটি “যদি-তাহলে”: x মিথ্যা হলে y সত্য হতেই হবে (¬x → y), আর y মিথ্যা হলে x সত্য (¬y → x). প্রতিটি literal-কে একটি vertex বানিয়ে প্রতিটি শর্ত থেকে এই দুটি edge দিলে পাওয়া যায় implication graph — n টি চলকে 2n টি vertex, m টি শর্তে 2m টি edge. একটি পথ x ⇝ y মানে “x সত্য হলে y-কেও সত্য হতে হবে” — implication-গুলো শিকলের মতো জোড়া লাগে।
সূত্রটি সমাধানযোগ্য, যদি এবং কেবল যদি কোনো চলক x-এর জন্য x আর ¬x একই SCC-তে না থাকে। সমাধানযোগ্য হলে একটি সমাধান: condensation-এর topological ক্রমে x-এর SCC যদি ¬x-এর SCC-র পরে আসে, তাহলে x = true, নইলে false.
| edge-এর সংখ্যা | |
| সিদ্ধান্ত |
- শর্তগুলো. তিনটি চলক, তাই ছয়টি literal-vertex: a, ¬a, b, ¬b, c, ¬c. চারটি শর্ত থেকে আটটি edge আসবে।
- প্রথম দুই শর্ত. (a ∨ b): রাফি না গেলে মিমকে যেতেই হবে (¬a → b), মিম না গেলে রাফিকে (¬b → a). (¬a ∨ c)-এর literal ¬a, তাই তার উল্টো a থেকে edge: a → c; আর ¬c → ¬a.
- শেষ দুই শর্ত. (¬b ∨ ¬c) থেকে b → ¬c আর c → ¬b; (¬c ∨ ¬a) থেকে c → ¬a আর a → ¬c. মোট আটটি edge — প্রতিটি শর্তের দুটি edge একে অন্যের “আয়না” (contrapositive).
- SCC. a → c → ¬b → a একটি চক্র, তাই {a, c, ¬b} একটি SCC; ¬a → b → ¬c → ¬a আরেকটি। কোনো চলক নিজের উল্টোর সাথে একই SCC-তে নেই — সমাধান আছে। Condensation-এ S1 → S2 (a → ¬c edge দিয়ে).
- মান বসানো. S2 ক্রমে পরে (sink), তাই S2-এর literal-গুলো সত্য: ¬a, b, ¬c. অর্থাৎ রাফি যাবে না, মিম যাবে, তানিম যাবে না। চারটি শর্তই মেলে। যুক্তিটি সহজ: sink-এর literal সত্য করলে implication তাকে আর কোথাও টেনে নেয় না।
- পঞ্চম শর্ত. এবার যোগ করুন “রাফি বা তানিম, অন্তত একজন যাবে” (a ∨ c): নতুন edge ¬a → c আর ¬c → a. এখন a → ¬c → a, আর ¬a → c → ¬b → a → ¬c → ¬a — a আর ¬a একই SCC-তে: রাফি গেলে তাকে না যেতেই হবে, না গেলে যেতেই হবে। কোনো সমাধান নেই।
কেন উপপাদ্যটি সত্য. প্রয়োজনীয়তা: x আর ¬x একই SCC-তে থাকলে x ⇝ ¬x আর ¬x ⇝ x দুটি পথই আছে; x = true হলে প্রথম পথ ¬x-কে true করতে বাধ্য করে, x = false হলে দ্বিতীয় পথ x-কে — দুটোই অসম্ভব। যথেষ্টতা: topological ক্রমে প্রতিটি SCC-র অবস্থান t(·). নিয়ম: x = true যদি t(x) > t(¬x). ধরুন কোনো শর্ত (p ∨ q) ভেঙেছে, অর্থাৎ p আর q দুটোই false. তাহলে নিয়ম অনুযায়ী t(p) < t(¬p) আর t(q) < t(¬q). আবার edge ¬p → q আর ¬q → p থেকে t(¬p) ≤ t(q) আর t(¬q) ≤ t(p) (edge সবসময় topological ক্রমে সামনে যায়, বা একই SCC-তে থাকে). জুড়ে দিলে t(p) < t(¬p) ≤ t(q) < t(¬q) ≤ t(p) — অর্থাৎ t(p) < t(p), অসম্ভব। ∎ Tarjan SCC-গুলোকে উল্টো topological ক্রমে নম্বর দেয়, তাই Tarjan দিয়ে লিখলে নিয়মটি উল্টে যায়: x = true যদি comp[x] < comp[¬x].
কোডে literal রাখার প্রচলিত কৌশল: চলক i-এর জন্য vertex 2i = xi আর 2i + 1 = ¬xi; তাহলে কোনো literal-এর উল্টো পাওয়া যায় এক XOR-এ, lit ^ 1. পুরো 2-SAT তাই O(n + m): গ্রাফ বানানো, একবার Tarjan, তারপর প্রতিটি চলকে একটি তুলনা। ক্লাস রুটিন বা পরীক্ষার হলের সময়সূচিতে “দুটি বিকল্পের একটি” ধরনের সিদ্ধান্ত, মানচিত্রে নাম বসানোর কিছু মডেল, বা দুই রঙে ভাগাভাগির শর্ত — এমন অনেক সমস্যা 2-SAT-এ নামানো যায়।
বৃত্তাকার নির্ভরতা খোঁজা
একটি সফটওয়্যার প্রজেক্টে মডিউলগুলো একে অন্যকে import করে; একটি dependency graph-এ edge A → B মানে “A চলতে B লাগে”. Build করতে হলে topological ক্রম লাগে (পর্ব ৪২), আর তা সম্ভব শুধু যদি গ্রাফটি DAG হয়। Cycle থাকলে কম্পাইলার বা প্যাকেজ ম্যানেজার শুধু বলে “circular dependency” — কিন্তু কোন মডিউলগুলো এর জন্য দায়ী? উত্তর: একাধিক সদস্যের প্রতিটি SCC একটি জট, যার ভেতরের সবাই একে অন্যের ওপর ঘুরে-ফিরে নির্ভরশীল; জট ছাড়াতে প্রতিটি SCC-র ভেতরের অন্তত একটি edge ভাঙতে হবে। সাবধান: একটি vertex-এর নিজের দিকে edge (u → u, self-loop) থাকলেও সেটি cycle, যদিও SCC-তে সে একা।
একই প্রশ্ন অপারেটিং সিস্টেমে: process P1 অপেক্ষা করছে P2-র হাতে থাকা resource-এর জন্য, P2 অপেক্ষা করছে P3-র জন্য, আর P3 P1-এর জন্য — deadlock. “কে কার জন্য অপেক্ষা করছে” গ্রাফে (wait-for graph) একাধিক সদস্যের SCC মানেই এমন একটি চক্র। Spreadsheet-এ “এই ঘরের সূত্র ওই ঘরের ওপর নির্ভর করে” গ্রাফেও একই কথা — circular reference-এর সতর্কবার্তা আসলে একটি cycle খুঁজে পাওয়া।
সামাজিক নেটওয়ার্কের পরস্পর-যুক্ত দল
Facebook-এর বন্ধুত্ব দুদিকের (undirected), কিন্তু “follow” একদিকের — আপনি একজন ক্রিকেটারকে follow করেন, তিনি আপনাকে করেন না। Follow-গ্রাফের একটি SCC এমন একদল মানুষ, যাদের যেকোনো একজনের পোস্ট share-এর শিকল ধরে দলের যেকোনো জনের কাছে পৌঁছাতে পারে — আর ফিরেও আসতে পারে। বাস্তব বড় নেটওয়ার্কে সাধারণত একটি বিশাল SCC থাকে, আর তার চারপাশে অনেক ছোট ছোট দল আর একা মানুষ। ওয়েবের লিংক-গ্রাফেও একই ধাঁচ দেখা গেছে (প্রশ্ন ০৩). সুপারিশ (recommendation) বা গুজব কতদূর ছড়াবে — এমন বিশ্লেষণের প্রথম ধাপ প্রায়ই এই SCC-ভাগ।
৭ · নেটওয়ার্কের দুর্বল জায়গা — bridge আর articulation point
এবার দিক ছাড়া, অর্থাৎ undirected গ্রাফ। একটি ইন্টারনেট সেবাদাতার আটটি router (0 থেকে 7) আর দশটি কেবল। প্রকৌশলীর প্রশ্ন: কোন একটি কেবল কাটা পড়লে নেটওয়ার্ক দুই টুকরো হয়ে যাবে? আর কোন একটি router বিকল হলে? প্রথম ধরনের edge-কে বলে bridge (cut edge), দ্বিতীয় ধরনের vertex-কে articulation point (cut vertex). সংজ্ঞা: যাকে সরালে connected component-এর সংখ্যা বাড়ে।
ছবি দেখে বলা সহজ: 1–3 কেবল কাটলে বাঁয়ের ত্রিভুজ আলাদা; router 4 বিকল হলে 6 আর 7 বিচ্ছিন্ন। কিন্তু 6–7? সেখানে দুটি কেবল — একটি কাটলেও অন্যটি আছে, তাই 6–7 bridge নয়. তবু router 6 বিকল হলে 7 একা হয়ে যায় — 6 একটি articulation point. প্রতিটি edge বা vertex একে একে সরিয়ে BFS চালালে O(E · (V + E)) বা O(V · (V + E)) সময়; আমরা চাই এক DFS-এ, O(V + E)-এ।
Undirected DFS-এর একটি উপহার — cross edge নেই
Undirected গ্রাফে DFS চালালে প্রতিটি edge হয় tree edge, নয় back edge — কোনো edge দুটি আলাদা শাখাকে জোড়ে না। কারণ: u-তে থাকার সময় কোনো প্রতিবেশী v অদেখা থাকলে DFS তাকে u-এর subtree-তেই ঢুকিয়ে নেয়। তাই একটি subtree থেকে বাইরে যাওয়ার একমাত্র উপায় হলো কোনো back edge দিয়ে ওপরের দিকে, পূর্বপুরুষের কাছে ওঠা। low[v] ঠিক এটাই মাপে: v-র subtree থেকে একটি back edge দিয়ে সবচেয়ে ওপরের (ছোট disc-এর) কোন vertex-এ ওঠা যায়।
Bridge: tree edge (u, v) bridge, যদি এবং কেবল যদি low[v] > disc[u] — v-র subtree থেকে u-তে বা তার ওপরে ওঠার কোনো back edge নেই, তাই (u, v) কাটলে subtree আলাদা। Back edge কখনো bridge নয় — সে একটি cycle-এর অংশ।
Articulation point (u root নয়): u articulation point, যদি এবং কেবল যদি u-র অন্তত একটি child v-এর জন্য low[v] ≥ disc[u] — v-র subtree বড়জোর u পর্যন্ত উঠতে পারে, u-র ওপরে নয়; u সরালে subtree আলাদা।
Root-এর নিয়ম: root articulation point, যদি এবং কেবল যদি DFS tree-তে তার অন্তত দুটি child থাকে। Cross edge নেই, তাই দুই child-এর subtree শুধু root দিয়েই যুক্ত।
পার্থক্যটি মাত্র একটি সমান চিহ্ন, তাই মনে রাখার উপায়: bridge-এর জন্য v-র subtree-কে edge-টি ছাড়া u-তে পৌঁছাতে পারলেই চলে (low[v] ≤ disc[u] হলে bridge নয়); articulation point-এর জন্য u নিজেই সরে যায়, তাই u-তে পৌঁছানো যথেষ্ট নয় — u-র ওপরে উঠতে হবে (low[v] < disc[u] হলে তবেই নিরাপদ)। Root-এর জন্য আলাদা নিয়ম কেন লাগে? Root-এর ওপরে কেউ নেই, তাই প্রতিটি child-এর জন্য low[v] ≥ disc[root] = 0 সবসময় সত্য — সাধারণ শর্ত বসালে প্রতিটি root ভুল করে articulation point হয়ে যেত।
Multi-edge সামলানো. Low হিসাবের সময় parent-এ ফেরার edge-টি বাদ দিতে হয়, নইলে প্রতিটি v সেই tree edge দিয়েই u-তে “ফিরে” low[v] ≤ disc[u] বানাত, আর কোনো bridge পাওয়া যেত না। অনেকে বাদ দেন “parent vertex”-কে — কিন্তু multigraph-এ, যেখানে 6 আর 7-এর মধ্যে দুটি কেবল, তাতে দ্বিতীয় কেবলটিও বাদ পড়ে আর 6–7 ভুল করে bridge হয়ে যায়। সঠিক উপায়: প্রতিটি edge-কে একটি id দিন, আর বাদ দিন শুধু যে edge দিয়ে এসেছেন, সেই id-কে।
| Bridge | |
| Articulation point |
- ত্রিভুজে নামা. DFS 0 → 1 (e0) → 2 (e1), disc 0, 1, 2. 2-এর কাছে e1 হলো আসার edge — বাদ। e2 দিয়ে 0 আগেই দেখা: back edge, low[2] = min(2, disc[0]) = 0.
- 1-এ ফেরা. low[1] = min(1, low[2]) = 0. Child 2-এর পরীক্ষা: low[2] = 0 > disc[1] = 1? না, তাই 1–2 bridge নয়। 0 ≥ 1? না, তাই এই child-এর কারণে 1 articulation point নয় — 2 ঘুরে 0-তে, অর্থাৎ 1-এর ওপরে ওঠা যায়।
- দ্বিতীয় ত্রিভুজ. 1 → 3 (e3) → 4 (e4) → 5 (e5), disc 3, 4, 5. 5 থেকে e6 দিয়ে 3: back edge, low[5] = 3. 4-এ ফিরে low[4] = 3; child 5: 3 > 4? না; 3 ≥ 4? না।
- সমান্তরাল কেবল. 4 → 6 (e7) → 7 (e8). 7-এর কাছে e8 আসার edge, বাদ — কিন্তু e9 অন্য একটি edge, যা 6-এ যায়: back edge, low[7] = 6. 6-এ ফিরে child 7: 6 > 6? না — 6–7 bridge নয়। 6 ≥ 6? হ্যাঁ — 6 articulation point. Parent vertex বাদ দিলে e9-ও বাদ পড়ত, low[7] = 7, আর 6–7 ভুল করে bridge হতো।
- প্রথম bridge. 4-এ ফিরে low[4] = min(3, 6) = 3. Child 6: low[6] = 6 > disc[4] = 4 — 6-এর subtree 4 পর্যন্তও উঠতে পারে না: 4–6 bridge. আর 6 ≥ 4, তাই 4 articulation point.
- আরও ওপরে. 3-এ ফিরে child 4: low[4] = 3 ≥ disc[3] = 3 — 4-এর subtree 3 পর্যন্তই উঠতে পারে, ওপরে নয়: 3 articulation point; কিন্তু 3 > 3 নয়, তাই 3–4 bridge নয়। 1-এ ফিরে child 3: low[3] = 3 > 1 — 1–3 bridge, আর 1 articulation point.
- Root-এর নিয়ম. 0-তে ফিরে child 1: low[1] = 0 > 0? না, 0–1 bridge নয়। 0 ≥ 0 সত্য, কিন্তু 0 root — শুধু child গোনা হয়: DFS tree-তে 0-এর একটাই child (2 পৌঁছানো হয়েছে 1 দিয়ে), তাই 0 articulation point নয়। ঠিকই: 0 সরালেও 1 আর 2 যুক্ত। DFS 1 থেকে শুরু হলে root 1-এর দুটি child হতো (0 বা 2-এর দিক, আর 3) — তখন root-এর নিয়মেই 1 articulation point হতো।
প্রমাণের সারকথা (bridge). Tree edge (u, v) কাটলে v-র subtree Tv বাকি গ্রাফের সাথে যুক্ত থাকে শুধু তখন, যখন Tv-এর বাইরে যাওয়ার অন্য কোনো edge আছে। Cross edge নেই, তাই এমন edge মানে Tv থেকে u বা তার কোনো পূর্বপুরুষে যাওয়া একটি back edge — যার অস্তিত্ব ঠিক low[v] ≤ disc[u]-এর সমান। তাই bridge ⇔ low[v] > disc[u]. Articulation point-এর যুক্তি একই, শুধু u নিজে সরে যায় বলে u-তে পৌঁছানো back edge আর কাজে আসে না: শর্তটি হয় low[v] ≥ disc[u]. ∎
৮ · কোড, biconnected component আর বাস্তবের নেটওয়ার্ক
ওপরের নেটওয়ার্কটি কোডে, edge-গুলো id-সহ। Adjacency list-এ প্রতিটি প্রতিবেশীর সাথে edge-এর id রাখা হয়েছে, আর DFS মনে রাখে কোন id দিয়ে সে এসেছে।
#include <algorithm>
#include <iostream>
#include <utility>
#include <vector>
using namespace std;
int n = 8, timer = 0;
vector<pair<int, int>> edges = {{0, 1}, {1, 2}, {2, 0}, {1, 3}, {3, 4}, {4, 5}, {5, 3}, {4, 6}, {6, 7}, {6, 7}};
vector<vector<pair<int, int>>> adj(n); // (neighbour, edge id)
vector<int> disc(n, -1), low(n);
vector<bool> isCut(n);
vector<pair<int, int>> bridges;
void dfs(int u, int parentEdge) {
disc[u] = low[u] = timer++; int children = 0;
for (auto [v, id] : adj[u]) {
if (id == parentEdge) continue; // skip only the edge we came by
if (disc[v] != -1) { low[u] = min(low[u], disc[v]); continue; } // back edge
++children; // tree edge
dfs(v, id);
low[u] = min(low[u], low[v]);
if (low[v] > disc[u]) bridges.push_back({u, v});
if (parentEdge != -1 && low[v] >= disc[u]) isCut[u] = true;
}
if (parentEdge == -1 && children > 1) isCut[u] = true; // root rule
}
int main() {
for (int i = 0; i < (int)edges.size(); ++i) { auto [a, b] = edges[i]; adj[a].push_back({b, i}); adj[b].push_back({a, i}); }
for (int u = 0; u < n; ++u) if (disc[u] == -1) dfs(u, -1);
cout << "bridges:";
for (size_t i = 0; i < bridges.size(); ++i) cout << (i ? ", " : " ") << bridges[i].first << '-' << bridges[i].second;
cout << "\narticulation points:";
for (int u = 0; u < n; ++u) if (isCut[u]) cout << ' ' << u;
cout << "\nlow:"; for (int x : low) cout << ' ' << x;
cout << '\n';
}আউটপুট:
bridges: 4-6, 1-3
articulation points: 1 3 4 6
low: 0 0 0 3 3 3 6 6
Bridge দুটি পাওয়া গেছে DFS থেকে ফেরার ক্রমে — আগে গভীরের 4-6, পরে 1-3. low অ্যারে ডায়াগ্রামের শেষ ধাপের হুবহু। Articulation point একটি bool অ্যারেতে, কারণ একই vertex একাধিক child-এর কারণে বারবার চিহ্নিত হতে পারে — তালিকায় রাখলে ডুপ্লিকেট আসত। তালিকা থেকে {6, 7}-এর একটি কপি মুছে চালালে প্রথম লাইন হয় bridges: 6-7, 4-6, 1-3 আর low[7] হয় 7 — সমান্তরাল কেবলের পার্থক্যটি নিজে দেখে নিন। সময় O(V + E), জায়গা O(V + E).
from itertools import count
n = 8
edges = [(0, 1), (1, 2), (2, 0), (1, 3), (3, 4), (4, 5), (5, 3), (4, 6), (6, 7), (6, 7)]
adj = [[] for _ in range(n)]
for i, (a, b) in enumerate(edges): # store (neighbour, edge id)
adj[a].append((b, i))
adj[b].append((a, i))
disc, low = [-1] * n, [0] * n
is_cut, bridges = [False] * n, []
tick = count()
def dfs(u, parent_edge):
disc[u] = low[u] = next(tick)
children = 0
for v, eid in adj[u]:
if eid == parent_edge: continue # skip only the edge we came by, not a parallel one
if disc[v] != -1: # back edge
low[u] = min(low[u], disc[v])
continue
children += 1 # tree edge
dfs(v, eid)
low[u] = min(low[u], low[v])
if low[v] > disc[u]:
bridges.append((u, v))
if parent_edge != -1 and low[v] >= disc[u]:
is_cut[u] = True
if parent_edge == -1 and children > 1: # root rule
is_cut[u] = True
for u in range(n):
if disc[u] == -1:
dfs(u, -1)
print("bridges:", ", ".join(f"{a}-{b}" for a, b in bridges))
print("articulation points:", *[u for u in range(n) if is_cut[u]])
print("low:", *low)আউটপুট C++-এর সাথে হুবহু এক:
bridges: 4-6, 1-3
articulation points: 1 3 4 6
low: 0 0 0 3 3 3 6 6
লক্ষ করুন if disc[v] != -1 শাখাটি শুধু পূর্বপুরুষের দিকের back edge নয়, বংশধরের দিক থেকে দেখা একই edge-ও ধরে (যেমন 3 থেকে e6 দিয়ে 5, যে আগেই শেষ) — তখন disc[v] > disc[u], তাই min কিছু বদলায় না, ক্ষতিও নেই। Root চেনা হয় parent_edge == -1 দিয়ে, আর গ্রাফ connected না হলে প্রতিটি component-এর জন্য বাইরের লুপ নতুন root দিয়ে DFS শুরু করে। SCC-র কোডের মতোই, খুব বড় গ্রাফে Python-এ iterative DFS লাগবে।
Biconnected আর 2-edge-connected component — সংক্ষেপে
Bridge আর articulation point গ্রাফকে “শক্ত টুকরো”-য় ভাগ করে। দুই ধরনের ভাগ আছে:
- 2-edge-connected component: সব bridge মুছে ফেললে যে component-গুলো থাকে। আমাদের নেটওয়ার্কে {0, 1, 2}, {3, 4, 5}, {6, 7}. প্রতিটির ভেতরে যেকোনো একটি কেবল কাটলেও সবাই যুক্ত। Vertex-গুলো এতে ভাগ হয়। প্রতিটিকে একটি বিন্দু বানালে bridge-গুলোই edge হয়ে একটি bridge tree তৈরি করে — এখানে একটি শিকল: {0,1,2} — {3,4,5} — {6,7}.
- Biconnected component (block): articulation point ছাড়া সবচেয়ে বড় টুকরো, যেখানে যেকোনো একটি router বিকল হলেও বাকিরা যুক্ত। এখানে edge-গুলো ভাগ হয়, আর articulation point একাধিক block-এ থাকে: {0,1,2}, {1,3}, {3,4,5}, {4,6}, {6,7} (দুটি কেবলসহ) — পাঁচটি block. Block আর articulation point দিয়ে বানানো block-cut tree দেখায় কোন router কোন অংশগুলোকে ধরে রেখেছে।
দুটোই একই DFS-এ বের হয়। Biconnected component-এর জন্য একটি edge-এর stack রাখুন: প্রতিটি tree edge আর back edge push করুন; যখন child v-এর জন্য low[v] ≥ disc[u], তখন stack থেকে (u, v) পর্যন্ত সব edge pop — সেটি একটি block. ঠিক Tarjan-এর SCC-র stack-কৌশল, শুধু vertex-এর বদলে edge. মোট সময় আবার O(V + E).
আরও গভীরে — iterative DFS আর directed গ্রাফের “দুর্বল জায়গা”: দশ লাখ vertex-এর একটি শিকলে recursive DFS C++-এও stack overflow দিতে পারে, তাই বড় সিস্টেমে Tarjan লেখা হয় একটি explicit stack দিয়ে, যেখানে প্রতিটি frame মনে রাখে “adjacency list-এর কত নম্বর প্রতিবেশী পর্যন্ত দেখা হয়েছে”. Dijkstra (১৯৭৬) আর পরে Gabow (২০০০) path-based নামে আরেক ধরনের এক-পাসের SCC অ্যালগরিদম দেন, যা low-এর বদলে দুটি stack ব্যবহার করে। আর directed গ্রাফেও bridge-এর অনুরূপ ধারণা আছে — যে edge বা vertex সরালে SCC-র সংখ্যা বাড়ে (strong bridge, strong articulation point); এগুলোও রৈখিক সময়ে বের করা যায়, তবে dominator tree-র মতো আরও উন্নত হাতিয়ার লাগে।
পরীক্ষা ও ইন্টারভিউতে:
- Condensation গ্রাফ সবসময় DAG কেন? — cycle থাকলে সেই SCC-গুলোর সবাই পরস্পর reachable হতো, অর্থাৎ তারা একটিই SCC — SCC-র সর্বোচ্চতার সাথে স্ববিরোধ।
- Kosaraju-র দ্বিতীয় পাস কেন উল্টো গ্রাফে আর finish-এর উল্টো ক্রমে? — সবচেয়ে বড় finish-এর vertex source SCC-তে থাকে; উল্টো গ্রাফে সেটি sink, তাই DFS সেই SCC-র বাইরে যেতে পারে না।
- Tarjan-এ edge-এর অন্য প্রান্ত দেখা কিন্তু stack-এ না থাকলে কী করবেন? — উপেক্ষা; সেটি শেষ হয়ে যাওয়া SCC, low-এ নিলে দুটি SCC ভুল করে মিশে যায়।
- Bridge আর articulation point-এর শর্তের পার্থক্য? — bridge: low[v] > disc[u]; articulation point: low[v] ≥ disc[u] (root ছাড়া), root হলে অন্তত দুটি DFS child.
- 2-SAT কখন অসমাধানযোগ্য, আর সময় কত? — কোনো x আর ¬x একই SCC-তে থাকলে; n চলক আর m শর্তে O(n + m).
৯ · AI-এর যুগে SCC আর bridge — সুন্দর কোড, সূক্ষ্ম ভুল
“Tarjan-এর SCC লিখে দাও” বা “bridges in a graph” — AI সহকারী মুহূর্তে চল্লিশ লাইন দেবে, আর ছোট উদাহরণে তা প্রায়ই ঠিক উত্তরও দেবে। ভুলগুলো লুকিয়ে থাকে ঠিক সেই জায়গায়, যেখানে এই পর্বে সময় দিয়েছি: onStack-এর শর্ত, parent edge বনাম parent vertex, root-এর নিয়ম, > বনাম ≥. এসব ভুল সাধারণ টেস্টে ধরা পড়ে না — ধরা পড়ে শুধু বিশেষ গঠনের গ্রাফে।
এই বিষয়ে AI সাধারণত যেসব ভুল করে
| AI-এর ভুল | কীভাবে ধরবেন |
|---|---|
Tarjan-এ onStack না দেখে “visited হলেই” low[u] = min(low[u], disc[v]) | এই পর্বের মানচিত্র চালান: 5 → 6 edge B-কে A-র সাথে মিশিয়ে দেবে (SCC দুটি আসবে) |
| Bridge-এ parent vertex বাদ দেওয়া | দুই vertex-এর মধ্যে দুটি সমান্তরাল edge দিন — ভুল কোড সেটিকে bridge বলবে |
| Articulation point-এ root-এর নিয়ম ভুলে যাওয়া | একটি সরল ত্রিভুজ 0–1–2: সঠিক উত্তর “কোনো articulation point নেই”, ভুল কোড 0 দেখাবে |
| Bridge-এ ≥ বা articulation point-এ > লেখা | শিকল 0–1–2: bridge দুটি, articulation point শুধু 1 — দুটোই মিলিয়ে দেখুন |
| Kosaraju-র দ্বিতীয় পাস মূল গ্রাফে, বা finish-এর বাড়তি ক্রমে | এই পর্বের মানচিত্রে চালান — সঠিক উত্তর তিনটি SCC |
| 2-SAT-এ মান বসানোর দিক উল্টো (Tarjan-এর নম্বর উল্টো topological) | পাওয়া মান দিয়ে প্রতিটি clause নিজে পরীক্ষা করুন — একটি লুপই যথেষ্ট |
| “Complexity O(V · E)” বা “O(V²)” — adjacency list-এ | প্রতিটি vertex একবার push/pop, প্রতিটি edge একবার দেখা — O(V + E) |
AI-কে কোড লিখে দিতে না বলে, আপনার কোড আর trace পরীক্ষা করতে বলুন — আর প্রতিটি দাবির পাল্টা-উদাহরণ চান:
কোড যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১ঢাকার সব রাস্তা একমুখী করে দিলেও কি সব জায়গায় যাওয়া-আসা সম্ভব রাখা যায়? Robbins-এর উপপাদ্য bridge আর SCC-কে কীভাবে জোড়ে?
যানজট কমাতে শহর কর্তৃপক্ষ প্রায়ই রাস্তা একমুখী করে — দুই লেন একদিকে চললে মোড়ে মুখোমুখি সংঘর্ষ কমে, প্রবাহ বাড়ে। কিন্তু একটি শর্ত না মানলে বিপদ: কোনো এলাকায় ঢোকা গেলেও বের হওয়া যাবে না। গণিতের ভাষায় প্রশ্নটি: একটি undirected গ্রাফের প্রতিটি edge-কে একটি দিক দিয়ে এমন directed গ্রাফ বানানো যায় কি, যেটি strongly connected — অর্থাৎ পুরোটা একটি SCC?
১৯৩৯ সালে Herbert Robbins American Mathematical Monthly-তে এর পূর্ণ উত্তর দেন, প্রবন্ধের শিরোনামেই ছিল ট্রাফিক নিয়ন্ত্রণের কথা। Robbins-এর উপপাদ্য: একটি connected undirected গ্রাফকে strongly connected-ভাবে দিক দেওয়া যায়, যদি এবং কেবল যদি গ্রাফে কোনো bridge না থাকে। একদিক সহজ: একটি bridge যে দিকেই দিন, উল্টো দিকে ফেরার আর কোনো রাস্তা নেই — bridge-এর দুই পাশ কখনো পরস্পর reachable হবে না। Bridge-বিহীন গ্রাফে কী করবেন, সেটিই সুন্দর অংশ।
এই পর্বের DFS-ই উত্তর দেয়। Undirected গ্রাফে একটি DFS চালান। প্রতিটি tree edge-কে দিক দিন ওপর থেকে নিচে (parent → child), আর প্রতিটি back edge-কে নিচ থেকে ওপরে (বংশধর → পূর্বপুরুষ). Root থেকে tree edge ধরে সবাইকে পৌঁছানো যায়। আর ফেরার পথ? যেকোনো non-root v-এর জন্য bridge নেই মানে low[v] ≤ disc[parent], অর্থাৎ v-র subtree থেকে একটি back edge v-এর parent বা তার ওপরে যায় — tree edge-এ নেমে তারপর সেই back edge ধরে v-এর চেয়ে ওপরে ওঠা যায়। Induction-এ সবাই root-এ ফিরতে পারে, তাই সবাই একটি SCC. এক DFS-এ, O(V + E)-এ, পুরো শহরের একমুখী নকশা!
বাস্তবে অবশ্য শুধু strongly connected হওয়াই যথেষ্ট নয়: একমুখী করার পর কিছু যাত্রা অনেক লম্বা হয়ে যেতে পারে, তাই প্রকৌশলীরা দূরত্ব কতটা বাড়ে তাও হিসাব করেন — সেই প্রশ্নগুলো অনেক কঠিন, কিছু NP-hard. আর Robbins-এর যুক্তিতে লুকিয়ে আছে এই পর্বের মূল কথা: bridge হলো এমন রাস্তা, যা একমুখী করলেই শহর ভাগ হয়ে যায়, আর SCC হলো একমুখী জগতে “ফিরে আসা যায়” এমন এলাকা।
মূল উপলব্ধি: একমুখী রাস্তার নকশা আর নেটওয়ার্কের দুর্বল কেবল — একই প্রশ্নের দুই রূপ। Bridge-বিহীন গ্রাফকে সবসময় strongly connected-ভাবে দিক দেওয়া যায়, আর DFS-এর tree/back edge-ই সেই দিক বলে দেয়।
প্র ০২Tarjan-এর ১৯৭২ সালের প্রবন্ধকে কেন গ্রাফ অ্যালগরিদমের একটি মোড়-ঘোরানো কাজ বলা হয়?
DFS নিজে নতুন নয় — গোলকধাঁধা থেকে বের হওয়ার “গলির মুখে চিহ্ন দাও, কানাগলিতে পৌঁছালে ফিরে এসো” ধরনের কৌশল (Trémaux-এর নিয়ম, পর্ব ৪১) উনিশ শতকেই আলোচিত হয়েছিল। কিন্তু ১৯৭০-এর শুরুতে কম্পিউটার বিজ্ঞানে DFS-কে মূলত গ্রাফ “ঘোরার” একটি উপায় হিসেবেই দেখা হতো। Robert Tarjan, তখন Stanford-এর তরুণ গবেষক, John Hopcroft-এর সাথে কাজ করতে গিয়ে দেখালেন যে DFS আসলে গ্রাফের গঠন সম্পর্কে অনেক গভীর তথ্য দেয় — যদি আপনি প্রতিটি vertex-এ কখন ঢুকলেন আর edge-গুলো কোন ধরনের (tree, back, cross), তা খেয়াল রাখেন।
১৯৭২ সালে SIAM Journal on Computing-এ প্রকাশিত “Depth-first search and linear graph algorithms” প্রবন্ধে Tarjan দুটি সমস্যার রৈখিক সময়ের সমাধান দেন: directed গ্রাফের strongly connected component, আর undirected গ্রাফের biconnected component. দুটিতেই একই কৌশল — DFS-এর ক্রমিক নম্বর আর “lowpoint” বা low-link মান। আগে এমন সমস্যায় প্রায়ই O(V²) বা তার বেশি সময় লাগত; এখন লাগল গ্রাফটি একবার পড়ার সময়। Hopcroft আর Tarjan একই বছরগুলোতে biconnected component-এর আরেকটি বিবরণ প্রকাশ করেন, আর ১৯৭৪ সালে দেন planarity testing-এর রৈখিক সময়ের অ্যালগরিদম — একটি গ্রাফকে কাগজে কোনো দুটি edge না কেটে আঁকা যায় কি না, যা বৈদ্যুতিক সার্কিটের নকশায় গুরুত্বপূর্ণ। এরও ভিত্তি DFS.
এই ধারার কাজের স্বীকৃতি আসে ১৯৮৬ সালে: Hopcroft আর Tarjan যৌথভাবে Turing Award পান — অ্যালগরিদম আর ডেটা স্ট্রাকচারের নকশা ও বিশ্লেষণে মৌলিক অবদানের জন্য। Tarjan-এর নাম এই কোর্সে বারবার আসবে: পর্ব ৩৩-এর union-find-এর α(n) বিশ্লেষণ, splay tree, Fibonacci heap (Fredman-এর সাথে), offline LCA, আর এই পর্বের bridge (১৯৭৪ সালের একটি ছোট নোটে তিনি bridge খোঁজার পদ্ধতিও প্রকাশ করেন).
প্রবন্ধটির আসল শিক্ষা ছিল একটি দৃষ্টিভঙ্গি: “রৈখিক সময়” — O(V + E) — কে লক্ষ্য হিসেবে নেওয়া। কোনো অ্যালগরিদম এর চেয়ে দ্রুত হতে পারে না, কারণ গ্রাফটি অন্তত একবার পড়তে হয়। আজ আমরা ধরে নিই যে SCC বা bridge O(V + E)-এ হবে, কিন্তু সেই প্রত্যাশাটিই তৈরি করেছে এই কাজ।
মূল উপলব্ধি: DFS শুধু ঘোরার পদ্ধতি নয় — disc, finish আর low-এর মতো কয়েকটি সংখ্যা রাখলে সে গ্রাফের গোপন কাঠামো (SCC, bridge, block, planarity) একবারের হাঁটাতেই বলে দেয়।
প্র ০৩দুটি অ্যালগরিদমই O(V + E) — তাহলে বাস্তবে Kosaraju না Tarjan? আর কোটি কোটি পাতার ওয়েব-গ্রাফে SCC কেমন দেখায়?
কাগজে দুটি সমান, কিন্তু প্রকৌশলে পার্থক্য আছে। Kosaraju গ্রাফটি দুবার পড়ে আর একটি পূর্ণ উল্টো কপি বানায় — E টি edge-এর জন্য দ্বিগুণ মেমরি, আর মেমরিতে ছড়ানো ডেটা দুবার ঘোরায় cache-এর দিক থেকেও খরচ বেশি। Tarjan এক পাসে কাজ সারে, বাড়তি শুধু O(V). তাই বড় গ্রাফে Tarjan সাধারণত দ্রুত। অন্যদিকে Kosaraju-র কোড শুধু দুটি সাধারণ DFS — ভুলের সুযোগ কম, আর পরীক্ষায় বা কনটেস্টে চাপের মধ্যে অনেকে এটিই বেছে নেন। Kosaraju-র আরেকটি সুবিধা: SCC বের হয় condensation-এর topological ক্রমে, যা কিছু সমস্যায় সরাসরি কাজে লাগে (Tarjan দেয় উল্টো ক্রম — তাও এক লাইনে উল্টানো যায়).
আরেকটি পরিবার আছে: path-based অ্যালগরিদম। Edsger Dijkstra ১৯৭৬ সালের বই A Discipline of Programming-এ এমন একটি পদ্ধতি দেন, আর Harold Gabow ২০০০ সালে একটি পরিচ্ছন্ন রূপ প্রকাশ করেন, যা low-এর বদলে দুটি stack রাখে। আর খুব বড় গ্রাফে, যেখানে একটি মেশিন যথেষ্ট নয়, DFS-কে সমান্তরাল (parallel) করা কঠিন — DFS স্বভাবতই একটির পর একটি। তখন “forward–backward” ধাঁচের পদ্ধতি চলে: একটি vertex থেকে সামনের দিকে আর পেছনের দিকে BFS চালিয়ে দুটির ছেদ একটি SCC, বাকি অংশে একই কাজ সমান্তরালে।
ওয়েবের কথা। ২০০০ সালে Andrei Broder ও সহকর্মীরা AltaVista সার্চ ইঞ্জিনের সংগ্রহ করা বিশাল লিংক-গ্রাফে SCC বিশ্লেষণ করে “Graph structure in the Web” প্রবন্ধে একটি বিখ্যাত ছবি দেন — “bow-tie” বা বো-টাইয়ের আকার। মাঝখানে একটি বিশাল SCC (মোট পাতার প্রায় এক-চতুর্থাংশ), যার ভেতরের যেকোনো পাতা থেকে লিংক ধরে যেকোনো পাতায় যাওয়া যায়। এক পাশে IN — যেখান থেকে বিশাল SCC-তে পৌঁছানো যায়, ফেরা যায় না; অন্য পাশে OUT — যেখানে SCC থেকে পৌঁছানো যায়, কিন্তু সেখান থেকে আর ফেরা যায় না (যেমন কোনো প্রতিষ্ঠানের ভেতরের পাতা)। আর আছে “tendril” আর বিচ্ছিন্ন টুকরো। এই কাঠামো ব্যাখ্যা করে কেন কিছু পাতা crawler কখনো খুঁজে পায় না, আর PageRank-এর মতো হিসাবে কেন “জাম্প” যোগ করতে হয়।
মূল উপলব্ধি: একই asymptotic সময়ের দুটি অ্যালগরিদমের মধ্যে বাছাই নির্ভর করে মেমরি, সরলতা, আউটপুটের ক্রম আর সমান্তরালতার ওপর — আর SCC-ভাগ বিশাল বাস্তব নেটওয়ার্কের গঠন বোঝার প্রথম মানচিত্র।
প্র ০৪3-SAT NP-complete, অথচ 2-SAT রৈখিক সময়ে — শর্তে মাত্র একটি literal কম হলে সমস্যা এত সহজ হয়ে যায় কেন?
১৯৭১ সালে Stephen Cook প্রমাণ করেন যে সাধারণ boolean satisfiability (SAT) NP-complete — ইতিহাসের প্রথম এমন সমস্যা; Leonid Levin স্বাধীনভাবে সোভিয়েত ইউনিয়নে একই ধারণায় পৌঁছান। Cook-এর প্রবন্ধেই দেখা যায় যে প্রতি শর্তে তিনটি literal-এর 3-SAT-ও সমান কঠিন। অথচ প্রতি শর্তে দুটি literal হলে? Melven Krom ১৯৬৭ সালেই এমন সূত্র (যাদের আজও কখনো “Krom formula” বলা হয়) নিয়ে কাজ করেছিলেন, আর ১৯৭৯ সালে Bengt Aspvall, Michael Plass ও Robert Tarjan দেখান যে implication graph-এর SCC দিয়ে 2-SAT রৈখিক সময়ে সমাধান হয় — এই পর্বের পদ্ধতিটিই।
পার্থক্যের মূলে আছে implication-এর আকার। দুই literal-এর শর্ত (x ∨ y) বলে: “x মিথ্যা হলে y সত্য হতেই হবে” — একটি নিশ্চিত, একক পরিণতি। তাই একটি সিদ্ধান্ত নিলে তার ফল শিকলের মতো এগোয়, কোনো শাখা নেই; পুরো যুক্তিটি একটি directed গ্রাফে পথ খোঁজা। কিন্তু তিন literal-এর শর্ত (x ∨ y ∨ z) বলে: “x মিথ্যা হলে y বা z সত্য” — এখানে পরিণতি একটি বিকল্প, কোনটি সত্য তা অনির্ধারিত। প্রতিটি সিদ্ধান্ত গাছের মতো শাখা ছড়ায়, আর সবচেয়ে খারাপ ক্ষেত্রে শাখাগুলো সূচকীয় হারে বাড়ে। গ্রাফের একটি পথ আর এই পরিস্থিতি ধরতে পারে না।
একই ধাঁচ আরও অনেক জায়গায়। গ্রাফকে দুই রঙে রাঙানো (bipartite পরীক্ষা, পর্ব ৩৩-এর parity DSU) সহজ, তিন রঙে রাঙানো NP-complete. দুই-মাত্রিক matching সহজ (পর্ব ৪৭), তিন-মাত্রিক NP-complete. “২” প্রায়ই সেই সীমা, যেখানে প্রতিটি পছন্দ একটি নিশ্চিত পরিণতি দেয়।
বাস্তবে 3-SAT “কঠিন” বলে অচল নয়: আধুনিক SAT solver (CDCL — conflict-driven clause learning) লাখো চলকের শিল্প-সমস্যাও প্রায়ই দ্রুত সমাধান করে — চিপের নকশা যাচাই, সফটওয়্যারের প্যাকেজ-নির্ভরতা মেলানো, সময়সূচি। মজার ব্যাপার, এই solver-গুলোর ভেতরে “unit propagation” নামে যে কৌশল সবচেয়ে বেশি সময় নেয়, সেটি আসলে implication-এর শিকল ধরে এগোনো — ঠিক 2-SAT-এর মতো, শুধু যখন একটি শর্তের বাকি সব literal মিথ্যা হয়ে একটিই বাকি থাকে। NP-hard সমস্যার মোকাবিলা পর্ব ৬৫-এ।
মূল উপলব্ধি: জটিলতার সীমারেখা প্রায়ই খুব সরু — একটি literal কম হলে “বিকল্প” হয়ে যায় “নিশ্চিত পরিণতি”, আর সূচকীয় অনুসন্ধান নেমে আসে একটি গ্রাফের SCC-তে।
প্র ০৫বাস্তবের অবকাঠামোয় bridge আর articulation point কোথায় — দেশের ইন্টারনেট থেকে বিদ্যুৎ গ্রিড পর্যন্ত, আর প্রকৌশলীরা কীভাবে তা দূর করেন?
একটি নেটওয়ার্ক যত বড়ই হোক, একটি bridge থাকলে তার নির্ভরযোগ্যতা সেই একটি সংযোগের সমান। বাংলাদেশের আন্তর্জাতিক ইন্টারনেট সংযোগ এর একটি পরিচিত উদাহরণ। ২০০৬ সালের দিকে দেশটি SEA-ME-WE 4 সাবমেরিন কেবলের সাথে যুক্ত হয়, কক্সবাজারের ল্যান্ডিং স্টেশন দিয়ে। কয়েক বছর এটিই ছিল প্রধান আন্তর্জাতিক পথ — কেবলে কোথাও সমস্যা হলে বা মেরামত চললে সারা দেশের ইন্টারনেট ব্যাপকভাবে ধীর বা বিচ্ছিন্ন হয়ে যেত। গ্রাফের ভাষায় সেই কেবল ছিল দেশ আর বাকি বিশ্বের মধ্যে প্রায় একটি bridge, আর ল্যান্ডিং স্টেশন একটি articulation point. পরে স্থলপথে প্রতিবেশী দেশের মধ্য দিয়ে সংযোগ (ITC) আর ২০১৭ সালের দিকে কুয়াকাটায় দ্বিতীয় সাবমেরিন কেবল SEA-ME-WE 5 যুক্ত হওয়ায় একাধিক বিকল্প পথ তৈরি হয় — bridge আর bridge থাকে না।
বিদ্যুৎ খাতে একই ভাবনার নাম “N − 1 নীতি”: গ্রিড এমনভাবে নকশা করা হয় যাতে যেকোনো একটি লাইন বা একটি ট্রান্সফরমার বিকল হলেও সব গ্রাহক বিদ্যুৎ পান — অর্থাৎ গ্রিডের সংযোগ-গ্রাফে কোনো bridge বা articulation point না থাকা (আর তার সাথে লাইনের ধারণক্ষমতার হিসাব)। টেলিকম নেটওয়ার্কে এই কারণেই ring topology এত জনপ্রিয়: অপটিক্যাল ফাইবার একটি বৃত্তে সাজালে যেকোনো এক জায়গায় কাটা পড়লে ডেটা উল্টো দিক ঘুরে যায় — একটি cycle-এ কোনো bridge থাকে না। ঢাকার রাস্তার কোনো ফাইবার কাটা পড়লেও তাই অনেক সময় গ্রাহক টেরই পান না।
গ্রাফ তত্ত্বে এর গভীর রূপ হলো k-connectivity আর Menger-এর উপপাদ্য (১৯২৭): দুটি vertex-কে আলাদা করতে কমপক্ষে যতগুলো vertex সরাতে হয়, ঠিক ততগুলো পরস্পর-বিচ্ছিন্ন পথ তাদের মধ্যে আছে। Bridge-বিহীন মানে প্রতিটি জোড়ার মধ্যে অন্তত দুটি edge-বিচ্ছিন্ন পথ; articulation point-বিহীন (biconnected) মানে অন্তত দুটি vertex-বিচ্ছিন্ন পথ। “ন্যূনতম কয়টি নতুন সংযোগ যোগ করলে কোনো bridge থাকবে না” — এই প্রশ্নের উত্তরও আসে bridge tree-র পাতা (leaf) গুনে। আর গ্রাফ তত্ত্বের আদি যুগের একটি অ্যালগরিদমেও bridge-এর ধারণা আছে: Euler পথ আঁকার Fleury-র নিয়ম (১৮৮৩) — “বিকল্প থাকলে কখনো bridge পার হবেন না”.
মূল উপলব্ধি: Bridge আর articulation point হলো নেটওয়ার্কের “single point of failure” — এক DFS-এ, O(V + E)-এ এদের খুঁজে বের করা যায়, আর বিকল্প পথ যোগ করে এদের দূর করাই নির্ভরযোগ্য অবকাঠামো নকশার প্রথম পাঠ্য নীতি।
অনুশীলন
খাতা-কলম নিন। প্রথম তিনটি হাতে করার, শেষটিতে কোড লিখতে হবে।
-
হাতে trace করুন (Kosaraju): পাঁচটি vertex, adjacency list
0: 1,1: 0, 2,2: 3,3: 2, 4,4: (কিছু নেই). প্রথম পাসের finish-এর ক্রম, উল্টো গ্রাফের adjacency list (u-র ক্রমে edge উল্টে যোগ করে), আর দ্বিতীয় পাসে পাওয়া SCC-গুলো লিখুন।প্রথম পাস (0 থেকে): 0 → 1; 1 → 0 দেখা; 1 → 2 → 3; 3 → 2 দেখা; 3 → 4, 4 শেষ। তারপর 3, 2, 1, 0 শেষ। Finish-এর ক্রম
4 3 2 1 0, দ্বিতীয় পাসের ক্রম0 1 2 3 4.উল্টো গ্রাফ:
0: 1,1: 0,2: 1, 3,3: 2,4: 3. দ্বিতীয় পাস: 0 থেকে 0 → 1 → (0 দেখা) — SCC {0, 1}. 1 নেওয়া। 2 থেকে 2 → 1 (নেওয়া), 2 → 3 → 2 — SCC {2, 3}. 3 নেওয়া। 4 থেকে 4 → 3 (নেওয়া) — SCC {4}. Condensation: {0,1} → {2,3} → {4}, একটি শিকল, আর SCC-গুলো ঠিক এই topological ক্রমে বেরিয়েছে। -
হাতে trace করুন (low-link): undirected গ্রাফ, edge-গুলো id-সহ: e0 = 0–1, e1 = 1–2, e2 = 2–3, e3 = 3–1, e4 = 3–4. প্রতিবেশীরা edge id-এর ক্রমে দেখা হয়। 0 থেকে DFS চালিয়ে disc আর low লিখুন, তারপর সব bridge আর articulation point বের করুন।
DFS: 0 (disc 0) → 1 (1) → 2 (2) → 3 (3). 3-এর কাছে e2 আসার edge, বাদ; e3 দিয়ে 1: back edge, low[3] = 1. e4 দিয়ে 4 (disc 4): 4-এর আর কোনো edge নেই, low[4] = 4. 3-এ ফিরে child 4: 4 > 3 — 3–4 bridge, আর 4 ≥ 3 — 3 articulation point. 2-এ ফিরে low[2] = 1; child 3: 1 > 2 না, 1 ≥ 2 না। 1-এ ফিরে low[1] = min(1, 1) = 1; তারপর e3 দিয়ে 3 আগে দেখা: low[1] = min(1, 3) = 1. Child 2: low[2] = 1 ≥ disc[1] = 1 — 1 articulation point; 1 > 1 না, 1–2 bridge নয়। 0-তে ফিরে child 1: low[1] = 1 > 0 — 0–1 bridge. 0 root, একটাই child, তাই articulation point নয়।
চূড়ান্ত: disc =
[0, 1, 2, 3, 4], low =[0, 1, 1, 1, 4]; bridge 0–1 আর 3–4; articulation point 1 আর 3. যাচাই: 1 সরালে 0 একা, 3 সরালে 4 একা — ঠিক। -
ভেবে বলুন (2-SAT): (ক) শর্ত (x ∨ y), (¬x ∨ y), (x ∨ ¬y)-এর implication graph বানিয়ে SCC বের করুন আর একটি সমাধান দিন। (খ) চতুর্থ শর্ত (¬x ∨ ¬y) যোগ করলে কী হয়, আর কেন?
(ক) Edge: (x ∨ y) → ¬x → y, ¬y → x; (¬x ∨ y) → x → y, ¬y → ¬x; (x ∨ ¬y) → ¬x → ¬y, y → x. SCC: x → y → x, তাই {x, y}; ¬x → ¬y → ¬x, তাই {¬x, ¬y}. কেউ নিজের উল্টোর সাথে নেই — সমাধান আছে। Condensation: {¬x, ¬y} → {x, y} (¬x → y edge দিয়ে), তাই {x, y} পরে: x = true, y = true. যাচাই: তিনটি শর্তই সত্য।
(খ) নতুন edge x → ¬y আর y → ¬x. এখন x → ¬y → x (¬y → x আগে থেকেই ছিল), আর x → ¬y → ¬x → y → x — x আর ¬x একই SCC-তে: সমাধান নেই. সহজ ভাষায়, চারটি শর্ত মিলে x আর y-এর চারটি সম্ভাব্য মানের প্রতিটিকেই নিষেধ করেছে — প্রতিটি শর্ত ঠিক একটি জোড়াকে বাদ দেয়।
-
কোড লিখুন: একটি directed গ্রাফ strongly connected নয়। ন্যূনতম কয়টি নতুন একমুখী রাস্তা যোগ করলে এটি strongly connected হবে? জানা ফল (Eswaran ও Tarjan, ১৯৭৬): SCC একটির বেশি হলে উত্তর max(source SCC-র সংখ্যা, sink SCC-র সংখ্যা), নইলে 0. Tarjan-এর কোডে একটি
comp[v]অ্যারে যোগ করে উত্তরটি বের করুন, আর এই পর্বের মানচিত্রে হাতে মেলান।Tarjan-এর pop-এর লুপে প্রতিটি w-এর জন্য
comp[w] = comps.size() - 1(C++) বাcomp[w] = len(comps)(Python, append-এর আগে) বসান। তারপর condensation-এর in-degree আর out-degree গুনুন:// after Tarjan: comp[v] = index of v's SCC, k = comps.size() int k = comps.size(); if (k == 1) { cout << 0 << '\n'; return 0; } vector<int> in(k), out(k); for (int u = 0; u < n; ++u) for (int v : adj[u]) if (comp[u] != comp[v]) { ++out[comp[u]]; ++in[comp[v]]; } // edge between two SCCs int sources = count(in.begin(), in.end(), 0), sinks = count(out.begin(), out.end(), 0); cout << max(sources, sinks) << '\n';হাতে: condensation A → B, A → C, B → C. Source শুধু A (কেউ ঢোকে না), sink শুধু C (কেউ বের হয় না) — উত্তর 1. যেমন 6 → 0 রাস্তাটি যোগ করলে 6 থেকে 0-তে, তারপর 0 → 1 → 3 → 4 → 5 → 6 — সবাই একটি SCC. গুনতে বাড়তি O(V + E), মোট O(V + E). সাবধান: একই দুই SCC-র মধ্যে অনেক edge থাকলে in/out একাধিকবার বাড়ে, কিন্তু আমরা শুধু “শূন্য কি না” দেখি, তাই সমস্যা নেই।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমSCC দিয়ে একটি 2-SAT সূত্র সমাধানের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- প্রতিটি শর্ত (x ∨ y) থেকে দুটি edge ¬x → y আর ¬y → x যোগ করে implication graph বানানো
- Implication graph-এ Tarjan বা Kosaraju চালিয়ে সব SCC বের করা
- প্রতিটি চলকের জন্য দেখা x আর ¬x একই SCC-তে কি না — কোথাও হলে “সমাধান নেই” বলে থামা
- SCC-গুলোর topological ক্রম ধরে মান বসানো: x-এর SCC ক্রমে পরে হলে x = true
- পাওয়া মানগুলো দিয়ে প্রতিটি শর্ত পরীক্ষা করে নিশ্চিত হওয়া
V টি vertex আর E টি edge-এর একটি directed গ্রাফ adjacency list-এ রাখা আছে। Tarjan-এর অ্যালগরিদমে সব strongly connected component বের করতে মোট সময় কত?
- O(V log V + E)
- O(V + E)
- O(V · E)
- O(V²)
একটি undirected গ্রাফের DFS tree-তে u হলো v-এর parent. Tree edge (u, v) কোন শর্তে bridge?
- low[v] ≥ disc[u]
- disc[v] > disc[u]
- low[v] > disc[u]
- low[u] > disc[v]
একটি connected undirected গ্রাফে DFS-এর root r কখন articulation point?
- r-এর কোনো child v-এর জন্য low[v] ≥ disc[r] হলে
- DFS tree-তে r-এর অন্তত দুটি child থাকলে
- গ্রাফে r-এর degree অন্তত 2 হলে
- root কখনো articulation point হয় না
একটি 2-SAT সূত্রের implication graph-এ কোনো চলক x-এর জন্য x আর ¬x একই strongly connected component-এ আছে। কী বলা যায়?
- সূত্রটির কোনো সমাধান নেই
- যেকোনো সমাধানে x = true
- যেকোনো সমাধানে x = false
- বাকি চলকগুলো না দেখে কিছু বলা যায় না
Kosaraju-র অ্যালগরিদমের দ্বিতীয় পাসে কী করা হয়?
- মূল গ্রাফে, finish time-এর উল্টো ক্রমে DFS
- উল্টো গ্রাফে, finish time-এর বাড়তি ক্রমে DFS
- উল্টো গ্রাফে, vertex নম্বরের ক্রমে BFS
- উল্টো গ্রাফে, সবচেয়ে বড় finish time থেকে শুরু করে কমতে থাকা ক্রমে DFS
একটি undirected গ্রাফে u আর v-এর মধ্যে দুটি সমান্তরাল edge আছে, আর এই দুটি ছাড়া u থেকে v-তে অন্য কোনো পথ নেই। এই edge দুটি সম্পর্কে কোনটি সত্য?
- দুটিই bridge
- DFS-এ যেটি tree edge হয়, শুধু সেটি bridge
- কোনোটিই bridge নয়
- বাকি গ্রাফ না জানলে বলা যায় না
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- SCC = পরস্পর reachable vertex-এর সবচেয়ে বড় দল; প্রতিটি vertex ঠিক একটি SCC-তে; condensation সবসময় DAG.
- Kosaraju: G-তে DFS করে finish-এর ক্রম → উল্টো গ্রাফে সবচেয়ে বড় finish থেকে DFS; প্রতিটি DFS tree একটি SCC, topological ক্রমে।
- Tarjan: এক DFS, disc/low আর stack; low শুধু stack-এ থাকা vertex থেকে; low[u] == disc[u] হলে u পর্যন্ত pop = একটি SCC. দুটোই O(V + E).
- 2-SAT: (x ∨ y) → ¬x → y, ¬y → x; x আর ¬x একই SCC-তে হলে সমাধান নেই; নইলে topological ক্রমে পরের literal সত্য।
- Bridge: low[v] > disc[u]; articulation point: low[v] ≥ disc[u], root হলে ≥ ২টি child; parent edge id বাদ দিন, vertex নয়।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৪৭ · Network flow, min cut আর bipartite matchingপরবর্তী পর্বধারণক্ষমতাসহ গ্রাফ, augmenting path আর max-flow min-cut.
- ডেটা স্ট্রাকচার ও অ্যালগরিদম · কোর্স হোম৭২টি পর্বপুরো কোর্সের রোডম্যাপ — নয়টি মডিউল।
- পর্ব ৪১ · Depth-first search (DFS)আগের ভিত্তিdisc, finish time আর edge-এর শ্রেণিভাগ — এই পর্বের সব অ্যালগরিদমের ভিত্তি।
- পর্ব ৪২ · Topological sort আর DAGআগের ভিত্তিCondensation DAG-এর ওপর কাজ করার জন্য দরকারি।
- C++ প্রোগ্রামিং · STL algorithmপাশের ট্র্যাক
min,countআর কনটেস্টে দরকারি তৈরি অ্যালগরিদম। - Python প্রোগ্রামিং · অ্যালগরিদম ও কনটেস্টে Pythonপাশের ট্র্যাকRecursion-এর সীমা আর বড় DFS-এর কৌশল।