পাঠ ৫০ · ৫৬-এর মধ্যে · মডিউল ১১
Home / Courses / Formal Language & Automata Theory / Theory of Computation / NP-হার্ডনেস মোকাবিলা

NP-হার্ডনেস মোকাবিলা — অ্যাপ্রক্সিমেশন ও হিউরিস্টিক

Coping with NP-hardness — approximation & heuristics
৭ মিনিট পড়া উচ্চ · Advanced Python কোডসহ সম্পূর্ণ বাংলায়

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

  • NP-হার্ড প্রবলেমের সাথে বাস্তবে মোকাবিলার তিনটি স্ট্র্যাটেজি — অ্যাপ্রক্সিমেশন, হিউরিস্টিক, বিশেষ-কেস অ্যালগরিদম
  • ভার্টেক্স কভারের গ্রিডি 2-অ্যাপ্রক্সিমেশন অ্যালগরিদম ও এর প্রমাণযোগ্য গ্যারান্টি
  • অ্যাপ্রক্সিমেশন ও হিউরিস্টিকের মধ্যে সততাপূর্ণ পার্থক্য — গ্যারান্টি আছে কি না
  • Python-এ অ্যাপ্রক্সিমেট সমাধান বনাম ব্রুট-ফোর্স সত্যিকারের মিনিমাম — গতি বনাম অপ্টিমালিটির ট্রেড-অফ সরাসরি দেখা

১ · তত্ত্ব থেকে বাস্তবে — এখন কী করব

L47 দেখিয়েছে ভার্টেক্স কভার, TSP, গ্রাফ কালারিং-এর মতো বাস্তবে সত্যিই গুরুত্বপূর্ণ প্রবলেম NP-কমপ্লিট, আর L49 দেখিয়েছে এদের জন্য কোনো দক্ষ সঠিক অ্যালগরিদম পাওয়া যাবে কি না তা এখনো অজানা (এবং বেশিরভাগ বিজ্ঞানীর বিশ্বাস অনুযায়ী, সম্ভবত কখনোই পাওয়া যাবে না)। কিন্তু বাস্তবের সফটওয়্যার ইঞ্জিনিয়ারদের প্রতিদিনই এসব প্রবলেম সমাধান করতে হয় — একটি ডেলিভারি কোম্পানিকে রুট প্ল্যান করতেই হয়, একটি নেটওয়ার্ককে মনিটরিং পয়েন্ট বসাতেই হয়। তাহলে বাস্তবে কী করা হয়?

অ্যাপ্রক্সিমেশন অ্যালগরিদম
সঠিক অপ্টিমামের বদলে, প্রকৃত অপ্টিমামের একটি প্রমাণযোগ্য নিশ্চিত ফ্যাক্টরের মধ্যে (যেমন "সর্বোচ্চ ২ গুণ") একটি সমাধান মেনে নেওয়া — দ্রুত, এবং কতটা খারাপ হতে পারে তা গাণিতিকভাবে জানা।
হিউরিস্টিক
সিমুলেটেড অ্যানিলিং বা লোকাল-সার্চ-স্টাইল পদ্ধতি — বাস্তব-জগতের সাধারণ ইনস্ট্যান্সে অভিজ্ঞতাগতভাবে ভালো কাজ করে, কিন্তু কোনো প্রমাণযোগ্য ওয়ার্স্ট-কেস গ্যারান্টি ছাড়াই।
বিশেষ-কেস/সীমাবদ্ধ-ইনপুট অ্যালগরিদম
কিছু NP-হার্ড প্রবলেম নির্দিষ্ট সীমাবদ্ধ ইনপুট গঠনে (যেমন ট্রি-আকৃতির গ্রাফ) পলিনমিয়াল হয়ে যায় — cross-ref ../dsa/-এ নির্দিষ্ট গ্রাফ-ক্লাসের অ্যালগরিদমের জন্য।

২ · অ্যাপ্রক্সিমেশন বনাম হিউরিস্টিক — সৎ পার্থক্য

এই দুটি স্ট্র্যাটেজি প্রায়ই গুলিয়ে ফেলা হয়, কিন্তু এদের মধ্যে একটি গুরুত্বপূর্ণ, স্পষ্ট পার্থক্য আছে — অ্যাপ্রক্সিমেশন অ্যালগরিদমের একটি গাণিতিকভাবে প্রমাণিত ওয়ার্স্ট-কেস গ্যারান্টি থাকে (যেমন "ফলাফল কখনোই প্রকৃত অপ্টিমামের ২ গুণের বেশি হবে না, যেকোনো ইনপুটে") — অথচ হিউরিস্টিকের এই ধরনের কোনো গ্যারান্টি নেই, শুধু অভিজ্ঞতাগত (empirical) প্রমাণ যে এটি "সাধারণত ভালো কাজ করে"। এই পাঠ প্রমাণযোগ্য গ্যারান্টিসহ একটি বাস্তব অ্যাপ্রক্সিমেশন অ্যালগরিদম বাস্তবায়ন করবে।

৩ · ভার্টেক্স কভারের জন্য গ্রিডি 2-অ্যাপ্রক্সিমেশন

L47-এর ভার্টেক্স কভারVertex Coverএকটি গ্রাফের ভার্টেক্সের এমন একটি ক্ষুদ্রতম সাবসেট খুঁজে বের করা যাতে প্রতিটি এজের অন্তত একটি প্রান্তবিন্দু সেই সাবসেটে থাকে। NP-কমপ্লিট প্রবলেমের জন্য একটি সহজ, স্ট্যান্ডার্ড 2-অ্যাপ্রক্সিমেশন অ্যালগরিদম আছে:

অ্যালগরিদম · Greedy Vertex Cover

যতক্ষণ আনকভার্ড এজ বাকি আছে: (১) যেকোনো একটি আনকভার্ড এজ বেছে নাও, (২) সেই এজের দুটো প্রান্তবিন্দুই কভারে যোগ করো, (৩) এই দুটো ভার্টেক্সের সংস্পর্শে থাকা সব এজ কভার্ড হিসেবে সরিয়ে দাও, (৪) পুনরাবৃত্তি করো।

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

Python
# ভার্টেক্স কভারের জন্য একটি সত্যিকারের গ্রিডি 2-অ্যাপ্রক্সিমেশন, ব্রুট-ফোর্স সত্যিকারের
# মিনিমামের বিপরীতে যাচাইসহ

import itertools

def greedy_vertex_cover(edges):
    remaining = set(frozenset(e) for e in edges)
    cover = set()
    while remaining:
        edge = next(iter(remaining))          # যেকোনো একটি আনকভার্ড এজ
        u, v = tuple(edge)
        cover.add(u)
        cover.add(v)                           # দুটো প্রান্তবিন্দুই কভারে যোগ
        remaining = {e for e in remaining if u not in e and v not in e}
    return cover

def brute_force_min_vertex_cover(vertices, edges):
    edge_sets = [frozenset(e) for e in edges]
    vertices = list(vertices)
    for size in range(len(vertices) + 1):      # ছোট সাইজ থেকে শুরু -- প্রথম সফল = মিনিমাম
        for subset in itertools.combinations(vertices, size):
            subset_set = set(subset)
            if all(e & subset_set for e in edge_sets):
                return subset_set
    return set(vertices)

# উদাহরণ ১: একটি ডিসজয়েন্ট ম্যাচিং -- গ্রিডির জন্য সবচেয়ে "টাইট" (worst-case) কেস
vertices1 = [1, 2, 3, 4, 5, 6]
edges1 = [(1, 2), (3, 4), (5, 6)]

greedy1 = greedy_vertex_cover(edges1)
optimal1 = brute_force_min_vertex_cover(vertices1, edges1)
print("গ্রাফ ১ (ডিসজয়েন্ট এজ):")
print("  গ্রিডি কভার  :", sorted(greedy1), "| সাইজ", len(greedy1))
print("  প্রকৃত মিনিমাম:", sorted(optimal1), "| সাইজ", len(optimal1))
print("  রেশিও:", len(greedy1) / len(optimal1), "(<=2 হওয়ার কথা)")
assert len(greedy1) <= 2 * len(optimal1)

# উদাহরণ ২: একটি ত্রিভুজ + একটি পেন্ডান্ট এজ
vertices2 = [1, 2, 3, 4]
edges2 = [(1, 2), (2, 3), (1, 3), (3, 4)]

greedy2 = greedy_vertex_cover(edges2)
optimal2 = brute_force_min_vertex_cover(vertices2, edges2)
print("\nগ্রাফ ২ (ত্রিভুজ + পেন্ডান্ট):")
print("  গ্রিডি কভার  :", sorted(greedy2), "| সাইজ", len(greedy2))
print("  প্রকৃত মিনিমাম:", sorted(optimal2), "| সাইজ", len(optimal2))
print("  রেশিও:", len(greedy2) / len(optimal2), "(<=2 হওয়ার কথা)")
assert len(greedy2) <= 2 * len(optimal2)

print("\n>> উভয় গ্রাফেই গ্রিডি অ্যাপ্রক্সিমেশন প্রকৃত মিনিমামের সর্বোচ্চ ২ গুণ -- গ্যারান্টি নিশ্চিত")

    
গ্রাফ ১-এ (ডিসজয়েন্ট এজ) লক্ষ্য করুন গ্রিডি ঠিক প্রকৃত মিনিমামের ঠিক দ্বিগুণ ভার্টেক্স ব্যবহার করে (৬ বনাম ৩) — এটাই গ্যারান্টির "টাইট" (tight) সীমা, প্রমাণ করে ২-এর বাউন্ড শুধু তাত্ত্বিক নয়, বাস্তবে অর্জনযোগ্যও। গ্রাফ ২-এ রেশিও এর চেয়ে ভালো (কাছাকাছি) — বাস্তবে গ্রিডি প্রায়ই ২-এর চেয়ে ভালো করে, কিন্তু গ্যারান্টিটা সবসময় ২-এর মধ্যেই থাকে বলে দেয়, যাই হোক না কেন ইনপুট গ্রাফ।
মূল কথা · Key takeaway

NP-কমপ্লিট প্রবলেমের সাথে বাস্তবে মোকাবিলার তিনটি পথ — প্রমাণযোগ্য গ্যারান্টিসহ অ্যাপ্রক্সিমেশন, গ্যারান্টিহীন কিন্তু ব্যবহারিক হিউরিস্টিক, ও বিশেষ-কেস পলিনমিয়াল অ্যালগরিদম। L47-L49-এর তত্ত্ব আমাদের বলে "নিখুঁত, দ্রুত, সর্বজনীন সমাধান নেই" — কিন্তু এই তত্ত্বই আমাদের সঠিকভাবে জানায় কতটা আপস (trade-off) করা নিরাপদ, ঠিক যেমন ভার্টেক্স কভারের ২-গুণ গ্যারান্টি একটি গাণিতিকভাবে নিশ্চিত, নির্ভরযোগ্য সীমা দেয়।

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

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

প্র ০১ গ্রিডি ভার্টেক্স কভার কেন শুধু একটি এজের একপ্রান্ত না নিয়ে দুটো প্রান্তবিন্দুই কভারে যোগ করে?

কারণ অ্যালগরিদম জানে না কোন প্রান্তবিন্দুটি প্রকৃত মিনিমাম কভারে আছে (সেটা জানতে হলে সমস্যাটাই সমাধান করে ফেলতে হবে!) — তাই নিরাপদ থাকতে দুটোই যোগ করে, নিশ্চিত করে এজটি কভার্ড হবে যেভাবেই হোক। এই "নিরাপদ কিন্তু কিছুটা অপচয়" সিদ্ধান্তটাই ঠিক ২-গুণ গ্যারান্টির উৎস — একটি ভার্টেক্স যথেষ্ট হতো এমন ক্ষেত্রেও দুটো নেওয়ায় সর্বোচ্চ ২ গুণ পর্যন্ত অপচয় হতে পারে, তার বেশি নয়।

প্র ০২ একটি হিউরিস্টিক ব্যবহার করা কি কখনো যুক্তিসঙ্গত, যদি তার কোনো প্রমাণযোগ্য গ্যারান্টিই না থাকে?

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

প্র ০৩ উপরের কোড সেলে গ্রাফ ১-এ গ্রিডি কেন ঠিক ৬টি ভার্টেক্স বেছে নেয় (৩টি এজের প্রতিটির দুটো প্রান্তবিন্দু), অথচ প্রকৃত মিনিমাম মাত্র ৩?

গ্রাফ ১-এ এজগুলো সম্পূর্ণ বিচ্ছিন্ন (ডিসজয়েন্ট) — কোনো দুটো এজ একই ভার্টেক্স শেয়ার করে না। তাই প্রতিটি এজ থেকে মাত্র একটি প্রান্তবিন্দু নিলেই (প্রতি এজে ৩টি মোট) পুরো গ্রাফ কভার হয়ে যায় — এটাই প্রকৃত মিনিমাম, ৩। কিন্তু গ্রিডি প্রতিটি বাছাই করা এজের উভয় প্রান্তবিন্দু নেয় (কারণ কোনটা "সঠিক" প্রান্তবিন্দু তা জানে না), তাই প্রতিটি এজে ২টি করে, মোট ৬। এই ডিসজয়েন্ট-এজ গঠনটাই ২-গুণ গ্যারান্টিকে "টাইট" করে তোলে — অর্থাৎ এমন একটি ইনপুট যেখানে গ্যারান্টি ঠিক তার সীমায় পৌঁছায়।

অনুশীলন

  1. চিন্তা করুন: একটি গ্রাফ কল্পনা করুন যেখানে একটি একক ভার্টেক্স বাকি সব ভার্টেক্সের সাথে সংযুক্ত (একটি "স্টার" গ্রাফ) — গ্রিডি এখানে কী করবে, এবং প্রকৃত মিনিমামের কতটা কাছাকাছি যাবে বলে মনে করেন?

    একটি স্টার গ্রাফে (কেন্দ্র c, বাকি সব পাতা), গ্রিডি প্রথম এজ বাছাই করার সময় সেই এজের দুটো প্রান্তবিন্দু — কেন্দ্র c ও একটি পাতা — কভারে যোগ করবে, যা কেন্দ্র c-এর সব এজ সরিয়ে দেয় (কারণ c-সংশ্লিষ্ট প্রতিটি এজ এখন কভার্ড)। ফলে গ্রিডি মাত্র ২টি ভার্টেক্স নেয় (c এবং একটি পাতা), যদিও প্রকৃত মিনিমাম মাত্র ১টি (শুধু c)। এখানেও রেশিও ঠিক ২, কিন্তু ছোট সংখ্যায় (২ বনাম ১) — দেখায় গ্যারান্টি ছোট গ্রাফেও ধরে থাকে।

  2. পরীক্ষা করুন: উপরের কোড সেলে edges1-এ আরও দুটো ডিসজয়েন্ট এজ (যেমন (7,8), (9,10)) যোগ করে Run চাপুন — রেশিও এখনো ঠিক ২ থাকে কি না, এবং কেন থাকে যাচাই করুন।

    ৫টি সম্পূর্ণ ডিসজয়েন্ট এজে গ্রিডি এখনো প্রতিটি এজের দুটো প্রান্তবিন্দু নেবে (মোট ১০টি ভার্টেক্স), অথচ প্রকৃত মিনিমাম প্রতি এজে একটি করে (৫টি ভার্টেক্স) — রেশিও এখনো ঠিক $10/5=2$। এটি নিশ্চিত করে ডিসজয়েন্ট- ম্যাচিং গঠনের জন্য রেশিও যেকোনো সংখ্যক এজে ঠিক ২-তেই স্থির থাকে, প্রমাণ করে গ্যারান্টিটি ইনপুট সাইজ-নিরপেক্ষ।

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

পাঠ ৪৯
P বনাম NP প্রশ্ন