পাঠ ৩২ · ৫৭-এর মধ্যে · মডিউল ৭

স্ট্রংলি কানেক্টেড কম্পোনেন্ট — টার্জান ও কোসারাজু

Strongly connected components — Tarjan's and Kosaraju's algorithms
১১ মিনিট পড়া মধ্যম-কঠিন · Intermediate-Advanced Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • SCC-এর আনুষ্ঠানিক সংজ্ঞা এবং কেন এটি একটি সমতুল্যতা সম্পর্ক (equivalence relation) তৈরি করে
  • কোসারাজু অ্যালগরিদমের সম্পূর্ণ যুক্তি ও ইমপ্লিমেন্টেশন — কেন রিভার্স গ্রাফে ফিনিশ-অর্ডারে DFS চালালে সঠিক কম্পোনেন্ট পাওয়া যায়
  • টার্জানের অ্যালগরিদমের low-link পদ্ধতির সংক্ষিপ্ত ধারণা (তুলনার জন্য)
  • একটি স্বাধীন ব্রুট-ফোর্স মিউচুয়াল-রিচেবিলিটি চেক দিয়ে SCC আউটপুট যাচাই করার পদ্ধতি

১ · SCC কী এবং কেন এটি একটি সমতুল্যতা সম্পর্ক

দুটো ভার্টেক্স $u, v$ পারস্পরিকভাবে পৌঁছানোযোগ্য (mutually reachable) যদি $u$ থেকে $v$-এ যাওয়ার একটি ডাইরেক্টেড পাথ থাকে এবং $v$ থেকে $u$-এ যাওয়ার একটি ডাইরেক্টেড পাথ থাকে। একটি স্ট্রংলি কানেক্টেড কম্পোনেন্টSCCভার্টেক্সদের একটি সর্বোচ্চ (maximal) সেট, যার মধ্যে প্রতিটি জোড়া পারস্পরিকভাবে পৌঁছানোযোগ্য। হলো ভার্টেক্সদের এমন একটি সর্বোচ্চ সেট যেখানে প্রতিটি জোড়া পারস্পরিকভাবে পৌঁছানোযোগ্য। "মিউচুয়াল রিচেবিলিটি" একটি সমতুল্যতা সম্পর্ক (reflexive, symmetric, transitive) — তাই এটি স্বাভাবিকভাবেই পুরো ভার্টেক্স সেটকে ডিসজয়েন্ট SCC-তে পার্টিশন করে দেয়। যদি প্রতিটি SCC-কে একটি একক "সুপার-ভার্টেক্স" হিসেবে সংকুচিত করা হয় (condensation), ফলাফল গ্রাফটি সবসময় একটি DAG হয় — এই সংযোগটি L31-এর টপোলজিক্যাল সর্টের সাথে SCC-কে সরাসরি সম্পর্কিত করে।

২ · কোসারাজু অ্যালগরিদম — দুই-পাস DFS

কোসারাজু অ্যালগরিদমের মূল কৌশল তিনটি ধাপে:

পাস ১ — ফিনিশ অর্ডার
মূল গ্রাফে সব ভার্টেক্সের উপর DFS চালিয়ে প্রতিটি ভার্টেক্স "ফিনিশ" হওয়ার ক্রম রেকর্ড করা (একটি স্ট্যাকে পুশ)।
পাস ২ — গ্রাফ রিভার্স করা
প্রতিটি এজ $(u,v)$-কে $(v,u)$-তে উল্টে একটি নতুন রিভার্স গ্রাফ $G^T$ তৈরি করা।
পাস ৩ — রিভার্স অর্ডারে DFS
ফিনিশ-অর্ডারের বিপরীত ক্রমে (সবচেয়ে বেশি ফিনিশ-টাইমযুক্ত ভার্টেক্স আগে) $G^T$-এ DFS চালিয়ে, প্রতিটি DFS ট্রি ঠিক একটি SCC গঠন করে।

কেন এটি কাজ করে (সংক্ষিপ্ত যুক্তি): মূল গ্রাফের কন্ডেন্সেশন একটি DAG, এবং পাস ১-এর ফিনিশ-অর্ডার (L31-এর DFS-ভিত্তিক টপোলজিক্যাল সর্টের যুক্তি অনুযায়ী) কন্ডেন্সেশন DAG-এর একটি "রিভার্স টপোলজিক্যাল" ধারণা দেয় — সবচেয়ে বেশি ফিনিশ-টাইমযুক্ত SCC কন্ডেন্সেশন DAG-এ "সবার আগে" (কোনো SCC তার দিকে নির্দেশ করছে না এমন)। রিভার্স গ্রাফে এই ক্রমে DFS চালালে, প্রতিটি DFS একটি নির্দিষ্ট SCC-এর বাইরে "লিক" করতে পারে না, কারণ কন্ডেন্সেশন DAG-এ সেই দিকের এজ রিভার্স করার পর অন্য কোনো SCC-এ পৌঁছানোর পথ থাকে না (থাকলে সেটি একটি সাইকেল তৈরি করত, যা কন্ডেন্সেশন DAG-এর অ্যাসাইক্লিক ধর্মের বিরোধী)।

মূল গ্রাফে DFS — ফিনিশ অর্ডার রেকর্ড গ্রাফের প্রতিটি এজ রিভার্স করা রিভার্স ফিনিশ-অর্ডারে রিভার্স গ্রাফে DFS — প্রতিটি ট্রি = একটি SCC SCC লিস্ট
তিনটি ধাপই $O(V+E)$ সময়ে চলে, তাই সম্পূর্ণ অ্যালগরিদম $\Theta(V+E)$।
টার্জানের অ্যালগরিদম — সংক্ষিপ্ত তুলনা

টার্জানের অ্যালগরিদম একই ফলাফল দেয় কিন্তু একটি সিঙ্গেল DFS পাসে — প্রতিটি ভার্টেক্সের জন্য একটি discovery time ও একটি low-link value (সেই ভার্টেক্স থেকে DFS-ট্রি-এজ ও অন্তত একটি ব্যাক-এজ ব্যবহার করে পৌঁছানো সবচেয়ে ছোট discovery time) রক্ষণাবেক্ষণ করে। যখন কোনো ভার্টেক্সের low-link তার নিজের discovery time-এর সমান হয়, সেটি একটি SCC-এর "মূল" — তখন একটি সহায়ক স্ট্যাক থেকে সেই SCC-এর সব ভার্টেক্স পপ করা হয়। এটি গ্রাফ রিভার্স করার প্রয়োজন এড়ায় (তাই সামান্য কম মেমরি/কনস্ট্যান্ট-ফ্যাক্টর ভালো), কিন্তু low-link আপডেট লজিক কোসারাজুর তুলনায় বোঝা কঠিন — তাই এই পাঠে আমরা কোসারাজু সম্পূর্ণভাবে ইমপ্লিমেন্ট করছি, যা যাচাই করাও সহজ।

৩ · ইমপ্লিমেন্টেশন ও ব্রুট-ফোর্স যাচাই

কোসারাজু ইমপ্লিমেন্ট করার পর, আমরা একটি সম্পূর্ণ স্বাধীন যাচাই লিখব: দাবিকৃত প্রতিটি SCC-এর ভেতরে প্রতিটি ভার্টেক্স-জোড়ার জন্য মূল গ্রাফে সরাসরি DFS/BFS-স্টাইল রিচেবিলিটি চেক করে মিউচুয়াল রিচেবিলিটি নিশ্চিত করব — এবং ভিন্ন SCC-তে থাকা কোনো জোড়া যেন ভুলবশত মিউচুয়ালি রিচেবল না হয় তাও চেক করব।

Python
def kosaraju_scc(graph, vertices):
    visited = set()
    finish_order = []

    def dfs_pass1(start):
        stack = [(start, iter(graph.get(start, [])))]
        visited.add(start)
        while stack:
            node, neighbors = stack[-1]
            advanced = False
            for v in neighbors:
                if v not in visited:
                    visited.add(v)
                    stack.append((v, iter(graph.get(v, []))))
                    advanced = True
                    break
            if not advanced:
                finish_order.append(node)
                stack.pop()

    for v in vertices:
        if v not in visited:
            dfs_pass1(v)

    reverse_graph = {v: [] for v in vertices}
    for u in vertices:
        for v in graph.get(u, []):
            reverse_graph[v].append(u)

    visited2 = set()
    components = []

    def dfs_pass2(start):
        comp = [start]
        visited2.add(start)
        stack = [start]
        while stack:
            node = stack.pop()
            for v in reverse_graph.get(node, []):
                if v not in visited2:
                    visited2.add(v)
                    comp.append(v)
                    stack.append(v)
        return comp

    for v in reversed(finish_order):
        if v not in visited2:
            components.append(dfs_pass2(v))

    return components

def reachable_set(graph, source, vertices):
    # সম্পূর্ণ স্বাধীন সরল DFS -- কোসারাজুর কোনো ধারণা (ফিনিশ অর্ডার, রিভার্স গ্রাফ) ব্যবহার করে না
    visited = {source}
    stack = [source]
    while stack:
        u = stack.pop()
        for v in graph.get(u, []):
            if v not in visited:
                visited.add(v)
                stack.append(v)
    return visited

def verify_sccs_bruteforce(graph, vertices, components):
    reach = {v: reachable_set(graph, v, vertices) for v in vertices}
    ok = True
    # চেক ১: একই দাবিকৃত SCC-এর ভেতরে প্রতিটি জোড়া মিউচুয়ালি রিচেবল হতেই হবে
    for comp in components:
        for u in comp:
            for v in comp:
                if v not in reach[u] or u not in reach[v]:
                    ok = False
                    print(f"  FAIL: {u} ও {v} মিউচুয়ালি রিচেবল নয় কিন্তু একই SCC-তে দাবি করা হয়েছে!")
    # চেক ২: ভিন্ন দাবিকৃত SCC-এর কোনো জোড়া যেন মিউচুয়ালি রিচেবল না হয়
    for i in range(len(components)):
        for j in range(len(components)):
            if i == j:
                continue
            u = components[i][0]
            for v in components[j]:
                if v in reach[u] and u in reach[v]:
                    ok = False
                    print(f"  FAIL: {u} (SCC {i}) ও {v} (SCC {j}) মিউচুয়ালি রিচেবল কিন্তু ভিন্ন SCC-তে দাবি করা হয়েছে!")
    return ok

test_graphs = [
    {
        "name": "একাধিক সাইকেলসহ গ্রাফ (প্রত্যাশিত ৪টি SCC)",
        "vertices": list(range(8)),
        "edges": {0: [1], 1: [2], 2: [0, 3], 3: [4], 4: [5], 5: [6], 6: [4, 7], 7: []},
    },
    {
        "name": "একটি DAG (কোনো সাইকেল নেই, প্রতিটি ভার্টেক্স তার নিজস্ব SCC)",
        "vertices": [0, 1, 2, 3],
        "edges": {0: [1], 1: [2], 2: [], 3: [1]},
    },
]

for t in test_graphs:
    comps = kosaraju_scc(t["edges"], t["vertices"])
    print(f"\n{t['name']}")
    print(f"  কোসারাজুর SCC: {comps}")
    verified = verify_sccs_bruteforce(t["edges"], t["vertices"], comps)
    print(f"  ব্রুট-ফোর্স মিউচুয়াল-রিচেবিলিটি যাচাই পাস: {verified}")
    assert verified

    
প্রথম টেস্ট গ্রাফে {0,1,2} একটি সাইকেল তৈরি করে (0→1→2→0), {4,5,6} আরেকটি (4→5→6→4), আর 3 ও 7 প্রতিটি নিজের একক-ভার্টেক্স SCC (কোনো ফেরত পথ নেই)। দ্বিতীয় গ্রাফে কোনো সাইকেল না থাকায় (এটি একটি DAG) প্রতিটি ভার্টেক্স তার নিজের SCC — reachable_set ফাংশনটি কোসারাজুর নিজস্ব কোনো ডেটা স্ট্রাকচার (ফিনিশ অর্ডার, রিভার্স গ্রাফ) পুনরায় ব্যবহার করে না, তাই এটি একটি সত্যিকারের স্বাধীন ক্রস-চেক।
মূল কথা · Key takeaway

SCC খুঁজে বের করা আসলে "গ্রাফের কোন অংশগুলো একে অপরের উপর সম্পূর্ণ নির্ভরশীল (চক্রাকারে)" প্রশ্নের উত্তর দেয় — ওয়েব পেজ লিংক গ্রাফে "মিউচুয়ালি-লিংকড" ক্লাস্টার খুঁজে বের করা, বা সোশ্যাল নেটওয়ার্কে পারস্পরিক অনুসরণকারী গ্রুপ শনাক্ত করার মতো বাস্তব প্রয়োগ আছে। কন্ডেন্সেশন DAG তৈরি করার পর, L31-এর টপোলজিক্যাল সর্ট সরাসরি প্রয়োগ করে "কোন কম্পোনেন্ট আগে প্রসেস করতে হবে" তা নির্ণয় করা যায়।

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।

প্র ০১ একটি আনডাইরেক্টেড গ্রাফে SCC ধারণাটি কেন অর্থহীন (বা তুচ্ছ)?

একটি আনডাইরেক্টেড গ্রাফে প্রতিটি এজ দুই দিকেই কাজ করে, তাই $u$ থেকে $v$-এ পাথ থাকলে $v$ থেকে $u$-এও স্বয়ংক্রিয়ভাবে পাথ থাকে — অর্থাৎ "মিউচুয়াল রিচেবিলিটি" আর "রিচেবিলিটি"-এর মধ্যে কোনো পার্থক্য থাকে না। তাই আনডাইরেক্টেড গ্রাফে SCC মানে শুধু সাধারণ কানেক্টেড কম্পোনেন্ট — একটি সহজ single-pass DFS/BFS দিয়েই বের করা যায়, কোসারাজু বা টার্জানের মতো জটিল অ্যালগরিদমের প্রয়োজন নেই।

প্র ০২ কেন পাস ১-এ যেকোনো DFS ফিনিশ-অর্ডার ব্যবহার করলেই চলে না — নির্দিষ্টভাবে রিভার্স ফিনিশ-অর্ডারে পাস ৩ চালাতে হয় কেন?

ফিনিশ-অর্ডার কন্ডেন্সেশন DAG-এর একটি "রিভার্স টপোলজিক্যাল" ক্রম দেয় — সবচেয়ে বেশি ফিনিশ-টাইমযুক্ত SCC-টি কন্ডেন্সেশন DAG-তে সবচেয়ে "উপরে" (কোনো ইনকামিং এজ নেই এমন)। যদি আমরা ফিনিশ-অর্ডারেই (রিভার্স না করে) পাস ৩ চালাতাম, তাহলে DFS একটি SCC থেকে শুরু করে অন্য SCC-তে "লিক" করে যেতে পারত (কারণ সেই দিকের এজ তখনও উপস্থিত থাকত মূল গ্রাফে) — রিভার্স গ্রাফে এবং রিভার্স ফিনিশ-অর্ডারে চালানোর সমন্বয়টিই প্রতিটি DFS ট্রিকে ঠিক একটি SCC-এর মধ্যে সীমাবদ্ধ রাখে।

প্র ০৩ উপরের প্রথম টেস্ট গ্রাফে যদি ভার্টেক্স ৭ থেকে ভার্টেক্স ৪-এ একটি নতুন এজ যোগ করা হতো (7→4), তাহলে SCC-এর সংখ্যা কী পরিবর্তন হতো?

বর্তমানে {4,5,6} ইতিমধ্যে একটি সাইকেল, এবং 6→7 এজ আছে কিন্তু 7-এর ফেরত পথ নেই বলে 7 আলাদা SCC। যদি 7→4 যোগ করা হয়, তাহলে 4→5→6→7→4 একটি বড় সাইকেল তৈরি করবে, তাই {4,5,6,7} একটি একক SCC হয়ে যাবে — মোট SCC সংখ্যা ৪ থেকে কমে ৩ হবে ({0,1,2}, {3}, {4,5,6,7})।

অনুশীলন

  1. চিন্তা করুন: দ্বিতীয় টেস্ট গ্রাফে (DAG, 0:[1],1:[2],2:[],3:[1]) যদি একটি নতুন এজ 2 -> 0 যোগ করা হয়, তাহলে কতটি SCC থাকবে এবং কোনগুলো?

    এই নতুন এজটি একটি সাইকেল তৈরি করবে 0→1→2→0, তাই {0,1,2} একটি একক SCC হবে। ভার্টেক্স 3-এর শুধু 1-এর দিকে একটি এজ আছে, ফেরত পথ নেই, তাই এটি আলাদা SCC থাকবে। মোট ২টি SCC: {0,1,2} ও {3}।

  2. পরীক্ষা করুন: কোড সেলে দ্বিতীয় টেস্ট গ্রাফের edges-এ 2: [0] (মূল খালি লিস্টের বদলে) সেট করে Run চেপে আপনার অনুমান যাচাই করুন।

    আউটপুটে দেখা যাবে কোসারাজু এখন ২টি কম্পোনেন্ট দেয় — একটি {0,1,2}-এর কোনো পারমুটেশন ধারণকারী তিন-ভার্টেক্স কম্পোনেন্ট, আরেকটি শুধু [3] — এবং ব্রুট-ফোর্স যাচাই এখনও True রিটার্ন করবে।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

  • কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৭টি পাঠ শর্টেস্ট পাথ, ম্যাক্স ফ্লো, ব্যাকট্র্যাকিং, স্ট্রিং অ্যালগরিদম, অ্যামর্টাইজড অ্যানালাইসিস, NP-কমপ্লিটনেস ও অ্যাপ্রক্সিমেশন — বাকি পাঠগুলো শীঘ্রই যুক্ত হবে।
  • Data Structures & Algorithms কোর্স সহোদর কোর্স গ্রাফ ট্রাভার্সাল ও কানেক্টিভিটির মৌলিক ইমপ্লিমেন্টেশন সেই কোর্সে কভার করা হয়েছে।
  • সব Courses দেখুন ABCL TECH C, C++, Python, Java, JavaScript, DSA, DBMS, Discrete Mathematics, System Design, Cybersecurity, Cloud Computing & DevOps, Computer Networks, Operating Systems, Computer Architecture, Programming Languages & Compiler Design, Software Engineering & Git, Theory of Computation, Engineering Economics, Full-Stack Web Frameworks, Mobile App Development, Ethics in Computing & AI Safety, Software Testing & Quality Assurance ও Design and Analysis of Algorithms — সব এক জায়গায়।
পূর্ববর্তী পাঠ
টপোলজিক্যাল সর্ট