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

P বনাম NP ও অ্যালগরিদম ডিজাইনে এর প্রভাব

P vs NP and what it means for algorithm design
৬ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

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

  • P ও NP ক্লাসের অনানুষ্ঠানিক কিন্তু সুনির্দিষ্ট সংজ্ঞা
  • "সার্টিফিকেট" ও "ভেরিফায়ার" ধারণা — কেন "যাচাই করা সহজ" আর "খুঁজে বের করা সহজ" এক জিনিস নয়
  • একটি ছোট বুলিয়ান ফর্মুলার জন্য সত্যিকারের, চলমান পলিনোমিয়াল-টাইম ভেরিফায়ার কোড
  • P=?NP প্রশ্নটি এখনও কেন খোলা, এবং এটি নিজের অ্যালগরিদম ডিজাইনের কাজে কীভাবে সিদ্ধান্ত নিতে সাহায্য করে

১ · P — যা দ্রুত সমাধান করা যায়

এই কোর্সের M2-M10 পর্যন্ত যত অ্যালগরিদম দেখেছি — মার্জ সর্ট, ডাইজকস্ট্রা, DP-ভিত্তিক সমাধান — তাদের প্রায় সবগুলোই একটি সাধারণ ক্লাসের অন্তর্ভুক্ত:

$$P = \{\, L : L\text{-কে সিদ্ধান্ত নেওয়ার মতো একটি ডিটারমিনিস্টিক অ্যালগরিদম আছে যা } O(n^k) \text{ সময়ে চলে, কোনো ধ্রুবক } k \text{-এর জন্য} \,\}$$

সহজ কথায়: PPolynomial timeইনপুটের আকার $n$ বাড়লে রানটাইম $n$-এর কোনো নির্দিষ্ট ঘাতে (পাওয়ারে) বাড়ে — $n^2$, $n^3$ ইত্যাদি — $2^n$ বা $n!$-এর মতো নয়। হলো সেই সমস্যাগুলো যেগুলোর জন্য আমরা সরাসরি, কোনো ট্রায়াল-অ্যান্ড-এরর ছাড়াই, একটি কার্যকর সমাধান অ্যালগরিদম লিখতে পারি। L05-L06-এ ইতিমধ্যে এই "পলিনোমিয়াল বনাম এক্সপোনেনশিয়াল" গ্রোথ রেটের পার্থক্য বিস্তারিত দেখা হয়েছে — এখানে আমরা সেটাকে একটি সমস্যার ক্লাস সংজ্ঞায়িত করতে ব্যবহার করছি।

২ · NP — যা দ্রুত যাচাই করা যায়

এখন প্রশ্ন হলো: এমন সমস্যা কি আছে যেগুলোর জন্য সমাধান খুঁজে বের করা কঠিন মনে হয়, কিন্তু কেউ একটা সমাধান প্রস্তাব করলে সেটা সঠিক কিনা যাচাই করা দ্রুত? উত্তর হ্যাঁ — এবং এই ধরনের সমস্যাগুলোর ক্লাসের নামই NP (Nondeterministic Polynomial time)।

$$L \in NP \iff \exists\ \text{পলিনোমিয়াল } p \text{ ও পলিনোমিয়াল-টাইম অ্যালগরিদম } V \text{ এমন যে, প্রতিটি } x \text{-এর জন্য: } x \in L \iff \exists\, c,\ |c| \le p(|x|),\ V(x, c) = \text{Accept}$$

এখানে $c$-কে বলা হয় সার্টিফিকেট (certificate) বা প্রমাণ, আর $V$-কে বলা হয় ভেরিফায়ার (verifier)। লক্ষণীয় — এই সংজ্ঞায় কোথাও বলা নেই যে $c$ কীভাবে খুঁজে পাওয়া যাবে; শুধু বলা হয়েছে, $c$ থাকলে $V$ সেটাকে পলিনোমিয়াল সময়ে চেক করতে পারবে।

ক্লাসিক উদাহরণ — SAT (Boolean Satisfiability)

একটি বুলিয়ান ফর্মুলা $\phi$ নিন, যেমন $\phi = (x_0 \lor x_1 \lor \lnot x_2) \land (\lnot x_0 \lor x_2) \land (x_1 \lor \lnot x_2) \land (\lnot x_1 \lor \lnot x_0 \lor x_2)$। প্রশ্ন: এমন কোনো $\text{True}/\text{False}$ assignment আছে কিনা যা $\phi$-কে True করে? $n$ ভ্যারিয়েবলের জন্য $2^n$টি সম্ভাব্য assignment আছে — সবগুলো try করা এক্সপোনেনশিয়াল কাজ। কিন্তু কেউ যদি একটা candidate assignment $c$ প্রস্তাব করে, সেটা $\phi$-কে সন্তুষ্ট করে কিনা যাচাই করতে শুধু প্রতিটি ক্লজে একবার করে চোখ বুলিয়ে দেখতে হয় — এটি $\phi$-এর আকারের সাপেক্ষে লিনিয়ার (তাই পলিনোমিয়াল) কাজ।

ইনপুট x + সার্টিফিকেট c (একটি প্রস্তাবিত সমাধান/candidate) ভেরিফায়ার V(x, c) পলিনোমিয়াল সময়ে চেক করে Accept / Reject (c বৈধ কিনা) c কোথা থেকে এলো? — খুঁজে বের করা কঠিন হতে পারে (এক্সপোনেনশিয়াল সার্চ স্পেস — এটাই NP-হার্ডনেসের মূল কথা, দেখুন L48)
NP-এর সংজ্ঞা শুধু ভেরিফিকেশনের গতি নিয়ে — সার্টিফিকেট থাকলে তা যাচাই দ্রুত। সার্টিফিকেট খুঁজে বের করা সহজ হবে এমন কোনো নিশ্চয়তা NP দেয় না।

নিচের কোডে ঠিক এই ভেরিফায়ারটাই বাস্তবে লেখা হয়েছে — উপরের $\phi$ ফর্মুলাটি ক্লজের একটি লিস্ট হিসেবে প্রতিনিধিত্ব করা হয়েছে, এবং verify_assignment ফাংশনটি একটি candidate assignment নিয়ে সেটা $\phi$-কে সন্তুষ্ট করে কিনা চেক করে — সমাধান খোঁজে না, শুধু যাচাই করে।

Python
# phi = (x0 OR x1 OR NOT x2) AND (NOT x0 OR x2) AND (x1 OR NOT x2) AND (NOT x1 OR NOT x0 OR x2)
# প্রতিটি ক্লজ = লিটারেলের একটি লিস্ট; প্রতিটি লিটারেল = (variable_index, negated_কিনা)
formula = [
    [(0, False), (1, False), (2, True)],
    [(0, True), (2, False)],
    [(1, False), (2, True)],
    [(1, True), (0, True), (2, False)],
]

def verify_assignment(formula, assignment):
    """সার্টিফিকেট (candidate assignment) যাচাই করে -- প্রতিটি ক্লজে
    অন্তত একটি লিটারেল True কিনা দেখে। সময় জটিলতা O(m), যেখানে m হলো
    ফর্মুলার মোট লিটারেল সংখ্যা -- ইনপুটের (n ভ্যারিয়েবল) সাপেক্ষে পলিনোমিয়াল।"""
    literal_checks = 0
    for clause in formula:
        clause_satisfied = False
        for var_idx, negated in clause:
            literal_checks += 1
            value = assignment[var_idx]
            if negated:
                value = not value
            if value:
                clause_satisfied = True
                break
        if not clause_satisfied:
            return False, literal_checks
    return True, literal_checks

candidate = [True, True, True]  # x0=True, x1=True, x2=True -- এটাই "সার্টিফিকেট"
is_sat, checks = verify_assignment(formula, candidate)
print(f"candidate {candidate} -> সন্তুষ্ট: {is_sat}, মোট লিটারেল-চেক: {checks}")

wrong_candidate = [True, True, False]
is_sat2, checks2 = verify_assignment(formula, wrong_candidate)
print(f"candidate {wrong_candidate} -> সন্তুষ্ট: {is_sat2}, মোট লিটারেল-চেক: {checks2}")

n = 3
print(f"\n{n} ভ্যারিয়েবলের জন্য সম্ভাব্য assignment সংখ্যা: 2^{n} = {2 ** n}")
print("প্রতিটি candidate যাচাই করতে মাত্র O(m) সময় লাগে -- কিন্তু 'কোনটা কাজ করে' তা")
print("খুঁজে বের করতে ব্রুট-ফোর্সে সবগুলো সম্ভাবনা try করা লাগতে পারে (n বড় হলে এক্সপোনেনশিয়াল)।")

    
প্রথম candidate [True, True, True]-এর জন্য verify_assignment সবগুলো ক্লজ চেক করে True রিটার্ন করে (৭টি লিটারেল-চেকে)। দ্বিতীয় [True, True, False]-এর ক্ষেত্রে দ্বিতীয় ক্লজ ($\lnot x_0 \lor x_2$) কোনো লিটারেলেই সন্তুষ্ট হয় না, তাই ফাংশনটি তাড়াতাড়ি False রিটার্ন করে থামে (মাত্র ৩টি চেকে) — বাকি ক্লজগুলো আর দেখারই প্রয়োজন হয় না। দুই ক্ষেত্রেই কাজের পরিমাণ ফর্মুলার আকারের সাপেক্ষে ছোট ও পলিনোমিয়াল — n ভ্যারিয়েবল হলেও, ভেরিফিকেশনের খরচ কখনোই $2^n$ স্পর্শ করে না।

৩ · P=?NP — কেন এই প্রশ্নটা এখনো খোলা

এতক্ষণে একটা জিনিস স্পষ্ট হওয়ার কথা: $P \subseteq NP$ — কারণ যদি কোনো সমস্যা পলিনোমিয়াল সময়ে সরাসরি সমাধান করা যায়, তাহলে সেই সমাধান অ্যালগরিদমটাকেই একটা (সার্টিফিকেট উপেক্ষা করা) ভেরিফায়ার হিসেবে ব্যবহার করা যায়। কিন্তু উল্টো দিকটা — $NP \subseteq P$ কিনা, অর্থাৎ যা কিছু দ্রুত যাচাই করা যায় তা কি সবসময় দ্রুত খুঁজেও বের করা যায় — এটাই কম্পিউটার সায়েন্সের সবচেয়ে বিখ্যাত খোলা প্রশ্ন, যাকে বলা হয় $P \overset{?}{=} NP$।

এই প্রশ্নের আনুষ্ঠানিক সংজ্ঞা, ইতিহাস, এবং কেন এটি প্রমাণ করা এত কঠিন — তা নিয়ে Theory of Computation কোর্সের "The P vs NP Question" পাঠ-এ পূর্ণাঙ্গ আলোচনা আছে। P ও NP-এর সম্পূর্ণ আনুষ্ঠানিক সংজ্ঞা ও প্রমাণ পদ্ধতির জন্য দেখুন "Time Complexity and the Class P" এবং "The Class NP and Verifiers" পাঠদুটো।

একজন অ্যালগরিদম ডিজাইনারের জন্য এর ব্যবহারিক অর্থ কী

আপনি কোনো প্রমাণিত উপপাদ্য ছাড়াই প্রতিদিনের কাজে এই সিদ্ধান্তটা নিতে পারবেন: যদি কোনো সমস্যায় প্রস্তাবিত সমাধান যাচাই করা সহজ মনে হয় (অর্থাৎ এটি NP-তে আছে বলে মনে হয়), কিন্তু আপনি সরাসরি একটা পলিনোমিয়াল-টাইম সমাধান অ্যালগরিদম বহুদিন চেষ্টা করেও খুঁজে পাননি — সেটা এই ইঙ্গিত দেয় যে সমস্যাটি হয়তো NP-হার্ড, অর্থাৎ $P = NP$ প্রমাণিত না হওয়া পর্যন্ত এর জন্য কোনো পলিনোমিয়াল-টাইম সঠিক অ্যালগরিদম পাওয়ার সম্ভাবনা কম। L48-এ আমরা ঠিক এই ইঙ্গিতগুলো চেনার একটি প্র্যাকটিক্যাল চেকলিস্ট দেখব।

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

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

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

না — অন্তত এখনো প্রমাণিত নয়। "পলিনোমিয়াল-টাইম ভেরিফায়ার আছে" মানেই "পলিনোমিয়াল-টাইম সলভার আছে" নয়; এই দুটো বিবৃতি সমতুল্য হবে কেবল যদি $P = NP$ প্রমাণিত হয়। বর্তমানে জানা আছে $P \subseteq NP$ (সমাধান করতে পারলে যাচাইও করা যায়), কিন্তু উল্টোটা এখনো খোলা প্রশ্ন — তাই সাধারণভাবে বলা যায় না যে NP-এর প্রতিটি সমস্যাই P-তে আছে।

প্র ০২ কোড সেলে candidate = [True, True, True] সফল হলো — কিন্তু বাস্তবে এই assignment-টা আমরা কীভাবে পেলাম?

এখানে মাত্র ৩টি ভ্যারিয়েবল থাকায় হাতে-কলমে ৮টি সম্ভাব্য combination চেক করে বের করা সম্ভব হয়েছে — এটাই brute force। কিন্তু বাস্তব-জীবনের SAT ইনস্ট্যান্সে শত-হাজার ভ্যারিয়েবল থাকতে পারে, তখন $2^n$ combination brute force করে খোঁজা ব্যবহারিকভাবে অসম্ভব। লক্ষণীয়, verify_assignment ফাংশনটির খরচ কিন্তু $n$ যাই হোক না কেন ফর্মুলার আকারের সাপেক্ষে পলিনোমিয়ালই থাকে — এটাই "যাচাই সহজ, খোঁজা কঠিন হতে পারে" পার্থক্যের মূল কথা।

প্র ০৩ verify_assignment কি সবসময় সবগুলো ক্লজ শেষ পর্যন্ত চেক করে, নাকি মাঝপথে থামতে পারে?

এটি দুই জায়গায় তাড়াতাড়ি থামতে পারে: (১) একটি ক্লজের মধ্যে যেই মুহূর্তে কোনো একটি লিটারেল True হয়ে যায়, break দিয়ে ওই ক্লজের বাকি লিটারেল আর চেক করে না; (২) যেই মুহূর্তে কোনো একটি ক্লজ সন্তুষ্ট হয় না, পুরো ফাংশনই সাথে সাথে False রিটার্ন করে, বাকি ক্লজগুলো আর দেখেই না। এই জন্যই কোড সেলের আউটপুটে ভুল candidate-টির জন্য চেক সংখ্যা (৩) সঠিক candidate-টির (৭) চেয়ে কম — তবে দুই ক্ষেত্রেই worst-case খরচ ফর্মুলার মোট লিটারেল সংখ্যা $m$-এর বেশি হতে পারে না, তাই এটি এখনও $O(m)$।

অনুশীলন

  1. চিন্তা করুন: কোড সেলের formula-এর জন্য candidate = [False, True, True] (অর্থাৎ $x_0=\text{False}, x_1=\text{True}, x_2=\text{True}$) হাতে-কলমে চেক করে বলুন এটি $\phi$-কে সন্তুষ্ট করে কিনা।

    হ্যাঁ, সন্তুষ্ট করে। ক্লজ ১: $x_0 \lor x_1 \lor \lnot x_2 = F \lor T \lor F = T$। ক্লজ ২: $\lnot x_0 \lor x_2 = T \lor T = T$। ক্লজ ৩: $x_1 \lor \lnot x_2 = T \lor F = T$। ক্লজ ৪: $\lnot x_1 \lor \lnot x_0 \lor x_2 = F \lor T \lor T = T$। সবগুলো ক্লজ True, তাই পুরো ফর্মুলা সন্তুষ্ট।

  2. পরীক্ষা করুন: কোড সেলে candidate-এর মান [False, True, True]-এ বদলে Run চেপে আপনার হাতে-কলমের হিসাব যাচাই করুন, তারপর [True, False, True] দিয়ে চেষ্টা করে দেখুন এটি কেন ব্যর্থ হয়।

    [False, True, True] সন্তুষ্ট করবে (আউটপুটে True দেখাবে), অনুশীলন ১-এর হিসাবের সাথে মিলে যাবে। [True, False, True]-এর ক্ষেত্রে ক্লজ ৩ ($x_1 \lor \lnot x_2$) ব্যর্থ হবে ($x_1=\text{False}$, $\lnot x_2 = \lnot\text{True} = \text{False}$, তাই $F \lor F = F$) — ফাংশনটি সেখানেই থেমে False রিটার্ন করবে।

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

আগের পাঠ
ডাইনামিক অ্যারে ও পাথ-কম্প্রেশনসহ ইউনিয়ন-ফাইন্ড