এই পর্বে যা শিখবেন
- রুটিন, রুট, সিট-প্ল্যান আর লোডিংয়ের মতো দৈনন্দিন কাজের ভেতরে লুকানো NP-hard সমস্যা চেনা, আর P-তে থাকা চেহারায় একই রকম সমস্যা থেকে আলাদা করা
- ছোট ইনপুটে কোন exponential পদ্ধতি কত বড় n পর্যন্ত চলে, আর tree, bipartite ও interval গঠন কীভাবে সমস্যাকে সহজ করে
- Fixed-parameter tractability: vertex cover-এর bounded search tree, তার প্রমাণ, kernel আর brute force-এর সাথে তুলনা
- Nearest neighbour + 2-opt, hill climbing, simulated annealing-এর তাপমাত্রা, tabu search — আর এগুলোর সীমা
- SAT, ILP ও CP solver-এর জন্য সমস্যা লেখা, benchmark করা, আর সিদ্ধান্তদাতাকে gap দিয়ে বোঝানো
১ · “NP-hard” শুনে ভয় নয় — এর আসল মানে কী
পর্ব ৬৪-এ দেখেছেন P, NP, NP-complete আর reduction. সংক্ষেপে: একটি সমস্যা NP-hard মানে NP-এর প্রতিটি সমস্যাকে polynomial সময়ে এতে রূপান্তর করা যায়। তাই এর একটি polynomial সময়ের নির্ভুল অ্যালগরিদম পেলে P = NP প্রমাণ হয়ে যাবে — যা পঞ্চাশ বছরের বেশি সময় ধরে কেউ পারেননি। বাস্তবে কথাটির অর্থ: এই সমস্যার এমন কোনো অ্যালগরিদম জানা নেই, যা সব ইনপুটে, দ্রুত, আর নির্ভুল উত্তর দেয়।
বাক্যটিতে তিনটি দাবি আছে — “সব ইনপুটে”, “দ্রুত” (polynomial সময়ে) আর “নির্ভুল” (প্রমাণিত সর্বোত্তম)। পুরো পর্বের মূল কৌশল একটাই: এই তিনটির অন্তত একটি ছেড়ে দিন।
“সব ইনপুট” ছাড়ুন
আপনার ইনপুট হয়তো ছোট, বা বিশেষ গঠনের (tree, bipartite), বা উত্তরটি ছোট। তখন নির্ভুল আর যথেষ্ট দ্রুত দুটোই সম্ভব — অংশ ৩, ৪ আর ৫.
exact · ছোট বা বিশেষ ইনপুট“নির্ভুল” একটু ছাড়ুন
প্রমাণিত নিশ্চয়তাসহ কাছাকাছি উত্তর — approximation (অংশ ৬); অথবা নিশ্চয়তা ছাড়াই খুব ভালো উত্তর — heuristic আর local search (অংশ ৭–৮).
প্রায়-সর্বোত্তম · দ্রুত“দ্রুত” একটু ছাড়ুন
Worst case exponential, কিন্তু বাস্তবের ইনপুটে চমৎকার — branch and bound আর SAT/ILP solver (অংশ ৩ আর ৯).
exact · বাস্তবে দ্রুতঢাকা থেকে চট্টগ্রাম যাওয়ার কথা ভাবুন। “সবচেয়ে কম সময়, সবচেয়ে কম খরচ, আর সবচেয়ে আরামে” — তিনটি একসাথে চাইলে কোনো বাহনই মেলে না। বিমান দ্রুত কিন্তু দামি, ট্রেন আরামের কিন্তু সময় বেশি, বাস সস্তা কিন্তু জ্যামের ঝুঁকি। কোনটি ছাড়বেন, তা ঠিক করলেই সিদ্ধান্ত সহজ। NP-hard সমস্যাতেও তা-ই: কোন দাবি ছাড়বেন, সেটিই প্রকৌশলের আসল সিদ্ধান্ত।
NP-hard প্রমাণ কেন কাজের
একজন প্রোগ্রামার সপ্তাহের পর সপ্তাহ দ্রুত নির্ভুল অ্যালগরিদম খুঁজে হয়রান হতে পারেন। সমস্যাটি NP-hard প্রমাণ করতে পারলে সেই খোঁজ বন্ধ করে সঠিক প্রশ্নে যাওয়া যায়: “কোন দাবিটি ছাড়ব?” প্রমাণের পদ্ধতি পর্ব ৬৪-এর polynomial-time reduction: একটি জানা NP-hard সমস্যা Y-এর যেকোনো ইনপুটকে আপনার সমস্যা X-এর ইনপুটে বদলে দেখান। দিকটি গুরুত্বপূর্ণ — Y থেকে X-এ। উল্টো দিকে (X থেকে Y-এ) reduction শুধু বলে X বড়জোর Y-এর মতো কঠিন, যা সহজ সমস্যার বেলাতেও সত্য।
২ · ছদ্মবেশে NP-hard — কাজের জায়গায় চেনার উপায়
কাজের জায়গায় কেউ বলবে না “একটি graph coloring সমাধান করো।” বলবে, “আগামী মাসের পরীক্ষার রুটিন বানাও।” নিচের টেবিলে চারটি পরিচিত কাজ আর তাদের ভেতরের ক্লাসিক সমস্যা:
| বাস্তবের কাজ | ভেতরের ক্লাসিক সমস্যা | কোথায় কঠিনতা |
|---|---|---|
| পরীক্ষার রুটিন: যে দুটি বিষয়ের অন্তত একজন শিক্ষার্থী আছে, সেগুলো একই সময়ে নয় | Graph coloring: বিষয় = vertex, সংঘাত = edge, সময়-স্লট = রং | সবচেয়ে কম স্লট বের করা; এমনকি “৩টি স্লটে সম্ভব কি?” প্রশ্নটিই NP-complete |
| পরীক্ষার হলে সিট: পাশাপাশি দুজন একই বিভাগের নয় | সিটের পাশাপাশি-গ্রাফে রং (বিভাগ) বসানো, প্রতিটি বিভাগের নির্দিষ্ট সংখ্যা | সাধারণ গ্রাফে কঠিন — কিন্তু হলটি একটি গ্রিড, তাই প্রায়ই দাবার ছকের নকশাতেই মেটে (অংশ ৪) |
| ই-কমার্সের ডেলিভারি ভ্যানের রুট | TSP; কয়েকটি ভ্যান, ধারণক্ষমতা আর সময়সীমা থাকলে vehicle routing problem (VRP) | একটি ভ্যান আর ২০টি ঠিকানাতেই ১৯! ≈ ১.২ × ১০১৭ ক্রম |
| স্কুল বা বিশ্ববিদ্যালয়ের ক্লাস রুটিন | Timetabling: coloring + কক্ষ, শিক্ষক আর পছন্দের শর্ত | শর্ত যত বাড়ে, বৈধ রুটিন আছে কি না সেটাই কঠিন প্রশ্ন |
| গার্মেন্টস কারখানার কার্টন ট্রাকে তোলা | Bin packing: ওজনের জিনিস, সমান ধারণক্ষমতার বাক্স, বাক্স সবচেয়ে কম | সংখ্যাগুলো ছোট হলেও কঠিন (strongly NP-hard), তাই knapsack-এর মতো O(nW) DP-র সুবিধা নেই |
চেনার লক্ষণ — আর চেহারায় একই, অথচ সহজ
কিছু লক্ষণ দেখলেই সতর্ক হন: সমস্যাটি “সব উপসেট / সব ক্রম / সব ভাগাভাগি থেকে সেরাটি” চায়; জোড়ায় জোড়ায় সংঘাত আছে (“এ আর ও একসাথে নয়”); “প্রতিটি জায়গায় ঠিক একবার” যেতে হয়; অথবা ধারণক্ষমতায় পূর্ণসংখ্যক জিনিস ভরতে হয়। কিন্তু সাবধান — ছোট একটি বদলেই কঠিন সমস্যা সহজ হয়ে যায়:
| P-তে (দ্রুত সমাধান জানা) | NP-hard (চেহারায় প্রায় একই) |
|---|---|
| Shortest path (পর্ব ৪৩) | Longest simple path |
| Euler circuit — প্রতিটি রাস্তা একবার | Hamiltonian cycle — প্রতিটি জায়গা একবার (পর্ব ৫৬) |
| 2-coloring — bipartite কি না, BFS দিয়ে | 3-coloring |
| 2-SAT — implication graph আর SCC (পর্ব ৪৬) | 3-SAT |
| Minimum spanning tree (পর্ব ৪৫) | Steiner tree — শুধু কিছু vertex জুড়লেই চলে |
| Min cut (পর্ব ৪৭) | Max cut |
| Bipartite গ্রাফে maximum matching | 3-dimensional matching |
দুই ভুল এড়ান। এক, “দেখতে কঠিন, তাই NP-hard” — প্রমাণ না করে এ কথা বলবেন না; বাঁ কলামের সমস্যাগুলোও প্রথমে কঠিন মনে হয়। দুই, “NP-hard, তাই আমার ইনপুটেও অসম্ভব” — NP-hard একটি worst-case কথা; আপনার নির্দিষ্ট ইনপুট হয়তো ছোট বা সুন্দর গঠনের।
৩ · হাতিয়ার ১ — ছোট ইনপুট: exponential, তবু যথেষ্ট দ্রুত
NP-hard শুনলে অনেকে ভাবেন “exponential, মানে অচল।” কিন্তু প্রতি রাতে ১৫টি দোকানে মাল দেওয়া একটি ভ্যানের জন্য 215 · 15² ≈ ৭৪ লক্ষ ধাপ — চোখের পলকে শেষ। প্রশ্ন “exponential কি না” নয়, প্রশ্ন “আমার n-এ কত ধাপ।” মোটামুটি হিসাবে একটি সাধারণ কম্পিউটার সেকেন্ডে ১০৮-এর কাছাকাছি সরল কাজ করে। সেই হিসাবে:
| পদ্ধতি | সময় | এক সেকেন্ডে মোটামুটি কত n |
|---|---|---|
| সব ক্রম (TSP-এর brute force) | O(n!) | ১০–১১ |
| সব উপসেট পরীক্ষা | O(2ⁿ · n) | ২০–২৩ |
| Bitmask DP — যেমন Held–Karp (পর্ব ৫৫) | O(2ⁿ · n²) | ~২০ |
| Meet in the middle — দুই অর্ধেকের তালিকা মিলিয়ে | O(2n/2 · n) | ~৪০ |
| Backtracking + pruning (পর্ব ৫৬), branch and bound (পর্ব ৫৭) | Worst case exponential | ইনপুটের ওপর নির্ভর করে — ভালো bound থাকলে কয়েকশো থেকে হাজার |
খেয়াল করুন, ভিত্তি কমানোও বিশাল লাভ। n! থেকে 2ⁿ · n²-এ গেলে n = ২০-এ কাজ প্রায় ২.৪ × ১০১৮ থেকে নেমে ৪ × ১০৮; আর 2ⁿ থেকে 2n/2-এ গেলে একই সময়ে প্রায় দ্বিগুণ বড় n সামলানো যায়। Exponential অ্যালগরিদমের জগতেও “ভালো” আর “খারাপ” আছে।
আরেকটি “ছোট” আছে — সংখ্যার মান ছোট। 0/1 knapsack NP-hard, কিন্তু পর্ব ৫৩-এর DP O(nW) সময়ের, যা ধারণক্ষমতা W ছোট হলে দারুণ চলে। এমন সময়কে বলে pseudo-polynomial. ব্যাংকের টাকার হিসাবে W হয়তো কয়েক কোটি — তখন আর কাজে আসে না; কিন্তু ১০০ কেজির ভ্যানে কেজির হিসাবে W = ১০০, খুব সহজ।
৪ · হাতিয়ার ২ — গঠন কাজে লাগান: P-তে থাকা বিশেষ রূপ
NP-hard প্রমাণ হয় সাধারণ ইনপুটের জন্য। কিন্তু বাস্তবের ইনপুট প্রায়ই সাধারণ নয় — তার একটি বিশেষ আকার থাকে, আর সেই আকারে সমস্যাটি polynomial হয়ে যায়। কয়েকটি গুরুত্বপূর্ণ উদাহরণ:
- Bipartite গ্রাফ। পরীক্ষার হলের সারি-কলামের গ্রিডে (শুধু সামনে-পেছনে আর ডানে-বাঁয়ে পাশাপাশি ধরলে) গ্রাফটি bipartite: দাবার ছকের সাদা ঘরে এক বিভাগ, কালো ঘরে আরেক — দুই রঙেই শেষ। কোণাকুণি পাশাপাশিও গুনলে চারটি বিভাগ দিয়ে ২ × ২ নকশা পুনরাবৃত্তি করলেই চলে। পরিদর্শক আর হলের জোড় বানানো (bipartite maximum matching, পর্ব ৪৭) P-তে; আর König-এর উপপাদ্য বলে bipartite গ্রাফে সবচেয়ে ছোট vertex cover-এর আকার = maximum matching-এর আকার — তাই সেখানে vertex cover-ও সহজ।
- 2-SAT. প্রতিটি শর্তে বড়জোর দুটি চলক থাকলে (“রহিম আর করিম একই দলে নয়”) implication graph-এর SCC দিয়ে রৈখিক সময়ে সমাধান (পর্ব ৪৬). তিনটি হলেই NP-complete.
- Interval graph. প্রতিটি vertex যদি সময়ের একটি ব্যবধান হয় (ক্লাস ৯টা–১০:৩০), আর মিলে যাওয়া ব্যবধানে edge — তাহলে এটি interval graph. এখানে coloring মানে পর্ব ৫০-এর interval partitioning: শুরুর সময় ধরে সাজিয়ে greedy, আর রঙের সংখ্যা = একসাথে সর্বোচ্চ কয়টি ব্যবধান মেলে — যা একটি lower bound-ও, তাই সর্বোত্তম। শুধু সময়ের সংঘাত থাকলে কক্ষ বণ্টন তাই সহজ; বিষয়ভিত্তিক সংঘাত (একই শিক্ষার্থী) যোগ হলেই সাধারণ coloring.
- Tree. সাধারণ গ্রাফে minimum vertex cover আর maximum independent set NP-hard, কিন্তু গ্রাফটি tree হলে পর্ব ৫৫-এর tree DP দিয়ে O(n) সময়ে। নিচের স্টেপারে দেখুন।
Tree-তে vertex cover-এর DP: প্রতিটি node v-এর জন্য দুটি সংখ্যা — in[v] = v-কে cover-এ নিলে v-এর subtree-তে সবচেয়ে ছোট cover; out[v] = v-কে না নিলে। v নেওয়া হলে সন্তানদের স্বাধীনতা আছে: in[v] = 1 + Σ min(in[c], out[c]). v না নেওয়া হলে প্রতিটি edge v–c ঢাকতে প্রতিটি সন্তান c-কে নিতেই হবে: out[v] = Σ in[c]. Leaf-এ in = 1, out = 0.
1
/ \
2 3
/ \ \
4 5 6
| 4, 5, 6 (leaf) | |
| 2 | |
| 3 | |
| 1 (root) | |
| উত্তর |
- Leaf. ৪, ৫, ৬-এর নিচে কোনো edge নেই। নিজেকে নিলে খরচ ১ (in = ১), না নিলে ০ (out = ০).
- Node ২. in[২] = ১ + min(১, ০) + min(১, ০) = ১ — ২ নিলে ৪ আর ৫ লাগে না। out[২] = in[৪] + in[৫] = ২ — ২ বাদ দিলে edge ২–৪ আর ২–৫ ঢাকতে ৪ আর ৫ দুটোই লাগবে।
- Node ৩. in[৩] = ১ + min(১, ০) = ১; out[৩] = in[৬] = ১. দুটি সমান — ৩ নিন বা ৬ নিন, একই খরচ।
- Root ১. in[১] = ১ + min(১, ২) + min(১, ১) = ৩; out[১] = in[২] + in[৩] = ১ + ১ = ২. উত্তর min(৩, ২) = ২.
- উত্তর ফিরিয়ে আনা. Root-এ out ছোট, তাই ১ বাদ — তাহলে ২ আর ৩ নিতেই হবে। ২ নেওয়া, তাই ৪ আর ৫-এর জন্য ছোটটি: out (০), বাদ। ৩-এর নিচে ৬ বাদ। Cover {২, ৩}; বাকি {১, ৪, ৫, ৬} একটি maximum independent set — যেকোনো vertex cover-এর বাইরের vertex-গুলো সবসময় independent.
কেন ঠিক: subtree-র উচ্চতা ধরে induction. v-এর subtree-র যেকোনো cover-এ হয় v আছে, নয়তো নেই। থাকলে প্রতিটি সন্তানের subtree আলাদাভাবে ঢাকতে হয়, আর প্রতিটির সেরা খরচ min(in[c], out[c]); না থাকলে edge v–c-এর জন্য c থাকতেই হবে, সেরা খরচ in[c]. এই দুই ক্ষেত্রের বাইরে কিছু নেই। জটিলতা: প্রতিটি node একবার, প্রতিটি edge একবার — সময় O(n), জায়গা O(n).
এই ধারণার বড় রূপ treewidth: গ্রাফটি গাছের কতটা কাছাকাছি, তার একটি মাপ। Treewidth w হলে বহু NP-hard গ্রাফ সমস্যা প্রায় O(cw · n) সময়ে সমাধান হয় — গাছের মতো ছোট “ব্যাগ” ধরে DP. রাস্তার নেটওয়ার্ক, সার্কিট আর কম্পাইলারের control-flow গ্রাফে treewidth প্রায়ই ছোট, তাই এই কৌশল বাস্তবেও চলে।
৫ · Fixed-parameter tractability — vertex cover খোঁজের গাছে
ঢাকার একটি এলাকায় প্রতিটি রাস্তায় নজর রাখতে মোড়ে মোড়ে CCTV বসবে। একটি মোড়ের ক্যামেরা সেই মোড়ে মেলা সব রাস্তা দেখে। বাজেটে বড়জোর k টি ক্যামেরা। প্রশ্ন: “k টি মোড় বেছে কি প্রতিটি রাস্তা ঢাকা যায়?” এটাই vertex cover-এর decision রূপ — NP-complete. এলাকায় হয়তো n = ১০০০ মোড়, কিন্তু k = ১০. সব উপসেট 21000, অসম্ভব। কিন্তু খোঁজের exponential অংশটি যদি শুধু k-তে আটকে রাখা যায়?
এটাই fixed-parameter tractability (FPT): সময় f(k) · nc, যেখানে k একটি ছোট প্যারামিটার আর c ধ্রুবক। Vertex cover-এর জন্য একটি সুন্দর পর্যবেক্ষণই যথেষ্ট — যেকোনো edge (u, v) ঢাকতে u বা v-এর অন্তত একটি cover-এ থাকতেই হবে। তাই দুই শাখা: u নিন, নয়তো v নিন। প্রতিটি শাখায় বাজেট k এক কমে। এই কৌশলের নাম bounded search tree.
Pseudocode — does G have a vertex cover of size ≤ k?
VC(G, k):
if G has no edge: return YES
if k = 0: return NO
pick any edge (u, v)
return VC(G − u, k − 1) or VC(G − v, k − 1) // G − x: delete x and its edgesনিচের গ্রাফে ৫টি মোড় আর ৫টি রাস্তা: 1–2, 2–3, 3–1, 3–4, 4–5. প্রথমে k = ২ দিয়ে, তারপর k = ৩ দিয়ে খোঁজ চলবে। প্রতিবার বাকি রাস্তাগুলোর প্রথমটি বাছা হয়।
| বাজেট k | |
| এ পর্যন্ত node | |
| যে cover পরীক্ষা হলো |
- শুরু, k = ২. পাঁচটি রাস্তাই খোলা। প্রথম রাস্তা ১–২: ঢাকতে হলে ১ বা ২-এর একটি লাগবেই — তাই শুধু এই দুই শাখা, আর কিছু দেখার দরকার নেই।
- শাখা “১ নিন”. ১–২ আর ৩–১ ঢাকা পড়ল; বাকি ২–৩, ৩–৪, ৪–৫, বাজেট ১. রাস্তা ২–৩-এর জন্য আবার দুই শাখা: ২ নিলে ৩–৪ আর ৪–৫ বাকি, বাজেট ০ — ব্যর্থ। ৩ নিলে ৪–৫ বাকি — ব্যর্থ।
- শাখা “২ নিন”. বাকি ৩–১, ৩–৪, ৪–৫, বাজেট ১. ৩ নিলে ৪–৫ বাকি, ১ নিলে ৩–৪ আর ৪–৫ বাকি — দুটিই ব্যর্থ। সব শাখা ব্যর্থ, তাই প্রমাণ: ২টি ক্যামেরায় হবে না। “না” উত্তরে পুরো গাছ দেখতে হয় — ঠিক ২৩ − ১ = ৭টি node.
- k = ৩. একই প্রথম দুই সিদ্ধান্ত (১, তারপর ২), এবার বাজেট ১ বাকি, রাস্তা ৩–৪ আর ৪–৫. ৩ নিলে ৪–৫ খোলা থাকে — ব্যর্থ। ৪ নিলে দুটিই ঢাকা — হ্যাঁ, cover {১, ২, ৪}.
- কেন গাছ ছোট. প্রতিটি স্তরে বাজেট এক কমে, তাই গভীরতা ≤ k; প্রতিটি node-এর বড়জোর দুই সন্তান। ফলে leaf ≤ 2k, মোট node ≤ 2k+1 − 1 — n যত বড়ই হোক। এখানে ১৫-এর মধ্যে ৫টি node-ই যথেষ্ট হলো।
সঠিকতা: যদি k আকারের একটি cover C থাকে, তাহলে বাছা edge (u, v)-এর জন্য u ∈ C বা v ∈ C; ধরুন u ∈ C. তাহলে C − {u} গ্রাফ G − u-এর একটি cover, আকার k − 1 — অর্থাৎ ওই শাখা হ্যাঁ বলবে (k-এর ওপর induction). উল্টো দিকে, কোনো শাখা হ্যাঁ বললে সেই শাখার cover-এ বাছা vertex যোগ করলেই G-এর cover. জটিলতা: ≤ 2k+1 node, প্রতিটিতে edge তালিকা ছেঁকে নিতে O(m). আর শুরুতেই একটি সহজ পরীক্ষা: k টি vertex বড়জোর k(n − 1) টি edge ঢাকতে পারে, তাই m > k(n − 1) হলে সরাসরি “না”. ফলে m = O(kn), আর মোট সময় O(2k · k · n) — n-এ রৈখিক। জায়গা: রিকার্শনের গভীরতা k, প্রতিটি স্তরে একটি edge তালিকা — O(k · m).
Kernel — আগে ইনপুট ছোট করুন
আরও একটি চমৎকার কৌশল kernelization. Buss-এর নিয়ম: যে মোড়ে k-এর বেশি রাস্তা মিলেছে, তাকে cover-এ নিতেই হবে — না নিলে তার সব (k-এর বেশি) প্রতিবেশীকে নিতে হতো, বাজেট ছাড়িয়ে যেত। এমন সব vertex নিয়ে নেওয়ার পর প্রতিটি vertex-এর degree ≤ k, তাই বাকি k টি vertex বড়জোর k² টি edge ঢাকতে পারে; k²-এর বেশি edge বাকি থাকলে উত্তর “না”. যা বাকি থাকে তা একটি kernel — আকার শুধু k-এর ওপর নির্ভর করে, n-এর ওপর নয়। Kernel-এর ওপর খোঁজের গাছ চালালে মোট সময় O(n + m + 2k · k²).
কোডে — FPT খোঁজের গাছ বনাম brute force
১২টি মোড় আর ১৬টি রাস্তার একটি নির্দিষ্ট গ্রাফে প্রোগ্রামটি k = ০, ১, ২, … ধরে খোঁজের গাছ চালায়, প্রতিটি k-এ কতটি node লাগল ছাপে, আর শেষে সব 212 = ৪০৯৬টি উপসেট দেখে brute force দিয়ে উত্তর মেলায়।
#include <algorithm>
#include <bit>
#include <cstdio>
#include <utility>
#include <vector>
using namespace std;
using Edges = vector<pair<int, int>>;
const int N = 12; // 12 junctions, 16 roads
const Edges E = {{0, 1}, {0, 2}, {1, 3}, {2, 3}, {3, 4}, {4, 5}, {4, 6}, {5, 7},
{6, 7}, {7, 8}, {8, 9}, {2, 9}, {1, 5}, {9, 10}, {10, 11}, {8, 11}};
long long nodes = 0;
vector<int> cover;
bool vc(const Edges& es, int k) { // is there a vertex cover of size <= k?
nodes++;
if (es.empty()) return true; // every edge covered
if (k == 0) return false; // edges left, budget used up
for (int x : {es[0].first, es[0].second}) { // edge (u, v): u or v MUST be in the cover
Edges rest;
for (auto [a, b] : es) if (a != x && b != x) rest.push_back({a, b});
cover.push_back(x);
if (vc(rest, k - 1)) return true;
cover.pop_back();
}
return false;
}
int main() {
long long total = 0;
for (int k = 0; ; k++) { // try k = 0, 1, 2, ... until YES
nodes = 0;
bool ok = vc(E, k);
total += nodes;
printf("FPT k=%d: %s, nodes=%lld (limit %d)\n", k, ok ? "YES" : "NO ", nodes, (1 << (k + 1)) - 1);
if (ok) break;
}
printf("cover:");
for (int x : cover) printf(" %d", x);
printf("\nFPT total nodes = %lld\n", total);
int best = N; // brute force: all 2^N subsets
for (unsigned m = 0; m < (1u << N); m++)
if (all_of(E.begin(), E.end(), [m](auto e) { return (m >> e.first & 1) || (m >> e.second & 1); }))
best = min(best, popcount(m));
printf("brute force: %d subsets checked, minimum cover size = %d\n", 1 << N, best);
}আউটপুট:
FPT k=0: NO , nodes=1 (limit 1)
FPT k=1: NO , nodes=3 (limit 3)
FPT k=2: NO , nodes=7 (limit 7)
FPT k=3: NO , nodes=15 (limit 15)
FPT k=4: NO , nodes=31 (limit 31)
FPT k=5: NO , nodes=63 (limit 63)
FPT k=6: YES, nodes=124 (limit 127)
cover: 1 2 4 7 8 10
FPT total nodes = 244
brute force: 4096 subsets checked, minimum cover size = 6
প্রতিটি “NO” লাইনে node সংখ্যা ঠিক সীমার সমান (2k+1 − 1): “না” প্রমাণ করতে প্রতিটি শাখা শেষ পর্যন্ত দেখতে হয়। “YES” আগেই থামে — ১২৭-এর মধ্যে ১২৪. for (int x : {es[0].first, es[0].second}) হলো দুই শাখা; rest হলো G − x-এর edge তালিকা। সব k মিলিয়ে ২৪৪টি node, brute force-এ ৪০৯৬টি উপসেট — আর প্রতিটি উপসেটে ১৬টি edge পরীক্ষা। n = ১২-তে পার্থক্য ১৭ গুণ; n = ১০০০ আর k = ১০ হলে brute force-এর 21000-এর পাশে খোঁজের গাছ বড়জোর ২০৪৭টি node.
N = 12 # 12 junctions, 16 roads
E = [(0, 1), (0, 2), (1, 3), (2, 3), (3, 4), (4, 5), (4, 6), (5, 7),
(6, 7), (7, 8), (8, 9), (2, 9), (1, 5), (9, 10), (10, 11), (8, 11)]
nodes, cover = 0, []
def vc(es, k): # is there a vertex cover of size <= k?
global nodes
nodes += 1
if not es: return True # every edge covered
if k == 0: return False # edges left, budget used up
for x in es[0]: # edge (u, v): u or v MUST be in the cover
cover.append(x)
if vc([(a, b) for a, b in es if a != x and b != x], k - 1): return True
cover.pop()
return False
total, k = 0, 0
while True: # try k = 0, 1, 2, ... until YES
nodes = 0
ok = vc(E, k)
total += nodes
print(f"FPT k={k}: {'YES' if ok else 'NO '}, nodes={nodes} (limit {2 ** (k + 1) - 1})")
if ok: break
k += 1
print("cover:", *cover)
print(f"FPT total nodes = {total}")
best = min(bin(m).count("1") for m in range(1 << N) # brute force: all 2^N subsets
if all(m >> a & 1 or m >> b & 1 for a, b in E))
print(f"brute force: {1 << N} subsets checked, minimum cover size = {best}")আউটপুট (C++-এর সাথে হুবহু এক):
FPT k=0: NO , nodes=1 (limit 1)
FPT k=1: NO , nodes=3 (limit 3)
FPT k=2: NO , nodes=7 (limit 7)
FPT k=3: NO , nodes=15 (limit 15)
FPT k=4: NO , nodes=31 (limit 31)
FPT k=5: NO , nodes=63 (limit 63)
FPT k=6: YES, nodes=124 (limit 127)
cover: 1 2 4 7 8 10
FPT total nodes = 244
brute force: 4096 subsets checked, minimum cover size = 6
for x in es[0] edge-এর দুই প্রান্ত ধরে দুই শাখা চালায়; list comprehension দিয়ে x ছোঁয়া edge বাদ যায়। cover একটি stack-এর মতো: শাখায় ঢোকার সময় push, ব্যর্থ হলে pop — backtracking-এর পরিচিত ছাঁচ। Brute force অংশে m >> a & 1 দেখে vertex a উপসেট m-এ আছে কি না — পর্ব ৫৫-এর bitmask কৌশল।
আরও গভীরে: একটু চালাক branching (degree ১ বা ২-এর vertex আলাদাভাবে সামলানো) দিয়ে vertex cover-এর সেরা জানা FPT অ্যালগরিদম প্রায় O(1.2738k + kn) সময়ের (Chen, Kanj ও Xia, ২০১০). কিন্তু সব প্যারামিটার এত ভালো নয় — independent set বা clique-কে উত্তরের আকার k দিয়ে মাপলে সেগুলো W[1]-hard, অর্থাৎ f(k) · nc অ্যালগরিদম আশা করা হয় না। আর Courcelle-এর উপপাদ্য বলে, monadic second-order logic-এ লেখা যায় এমন যেকোনো গ্রাফ-ধর্ম treewidth-সীমিত গ্রাফে রৈখিক সময়ে যাচাই করা যায় — যদিও ধ্রুবকটি ভয়ংকর বড়।
৬ · হাতিয়ার ৩ — নিশ্চয়তাসহ approximation
ইনপুট বড়, গঠনও সাধারণ — তখন “নির্ভুল” দাবিটি একটু ছাড়ুন, কিন্তু নিয়ন্ত্রিতভাবে। একটি approximation algorithm polynomial সময়ে চলে আর প্রমাণ দেয়: উত্তর কখনো সর্বোত্তমের নির্দিষ্ট গুণের বেশি খারাপ নয়। পর্ব ৬৬ পুরোটাই এর জন্য; এখানে শুধু স্বাদ নিন।
- Vertex cover, ২ গুণ: যেকোনো না-ঢাকা edge নিন, তার দুই প্রান্তই cover-এ দিন, তাদের ছোঁয়া সব edge মুছুন, আবার করুন। বাছা edge-গুলো কোনো vertex ভাগ করে না (একটি matching), আর যেকোনো cover-কে প্রতিটি থেকে অন্তত একটি প্রান্ত নিতে হয় — তাই সর্বোত্তম ≥ বাছা edge-এর সংখ্যা, আর আমাদের উত্তর ঠিক তার দ্বিগুণ।
- Metric TSP, ১.৫ গুণ: দূরত্ব যদি triangle inequality মানে (সরাসরি যাওয়া কখনো ঘুরে যাওয়ার চেয়ে লম্বা নয়), Christofides-এর ১৯৭৬ সালের অ্যালগরিদম MST আর matching মিলিয়ে সর্বোত্তমের বড়জোর ১.৫ গুণ দেয়। কিন্তু দূরত্ব যেমন খুশি হলে কোনো ধ্রুবক গুণের নিশ্চয়তাই সম্ভব নয়, যদি না P = NP.
- Bin packing: বড় থেকে ছোট সাজিয়ে প্রতিটি জিনিস প্রথম যে ট্রাকে ধরে সেখানে দিন (First Fit Decreasing) — ট্রাকের সংখ্যা মোটামুটি সর্বোত্তমের ১১/৯ গুণের বেশি নয়, সাথে একটি ছোট ধ্রুবক।
পরীক্ষা ও ইন্টারভিউতে — এই প্রশ্নগুলো প্রায়ই আসে:
- NP-hard সমস্যা দেখলে কী কী পথ আছে? ছোট ইনপুটে exact (DP/B&B), বিশেষ গঠনে polynomial, ছোট প্যারামিটারে FPT, approximation, heuristic/local search, আর SAT/ILP solver.
- Tree-তে minimum vertex cover কত সময়ে? Tree DP, O(n): in[v] = 1 + Σ min(in[c], out[c]), out[v] = Σ in[c].
- Vertex cover কেন FPT? প্রতিটি edge-এর এক প্রান্ত লাগবেই — দুই শাখা, প্রতিটিতে k এক কমে; গাছে ≤ 2k leaf, সময় O(2k · k · n).
- Approximation আর heuristic-এর পার্থক্য? Approximation-এর গুণমানের প্রমাণিত সীমা আছে; heuristic-এর নেই — শুধু অভিজ্ঞতায় ভালো।
- নতুন সমস্যা X NP-hard প্রমাণের দিক কোনটি? জানা NP-hard Y থেকে X-এ polynomial reduction.
৭ · হাতিয়ার ৪ — heuristic আর local search: 2-opt
বাস্তবের বেশিরভাগ বড় রুটিং আর সময়সূচির সফটওয়্যার শেষ পর্যন্ত heuristic চালায়: দ্রুত, প্রায়ই চমৎকার, কিন্তু কোনো প্রমাণিত নিশ্চয়তা নেই। কাঠামোটি সাধারণত দুই ধাপের:
- Greedy construction: একটি সম্পূর্ণ সমাধান দ্রুত বানানো — যেমন TSP-এর nearest neighbour (সবসময় সবচেয়ে কাছের অদেখা দোকানে যাওয়া), বা coloring-এ সবচেয়ে বেশি সংঘাতের বিষয়কে আগে রং দেওয়া।
- Improvement: local search — সমাধানটিতে একটি ছোট পরিবর্তন (move) করে দেখা, ভালো হলে রাখা, যতক্ষণ উন্নতি হয়।
TSP-এর সবচেয়ে বিখ্যাত move হলো 2-opt: tour থেকে দুটি edge a–b আর c–e সরিয়ে a–c আর b–e জুড়ুন। এতে b থেকে c পর্যন্ত অংশটি উল্টো দিকে চলে। লাভ = d(a,b) + d(c,e) − d(a,c) − d(b,e); ধনাত্মক হলেই move নিন। সমতলে দুটি edge পরস্পর কাটলে (crossing) এই move সবসময় লাভজনক — triangle inequality থেকে আসে, কারণ কাটা দুই কর্ণের যোগফল সামনের দুই বাহুর যোগফলের চেয়ে বড়। তাই 2-opt শেষে tour-এ কোনো crossing থাকে না। নিচে ৮টি দোকানের উদাহরণ (স্থানাঙ্ক কিলোমিটারে, দোকান ০ থেকে শুরু):
| tour | |
| দৈর্ঘ্য (km) | |
| move |
- Nearest neighbour. দোকান ০ থেকে সবসময় সবচেয়ে কাছেরটিতে: ০ → ৫ → ৪ → ৩ → ৭, তারপর বাকি সব দূরে, তাই লম্বা লাফ ৭ → ১ (১৩ km), শেষে ৬ থেকে ০-তে ফেরা, ১২.০৮ km পথ। দৈর্ঘ্য ৪৯.২৫. লাল দুই edge ৫–৪ আর ৭–১ পরস্পর কেটেছে।
- প্রথম 2-opt. ৫–৪ (৪.৪৭) আর ৭–১ (১৩.০০) সরিয়ে ৫–৭ (৬.৪০) আর ৪–১ (১০.৩০) — মাঝের অংশ ৪-৩-৭ উল্টে ৭-৩-৪. লাভ ১৭.৪৭ − ১৬.৭০ = ০.৭৭, দৈর্ঘ্য ৪৮.৪৮. কিন্তু নতুন edge ৪–১ এখন ৬–০-কে কাটছে (লাল)।
- দ্বিতীয় 2-opt. ৪–১ (১০.৩০) আর ৬–০ (১২.০৮) সরিয়ে ৪–৬ (১১.৬৬) আর ১–০ (৪.০০) — লাভ ৬.৭২, দৈর্ঘ্য ৪১.৭৬. এখন কোনো দুই edge কাটে না, আর কোনো 2-opt move-ই আর দৈর্ঘ্য কমায় না — এটি একটি local optimum.
- সর্বোত্তমের সাথে তুলনা. সব ৫০৪০টি tour দেখে সর্বোত্তম ৪০.৯৯ (গাঢ় দাগে), ডটেড দাগে 2-opt-এর tour. পার্থক্য দোকান ৪-এর জায়গায়: সর্বোত্তমে ৪ বসেছে ৭ আর ৫-এর মাঝে। একটি দোকানকে তুলে অন্য জায়গায় বসানো (Or-opt move) 2-opt-এর প্রতিবেশের বাইরে — তাই local optimum গ্লোবাল নয়, ঘাটতি ১.৮৮%.
জটিলতা: একটি পূর্ণ পাসে O(n²) জোড়া (i, j) পরীক্ষা, প্রতিটির লাভ O(1); move নিলে অংশ উল্টাতে O(n). কয়টি পাস লাগবে তার ভালো সীমা নেই — কিছু কৃত্রিম ইনপুটে 2-opt exponential সংখ্যক move নিতে পারে, যদিও বাস্তবে n-এর ছোট গুণিতক পাসেই থামে। বড় n-এ প্রতিটি শহরের শুধু কাছের ১০–২০টি প্রতিবেশী (neighbour list) দেখে কাজ অনেক কমানো হয়। জায়গা O(n).
কোডে — nearest neighbour + 2-opt, তারপর brute force দিয়ে যাচাই
প্রোগ্রামটি ওপরের ৮টি দোকানে nearest neighbour বানায়, first-improvement 2-opt চালায় (প্রথম যে লাভজনক move পায়, সেটিই নেয়), প্রতিটি move ছাপে, আর শেষে দোকান ০ স্থির রেখে বাকি ৭টির সব ৭! = ৫০৪০টি ক্রম দেখে সর্বোত্তম বের করে।
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <numeric>
#include <utility>
#include <vector>
using namespace std;
const vector<pair<int, int>> P = {{12, 3}, {16, 3}, {18, 8}, {5, 10}, {7, 8}, {9, 4}, {17, 14}, {4, 8}};
const int n = P.size(); // 8 shops (x, y) in km
double d(int a, int b) {
double dx = P[a].first - P[b].first, dy = P[a].second - P[b].second;
return sqrt(dx * dx + dy * dy);
}
double length(const vector<int>& t) {
double s = 0.0; for (int i = 0; i < n; i++) s += d(t[i], t[(i + 1) % n]);
return s;
}
void show(const char* name, const vector<int>& t) {
printf("%-9s", name); for (int x : t) printf("%d-", x);
printf("0 length %.2f\n", length(t));
}
int main() {
vector<int> t = {0}; // nearest neighbour from shop 0
while ((int)t.size() < n) {
int c = -1;
for (int j = 0; j < n; j++)
if (find(t.begin(), t.end(), j) == t.end() && (c < 0 || d(t.back(), j) < d(t.back(), c))) c = j;
t.push_back(c);
}
show("NN", t);
for (bool improved = true; improved; ) { // 2-opt, first improvement
improved = false;
for (int i = 0; i < n - 1; i++) for (int j = i + 2; j < n; j++) {
int a = t[i], b = t[i + 1], c = t[j], e = t[(j + 1) % n];
double gain = d(a, b) + d(c, e) - d(a, c) - d(b, e);
if (a == e || gain <= 1e-9) continue; // replace a-b, c-e by a-c, b-e
reverse(t.begin() + i + 1, t.begin() + j + 1); improved = true;
printf(" 2-opt: drop %d-%d,%d-%d add %d-%d,%d-%d gain %.2f\n", a, b, c, e, a, c, b, e, gain);
}
}
show("2-opt", t);
vector<int> p(n), best; iota(p.begin(), p.end(), 0);
double bl = INFINITY; long long cnt = 0;
do { cnt++; if (length(p) < bl) { bl = length(p); best = p; } } // exact: all (n-1)! tours
while (next_permutation(p.begin() + 1, p.end()));
show("optimum", best);
printf("checked %lld tours; 2-opt gap = %.2f%%\n", cnt, 100 * (length(t) - bl) / bl);
}আউটপুট:
NN 0-5-4-3-7-1-2-6-0 length 49.25
2-opt: drop 5-4,7-1 add 5-7,4-1 gain 0.77
2-opt: drop 4-1,6-0 add 4-6,1-0 gain 6.72
2-opt 0-5-7-3-4-6-2-1-0 length 41.76
optimum 0-1-2-6-3-7-4-5-0 length 40.99
checked 5040 tours; 2-opt gap = 1.88%
দুটি 2-opt লাইন স্টেপারের দুই ধাপের সাথে হুবহু মেলে। reverse(t.begin() + i + 1, t.begin() + j + 1) হলো move-এর পুরো কাজ: অবস্থান i+1 থেকে j পর্যন্ত উল্টালে edge a–b আর c–e বদলে a–c আর b–e হয়। gain <= 1e-9 হলে move বাদ — দশমিকের rounding-এর কারণে প্রায়-শূন্য লাভকে লাভ ভেবে অনন্ত লুপ এড়াতে। শেষ লাইন: brute force ৫০৪০টি tour দেখেছে, আর 2-opt সর্বোত্তমের ১.৮৮% বেশি দৈর্ঘ্যে থেমেছে।
import itertools, math
P = [(12, 3), (16, 3), (18, 8), (5, 10), (7, 8), (9, 4), (17, 14), (4, 8)] # 8 shops (x, y) in km
n = len(P)
def d(a, b):
dx, dy = P[a][0] - P[b][0], P[a][1] - P[b][1]
return math.sqrt(dx * dx + dy * dy)
def length(t):
s = 0.0
for i in range(n): s += d(t[i], t[(i + 1) % n])
return s
def show(name, t): print(f"{name:<9}" + "-".join(map(str, t)) + f"-0 length {length(t):.2f}")
t = [0] # nearest neighbour from shop 0
while len(t) < n:
t.append(min((j for j in range(n) if j not in t), key=lambda j: d(t[-1], j)))
show("NN", t)
improved = True
while improved: # 2-opt, first improvement
improved = False
for i in range(n - 1):
for j in range(i + 2, n):
a, b, c, e = t[i], t[i + 1], t[j], t[(j + 1) % n]
if a == e: continue
gain = d(a, b) + d(c, e) - d(a, c) - d(b, e)
if gain > 1e-9: # replace a-b, c-e by a-c, b-e
t[i + 1:j + 1] = t[i + 1:j + 1][::-1]; improved = True
print(f" 2-opt: drop {a}-{b},{c}-{e} add {a}-{c},{b}-{e} gain {gain:.2f}")
show("2-opt", t)
best, bl, cnt = None, math.inf, 0
for p in itertools.permutations(range(1, n)): # exact: all (n-1)! tours
cnt += 1; tour = [0, *p]; l = length(tour)
if l < bl: bl, best = l, tour
show("optimum", best)
print(f"checked {cnt} tours; 2-opt gap = {100 * (length(t) - bl) / bl:.2f}%")আউটপুট (C++-এর সাথে হুবহু এক):
NN 0-5-4-3-7-1-2-6-0 length 49.25
2-opt: drop 5-4,7-1 add 5-7,4-1 gain 0.77
2-opt: drop 4-1,6-0 add 4-6,1-0 gain 6.72
2-opt 0-5-7-3-4-6-2-1-0 length 41.76
optimum 0-1-2-6-3-7-4-5-0 length 40.99
checked 5040 tours; 2-opt gap = 1.88%
t[i + 1:j + 1] = t[i + 1:j + 1][::-1] হলো Python-এ অংশ উল্টানো। min(..., key=...) সমান দূরত্বে প্রথমটি নেয়, C++-এর < তুলনাও তাই — এজন্য দুই ভাষার tour এক। দূরত্ব একই ক্রমে যোগ হয়, তাই দশমিকের শেষ অঙ্কও মেলে। itertools.permutations আর next_permutation দুটিই lexicographic ক্রমে চলে।
৮ · Hill climbing থেকে simulated annealing, tabu আর genetic
ওপরের 2-opt আসলে hill climbing: শুধু ভালো move নিন, আর না থাকলে থামুন। কুয়াশার মধ্যে পাহাড়ে ওঠার মতো — সবসময় ওপরের দিকে পা ফেলুন, আর যেখানে চারদিকেই নিচে, সেখানে থামুন। সমস্যা: সেটি হয়তো একটি ছোট টিলা, পাশেই বড় পাহাড়। Local optimum থেকে বেরোনোর কৌশলগুলোকে একসাথে বলে metaheuristic.
Simulated annealing-এর বুদ্ধি ধাতু গলিয়ে ধীরে ঠান্ডা করার (annealing) প্রক্রিয়া থেকে: গরম অবস্থায় পরমাণু ছোটাছুটি করে, ধীরে ঠান্ডা হলে সুশৃঙ্খল কম-শক্তির কাঠামোয় বসে। অ্যালগরিদমে একটি “তাপমাত্রা” T থাকে। একটি এলোমেলো প্রতিবেশী নিন; খরচ বাড়ার পরিমাণ Δ. Δ ≤ 0 (উন্নতি) হলে নিন; খারাপ হলেও (Δ > 0) নিন e−Δ/T সম্ভাবনায়। T ধীরে কমে — এই নিয়মের নাম cooling schedule.
Pseudocode — simulated annealing (minimisation)
S ← initial solution (e.g. nearest neighbour); best ← S; T ← T0
repeat until T < Tmin or time is up:
S' ← random neighbour of S // e.g. a random 2-opt move
Δ ← cost(S') − cost(S)
if Δ ≤ 0 or random() < exp(−Δ / T): S ← S'
if cost(S) < cost(best): best ← S
T ← α · T // geometric cooling, α ≈ 0.95 … 0.999
return bestনিচের স্টেপারে একটি geometric schedule: T = ১৬ থেকে শুরু, প্রতিবার অর্ধেক (α = ০.৫ — বোঝানোর জন্য খুব দ্রুত; বাস্তবে α ০.৯৫-এর ওপরে)। দুটি খারাপ move ধরুন: সামান্য খারাপ (Δ = +১ km) আর বেশ খারাপ (Δ = +৪ km).
| তাপমাত্রা T | |
| Δ = +1 নেওয়ার সম্ভাবনা | |
| Δ = +4 নেওয়ার সম্ভাবনা | |
| আচরণ |
- খুব গরম, T = ১৬. সামান্য খারাপ move প্রায় ৯৪% বার, বেশ খারাপটিও ৭৮% বার নেওয়া হয়। খোঁজ প্রায় এলোমেলো ঘোরে — কোনো টিলায় আটকে থাকে না, পুরো এলাকা ঘুরে দেখে।
- T = ৮. সম্ভাবনা ০.৮৮ আর ০.৬১. এখনো অনেক খারাপ move নেয়, তবে বড় খারাপগুলো একটু কম।
- T = ৪. Δ = +৪ এখন মাত্র ৩৭% বার। খোঁজ ভালো অঞ্চলের দিকে ঝুঁকছে, তবু মাঝে মাঝে বড় লাফ দিয়ে অন্য উপত্যকায় যায়।
- T = ২. বড় খারাপ move মাত্র ১৩.৫% বার, ছোট খারাপ এখনো ৬১%. এখন মূলত একটি ভালো অঞ্চলের ভেতরে খুঁটিনাটি ঠিক হচ্ছে।
- T = ১. বড় খারাপ move প্রায় বন্ধ (১.৮%), ছোটগুলো ৩৭%. সমাধান একটি গভীর উপত্যকায় স্থির হচ্ছে।
- প্রায় ঠান্ডা, T = ০.১. খারাপ move কার্যত শূন্য — এখন এটি পুরোপুরি hill climbing. শুরুর ঘোরাঘুরিতে যদি ভালো উপত্যকায় পৌঁছানো গিয়ে থাকে, শেষটা সেখানে একটি ভালো local optimum. তাই সবসময় পথে পাওয়া সেরা সমাধানটি (best) আলাদা করে রাখা হয়।
একই পরিবারের আরও দুই সদস্য। Tabu search প্রতিবার সেরা প্রতিবেশীতে যায় — খারাপ হলেও — কিন্তু সাম্প্রতিক move-গুলো একটি “নিষিদ্ধ তালিকা”য় রাখে, যাতে সাথে সাথে ফিরে না আসে; এভাবে টিলা থেকে নেমে অন্য পাহাড়ে ওঠে। Genetic algorithm একসাথে অনেক সমাধানের “জনসংখ্যা” রাখে, ভালোগুলোর অংশ মিলিয়ে (crossover) আর একটু বদলে (mutation) নতুন প্রজন্ম বানায়। সবগুলোর সাধারণ সত্য: কোনো নিশ্চয়তা নেই — কতক্ষণ চালালে কতটা ভালো, তার প্রমাণ নেই, আর টিউনিং (T0, α, তালিকার দৈর্ঘ্য, জনসংখ্যা) ইনপুট ধরে বদলায়। এলোমেলোতা লাগে বলে একটি নির্দিষ্ট seed-এর PRNG ব্যবহার করুন, যাতে ফল পুনরায় পাওয়া যায় আর বাগ ধরা যায়।
Heuristic-এর উত্তর U, আর একটি lower bound L (যেমন পর্ব ৫৭-এর 1-tree বা LP relaxation) থাকলে সর্বোত্তম OPT অবশ্যই L ≤ OPT ≤ U-এর মধ্যে। তাই (U − L) / L হলো একটি প্রমাণিত সর্বোচ্চ ঘাটতি — heuristic নিজে কোনো প্রমাণ না দিলেও। “আমাদের রুট সর্বোত্তমের বড়জোর ৩% বেশি” — এই বাক্যটি শুধু তখনই বলা যায়।
৯ · হাতিয়ার ৫ — solver: SAT, ILP আর constraint programming
অনেক সময় সবচেয়ে ভালো অ্যালগরিদম হলো নিজে অ্যালগরিদম না লেখা। কয়েক দশক ধরে হাজারো গবেষকের পরিশ্রমে তৈরি তিন ধরনের solver আছে; আপনার কাজ শুধু সমস্যাটি তাদের ভাষায় লেখা, যাকে বলে formulation.
SAT solver
SAT: true/false চলক আর clause. আধুনিক SAT solver-এর ভিত্তি CDCL — backtracking, কিন্তু প্রতিটি বিরোধ থেকে একটি নতুন clause শিখে অনেক ধাপ পেছনে লাফায়। হার্ডওয়্যার যাচাই, software verification আর প্যাকেজ-নির্ভরতা মেলাতে ব্যবহৃত।
MiniSat · Kissat · CaDiCaLILP / MIP solver
Integer programming: linear শর্ত, linear লক্ষ্য, পূর্ণসংখ্যা চলক। ভেতরে LP relaxation, branch and cut আর heuristic — পর্ব ৫৭-এর গাছ, শিল্পের মাপে। প্রতিটি মুহূর্তে gap দেখায়।
HiGHS · SCIP · CBC · Gurobi · CPLEXConstraint programming
CP: চলক, তাদের সম্ভাব্য মান, আর উচ্চস্তরের শর্ত (“সব আলাদা”, “একসাথে বড়জোর ৩টি কাজ”)। Solver propagation দিয়ে অসম্ভব মান ছেঁটে খোঁজে — সময়সূচি আর রোস্টারে দারুণ।
OR-Tools CP-SAT · MiniZincসমস্যা লেখা — পরীক্ষার রুটিন একটি integer program হিসেবে
Pseudocode — exam timetabling (graph colouring) as an integer program
variables: x[e][s] ∈ {0,1} exam e is held in slot s
y[s] ∈ {0,1} slot s is used
minimise Σ_s y[s] // fewest slots
subject to Σ_s x[e][s] = 1 for every exam e // each exam exactly once
x[e][s] ≤ y[s] for every e, s // a used slot is counted
x[e][s] + x[f][s] ≤ 1 for every clash (e, f), every slot sশেষ শর্তটিই coloring-এর নিয়ম: একই শিক্ষার্থী আছে এমন দুই বিষয় e আর f একই স্লটে নয়। একই সমস্যা SAT-এ লেখা যায় clause (¬xe,s ∨ ¬xf,s) দিয়ে, আর CP-তে “প্রতিটি বিষয়ের স্লট একটি চলক, সংঘাতী জোড়ায় মান আলাদা” লিখে। নতুন শর্ত যোগ করা খুব সহজ — “কোনো শিক্ষার্থীর একদিনে দুটির বেশি পরীক্ষা নয়” মানে আরও কয়েক সারি linear শর্ত — যা নিজে লেখা heuristic-এ পুরো কোড বদলে দিতে পারে।
Benchmark আর “যথেষ্ট ভালো”
পদ্ধতি বাছাইয়ের আগে মাপুন। তুলনার জন্য প্রচলিত benchmark আছে — TSP-এর TSPLIB, integer programming-এর MIPLIB, আর প্রতি বছরের SAT Competition. নিজের প্রতিষ্ঠানের আসল ডেটার কয়েকটি নমুনা রাখুন, আর প্রতিটি পদ্ধতির সময় আর gap একটি টেবিলে লিখুন। প্রায়ই দেখবেন: শেষ ১%-এর জন্য সময় দশ গুণ বাড়ে, অথচ ইনপুট নিজেই অনিশ্চিত — ঢাকার রাস্তায় কাল বিকেলের যাত্রার সময় ১০–২০% এদিক-ওদিক হতেই পারে। ১% gap-এর রুট তখন বাস্তবে “সর্বোত্তম”-এর চেয়ে আলাদা কিছু নয়।
“এই সমস্যাটি গণিতে প্রমাণিত-কঠিন শ্রেণির (NP-hard): বিশ্বের কেউই এমন পদ্ধতি জানেন না যা সব ক্ষেত্রে দ্রুত আর একেবারে নিখুঁত উত্তর দেয়। আমাদের ১৮টি দোকানে নিখুঁত উত্তর বের করা সম্ভব; কিন্তু ৩০০টি ঠিকানায় তা অসম্ভব। তাই আমরা এমন রুট দিচ্ছি যার দৈর্ঘ্য প্রমাণিতভাবে সম্ভাব্য সেরার বড়জোর ২% বেশি — আর এটি ১ মিনিটে আসে। বাকি ২% পেতে হয়তো দিনের পর দিন লাগবে, আর তা রাস্তার জ্যামের ওঠানামার চেয়েও কম।” সংখ্যা, সময় আর gap — এই তিনটিই আলোচনার ভাষা।
ইতিহাসের পাতা থেকে: ১৯৭৯ সালে Michael Garey আর David Johnson প্রকাশ করেন Computers and Intractability: A Guide to the Theory of NP-Completeness — শত শত NP-complete সমস্যার তালিকাসহ, যা আজও “কোনো সমস্যা আগে কেউ কঠিন প্রমাণ করেছেন কি না” দেখার প্রথম জায়গা। বইয়ের শুরুর কার্টুনে এক কর্মী বসকে বলছেন: “আমি দক্ষ অ্যালগরিদম খুঁজে পাচ্ছি না, কিন্তু এই বিখ্যাত মানুষেরাও পাননি” — পেছনে সারি বেঁধে দাঁড়ানো বিখ্যাত গবেষকেরা। Local search-এর দিকে, Brian Kernighan আর Shen Lin ১৯৭০ সালে গ্রাফ ভাগ করার একটি heuristic দেন (Kernighan–Lin), আর ১৯৭৩ সালে Lin ও Kernighan-এর TSP heuristic (Lin–Kernighan) আজও সেরা TSP heuristic-গুলোর ভিত্তি। ১৯৮৩ সালে Kirkpatrick, Gelatt ও Vecchi Science পত্রিকায় simulated annealing-কে অপ্টিমাইজেশনের সাধারণ পদ্ধতি হিসেবে পরিচয় করান।
১০ · AI-এর যুগে NP-hard সমস্যা — দাবি যাচাই করুন
“আমার ডেলিভারি রুটের সেরা সমাধান দাও” — AI আত্মবিশ্বাসী সুরে একটি greedy কোড দেবে আর বলবে “এটি সর্বোত্তম রুট দেয়, O(n²) সময়ে।” এই এক বাক্যে দুটি দাবি, আর দুটিই একসাথে সত্য হলে P = NP প্রমাণ হয়ে যায়। NP-hard সমস্যায় AI-এর উত্তর তাই বিশেষভাবে যাচাই করতে হয়।
এই বিষয়ে AI সাধারণত যেসব ভুল করে
| AI-এর ভুল | কীভাবে ধরবেন |
|---|---|
| Greedy বা heuristic-কে “optimal” বলা — যেমন degree ধরে coloring বা nearest neighbour TSP | ছোট ইনপুটে brute force-এর সাথে মেলান; একটি প্রতি-উদাহরণই যথেষ্ট |
| NP-hard সমস্যার জন্য polynomial জটিলতা দাবি করা | জিজ্ঞেস করুন “কেন এটি P = NP প্রমাণ করে না?” — সাধারণত দেখা যায় উত্তর নির্ভুল নয়, বা সময় আসলে exponential |
| Reduction-এর দিক উল্টো — “আমার সমস্যাকে SAT-এ রূপান্তর করেছি, তাই এটি NP-hard” | কঠিনতা প্রমাণে জানা NP-hard সমস্যা থেকে আপনার সমস্যায় যেতে হয় |
| Simulated annealing-এ সম্ভাবনা e+Δ/T লেখা, বা উন্নতিতেও সম্ভাবনা প্রয়োগ করা | Δ > 0-এর সম্ভাবনা ১-এর কম কি না, আর T কমলে কমছে কি না ছাপিয়ে দেখুন |
| 2-opt-এ ভুল অংশ উল্টানো (i থেকে j, i+1 থেকে j+1-এর বদলে) — off-by-one | প্রতিটি move-এর পর পুরো দৈর্ঘ্য নতুন করে হিসাব করে প্রত্যাশিত লাভের সাথে মেলান |
| বিশেষ গঠন না দেখেই NP-hard ঘোষণা — যেমন bipartite গ্রাফে vertex cover বা tree-তে independent set | প্রশ্ন করুন: “আমার ইনপুট কি tree, bipartite, interval, বা উত্তর ছোট?” |
AI-কে সমাধান লিখে দিতে না বলে, সঠিক হাতিয়ার বাছতে আর আপনার দাবি পরীক্ষা করতে বলুন:
NP-hard সমস্যার সমাধান যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১Garey আর Johnson-এর বিখ্যাত কার্টুনটি আসলে কী শেখায় — আর NP-hard প্রমাণ একজন প্রকৌশলীর কাজে কীভাবে লাগে?
১৯৭৯ সালের বই Computers and Intractability-এর শুরুতে তিনটি কার্টুনের একটি গল্প আছে। বস একজন কর্মচারীকে একটি কঠিন নকশা-সমস্যার দ্রুত অ্যালগরিদম বের করতে বলেছেন। প্রথম ছবিতে কর্মচারী বলছেন, “পারছি না, মনে হয় আমি যথেষ্ট বুদ্ধিমান নই” — চাকরি যাওয়ার ঝুঁকি। দ্বিতীয় ছবি আদর্শ: “পারছি না, কারণ এমন অ্যালগরিদম অসম্ভব” — কিন্তু এমন প্রমাণ দেওয়া মানে P ≠ NP প্রমাণ করা, যা কেউ পারেননি। তৃতীয় ছবি বাস্তবসম্মত: “পারছি না, কিন্তু এই বিখ্যাত মানুষেরাও পারেননি” — পেছনে সারি বেঁধে দাঁড়ানো বিখ্যাত গবেষকেরা।
তৃতীয় ছবিটিই NP-completeness তত্ত্বের ব্যবহারিক মূল্য। আপনার সমস্যাকে একটি জানা NP-complete সমস্যার সমান কঠিন দেখাতে পারলে আপনি বলতে পারেন: এর দ্রুত নির্ভুল অ্যালগরিদম বের করা মানে হাজারো সমস্যার একসাথে সমাধান, যা কয়েক দশকের চেষ্টাতেও কেউ পারেননি। এটি অযোগ্যতার স্বীকারোক্তি নয়, বরং একটি পেশাদার রায়।
এর পরের ধাপ আরও গুরুত্বপূর্ণ: প্রমাণটি প্রশ্ন বদলে দেয়। “সেরা উত্তর কীভাবে পাব?”-র বদলে আসে “কোন ছাড় দেব?” — ইনপুট কি ছোট, গঠন কি বিশেষ, ২% ঘাটতি কি চলবে, সময় কত? Garey ও Johnson-এর বইয়ের পরিশিষ্টে শত শত সমস্যার তালিকা আর কোন বিশেষ রূপে সেগুলো সহজ, তার নোটও আছে — বাস্তবের প্রকৌশলী প্রায়ই সেখানে নিজের সমস্যার কাছাকাছি রূপ খুঁজে পান। কাজেই NP-hard প্রমাণ হলো বিশ্লেষণের শুরু, শেষ নয়।
মূল উপলব্ধি: NP-hard প্রমাণ বৃথা খোঁজ থামায় আর সঠিক আলোচনা শুরু করায় — কোন দাবি ছাড়লে কাজ চলে, সেটাই আসল নকশার সিদ্ধান্ত।
প্র ০২SAT প্রথম প্রমাণিত NP-complete সমস্যা, তবু SAT solver কীভাবে লক্ষ লক্ষ চলকের শিল্প-সমস্যা মেটায়?
১৯৭১ সালে Stephen Cook (আর স্বাধীনভাবে Leonid Levin) দেখান যে SAT NP-complete — পর্ব ৬৪-এর গল্প। অথচ আজ চিপ নকশা যাচাই, সফটওয়্যারের বাগ খোঁজা আর প্যাকেজ-নির্ভরতা মেলাতে নিয়মিত লক্ষ লক্ষ চলক আর clause-এর SAT সমস্যা কয়েক সেকেন্ডে মেটে। বিরোধ নেই — NP-completeness worst case-এর কথা; শিল্পের সূত্রগুলো worst case নয়। এদের মধ্যে গভীর গঠন থাকে: চলকগুলো ছোট ছোট দলে জড়িত, অনেক শর্ত প্রায় তাৎক্ষণিকভাবে অন্য চলকের মান ঠিক করে দেয়।
প্রকৌশলের অগ্রগতি এসেছে ধাপে ধাপে। ১৯৬০ সালে Davis ও Putnam, তারপর ১৯৬২ সালে Davis, Logemann ও Loveland-এর DPLL — backtracking আর unit propagation (একটি clause-এ একটিমাত্র খোলা literal থাকলে তার মান বাধ্য)। ১৯৯০-এর দশকে GRASP solver দেখায় বিরোধ বিশ্লেষণ করে নতুন clause শেখা (CDCL) আর বিরোধের আসল কারণে অনেক ধাপ পেছনে লাফানো। ২০০১ সালের Chaff আনে দ্রুত ডেটা স্ট্রাকচার (watched literals) আর সক্রিয় চলক আগে বাছার নিয়ম। সাথে যুক্ত হয় নিয়মিত restart. ফলাফল: কয়েক দশকে বাস্তবের সমস্যায় গতি বহু গুণ বেড়েছে — হার্ডওয়্যারের উন্নতির চেয়ে অনেক বেশি।
কিন্তু এলোমেলো সূত্রে ছবিটি আলাদা। এলোমেলো 3-SAT-এ clause আর চলকের অনুপাত প্রায় ৪.২৬-এর কাছে গেলে সূত্রগুলো হঠাৎ “প্রায় সবসময় সমাধানযোগ্য” থেকে “প্রায় কখনো নয়”-তে বদলায়, আর ঠিক সেখানে solver সবচেয়ে বেশি কষ্ট পায় — এই phase transition নিয়ে ১৯৯০-এর দশকে প্রচুর গবেষণা হয়েছে। অর্থাৎ কঠিনতা আছে, শুধু শিল্পের ইনপুটে সাধারণত দেখা দেয় না।
মূল উপলব্ধি: “NP-complete” একটি worst-case শ্রেণি, ভাগ্য নয় — গঠনওয়ালা বাস্তব ইনপুটে চালাক backtracking আর শেখার কৌশল বিস্ময়কর কাজ করে, তাই সমস্যাকে SAT-এ রূপান্তর প্রায়ই সবচেয়ে বাস্তবসম্মত সমাধান।
প্র ০৩Simulated annealing পদার্থবিজ্ঞান থেকে কীভাবে এল, আর এর কি আদৌ কোনো তাত্ত্বিক নিশ্চয়তা আছে?
শিকড় ১৯৫৩ সালের। Nicholas Metropolis ও সহকর্মীরা পদার্থের পরমাণুর আচরণ কম্পিউটারে অনুকরণ করতে একটি নিয়ম দেন: এলোমেলো একটি পরিবর্তন প্রস্তাব করুন; শক্তি কমলে নিন, বাড়লে নিন e−ΔE/kT সম্ভাবনায়। দীর্ঘ সময় চালালে এতে নির্দিষ্ট তাপমাত্রায় পদার্থের সঠিক পরিসংখ্যানিক অবস্থা পাওয়া যায়। ধাতুবিদ্যায় annealing মানে ধাতু গরম করে ধীরে ঠান্ডা করা, যাতে স্ফটিক সুশৃঙ্খল হয়; দ্রুত ঠান্ডা করলে (quenching) এলোমেলো, ভঙ্গুর কাঠামো আটকে যায়।
১৯৮৩ সালে IBM-এর Scott Kirkpatrick, C. Daniel Gelatt ও Mario Vecchi Science পত্রিকায় বলেন: অপ্টিমাইজেশনের খরচকে “শক্তি” ভাবুন, আর Metropolis-এর নিয়মে তাপমাত্রা ধীরে কমান। তাঁরা চিপের নকশা আর TSP-এ এটি দেখান। প্রায় একই সময়ে স্লোভাকিয়ার Vlado Černý স্বাধীনভাবে TSP-এ একই ধারণা প্রকাশ করেন (১৯৮৫). ধারণাটি এত সরল আর সাধারণ যে খুব দ্রুত ছড়িয়ে পড়ে।
তাত্ত্বিক নিশ্চয়তা আছে, কিন্তু তা প্রায় ব্যবহারের অযোগ্য। ১৯৮০-এর দশকে (যেমন Geman ও Geman, Hajek-এর কাজে) প্রমাণ হয় যে তাপমাত্রা যদি লগারিদমিক হারে কমে — Tk ≈ c / log k — তাহলে সম্ভাবনায় সর্বোত্তমে পৌঁছানোর নিশ্চয়তা আছে। কিন্তু এত ধীর ঠান্ডা করতে যত ধাপ লাগে, তা সব সমাধান গুনে দেখার চেয়েও বেশি হতে পারে। তাই বাস্তবে geometric schedule (T ← αT) চলে — দ্রুত, ভালো, কিন্তু নিশ্চয়তাহীন।
আরও একটি সূক্ষ্ম বিষয়: ফল নির্ভর করে move-এর সংজ্ঞার ওপর। TSP-এ এলোমেলো 2-opt move ভালো কাজ করে, কারণ এটি tour-এর ছোট পরিবর্তন; শহর দুটির জায়গা বদল (swap) সাধারণত খারাপ কাজ করে, কারণ এতে চারটি edge এলোমেলোভাবে বদলায়।
মূল উপলব্ধি: Simulated annealing বলে “মাঝে মাঝে খারাপ পথে হাঁটা দীর্ঘমেয়াদে ভালো” — কিন্তু তত্ত্বের নিশ্চয়তা যে গতিতে আসে, বাস্তব তা দিতে পারে না; তাই এটি একটি শক্তিশালী heuristic, প্রমাণিত অ্যালগরিদম নয়।
প্র ০৪১৯৭৩ সালের Lin–Kernighan heuristic পঞ্চাশ বছর পরও কেন TSP-এর সেরা হাতিয়ারগুলোর ভিত্তি?
গল্পটি শুরু Bell Labs-এ। ১৯৭০ সালে Brian Kernighan আর Shen Lin গ্রাফকে দুই সমান ভাগে ভাগ করার একটি heuristic দেন — সার্কিটের অংশগুলো দুটি বোর্ডে এমনভাবে বসানো, যাতে বোর্ডের মাঝের তার সবচেয়ে কম হয়। এর মূল বুদ্ধি ছিল নতুন: একটি একটি করে move না দেখে, একটির পর একটি অদলবদলের একটি শৃঙ্খল তৈরি করা, এমনকি মাঝপথে কিছু অদলবদল খারাপ হলেও; তারপর শৃঙ্খলের যে বিন্দুতে মোট লাভ সর্বোচ্চ, সেখান পর্যন্ত রাখা।
১৯৭৩ সালে Lin ও Kernighan একই ধারণা TSP-এ আনেন। 2-opt সবসময় ঠিক দুটি edge বদলায়, 3-opt তিনটি; কিন্তু কত edge বদলালে সেরা, তা আগে থেকে জানা যায় না। Lin–Kernighan প্রতিটি ধাপে k বাড়াতে থাকে — একটি edge সরান, নতুন জুড়ুন, আবার সরান — যতক্ষণ সম্ভাব্য লাভ ধনাত্মক থাকে। এই “variable-depth” খোঁজ k-opt-এর বিশাল প্রতিবেশ সস্তায় অন্বেষণ করে।
পরের দশকগুলোতে এটি আরও শাণিত হয়েছে। Keld Helsgaun-এর LKH বাস্তবায়ন বহু বড় TSP benchmark-এ এখন পর্যন্ত জানা সেরা tour খুঁজে পেয়েছে, আর Concorde-এর মতো exact solver-ও শুরুর incumbent বানাতে Lin–Kernighan-ধাঁচের heuristic ব্যবহার করে — পর্ব ৫৭-এ দেখেছেন, branch and bound-এ ভালো শুরুর উত্তর কত জরুরি। “Iterated” রূপে local optimum-এ পৌঁছে tour-কে একটু এলোমেলো করে (যেমন double-bridge move) আবার উন্নতি করা হয়।
কেন এত সফল? সমতলের TSP-এ ভালো tour-গুলোর গঠন “স্থানীয়”: কাছের শহর কাছেই থাকে, আর সর্বোত্তম tour-এর বেশিরভাগ edge প্রতিটি শহরের কাছের কয়েকটি প্রতিবেশীর মধ্যেই। তাই ছোট, বুদ্ধিমান পরিবর্তনের ধারা দিয়ে প্রায় পুরো দূরত্ব পেরোনো যায়।
মূল উপলব্ধি: ভালো local search-এর শক্তি আসে প্রতিবেশের নকশা থেকে — Lin–Kernighan দেখিয়েছে, move কত বড় হবে তা খোঁজকেই ঠিক করতে দিলে সরল 2-opt আর অসম্ভব n!-এর মাঝে একটি কার্যকর পথ পাওয়া যায়।
প্র ০৫Vertex cover FPT, অথচ তার পরিপূরক independent set নয় — একই সমস্যা হলে প্যারামিটারে এত পার্থক্য কেন?
গ্রাফ G-তে S একটি vertex cover হলে তার বাইরের সব vertex একটি independent set — আর উল্টোটাও সত্য। তাই “n − k আকারের independent set আছে কি?” আর “k আকারের vertex cover আছে কি?” একই প্রশ্ন। Polynomial কি না, এই প্রশ্নে দুটি একসাথে ওঠে বা পড়ে। কিন্তু প্যারামিটার ধরে দেখলে ছবি পুরো বদলে যায়।
Vertex cover-এ k ছোট মানে উত্তর ছোট, আর প্রতিটি edge আমাদের হাতে দুটি বিকল্প দেয় — খোঁজের গাছ 2k. Independent set-এ k ছোট (ধরুন ১০ জন পরস্পর-অপরিচিত মানুষ খুঁজছেন) মানে vertex cover-টি বিশাল, n − ১০; তার জন্য 2n−10-এর গাছ কোনো কাজের নয়। আবার independent set-এর জন্য এমন কোনো “বাধ্যতামূলক বিকল্প” নেই — একটি vertex নেওয়ার বা না নেওয়ার সিদ্ধান্ত খোঁজকে k-এর মধ্যে বেঁধে রাখে না। সরাসরি পদ্ধতি সব k-উপসেট পরীক্ষা, প্রায় nk — যেখানে exponent-এ k, যা FPT নয়।
১৯৯০-এর দশকে Rodney Downey আর Michael Fellows এই পার্থক্যকে একটি পূর্ণ তত্ত্বে রূপ দেন — parameterized complexity (তাঁদের বই ১৯৯৯ সালের)। তাঁরা W-hierarchy নামে শ্রেণি বানান; k-clique আর k-independent set হলো W[1]-complete, আর ধারণা করা হয় W[1]-hard সমস্যা FPT নয় — অনেকটা P ≠ NP-এর মতো একটি অপ্রমাণিত বিশ্বাস। পরে Exponential Time Hypothesis দিয়ে আরও সূক্ষ্ম নিম্নসীমা দেখানো হয়: ETH সত্য হলে k-clique-এর জন্য no(k) সময়ের অ্যালগরিদম নেই।
বাস্তবে শিক্ষাটি সরাসরি: সমস্যার “কোন সংখ্যাটি ছোট”, তা খুঁজুন। উত্তরের আকার, treewidth, বিভিন্ন ধরনের কাজের সংখ্যা, বা কতগুলো শর্ত ভাঙা চলবে — একই সমস্যা এক প্যারামিটারে সহজ, আরেকটিতে আশাহীন।
মূল উপলব্ধি: জটিলতা শুধু সমস্যার ধর্ম নয়, কোন সংখ্যা দিয়ে মাপছেন তারও ধর্ম — সঠিক প্যারামিটার বাছা NP-hard সমস্যা মোকাবিলার সবচেয়ে শক্তিশালী কৌশলগুলোর একটি।
অনুশীলন
প্রথম দুটি হাতে trace করার, তৃতীয়টি কোড লেখার, চতুর্থটি সঠিক হাতিয়ার বাছাইয়ের।
-
হাতে trace — খোঁজের গাছ: চারটি মোড়ের গ্রাফ, রাস্তা ক্রমে: 1–2, 2–3, 3–4, 4–1, 1–3. প্রতিবার বাকি রাস্তার প্রথমটি বেছে bounded search tree চালান, প্রথমে k = ১, তারপর k = ২. প্রতিটি k-এ কতটি node, আর উত্তর কী?
k = ১: রাস্তা ১–২. “১ নিন” → বাকি ২–৩, ৩–৪, বাজেট ০ — ব্যর্থ। “২ নিন” → বাকি ৩–৪, ৪–১, ১–৩ — ব্যর্থ। মোট ৩টি node, উত্তর না (= ২২ − ১, পুরো গাছ)।
k = ২: “১ নিন” → বাকি ২–৩, ৩–৪, বাজেট ১; রাস্তা ২–৩: “২ নিন” → বাকি ৩–৪, বাজেট ০ — ব্যর্থ; “৩ নিন” → কিছু বাকি নেই — হ্যাঁ, cover {১, ৩}. মোট ৪টি node: root, [১], [১,২], [১,৩]. খেয়াল করুন, ৪-চক্রের কর্ণ ১–৩ থাকায় {১, ৩} সব পাঁচটি রাস্তা ঢাকে।
-
হাতে trace — 2-opt: চারটি বিন্দু A(0, 0), B(4, 0), C(4, 3), D(0, 3). Tour A → C → B → D → A-এর দৈর্ঘ্য কত? কোন দুটি edge কাটে? 2-opt move-টি করে নতুন tour আর লাভ বের করুন। নতুন tour-এ আর কোনো 2-opt উন্নতি আছে কি?
দৈর্ঘ্য: AC = ৫, CB = ৩, BD = ৫, DA = ৩ — মোট ১৬. দুই কর্ণ A–C আর B–D পরস্পর কাটে।
Move: a = A, b = C, c = B, e = D. A–C আর B–D সরিয়ে A–B (৪) আর C–D (৪) জুড়ুন; মাঝের অংশ C, B উল্টে B, C. নতুন tour A → B → C → D → A = ৪ + ৩ + ৪ + ৩ = ১৪, লাভ ১০ − ৮ = ২.
আর উন্নতি? বাকি দুটি সম্ভাব্য 2-opt move: A–B আর C–D বদলালে আবার দুই কর্ণ (৫ + ৫ = ১০ > ৮); B–C আর D–A বদলালে B–D আর C–A, আবার কর্ণ (১০ > ৬). কোনো উন্নতি নেই — local optimum, আর এখানে আয়তক্ষেত্রের পরিসীমাই সর্বোত্তম, তাই গ্লোবালও।
-
কোড লিখুন — tree-তে minimum vertex cover: একটি tree-র adjacency list
adjদেওয়া আছে — vertex 0…n−1, root 0. একটি DFS ফাংশন লিখুন যা প্রতিটি vertex-এর in আর out হিসাব করে, আর উত্তর min(in[0], out[0]) ফেরায়। অংশ ৪-এর ছয় vertex-এর tree-তে (0-ভিত্তিক করে) উত্তর ২ আসে কি না দেখুন। সময় আর জায়গা কত?একটি সম্ভাব্য সমাধান:
vector<vector<int>> adj; vector<int> in_, out_; void dfs(int v, int parent) { in_[v] = 1; out_[v] = 0; // a leaf: take it (1) or not (0) for (int c : adj[v]) { if (c == parent) continue; dfs(c, v); in_[v] += min(in_[c], out_[c]); // v taken: child is free out_[v] += in_[c]; // v not taken: edge v-c needs c } } // usage: in_.assign(n, 0); out_.assign(n, 0); dfs(0, -1); answer = min(in_[0], out_[0]);অংশ ৪-এর tree (১ → ০, ২ → ১, ৩ → ২, ৪ → ৩, ৫ → ৪, ৬ → ৫ নামে) দিলে in[০] = ৩, out[০] = ২, উত্তর ২. প্রতিটি vertex একবার ডাকা হয় আর প্রতিটি edge দুবার দেখা হয় — সময় O(n). জায়গা O(n): দুটি অ্যারে আর রিকার্শনের গভীরতা (লম্বা শিকলের মতো tree-তে গভীরতা n পর্যন্ত — তখন stack overflow এড়াতে iterative DFS ভালো)।
inশব্দটি কিছু ভাষায় সংরক্ষিত, তাই নামin_. -
সঠিক হাতিয়ার বাছুন: প্রতিটি পরিস্থিতিতে কোন পদ্ধতি নেবেন, আর কেন? (ক) প্রতি রাতে একটি ভ্যান ১৫টি দোকানে যায়, নিখুঁত রুট চাই। (খ) একটি কুরিয়ার কোম্পানির প্রতিদিন ৩,০০০ ঠিকানা আর ২০টি ভ্যান। (গ) ৪০ জন পরিদর্শককে ৪০টি হলে বসাতে হবে, প্রতিটি জোড়ার একটি “অপছন্দ” খরচ আছে, মোট খরচ কম চাই। (ঘ) ৫,০০০ রাস্তার একটি এলাকায় বড়জোর ৮টি মোড়ে ক্যামেরা বসিয়ে সব রাস্তা ঢাকা যায় কি?
(ক) Held–Karp bitmask DP: 215 · 15² ≈ ৭৪ লক্ষ ধাপ — নির্ভুল আর তাৎক্ষণিক। n ছোট, তাই exponential হলেও সমস্যা নেই।
(খ) এটি VRP, n বিশাল — exact অসম্ভব। Greedy construction (যেমন কাছাকাছি ঠিকানা দলে ভাগ করে nearest neighbour), তারপর 2-opt/Or-opt বা simulated annealing; অথবা একটি VRP solver (যেমন OR-Tools-এর routing). সাথে একটি lower bound দিয়ে gap মাপুন।
(গ) ছদ্মবেশী সহজ সমস্যা! এটি assignment problem — bipartite গ্রাফে সবচেয়ে কম খরচের perfect matching, যা Hungarian পদ্ধতি বা min-cost flow দিয়ে polynomial সময়ে (O(n³)) নির্ভুল সমাধান হয়। এখানে heuristic লাগবেই না।
(ঘ) Vertex cover, k = ৮: bounded search tree-এ বড়জোর ২৮ = ২৫৬টি leaf. আগে Buss kernel: ৮-এর বেশি রাস্তার মোড় সরাসরি নিন; তারপর ৬৪-এর বেশি রাস্তা বাকি থাকলে উত্তর “না”. মুহূর্তেই নির্ভুল উত্তর।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমকাজের জায়গায় একটি কঠিন অপ্টিমাইজেশন সমস্যা পেলে ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- সমস্যাটি নিখুঁতভাবে লেখা — ইনপুট, শর্ত আর কী কম বা বেশি করতে হবে
- একটি জানা NP-hard সমস্যার সাথে মেলানো, আর reduction দিয়ে কঠিনতা নিশ্চিত করা
- ইনপুটের আকার, বিশেষ গঠন (tree, bipartite) আর ছোট প্যারামিটার খোঁজা
- পদ্ধতি বাছাই — ছোট বা গঠনওয়ালা হলে exact, নইলে approximation, heuristic বা solver
- Lower bound দিয়ে gap মেপে আসল ডেটায় benchmark করে ফল জানানো
“k আকারের vertex cover আছে কি?” প্রশ্নের bounded search tree-তে প্রতিবার একটি edge (u, v) নিয়ে u বা v-তে শাখা হয়। k = ১০ হলে গাছে সর্বোচ্চ কতটি leaf?
- ১০
- ১০২৪
- ১০! = ৩৬,২৮,৮০০
- 2n, n = vertex সংখ্যা
নিচের কোন সমস্যাটির polynomial সময়ের অ্যালগরিদম জানা আছে?
- 3-SAT
- সাধারণ গ্রাফে Hamiltonian cycle
- 2-SAT
- সাধারণ গ্রাফে 3-coloring
Simulated annealing-এ বর্তমান তাপমাত্রা T = 3. একটি move খরচ Δ = +6 বাড়ায়। এই খারাপ move নেওয়ার সম্ভাবনা কত?
- ≈ ০.১৩৫
- ≈ ৭.৩৯
- ০.৫
- ০ — খারাপ move কখনো নেওয়া হয় না
একটি নতুন সমস্যা X NP-hard, এটি প্রমাণ করতে কী দেখাতে হয়?
- X-কে polynomial সময়ে SAT-এ রূপান্তর করা যায়
- X-এর প্রস্তাবিত উত্তর polynomial সময়ে যাচাই করা যায়
- একটি জানা NP-hard সমস্যা থেকে X-এ polynomial reduction
- X-এর জানা অ্যালগরিদম বড় ইনপুটে খুব ধীর
TSP-এর একটি tour 2-opt-এর local optimum. এ সম্পর্কে কোনটি সত্য?
- এটিই নিশ্চিতভাবে সবচেয়ে ছোট tour
- এটি সবসময় সর্বোত্তমের বড়জোর ১.৫ গুণ
- এতে অন্তত একটি crossing edge-জোড়া থাকবেই
- কোনো 2-opt move-ই দৈর্ঘ্য কমায় না, তবু এটি সর্বোত্তমের চেয়ে লম্বা হতে পারে
n টি vertex-এর একটি tree-তে minimum vertex cover বের করার সবচেয়ে ভালো জানা সময় কত?
- O(n), tree DP দিয়ে
- Tree-তেও NP-hard, তাই exponential
- O(2n)
- O(n³), শুধু matching দিয়ে
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- NP-hard মানে সব ইনপুটে + দ্রুত + নির্ভুল একসাথে জানা নেই — একটি দাবি ছাড়লেই পথ খোলে। প্রমাণ: জানা NP-hard সমস্যা থেকে আপনার সমস্যায় reduction.
- রুটিন ≈ graph coloring, রুট ≈ TSP/VRP, লোডিং ≈ bin packing; কিন্তু shortest path, 2-SAT, bipartite matching, interval coloring P-তে।
- ছোট n-এ exact (bitmask DP, meet in the middle, B&B); tree-তে tree DP O(n); ছোট উত্তরে FPT — vertex cover O(2k · k · n), কারণ প্রতিটি edge-এর এক প্রান্ত লাগবেই।
- Approximation দেয় প্রমাণিত সীমা; heuristic (nearest neighbour + 2-opt, simulated annealing e−Δ/T, tabu, genetic) দেয় দ্রুত ভালো উত্তর, নিশ্চয়তা নয় — lower bound দিয়ে gap মাপুন।
- SAT (CDCL), ILP/MIP আর CP solver-এর জন্য সমস্যা লিখুন; benchmark করুন, আর সিদ্ধান্তদাতাকে সময় + gap দিয়ে বোঝান।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৬৬ · Approximation অ্যালগরিদম — vertex cover, set cover আর metric TSPপরবর্তী পর্বপ্রমাণিত নিশ্চয়তাসহ প্রায়-সর্বোত্তম উত্তর।
- ডেটা স্ট্রাকচার ও অ্যালগরিদম · কোর্স হোম৭২টি পর্বপুরো কোর্সের রোডম্যাপ — নয়টি মডিউল।
- পর্ব ৬৪ · P বনাম NP — NP-complete আর reductionআগের ভিত্তিNP-hard-এর সংজ্ঞা আর reduction-এর কৌশল।
- পর্ব ৫৭ · Branch and bound আর traveling salesmanআগের ভিত্তিExact খোঁজ, lower bound আর solver-এর ভেতরের গাছ।
- পর্ব ৫৫ · উন্নত DP — bitmask, digit আর tree DPআগের ভিত্তিHeld–Karp আর tree DP — ছোট ইনপুট আর বিশেষ গঠনের হাতিয়ার।
- C++ প্রোগ্রামিং · STL algorithmপাশের ট্র্যাক
reverse,next_permutationআরall_of— এই পর্বের কোডের হাতিয়ার। - Python প্রোগ্রামিং · Python-এ অ্যালগরিদমপাশের ট্র্যাক
itertoolsদিয়ে brute force যাচাই।