পাঠ ৫১ · ৫৭-এর মধ্যে · মডিউল ১২
Home / Courses / Design and Analysis of Algorithms / ভার্টেক্স কভার অ্যাপ্রক্সিমেশন

ভার্টেক্স কভার ও সেট কভার অ্যাপ্রক্সিমেশন

Vertex cover & set cover approximation
১২ মিনিট পড়া মধ্যম-কঠিন · Intermediate-Advanced Python কোডসহ সম্পূর্ণ বাংলায়

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

  • ভার্টেক্স কভার সমস্যার সংজ্ঞা এবং ম্যাক্সিমাল-ম্যাচিং-ভিত্তিক ২-অ্যাপ্রক্সিমেশন অ্যালগরিদম
  • সম্পূর্ণ প্রমাণ — কেন এই অ্যালগরিদমের ফল কখনো সত্যিকারের অপটিমামের দ্বিগুণের বেশি হয় না
  • প্রমাণটি সত্যিই ধরে কি না তা ব্রুট-ফোর্স এক্স্যাক্ট সলভারের বিপরীতে বহু র‍্যান্ডম গ্রাফে কম্পিউটেশনালি যাচাই
  • সেট কভারে সাধারণীকরণ, এবং কেন এই ধ্রুবক-ফ্যাক্টর গ্যারান্টি সাধারণ সেট কভারে টেকে না

১ · ভার্টেক্স কভার সমস্যা

একটি গ্রাফ $G = (V, E)$ দেওয়া থাকলে, একটি ভার্টেক্স কভারVertex Coverভার্টেক্সের এমন একটি সাবসেট $C \subseteq V$ যেন প্রতিটি এজ $(u,v) \in E$-এর জন্য $u \in C$ অথবা $v \in C$ (অথবা উভয়ই)। হলো ভার্টেক্সের এমন একটি সাবসেট যা গ্রাফের প্রতিটি এজকে "কভার" করে। লক্ষ্য: সবচেয়ে ছোট আকারের এমন একটি সাবসেট খুঁজে বের করা। এই সমস্যাটি ক্লাসিক NP-হার্ড সমস্যাগুলোর একটি (দেখুন Theory of Computation কোর্স-এ এর NP-কমপ্লিটনেসের আনুষ্ঠানিক প্রমাণ)।

২ · ম্যাক্সিমাল-ম্যাচিং-ভিত্তিক ২-অ্যাপ্রক্সিমেশন

অ্যালগরিদমটি আশ্চর্যজনকভাবে সরল:

  1. $C \gets \emptyset$ (খালি কভার) দিয়ে শুরু করো।
  2. যতক্ষণ কোনো এজ বাকি আছে (যা এখনো $C$ দ্বারা কভার হয়নি): যেকোনো একটি এমন এজ $(u,v)$ বেছে নাও, $u$ ও $v$ দুটোকেই $C$-তে যোগ করো।
  3. $u$ বা $v$-এর সাথে যুক্ত সব এজ (কেবল $(u,v)$ নয়, $u$ বা $v$-এর অন্য যেকোনো প্রতিবেশীর সাথের এজও) বাদ দাও, কারণ এরা এখন কভার হয়ে গেছে।
  4. ধাপ ২-৩ পুনরাবৃত্তি করো যতক্ষণ না কোনো এজ বাকি থাকে।

লক্ষ্য করুন — প্রতিবার যে এজগুলো বেছে নেওয়া হয়, সেগুলো স্বয়ংক্রিয়ভাবে পরস্পর ডিসজয়েন্ট (কোনো সাধারণ ভার্টেক্স নেই), কারণ একটি এজ বেছে নেওয়ার সাথে সাথেই তার দুই প্রান্তের সব এজ সরিয়ে ফেলা হয় — তাই পরবর্তী কোনো নির্বাচিত এজ আগের কোনো ভার্টেক্স স্পর্শ করতে পারে না। এই নির্বাচিত এজগুলোর সেট $M$ তাই একটি ম্যাক্সিমাল ম্যাচিংMaximal Matchingএজের এমন একটি সেট যেখানে কোনো দুটি এজ ভার্টেক্স শেয়ার করে না, এবং এতে আর কোনো নতুন এজ যোগ করা সম্ভব নয় (গ্রাফের বাকি প্রতিটি এজ ইতিমধ্যে কোনো ম্যাচড ভার্টেক্স স্পর্শ করে)। — একটি এমন সেট যা আর বড় করা যায় না।

যেকোনো এজ $(u,v)$ বেছে নাও $u$ ও $v$ দুটোই কভার $C$-তে যোগ করো $u$/$v$-স্পর্শী সব এজ বাদ দাও (এখন কভারড) এজ বাকি? হ্যাঁ → ধাপ ১
প্রতিটি রাউন্ডে নির্বাচিত এজগুলো একে অপরের থেকে ডিসজয়েন্ট থাকে — এরাই মিলে তৈরি করে একটি ম্যাক্সিমাল ম্যাচিং $M$, আর কভার $C$-এর আকার হয় ঠিক $2|M|$।

৩ · প্রমাণ — কেন এটি একটি ২-অ্যাপ্রক্সিমেশন

দুটো পর্যবেক্ষণ মিলিয়ে প্রমাণটি সম্পূর্ণ হয়:

নিচের সীমা: $\text{OPT} \ge |M|$
$M$-এর এজগুলো পরস্পর ডিসজয়েন্ট, তাই কোনো একটি ভার্টেক্স দুটো ভিন্ন ম্যাচড এজ কভার করতে পারে না। যেকোনো বৈধ ভার্টেক্স কভারকে তাই প্রতিটি ম্যাচড এজ থেকে আলাদা অন্তত একটি ভার্টেক্স নিতে হবে — অর্থাৎ যেকোনো কভারের আকার অন্তত $|M|$।
উপরের সীমা: $\text{ALG} = 2|M|$
অ্যালগরিদম প্রতিটি ম্যাচড এজের জন্য ঠিক ২টি ভার্টেক্স কভারে যোগ করে, এবং মোট $|M|$টি এজ ম্যাচ করা হয়েছে (ম্যাক্সিমাল হওয়া পর্যন্ত), তাই $|C| = 2|M|$।

এই দুটো একসাথে মিলিয়ে:

$$\text{ALG}(I) = 2|M| \le 2\cdot\text{OPT}(I)$$

এটি L50-এ সংজ্ঞায়িত অ্যাপ্রক্সিমেশন রেশিও সংজ্ঞা অনুযায়ীই ঠিক একটি ২-অ্যাপ্রক্সিমেশন — এবং প্রমাণটি কোনো নির্দিষ্ট গ্রাফের গঠনের উপর নির্ভর করে না, তাই সব গ্রাফে সাধারণভাবে সত্য।

এবার প্রমাণটি প্রকৃতপক্ষে ধরে কি না তা যাচাই করা যাক — অ্যালগরিদমটি বাস্তবায়ন করে, একটি ব্রুট-ফোর্স এক্স্যাক্ট সলভার (সব সম্ভাব্য সাবসেট সাইজ $0, 1, 2, \dots$ ক্রমান্বয়ে পরীক্ষা করে প্রথম বৈধ কভার খুঁজে বের করা) বানিয়ে, বহু র‍্যান্ডম ছোট গ্রাফে রেশিও সত্যিই গণনা করে দেখানো হচ্ছে:

Python
import random
from itertools import combinations

def approx_vertex_cover(edges):
    # ম্যাক্সিমাল-ম্যাচিং-ভিত্তিক ২-অ্যাপ্রক্সিমেশন
    remaining = set(edges)
    cover = set()
    while remaining:
        u, v = next(iter(remaining))       # যেকোনো একটি এখনো-না-কভার-হওয়া এজ
        cover.add(u)
        cover.add(v)
        remaining = {(a, b) for (a, b) in remaining if a != u and a != v and b != u and b != v}
    return cover

def brute_force_vertex_cover(n, edges):
    # সাবসেট সাইজ ০, ১, ২, ... ক্রমান্বয়ে বাড়িয়ে প্রথম বৈধ কভার খোঁজা -- এটিই সত্যিকারের ন্যূনতম
    vertices = list(range(n))
    for k in range(len(vertices) + 1):
        for subset in combinations(vertices, k):
            s = set(subset)
            if all(u in s or v in s for u, v in edges):
                return s
    return set(vertices)

def random_graph(rng, n, edge_prob):
    edges = []
    for u in range(n):
        for v in range(u + 1, n):
            if rng.random() < edge_prob:
                edges.append((u, v))
    return edges

rng = random.Random(7)
worst_ratio = 0.0
print(f"{'trial':>5} | {'n':>3} | {'edges':>6} | {'ALG':>4} | {'OPT':>4} | {'ratio':>6}")
for trial in range(15):
    n = rng.randint(4, 8)
    edge_prob = rng.choice([0.3, 0.4, 0.5, 0.6])
    edges = random_graph(rng, n, edge_prob)
    if not edges:
        continue
    approx = approx_vertex_cover(edges)
    opt = brute_force_vertex_cover(n, edges)
    ratio = len(approx) / len(opt)
    worst_ratio = max(worst_ratio, ratio)
    print(f"{trial:>5} | {n:>3} | {len(edges):>6} | {len(approx):>4} | {len(opt):>4} | {ratio:>6.2f}")
    assert len(approx) <= 2 * len(opt), "২-অ্যাপ্রক্সিমেশন বাউন্ড ভঙ্গ হয়েছে!"

print(f"\nসর্বোচ্চ পর্যবেক্ষিত ratio (ALG/OPT): {worst_ratio:.2f} -- সবসময় <= 2.00")

    
brute_force_vertex_cover ছোট সাইজ থেকে শুরু করে ($k=0,1,2,\dots$) প্রথম যে সাইজে একটি বৈধ কভার পাওয়া যায় সেটিই রিটার্ন করে দেয় — তাই এটি নিশ্চিতভাবে সত্যিকারের ন্যূনতম কভার, কোনো হিউরিস্টিক অনুমান নয়। এই এক্স্যাক্ট মানের বিপরীতেই assert রেশিও যাচাই করছে — এবং $n \le 8$ হওয়ায় $2^8 = 256$টি সাবসেট পরীক্ষা করাও Pyodide-এ তাৎক্ষণিক।

৪ · সাধারণীকরণ — সেট কভার

সেট কভারSet Coverএকটি ইউনিভার্স $U$ এবং সাবসেটের একটি সংগ্রহ $S_1,\dots,S_m \subseteq U$ দেওয়া থাকলে, সবচেয়ে কম সংখ্যক সাবসেট বেছে নেওয়া যাদের ইউনিয়ন পুরো $U$ কভার করে। ভার্টেক্স কভারের একটি সাধারণীকরণ: প্রতিটি ভার্টেক্স $v$-কে একটি সাবসেট হিসেবে ভাবা যায় ($v$-এর সাথে যুক্ত সব এজের সেট), আর "ইউনিভার্স" হলো গ্রাফের সব এজ — তখন ভার্টেক্স কভার খোঁজা মানেই এই বিশেষ ধরনের সাবসেট সংগ্রহে সেট কভার খোঁজা।

সাধারণ সেট কভারের জন্য প্রমিত গ্রিডি অ্যালগরিদম সহজ: প্রতি ধাপে সেই সাবসেটটি বেছে নাও যা এখনো-অকভার্ড সবচেয়ে বেশি ইউনিভার্স-এলিমেন্ট কভার করে। কিন্তু এর প্রমাণিত অ্যাপ্রক্সিমেশন রেশিও ধ্রুবক নয় — $H(n) = 1 + \tfrac12 + \tfrac13 + \dots + \tfrac1n \approx \ln n$ ($n$-তম harmonic number, ইউনিভার্সের আকারের উপর নির্ভরশীল, discrete math-এর পরিচিত ধারণা)। এটি একটি সুপরিচিত ফলাফল — এবং প্রমাণিত আছে যে $P \ne NP$ ধরে নিলে, সাধারণ সেট কভারের জন্য এর চেয়ে উল্লেখযোগ্যভাবে ভালো (ধ্রুবক-ফ্যাক্টর) কোনো অ্যাপ্রক্সিমেশন সম্ভব নয়।

তাহলে ভার্টেক্স কভার কেন ধ্রুবক-২ পেল অথচ সাধারণ সেট কভার পায়নি? কারণ ভার্টেক্স কভারের সাবসেটগুলোর একটি বিশেষ গঠন আছে (প্রতিটি সাবসেট ঠিক একটি ভার্টেক্সের সাথে যুক্ত এজগুলো নিয়ে গঠিত) — এই অতিরিক্ত গ্রাফ-স্ট্রাকচারই ম্যাচিং-ভিত্তিক কৌশলকে সম্ভব করে তোলে, যা সাধারণ, স্বেচ্ছাচারী সাবসেট সংগ্রহে কাজ করে না।

নিচে ছোট ইনস্ট্যান্সে গ্রিডি সেট কভারকে ব্রুট-ফোর্স এক্স্যাক্ট সেট কভারের বিপরীতে যাচাই করা হলো:

Python
import random
from itertools import combinations

def greedy_set_cover(universe, subsets):
    universe = set(universe)
    covered = set()
    chosen = []
    remaining_subsets = list(subsets)
    while covered != universe and remaining_subsets:
        best = max(remaining_subsets, key=lambda s: len(set(s) - covered))
        chosen.append(best)
        covered |= set(best)
        remaining_subsets.remove(best)
    return chosen

def brute_force_set_cover(universe, subsets):
    universe = set(universe)
    n = len(subsets)
    for k in range(1, n + 1):
        for combo in combinations(range(n), k):
            union = set()
            for idx in combo:
                union |= set(subsets[idx])
            if union == universe:
                return [subsets[idx] for idx in combo]
    return list(subsets)

def random_set_cover_instance(rng, universe_size, num_subsets):
    universe = list(range(universe_size))
    subsets = []
    for _ in range(num_subsets):
        size = rng.randint(1, universe_size)
        subsets.append(tuple(rng.sample(universe, size)))
    # নিশ্চিত করা যে ইউনিয়ন পুরো ইউনিভার্স কভার করে (নাহলে কোনো বৈধ কভারই নেই)
    covered = set()
    for s in subsets:
        covered |= set(s)
    if covered != set(universe):
        subsets.append(tuple(universe))
    return universe, subsets

rng = random.Random(3)
print(f"{'trial':>5} | {'universe':>8} | {'greedy #sets':>12} | {'OPT #sets':>9} | {'ratio':>6}")
for trial in range(8):
    universe, subsets = random_set_cover_instance(rng, universe_size=rng.randint(4, 6), num_subsets=rng.randint(3, 6))
    greedy = greedy_set_cover(universe, subsets)
    opt = brute_force_set_cover(universe, subsets)
    ratio = len(greedy) / len(opt)
    print(f"{trial:>5} | {len(universe):>8} | {len(greedy):>12} | {len(opt):>9} | {ratio:>6.2f}")

print("\nএই ছোট ইনস্ট্যান্সগুলোতে গ্রিডি প্রায়ই অপটিমামের কাছাকাছি -- কিন্তু লক্ষ্য করুন এখানে কোনো")
print("assert ratio <= 2 নেই, কারণ সাধারণ সেট কভারে এমন ধ্রুবক গ্যারান্টি প্রমাণিতভাবেই নেই।")

    
মূল কথা · Key takeaway

দুটো সমস্যা কাঠামোগতভাবে সম্পর্কিত হলেও তাদের অ্যাপ্রক্সিমেশন-যোগ্যতা ভিন্ন হতে পারে — ভার্টেক্স কভারের গ্রাফ-স্ট্রাকচার একটি ধ্রুবক-২ গ্যারান্টি দেয়, কিন্তু তার সাধারণীকরণ (সেট কভার) মাত্র লগারিদমিক গ্যারান্টি দেয়। L52-এ আমরা আরেকটি ধ্রুবক-২ অ্যাপ্রক্সিমেশন দেখব — মেট্রিক TSP-তে, যেখানে গ্যারান্টিটা আসে ত্রিভুজ অসমতা (triangle inequality) থেকে।

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

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

প্র ০১ কেন অ্যালগরিদম প্রতিটি ম্যাচড এজের দুটো প্রান্তবিন্দুই কভারে নেয়, একটি নয়? একটি নিলে তো কভারের আকার আরও ছোট হতো?

একটি নিলে কভারটি হয়তো অবৈধ হয়ে যেত — কারণ আমরা জানি না কোন প্রান্তটি "সঠিক" (সেটা জানতেই তো ভার্টেক্স কভার সমস্যাটা সমাধান করতে হচ্ছে, যা NP-হার্ড)। দুটোই নেওয়ার সৌন্দর্য হলো এটি কোনো অতিরিক্ত তথ্য ছাড়াই, দ্রুত (পলিনোমিয়াল সময়ে) একটি নিশ্চিতভাবে বৈধ কভার তৈরি করে — মূল্য হিসেবে আকার সত্যিকারের অপটিমামের দ্বিগুণ পর্যন্ত হতে পারে, যা প্রমাণে ঠিক গণনা করা হয়েছে।

প্র ০২ যদি গ্রাফে কোনো এজই না থাকে, তাহলে ভার্টেক্স কভার অ্যালগরিদম কী রিটার্ন করবে, এবং এই কেসে রেশিও নিয়ে কী সমস্যা হতে পারে?

এজ না থাকলে while remaining লুপটি একবারও চলবে না, তাই কভার খালি সেট -- এবং প্রকৃত অপটিমামও খালি সেট (আকার ০), কারণ কভার করার মতো কোনো এজই নেই। রেশিও $0/0$ — গাণিতিকভাবে অসংজ্ঞায়িত। এই কারণেই উপরের কোডে if not edges: continue দিয়ে খালি-এজ গ্রাফগুলো বাদ দেওয়া হয়েছে, যাতে ভাগ করার সময় শূন্য-দিয়ে-ভাগ সমস্যা না হয়।

প্র ০৩ সেট কভারের কোডে assert ratio <= 2 রাখা হয়নি কেন, অথচ ভার্টেক্স কভারের কোডে রাখা হয়েছে?

কারণ এটি সৎ থাকা — ভার্টেক্স কভারের জন্য ২-অ্যাপ্রক্সিমেশন প্রমাণিত, তাই সেই assert একটি প্রমাণিত গাণিতিক দাবি যাচাই করছে (এবং এটি সত্যিই সবসময় পাস করবে)। কিন্তু সাধারণ সেট কভারের গ্রিডি অ্যালগরিদমের জন্য প্রমাণিত গ্যারান্টি হলো $O(\log n)$, ধ্রুবক ২ নয় — বড় ইউনিভার্সে রেশিও তাত্ত্বিকভাবে ২ ছাড়িয়ে যেতে পারে। এমন একটি assert রাখলে তা মিথ্যা নিশ্চয়তা দিত যা তত্ত্বের সাথে সামঞ্জস্যপূর্ণ নয়।

অনুশীলন

  1. চিন্তা করুন: একটি "স্টার গ্রাফ" (একটি কেন্দ্রীয় ভার্টেক্স $c$, এবং $c$-এর সাথে যুক্ত $k$টি অন্য ভার্টেক্স, তাদের মধ্যে কোনো এজ নেই) ধরুন। এখানে সত্যিকারের অপটিমাল কভার কত আকারের, এবং ম্যাক্সিমাল-ম্যাচিং অ্যালগরিদম কী দেবে?

    অপটিমাল কভার আকার ১ — শুধু কেন্দ্রীয় ভার্টেক্স $c$ নিলেই সব $k$টি এজ কভার হয়ে যায়। কিন্তু অ্যালগরিদম যেকোনো একটি এজ $(c, v_1)$ বেছে নিয়ে $c$ ও $v_1$ দুটোই কভারে নেবে — যেহেতু $c$ কভারে যোগ হওয়ার সাথে সাথেই বাকি সব এজ (যেগুলো $c$-স্পর্শী) সরে যায়, লুপ এখানেই থেমে যাবে, চূড়ান্ত কভার আকার ২। এটি রেশিও $2/1 = 2$ — ঠিক গ্যারান্টির সীমায়, যা দেখায় বাউন্ডটি টাইট (আরও ভালো করা সবসময় সম্ভব নয়)।

  2. পরীক্ষা করুন: উপরের প্রথম কোড সেলে edge_prob-এর তালিকায় 0.9 যোগ করে (ঘন গ্রাফ) এবং n-এর রেঞ্জ rng.randint(6, 8) রেখে Run চেপে দেখুন — ঘন গ্রাফে রেশিও সাধারণত কমে যায় কি না লক্ষ্য করুন।

    ঘন গ্রাফে (অনেক এজ) সাধারণত অপটিমাল কভারও তুলনামূলক বড় হতে থাকে (অনেক ভার্টেক্স কভারে না নিলে অনেক এজ বাদ থেকে যায়), তাই ALG ও OPT-এর মধ্যে ফারাক প্রায়ই কমে আসে এবং রেশিও ১.০-১.৫-এর কাছাকাছি নেমে আসে। তবে assert len(approx) <= 2 * len(opt) সবসময়ই পাস করবে, কারণ প্রমাণটি গ্রাফের ঘনত্বের উপর নির্ভর করে না।

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

আগের পাঠ
অ্যাপ্রক্সিমেশন অ্যালগরিদম ও অ্যাপ্রক্সিমেশন রেশিও