সরাসরি পর্বে যান
পর্ব ৫০ · ৫৫-এর মধ্যে · মডিউল ৭

কনটেস্ট প্রোগ্রামিংয়ে C++ — STL দিয়ে দ্রুত আর নির্ভুল সমাধান

C++ for Competitive Programming — Fast, Correct Solutions with the STL

Taught in Bangla, this lesson applies C++ to contest programming: reading constraints to pick a complexity, a contest template explained line by line including fast I/O and bits/stdc++.h, the STL toolkit, patterns such as prefix sums, two pointers, binary search on the answer, greedy, BFS and DP, and stress testing.

এই পর্বে যা শিখবেন

  • কনটেস্টের জগৎ — Codeforces, AtCoder, CodeChef, LeetCode, ICPC আর বাংলাদেশের জাতীয় কনটেস্ট
  • constraints দেখে প্রয়োজনীয় complexity ঠিক করা, আর একটি কনটেস্ট টেমপ্লেটের প্রতিটি লাইনের কারণ
  • কনটেস্টে সবচেয়ে কাজের STL টুল, আর প্রতিটির একটি করে ফাঁদ
  • সাতটি চেনা ছাঁচ: prefix sum, two pointers, binary search on answer, greedy, BFS, DP আর sieve
  • stress testing, WA-র সাধারণ কারণ, অনুশীলনের কৌশল, কনটেস্টের নীতি আর AI-এর সঠিক ব্যবহার

১ · কনটেস্ট প্রোগ্রামিং কী — আর C++ কেন সবার পছন্দ

একটি কনটেস্ট প্রোগ্রামিং সমস্যা হলো খুব নিখুঁতভাবে বলা একটি ধাঁধা। ইনপুট কেমন হবে, আউটপুট ঠিক কীভাবে ছাপতে হবে, সংখ্যাগুলো সর্বোচ্চ কত বড় হতে পারে, সময় কত সেকেন্ড, মেমরি কত — সব লেখা থাকে। আপনি একটি প্রোগ্রাম জমা দেন। একটি online judge সেটিকে গোপন অনেকগুলো টেস্টে চালায় আর ছোট একটি রায় দেয়, যাকে বলে verdict: AC (সব ঠিক), WA (কোথাও ভুল উত্তর), TLE (সময় পেরিয়েছে), RE (প্রোগ্রাম ভেঙে পড়েছে)।

তাই এখানে “কাজ করে” মানে তিনটি জিনিস একসাথে: সঠিক (প্রতিটি গোপন টেস্টে), দ্রুত (সময়সীমার ভেতরে), আর সাশ্রয়ী (মেমরির সীমার ভেতরে)। আর সবকিছু করতে হয় ঘড়ির বিরুদ্ধে — কনটেস্ট সাধারণত দুই থেকে পাঁচ ঘণ্টার।

ক্রিকেটের টি-টোয়েন্টি ম্যাচের কথা ভাবুন। টেস্ট ক্রিকেটের ব্যাটসম্যান সারাদিন ধরে ধৈর্যের ইনিংস খেলেন; টি-টোয়েন্টিতে ২০ ওভারে যত দ্রুত সম্ভব রান তুলতে হয়, আর একটি ভুল শটেই আউট। দুটোই ক্রিকেট, দুটোর মূল কৌশল এক, কিন্তু খেলার ধরন আলাদা। কনটেস্টের কোড হলো টি-টোয়েন্টি: ছোট, দ্রুত লেখা, একবার জমা দিয়ে শেষ। প্রোডাকশনের কোড হলো টেস্ট ম্যাচ: বছরের পর বছর অন্য মানুষ পড়বে, বদলাবে, রক্ষণাবেক্ষণ করবে। ভালো খেলোয়াড় দুই ধরনই জানেন, আর জানেন কোনটি কখন।

কনটেস্টের জগৎ

মঞ্চধরনকার জন্য ভালো
Codeforcesপ্রায় প্রতি সপ্তাহে rated কনটেস্ট, বিভিন্ন Division; পরে editorial; অন্যের কোডে ভুল ধরার (hack) সুযোগনিয়মিত চর্চা আর rating দিয়ে উন্নতি মাপা
AtCoderজাপানের প্ল্যাটফর্ম; নিয়মিত Beginner Contest; পরিষ্কার, গাণিতিকভাবে সুন্দর সমস্যানতুনদের ভিত্তি মজবুত করা
CodeChefভারতের প্ল্যাটফর্ম; নিয়মিত ছোট কনটেস্ট, শেখার উপকরণদক্ষিণ এশিয়ার সময় অনুযায়ী অংশ নেওয়া
LeetCodeফাংশন লিখে জমা দেওয়ার ধরন; সাপ্তাহিক কনটেস্ট; চাকরির ইন্টারভিউ-ধাঁচের সমস্যাকোডিং ইন্টারভিউর প্রস্তুতি (পর্ব ৫৫)
ICPCবিশ্ববিদ্যালয়ের তিনজনের দল, একটি কম্পিউটার, পাঁচ ঘণ্টা; আঞ্চলিক পর্ব থেকে World Finalsবিশ্ববিদ্যালয়ের দলগত প্রতিযোগিতা
দেশের কনটেস্টজাতীয় পর্যায়ের NCPC, বিভিন্ন বিশ্ববিদ্যালয়ের IUPC; স্কুল-কলেজের জন্য ইনফরমেটিক্স অলিম্পিয়াডদেশের প্রতিযোগীদের সাথে নিজেকে মাপা

বাংলাদেশে কনটেস্ট প্রোগ্রামিংয়ের ঐতিহ্য বেশ শক্ত। ঢাকায় বহু বছর ধরে ICPC-র একটি এশীয় আঞ্চলিক পর্ব (Dhaka Regional) হয়ে আসছে, আর দেশের বিশ্ববিদ্যালয়ের দলগুলো প্রায় নিয়মিতভাবে World Finals-এ যায়। এই সংস্কৃতি গড়ে উঠেছে শিক্ষক, কোচ আর প্রাক্তন প্রতিযোগীদের দীর্ঘ স্বেচ্ছাশ্রমে — যাঁরা ক্যাম্প চালান, সমস্যা লেখেন, নতুনদের হাতে ধরে শেখান।

C++ কেন কনটেস্টের সবচেয়ে জনপ্রিয় ভাষা

দুটি কারণ। প্রথমত গতি: C++ সরাসরি machine code-এ compile হয়, garbage collector নেই, তাই একই algorithm সাধারণত Python-এর চেয়ে অনেক গুণ আর Java-র চেয়েও কিছুটা দ্রুত চলে। সময়সীমা যখন টানটান, এই পার্থক্যই AC আর TLE-এর পার্থক্য। দ্বিতীয়ত STL: পর্ব ৩০–৩৫-এ যা শিখেছেন — vector, sort, map, set, priority_queue, lower_bound — সব হাতের কাছে, পরীক্ষিত আর দ্রুত। C-তে এগুলো নিজে লিখতে হতো, আর প্রতিটি হাতে লেখা heap বা balanced tree একটি নতুন বাগের সুযোগ।

কনটেস্টের কোড

এক ফাইল, এক লেখক, একবার চলে। ছোট নাম, global array, ইনপুট সবসময় বৈধ ধরা হয়। লক্ষ্য: কম সময়ে সঠিক উত্তর।

দ্রুত লেখা

প্রোডাকশনের কোড

অনেক ফাইল, অনেক লেখক, বছরের পর বছর চলে। পরিষ্কার নাম, ইনপুট যাচাই, টেস্ট, error handling (পর্ব ৩৭, ৪৮)। লক্ষ্য: বহুদিন নিরাপদে বদলানো যায় এমন কোড।

দীর্ঘদিন টেকা

দুই জগতে একই

complexity-র বোধ, edge case নিয়ে ভাবা, STL-এ দক্ষতা, আর নিজের কোড সন্দেহ করে পরীক্ষা করার অভ্যাস — দুই জায়গাতেই সমান দামি।

রাখার মতো অভ্যাস

২ · সমস্যা পড়া — constraints থেকে প্রয়োজনীয় complexity

নতুনরা সমস্যার গল্প পড়ে সরাসরি কোড লিখতে বসেন। অভিজ্ঞরা প্রথমে চোখ দেন নিচের ছোট লাইনটিতে: constraints. “1 ≤ n ≤ 2·105, time limit 1 second” — এই লাইনটি আসলে বলে দেয়, কোন algorithm চলবে আর কোনটি চলবে না। কারণ সাধারণ একটি জাজ মেশিন এক সেকেন্ডে মোটামুটি ১০৮-এর মতো সরল অপারেশন (যোগ, তুলনা, array পড়া) করতে পারে। এটি নিখুঁত সংখ্যা নয় — cache, মেমরির ধরন আর অপারেশনের জটিলতায় কয়েক গুণ এদিক-ওদিক হয় — কিন্তু সিদ্ধান্ত নেওয়ার জন্য যথেষ্ট ভালো অনুমান। Big-O-তে n বসিয়ে দেখুন, ফল ১০৮-এর আশেপাশে বা নিচে কি না।

n-এর সর্বোচ্চ মানসাধারণত চলেচেনা উদাহরণ
n ≤ ১০–১১O(n!)সব বিন্যাস পরীক্ষা (next_permutation)
n ≤ ২০O(2n) বা O(2n·n)সব উপসেট, bitmask
n ≤ ৫০০O(n³)তিনটি nested loop, Floyd–Warshall
n ≤ ৫০০০O(n²)দুটি nested loop, সাধারণ 2D DP
n ≤ ১০৫–১০৬O(n log n) বা O(n)sort, map/set, binary search, prefix sum
n ≤ ১০৮ বা তার বেশিO(n), O(√n), O(log n) বা O(1)সূত্র, গণিত, binary search

এখন একটি বাস্তব সমস্যায় পুরো সিদ্ধান্তটি ধাপে ধাপে দেখুন। সমস্যা: “n টি সংখ্যা দেওয়া আছে। q টি প্রশ্ন আসবে, প্রতিটিতে l আর r — l থেকে r পর্যন্ত সংখ্যাগুলোর যোগফল কত?” এটি বিখ্যাত range sum queries সমস্যা।

সমস্যার নিচের লাইনconstraints
1 ≤ n, q ≤ 2·10^5
-10^9 ≤ a[i] ≤ 10^9
1 ≤ l ≤ r ≤ n
Time limit: 1 second
Memory limit: 256 MB
প্রথম ভাবনাপ্রতি প্রশ্নে লুপ
for each query (l, r):
    sum = 0
    for i = l .. r:  sum += a[i]

সবচেয়ে খারাপ: q × n = 2·10^5 × 2·10^5
                     = 4·10^10 অপারেশন
বাজেটের সাথে মেলানো১ সেকেন্ড ≈ 10^8
দরকার:  4·10^10
বাজেট:  1·10^8
অনুপাত: 400 গুণ বেশি  =>  নিশ্চিত TLE

n ≤ 2·10^5 মানে লক্ষ্য:
O(n log n) বা O(n + q)
ভালো ধারণাprefix sum
একবার আগে থেকে হিসাব:  pre[i] = a[1] + ... + a[i]
তারপর প্রতি প্রশ্নে:     pre[r] - pre[l-1]

খরচ: n (তৈরি) + q (প্রশ্ন) = 4·10^5
বাজেটের 250 ভাগের 1 ভাগ  =>  নিশ্চিন্ত
শেষ যাচাইমান কত বড়?
সর্বোচ্চ যোগফল: 2·10^5 × 10^9 = 2·10^14
int-এর সীমা:    প্রায় 2.1·10^9     =>  overflow!
long long:      প্রায় 9.2·10^18    =>  নিরাপদ

সিদ্ধান্ত: prefix sum, long long-এ
  1. আগে constraints. গল্প পরে, সীমা আগে। এখানে n আর q দুটিই দুই লাখ পর্যন্ত, আর সংখ্যাগুলো ঋণাত্মকও হতে পারে। সময় মাত্র ১ সেকেন্ড।
  2. সবচেয়ে সরল সমাধান। প্রতিটি প্রশ্নে l থেকে r পর্যন্ত লুপ। নমুনা ইনপুট ছোট, তাই এটি নমুনায় ঠিক উত্তর দেবে — আর এখানেই ফাঁদ। সবচেয়ে খারাপ ক্ষেত্রে প্রতিটি প্রশ্ন পুরো array জুড়ে: মোট ৪·১০১০ অপারেশন।
  3. বাজেটের সাথে তুলনা। ১ সেকেন্ডে মোটামুটি ১০৮, দরকার তার ৪০০ গুণ। কোনো optimization দিয়ে এই ফাঁক মেটে না — algorithm-ই বদলাতে হবে। টেবিল বলছে n ≤ ২·১০৫-এ লক্ষ্য O(n log n) বা তার চেয়ে ভালো।
  4. পুনরাবৃত্ত কাজ একবার করুন। প্রতিটি প্রশ্ন একই যোগগুলো বারবার করছে। শুরুতে একবার চলতি যোগফল (prefix sum) বানিয়ে রাখলে প্রতিটি প্রশ্নের উত্তর একটি বিয়োগ। মোট খরচ O(n + q) — বাজেটের খুব সামান্য অংশ।
  5. মানের আকারও constraints-এর অংশ। যোগফল ২·১০১৪ পর্যন্ত যেতে পারে, যা int-এ ধরে না। algorithm ঠিক, কিন্তু int-এ রাখলে বড় টেস্টে WA. তাই সিদ্ধান্ত সম্পূর্ণ হলো দুই অংশে: prefix sum, আর long long. এই সমাধানের কোড ৫ নম্বর অংশে।
মূল ধারণা

constraints হলো সমস্যা-লেখকের দেওয়া ইঙ্গিত। n ≤ ২০ দেখলে লেখক প্রায় বলেই দিচ্ছেন “সব উপসেট পরীক্ষা করো”; n ≤ ১০৫ দেখলে বলছেন “n² ভুলে যাও”। আর সংখ্যার সীমা বলে দেয় কোন টাইপ লাগবে। কোড লেখার আগেই এই দুটি সিদ্ধান্ত নিন।

৩ · একটি কনটেস্ট টেমপ্লেট — প্রতিটি লাইনের কারণ

অনেক প্রতিযোগী প্রতিটি সমস্যা একটি তৈরি টেমপ্লেট দিয়ে শুরু করেন। ইন্টারনেটে শত শত লাইনের macro-ভরা টেমপ্লেট পাবেন; সেগুলো কপি করবেন না। নিচের ছোট টেমপ্লেটটির প্রতিটি লাইন আপনি ব্যাখ্যা করতে পারবেন — সেটাই আসল কথা।

#include <bits/stdc++.h>      // শুধু কনটেস্টে: GCC-র "সব header একসাথে"
using namespace std;           // শুধু কনটেস্টে: std:: না লিখতে
using ll = long long;          // বড় যোগফল আর গুণফলের জন্য

void solve() {
    int n;
    cin >> n;
    vector<ll> a(n);           // প্রতিটি test case-এ নতুন — পুরোনো অবস্থা নেই
    for (auto& x : a) cin >> x;
    cout << accumulate(a.begin(), a.end(), 0LL) << '\n';
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int t = 1;
    cin >> t;                  // একাধিক test case থাকলে
    while (t--) solve();
}

fast I/O — দুই লাইন, আর কেন কাজ করে

দশ লাখ সংখ্যা পড়তে হলে শুধু ইনপুট পড়াতেই সময়সীমা শেষ হয়ে যেতে পারে। ডিফল্টভাবে C++-এর cin/cout দুটি কারণে ধীর। প্রথমত, এরা C-র scanf/printf-এর সাথে “মিলিয়ে” চলে, যাতে কেউ দুই ধরনের I/O মেশালেও লেখার ক্রম ঠিক থাকে — এর জন্য C++ stream নিজের বড় buffer ব্যবহার করতে পারে না। sync_with_stdio(false) এই মেলানো বন্ধ করে, ফলে stream নিজের buffer পায় আর অনেক দ্রুত হয়। দাম: এরপর একই প্রোগ্রামে printf আর cout মেশানো যাবে না, মেশালে আউটপুটের ক্রম এলোমেলো হতে পারে।

দ্বিতীয়ত, ডিফল্টভাবে cin “বাঁধা” থাকে cout-এর সাথে: প্রতিবার কিছু পড়ার আগে cout-এর buffer খালি (flush) করা হয়, যাতে মানুষ প্রশ্নটি দেখে তারপর উত্তর টাইপ করতে পারে। কনটেস্টে কোনো মানুষ পর্দায় বসে নেই, তাই cin.tie(nullptr) দিয়ে বাঁধন খুলে দেওয়া হয়। এই দুই লাইন মিলে বড় ইনপুটে I/O কয়েক গুণ দ্রুত হতে পারে (বিস্তারিত পর্ব ০৭-এ)।

endl নয়, '\n'. std::endl নতুন লাইনের সাথে প্রতিবার flush-ও করে। দুই লাখ লাইন ছাপাতে দুই লাখবার flush — fast I/O-র পুরো লাভ শেষ। আর একটি ব্যতিক্রম মনে রাখুন: interactive সমস্যায় (যেখানে জাজ আপনার উত্তর দেখে পরের ইনপুট দেয়) প্রতিটি প্রশ্নের পরে নিজে cout.flush() বা endl দিতেই হবে, নইলে দুই পক্ষ একে অপরের অপেক্ষায় আটকে থাকে।

long long — overflow-এর বিরুদ্ধে প্রথম প্রতিরক্ষা

int সাধারণত ৩২-bit, সর্বোচ্চ প্রায় ২.১·১০৯. কনটেস্টে সংখ্যা প্রায়ই ১০৯ পর্যন্ত হয়, তাই দুটি যোগ করলেই সীমার কিনারায়, আর দুটি গুণ করলে নিশ্চিত signed overflow — যা C++-এ undefined behavior (পর্ব ৪৩)। using ll = long long; একটি type alias; long long কমপক্ষে ৬৪-bit, সর্বোচ্চ প্রায় ৯.২·১০১৮. সবচেয়ে কুখ্যাত ফাঁদটি দেখুন:

int a = 1'000'000'000, b = 3;
ll wrong = a * b;              // গুণ হয় int-এ, overflow হয় আগে, তারপর ll-এ রাখা
ll right = 1LL * a * b;        // প্রথমেই ll, তাই গুণও ll-এ: 3000000000

ফলের ভ্যারিয়েবল ll হলেই চলে না — হিসাবটি কোন টাইপে হচ্ছে, সেটিই আসল। accumulate-এর শুরুর মানেও একই নিয়ম: 0 দিলে যোগফল int-এ, 0LL দিলে long long-এ (পর্ব ৩৩)।

একাধিক test case — প্রতিবার পরিষ্কার শুরু

আধুনিক কনটেস্টে প্রথম লাইনে প্রায়ই থাকে t — test case-এর সংখ্যা, কখনো ১০৪ পর্যন্ত। দুটি নিয়ম। এক: প্রতিটি test case-এর ডেটা solve()-এর ভেতরে local ভ্যারিয়েবলে রাখুন, তাহলে আগের test case-এর মান কখনো পরেরটিতে ঢোকে না। global array ব্যবহার করলে প্রতিবার নিজে পরিষ্কার করতে হবে — আর সেটা ভুলে যাওয়া কনটেস্টের সবচেয়ে সাধারণ WA-গুলোর একটি। দুই: পুরো array পরিষ্কার নয়, শুধু ব্যবহৃত n টি ঘর। সমস্যা প্রায়ই বলে “সব test case মিলিয়ে n-এর যোগফল ২·১০৫-এর বেশি নয়” — প্রতিবার ২·১০৫ ঘরের array পরিষ্কার করলে ১০৪ test case-এ ২·১০৯ অপারেশন, অর্থাৎ TLE.

bits/stdc++.h আর using namespace std; — সৎ কথা

bits/stdc++.h C++ standard-এর কোনো header নয়। এটি GCC-র library (libstdc++)-র ভেতরের একটি ফাইল, যা প্রায় সব standard header একসাথে include করে। কনটেস্টে এটি গ্রহণযোগ্য, কারণ জাজ প্রায় সবসময় GCC, আর কোন header লাগবে তা ভাবার সময় বাঁচে। কিন্তু MSVC-তে এটি নেই, অনেক সিস্টেমের Clang/libc++-তেও স্বাভাবিকভাবে নেই, আর প্রতিটি ফাইলে পুরো library টেনে আনায় compile-এর সময় বাড়ে। using namespace std;-ও কনটেস্টে চলে, কিন্তু বড় প্রজেক্টে নামের সংঘাত ঘটায় (পর্ব ১১)। কনটেস্টেও এর ছোট্ট ফাঁদ আছে: count, next বা distance নামে নিজের global ভ্যারিয়েবল বানালে std-এর একই নামের ফাংশনের সাথে মিলে “ambiguous” error আসতে পারে।

নিয়ম

কনটেস্টে চলে, প্রোডাকশনে নয়: bits/stdc++.h, using namespace std;, এক অক্ষরের নাম, global array, #define int long long-এর মতো macro. পর্ব ৫১-এর ক্যাপস্টোনে বা চাকরির কোডে এগুলো লিখলে code review-তে ফেরত আসবে — আর সেটা ঠিকই। এই কোর্সের বাকি সব জায়গায় আমরা std:: আর নির্দিষ্ট header-ই লিখি।

৪ · STL-এর অস্ত্রাগার — কনটেস্টের সবচেয়ে কাজের টুল

পর্ব ৩০–৩৫-এ STL বিস্তারিত শিখেছেন। এখানে শুধু কনটেস্টের চোখে দেখা: কোনটি কখন, আর কোথায় পা পিছলায়।

টুলকনটেস্টে কাজফাঁদ
vector + sortপ্রায় সব সমস্যার শুরু; O(n log n)comparator-এ <= নয় — UB, crash পর্যন্ত
pair, tupleনিজে থেকেই অভিধানের ক্রমে তুলনা হয়দ্বিতীয় উপাদান উল্টো ক্রমে চাইলে নিজে comparator লিখুন
map, setসাজানো, প্রতি কাজে O(log n) নিশ্চিতset-এ std::lower_bound নয়, s.lower_bound(x)
unordered_mapগড়ে O(1)ইচ্ছাকৃত collision-এর টেস্টে O(n) — anti-hash
priority_queueবারবার সবচেয়ে বড়/ছোটটি বের করা; Dijkstraডিফল্টে সবচেয়ে বড়টি ওপরে
lower_bound / upper_boundসাজানো ডেটায় O(log n) খোঁজা, গোনাডেটা সাজানো না হলে নীরবে ভুল
accumulateযোগফল এক লাইনেশুরুর মান 0LL, নইলে overflow
next_permutationসব বিন্যাস একে একে (n ≤ ১০)শুরুতে sort না করলে সব বিন্যাস আসে না
gcd, lcmC++17 থেকে <numeric>-এlcm বড় হয় দ্রুত — ll
bitsetহাজারো bool একসাথে, প্রায় ৬৪ গুণ দ্রুত অপারেশনআকার compile-এর সময়েই জানা থাকতে হবে
__builtin_popcountকয়টি bit 1, এক নির্দেশেGCC/Clang-এর নিজস্ব; long long-এ ...popcountll

sort, comparator আর pair-এর তুলনা

std::pair আর std::tuple নিজে থেকেই lexicographic ক্রমে তুলনা হয়: আগে প্রথম উপাদান, সমান হলে দ্বিতীয়টি। তাই “শেষ হওয়ার সময় অনুযায়ী সাজাও, সমান হলে শুরুর সময়” লিখতে আলাদা comparator লাগে না — pair-এ {end, start} রেখে সরাসরি sort করলেই হয়। ভিন্ন নিয়ম চাইলে comparator:

vector<pair<int, int>> v{{3, 9}, {1, 4}, {3, 2}};
sort(v.begin(), v.end());       // {1,4} {3,2} {3,9} — আগে first, তারপর second
sort(v.begin(), v.end(), [](auto& x, auto& y) {
    if (x.first != y.first) return x.first > y.first;   // first বড় থেকে ছোট
    return x.second < y.second;                          // সমান হলে second ছোট থেকে বড়
});

map, set আর unordered_map — anti-hash সতর্কতা

std::map আর std::set ভেতরে balanced tree, তাই প্রতিটি কাজ সবসময় O(log n) — কোনো ইনপুটেই খারাপ হয় না। std::unordered_map hash table, গড়ে O(1), কিন্তু সবচেয়ে খারাপ ক্ষেত্রে O(n). সাধারণ ডেটায় সেই খারাপ ক্ষেত্র প্রায় আসে না। সমস্যা হলো, কনটেস্ট সাধারণ ডেটা নয়।

GCC-তে পূর্ণসংখ্যার std::hash মূলত সংখ্যাটিকেই ফেরত দেয়, আর bucket-এর সংখ্যা কীভাবে বাড়ে তা-ও জানা। তাই কেউ চাইলে এমন হাজারো সংখ্যা বানাতে পারেন, যারা সবাই একই bucket-এ পড়ে — পর্ব ৩১-এর hash flooding. Codeforces-এর মতো যেসব প্ল্যাটফর্মে অন্য প্রতিযোগী আপনার কোডে hack করতে পারেন, বা পরে এমন টেস্ট যোগ হয়, সেখানে এমন anti-hash টেস্ট-এ সুন্দর একটি সমাধানও TLE হয়ে যায়। প্রতিরক্ষা দুটি:

  • সহজ পথ: map ব্যবহার করুন। log n-এর বাড়তি খরচ সাধারণত সময়সীমায় ধরে যায়, আর নিশ্চয়তা পুরো।
  • দ্রুত পথ: একটি custom hash — যেমন splitmix64-এর মতো একটি ভালো mixing ফাংশন, যার সাথে প্রোগ্রাম চালুর সময় ঘড়ি থেকে নেওয়া একটি এলোমেলো সংখ্যা মেশানো হয়। আক্রমণকারী আগে থেকে জানতে পারেন না কোন সংখ্যা কোন bucket-এ পড়বে।
struct SafeHash {                                   // splitmix64 + এলোমেলো seed
    static unsigned long long mix(unsigned long long x) {
        x += 0x9e3779b97f4a7c15ULL;
        x = (x ^ (x >> 30)) * 0xbf58476d1ce4e5b9ULL;
        x = (x ^ (x >> 27)) * 0x94d049bb133111ebULL;
        return x ^ (x >> 31);
    }
    size_t operator()(unsigned long long x) const {
        static const unsigned long long seed =
            chrono::steady_clock::now().time_since_epoch().count();
        return mix(x + seed);
    }
};
unordered_map<long long, int, SafeHash> cnt;

set-এর lower_bound — একই নাম, ভিন্ন খরচ

পর্ব ৩৩-এর std::lower_bound সাজানো vector-এ O(log n). কিন্তু set-এর iterator লাফ দিতে পারে না, এক এক ঘর হাঁটে — তাই std::lower_bound(s.begin(), s.end(), x) তুলনা O(log n) করলেও মোট সময় O(n). দুই লাখ প্রশ্নে সেটাই TLE. সমাধান: set-এর নিজের member — s.lower_bound(x), যা tree ধরে নামে, সত্যিকারের O(log n). এটি কনটেস্টের সবচেয়ে প্রচলিত “অদৃশ্য” TLE-গুলোর একটি।

priority_queue — Dijkstra-র এক ঝলক

std::priority_queue ডিফল্টে সবচেয়ে বড় উপাদানটি ওপরে রাখে (max-heap). সবচেয়ে ছোটটি চাইলে তৃতীয় template argument-এ greater<>. গ্রাফে সবচেয়ে ছোট পথ খোঁজার Dijkstra-র algorithm ঠিক এটিই ব্যবহার করে: “এখন পর্যন্ত সবচেয়ে কাছের যে জায়গাটির দূরত্ব চূড়ান্ত হয়নি, সেটি আগে।”

priority_queue<pair<ll, int>, vector<pair<ll, int>>, greater<>> pq;  // ছোটটি ওপরে
pq.push({0, src});                       // {দূরত্ব, node}
while (!pq.empty()) {
    auto [d, u] = pq.top(); pq.pop();
    if (d != dist[u]) continue;          // পুরোনো, বাতিল হয়ে যাওয়া entry
    for (auto [v, w] : adj[u])
        if (dist[u] + w < dist[v]) { dist[v] = dist[u] + w; pq.push({dist[v], v}); }
}

ছোট কিন্তু ধারালো

vector<int> p{3, 1, 2};
sort(p.begin(), p.end());                         // আগে সবচেয়ে ছোট বিন্যাস
do { /* 1 2 3, 1 3 2, 2 1 3, ... মোট 3! = 6 */ }
while (next_permutation(p.begin(), p.end()));     // শেষ বিন্যাসের পরে false

ll g = gcd(12LL, 18LL), l = lcm(4LL, 6LL);        // 6 আর 12 (C++17)
bitset<1000> seen;  seen[5] = 1;  seen.count();   // কয়টি 1
int ones = __builtin_popcount(13u);               // 13 = 1101, তাই 3
int ones2 = popcount(13u);                        // C++20-এর standard সংস্করণ, <bit>

next_permutation একটি range-কে অভিধানের ক্রমে পরের বিন্যাসে বদলায়, আর শেষ বিন্যাসের পরে false দিয়ে আবার প্রথমটিতে ফেরে — তাই সব বিন্যাস পেতে শুরুতে sort জরুরি। std::gcd আর std::lcm C++17-এর আগে ছিল না; পুরোনো কোডে GCC-র __gcd দেখবেন। std::bitset bit-গুলো ৬৪-bit শব্দে ভরে রাখে, তাই দুটি bitset-এর AND/OR/shift একসাথে ৬৪টি bool-এর কাজ করে। __builtin_popcount standard নয়, GCC আর Clang-এর নিজস্ব; C++20 থেকে বহনযোগ্য বিকল্প std::popcount (শুধু unsigned টাইপে)।

৫ · চেনা ছাঁচ ১ — prefix sum আর two pointers

কনটেস্টের হাজারো সমস্যা আসলে কয়েক ডজন চেনা ছাঁচের নতুন পোশাক। নতুন সমস্যা দেখে “এটি কোন ছাঁচ?” — এই প্রশ্নটিই অর্ধেক সমাধান। এই অংশ আর পরের অংশে সাতটি সবচেয়ে প্রচলিত ছাঁচ, প্রতিটির একটি ছোট উদাহরণসহ। (ছাঁচ চেনার সাধারণ পদ্ধতি C কোর্সের কনটেস্ট পর্বে আছে; এখানে জোর C++ আর STL-এ।)

ছাঁচ ১: prefix sum — একবার হিসাব, বারবার উত্তর

prefix sum-এর ভাবনা ২ নম্বর অংশে দেখেছেন। pre[i] = প্রথম i টি সংখ্যার যোগফল, আর pre[0] = 0. তাহলে l থেকে r পর্যন্ত যোগফল = pre[r] - pre[l-1], কারণ প্রথম r টির যোগফল থেকে প্রথম l−1 টির যোগফল বাদ দিলে ঠিক মাঝের অংশটুকু থাকে। ক্রিকেটের স্কোরবোর্ডের মতো: ১০ ওভার শেষে ৭২, ১৫ ওভার শেষে ১১০ — তাহলে ১১ থেকে ১৫ নম্বর ওভারে ৩৮ রান, প্রতিটি ওভার আলাদা না গুনেই।

1vector<ll> pre(n + 1, 0);       // a = 4 -2 7 1 52for (int i = 1; i <= n; ++i)3    pre[i] = pre[i - 1] + a[i];4// প্রশ্ন: l = 2, r = 55ll ans = pre[r] - pre[l - 1];
ভ্যারিয়েবল
i
a[i]
pre[0..5]
ans
  1. এক ঘর বেশি, শূন্য দিয়ে শুরু। n = 5 টি সংখ্যার জন্য ৬টি ঘর। pre[0] = 0 হলো “কিছুই না যোগ করলে যোগফল” — এর কারণে l = 1 হলেও pre[l-1] বৈধ থাকে, আলাদা if লাগে না।
  2. i = 1. pre[1] = pre[0] + 4 = 4. প্রতিটি ঘর আগের ঘরের ওপর মাত্র একটি যোগ।
  3. i = 2. pre[2] = 4 + (-2) = 2. ঋণাত্মক সংখ্যাতেও একই নিয়ম — prefix sum শুধু ধনাত্মক সংখ্যার জন্য নয়।
  4. i = 3. pre[3] = 2 + 7 = 9.
  5. i = 4. pre[4] = 9 + 1 = 10.
  6. i = 5, তৈরি শেষ। pre[5] = 15, পুরো array-র যোগফল। তৈরি করতে লাগল n টি যোগ — O(n), একবারই।
  7. প্রশ্নের উত্তর এক বিয়োগে। ২ থেকে ৫ নম্বর: pre[5] - pre[1] = 15 - 4 = 11. মিলিয়ে দেখুন: −২ + ৭ + ১ + ৫ = ১১. প্রশ্ন যতই আসুক, প্রতিটির খরচ O(1).

এবার পুরো সমাধানটি, কনটেস্টের ধরনে: একাধিক test case, fast I/O, long long. (এখানে আমরা std:: লিখেছি — কনটেস্টের কোডও এভাবে লেখা যায়।) দ্বিতীয় test case-টি ইচ্ছা করে বড় সংখ্যায়, overflow যাচাইয়ের জন্য:

#include <iostream>
#include <vector>
using ll = long long;

int main() {
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);
    int t;
    std::cin >> t;
    while (t--) {
        int n, q;
        std::cin >> n >> q;
        std::vector<ll> pre(n + 1, 0);    // প্রতিটি test case-এ নতুন
        for (int i = 1; i <= n; ++i) { ll x; std::cin >> x; pre[i] = pre[i - 1] + x; }
        while (q--) {
            int l, r;
            std::cin >> l >> r;
            std::cout << pre[r] - pre[l - 1] << '\n';
        }
    }
}

আউটপুট চার লাইন: 9, 11, 1, 3000000000. প্রথম test case-এ তিনটি প্রশ্ন: ১–৩ (৪ − ২ + ৭ = ৯), ২–৫ (১১, stepper-এর হিসাব), আর ৪–৪ (একটি মাত্র সংখ্যা, ১)। দ্বিতীয় test case-এ তিনটি ১০৯-এর যোগফল ৩·১০৯ — int-এর সীমা (প্রায় ২.১·১০৯) পেরিয়ে গেছে। pre যদি std::vector<int> হতো, এখানে overflow হতো: UB, বাস্তবে সাধারণত একটি ঋণাত্মক আবর্জনা সংখ্যা। pre vector-টি লুপের ভেতরে তৈরি, তাই প্রথম test case-এর মান দ্বিতীয়টিতে ঢোকার কোনো পথ নেই। আর '\n', endl নয় — দুই লাখ প্রশ্নেও flush হবে শেষে একবার।

ছাঁচ ২: two pointers — দুই আঙুলের জানালা

প্রশ্ন: ধনাত্মক সংখ্যার একটি তালিকায় সবচেয়ে লম্বা পরপর অংশ কোনটি, যার যোগফল S-এর বেশি নয়? যেমন একটি দোকানের প্রতিদিনের খরচ, আর জানতে চান সর্বোচ্চ কতদিন টানা চালানো যায় বাজেট না পেরিয়ে। সব জোড়া (l, r) পরীক্ষা করলে O(n²)। two pointers কৌশলে দুটি আঙুল — l আর r — দুজনেই শুধু সামনে এগোয়: r নতুন সংখ্যা জানালায় ঢোকায়, আর যোগফল S পেরোলে l পুরোনো সংখ্যা বের করে দেয়। এমন চলমান জানালাকে বলে sliding window.

1int l = 0, best = 0; ll sum = 0;   // a = 2 1 3 2 4 1, S = 62for (int r = 0; r < n; ++r) {3    sum += a[r];4    while (sum > S) sum -= a[l++];5    best = max(best, r - l + 1);6}
জানালার অবস্থা
r, l
জানালা
sum
best
  1. শুরু। জানালা খালি, l = 0. সংখ্যাগুলো ধনাত্মক — এই শর্তটিই কৌশলটিকে বৈধ করে: জানালা বড় করলে যোগফল শুধু বাড়ে, ছোট করলে শুধু কমে।
  2. r = 0. 2 ঢুকল, sum = 2 ≤ 6. জানালার দৈর্ঘ্য ১, best = 1.
  3. r = 1. 1 ঢুকল, sum = 3. দৈর্ঘ্য ২, best = 2.
  4. r = 2. 3 ঢুকল, sum = 6 — ঠিক সীমায়, চলবে। দৈর্ঘ্য ৩, best = 3.
  5. r = 3, প্রথম সংকোচন। 2 ঢুকে sum = 8 > 6. তাই l এগিয়ে বাঁয়ের 2 বের করে দিল: sum = 6, l = 1. জানালা [1 3 2], দৈর্ঘ্য ৩ — best একই থাকল।
  6. r = 4, দুবার সংকোচন। 4 ঢুকে sum = 10. বের হলো 1 (sum 9), তারপর 3 (sum 6). l = 3, জানালা [2 4]. লক্ষ করুন, l কখনো পেছনে ফেরে না।
  7. r = 5, শেষ। 1 ঢুকে sum = 7, বের হলো 2, sum = 5. উত্তর best = 3. r মোট n বার এগোল, l-ও সব মিলিয়ে সর্বোচ্চ n বার — ভেতরে while থাকলেও মোট কাজ O(n), O(n²) নয়।

৬ · চেনা ছাঁচ ২ — binary search on answer, greedy, BFS, DP, sieve

ছাঁচ ৩: binary search on answer — উত্তরটাই খুঁজুন

কখনো সরাসরি উত্তর বের করা কঠিন, কিন্তু “এই মানটি কি যথেষ্ট?” — এই হ্যাঁ/না প্রশ্নের উত্তর দেওয়া সহজ। আর উত্তরটি যদি monotonic হয় — কোনো মান যথেষ্ট হলে তার চেয়ে বড় সব মানও যথেষ্ট — তাহলে সম্ভাব্য উত্তরের পুরো পরিসরে binary search চালানো যায়। একে বলে binary search on answer.

উদাহরণ: একটি কুরিয়ার অফিসে ৬টি পার্সেলের ওজন 3 2 2 4 1 4, এই ক্রমেই পাঠাতে হবে, আর ৩ দিনের মধ্যে সব শেষ করতে হবে। প্রতিদিনের ভ্যানের সর্বনিম্ন ধারণক্ষমতা কত হলে চলবে? “ধারণক্ষমতা c হলে কয়দিন লাগে” — এটি greedy-ভাবে সহজে গোনা যায়: যতক্ষণ ধরে ততক্ষণ আজকের ভ্যানে তুলুন, না ধরলে পরের দিন। আর c বাড়ালে দিন কখনো বাড়ে না — monotonic.

1ll lo = 4, hi = 16;              // সবচেয়ে ভারী, মোট ওজন2while (lo < hi) {3    ll mid = lo + (hi - lo) / 2;4    if (days(mid) <= 3) hi = mid; else lo = mid + 1;5}   // উত্তর: lo
খোঁজার পরিসর
lo, hi
mid
ভাগ
দিন, সিদ্ধান্ত
  1. পরিসর ঠিক করা। ধারণক্ষমতা সবচেয়ে ভারী পার্সেলের (৪) কম হলে সেটি কোনোদিনই যাবে না, তাই lo = 4. মোট ওজন (১৬) হলে একদিনেই সব যায়, তাই hi = 16. উত্তর এর মাঝে কোথাও।
  2. mid = 10. greedy ভাগ: [3 2 2] = 7 (4 যোগ করলে 11, ধরে না), তারপর [4 1 4] = 9. মাত্র ২ দিন — যথেষ্ট। উত্তর 10 বা তার কম, তাই hi = 10.
  3. mid = 7. [3 2 2] = 7, [4 1] = 5, [4]. ৩ দিন — এখনো যথেষ্ট। hi = 7.
  4. mid = 5. [3 2], [2] (4 যোগ করলে 6), [4 1], [4]. ৪ দিন — বেশি। তাই 5 আর তার চেয়ে ছোট সব বাতিল: lo = 6.
  5. mid = 6. [3 2], [2 4], [1 4]. ঠিক ৩ দিন — যথেষ্ট। hi = 6.
  6. উত্তর 6. lo আর hi মিলে গেছে। মাত্র ৪ বার days() ডাকা হলো, প্রতিবার O(n). মোট O(n log(মোট ওজন)) — ওজন ১০১৪ পর্যন্ত হলেও মাত্র ৪৭-এর মতো ধাপ। lo + (hi - lo) / 2 লেখা হয়েছে, যাতে খুব বড় lo আর hi যোগ করে overflow না হয়।

ছাঁচ ৪: greedy with sorting — সবচেয়ে কম বাস

সমস্যা: শিক্ষাসফরে কয়েকটি দল যাবে, প্রতিটি দলের সদস্যসংখ্যা জানা। প্রতিটি বাসে সর্বোচ্চ C জন ধরে, প্রতিটি দলকে একসাথে থাকতে হবে, আর নিয়ম অনুযায়ী এক বাসে সর্বোচ্চ দুটি দল। সবচেয়ে কম কয়টি বাস লাগবে? greedy ভাবনা: সবচেয়ে বড় দলটির জন্য একটি বাস লাগবেই। তার সাথে কাউকে বসানো গেলে সবচেয়ে ছোট দলটিকেই বসান — কারণ যে দলই বড়টির সাথে বসতে পারে, সবচেয়ে ছোটটিও পারে, আর ছোটটিকে এখন ব্যবহার করলে পরের জন্য বড়গুলো রইল না, ছোটগুলোই রইল। এমন যুক্তিকে বলে exchange argument: যেকোনো সর্বোত্তম সমাধানকে অদলবদল করে greedy-র সমাধানে আনা যায়, খারাপ না করে।

sort(g.begin(), g.end());                  // C = 10: 7 2 5 9 3 4 -> 2 3 4 5 7 9
int i = 0, j = n - 1, buses = 0;
while (i <= j) {
    if (i < j && g[i] + g[j] <= C) ++i;    // সবচেয়ে ছোটটি বড়টির সাথে যায়
    --j; ++buses;                          // সবচেয়ে বড়টি এই বাসে
}   // 9 একা, 7+2, 5+3, 4 একা  =>  4 বাস

greedy-র বিপদ হলো, সেটি প্রায়ই দেখতে ঠিক লাগে কিন্তু ভুল। তাই প্রমাণ — অন্তত exchange argument-এর মতো একটি যুক্তি — না পেলে greedy-কে stress test দিয়ে (৭ নম্বর অংশ) যাচাই করুন।

ছাঁচ ৫: BFS on a grid — গোলকধাঁধায় সবচেয়ে ছোট পথ

একটি গোলকধাঁধা: . মানে খোলা ঘর, # মানে দেয়াল। বাঁ-ওপরের কোণ থেকে ডান-নিচের কোণে যেতে সবচেয়ে কম কয় ধাপ লাগে (প্রতি ধাপে ওপর, নিচ, ডান বা বাঁয়ে এক ঘর)? BFS (breadth-first search) শুরুর ঘর থেকে ঢেউয়ের মতো ছড়ায়: আগে ১ ধাপ দূরের সব ঘর, তারপর ২ ধাপ দূরের সব, তারপর ৩... পুকুরে ঢিল ফেললে যেমন বৃত্ত ছড়ায়। একটি ঘরে প্রথমবার যখন পৌঁছায়, সেটিই তার সবচেয়ে ছোট দূরত্ব, কারণ প্রতিটি ধাপের খরচ সমান। এর জন্য দরকার একটি std::queue — যে আগে এসেছে, সে আগে যাবে।

#include <iostream>
#include <queue>
#include <string>
#include <vector>
int main() {
    int r, c; std::cin >> r >> c;
    std::vector<std::string> g(r); for (auto& row : g) std::cin >> row;
    std::vector<std::vector<int>> d(r, std::vector<int>(c, -1));   // -1 = এখনো যাইনি
    std::queue<std::pair<int, int>> q; q.push({0, 0}); d[0][0] = 0;
    const int dr[] = {1, -1, 0, 0}, dc[] = {0, 0, 1, -1};
    while (!q.empty()) {
        auto [x, y] = q.front(); q.pop();
        for (int k = 0; k < 4; ++k) {      // চার দিকে এক ঘর করে
            int nx = x + dr[k], ny = y + dc[k];
            if (nx < 0 || ny < 0 || nx >= r || ny >= c       // গ্রিডের বাইরে,
                || g[nx][ny] == '#' || d[nx][ny] != -1) continue;  // দেয়াল, বা আগে দেখা
            d[nx][ny] = d[x][y] + 1; q.push({nx, ny});
        }
    }
    std::cout << d[r - 1][c - 1] << '\n';
}

আউটপুট: 16. গোলকধাঁধাটি সাপের মতো: প্রথম সারি ধরে ডানে ৪ ধাপ, ডান কিনারা দিয়ে নিচে ২ ধাপ, তৃতীয় সারি ধরে বাঁয়ে ৪ ধাপ, বাঁ কিনারা দিয়ে নিচে ২ ধাপ, শেষ সারি ধরে ডানে ৪ ধাপ — মোট ১৬. দেয়াল না থাকলে লাগত মাত্র ৮. d একসাথে দুটি কাজ করে: দূরত্ব রাখে, আর -1 দিয়ে বলে “এখনো যাইনি” — তাই কোনো ঘর দুবার queue-তে ঢোকে না, আর মোট খরচ O(r × c). শেষ ঘরে পৌঁছানো না গেলে উত্তর থাকত -1 — অনেক সমস্যা ঠিক এটিই ছাপতে বলে। dr/dc array দিয়ে চার দিক একটি লুপে লেখা কনটেস্টের একটি চিরচেনা কৌশল; এই গ্রিড আসলে একটি গ্রাফ, যার প্রতিটি ঘর একটি node.

ছাঁচ ৬: সরল 1D DP — greedy যেখানে হারে

৬ টাকা দিতে হবে, নোট আছে ১, ৩ আর ৪ টাকার (যত খুশি)। সবচেয়ে কম কয়টি নোট? greedy বলবে “সবচেয়ে বড়টি আগে”: ৪ + ১ + ১ = ৩টি নোট। কিন্তু ৩ + ৩ = ২টি। এখানে greedy হারে, দরকার dynamic programming: ছোট উপসমস্যার উত্তর একবার বের করে রেখে বড়টি গড়া। dp[x] = x টাকা দিতে সবচেয়ে কম নোট।

const int INF = 1e9;
vector<int> dp(X + 1, INF);  dp[0] = 0;          // 0 টাকায় 0 নোট
for (int x = 1; x <= X; ++x)
    for (int c : coins)                             // শেষ নোটটি c হলে?
        if (c <= x && dp[x - c] != INF) dp[x] = min(dp[x], dp[x - c] + 1);
// coins = {1, 3, 4}, X = 6:  dp = 0 1 2 1 1 2 2  =>  উত্তর 2

DP-র আসল প্রশ্ন কোড নয়, অবস্থা (state) আর রূপান্তর: “dp[x] কী বোঝায়, আর সেটি কোন ছোট dp থেকে আসে?” এই দুটি এক বাক্যে বলতে পারলে কোড প্রায় নিজেই লেখা হয়ে যায়। খরচ O(X × নোটের ধরন).

ছাঁচ ৭: sieve — মৌলিক সংখ্যা একসাথে

১০৭ পর্যন্ত সব মৌলিক সংখ্যা দরকার হলে প্রতিটি সংখ্যা আলাদা করে পরীক্ষা না করে Sieve of Eratosthenes: প্রতিটি মৌলিক সংখ্যার সব গুণিতক কেটে দিন, খরচ O(n log log n). পর্ব ৩৯-এ দেখেছেন কীভাবে এটি constexpr দিয়ে compile-এর সময়েই চালানো যায়। কনটেস্টে সাধারণত vector<bool> বা bitset দিয়ে লেখা হয়, যাতে প্রতিটি সংখ্যার জন্য মাত্র একটি bit লাগে — ১০৭ সংখ্যায় মোটে প্রায় ১.২৫ MB.

৭ · কনটেস্টে ডিবাগিং — stress testing আর WA-র চেনা কারণ

WA পেয়েছেন, কিন্তু নমুনা তো মিলছে। জাজ বলবে না কোন টেস্টে ভুল। কোড বারবার পড়ে ভুল খোঁজা ধীর, আর নিজের কোডে নিজের ভুল চোখে পড়ে কম। এর চেয়ে কার্যকর পদ্ধতি stress testing — যন্ত্রকে দিয়ে ভুল খোঁজানো। চারটি অংশ:

  1. আসল সমাধান — দ্রুত, কিন্তু সন্দেহজনক।
  2. একটি brute force সমাধান — ধীর, কিন্তু এত সরল যে ভুলের সুযোগ প্রায় নেই (যেমন range sum-এর জন্য প্রতি প্রশ্নে সাধারণ লুপ)।
  3. একটি generator — একটি seed নিয়ে ছোট এলোমেলো ইনপুট বানায় (n ≤ ৮, সংখ্যা ≤ ১০)। ছোট, যাতে ভুল ধরা পড়লে হাতে বোঝা যায়।
  4. একটি লুপ — হাজারবার generator চালায়, দুই সমাধানের আউটপুট মেলায়, প্রথম অমিলেই থেমে সেই ইনপুটটি দেখায়।
// gen.cpp — seed কমান্ড লাইন থেকে, তাই প্রতিবার ভিন্ন কিন্তু আবার ঘটানো যায়
int main(int argc, char* argv[]) {
    std::mt19937 rng(std::stoi(argv[1]));
    int n = rng() % 8 + 1;
    std::cout << n << '\n';
    for (int i = 0; i < n; ++i) std::cout << static_cast<int>(rng() % 21) - 10 << ' ';
    std::cout << '\n';
}
for i in $(seq 1 1000); do
  ./gen $i > in.txt
  ./sol < in.txt > a.txt;  ./brute < in.txt > b.txt
  cmp -s a.txt b.txt || { echo "mismatch, seed $i"; cat in.txt; break; }
done

generator-এ নির্দিষ্ট seed-এর std::mt19937, যাতে যে seed-এ ভুল ধরা পড়ল, সেটি দিয়ে একই ইনপুট আবার বানানো যায়। পুরো পদ্ধতি — কীভাবে ভুল ইনপুট আরও ছোট করবেন, কেন এটি মানুষের চোখের চেয়ে ভালো — C কোর্সের কনটেস্ট পর্বে বিস্তারিত; আর পর্ব ৪৮-এর টেস্টিংয়ের সাথে এর মিল লক্ষ করুন — এটি আসলে ছোট আকারের property-based testing.

WA-র চেনা কারণ — জমা দেওয়ার আগে একবার দেখুন

কারণলক্ষণপ্রতিকার
overflowছোট টেস্টে ঠিক, বড়টিতে ঋণাত্মক বা অদ্ভুত সংখ্যাll, 1LL *, 0LL; সর্বোচ্চ মান হিসাব করুন
off-by-oneসীমার ঠিক কিনারায় ভুলn = 1, l = r, l = 1, r = n হাতে চালান
test case-এর পুরোনো অবস্থাপ্রথম test case ঠিক, পরেরগুলো ভুলডেটা solve()-এর ভেতরে; global হলে n ঘর reset
অর্ধেক ইনপুট পড়ে থেমে যাওয়াউত্তর আগেই জেনে return, বাকি ইনপুট পরের test case-এ ঢোকেআগে পুরো ইনপুট পড়ুন, তারপর সিদ্ধান্ত
আউটপুটের ফরম্যাটউত্তর ঠিক, তবু WA"YES"/"Yes", দশমিকের ঘর, প্রতি উত্তর আলাদা লাইনে?
edge caseসব সমান, সব ঋণাত্মক, খালি উত্তরনিজের ছোট টেস্টে এগুলো রাখুন
অনুশীলনের সময়

ঘরে অনুশীলনের সময় -fsanitize=address,undefined দিয়ে compile করুন (পর্ব ৪৩)। array-র সীমার বাইরে লেখা আর signed overflow — যা জাজে হয়তো নীরবে ভুল উত্তর দিত — তখন সঙ্গে সঙ্গে লাইন নম্বরসহ ধরা পড়ে। GCC-তে -D_GLIBCXX_DEBUG দিলে vector-এর []-ও সীমা যাচাই করে।

৮ · অনুশীলনের কৌশল আর কনটেস্টের নীতি

কীভাবে উন্নতি হয়

  • rating-এর মই বেয়ে ওঠা: Codeforces-এ প্রতিটি সমস্যার একটি কঠিনতার রেটিং থাকে। নিজের বর্তমান rating-এর চেয়ে একটু (মোটামুটি ১০০–৩০০) বেশি কঠিন সমস্যা নিয়মিত করুন — খুব সহজ সমস্যায় আরাম, খুব কঠিনে হতাশা; শেখা হয় মাঝখানে। AtCoder-এর Beginner Contest-এর আগের সমস্যাগুলো শুরুর জন্য চমৎকার সিঁড়ি।
  • upsolving: কনটেস্ট শেষে যে সমস্যা পারেননি, সেটি পরে সমাধান করা। বেশিরভাগ উন্নতি এখানেই হয়, কনটেস্টের সময়ে নয়। নিয়ম করুন: প্রতিটি কনটেস্টের পরে অন্তত একটি না-পারা সমস্যা।
  • editorial পড়ার নিয়ম: অন্তত ৩০–৬০ মিনিট নিজে লড়াই করুন। তারপর editorial-এর শুধু প্রথম ইঙ্গিত পড়ে আবার চেষ্টা করুন। পুরো সমাধান পড়লে বন্ধ করে নিজে লিখুন — কপি করে নয়। শেষে লিখে রাখুন: “কোন ধারণাটি আমার মাথায় আসেনি, আর পরেরবার কোন লক্ষণ দেখে আসবে?”
  • বিষয় ধরে গভীর চর্চা: একটি ছাঁচ (যেমন binary search on answer) নিয়ে টানা ১০–১৫টি সমস্যা। CSES Problem Set এর জন্য খুব ভালোভাবে সাজানো।

কনটেস্টের নীতি

কনটেস্টের মূল্য একটাই — এটি আপনার নিজের দক্ষতা মাপে। সেই মাপ মিথ্যা হলে rating-এর কোনো অর্থ থাকে না, আর বাকি প্রতিযোগীদের প্রতি অন্যায় হয়।

  • plagiarism নয়। কনটেস্ট চলাকালীন অন্যের কোড নেওয়া, বন্ধুর সাথে সমাধান ভাগ করা, বা কোথাও প্রকাশিত সমাধান জমা দেওয়া — প্ল্যাটফর্মগুলো কোড-মিলের যন্ত্র দিয়ে এটি ধরে, আর শাস্তি হিসেবে ফল বাতিল থেকে অ্যাকাউন্ট বন্ধ পর্যন্ত হতে পারে।
  • AI-এর নিয়ম আগে পড়ুন। AI এখন অনেক কনটেস্ট-সমস্যা নিজেই সমাধান করতে পারে, তাই অনেক প্ল্যাটফর্ম কনটেস্ট চলাকালীন AI দিয়ে সমাধান লেখানো সীমিত বা নিষিদ্ধ করেছে। নিয়ম প্ল্যাটফর্ম ভেদে আলাদা, আর সময়ের সাথে বদলায় — তাই প্রতিটি কনটেস্টের আগে সেই কনটেস্টের নিয়মটি নিজে পড়ুন। ICPC-র মতো সরাসরি উপস্থিতির কনটেস্টে ইন্টারনেটই থাকে না।
  • নিজের আগে লেখা কোড (যেমন নিজের টেমপ্লেট বা লাইব্রেরি) ব্যবহার সাধারণত বৈধ — কিন্তু সেটিও নিয়মে দেখে নিন। ICPC-তে সাধারণত নির্দিষ্ট পাতার একটি ছাপানো নোট (team reference document) নেওয়া যায়।
C জানলে

C-তে কনটেস্ট করলে scanf/printf এমনিতেই দ্রুত, আর long long, overflow, multi-test-এর সব নিয়ম একই। পার্থক্য STL-এ: C-তে sort-এর জন্য qsort, আর map, set, priority_queue, binary search সব হাতে লিখতে হয় — যা কনটেস্টের চাপের মধ্যে বাগের বড় উৎস। verdict, ছাঁচ চেনা আর stress testing-এর বিস্তারিত আলোচনা আছে C কোর্সের কনটেস্ট প্রোগ্রামিং পর্বে।

পরীক্ষা ও ইন্টারভিউতে
  • sync_with_stdio(false) আর cin.tie(nullptr) কী করে? — প্রথমটি C stdio-র সাথে মেলানো বন্ধ করে stream-কে নিজের buffer দেয়; দ্বিতীয়টি প্রতিবার পড়ার আগে cout-এর flush বন্ধ করে। এরপর printf আর cout মেশানো যায় না।
  • map আর unordered_map-এর complexity? — map: balanced tree, সবসময় O(log n); unordered_map: hash table, গড়ে O(1), সবচেয়ে খারাপ ক্ষেত্রে O(n).
  • অনেকগুলো range sum প্রশ্ন দ্রুত কীভাবে? — prefix sum: O(n)-এ তৈরি, প্রতিটি প্রশ্ন pre[r] - pre[l-1], O(1).
  • priority_queue-এর ডিফল্ট আচরণ কী, min-heap কীভাবে? — ডিফল্টে max-heap (সবচেয়ে বড়টি top); min-heap-এর জন্য comparator std::greater<>.
  • bits/stdc++.h কি standard header? — না, GCC-র libstdc++-এর নিজস্ব ফাইল; বহনযোগ্য নয়, compile ধীর করে।

৯ · AI-এর যুগে — নমুনায় পাস, গোপন টেস্টে ফেল

AI-কে একটি কনটেস্ট সমস্যা দিলে প্রায়ই এমন কোড আসে, যা নমুনা ইনপুটে নিখুঁত উত্তর দেয়। ঠিক এখানেই ফাঁদ: নমুনা ছোট, আর গোপন টেস্ট বড় ও নিষ্ঠুর। AI-এর সাধারণ ভুলগুলো প্রায় সবই এই দুই জগতের ফাঁকে পড়ে:

AI যা করেগোপন টেস্টে ফলযা দেখবেন
constraints না দেখে সরল O(n²) সমাধানTLEn-এর সর্বোচ্চ মান বসিয়ে অপারেশন গুনুন
int-এ যোগফল বা গুণফলবড় টেস্টে WAসর্বোচ্চ মান হিসাব; ll, 1LL *, 0LL
“দেখতে ঠিক” greedy, প্রমাণ ছাড়াকোনো কোনো টেস্টে WAপ্রমাণ চান, বা brute force দিয়ে stress test
unordered_map, নিরাপত্তা ছাড়াhack বা anti-hash টেস্টে TLEmap বা custom hash
set-এ std::lower_boundTLEs.lower_bound(x)
প্রতি লাইনে endl, fast I/O নেইবড় ইনপুটে TLEটেমপ্লেটের দুই লাইন আর '\n'
অন্য একটি পরিচিত সমস্যার সমাধানWA — সমস্যাটি আসলে একটু আলাদাশর্তগুলো নিজে আবার পড়ুন

শেষ সারিটি সবচেয়ে সূক্ষ্ম। AI বিশাল পরিমাণ পুরোনো সমস্যা দেখে শিখেছে, তাই নতুন সমস্যাটি কোনো পুরোনো সমস্যার মতো দেখালে সে প্রায়ই পুরোনোটির সমাধান দেয় — একটি শর্তের পার্থক্য খেয়াল না করে।

AI-এর সবচেয়ে ভালো ব্যবহার কনটেস্টের পরে, শেখার জন্য — যদি সেই প্ল্যাটফর্মের নিয়ম তা অনুমতি দেয়। কনটেস্ট শেষে editorial বুঝতে না পারলে AI-কে একজন ধৈর্যশীল শিক্ষক হিসেবে ব্যবহার করুন — সমাধানদাতা হিসেবে নয়:

কনটেস্ট শেষ হওয়ার পরে, editorial বোঝার জন্য এমন prompt দিন:

আমি C++ দিয়ে কনটেস্ট প্রোগ্রামিং শিখছি। কনটেস্ট শেষ হয়ে গেছে; আমি একটি সমস্যা পারিনি আর editorial পড়েও পুরোটা বুঝিনি। নিচে সমস্যা, constraints আর editorial দিলাম। ১. পুরো কোড দিও না। আগে শুধু মূল ধারণাটি এক-দুই বাক্যে বলো, আর কোন লক্ষণ দেখে এই ধারণা মাথায় আসা উচিত ছিল। ২. constraints থেকে প্রয়োজনীয় complexity কীভাবে বোঝা যায়, হিসাব করে দেখাও। ৩. একটি ছোট উদাহরণে algorithm-টি হাতে চালিয়ে দেখাও, প্রতিটি ধাপে কী বদলায়। ৪. সমাধানটি কেন সঠিক, তার যুক্তি দাও (greedy হলে exchange argument). ৫. কোন কোন edge case আর overflow-এর জায়গায় ভুল হতে পারে, তালিকা দাও। তারপর আমি নিজে কোড লিখব; তুমি শুধু আমার কোডের ভুল খুঁজে দেবে।

AI-এর কনটেস্ট-কোড যাচাইয়ের চেকলিস্ট

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

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১ICPC শুরু হয়েছিল ১৯৭০-এর দশকে যুক্তরাষ্ট্রের একটি বিশ্ববিদ্যালয়ে। একটি ছোট স্থানীয় প্রতিযোগিতা কীভাবে বিশ্বের সবচেয়ে বড় বিশ্ববিদ্যালয় প্রোগ্রামিং প্রতিযোগিতা হলো, আর এর নিয়মগুলো কী শেখায়?

ICPC-র শিকড় ১৯৭০ সালে যুক্তরাষ্ট্রের Texas A&M University-তে আয়োজিত একটি প্রোগ্রামিং প্রতিযোগিতায়, যার আয়োজক ছিল কম্পিউটার বিজ্ঞানের সম্মান-সংগঠন Upsilon Pi Epsilon-এর একটি শাখা। কয়েক বছরের মধ্যে আরও বিশ্ববিদ্যালয় যোগ দেয়, আর ১৯৭৭ সালে প্রথম “World Finals” হয় Association for Computing Machinery (ACM)-এর একটি সম্মেলনের সাথে। এই কারণেই দীর্ঘদিন এর নাম ছিল ACM ICPC. ১৯৮০-র দশকের শেষ থেকে এর সদর দপ্তর Baylor University-তে, আর এক দীর্ঘ সময় IBM ছিল প্রধান পৃষ্ঠপোষক। ২০১০-এর দশকের শেষে ACM-এর সাথে আনুষ্ঠানিক সম্পর্ক বদলায়, আর এখন এটি শুধু “ICPC” নামে, একটি নিজস্ব ফাউন্ডেশনের অধীনে চলে। আজ শতাধিক দেশের হাজারো বিশ্ববিদ্যালয়ের দল আঞ্চলিক পর্বে অংশ নেয়।

এর নিয়মগুলো লক্ষ করার মতো। তিনজনের দল, কিন্তু কম্পিউটার একটি — তাই কে কখন কিবোর্ডে বসবে, কে কাগজে ভাববে, কে অন্যের কোড পড়ে ভুল খুঁজবে, তা ঠিক করাই কৌশলের বড় অংশ। ফল ঠিক হয় সমাধান করা সমস্যার সংখ্যায়; সমান হলে কম “penalty time”-এ — প্রতিটি সমাধানের সময় যোগ, আর সেই সমস্যায় আগের প্রতিটি ভুল জমার জন্য ২০ মিনিট শাস্তি। তাই এখানে “জমা দিয়ে দেখি” অভ্যাসের দাম আছে; আগে নিশ্চিত হওয়া শেখায়। আর অনেক আসরেই শেষ এক ঘণ্টায় scoreboard “frozen” থাকে, শেষ মুহূর্তের উত্তেজনা পুরস্কার মঞ্চ পর্যন্ত টিকিয়ে রাখতে।

বাংলাদেশের জন্য ICPC বিশেষ। বহু বছর ধরে ঢাকায় এশীয় আঞ্চলিক পর্ব হচ্ছে, আর দেশের দলগুলো ১৯৯০-এর দশকের শেষ দিক থেকে World Finals-এ অংশ নিয়ে আসছে। ২০২২ সালের নভেম্বরে ঢাকা নিজেই ICPC World Finals-এর আয়োজক ছিল — দেশের প্রোগ্রামিং সমাজের দীর্ঘ পরিশ্রমের একটি স্বীকৃতি। এই সাফল্যের পেছনে আছে বিশ্ববিদ্যালয়ের কোচ, প্রাক্তন প্রতিযোগী আর স্বেচ্ছাসেবীদের তৈরি ক্যাম্প, অনলাইন judge আর সমস্যা-লেখার সংস্কৃতি; অনেক প্রাক্তন প্রতিযোগী পরে বিশ্বের বড় প্রযুক্তি প্রতিষ্ঠানে কাজ করেছেন।

ICPC-র সাফল্য একটি সরল ধারণার ওপর দাঁড়ানো: সমস্যা স্পষ্ট, বিচার স্বয়ংক্রিয় আর নিরপেক্ষ, ফল সাথে সাথে। ঠিক এই কাঠামো পরে Codeforces, AtCoder-এর মতো অনলাইন মঞ্চে ছড়িয়ে পড়েছে, যেখানে যেকোনো দেশের যে কেউ প্রতি সপ্তাহে অংশ নিতে পারেন।

মূল উপলব্ধি: ICPC শুধু algorithm-এর প্রতিযোগিতা নয়; সীমিত সম্পদ (একটি কম্পিউটার, পাঁচ ঘণ্টা) ভাগ করে দলগতভাবে সিদ্ধান্ত নেওয়া আর ভুলের দাম বুঝে কাজ করার প্রশিক্ষণ — যা পেশাদার জীবনেও খুব চেনা।

প্র ০২কনটেস্টে C++ এত প্রভাবশালী কেন? আর Python বা Java কখন যুক্তিসংগত — এমনকি ভালো — পছন্দ?

বড় প্ল্যাটফর্মগুলোর জমা পড়া সমাধানের পরিসংখ্যান দেখলে বোঝা যায়, বেশিরভাগই C++-এ। কারণ তিনটি। প্রথম, গতি। C++ আগেই machine code-এ compile হয়, প্রতিটি int সরাসরি রেজিস্টারে থাকে, vector-এর উপাদান মেমরিতে পাশাপাশি — cache-এর জন্য আদর্শ। CPython প্রতিটি অপারেশন একটি interpreter দিয়ে চালায়, আর প্রতিটি সংখ্যা একটি আলাদা object; সাধারণ লুপে পার্থক্য অনেক গুণ হতে পারে। Java JIT compile করে বলে বেশ দ্রুত, কিন্তু JVM চালু হওয়া, garbage collection আর boxing-এর খরচ টানটান সময়সীমায় কখনো কখনো চোখে পড়ে।

দ্বিতীয়, STL. balanced tree (set/map), heap, binary search, sort, permutation, bitset — সব তৈরি, আর template বলে টাইপ অনুযায়ী optimize হয় (পর্ব ৩৩-এর qsort বনাম sort-এর গল্প)। GCC-র library-তে আরও কিছু বিশেষ কাঠামো আছে, যেমন policy-based data structure-এর order-statistics tree, যা কনটেস্টে জনপ্রিয়। তৃতীয়, নেটওয়ার্ক প্রভাব: বেশিরভাগ editorial, শিক্ষামূলক বই আর সমাধানের উদাহরণ C++-এ, আর সমস্যা-লেখকেরাও সাধারণত নিজেদের সমাধান C++-এ লিখে সময়সীমা ঠিক করেন। ফলে নতুনরাও C++ বেছে নেন, আর চক্রটি নিজেকে শক্তিশালী করে।

তবু অন্য ভাষার জায়গা আছে। Python-এ পূর্ণসংখ্যার আকারের কোনো সীমা নেই, তাই বিশাল সংখ্যার হিসাবে এটি অসাধারণ সুবিধাজনক; কোডও ছোট, তাই সহজ সমস্যা দ্রুত লেখা যায়। অনেক প্ল্যাটফর্মে PyPy থাকে, যা JIT দিয়ে Python-কে অনেক দ্রুত করে। AtCoder-এর মতো কিছু প্ল্যাটফর্মে বহু সমস্যা Python-এও সময়ের ভেতরে পাস করে। Java-র BigInteger আর BigDecimal দীর্ঘদিন বড় সংখ্যার সমস্যায় জনপ্রিয় ছিল, আর ICPC-তে C, C++, Java, Python, Kotlin-এর মতো কয়েকটি ভাষা সাধারণত অনুমোদিত থাকে। অনেক অভিজ্ঞ প্রতিযোগী তাই মূলত C++ লেখেন, কিন্তু বড় সংখ্যার কোনো সমস্যায় Python-এ চলে যান।

সতর্কতা: সময়সীমা সাধারণত C++-এর হিসাবে ঠিক হয়, আর ICPC-র মতো কনটেস্টে প্রায়ই বলা থাকে না যে প্রতিটি সমস্যা প্রতিটি ভাষায় সমাধানযোগ্য হবেই। ধীর ভাষায় সঠিক algorithm-ও TLE খেতে পারে।

মূল উপলব্ধি: ভাষা একটি টুল। C++ জেতে কাঁচা গতি আর STL-এর জোরে, কিন্তু সমস্যার ধরন বুঝে টুল বদলানো — বড় সংখ্যায় Python, দ্রুত প্রোটোটাইপে স্ক্রিপ্ট — অভিজ্ঞতারই লক্ষণ।

প্র ০৩কনটেস্টে যে অভ্যাসগুলো আপনাকে দ্রুত করে, পেশাদার কোডে সেগুলোর অনেকগুলোই ক্ষতিকর। কোন অভ্যাস ভুলতে হবে, আর কোনগুলো সাথে রাখার মতো?

কনটেস্টের কোডের একজনই পাঠক — জাজ — আর জীবনকাল কয়েক মিনিট। এই দুটি শর্তে যা যুক্তিসংগত, তা বছরের পর বছর চলা, দশজনের পড়া কোডে উল্টো ফল দেয়। ভুলে যাওয়ার মতো অভ্যাস: এক অক্ষরের নাম (a, dp, g) — ছয় মাস পরে আপনি নিজেও বুঝবেন না। global array আর global অবস্থা — কে কখন বদলাল, তা ট্র্যাক করা কঠিন, আর thread (পর্ব ৪৫) এলে data race. #define int long long-এর মতো macro — ভাষার মৌলিক নিয়ম বদলে দেয়, পাঠককে বিভ্রান্ত করে। bits/stdc++.h আর using namespace std; — বহনযোগ্যতা আর নামের নিরাপত্তা নষ্ট। ইনপুট সবসময় বৈধ ধরা — বাস্তবে ইনপুট আসে ব্যবহারকারী, নেটওয়ার্ক আর আক্রমণকারীর কাছ থেকে, তাই যাচাই (পর্ব ১৯, ৩৭) বাধ্যতামূলক। আর “কাজ করলেই হলো” — পেশাদার কোডে টেস্ট (পর্ব ৪৮), পরিষ্কার error বার্তা আর পর্যালোচনাযোগ্যতা সমান জরুরি।

আরেকটি সূক্ষ্ম অভ্যাস: অপ্রয়োজনীয় micro-optimization. কনটেস্টে সময়সীমা পেরোতে কখনো এক-আধটি কৌশল লাগে; প্রোডাকশনে না মেপে optimize করা (পর্ব ৪৯) প্রায়ই কোডকে জটিল করে, লাভ দেয় না। আর ক্লাস ডিজাইন, interface, মডিউল ভাগ — কনটেস্টে যার কোনো দরকার নেই — বড় সিস্টেমে সেটিই মূল কাজ (পর্ব ২৭, ৫১)।

সাথে রাখার মতো অভ্যাস কিন্তু অনেক, আর সেগুলো খুব দামি। কোড লেখার আগে complexity হিসাব — পেশাদার জগতে এটিই আলাদা করে “এক লাখ ব্যবহারকারীতে ধসে পড়া” সিস্টেমকে টিকে থাকা সিস্টেম থেকে। edge case নিয়ে ভাবা: খালি তালিকা, একটি উপাদান, সর্বোচ্চ মান। overflow-এর সচেতনতা — বাস্তবে এর দাম হতে পারে টাকার হিসাবে ভুল বা নিরাপত্তার ছিদ্র। STL-এ দক্ষতা — কোন container, কোন algorithm, তার খরচ। আর stress testing-এর মানসিকতা: “আমার কোড ঠিক” ধরে না নিয়ে একটি সরল বিকল্পের সাথে হাজারো এলোমেলো ইনপুটে মেলানো — পেশাদার জগতে এর নাম differential testing বা property-based testing, আর fuzzing (পর্ব ৪৩)-এর ভাবনাও কাছাকাছি।

বাস্তব পরামর্শ: দুটি আলাদা “মোড” রাখুন। কনটেস্টের ফাইলে কনটেস্টের নিয়ম, প্রজেক্টের ফাইলে প্রজেক্টের নিয়ম — আর একটির অভ্যাস যেন অন্যটিতে ঢুকে না পড়ে, তার জন্য প্রজেক্টে clang-tidy আর -Wall -Wextra -Werror-এর মতো টুল চালান।

মূল উপলব্ধি: কনটেস্ট শেখায় কীভাবে সমস্যা ভাবতে হয়; প্রোডাকশন শেখায় কীভাবে সেই ভাবনা অন্যের জন্য টেকসই করতে হয়। প্রথমটির দক্ষতা রাখুন, প্রথমটির শর্টকাটগুলো নয়।

প্র ০৪Antti Laaksonen-এর “Competitive Programmer’s Handbook” আর cp-algorithms ওয়েবসাইট কনটেস্ট শেখার প্রায় আদর্শ পাঠ্য হয়ে উঠেছে। এগুলো কী, আর কীভাবে ব্যবহার করলে সবচেয়ে বেশি লাভ?

ফিনল্যান্ডের Antti Laaksonen দীর্ঘদিন কনটেস্ট প্রোগ্রামিং শিখিয়েছেন আর সমস্যা লিখেছেন। তাঁর “Competitive Programmer’s Handbook” একটি বিনামূল্যের PDF বই, যা মৌলিক বিষয় — complexity, sorting, binary search, greedy, DP — থেকে শুরু করে গ্রাফ, tree-র প্রশ্ন, segment tree, string algorithm আর গণিত পর্যন্ত যায়। এর বিশেষত্ব হলো সংক্ষিপ্ততা আর স্পষ্টতা: প্রতিটি ধারণা অল্প কথায়, ছোট উদাহরণসহ, আর কোড C++-এ, STL ব্যবহার করে। এই পর্বের constraints-থেকে-complexity টেবিলের মতো টেবিল এই বইয়েও আছে। বইটির একটি বর্ধিত সংস্করণ পরে Springer থেকে “Guide to Competitive Programming” নামে ছাপা বই হিসেবে বেরিয়েছে।

বইটির সঙ্গী হলো CSES Problem Set — বিষয় ধরে সাজানো কয়েকশো ক্লাসিক সমস্যা, সহজ থেকে কঠিন। Range sum queries, শুধু ভাবনা বদলে একই সমস্যার কঠিনতর সংস্করণ, গ্রিডে BFS — এই পর্বের অনেক উদাহরণের ধরন সেখানে পাবেন। বই পড়ে সাথে সাথে সেই অধ্যায়ের সমস্যা করা — এই জুটি অনেকের কাছে সবচেয়ে কার্যকর পথ।

cp-algorithms (cp-algorithms.com) একটি উন্মুক্ত, সম্প্রদায়-চালিত ওয়েবসাইট। এর শুরু রুশ ভাষার বিখ্যাত সাইট e-maxx-এর ইংরেজি অনুবাদ হিসেবে, পরে স্বেচ্ছাসেবীরা GitHub-এ মিলে এটি বাড়িয়েছেন আর সংশোধন করছেন। এটি পাঠ্যবইয়ের চেয়ে বেশি একটি রেফারেন্স: নির্দিষ্ট algorithm — Dijkstra, sieve-এর রূপভেদ, string hashing, flow, number theory — গভীর ব্যাখ্যা, প্রমাণ আর বাস্তবায়নসহ। সাথে প্রায়ই অনুশীলনের সমস্যার তালিকা থাকে। আরও জনপ্রিয় উৎসের মধ্যে আছে Steven Halim আর Felix Halim-এর “Competitive Programming” বইয়ের সিরিজ আর USACO Guide.

সবচেয়ে বড় ভুল হলো এগুলো উপন্যাসের মতো পড়া। পড়ে বোঝা আর সমস্যায় প্রয়োগ করা আলাদা দক্ষতা। ভালো পদ্ধতি: একটি অধ্যায় পড়ুন, বই বন্ধ করে algorithm-টি নিজে লিখুন, তারপর অন্তত পাঁচ-ছয়টি সমস্যা করুন যেখানে সেটি লাগে — বিশেষত এমন সমস্যা, যেখানে প্রথমে বোঝাই যায় না যে এটি সেই ছাঁচ। cp-algorithms-কে ব্যবহার করুন দরকারের সময়: একটি সমস্যায় আটকে editorial-এ নতুন algorithm-এর নাম পেলে সেখানে গিয়ে সেটি গভীরভাবে শিখুন। আর নিজের ভাষায় নোট রাখুন — বাংলায় হলেও।

মূল উপলব্ধি: ভালো উৎস আপনাকে পথ দেখায়, কিন্তু পথ হাঁটতে হয় নিজের সমাধান করা সমস্যা দিয়ে। পড়া আর অনুশীলনের অনুপাত রাখুন অনুশীলনের দিকে ভারী।

প্র ০৫কনটেস্টে ভালো করা কি চাকরিতে ভালো করার পূর্বাভাস দেয়? প্রতিষ্ঠানগুলো এই দক্ষতা কতটা মূল্য দেয়, আর এর সীমা কোথায়?

একদিক থেকে উত্তর “হ্যাঁ, কিছুটা”। কনটেস্ট নিখুঁতভাবে মাপে কয়েকটি দক্ষতা: সমস্যাকে পরিচিত ছাঁচে ভাঙা, complexity-র বোধ, edge case নিয়ে ভাবা, চাপের মধ্যে নির্ভুল কোড লেখা, আর নিজের ভুল দ্রুত খুঁজে পাওয়া। এগুলো আসল সফটওয়্যারেও লাগে — বিশেষত পারফরম্যান্স-সংবেদনশীল জায়গায়: search engine, compiler, database, গেম ইঞ্জিন, trading system. এ কারণেই বহু বড় প্রযুক্তি প্রতিষ্ঠান কনটেস্টের পৃষ্ঠপোষকতা করেছে, আর তাদের ইন্টারভিউর একটি বড় অংশ LeetCode-ধাঁচের algorithm সমস্যা (পর্ব ৫৫)। বাংলাদেশেও অনেক সফটওয়্যার প্রতিষ্ঠান নিয়োগে কনটেস্টের অভিজ্ঞতাকে ইতিবাচকভাবে দেখে, আর নিজেরাও প্রোগ্রামিং কনটেস্ট আয়োজন করে।

অন্যদিক থেকে উত্তর “পুরোটা নয়”। পেশাদার কাজের বড় অংশ কনটেস্ট মাপেই না: অস্পষ্ট চাহিদা থেকে স্পষ্ট সমস্যা বানানো (কনটেস্টে সমস্যা আগে থেকেই নিখুঁত), লাখো লাইনের পুরোনো কোড পড়া আর নিরাপদে বদলানো, ডিজাইনের trade-off, অন্যের সাথে লিখিত ও মৌখিক যোগাযোগ, code review, আর মাসের পর মাস একটি প্রজেক্টে লেগে থাকা। একটি পরিচিত গল্প আছে: Google-এর Peter Norvig একবার একটি আলোচনায় বলেছিলেন, Google-এর নিজের তথ্যে নিয়োগ পাওয়া কর্মীদের মধ্যে কনটেস্টে ভালো করা আর কাজের মূল্যায়নের সম্পর্ক কিছুটা উল্টো দেখা গিয়েছিল। এটিকে “কনটেস্ট খারাপ” বলে পড়া ভুল — ব্যাখ্যার একটি অংশ হলো নির্বাচনের প্রভাব: যাঁরা ইতিমধ্যে নিয়োগ পেয়েছেন, তাঁদের মধ্যে কনটেস্টে ভালোরা হয়তো সেই দক্ষতার জোরে অন্য দুর্বলতা নিয়েও পার পেয়েছিলেন। শিক্ষাটি হলো: একটি দক্ষতা দিয়ে পুরো মানুষকে মাপা যায় না।

AI-এর যুগে প্রশ্নটি আরও জটিল। AI সিস্টেম এখন অনেক কনটেস্ট-সমস্যা ভালোভাবে সমাধান করতে পারে, তাই “algorithm জানা”-র বাজারমূল্য কিছুটা বদলাচ্ছে, আর অনেক প্রতিষ্ঠান ইন্টারভিউর ধরন নিয়ে নতুন করে ভাবছে। কিন্তু AI-এর কোড যাচাই করতে — এটি TLE খাবে কি না, overflow আছে কি না, greedy-টি আদৌ ঠিক কি না — ঠিক সেই দক্ষতাই লাগে, যা কনটেস্ট গড়ে তোলে।

ভারসাম্যপূর্ণ পথ: কনটেস্ট করুন algorithm-এর ভিত আর চাপের মধ্যে চিন্তার জন্য; সাথে অন্তত একটি সত্যিকারের প্রজেক্ট (পর্ব ৫১), open source-এ অবদান বা দলগত কাজ করুন বাকি দক্ষতার জন্য। শুধু rating দিয়ে নিজের মূল্য মাপবেন না, rating-কে অবহেলাও করবেন না।

মূল উপলব্ধি: কনটেস্টের দক্ষতা প্রয়োজনীয় ভিত্তির একটি শক্ত অংশ, কিন্তু পুরো দালান নয় — ভালো প্রকৌশলী সেই ভিত্তির ওপর ডিজাইন, যোগাযোগ আর রক্ষণাবেক্ষণের দক্ষতা গড়েন।

অনুশীলন

বেশিরভাগ কাজে কোড লিখতে হবে না — আগে অনুমান করুন, তারপর উত্তর মিলিয়ে নিন।

  1. constraints পড়ুন: প্রতিটি ক্ষেত্রে (সময়সীমা ১ সেকেন্ড) কোন complexity লক্ষ্য করবেন? (ক) n ≤ ১৮, সব উপসেট নিয়ে প্রশ্ন (খ) n ≤ ৩০০০, প্রতিটি জোড়া নিয়ে হিসাব (গ) n ≤ ১০৬ (ঘ) n ≤ ১০১৮, একটি সংখ্যা।

    (ক) O(2n·n) — ২১৮·১৮ ≈ ৪.৭·১০৬, স্বচ্ছন্দে চলে; bitmask দিয়ে সব উপসেট। (খ) O(n²) — ৯·১০৬, চলে; O(n³) হলে ২.৭·১০১০, চলবে না। (গ) O(n) বা O(n log n) — ১০৬·২০ = ২·১০৭, চলে; সাথে fast I/O অবশ্যই। (ঘ) O(log n) বা O(√n)-এর কাছাকাছি — n পর্যন্ত লুপ অসম্ভব; সূত্র, binary search বা গাণিতিক কৌশল। আর মান long long-এ।

  2. বাগ শিকার: এই কোড প্রথম test case-এ ঠিক উত্তর দেয়, কিন্তু জমা দিলে WA. কয়টি সমস্যা পান? (সংখ্যাগুলো ১০৯ পর্যন্ত, n ≤ ২·১০৫.)
    int cnt[200005];                      // global
    int main() {
        int t; cin >> t;
        while (t--) {
            int n; cin >> n;
            vector<int> a(n);
            for (auto& x : a) { cin >> x; if (x <= n) cnt[x]++; }
            int sum = accumulate(a.begin(), a.end(), 0);
            cout << sum << ' ' << cnt[1] << endl;
        }
    }

    তিনটি WA-র কারণ, একটি TLE-র ঝুঁকি। (১) cnt global, আর কোনো test case-এর পরে পরিষ্কার হয় না — দ্বিতীয় test case-এ প্রথমটির গণনাও যোগ হয়। প্রতিকার: vector<int> cnt(n + 1) লুপের ভেতরে। (২) accumulate(..., 0) — যোগফল int-এ, ২·১০১৪ পর্যন্ত যেতে পারে: overflow. শুরুর মান 0LL, আর sum হবে long long. (৩) ঋণাত্মক x আসলে? এখানে x ধনাত্মক ধরা হয়েছে; যদি constraints-এ x ≥ ১ না থাকে, cnt[x] ঋণাত্মক index-এ লিখবে — UB. (৪) fast I/O নেই, আর endl প্রতি লাইনে flush — বড় ইনপুটে TLE-র ঝুঁকি।

  3. টুল বাছুন: প্রতিটি কাজের জন্য সবচেয়ে উপযুক্ত STL টুল বলুন: (ক) একটি চলমান সংখ্যার সংগ্রহ, বারবার “x-এর সমান বা বড় সবচেয়ে ছোট সংখ্যা কোনটি?” (খ) Codeforces-এ ১০৫টি বড় সংখ্যার কোনটি কতবার এসেছে গোনা (গ) বারবার সবচেয়ে ছোট কাজটি বের করে তার জায়গায় নতুন কাজ ঢোকানো (ঘ) ৮ জনের বসার সব সম্ভাব্য ক্রম পরীক্ষা।

    (ক) std::set (বা একই মান বারবার এলে multiset), আর তার নিজের s.lower_bound(x) — std::lower_bound নয়, কারণ সেটি set-এ O(n). (খ) std::map<long long, int> — নিশ্চিত O(log n), hack-এর ভয় নেই; বা custom hash-সহ unordered_map. (গ) std::priority_queue with std::greater<> — min-heap. (ঘ) আগে sort, তারপর next_permutation-এর do-while — ৮! = ৪০৩২০টি ক্রম, স্বচ্ছন্দে চলে।

  4. নিজে লিখুন (কোড): উপরের range_sum.cpp বদলে প্রতিটি প্রশ্নে l থেকে r-এর মধ্যে কয়টি জোড় সংখ্যা আছে, তা ছাপান — প্রতিটি প্রশ্ন O(1)-এ। প্রথম test case-এর তিনটি প্রশ্নের উত্তর আগে অনুমান করুন।

    শুধু একটি লাইন বদলায়: pre[i] = pre[i - 1] + (x % 2 == 0 ? 1 : 0); — যোগফলের বদলে “জোড় হলে ১”-এর prefix sum. এটিই prefix sum-এর আসল শক্তি: যেকোনো “গোনা”-কে যোগফলে বদলানো যায়। x % 2 == 0 ঋণাত্মক সংখ্যাতেও ঠিক কাজ করে (−২ % ২ = ০)। a = 4 −2 7 1 5-এ জোড় হলো 4 আর −2, তাই উত্তর: ১–৩ → 2, ২–৫ → 1, ৪–৪ → 0. দ্বিতীয় test case-এ তিনটি ১০৯-ই জোড়, তাই 3. এখানে int-ই যথেষ্ট হতো, কারণ গোনা n-এর বেশি হয় না — টাইপ বাছাই সবসময় সর্বোচ্চ মান দেখে।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমএকটি কনটেস্ট সমস্যা সমাধানের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. সমস্যা আর constraints পড়ে n-এর সর্বোচ্চ মান ও সংখ্যার আকার দেখা
  2. constraints থেকে প্রয়োজনীয় complexity আর ডেটার টাইপ ঠিক করা
  3. নমুনা ইনপুটে ধারণাটি হাতে চালিয়ে মিলিয়ে দেখা
  4. কোড লেখা, তারপর নমুনা আর নিজের edge case-এ চালানো
  5. জমা দেওয়া; WA হলে brute force দিয়ে stress test

একটি কনটেস্ট সমস্যায় n ≤ ২·১০৫ আর সময়সীমা ১ সেকেন্ড। কোন complexity-র সমাধান নিরাপদে চলবে বলে আশা করা যায়?

  • O(n³)
  • O(n²)
  • O(n log n)
  • O(2n)

C++-এ std::cin.tie(nullptr) লিখলে কী বদলায়?

  • cin আর scanf-এর মেলানো বন্ধ হয়
  • প্রতিবার পড়ার আগে cout-এর স্বয়ংক্রিয় flush বন্ধ হয়
  • cin থেকে আর কিছু পড়া যায় না
  • endl আর flush করে না

C++-এ std::vector<int> v-তে ২·১০৫টি সংখ্যা, প্রতিটি ১০৯ পর্যন্ত। মোট যোগফলের জন্য কোনটি সঠিক?

  • std::accumulate(v.begin(), v.end(), 0)
  • long long s = std::accumulate(v.begin(), v.end(), 0);
  • std::accumulate(v.begin(), v.end(), 0.0)
  • std::accumulate(v.begin(), v.end(), 0LL)

C++-এ std::priority_queue<int> pq;-এ 5, 1, 8, 3 ঢোকালে pq.top() কত?

  • 8
  • 1
  • 5
  • 3

C++-এ একটি std::set<int> s-এ x-এর সমান বা বড় প্রথম উপাদান দ্রুত খুঁজতে কোনটি সবচেয়ে ভালো?

  • std::lower_bound(s.begin(), s.end(), x)
  • s.find(x)
  • s.lower_bound(x)
  • std::find(s.begin(), s.end(), x)

একাধিক test case-এর একটি কনটেস্ট প্রোগ্রাম প্রথম test case-এ ঠিক উত্তর দেয়, কিন্তু পরেরগুলোতে ভুল। সবচেয়ে সম্ভাব্য কারণ কোনটি?

  • fast I/O-র দুই লাইন নেই
  • আগের test case-এর অবস্থা (global array/counter) পরিষ্কার করা হয়নি
  • endl-এর বদলে '\n' লেখা হয়েছে
  • bits/stdc++.h ব্যবহার করা হয়েছে

Codeforces-এর মতো প্ল্যাটফর্মে std::unordered_map<int, int> ব্যবহার করা একটি সমাধান hack-এর পরে TLE পেল। সবচেয়ে সম্ভাব্য কারণ কী?

  • int চাবি unordered_map-এ রাখা যায় না
  • unordered_map সবসময় map-এর চেয়ে বেশি মেমরি নেয় বলে
  • unordered_map-এর প্রতিটি কাজ সবসময় O(log n)
  • ইচ্ছাকৃত collision-এর anti-hash ইনপুটে প্রতিটি কাজ O(n) হয়ে গেছে

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. আগে constraints, পরে কোড: n-এর সর্বোচ্চ মান বলে দেয় complexity (১ সেকেন্ড ≈ ১০৮ সরল অপারেশন), সংখ্যার সীমা বলে দেয় টাইপ।
  2. টেমপ্লেটের তিন রক্ষাকবচ: fast I/O আর '\n', long long (হিসাবটিও ll-এ), আর প্রতিটি test case-এ নতুন অবস্থা।
  3. STL-এর ফাঁদ চিনুন: 0LL, set-এ s.lower_bound, anti-hash-এর ভয়ে map বা custom hash, max-heap ডিফল্ট।
  4. ছাঁচ চিনুন: prefix sum, two pointers, binary search on answer, greedy (প্রমাণসহ), BFS, DP, sieve — আর WA হলে brute force দিয়ে stress test.
  5. দুই জগৎ আলাদা রাখুন: কনটেস্টের শর্টকাট প্রোডাকশনে নয়; plagiarism নয়; আর নিয়ম নিষেধ করলে কনটেস্টে AI নয় — AI কনটেস্টের পরে, শেখার জন্য।

পরের পর্বে: কনটেস্টের এক-ফাইলের দ্রুত কোড থেকে এবার উল্টো দিকে — কোর্সের ক্যাপস্টোন। এ পর্যন্ত শেখা সব কিছু — class, RAII, STL, error handling, CMake, টেস্ট — মিলিয়ে একটি আসল, কয়েক ফাইলের C++ সিস্টেম, যেটি অন্য কেউ পড়ে বুঝতে আর বদলাতে পারবে।

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

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান