এই পর্বে যা শিখবেন
- ICPC-ধাঁচের দলগত কনটেস্ট আর ব্যক্তিগত অনলাইন রাউন্ড কীভাবে চলে, র্যাংক আর penalty কীভাবে হিসাব হয়
- judge-এর প্রতিটি রায় (AC, WA, TLE, MLE, RE, CE) দেখে কোথায় ভুল খুঁজবেন
- সমস্যার সীমা দেখে কোড লেখার আগেই লক্ষ্য complexity ঠিক করা, আর দ্রুত ইনপুট-আউটপুটসহ একটি পরিষ্কার C++ template
- কোর্সের শেখার আটটি মডিউলের কৌশল — গণিত, greedy, binary search, prefix sum, BFS, DSU, Dijkstra, DP, segment tree, bitmask DP — আসল কনটেস্টের ছাঁচে চিনে প্রয়োগ করা
- দলের কৌশল, upsolving, virtual contest আর stress testing দিয়ে নিয়মিত উন্নতির একটি পরিকল্পনা
১ · কনটেস্টের দুনিয়া — দলগত ICPC আর ব্যক্তিগত অনলাইন রাউন্ড
Competitive programming মানে নির্দিষ্ট সময়ের মধ্যে কয়েকটি অ্যালগরিদম-নির্ভর সমস্যা সঠিক আর যথেষ্ট দ্রুতভাবে সমাধান করা। আপনি কোড জমা দেন, আর একটি online judge সেটি গোপন টেস্ট ইনপুটে চালিয়ে রায় দেয়। এখানে কেউ আপনার কোডের সৌন্দর্য দেখে না, দেখে শুধু তিনটি জিনিস: উত্তর ঠিক কি না, সময়ের ভেতরে কি না, আর মেমরির ভেতরে কি না। পুরো কোর্সে যা শিখেছেন — complexity, ডেটা স্ট্রাকচার, প্রমাণ — এখানে সবই চাপের মধ্যে পরীক্ষা হয়।
ICPC-ধাঁচের দলগত কনটেস্ট
ICPC (International Collegiate Programming Contest) বিশ্ববিদ্যালয়ের শিক্ষার্থীদের সবচেয়ে পরিচিত দলগত প্রোগ্রামিং প্রতিযোগিতা। বাংলাদেশে এর আঞ্চলিক পর্বের পাশাপাশি NCPC, IUPC-র মতো অনেক কনটেস্ট একই ধাঁচে হয়। নিয়ম জায়গাভেদে একটু আলাদা হতে পারে, তবে মূল কাঠামো সাধারণত এমন:
- তিনজনের দল, একটি কম্পিউটার। তিনজন মাথা কিন্তু একটি কিবোর্ড। তাই কে কখন টাইপ করবে, কে কাগজে ভাববে — এটিই দলের সবচেয়ে বড় সিদ্ধান্ত (অংশ ৭)।
- সাধারণত পাঁচ ঘণ্টা, দশ-বারোটির মতো সমস্যা। সমস্যাগুলো A, B, C… নামে থাকে, কিন্তু কঠিনতার ক্রমে সাজানো থাকে এমন কোনো নিশ্চয়তা নেই।
- র্যাংক প্রথমে সমাধান করা সমস্যার সংখ্যায়। সমান হলে কম মোট penalty time যার, সে এগিয়ে।
- Penalty-র হিসাব: প্রতিটি সমাধান করা সমস্যার জন্য কনটেস্ট শুরু থেকে সেটি accepted হওয়া পর্যন্ত মিনিট, আর সেই সমস্যায় আগের প্রতিটি ভুল জমার জন্য বাড়তি সময় (প্রচলিত নিয়মে ২০ মিনিট)। যে সমস্যা শেষ পর্যন্ত সমাধান হয়নি, তার ভুল জমা কোনো penalty যোগ করে না।
- Scoreboard freeze: শেষ অংশে (প্রায়ই শেষ এক ঘণ্টা) স্কোরবোর্ড থেমে যায়। আপনি নিজের রায় জানেন, অন্যদের নতুন সমাধান জানেন না। চূড়ান্ত ফল পরে একসাথে প্রকাশ হয় — এজন্য শেষ ঘণ্টায় “আমরা কোথায় আছি” না ভেবে যত বেশি সম্ভব সমাধানের দিকে মন দিতে হয়।
- ইন্টারনেট ছাড়া। on-site কনটেস্টে সাধারণত ইন্টারনেট বা AI ব্যবহারের সুযোগ নেই; অনেক জায়গায় ছাপানো কাগজের নোট (team notebook) নেওয়া যায়, আর কোড প্রিন্ট করার ব্যবস্থা থাকে। কোন জিনিস নেওয়া যাবে, তা প্রতিটি কনটেস্টের নিয়মে লেখা থাকে — আগে পড়ে নিন।
একটি উপমা: ICPC-র দল যেন একটি ছোট রেস্তোরাঁর রান্নাঘর, যেখানে চুলা মাত্র একটি। তিনজন রাঁধুনি — একজন চুলায়, দুজন কাটাকুটি আর মসলা তৈরিতে। চুলা কখনো খালি থাকা চলবে না, আবার কাঁচা রেসিপি নিয়ে চুলা দখল করে রাখাও চলবে না। কোড কাগজে প্রায় তৈরি হলে তবেই কম্পিউটারে বসা — এই নিয়মটিই ভালো দলগুলোকে আলাদা করে।
ব্যক্তিগত অনলাইন রাউন্ড
Codeforces, AtCoder বা বাংলাদেশি প্ল্যাটফর্ম Toph-এর মতো জায়গায় নিয়মিত অনলাইন কনটেস্ট হয়। এগুলো সাধারণত ব্যক্তিগত, দুই-আড়াই ঘণ্টার, আর বাসা থেকেই দেওয়া যায়। প্ল্যাটফর্মভেদে নিয়ম আলাদা — কোথাও ICPC-র মতো penalty, কোথাও সময়ের সাথে কমতে থাকা নম্বর, কোথাও কনটেস্ট চলাকালে শুধু কয়েকটি প্রাথমিক টেস্ট (pretest) আর শেষে পুরো টেস্ট। তাই প্রতিটি প্ল্যাটফর্মের নিয়মের পাতা একবার পড়ে নিন।
বেশিরভাগ প্ল্যাটফর্মে প্রতিটি রেটেড কনটেস্টের পর একটি rating বাড়ে বা কমে — প্রত্যাশার চেয়ে ভালো করলে বাড়ে। রেটিং দক্ষতার একটি মোটা দাগের মাপ, কিন্তু একদিনের খারাপ কনটেস্ট নিয়ে মন খারাপ করার কিছু নেই; দীর্ঘ সময়ের প্রবণতাই আসল। অনলাইন রাউন্ডের সবচেয়ে বড় উপহার হলো কনটেস্টের পরের editorial — সমস্যা-নির্মাতাদের লেখা সমাধান-ব্যাখ্যা। অংশ ৮-এ দেখব, শেখা আসলে ঘটে কনটেস্টের পরে।
| দিক | ICPC-ধাঁচ (দলগত) | অনলাইন রাউন্ড (ব্যক্তিগত) |
|---|---|---|
| মানুষ ও কম্পিউটার | ৩ জন, ১টি কম্পিউটার | ১ জন, নিজের কম্পিউটার |
| সময় ও সমস্যা | সাধারণত ৫ ঘণ্টা, ১০–১৩টি | সাধারণত ২–৩ ঘণ্টা, ৪–৮টি |
| র্যাংক | সমাধানের সংখ্যা, তারপর penalty | প্ল্যাটফর্মভেদে: penalty বা সময়ের সাথে কমা নম্বর |
| টেস্টের ফল | প্রতিটি জমায় পুরো রায় | অনেক সময় চলাকালে শুধু pretest |
| সবচেয়ে জরুরি দক্ষতা | দলের কাজ ভাগ, কম্পিউটারের সময় ব্যবস্থাপনা | দ্রুত পড়া, নির্ভুল কোড, নিজের ভুল নিজে ধরা |
২ · Judge-এর রায় — AC, WA, TLE, MLE, RE, CE মানে কী
প্রতিটি জমার পর judge একটি verdict দেয়। সাধারণত টেস্টগুলো ক্রমে চলে আর প্রথম যে টেস্টে সমস্যা হয়, সেটির রায় দেখানো হয়। তাই “WA on test 1” আর “WA on test 37” খুব আলাদা খবর: প্রথমটি মানে নমুনাতেই ভুল (বা আউটপুটের বিন্যাস ভুল), দ্বিতীয়টি মানে মূল ধারণা প্রায় ঠিক, কিন্তু কোনো বিশেষ ক্ষেত্র বাদ পড়েছে।
| রায় | মানে | সাধারণত কোথায় খুঁজবেন |
|---|---|---|
| AC — Accepted | সব টেস্টে ঠিক উত্তর, সময় ও মেমরির ভেতরে | কিছু না — পরের সমস্যায় যান |
| WA — Wrong Answer | প্রোগ্রাম চলেছে, কিন্তু উত্তর ভুল | overflow, edge case (n = 1, সব সমান), একাধিক টেস্টের মাঝে না-মোছা ডেটা, ভুল greedy, আউটপুটের বিন্যাস |
| TLE — Time Limit Exceeded | সময়সীমা পেরিয়েছে | complexity সীমার তুলনায় বেশি, অসীম লুপ, ধীর I/O, প্রতিটি টেস্টে পুরো বড় অ্যারে মোছা |
| MLE — Memory Limit Exceeded | মেমরির সীমা পেরিয়েছে | n × n টেবিল, অপ্রয়োজনীয় কপি, অসীমভাবে বাড়তে থাকা queue বা vector |
| RE — Runtime Error | চলার মাঝে প্রোগ্রাম ভেঙেছে | অ্যারের সীমার বাইরে, শূন্য দিয়ে ভাগ, গভীর recursion-এ stack overflow, খালি container থেকে pop |
| CE — Compilation Error | judge-এর compiler কোডটি compile করতে পারেনি | ভুল ভাষা বা সংস্করণ বাছাই, লোকাল কম্পিউটারে চলে এমন অপ্রচলিত header বা ফাংশন |
কিছু judge-এ আরও রায় থাকে — যেমন Presentation Error (উত্তর ঠিক, কিন্তু স্পেস বা নতুন লাইনের বিন্যাস ভুল), বা interactive সমস্যায় Idleness Limit. আজকাল বেশিরভাগ judge বাড়তি স্পেস উপেক্ষা করে, তবু বিবরণে যেভাবে চাওয়া হয়েছে ঠিক সেভাবে ছাপানো নিরাপদ। একটি গুরুত্বপূর্ণ সতর্কতা: C++-এ undefined behavior (যেমন সীমার বাইরে লেখা) সবসময় RE দেয় না — কখনো চুপচাপ অন্য ভেরিয়েবল নষ্ট করে WA দেয়। তাই WA পেলে সীমার বাইরের access-ও সন্দেহের তালিকায় রাখুন।
নিচের ডায়াগ্রামে একটি রায় পেলে কোন ক্রমে প্রশ্ন করবেন, তা দেখুন। ক্রমটি গুরুত্বপূর্ণ: আগে সস্তা পরীক্ষা (compile হয় কি না), শেষে দামি পরীক্ষা (ধারণাটাই ভুল কি না)।
- CE
compileভাষা আর header - RE
crashসীমা আর stack - TLE
timecomplexity আর I/O - MLE
memoryটেবিলের আকার - WA
answeredge case আর ধারণা - AC
doneপরের সমস্যা
1. Language: C++ (GNU C++17/20/23?) ঠিক বাছা হয়েছে? 2. লোকাল compiler-এ -Wall দিয়ে compile করেছি? 3. অপ্রচলিত header বা ফাংশন (conio.h, getch) আছে? 4. ভুল ফাইল জমা দিইনি তো?
1. অ্যারের আকার সর্বোচ্চ n-এর চেয়ে ছোট? (a[n] না a[n+1]?) 2. কোথাও শূন্য দিয়ে ভাগ বা mod? 3. recursion-এর গভীরতা 10^5-10^6 হতে পারে? 4. খালি stack/queue থেকে top() বা pop()? 5. main() শেষে return 0?
1. সর্বোচ্চ সীমায় মোট কাজ কত? (10^8-এর বেশি?) 2. fast I/O আছে? endl-এর বদলে '\n'? 3. T টি টেস্টে প্রতিবার পুরো MAX আকারের অ্যারে মুছছি? 4. কোনো লুপ শেষই হয় না এমন ইনপুট আছে? 5. Dijkstra-য় পুরোনো (stale) entry বাদ দিচ্ছি?
1. টেবিলের আকার x প্রতি ঘরের বাইট = কত MB? (5000 x 5000 long long = 200 MB!) 2. DP-তে শুধু আগের সারি লাগে? (rolling array) 3. ফাংশনে বড় vector মান হিসেবে (কপি) পাঠাচ্ছি? 4. BFS-এ একই ঘর বারবার queue-তে ঢুকছে?
1. overflow: int-এ 2.1 x 10^9 পেরোয় এমন গুণ বা যোগ? 2. একাধিক টেস্ট: আগের টেস্টের ডেটা মুছেছি? 3. edge case: n = 1, সব সমান, উত্তর 0 বা -1? 4. আউটপুটের বিন্যাস: প্রতি লাইনে একটি? YES না Yes? 5. তারপরও না মিললে: brute force + stress test (অংশ ৮) 6. শেষে সন্দেহ ধারণাতেই: greedy-র প্রমাণ আছে?
1. দলের খাতায় লিখুন: কোন সমস্যা, কত মিনিটে। 2. স্কোরবোর্ড দেখে পরের সমস্যা বাছুন। 3. কনটেস্টের পরে: কোডটি পরিষ্কার করে নিজের লাইব্রেরিতে রাখুন।
- CE. সবচেয়ে সস্তা ভুল, কিন্তু অনেক দলের মিনিট নষ্ট করে। লোকাল কম্পিউটারে যে কোড চলে, judge-এর compiler সংস্করণে তা না-ও চলতে পারে। অনেক কনটেস্টে CE-র জন্য penalty গোনা হয় না, তবু সময় তো যায়।
- RE. প্রথম সন্দেহ অ্যারের আকার আর সূচক। দ্বিতীয় সন্দেহ গভীর recursion — n = 106 দৈর্ঘ্যের একটি শিকলে recursive DFS প্রায়ই stack ভরিয়ে ফেলে (অংশ ৪)।
- TLE. আগে কাগজে হিসাব করুন: সর্বোচ্চ সীমায় আপনার অ্যালগরিদম কত কাজ করে। হিসাবে ঠিক থাকলে তবেই ছোট খরচগুলো (I/O, অ্যারে মোছা, stale entry) সন্দেহ করুন। হিসাবেই বেশি হলে ছোটখাটো optimization বৃথা — ধারণা বদলান।
- MLE. মেমরিও গোনা যায়: ঘরের সংখ্যা × প্রতি ঘরের বাইট। 256 MB মানে মোটামুটি 6.4 × 107 টি
intবা 3.2 × 107 টিlong long. - WA. সবচেয়ে কঠিন খোঁজ, তাই ক্রমটি মানুন: আগে যান্ত্রিক কারণ (overflow, না-মোছা ডেটা, বিন্যাস), তারপর edge case, তারপর stress test, আর একেবারে শেষে মেনে নিন যে ধারণাটাই ভুল হতে পারে।
- AC. অভিনন্দন — কিন্তু থামবেন না। দলের খাতায় লিখে, স্কোরবোর্ড দেখে পরের সমস্যা। কনটেস্টের পরে কোডটি পরিষ্কার করে নিজের লাইব্রেরিতে রাখলে পরের বার একই ধরনের সমস্যায় সময় বাঁচবে।
৩ · সীমা পড়ে লক্ষ্য ঠিক করা — complexity টেবিল, fast I/O আর C++ template
অভিজ্ঞ প্রতিযোগীরা সমস্যার গল্পের আগে constraints পড়েন। কারণ সীমাই প্রায় বলে দেয় কোন ধরনের সমাধান চাওয়া হচ্ছে। মোটা দাগের নিয়ম: C++ এক সেকেন্ডে মোটামুটি 108-এর ঘরে সরল কাজ করে (ক্যাশ আর ধ্রুবকের ওপর নির্ভর করে কয়েক গুণ কম-বেশি)। Time limit ১–২ সেকেন্ড হলে:
| n-এর সীমা | লক্ষ্য complexity | সাধারণত যে কৌশল | কোর্সে কোথায় |
|---|---|---|---|
| n ≤ 10 | O(n!) বা O(n! · n) | সব permutation, backtracking | পর্ব ৫৬ |
| n ≤ 20 | O(2n · n) | bitmask DP, সব subset, meet in the middle | পর্ব ৫৫ |
| n ≤ 500 | O(n³) | Floyd-Warshall, interval DP | পর্ব ৪৪, পর্ব ৫৪ |
| n ≤ 5,000 | O(n²) | দুই-মাত্রার DP, সব জোড়া | পর্ব ৫৩ |
| n ≤ 106 | O(n log n) বা O(n) | sorting, heap, binary search, segment tree, DSU, prefix sum | মডিউল ৩–৬ |
| n ≤ 1018 | O(log n) বা O(1) | গণিতের সূত্র, binary search, দ্রুত ঘাত | পর্ব ৬২, পর্ব ০৬ |
টেবিলটি উল্টো দিকেও কাজ করে, আর সেটাই আসল কৌশল: n ≤ 18 দেখলেই মাথায় আসা উচিত “সম্ভবত subset বা bitmask”; n ≤ 1018 দেখলে “লুপ চলবে না, সূত্র বা log”. একই সাথে সংখ্যার আকারও দেখুন: মান 109 পর্যন্ত আর সেগুলোর যোগ বা গুণ লাগলে long long. আর মোট ইনপুটের আকার দেখুন — “সব টেস্ট মিলিয়ে n-এর যোগফল ≤ 2 × 105” ধরনের বাক্য বলে দেয়, প্রতিটি টেস্টে O(n log n) নিরাপদ, কিন্তু প্রতিটি টেস্টে 106 আকারের অ্যারে মোছা নয়।
Python-এ সাবধান: একই অ্যালগরিদম Python-এ সাধারণত কয়েক দশ গুণ ধীর। n = 106-এ O(n log n) Python-এ সময়সীমা ছুঁয়ে ফেলতে পারে। অনেক judge Python-এর জন্য বাড়তি সময় দেয় না। তাই কনটেস্টে C++ প্রধান ভাষা রাখা নিরাপদ; Python কাজে লাগে বড় সংখ্যার হিসাবে (নিজে থেকেই overflow নেই) আর দ্রুত brute force লেখায়।
দ্রুত ইনপুট-আউটপুট
লাখ লাখ সংখ্যা পড়তে-লিখতে সময়ের বড় অংশ যেতে পারে। C++-এ fast I/O-এর দুই লাইন — ios::sync_with_stdio(false); (C-এর printf/scanf-এর সাথে সমন্বয় বন্ধ, তাই cin/cout নিজের buffer ব্যবহার করে) আর cin.tie(nullptr); (প্রতিটি cin-এর আগে cout খালি করা বন্ধ)। এর পর একই প্রোগ্রামে cin আর scanf মেশাবেন না। আর endl নয়, '\n' — কারণ endl প্রতিবার buffer খালি করে (flush), দশ লাখ লাইনে যা বড় খরচ। Python-এ লাইন ধরে input()-এর বদলে একবারে sys.stdin.buffer.read().split(), আর আউটপুট একটি "\n".join(...)-এ।
একটি পরিষ্কার C++ contest template
অনেকে কনটেস্টে লম্বা macro-ভরা template ব্যবহার করেন। শুরুতে তার দরকার নেই — ছোট, পড়া যায় এমন template-এ ভুল কম, আর দলের অন্য সদস্যও কোড পড়তে পারেন। নিচের template-টি এই পাতার প্রথম সমস্যার (A) সমাধানসহ; প্রতিটি লাইনের ব্যাখ্যা নিচে।
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
void solve() {
ll n, a, b;
cin >> n >> a >> b;
ll x = a / gcd(a, b); // lcm(a, b) = x * b, which may overflow
ll both = (x > n / b) ? 0 : n / (x * b); // lcm > n: no number <= n has both gifts
cout << n / a + n / b - both << '\n'; // inclusion-exclusion
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t = 1;
cin >> t;
while (t--) solve();
return 0;
}#include <bits/stdc++.h>— GCC-র একটি সুবিধাজনক header, যা প্রায় সব standard header একসাথে আনে। কনটেস্টে প্রচলিত, কারণ judge-এ প্রায় সবসময় GCC. পেশাদার কোডে এটি ব্যবহার করবেন না — এটি standard নয়, আর compile ধীর করে। কিছু অনলাইন compiler-এ<cassert>-এর মতো দু-একটি header আলাদা করে লাগতে পারে।using namespace std;— প্রতিবারstd::লেখা থেকে মুক্তি। কনটেস্টে ঠিক আছে; শুধু নিজের ভেরিয়েবলের নামcount,findবাgcd-এর মতো লাইব্রেরির নামের সাথে মিলে গেলে বিভ্রান্তি হতে পারে।using ll = long long;— একমাত্র “সংক্ষেপ”, কারণlong longবারবার লাগে। অনেকে#define int long longলেখেন; আমরা এড়িয়ে যাই, কারণ এতে সবintচুপচাপ বদলে যায়, মেমরি দ্বিগুণ হয়, আরmain-কেsigned mainলিখতে হয় — নতুনদের জন্য বিভ্রান্তিকর।void solve()— একটি টেস্টের পুরো কাজ এক ফাংশনে। এর প্রধান সুবিধা: ভেরিয়েবলগুলো ফাংশনের ভেতরে লোকাল, তাই প্রতিটি টেস্টে নিজে থেকেই নতুন করে তৈরি হয় — আগের টেস্টের ডেটা পরের টেস্টে ঢোকার ভুল (অংশ ৪) প্রায় অসম্ভব।ios::sync_with_stdio(false); cin.tie(nullptr);— fast I/O, ওপরে ব্যাখ্যা করা হয়েছে।main-এর একেবারে শুরুতে, কোনো পড়া-লেখার আগে।int t = 1; cin >> t;— একাধিক টেস্ট কেস-এর সমস্যায় প্রথমে T. একটিমাত্র টেস্টের সমস্যায় শুধুcin >> t;লাইনটি মুছে দিন —t = 1থেকে যায়, বাকি সব একই।while (t--) solve();— T বারsolve(). শেষেreturn 0;— কিছু judge শূন্য ছাড়া exit code পেলে RE দেয়।
আরেকটি অভ্যাস: লোকাল কম্পিউটারে g++ -std=c++20 -O2 -Wall -Wextra -fsanitize=address,undefined দিয়ে compile করে নমুনা চালান (sanitizer Linux আর macOS-এ সহজে চলে)। Sanitizer সীমার বাইরের access আর signed overflow-এর মতো undefined behavior চালানোর সময় ধরিয়ে দেয় — যে ভুলগুলো judge-এ রহস্যময় WA হয়ে দেখা দিত।
৪ · যে বাগগুলো বারবার ফিরে আসে — overflow, না-মোছা ডেটা, off-by-one আর recursion-এর গভীরতা
কনটেস্টে হারানো বেশিরভাগ মিনিট “কঠিন” ভুলে যায় না, যায় কয়েকটি পরিচিত ভুলে। এগুলোর একটি তালিকা মুখস্থ রাখুন, আর জমা দেওয়ার আগে এক নজর দেখে নিন।
১ · Integer overflow
int ধরে প্রায় ±2.1 × 109. দুটি 105 গুণ করলেই 1010. লুকানো রূপ: long long x = a * b; — যেখানে a, b int; গুণটি আগে int-এ হয়, তারপর রূপান্তর। ঠিক রূপ: 1LL * a * b. সমস্যা A-তে দেখবেন, long long-এও না আঁটলে গুণ এড়িয়ে ভাগ দিয়ে তুলনা করতে হয়।
২ · আগের টেস্টের ডেটা
Global অ্যারে একবারই শূন্য হয়, প্রতিটি টেস্টে নয়। দ্বিতীয় টেস্টে আগের visited, cnt বা adjacency list থেকে গেলে ভুল উত্তর। উল্টো ভুলও আছে: T = 105 টেস্টে প্রতিবার 106 আকারের অ্যারে memset করলে TLE. সমাধান: লোকাল vector, বা শুধু প্রথম n টি ঘর মোছা।
৩ · Off-by-one
১-ভিত্তিক ইনপুট আর ০-ভিত্তিক অ্যারে, < বনাম ≤, অর্ধ-খোলা [l, r) বনাম বন্ধ [l, r] — একটি ঘর এদিক-ওদিক। Off-by-one এড়াতে একটি নিয়ম বেছে নিয়ে পুরো কোডে মানুন, আর binary search-এ লুপ শেষে lo আর hi-এর মানে খাতায় লিখে রাখুন।
৪ · Recursion-এর গভীরতা
প্রতিটি recursive ডাক stack-এ জায়গা নেয়। n = 106-এর শিকলে recursive DFS অনেক সিস্টেমে stack ভরিয়ে stack overflow ঘটায় (RE). Python-এ ডিফল্ট সীমা মাত্র ১০০০টি ডাক। উপায়: নিজের stack দিয়ে iterative DFS, BFS, বা DSU-র find লুপ দিয়ে লেখা।
আরও কয়েকটি পরিচিত মুখ: সমস্যা বলে “উত্তর 109 + 7 দিয়ে mod করে” — আর আপনি বিয়োগের পর ঋণাত্মক মান ছাপালেন ((a - b) % M C++-এ ঋণাত্মক হতে পারে, ঠিক রূপ ((a - b) % M + M) % M); দশমিক সংখ্যা == দিয়ে তুলনা; “কোনো উত্তর না থাকলে -1” অংশটি ভুলে যাওয়া; আর একই সমস্যার দুই অংশে একই ভেরিয়েবলের নাম ব্যবহার। এগুলোর প্রতিটির জন্য একটি ছোট নিয়ম রাখুন, আর অংশ ৮-এর stress test দিয়ে বাকিগুলো ধরুন।
(১) সব নমুনা মিলেছে? (২) সর্বোচ্চ মানে কোথাও overflow? (৩) একাধিক টেস্ট হলে সব কিছু নতুন করে শুরু হয়? (৪) n = 1 বা সবচেয়ে ছোট ইনপুটে চলে? (৫) আউটপুটের বিন্যাস হুবহু? (৬) ডিবাগের বাড়তি প্রিন্ট মুছেছি? শেষেরটি হাসির মনে হলেও বহু দলের WA-র কারণ।
৫ · সমস্যা সেট, প্রথম ভাগ — A থেকে E
এবার আসল কাজ। নিচের দশটি সমস্যা একটি ছোট ICPC-ধাঁচের সেট হিসেবে সাজানো, সহজ থেকে কঠিন ক্রমে, আর প্রতিটি কোর্সের কোনো একটি মডিউলের এক বা একাধিক কৌশল চায়। আসল কনটেস্টে কোনো সমস্যার গায়ে “এটি DSU-র সমস্যা” লেখা থাকে না — তাই নিচের তালিকাটি শুধু পড়া শেষে মেলানোর জন্য; চাইলে না দেখে শুরু করুন।
| সমস্যা | নাম | কঠিনতা | কৌশল (পরে দেখুন) | রিভিশন |
|---|---|---|---|---|
| A | মেট্রো রেলের উপহার | সহজ | গণিত, inclusion-exclusion, overflow | পর্ব ৬২ |
| B | নৌকায় নদী পার | সহজ | greedy + two pointer | পর্ব ৫০, পর্ব ১৬ |
| C | খাতা দেখার ভাগ | সহজ–মাঝারি | উত্তরের ওপর binary search | পর্ব ১৯ |
| D | কোচিং রুমের ভিড় | মাঝারি | difference array + prefix sum | পর্ব ১৭ |
| E | জলাবদ্ধ শহর | মাঝারি | দুটি BFS, multi-source | পর্ব ৪০ |
| F | গ্রামে বিদ্যুতের লাইন | মাঝারি | DSU, union by size | পর্ব ৩৩ |
| G | এক রাস্তায় অর্ধেক ভাড়া | মাঝারি–কঠিন | অবস্থা-গ্রাফে Dijkstra | পর্ব ৪৩ |
| H | ফ্রিল্যান্সারের কাজ বাছাই | মাঝারি–কঠিন | DP + binary search | পর্ব ৫২, পর্ব ৫৩ |
| I | গুদামের সর্বনিম্ন মজুত | কঠিন | segment tree, (min, count) | পর্ব ৩৪ |
| J | ডেলিভারি রাইডারের পথ | কঠিন | bitmask DP (Held-Karp) | পর্ব ৫৫, পর্ব ৫৭ |
প্রতিটি সমস্যার সময়সীমা ১ সেকেন্ড আর মেমরি ২৫৬ MB, অন্য কিছু বলা না থাকলে। গল্পের নাম, জায়গা আর সংখ্যা বোঝার জন্য বানানো।
সমস্যা A · মেট্রো রেলের উপহার (সহজ)
মেট্রো রেলের বার্ষিকী উপলক্ষে একটি স্টেশনে ঘোষণা: দিনের প্রতি a-তম যাত্রী পাবেন একটি স্মারক কার্ড, আর প্রতি b-তম যাত্রী পাবেন একটি ফ্রি টিকিট। অর্থাৎ যাত্রী নম্বর k কার্ড পান যদি a দিয়ে k বিভাজ্য হয়, টিকিট পান যদি b দিয়ে বিভাজ্য হয় (দুটোই পেতে পারেন)। দিনে মোট n জন যাত্রী এলে কতজন অন্তত একটি উপহার পেলেন?
ইনপুট: প্রথম লাইনে T (টেস্টের সংখ্যা)। পরের T লাইনের প্রতিটিতে তিনটি সংখ্যা n, a, b.
আউটপুট: প্রতিটি টেস্টের জন্য এক লাইনে উত্তর।
সীমা: 1 ≤ T ≤ 105; 1 ≤ n, a, b ≤ 1018.
Input
3
10 2 3
1000000000000000000 1000000000000000000 999999999999999999
15 5 5
Output
7
2
3
Aসমাধান-ব্যাখ্যা: inclusion-exclusion আর গুণ না করে overflow এড়ানো
ধারণা. n পর্যন্ত a-এর গুণিতক ⌊n/a⌋ টি, b-এর ⌊n/b⌋ টি। দুটোরই গুণিতক — অর্থাৎ lcm(a, b)-এর গুণিতক — দুবার গোনা হয়েছে, তাই একবার বাদ: উত্তর = ⌊n/a⌋ + ⌊n/b⌋ − ⌊n/lcm⌋. এটি inclusion-exclusion-এর সবচেয়ে ছোট রূপ। প্রথম নমুনায় 5 + 3 − 1 = 7 (2, 3, 4, 6, 8, 9, 10).
ফাঁদ. n ≤ 1018 মানে লুপ চলবে না — সূত্রই একমাত্র পথ। আর lcm = a / gcd(a, b) × b, যা 1036 পর্যন্ত হতে পারে — long long-এর সীমা (প্রায় 9.2 × 1018) অনেক আগেই পার। দ্বিতীয় নমুনাটি ঠিক এই ফাঁদ: a আর b পরপর দুটি সংখ্যা, তাই gcd = 1 আর lcm প্রায় 1036. সমাধান হলো গুণ না করে তুলনা: x = a / g হলে lcm = x · b, আর x · b > n ⇔ x > ⌊n/b⌋ (পূর্ণসংখ্যায় সত্য)। lcm > n হলে দুটোর গুণিতক n-এর মধ্যে একটিও নেই। তৃতীয় নমুনা দেখায় a = b-ও আসতে পারে: 3 + 3 − 3 = 3.
Complexity. প্রতিটি টেস্টে একটি gcd: O(log min(a, b)); মোট O(T log 1018) সময়, O(1) বাড়তি জায়গা। T = 105 বলে fast I/O জরুরি।
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
void solve() {
ll n, a, b;
cin >> n >> a >> b;
ll x = a / gcd(a, b); // lcm(a, b) = x * b, which may overflow
ll both = (x > n / b) ? 0 : n / (x * b); // lcm > n: no number <= n has both gifts
cout << n / a + n / b - both << '\n'; // inclusion-exclusion
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t = 1;
cin >> t;
while (t--) solve();
return 0;
}আউটপুট:
7
2
3
তিনটি নমুনাই মিলেছে। দ্বিতীয় লাইনে লক্ষ করুন: x = 10^18 আর n / b = 1, তাই x > n / b — গুণ ছাড়াই জানা গেল lcm > n. সরাসরি a / g * b লিখলে signed overflow হতো — C++-এ undefined behavior, ফল যেকোনো কিছু হতে পারে। প্রতিটি টেস্ট solve()-এর ভেতরে, তাই কোনো ডেটা পরের টেস্টে যায় না।
import sys
from math import gcd
def main():
data = sys.stdin.buffer.read().split()
t = int(data[0])
out = []
for i in range(t):
n, a, b = (int(x) for x in data[1 + 3 * i: 4 + 3 * i])
lcm = a // gcd(a, b) * b # Python integers never overflow
out.append(n // a + n // b - n // lcm)
print("\n".join(map(str, out)))
main()আউটপুট C++-এর সাথে হুবহু এক:
7
2
3
Python-এর পূর্ণসংখ্যা যত বড় দরকার তত বড় হয়, তাই এখানে সরাসরি lcm গুণ করা নিরাপদ — 1036-ও কোনো সমস্যা নয়। বড় সংখ্যার সমস্যায় এটি Python-এর বড় সুবিধা। তবু পুরো ইনপুট একবারে পড়া হয়েছে, কারণ T = 105 লাইনে আলাদা input() ধীর।
সমস্যা B · নৌকায় নদী পার (সহজ)
বর্ষায় একটি গ্রামের সাঁকো ভেসে গেছে, তাই n জনকে নৌকায় নদী পার করাতে হবে। প্রতিটি নৌকা একবারই যায়, একসাথে সর্বোচ্চ দুজন নিতে পারে, আর দুজনের মোট ওজন C কেজির বেশি হতে পারবে না। i-তম জনের ওজন wi. সবাইকে পার করাতে অন্তত কয়টি নৌকা লাগবে?
ইনপুট: প্রথম লাইনে n আর C. দ্বিতীয় লাইনে n টি সংখ্যা w1 … wn.
আউটপুট: ন্যূনতম নৌকার সংখ্যা।
সীমা: 1 ≤ n ≤ 2 × 105; 1 ≤ wi ≤ C ≤ 109.
Input
5 100
60 40 70 30 90
Output
3
Bসমাধান-ব্যাখ্যা: সবচেয়ে ভারীজনকে সবচেয়ে হালকাজনের সাথে — exchange argument
ধারণা. ওজন সাজান। এখনো তীরে থাকা সবচেয়ে ভারী মানুষটি h-কে দেখুন। তাঁকে কোনো না কোনো নৌকায় যেতেই হবে। সবচেয়ে হালকা মানুষটি l-এর সাথে যদি h-এর ওজন C ছাড়ায়, তাহলে আর কারও সাথেই h যেতে পারবেন না — একা যান। না ছাড়ালে h আর l একসাথে যান। দুই প্রান্তে দুটি pointer, প্রতি ধাপে অন্তত একজন কমে: এটি greedy আর two pointer-এর সমন্বয়। নমুনায় সাজানো ক্রম 30 40 60 70 90: 90 একা (90 + 30 > 100), তারপর 70 + 30, তারপর 60 + 40 — মোট 3.
প্রমাণের খসড়া (exchange argument). ধরুন h + l ≤ C, আর একটি সেরা সমাধান S-এ h আর l একসাথে নেই। দুই ক্ষেত্র: (ক) h একা: l যদি y-এর সাথে থাকে, তাহলে (h), (l, y)-কে (h, l), (y) বানাই — নৌকা একই সংখ্যক; l একা থাকলে দুজনকে এক নৌকায় তুলে একটি নৌকা কমে। (খ) h আছেন y-এর সাথে, l আছেন z-এর সাথে (বা একা): অদলবদল করে (h, l) আর (y, z). বৈধ কি? h + l ≤ C ধরে নেওয়া; আর z ≤ h (h সবচেয়ে ভারী), তাই y + z ≤ y + h ≤ C. দুই ক্ষেত্রেই নৌকা বাড়েনি, আর এখন S-এ h আর l একসাথে। তাই greedy-র প্রথম সিদ্ধান্ত কোনো না কোনো সেরা সমাধানের সাথে মেলে; বাকি মানুষদের ওপর একই যুক্তি (induction) চালালে পুরো greedy-ই সেরা। ∎
Complexity. সাজানো O(n log n), দুই pointer O(n); জায়গা O(n). ওজনের যোগফল 2 × 109 পর্যন্ত হতে পারে, যা int-এর সীমার একটু ওপরে — তাই long long.
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
ll C;
cin >> n >> C;
vector<ll> w(n);
for (auto& x : w) cin >> x;
sort(w.begin(), w.end());
int i = 0, j = n - 1, boats = 0;
while (i <= j) { // j = heaviest person still on the bank
if (i < j && w[i] + w[j] <= C) // the lightest one can go with him
++i;
--j; // the heaviest always leaves now
++boats;
}
cout << boats << '\n';
}আউটপুট:
3
i < j শর্তটি জরুরি: শেষে i = j হলে একজনই বাকি, তাঁকে নিজের সাথে জোড়া যায় না। w এখানে long long, তাই w[i] + w[j] overflow করে না — int হলে C = 109-এর কাছাকাছি দুটি ওজনে যোগফল সীমা পেরিয়ে ঋণাত্মক হয়ে ভুল “≤ C” দেখাত। অংশ ৮-এ এই greedy-কে একটি brute force দিয়ে ২০০০টি এলোমেলো টেস্টে যাচাই করব।
import sys
def main():
data = sys.stdin.buffer.read().split()
n, C = int(data[0]), int(data[1])
w = sorted(map(int, data[2:2 + n]))
i, j, boats = 0, n - 1, 0
while i <= j: # j = heaviest person still on the bank
if i < j and w[i] + w[j] <= C:
i += 1 # the lightest one goes with him
j -= 1
boats += 1
print(boats)
main()আউটপুট C++-এর সাথে হুবহু এক:
3
Python-এ overflow-এর চিন্তা নেই, আর 2 × 105 টি সংখ্যা সাজানো আর একটি লুপ সহজেই সময়ের ভেতরে। ইনপুট পড়া একবারে, যাতে লাইন ভাঙা যেভাবেই থাক (একই লাইনে বা আলাদা লাইনে) কোড কাজ করে।
সমস্যা C · খাতা দেখার ভাগ (সহজ–মাঝারি)
একটি বোর্ডের পরীক্ষার খাতা n টি বান্ডিলে সারি করে সাজানো, i-তম বান্ডিলে ai টি খাতা। k জন পরীক্ষক কাজ ভাগ করবেন। নিয়ম: প্রত্যেকে সারির পাশাপাশি কয়েকটি বান্ডিল নেবেন (ক্রম বদলানো যাবে না, বান্ডিল ভাঙা যাবে না), আর সব বান্ডিল কাউকে না কাউকে দিতে হবে। সবাই একসাথে কাজ শুরু করেন, তাই কাজ শেষ হয় সবচেয়ে বেশি খাতা পাওয়া পরীক্ষকের সময়ে। সবচেয়ে বেশি খাতার সংখ্যাটি যতটা সম্ভব কম রাখুন — সেই সংখ্যাটি ছাপান।
ইনপুট: প্রথম লাইনে n আর k. দ্বিতীয় লাইনে a1 … an.
আউটপুট: ন্যূনতম সম্ভাব্য “সর্বোচ্চ ভার”.
সীমা: 1 ≤ k ≤ n ≤ 2 × 105; 1 ≤ ai ≤ 109.
Input
7 3
10 20 30 40 50 60 70
Output
110
Cসমাধান-ব্যাখ্যা: “সবচেয়ে কম কত” থেকে “X কি যথেষ্ট?” — উত্তরের ওপর binary search
পর্যবেক্ষণ. সরাসরি “সেরা ভাগ” খোঁজা কঠিন, কিন্তু উল্টো প্রশ্নটি সহজ: কেউ X-এর বেশি খাতা পাবেন না — এই শর্তে কতজন পরীক্ষক লাগে? বাঁ থেকে বান্ডিল নিতে থাকুন, যতক্ষণ যোগফল X ছাড়ায় না; ছাড়ালে নতুন পরীক্ষক। এই greedy need(X) সবচেয়ে কম পরীক্ষক দেয় (প্রতিটি পরীক্ষককে যতটা সম্ভব বেশি দেওয়া কখনো ক্ষতি করে না — “greedy stays ahead”: প্রতিটি ধাপে greedy যত বান্ডিল ঢেকেছে, যেকোনো বৈধ ভাগ ততটি পরীক্ষকে তার চেয়ে বেশি ঢাকতে পারে না)।
একমুখিতা. X বাড়লে need(X) কখনো বাড়ে না। তাই “need(X) ≤ k” একটি monotonic predicate: ছোট X-এ মিথ্যা, একটি সীমার পর থেকে সবসময় সত্য। সেই সীমাটিই উত্তর, আর সেটি খোঁজার উপায় পর্ব ১৯-এর উত্তরের ওপর binary search. নিচের সীমা max(a) (সবচেয়ে বড় বান্ডিল কাউকে তো নিতেই হবে), ওপরের সীমা মোট যোগফল (একজনই সব নিলে)।
| lo, hi | |
| mid | |
| greedy ভাগ | |
| need(mid) | |
| সিদ্ধান্ত |
- শুরু. lo = 70 (সবচেয়ে বড় বান্ডিল), hi = 280 (মোট)। mid = 175-এ মাত্র দুজন লাগে — তিনজন তো যথেষ্টই। উত্তর 175 বা তার কম, তাই hi = 175.
- mid = 122. ঠিক তিনজন: 100, 110, 70. এখনো সম্ভব, hi = 122. লক্ষ করুন, mid নিজেই সম্ভাব্য উত্তর হতে পারে, তাই hi = mid, mid − 1 নয়।
- mid = 96. চারজন লাগে — 40-কে প্রথম দলে রাখলে 100 > 96. অসম্ভব, তাই উত্তর 96-এর বেশি: lo = 97.
- mid = 109. আবার চারজন: 50 + 60 = 110 > 109, তাই 50 আর 60 আলাদা হয়ে যায়। lo = 110 — নিচের সীমা এখানেই উত্তরে পৌঁছে গেল; পরের ধাপগুলো শুধু hi নামিয়ে আনে।
- mid = 116. তিনজন যথেষ্ট, hi = 116.
- mid = 113. তিনজন, hi = 113.
- mid = 111. তিনজন, hi = 111.
- mid = 110. তিনজন — 50 + 60 ঠিক 110, সীমা ছোঁয় কিন্তু ছাড়ায় না। hi = 110.
- শেষ. lo = hi = 110. আটটি পরীক্ষাতেই উত্তর মিলল; মোট যোগফল S হলে পরীক্ষা লাগে প্রায় log₂ S বার — 2 × 1014-এও পঞ্চাশের কম।
Complexity. প্রতিটি need() O(n), পরীক্ষা O(log S) বার, S = Σa ≤ 2 × 1014: মোট O(n log S) ≈ 2 × 105 × 48 ≈ 107 — নিরাপদ। জায়গা O(n). বিকল্প DP (dp[j][i] = প্রথম i বান্ডিল j জনে) O(n² k) — শুধু ছোট n-এ brute force হিসেবে কাজের।
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, k;
cin >> n >> k;
vector<ll> a(n);
for (auto& x : a) cin >> x;
auto need = [&](ll X) { // examiners needed if nobody checks more than X
int cnt = 1;
ll cur = 0;
for (ll x : a) {
if (cur + x > X) { ++cnt; cur = 0; }
cur += x;
}
return cnt;
};
ll lo = *max_element(a.begin(), a.end());
ll hi = accumulate(a.begin(), a.end(), 0LL);
while (lo < hi) { // smallest X with need(X) <= k
ll mid = lo + (hi - lo) / 2;
if (need(mid) <= k) hi = mid;
else lo = mid + 1;
}
cout << lo << '\n';
}আউটপুট:
110
ডায়াগ্রামের সাথে হুবহু এক। তিনটি খুঁটিনাটি: (১) mid = lo + (hi - lo) / 2 — (lo + hi) / 2 এখানে overflow করত না, তবু অভ্যাসটি নিরাপদ; (২) accumulate-এর শুরু 0LL, নইলে যোগফল int-এ হতো আর overflow করত; (৩) lo শুরু হয় max(a) থেকে — 0 থেকে শুরু করলে need() ভুল উত্তর দিত, কারণ কোনো বান্ডিল X-এর চেয়ে বড় হলে সেটি একাই সীমা ভাঙে, অথচ greedy তাকে একটি দলে বসিয়ে দেয়।
সমস্যা D · কোচিং রুমের ভিড় (মাঝারি)
একটি কোচিং সেন্টারের বড় রুমটির দিন N টি সময়-ঘরে ভাগ করা (১ থেকে N). m টি বুকিং এসেছে; j-তম বুকিং lj থেকে rj পর্যন্ত (দুই প্রান্তসহ) প্রতিটি ঘরে রুমটি চায়। যে ঘরে অন্তত K টি বুকিং পড়ে, সেটি “ভিড়ের ঘর”. ম্যানেজার q টি প্রশ্ন করবেন: x থেকে y পর্যন্ত কয়টি ঘর ভিড়ের?
ইনপুট: প্রথম লাইনে N, m, K, q. পরের m লাইনে l r. পরের q লাইনে x y.
আউটপুট: প্রতিটি প্রশ্নের উত্তর এক লাইনে।
সীমা: 1 ≤ N ≤ 106; 1 ≤ m, q ≤ 2 × 105; 1 ≤ K ≤ m; 1 ≤ l ≤ r ≤ N; 1 ≤ x ≤ y ≤ N.
Input
10 4 2 3
1 4
3 7
6 10
4 5
1 10
5 9
8 10
Output
5
3
0
Dসমাধান-ব্যাখ্যা: দুই স্তরের prefix sum — আগে difference array, পরে গোনার prefix
সরল পথ কেন চলে না. প্রতিটি বুকিংয়ে l থেকে r পর্যন্ত লুপ চালালে সবচেয়ে খারাপ ক্ষেত্রে m × N = 2 × 1011. প্রতিটি প্রশ্নে x থেকে y গুনলে আরও 2 × 1011. দুটোই বাদ।
স্তর ১ — difference array. D[l] += 1, D[r + 1] −= 1; শেষে D-এর prefix sum নিলে c[i] = ঘর i-এ বুকিংয়ের সংখ্যা। নমুনায় c = 1 1 2 3 2 2 2 1 1 1. স্তর ২ — গোনার prefix. P[i] = 1 থেকে i-এর মধ্যে কয়টি ঘরে c ≥ K. তাহলে প্রশ্নের উত্তর P[y] − P[x − 1]. নমুনায় K = 2, ভিড়ের ঘর 3, 4, 5, 6, 7: প্রশ্ন (1, 10) → 5, (5, 9) → 5, 6, 7 = 3, (8, 10) → 0.
শুদ্ধতা. difference array-র invariant: D-এর প্রথম i টি ঘরের যোগফল = ঘর i-কে ঢাকে এমন বুকিংয়ের সংখ্যা, কারণ প্রতিটি বুকিং [l, r] ঠিক l ≤ i ≤ r হলে +1 অবদান রাখে। P-এর পার্থক্য সংজ্ঞা থেকেই সঠিক। মনে রাখুন D-এর আকার N + 2, কারণ r = N হলে D[N + 1] লেখা হয় — এখানে আকার N + 1 রাখলে RE বা চুপচাপ অন্য মেমরি নষ্ট।
Complexity. O(N + m + q) সময়, O(N) জায়গা। ইনপুটে প্রায় 8 × 105 টি সংখ্যা, তাই fast I/O. মূল অংশ:
vector<int> D(N + 2, 0), P(N + 1, 0);
for (int j = 0; j < m; ++j) { int l, r; cin >> l >> r; D[l]++; D[r + 1]--; }
int cur = 0;
for (int i = 1; i <= N; ++i) { cur += D[i]; P[i] = P[i - 1] + (cur >= K); }
while (q--) { int x, y; cin >> x >> y; cout << P[y] - P[x - 1] << '\n'; }পর্ব ১৭-এর ভাষায়: “রেঞ্জে যোগ, শেষে একবার পড়া” মানে difference array; “অনেকবার রেঞ্জের যোগফল পড়া” মানে prefix sum. এই সমস্যা দুটোকে পরপর জোড়ে। যদি বুকিং আর প্রশ্ন মিশে আসত (মাঝে নতুন বুকিং), তখন লাগত পর্ব ৩৫-এর lazy segment tree.
সমস্যা E · জলাবদ্ধ শহর (মাঝারি)
ভারী বৃষ্টিতে শহরের কিছু জায়গা থেকে পানি উঠছে। শহরটি R × C গ্রিড: . সাধারণ রাস্তা, # দালান (কেউ ঢুকতে পারে না), + উঁচু ফুটপাত (রাফি হাঁটতে পারে, পানি ওঠে না), W পানির উৎস, S রাফির শুরু, E বের হওয়ার পথ (S আর E ঠিক একটি করে; দুটিই সাধারণ রাস্তার মতো, পানি সেখানে উঠতে পারে)। প্রতি মিনিটে পানি প্রতিটি ভেজা ঘর থেকে পাশের (উপর-নিচ-ডান-বাম) দালান আর ফুটপাত বাদে সব ঘরে ছড়ায়। রাফিও প্রতি মিনিটে এক ঘর সরে। রাফি সময় t-তে কোনো ঘরে ঢুকতে পারে শুধু যদি পানি সেখানে t-এর পরে পৌঁছায় (একই মিনিটে পৌঁছালে চলবে না)। E-তে পৌঁছাতে কত মিনিট লাগবে? অসম্ভব হলে -1.
ইনপুট: প্রথম লাইনে R আর C, তারপর R টি লাইনে গ্রিড।
আউটপুট: ন্যূনতম মিনিট, বা -1.
সীমা: 1 ≤ R, C ≤ 1000; W শূন্য বা একাধিক থাকতে পারে।
Input
4 7
S......
+##W##+
+#####E
+++++++
Output
10
Eসমাধান-ব্যাখ্যা: আগে পানির সময়, তারপর রাফির BFS — আর একটি চমকপ্রদ পর্যবেক্ষণ
ধারণা. পানির চলাচল রাফির ওপর নির্ভর করে না। তাই প্রথমে সব W থেকে একসাথে একটি multi-source BFS চালিয়ে প্রতিটি ঘরের wt[x] = পানি পৌঁছানোর মিনিট বের করি (কখনো না পৌঁছালে অসীম)। তারপর S থেকে সাধারণ BFS, শুধু একটি বাড়তি শর্তে: ঘর x-এ সময় d + 1-এ ঢোকা যাবে কেবল যদি d + 1 < wt[x]. প্রথমবার E বের হলেই উত্তর।
কেন এই BFS ঠিক. কোনো ঘরে আগে পৌঁছানো সবসময় পরে পৌঁছানোর চেয়ে ভালো — পানি শুধু বাড়ে, কমে না, তাই আগে পৌঁছালে সেখান থেকে যে পথ খোলা, পরে পৌঁছালেও তার চেয়ে বেশি খোলা থাকে না। অপেক্ষা করারও লাভ নেই। তাই প্রতিটি ঘরের জন্য শুধু “সবচেয়ে আগে কখন পৌঁছানো যায়” মনে রাখলেই যথেষ্ট, আর সেটি BFS-এর স্তর-ধরা ক্রম নিজে থেকেই দেয় (পর্ব ৪০-এর BFS-এর মূল invariant).
নমুনায়. ওপরের সারি দিয়ে সোজা পথ ৮ মিনিটের। কিন্তু W-এর পানি ১ মিনিটে (0, 3)-এ উঠে সারিটি দু-দিকে ভিজিয়ে দেয় — রাফি সেখানে পৌঁছাত মিনিট ৩-এ, অনেক দেরিতে। নিচের ফুটপাত দিয়ে ঘুরপথে ১০ মিনিট, আর পানি ফুটপাতে ওঠে না, E-তেও পৌঁছায় না: উত্তর 10.
আরও গভীরে — ফুটপাত না থাকলে. যদি + না থাকত (পানি আর রাফি একই ঘরগুলোয় চলে), উত্তর হতো হয় পানি ছাড়া সবচেয়ে ছোট দূরত্ব b, নয়তো -1 — মাঝামাঝি কিছু নয়! কারণ: কোনো সবচেয়ে ছোট পথের কোনো ঘর X-এ পানি আগে পৌঁছালে, পানি সেই একই পথ ধরে E-তে পৌঁছায় সময় ≤ b-এ, আর রাফি তো b-এর আগে E-তে যেতেই পারে না। এমন পর্যবেক্ষণ সমস্যাকে আরও সহজ করে, কিন্তু নিশ্চিত না হলে সাধারণ দুই-BFS সমাধানটিই নিরাপদ। এই কারণেই সমস্যায় ফুটপাত যোগ করা হয়েছে — নইলে ঘুরপথ কখনো কাজে লাগত না।
Complexity. প্রতিটি BFS প্রতিটি ঘর একবার: O(R · C) সময় আর জায়গা — 106 ঘরে আরামসে। একটি গ্রিড আসলে একটি অন্তর্নিহিত গ্রাফ; adjacency list বানানোর দরকার নেই।
#include <bits/stdc++.h>
using namespace std;
int main() {
int R, C;
cin >> R >> C;
vector<string> g(R);
for (auto& row : g) cin >> row;
const int INF = INT_MAX, dr[] = {1, -1, 0, 0}, dc[] = {0, 0, 1, -1};
vector<vector<int>> wt(R, vector<int>(C, INF)), d(R, vector<int>(C, -1));
queue<pair<int, int>> q;
for (int i = 0; i < R; ++i) for (int j = 0; j < C; ++j)
if (g[i][j] == 'W') { wt[i][j] = 0; q.push({i, j}); }
while (!q.empty()) { // BFS 1: water, from every source together
auto [i, j] = q.front(); q.pop();
for (int k = 0; k < 4; ++k) {
int x = i + dr[k], y = j + dc[k];
if (x < 0 || y < 0 || x >= R || y >= C || g[x][y] == '#' || g[x][y] == '+' || wt[x][y] != INF) continue;
wt[x][y] = wt[i][j] + 1; q.push({x, y});
}
}
for (int i = 0; i < R; ++i) for (int j = 0; j < C; ++j)
if (g[i][j] == 'S') { d[i][j] = 0; q.push({i, j}); }
while (!q.empty()) { // BFS 2: Rafi, only into cells still dry
auto [i, j] = q.front(); q.pop();
if (g[i][j] == 'E') { cout << d[i][j] << '\n'; return 0; }
for (int k = 0; k < 4; ++k) {
int x = i + dr[k], y = j + dc[k];
if (x < 0 || y < 0 || x >= R || y >= C || g[x][y] == '#' || d[x][y] != -1) continue;
if (d[i][j] + 1 < wt[x][y]) { d[x][y] = d[i][j] + 1; q.push({x, y}); }
}
}
cout << -1 << '\n';
}আউটপুট:
10
দুটি queue-র জায়গায় একটিই ব্যবহার হয়েছে — প্রথম BFS শেষে সেটি খালি। dr, dc অ্যারে দিয়ে চার দিকের চলাচল এক লুপে, যা গ্রিড-সমস্যার প্রচলিত ভঙ্গি। পানি না থাকা ঘরের মান INT_MAX, তাই d + 1 < wt শর্ত সেখানে সবসময় সত্য। চাইলে নিজে পরীক্ষা করুন: ফুটপাতগুলোকে . বানালে পানি নিচের সারিতেও ছড়ায়, আর উত্তর হয় -1 — ঠিক ওপরের পর্যবেক্ষণ অনুযায়ী।
৬ · সমস্যা সেট, দ্বিতীয় ভাগ — F থেকে J
এখান থেকে সমস্যাগুলোয় একটি করে “চাবি” লুকানো থাকে — সঠিক ডেটা স্ট্রাকচার, বা অবস্থার (state) সঠিক সংজ্ঞা। চাবিটি পেলে কোড প্রায়ই ছোট; না পেলে কোনো optimization কাজে আসে না।
সমস্যা F · গ্রামে বিদ্যুতের লাইন (মাঝারি)
একটি উপজেলায় n টি গ্রাম, শুরুতে কোনোটির সাথে কোনোটির বিদ্যুৎ-সংযোগ নেই। একে একে q টি নতুন লাইন বসানো হবে; j-তম লাইন গ্রাম u আর v-কে জোড়ে (আগে থেকেই যুক্ত দুটি গ্রামের মধ্যেও লাইন বসতে পারে, এমনকি u = v-ও)। একে অপরের সাথে সরাসরি বা অন্য গ্রাম হয়ে যুক্ত গ্রামগুলো মিলে একটি “গ্রিড”. প্রতিটি লাইন বসানোর পর দুটি সংখ্যা ছাপান: এখন মোট কয়টি গ্রিড, আর সবচেয়ে বড় গ্রিডে কয়টি গ্রাম।
ইনপুট: প্রথম লাইনে n আর q, পরের q লাইনে u v.
আউটপুট: q টি লাইন, প্রতিটিতে দুটি সংখ্যা।
সীমা: 1 ≤ n, q ≤ 2 × 105; 1 ≤ u, v ≤ n.
Input
6 5
1 2
3 4
2 3
1 4
5 6
Output
5 2
4 2
3 4
3 4
2 4
Fসমাধান-ব্যাখ্যা: DSU, আর প্রতিটি union-এ দুটি সংখ্যা হালনাগাদ
ধারণা. লাইন শুধু যোগ হয়, কখনো সরে না — এটি পর্ব ৩৩-এর DSU-র আদর্শ ক্ষেত্র। গ্রিডের সংখ্যা শুরুতে n; প্রতিটি সফল union (দুই প্রান্ত আলাদা দলে) একটি করে কমায়। সবচেয়ে বড় গ্রিড কখনো ছোট হয় না, তাই একটি চলমান সর্বোচ্চই যথেষ্ট: union-এর পর নতুন দলের আকারের সাথে তুলনা। প্রতিটি লাইনের পর পুরো গ্রাফে DFS চালালে O(q · (n + q)) ≈ 8 × 1010 — বাদ।
শুদ্ধতা. invariant: প্রতিটি মুহূর্তে DSU-র দলগুলো ঠিক এখনকার গ্রাফের connected component. একটি edge (u, v) যোগ করলে শুধু u আর v-এর component দুটি মেলে (আগে থেকে এক হলে কিছুই বদলায় না) — DSU-র union ঠিক এটিই করে। চতুর্থ লাইন (1, 4) ঠিক এমন: 1 আর 4 আগেই এক দলে, তাই উত্তর আগের মতো 3 4.
Complexity. union by size আর path halving-এ প্রতিটি কাজ amortized O(α(n)): মোট O(n + q α(n)) সময়, O(n) জায়গা। শুধু union by size থাকলেও প্রতিটি find O(log n) — এখানে যথেষ্ট।
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, q;
cin >> n >> q;
vector<int> par(n + 1), sz(n + 1, 1);
iota(par.begin(), par.end(), 0);
auto find = [&](int x) {
while (par[x] != x) x = par[x] = par[par[x]]; // path halving, no recursion
return x;
};
int comps = n, biggest = 1;
while (q--) {
int u, v;
cin >> u >> v;
int a = find(u), b = find(v);
if (a != b) {
if (sz[a] < sz[b]) swap(a, b); // union by size
par[b] = a;
sz[a] += sz[b];
--comps;
biggest = max(biggest, sz[a]);
}
cout << comps << ' ' << biggest << '\n';
}
}আউটপুট:
5 2
4 2
3 4
3 4
2 4
find লুপ দিয়ে লেখা — recursion নেই, তাই 2 × 105 লম্বা শিকলেও stack-এর ভয় নেই। par আর sz-এর আকার n + 1, কারণ গ্রামের নম্বর ১ থেকে n. দুই লাখ লাইন আউটপুটে endl দিলে প্রতিটি লাইনে flush হতো; '\n' তা এড়ায়।
সমস্যা G · এক রাস্তায় অর্ধেক ভাড়া (মাঝারি–কঠিন)
নীলা একটি শহরের মোড় ১ থেকে মোড় n-এ যাবে। m টি দুই-মুখী রাস্তা, i-তম রাস্তায় রিকশাভাড়া wi টাকা। তার কাছে একটি কুপন আছে: যাত্রার একটি রাস্তায় ভাড়া হবে অর্ধেক (নিচের দিকে পূর্ণসংখ্যা, অর্থাৎ ⌊w/2⌋)। কুপন ব্যবহার না করলেও চলে। মোট ন্যূনতম ভাড়া কত? যাওয়া অসম্ভব হলে -1.
ইনপুট: প্রথম লাইনে n আর m, পরের m লাইনে u v w.
আউটপুট: ন্যূনতম ভাড়া, বা -1.
সীমা: 2 ≤ n ≤ 105; 0 ≤ m ≤ 2 × 105; 1 ≤ w ≤ 109; একই জোড়ায় একাধিক রাস্তা থাকতে পারে।
Input
5 6
1 2 10
2 5 10
1 3 4
3 4 4
4 5 9
3 5 20
Output
12
Gসমাধান-ব্যাখ্যা: vertex = (মোড়, কুপন বাকি কি না) — অবস্থা-গ্রাফে Dijkstra
প্রথম চিন্তা (ভুল). “সাধারণ shortest path বের করে তার সবচেয়ে দামি রাস্তায় কুপন।” নমুনাতেই ভাঙে: কুপন ছাড়া সেরা পথ 1 → 3 → 4 → 5 = 17, সেখানে সবচেয়ে দামি রাস্তা 9-এ কুপন দিলে 4 + 4 + 4 = 12 — এটি ঠিক উত্তর, কিন্তু কাকতালীয়। ধরুন সরাসরি একটি রাস্তা 1 → 5-এর ভাড়া 20 থাকত: কুপনে 10, যা 12-এর চেয়ে কম, অথচ সেটি কুপন ছাড়া সেরা পথে নেই। অর্থাৎ কুপন পথটাই বদলে দিতে পারে।
সঠিক ধারণা. প্রতিটি মোড়কে দুটি অবস্থায় ভাঙুন: (v, 0) — কুপন এখনো হাতে, (v, 1) — খরচ হয়ে গেছে। প্রতিটি রাস্তা (u, v, w) থেকে চারটি edge: (u, c) → (v, c) খরচ w, আর (u, 0) → (v, 1) খরচ ⌊w/2⌋ (আর উল্টো দিকে একইভাবে)। এখন প্রশ্নটি সাধারণ shortest path: (1, 0) থেকে (n, 0) বা (n, 1). সব ওজন ঋণাত্মক নয়, তাই Dijkstra চলে। এই কৌশলের নাম state-space search বা layered graph — পর্ব ৪৩-এ দেখেছেন, আর k টি কুপন হলে স্তর k + 1 টি।
শুদ্ধতা. নতুন গ্রাফের প্রতিটি (1, 0) → (n, ·) পথ আসল শহরে একটি যাত্রা, যেখানে সর্বোচ্চ একবার কুপন ব্যবহার হয়েছে (স্তর 1 থেকে 0-তে ফেরার edge নেই), আর খরচ হুবহু এক; উল্টোটাও সত্য। তাই দুই গ্রাফের সেরা খরচ সমান, আর Dijkstra নতুন গ্রাফে সঠিক।
Complexity. 2n টি vertex আর 6m-এর মতো directed edge: O((n + m) log n) সময়, O(n + m) জায়গা। মোট ভাড়া 105 × 109 = 1014 পর্যন্ত — long long.
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
cin >> n >> m;
vector<vector<pair<int, ll>>> adj(n + 1);
for (int i = 0; i < m; ++i) {
int u, v; ll w;
cin >> u >> v >> w;
adj[u].push_back({v, w});
adj[v].push_back({u, w});
}
const ll INF = LLONG_MAX;
vector<array<ll, 2>> d(n + 1, {INF, INF}); // d[v][c]: c = 1 if the coupon is used
priority_queue<tuple<ll, int, int>, vector<tuple<ll, int, int>>, greater<>> pq;
d[1][0] = 0;
pq.push({0, 1, 0});
auto relax = [&](int v, int c, ll nd) {
if (nd < d[v][c]) { d[v][c] = nd; pq.push({nd, v, c}); }
};
while (!pq.empty()) {
auto [du, u, c] = pq.top(); pq.pop();
if (du != d[u][c]) continue; // stale entry
for (auto [v, w] : adj[u]) {
relax(v, c, du + w); // pay full fare
if (c == 0) relax(v, 1, du + w / 2); // use the coupon on this road
}
}
ll ans = min(d[n][0], d[n][1]);
cout << (ans == INF ? -1 : ans) << '\n';
}আউটপুট:
12
নমুনায় তিনটি প্রার্থী: 1 → 2 → 5 কুপনসহ 10 + 5 = 15; 1 → 3 → 5 কুপনসহ 4 + 10 = 14; আর 1 → 3 → 4 → 5, কুপন 9-এ, 4 + 4 + 4 = 12 — সবচেয়ে কম। du != d[u][c] লাইনটি পুরোনো (stale) heap-entry বাদ দেয়; এটি না থাকলে উত্তর ঠিকই আসে, কিন্তু একই vertex বহুবার প্রক্রিয়া হয়ে বড় ইনপুটে TLE হতে পারে। প্রোগ্রামটি ছোট এলোমেলো গ্রাফে “প্রতিটি রাস্তায় একবার কুপন দিয়ে Floyd-Warshall” brute force-এর সাথে মিলিয়ে যাচাই করা।
সমস্যা H · ফ্রিল্যান্সারের কাজ বাছাই (মাঝারি–কঠিন)
তানভীর একজন ফ্রিল্যান্স ভিডিও এডিটর। তার সামনে n টি কাজের প্রস্তাব; i-তম কাজ দিন si থেকে ei পর্যন্ত (দুই দিনসহ) পুরো সময় নেয়, আর পারিশ্রমিক pi টাকা। একদিনে সে একটির বেশি কাজ করতে পারে না, আর কোনো কাজ আংশিক নেওয়া যায় না। কাজ বেছে নিয়ে সর্বোচ্চ মোট আয় কত?
ইনপুট: প্রথম লাইনে n, পরের n লাইনে s e p.
আউটপুট: সর্বোচ্চ মোট আয়।
সীমা: 1 ≤ n ≤ 2 × 105; 1 ≤ s ≤ e ≤ 109; 1 ≤ p ≤ 109.
Input
5
1 3 50
2 5 20
4 6 70
6 7 60
8 9 30
Output
150
Hসমাধান-ব্যাখ্যা: শেষের দিন অনুযায়ী সাজিয়ে “নেব, না নেব না” DP — আগের সামঞ্জস্যপূর্ণ কাজ binary search-এ
Greedy কেন নয়. পর্ব ৫০-এর “আগে শেষ হয় যে কাজ” greedy সর্বোচ্চ সংখ্যক কাজ দেয়, সর্বোচ্চ আয় নয়। নমুনায় greedy নেবে (1–3), (4–6), (8–9) — এখানে কাকতালীয়ভাবে ঠিক 150; কিন্তু যদি (2–5)-এর পারিশ্রমিক 200 হতো, greedy সেটি নিত না। পারিশ্রমিক থাকলে লাগে DP — এটি পরিচিত weighted interval scheduling.
অবস্থা ও রূপান্তর. কাজগুলো শেষের দিন অনুযায়ী সাজান: e1 ≤ e2 ≤ … dp[i] = প্রথম i টি কাজ থেকে সর্বোচ্চ আয়। i-তম কাজটি হয় বাদ (dp[i − 1]), নয়তো নেওয়া — তখন আগের যে কাজগুলো si-এর আগে শেষ হয় (e < si), শুধু সেগুলোই রাখা যায়। সাজানোর কারণে সেগুলো ঠিক প্রথম k টি, k = সাজানো e-তে si-এর lower_bound. তাই dp[i] = max(dp[i − 1], dp[k] + pi).
প্রমাণের খসড়া. প্রথম i কাজের যেকোনো সেরা বাছাইয়ে কাজ i হয় নেই — তাহলে সেটি প্রথম i − 1-এর একটি বৈধ বাছাই, মান ≤ dp[i − 1]; নয়তো আছে — তাহলে বাকিগুলোর প্রতিটি কাজ i-এর সাথে দিন ভাগ করে না, আর যেহেতু তাদের e ≤ ei, তাদের e < si; অর্থাৎ বাকিগুলো প্রথম k-এর একটি বৈধ বাছাই, মান ≤ dp[k]. দুই দিকের সর্বোচ্চই dp[i] (optimal substructure). দিন দুই প্রান্তসহ বলে শর্ত “e < s”, “e ≤ s” নয় — নমুনার (4–6) আর (6–7) একসাথে নেওয়া যায় না।
Complexity. সাজানো O(n log n), প্রতিটি কাজে একটি binary search O(log n): মোট O(n log n) সময়, O(n) জায়গা। আয় 2 × 1014 পর্যন্ত — long long. দিনের ওপর সরাসরি DP (109 দিন) চলত না; চাইলে coordinate compression করে করা যায়।
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
vector<array<ll, 3>> job(n); // {end, start, pay}
for (auto& [e, s, p] : job) cin >> s >> e >> p;
sort(job.begin(), job.end()); // by end day
vector<ll> ends(n), dp(n + 1, 0); // dp[i] = best using the first i jobs
for (int i = 0; i < n; ++i) ends[i] = job[i][0];
for (int i = 1; i <= n; ++i) {
auto [e, s, p] = job[i - 1];
// jobs that finish before day s are exactly the first k ones
int k = lower_bound(ends.begin(), ends.begin() + (i - 1), s) - ends.begin();
dp[i] = max(dp[i - 1], dp[k] + p); // skip job i, or take it
}
cout << dp[n] << '\n';
}আউটপুট:
150
সাজানোর পর dp = 0, 50, 50, 120, 120, 150: (1–3) 50; (2–5) নিলে 20, বাদ; (4–6) নিলে 50 + 70 = 120; (6–7) নিলে k = 2, dp[2] + 60 = 110 < 120, বাদ; (8–9) নিলে 120 + 30 = 150. array<ll, 3>-এ প্রথম উপাদান শেষের দিন রাখায় সাধারণ sort-ই ঠিক ক্রম দেয়। lower_bound-এর পরিসর ends.begin() + (i - 1) পর্যন্ত — শুধু আগের কাজগুলো। এটি n ≤ 10-এর সব subset পরীক্ষার brute force-এর সাথে এলোমেলো টেস্টে মেলানো হয়েছে।
সমস্যা I · গুদামের সর্বনিম্ন মজুত (কঠিন)
একটি সুপারশপ চেইনের n টি দোকান একটি সারিতে নম্বর দেওয়া, i-তম দোকানে চালের মজুত ai বস্তা। q টি ঘটনা আসে, দুই ধরনের: 1 i x — দোকান i-এর মজুত এখন x; 2 l r — দোকান l থেকে r-এর মধ্যে সবচেয়ে কম মজুত কত, আর কয়টি দোকানে ঠিক সেই পরিমাণ?
ইনপুট: প্রথম লাইনে n আর q, দ্বিতীয় লাইনে a1 … an, তারপর q টি ঘটনা।
আউটপুট: প্রতিটি ধরন-২ ঘটনার জন্য এক লাইনে দুটি সংখ্যা।
সীমা: 1 ≤ n, q ≤ 2 × 105; 1 ≤ ai, x ≤ 109; 1 ≤ l ≤ r ≤ n.
Input
6 6
5 3 3 8 3 9
2 1 6
1 3 1
2 2 4
2 4 6
1 5 8
2 4 6
Output
3 3
1 1
3 1
8 2
Iসমাধান-ব্যাখ্যা: segment tree-র প্রতিটি node-এ (সর্বনিম্ন, কতবার) — সঠিক combine-ই আসল কাজ
ধারণা. point update আর range query মিশে আছে — prefix sum বা sparse table চলবে না (update-এ সব নতুন করে বানাতে O(n)). পর্ব ৩৪-এর segment tree-এ প্রতিটি node একটি অংশের সারাংশ রাখে। এখানে সারাংশ একটি জোড়া (m, c): অংশের সর্বনিম্ন মান আর সেটি কতবার আছে। দুটি পাশাপাশি অংশ জোড়ার নিয়ম: m আলাদা হলে ছোটটির জোড়াটিই; সমান হলে (m, c1 + c2).
শুদ্ধতা. segment tree কাজ করে যেকোনো associative combine আর একটি identity উপাদানের জন্য (পর্ব ৩৪-এর monoid). আমাদের combine associative: তিনটি অংশে যেভাবেই জোড়া দিন, ফল হয় (তিনটির সর্বনিম্ন, যেসব অংশে সেই সর্বনিম্ন তাদের count-এর যোগফল)। identity হলো (∞, 0). এই কোডের iterative segment tree বাঁ আর ডান দিকের অংশ আলাদা ক্রমে জোড়ে; combine এখানে commutative-ও, তাই ক্রম নিয়ে চিন্তা নেই — আর tree-এর আকার n যেকোনো সংখ্যা হতে পারে, দুইয়ের ঘাত লাগে না।
নমুনায়. প্রথম প্রশ্ন: 5 3 3 8 3 9-এ সর্বনিম্ন 3, তিনবার। দোকান ৩ → 1 হওয়ার পর [2, 4] = 3 1 8: (1, 1). [4, 6] = 8 3 9: (3, 1). দোকান ৫ → 8 হলে [4, 6] = 8 8 9: (8, 2).
Complexity. বানানো O(n), প্রতিটি ঘটনা O(log n): মোট O(n + q log n) সময়, O(n) জায়গা (2n টি node). প্রতিটি প্রশ্নে সরাসরি লুপ চালালে n · q = 4 × 1010 — বাদ।
#include <bits/stdc++.h>
using namespace std;
using P = pair<int, int>; // {minimum, how many times}
P combine(P a, P b) {
if (a.first != b.first) return min(a, b);
return {a.first, a.second + b.second};
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, q;
cin >> n >> q;
vector<P> t(2 * n); // iterative segment tree, leaves at n..2n-1
for (int i = 0; i < n; ++i) { cin >> t[n + i].first; t[n + i].second = 1; }
for (int i = n - 1; i >= 1; --i) t[i] = combine(t[2 * i], t[2 * i + 1]);
while (q--) {
int type, x, y;
cin >> type >> x >> y;
if (type == 1) { // stock of shop x becomes y
int i = n + x - 1;
t[i] = {y, 1};
for (i /= 2; i >= 1; i /= 2) t[i] = combine(t[2 * i], t[2 * i + 1]);
} else { // query on shops x..y
P res = {INT_MAX, 0};
for (int l = n + x - 1, r = n + y; l < r; l /= 2, r /= 2) {
if (l & 1) res = combine(res, t[l++]);
if (r & 1) res = combine(res, t[--r]);
}
cout << res.first << ' ' << res.second << '\n';
}
}
}আউটপুট:
3 3
1 1
3 1
8 2
পাতাগুলো t[n] থেকে t[2n − 1], node i-এর সন্তান 2i আর 2i + 1. query-তে অর্ধ-খোলা পরিসর [l, r): 1-ভিত্তিক x..y থেকে l = n + x - 1, r = n + y — off-by-one-এর সবচেয়ে সম্ভাব্য জায়গা, তাই মন্তব্যসহ লেখা। count সর্বোচ্চ 2 × 105, তাই int যথেষ্ট। একই ছাঁচে “সর্বোচ্চ ও তার অবস্থান”, “gcd ও gcd-এর সমান কয়টি” ধরনের প্রশ্নও সমাধান হয় — শুধু combine বদলায়। কোডটি ছোট এলোমেলো ইনপুটে সরাসরি লুপের brute force-এর সাথে মেলানো।
সমস্যা J · ডেলিভারি রাইডারের পথ (কঠিন)
একজন ডেলিভারি রাইডার গুদাম (জায়গা 0) থেকে রওনা দিয়ে n − 1 টি দোকানে (জায়গা 1 … n − 1) একবার করে পার্সেল দেবেন, যেকোনো ক্রমে, আর শেষ দোকানেই দিন শেষ — গুদামে ফেরার দরকার নেই। জায়গা i থেকে j-তে যেতে d[i][j] মিনিট লাগে (ঢাকার একমুখী রাস্তার কারণে d[i][j] আর d[j][i] আলাদা হতে পারে)। সব দোকানে পৌঁছাতে মোট ন্যূনতম কত মিনিট?
ইনপুট: প্রথম লাইনে n, তারপর n × n ম্যাট্রিক্স d (d[i][i] = 0).
আউটপুট: ন্যূনতম মোট সময়।
সীমা: 1 ≤ n ≤ 16; 0 ≤ d[i][j] ≤ 106; সময়সীমা ২ সেকেন্ড।
Input
4
0 4 9 3
4 0 2 7
9 2 0 5
3 7 5 0
Output
10
Jসমাধান-ব্যাখ্যা: n ≤ 16 দেখেই bitmask — “কোন দোকানগুলো শেষ, এখন কোথায়” অবস্থার DP
সীমা পড়ুন. সব ক্রম পরীক্ষা করলে 15! ≈ 1.3 × 1012 — অসম্ভব। কিন্তু n ≤ 16 অংশ ৩-এর টেবিলে “2n”-এর ঘর। এটি আসলে পর্ব ৫৭-এর traveling salesman-এর একটি রূপ (ফেরা ছাড়া), যা NP-hard — তাই polynomial সমাধান আশা করছি না; চাই 15! থেকে 216 · 16² ≈ 1.7 × 107-এ নামা।
মূল পর্যবেক্ষণ. ভবিষ্যতের সিদ্ধান্তের জন্য অতীতের পুরো ক্রম লাগে না — লাগে শুধু (ক) কোন জায়গাগুলোতে যাওয়া হয়ে গেছে, আর (খ) এখন কোথায় আছি। প্রথমটি একটি bitmask দিয়ে লেখা যায়। dp[mask][v] = গুদাম থেকে শুরু করে ঠিক mask-এর জায়গাগুলো ঘুরে এখন v-তে থাকার ন্যূনতম সময়। শুরু dp[{0}][0] = 0; রূপান্তর: mask-এ নেই এমন u-তে গেলে dp[mask ∪ {u}][u] = min(…, dp[mask][v] + d[v][u]). উত্তর minv dp[সব][v]. এটিই ১৯৬২ সালে Richard Bellman আর আলাদাভাবে Michael Held ও Richard Karp-এর দেওয়া Held-Karp DP.
শুদ্ধতা. যেকোনো যাত্রার শেষ ধাপটি সরালে বাকিটি একই mask-এর (u বাদে) আর শেষ জায়গা v-এর একটি যাত্রা; তাই সেরা যাত্রার বাকি অংশও সেরা হতে হবে (optimal substructure), আর রূপান্তরটি সব সম্ভাব্য “শেষ ধাপ” পরীক্ষা করে। mask ছোট থেকে বড় ক্রমে প্রক্রিয়া করলে প্রতিটি অবস্থা ব্যবহারের আগেই চূড়ান্ত, কারণ রূপান্তর সবসময় mask-এ bit বাড়ায়।
নমুনায়. 0 → 3 → 2 → 1: 3 + 5 + 2 = 10. অন্য ক্রম, যেমন 0 → 1 → 2 → 3: 4 + 2 + 5 = 11.
Complexity. 2n · n টি অবস্থা, প্রতিটিতে n টি রূপান্তর: O(2n n²) সময়, O(2n n) জায়গা — n = 16-এ প্রায় 1.7 × 107 কাজ আর 106 টি int (৪ MB). মোট সময় সর্বোচ্চ 15 × 106, তাই int-এ আঁটে। মূল লুপ:
const int INF = 1e9;
vector<vector<int>> dp(1 << n, vector<int>(n, INF));
dp[1][0] = 0; // only the warehouse visited, standing there
for (int mask = 1; mask < (1 << n); ++mask)
for (int v = 0; v < n; ++v) {
if (dp[mask][v] == INF) continue;
for (int u = 0; u < n; ++u)
if (!(mask >> u & 1))
dp[mask | 1 << u][u] = min(dp[mask | 1 << u][u], dp[mask][v] + d[v][u]);
}
cout << *min_element(dp[(1 << n) - 1].begin(), dp[(1 << n) - 1].end()) << '\n';লক্ষ করুন 1 << u-এর অগ্রাধিকার: mask | 1 << u-এ shift আগে হয়, তাই ঠিক আছে; কিন্তু mask >> u & 1-এর মতো লেখায় সন্দেহ হলে বন্ধনী দিন। n = 1 হলে উত্তর 0 — dp[1][0] নিজেই। গুদামে ফেরার রূপ (আসল TSP) হলে উত্তর minv (dp[সব][v] + d[v][0]). এই DP ৭টি পর্যন্ত জায়গার সব permutation-এর brute force-এর সাথে এলোমেলো ম্যাট্রিক্সে মেলানো হয়েছে।
৭ · দলের কৌশল — পাঁচ ঘণ্টা, তিনজন, একটি কম্পিউটার
একই দক্ষতার দুটি দলের ফল প্রায়ই আলাদা হয় শুধু কৌশলের কারণে। কয়েকটি নিয়ম, যা বেশিরভাগ অভিজ্ঞ দল কোনো না কোনো রূপে মানে:
- প্রথম ১৫–২০ মিনিটে সব সমস্যা পড়া। সমস্যাগুলো তিন ভাগে ভাগ করে প্রত্যেকে পড়ুন, আর দলের খাতায় প্রতিটির এক লাইনের সারাংশ, সীমা আর আনুমানিক কঠিনতা লিখুন। সবচেয়ে সহজটি যে চিনবেন, তিনি সাথে সাথে কম্পিউটারে।
- কঠিনতা অনুযায়ী সাজানো, আর স্কোরবোর্ড পড়া। সমস্যার ক্রম কঠিনতার ক্রম নয়। কনটেস্ট চলাকালে স্কোরবোর্ডে দেখুন অনেক দল কোনটি সমাধান করছে — সেটি সম্ভবত সহজ। ফ্রিজের পর এই তথ্য আর আসে না, তাই তার আগেই পরিকল্পনা ঠিক করুন।
- কম্পিউটার কখনো খালি নয়, কখনো চিন্তার জায়গাও নয়। কোড কাগজে প্রায় তৈরি হলে তবেই কিবোর্ডে। কেউ টাইপ করার সময় বাকি দুজন পরের সমস্যা ভাবেন বা হাতে নমুনা মেলান।
- WA পেলে প্রিন্ট করে সরে যান। কম্পিউটারের সামনে বসে ডিবাগ করা মানে অন্য সবার সময় আটকে রাখা। কোড প্রিন্ট করুন (বা স্ক্রিনের ছবি খাতায় তুলুন), কম্পিউটার ছেড়ে দিন, আর কাগজে খুঁজুন — ভালো হয় আরেকজনকে নিয়ে জোড়ায় ডিবাগ: আপনি লাইন ধরে জোরে ব্যাখ্যা করবেন, তিনি প্রশ্ন করবেন। নিজের কোডে যে ভুল চোখে পড়ে না, অন্যের চোখে প্রায়ই সেকেন্ডে পড়ে।
- আটকে গেলে ছেড়ে দেওয়ার সাহস। একটি সমস্যায় ৪০–৫০ মিনিট কোনো অগ্রগতি না হলে দলে কথা বলুন — হয় অন্যজন নতুন চোখে দেখুন, নয়তো অন্য সমস্যায় যান।
- শেষ ঘণ্টায় একটি লক্ষ্য. দুটি আধা-সমাধানের চেয়ে একটি সম্পূর্ণ সমাধান ভালো। ফ্রিজের পর সবাই মিলে যে সমস্যাটি সবচেয়ে কাছে, সেটিতে।
নিচে একটি কাল্পনিক দলের (রাফি, নীলা, তানভীর) পাঁচ ঘণ্টা দেখুন — এই পাতার সমস্যা সেটেই। প্রতিটি ধাপে penalty-র হিসাবটি মিলিয়ে নিন।
| সময় | |
| কম্পিউটারে | |
| বাকি দুজন | |
| সমাধান | |
| মোট penalty (মিনিট) |
- শুরু. রাফি কম্পিউটারে template টাইপ করে রাখে, বাকি দুজন সমস্যা ভাগ করে পড়ে। কম্পিউটার প্রথম মিনিট থেকেই কাজে।
- প্রথম AC, ১৪ মিনিটে. A সূত্রের সমস্যা; রাফি overflow-এর ফাঁদ ধরে প্রথম জমাতেই AC. penalty 14.
- একটি WA-র দাম. নীলার B প্রথমে WA —
int-এ দুই ওজনের যোগফল overflow. ঠিক করে ৫২ মিনিটে AC. খরচ 52 + 20 = 72 মিনিট; একটি ভুল জমা পুরো ২০ মিনিট। - F, ৯০ মিনিটে. তানভীর কাগজে DSU লিখে রেখেছিল, তাই কম্পিউটারে বসে দ্রুত শেষ। penalty 86 + 90 = 176.
- C, ১২৫ মিনিটে. রাফি binary search-এর একমুখিতা নিশ্চিত হয়ে তবেই টাইপ করেছে। 176 + 125 = 301.
- TLE থেকে AC. G-তে TLE. নীলা কম্পিউটার ছেড়ে কোড প্রিন্ট করে তানভীরের সাথে দেখল — Dijkstra-য় পুরোনো heap-entry বাদ দেওয়ার লাইনটিই নেই। ঠিক করে ১৮৫ মিনিটে AC: 185 + 20 = 205, মোট 506.
- ফ্রিজ. স্কোরবোর্ড থেমে গেল। ফ্রিজের আগে অনেক দল E করেছিল, H প্রায় কেউ না — তাই সবাই E-তে। penalty বদলায়নি, কারণ নতুন AC নেই।
- E, ২৭৫ মিনিটে. 506 + 275 = 781. ছয়টি সমাধান।
- শেষ. H-এ দুটি ভুল জমা ছিল, কিন্তু সমাধান হয়নি — তাই সেগুলোর কোনো penalty নেই। চূড়ান্ত: ৬টি, 781 মিনিট। কাল সকালের কাজ: H আর বাকি সমস্যার upsolving (অংশ ৮)।
এই হিসাব থেকে একটি শিক্ষা: সমাধানের সময়ের যোগফলই penalty-র বড় অংশ, তাই সহজ সমস্যা আগে করা প্রায় সবসময় ভালো — একটি সমস্যায় যত মিনিট লাগে, তা তার পরে সমাধান হওয়া প্রতিটি সমস্যার AC-মুহূর্তে যোগ হয়ে যায়। ছোট কাজ আগে সারলে সেই ছোট সময়টুকুই পরের সবগুলোতে যোগ হয়, লম্বা কাজ আগে করলে লম্বা সময়টা। আর প্রতিটি ভুল জমা ২০ মিনিট — তাই জমার আগে অংশ ৪-এর ৩০ সেকেন্ডের চেকলিস্ট আসলে সময় বাঁচায়।
দলের খাতা (team notebook): অনেক কনটেস্টে কয়েক পাতার ছাপানো নোট নেওয়া যায়। সেখানে রাখার মতো: পরীক্ষিত DSU, Dijkstra, segment tree, Fenwick tree, KMP (পর্ব ৫৮), modular power ও inverse, sieve, geometry-র cross product (পর্ব ৬৩), max-flow (পর্ব ৪৭) আর এই পাতার সীমা-থেকে-complexity টেবিল। শুধু সেই কোডই রাখুন যা নিজে লিখেছেন আর বহুবার ব্যবহার করেছেন — অচেনা কোড কনটেস্টের চাপে ভুলভাবে বসানো সহজ।
৮ · অনুশীলনের পরিকল্পনা — upsolving, virtual contest, বিষয়ের মই আর stress testing
কনটেস্টে আপনি যা জানেন তা দেখান; শেখেন কনটেস্টের পরে. নিয়মিত উন্নতির জন্য চারটি অভ্যাস:
- Upsolving. প্রতিটি কনটেস্টের পর অন্তত পরের এক-দুটি সমস্যা, যা পারেননি, শেষ করুন — আগে নিজে, তারপর editorial-এর একটি ইঙ্গিত, তারপর পুরো ব্যাখ্যা। editorial পড়ে বুঝে নিজে কোড লিখুন, কপি নয়। শেষে খাতায় তিন লাইন: কোন পর্যবেক্ষণ মিস করেছিলাম, কোন সংকেত দেখে পরের বার চিনব, কোন ভুল কোডে ঢুকেছিল।
- Virtual contest. পুরোনো একটি কনটেস্ট আসল সময় ধরে দিন, যেন সত্যিই চলছে — মাঝে editorial বা অন্য কিছু দেখবেন না। দলগতভাবে করলে একটি কম্পিউটারের নিয়মও মানুন। এটিই চাপের মধ্যে সিদ্ধান্ত নেওয়ার একমাত্র অনুশীলন।
- বিষয়ের মই (topic ladder). নিজের স্তরের একটু ওপরের সমস্যা বেছে বিষয় ধরে ধরে এগোন। নিচের টেবিলটি এই কোর্সের পর্বের সাথে মিলিয়ে একটি ক্রম:
- নিজের ভুলের খাতা আর লাইব্রেরি. একই ভুল দ্বিতীয়বার করলে খাতায় লাল দাগ। আর প্রতিটি পরীক্ষিত কোড (DSU, segment tree…) একটি ফোল্ডারে — পরে team notebook এখান থেকেই।
| ধাপ | বিষয় | রিভিশনের পর্ব | এই সেটে |
|---|---|---|---|
| ১ | simulation (নিয়ম ধরে ধাপে ধাপে চালানো), ad hoc, ছোট গণিত, overflow | ৪, ৬, ৬২ | A |
| ২ | sorting, greedy, two pointer, prefix sum | ১৬, ১৭, ২৫, ৫০ | B, D |
| ৩ | binary search, উত্তরের ওপর binary search | ১৮, ১৯ | C |
| ৪ | STL, stack, queue, heap, hash | ১৫, ২৩, ৩২ | G-এর heap |
| ৫ | BFS, DFS, DSU, topological sort | ৪০, ৪১, ৪২, ৩৩ | E, F |
| ৬ | shortest path, MST | ৪৩, ৪৪, ৪৫ | G |
| ৭ | DP — ভিত্তি থেকে ক্লাসিক | ৫২, ৫৩, ৫৪ | H |
| ৮ | segment tree, Fenwick, sparse table | ৩৪, ৩৫, ৩৬, ৩৭ | I |
| ৯ | bitmask/digit/tree DP, string, LCA, flow | ৫৫, ৫৮, ৪৮, ৪৭ | J |
Stress testing — যখন WA আসে কিন্তু কারণ খুঁজে পান না
পর্ব ৬৭-এর ক্যাপস্টোনে পুরো পদ্ধতিটি দেখেছেন; এখানে কনটেস্টের ছোট রূপ। Stress testing-এর চারটি অংশ: (১) একটি সরল, নিশ্চিতভাবে সঠিক brute force — ধীর হলেও চলবে; (২) একটি test generator, যা নির্দিষ্ট seed থেকে খুব ছোট এলোমেলো ইনপুট বানায়; (৩) একটি লুপ, যা দুটি সমাধান চালিয়ে উত্তর মেলায়; (৪) অমিল পেলে সবচেয়ে ছোট ইনপুটটি ছাপানো, যাতে হাতে trace করা যায়। ICPC-তে ইন্টারনেট নেই, কিন্তু কম্পিউটার আছে — দশ মিনিটে লেখা একটি stress test প্রায়ই এক ঘণ্টার অন্ধ খোঁজ বাঁচায়।
নিচের প্রোগ্রামটি সমস্যা B-এর greedy-কে একটি brute force দিয়ে যাচাই করে: brute force প্রতিটি মানুষের জন্য সব বিকল্প (একা, অথবা প্রতিটি সম্ভাব্য সঙ্গী) চেষ্টা করে — কোনো সাজানো বা greedy যুক্তি ব্যবহার না করে।
#include <bits/stdc++.h>
using namespace std;
int greedy(vector<int> w, int C) { // the solution we want to trust
sort(w.begin(), w.end());
int i = 0, j = (int)w.size() - 1, boats = 0;
while (i <= j) { if (i < j && w[i] + w[j] <= C) ++i; --j; ++boats; }
return boats;
}
int brute(vector<int> w, int C) { // try every way: alone, or with someone
if (w.empty()) return 0;
int f = w.back(); w.pop_back();
int best = 1 + brute(w, C);
for (size_t k = 0; k < w.size(); ++k)
if (f + w[k] <= C) {
vector<int> rest = w;
rest.erase(rest.begin() + k);
best = min(best, 1 + brute(rest, C));
}
return best;
}
int main() {
mt19937 rng(2026); // fixed seed: the same tests every run
for (int t = 1; t <= 2000; ++t) {
int n = rng() % 8 + 1, C = rng() % 20 + 1;
vector<int> w(n);
for (int& x : w) x = rng() % C + 1; // 1..C, everyone fits alone
if (greedy(w, C) != brute(w, C)) {
printf("MISMATCH on test %d, C = %d\n", t, C);
return 0;
}
}
printf("all 2000 random tests passed\n");
}আউটপুট:
all 2000 random tests passed
mt19937-এর সংখ্যার ধারা C++ standard-এ নির্দিষ্ট, তাই seed 2026 মানে যেকোনো compiler-এ একই ২০০০টি টেস্ট — ভুল পেলে ঠিক সেই ইনপুট আবার তৈরি করা যায়। (uniform_int_distribution-এর ফল লাইব্রেরিভেদে আলাদা হতে পারে, তাই এখানে সরল %.) n ≤ 8 ইচ্ছাকৃত: brute force সেখানে তাৎক্ষণিক, আর ভুল পেলে হাতে মেলানো যায়। এবার নিজে একটি ভুল ঢুকিয়ে দেখুন — যেমন greedy-তে w[i] + w[j] <= C-এর বদলে < C: stress test কয়েকটি টেস্টের মধ্যেই MISMATCH ছাপবে। যে stress test একটি ইচ্ছাকৃত ভুল ধরতে পারে না, তার ওপর ভরসা করবেন না।
Stress test যা ধরে না: ছোট ইনপুট গতির সমস্যা দেখায় না। TLE-র সন্দেহ থাকলে সবচেয়ে বড় আর সবচেয়ে খারাপ আকৃতির ইনপুট আলাদা করে বানিয়ে সময় মাপুন — যেমন সমস্যা F-এ একটি লম্বা শিকল, সমস্যা G-তে অনেক parallel রাস্তা, সমস্যা A-তে T = 105 আর সব সংখ্যা 1018-এর কাছে।
৯ · AI-এর যুগে — অনুশীলনে সহকারী, কনটেস্টে নিয়ম মানা
আগে নিয়ম: on-site ICPC-ধাঁচের কনটেস্টে ইন্টারনেট আর AI ব্যবহারের সুযোগ সাধারণত থাকে না। অনেক অনলাইন প্ল্যাটফর্মও রেটেড রাউন্ড চলাকালে AI দিয়ে সমাধান লেখানো নিষেধ বা কঠোরভাবে সীমিত করেছে — আর নিয়ম বদলায়, তাই প্রতিটি প্ল্যাটফর্মের সর্বশেষ নিয়ম নিজে পড়ে নিন। নিয়ম ভাঙা শুধু অন্যায় নয়, নিজের প্রস্তুতিকেও ফাঁকি: আসল ICPC-র মঞ্চে AI থাকবে না।
কিন্তু অনুশীলনে — কনটেস্টের বাইরে, upsolving-এর সময় — AI একটি শক্তিশালী সহকারী হতে পারে, যদি তাকে উত্তর লেখার বদলে আপনার চিন্তা পরীক্ষার কাজ দেন। মনে রাখবেন, AI প্রায়ই আত্মবিশ্বাসের সাথে ভুল complexity বলে, off-by-one-এ ভুল করে, প্রমাণহীন greedy দেয় (সমস্যা B-এর মতো সমস্যায় “সবচেয়ে ভারী দুজনকে একসাথে” ধরনের ভুল নিয়ম), আর সমস্যা A-র মতো overflow প্রায়ই ভুলে যায়।
Upsolving-এর সময় এমনভাবে জিজ্ঞেস করুন — সমাধান নয়, ইঙ্গিত আর যাচাই চান:
AI-এর সাহায্য নেওয়ার পর নিজেকে যাচাইয়ের চেকলিস্ট
১০ · কোর্স সমাপ্তি — ৭২টি পর্বের পরে আপনি কোথায়
এটি নয়টি মডিউল আর ৭২টি পর্বের এই কোর্সের শেষ পর্ব। একবার পেছনে তাকান। পর্ব ০১-এ প্রশ্ন ছিল, “DSA কেন?” পর্ব ০৩-এ loop invariant দিয়ে প্রমাণ করতে শিখেছিলেন, পর্ব ০৪-এ Big-O দিয়ে বৃদ্ধির হার মাপতে। আজ আপনি একটি অচেনা সমস্যার সীমা পড়ে লক্ষ্য complexity ঠিক করতে পারেন, সঠিক ডেটা স্ট্রাকচার বেছে নিতে পারেন, সমাধানটি প্রমাণ করতে পারেন, আর brute force দিয়ে নিজের কোড যাচাই করতে পারেন। আন্তরিক অভিনন্দন — এটি সত্যিই বড় অর্জন।
পরীক্ষা ও সার্টিফিকেট: লগইন করলে ড্যাশবোর্ডে শেখার আটটি মডিউলের (পর্ব ১–৬৭) প্রতিটির একটি মডিউল টেস্ট আর একটি ফাইনাল পরীক্ষা পাবেন। শেখার সব পর্ব পড়া শেষ করে ফাইনালে ৬০% বা তার বেশি পেলে কোর্স-সমাপ্তির সার্টিফিকেট পাবেন। প্রতিটি পর্বের “পড়েছি” বোতাম চাপা থাকলে অগ্রগতি সেখানে দেখা যাবে — অগ্রগতি সেভ হয় শুধু লগইন করা অবস্থায়। পরীক্ষার আগে পর্ব ৬৮–৭০-এর প্রশ্নব্যাংক একবার ঘুরে আসুন।
এখন আপনি যা পারেন
- বিশ্লেষণের ভিত্তি (পর্ব ১–৯): Big-O, Ω, Θ; best, worst আর average case; loop invariant আর induction দিয়ে প্রমাণ; recursion, recurrence আর master theorem.
- লিনিয়ার স্ট্রাকচার (পর্ব ১০–১৭): array, string, linked list, stack, queue, STL ও Python-এর কালেকশন, two pointer আর prefix sum.
- সার্চিং ও সর্টিং (পর্ব ১৮–২৫): binary search ও উত্তরের ওপর binary search, merge sort, quick sort, heap, linear-time sort আর তুলনা-সর্টের নিম্নসীমা।
- ট্রি, হ্যাশ ও DSU (পর্ব ২৬–৩৩): BST, AVL, red-black, B-tree, trie, hash table আর union-find.
- রেঞ্জ কোয়েরি (পর্ব ৩৪–৩৮): segment tree, lazy propagation, Fenwick tree, sparse table, treap আর skip list.
- গ্রাফ (পর্ব ৩৯–৪৮): BFS, DFS, topological sort, Dijkstra, Bellman-Ford, Floyd-Warshall, MST, SCC, network flow আর LCA.
- ডিজাইন কৌশল (পর্ব ৪৯–৫৯): divide and conquer, greedy ও exchange argument, Huffman, DP-র সব রূপ, backtracking, branch and bound আর string অ্যালগরিদম।
- উন্নত তত্ত্ব (পর্ব ৬০–৬৭): amortized ও randomized বিশ্লেষণ, number theory, geometry, P বনাম NP, NP-hard সমস্যার মোকাবিলা, approximation আর ক্যাপস্টোন।
- প্রস্তুতি (পর্ব ৬৮–৭২): dry-run, MCQ, লিখিত ও ভাইভা, কোডিং ইন্টারভিউ আর এই পর্বের কনটেস্ট।
এরপর কোন পথে
নিয়মিত কনটেস্ট
সপ্তাহে অন্তত একটি অনলাইন রাউন্ড আর একটি virtual contest, পরদিন upsolving. ছয় মাস নিয়মিত থাকলে পার্থক্য নিজেই দেখবেন।
শুরু: এই পাতার সেট, পাঁচ ঘণ্টায়চাকরি ও ইন্টারভিউ
পর্ব ৭১-এর প্যাটার্নগুলো দিয়ে প্রতিদিন একটি সমস্যা, জোরে ভেবে আর সময় ধরে। পর্ব ৬৯–৭০ লিখিত পরীক্ষার জন্য।
পর্ব ৭১ভাষার গভীরে
C++-এর STL বা Python-এর কনটেস্ট-কৌশল আরও গভীরে শিখলে একই অ্যালগরিদম কম লাইনে, কম ভুলে লিখবেন।
C++ STL · Pythonপুরো রোডম্যাপ আবার
যে মডিউল দুর্বল লাগে, কোর্স হোম থেকে সেখানে ফিরে যান — বিশেষ করে DP আর গ্রাফ, যা কনটেস্টে সবচেয়ে বেশি আসে।
৯টি মডিউলভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১তিনজনের দলে একটি মাত্র কম্পিউটার — এই নিয়ম কেন, আর বাস্তব কাজের সাথে এর মিল কোথায়?
ICPC-র শুরু ১৯৭০ সালে, যুক্তরাষ্ট্রের Texas A&M বিশ্ববিদ্যালয়ে একটি স্থানীয় প্রতিযোগিতা হিসেবে; প্রথম World Finals হয় ১৯৭৭ সালে। তখন কম্পিউটার ছিল দামি আর দুর্লভ — একটি দলকে একটি যন্ত্রের সময় ভাগ করে নিতে হতো। আজ কম্পিউটার সস্তা, তবু নিয়মটি রয়ে গেছে, কারণ এটি এমন একটি দক্ষতা মাপে যা ব্যক্তিগত কনটেস্ট মাপে না: সীমিত সম্পদ ভাগ করে দল হিসেবে কাজ করা।
এক কম্পিউটারে তিনজন দ্রুত হতে পারেন শুধু যদি কেউ অপেক্ষায় বসে না থাকেন। তাই ভালো দলগুলো কাজ ভাগ করে: একজন টাইপ করছেন, একজন পরের সমস্যার প্রমাণ কাগজে লিখছেন, একজন প্রিন্ট করা কোডে বাগ খুঁজছেন। এটি অপারেটিং সিস্টেমের CPU scheduling-এর মতো — একটি প্রসেসর, অনেক কাজ, আর লক্ষ্য হলো প্রসেসর কখনো অলস না রাখা, আবার এমন কাজও না দেওয়া যা অন্য কিছুর অপেক্ষায় আটকে আছে।
সফটওয়্যার শিল্পে এর আত্মীয় হলো pair programming আর code review: দুজন একটি সমস্যা দেখলে একজনের অন্ধ জায়গা অন্যজনের চোখে পড়ে। কনটেস্টের “প্রিন্ট করে জোড়ায় ডিবাগ” ঠিক এই ধারণা। আর দলের মধ্যে স্পষ্ট যোগাযোগ — “এটি DSU, আমি পনেরো মিনিটে লিখব, আপনি তার মধ্যে G-এর প্রমাণ দেখুন” — বাস্তবের যেকোনো প্রকৌশল দলেও একই রকম কাজের।
মূল উপলব্ধি: ICPC শুধু অ্যালগরিদম নয়, সম্পদ আর মানুষ ব্যবস্থাপনারও পরীক্ষা। একটি কম্পিউটারকে সবচেয়ে ভালোভাবে ব্যবহার করতে শেখা মানে দলের সবার চিন্তাকে সমান্তরালে চালানো।
প্র ০২Penalty-তে প্রতিটি ভুল জমা ২০ মিনিট, আর শেষ ঘণ্টায় স্কোরবোর্ড ফ্রিজ — এই নকশা প্রতিযোগীদের আচরণ কীভাবে বদলায়?
Penalty-র নকশা আসলে একটি প্রণোদনার (incentive) ব্যবস্থা। প্রতিটি সমাধানের সময় যোগ হয় বলে দ্রুত সহজ সমস্যা শেষ করার প্রণোদনা থাকে। আর প্রতিটি ভুল জমায় বাড়তি সময় বলে “দেখি তো judge কী বলে” ধরনের অন্ধ জমা নিরুৎসাহিত হয় — জমার আগে নিজে যাচাই করার অভ্যাস তৈরি হয়। বাড়তি সময়টি খুব বড় হলে প্রতিযোগীরা ঝুঁকি নিতেই ভয় পেতেন, খুব ছোট হলে judge-কে debugger বানিয়ে ফেলতেন; প্রচলিত ২০ মিনিট এই দুইয়ের মাঝামাঝি একটি ভারসাম্য।
লক্ষ করুন, অসমাধিত সমস্যার ভুল জমার কোনো দাম নেই। ফলে শেষ মিনিটে একটি অনিশ্চিত সমাধান জমা দেওয়া প্রায় সবসময় যুক্তিসঙ্গত — হারানোর কিছু নেই। এ ধরনের হিসাব গেম থিওরির সরল প্রয়োগ।
স্কোরবোর্ড ফ্রিজের দুটি উদ্দেশ্য সাধারণত বলা হয়। প্রথমত নাটকীয়তা: সমাপনী অনুষ্ঠানে ফ্রিজের পরের জমাগুলো একে একে প্রকাশ হয়, আর র্যাংক চোখের সামনে বদলায়। দ্বিতীয়ত কৌশলগত: শেষ ঘণ্টায় দলগুলো প্রতিদ্বন্দ্বীর অবস্থা দেখে খেলতে পারে না — “ওরা H করে ফেলেছে, আমাদেরও করতে হবে” ধরনের প্রতিক্রিয়ার বদলে নিজের সেরা সিদ্ধান্তে মন দিতে হয়। এজন্যই ভালো দলগুলো ফ্রিজের আগেই শেষ ঘণ্টার পরিকল্পনা ঠিক করে।
মূল উপলব্ধি: নিয়ম জানাও দক্ষতার অংশ। penalty-র হিসাব বুঝলে কোন ক্রমে সমাধান করবেন আর কখন জমা দেবেন, সেই সিদ্ধান্তগুলো যুক্তিসঙ্গত হয়।
প্র ০৩দাবার জন্য বানানো একটি রেটিং পদ্ধতি কেন প্রোগ্রামিং কনটেস্টেও চলে — আর রেটিংকে কতটা গুরুত্ব দেবেন?
হাঙ্গেরীয়-আমেরিকান পদার্থবিদ Arpad Elo দাবা খেলোয়াড়দের দক্ষতা মাপার একটি পরিসংখ্যানভিত্তিক পদ্ধতি দেন, যা ১৯৬০ সালে মার্কিন দাবা ফেডারেশন আর ১৯৭০ সালে আন্তর্জাতিক দাবা ফেডারেশন FIDE গ্রহণ করে। মূল ধারণা সরল: দুজনের রেটিংয়ের পার্থক্য থেকে প্রত্যাশিত ফল অনুমান করা হয়; আসল ফল প্রত্যাশার চেয়ে ভালো হলে রেটিং বাড়ে, খারাপ হলে কমে, আর বাড়া-কমার পরিমাণ নির্ভর করে চমক কতটা বড় তার ওপর।
প্রোগ্রামিং কনটেস্টে একটি রাউন্ডে হাজারো প্রতিযোগী, তাই প্ল্যাটফর্মগুলো এই ধারণাকে বহু-খেলোয়াড়ের রূপে বদলে নেয়: আপনার রেটিং থেকে প্রত্যাশিত র্যাংক অনুমান করা হয়, আর আসল র্যাংক তার চেয়ে ভালো হলে রেটিং বাড়ে। খুঁটিনাটি প্ল্যাটফর্মভেদে আলাদা, কিন্তু মূল যুক্তি Elo-র আত্মীয়।
রেটিংয়ের সীমাবদ্ধতাও মনে রাখুন। এটি একটি নির্দিষ্ট ধরনের সমস্যায়, নির্দিষ্ট সময়ে, ব্যক্তিগত দ্রুততা মাপে — বড় সিস্টেম নকশা, দলের কাজ বা গবেষণার গভীরতা নয়। একটি খারাপ দিন (অসুস্থতা, ইন্টারনেট সমস্যা) রেটিং কমায়, অথচ দক্ষতা কমেনি। আর শুধু রেটিং বাড়ানোর জন্য সহজ সমস্যায় আটকে থাকলে শেখা থেমে যায়; উন্নতি আসে নিজের স্তরের একটু ওপরের সমস্যায় লড়াই করে।
মূল উপলব্ধি: রেটিং একটি কাজের থার্মোমিটার, লক্ষ্য নয়। দীর্ঘমেয়াদি প্রবণতা দেখুন, আর প্রতিটি কনটেস্টকে upsolving-এর জন্য নতুন সমস্যার উৎস হিসেবে ধরুন।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমকনটেস্টে একটি সমস্যা পড়া থেকে জমা দেওয়া পর্যন্ত ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- বিবরণ আর সীমা পড়ে লক্ষ্য complexity ঠিক করা
- নমুনা ইনপুট হাতে মিলিয়ে সমস্যাটি ঠিক বুঝেছি কি না দেখা
- ধারণা আর তার যুক্তি কাগজে লেখা
- কম্পিউটারে কোড লেখা
- নমুনা আর edge case চালিয়ে ৩০ সেকেন্ডের চেকলিস্ট মেলানো
- জমা দেওয়া
(সহজ) একটি সমস্যায় n ≤ 5000 আর সময়সীমা ১ সেকেন্ড। কোন complexity সবচেয়ে বেশি যা সাধারণত নিরাপদ?
- O(n³)
- O(n²)
- O(2n)
- O(n² log n)
(মাঝারি) n = 106 টি vertex-এর একটি শিকল-আকৃতির tree-তে recursive DFS লেখা একটি C++ প্রোগ্রাম নমুনায় ঠিক চলে, কিন্তু judge বড় টেস্টে Runtime Error দেয়। সবচেয়ে সম্ভাব্য কারণ কী?
- অ্যালগরিদমটি খুব ধীর
- উত্তরের বিন্যাস ভুল
- judge-এ compile হয়নি
- recursion-এর গভীরতায় stack overflow
(সহজ) ICPC-ধাঁচের র্যাংকিংয়ে দুটি দল সমান সংখ্যক সমস্যা সমাধান করলে সাধারণত কে এগিয়ে থাকে?
- যে দলের মোট penalty time কম
- যে দল কম বার জমা দিয়েছে
- যে দলের কোড ছোট
- যে দল প্রথম সমস্যাটি আগে সমাধান করেছে
(মাঝারি) একটি দল সমস্যা A সমাধান করল ৩০ মিনিটে, তার আগে দুটি ভুল জমাসহ; B সমাধান করল ১০০ মিনিটে, কোনো ভুল ছাড়া; C-তে তিনটি ভুল জমা দিল কিন্তু সমাধান হয়নি। প্রতিটি ভুল জমায় ২০ মিনিট ধরলে মোট penalty কত?
- 130
- 230
- 170
- 100
(মাঝারি) একাধিক টেস্ট কেসের একটি সমস্যায় প্রোগ্রাম প্রথম টেস্টে ঠিক উত্তর দেয়, কিন্তু দ্বিতীয় টেস্ট থেকে ভুল। কোডে একটি global অ্যারে cnt আছে, যা প্রতিটি টেস্টে শুধু বাড়ানো হয়। সবচেয়ে সম্ভাব্য কারণ কী?
- প্রোগ্রামটি দ্বিতীয় টেস্টে ধীর হয়ে যায়
- global অ্যারে শুরুতে আবর্জনা মান রাখে
- আগের টেস্টের মান
cnt-এ থেকে গেছে, প্রতিটি টেস্টের আগে মোছা হয়নি - fast I/O ব্যবহারের কারণে ইনপুট ভুল পড়া হচ্ছে
(সহজ) C++-এ int a = 100000, b = 100000; long long x = a * b; — x-এর মান নিয়ে কোনটি ঠিক?
- গুণটি int-এ হয়ে overflow হয়, তাই x ভুল (undefined behavior)
- x = 10000000000, কারণ x long long
- কোডটি compile-ই হবে না
- C++ নিজে থেকেই গুণটি long long-এ করে
(সহজ) প্রতিযোগী প্রোগ্রামিংয়ে “upsolving” বলতে কী বোঝায়?
- কনটেস্টের সময় কঠিন সমস্যা আগে সমাধান করা
- অন্য কারও সমাধান কপি করে জমা দেওয়া
- রেটিং বাড়াতে সহজ সমস্যা বারবার জমা দেওয়া
- কনটেস্ট শেষে যে সমস্যাগুলো পারেননি, সেগুলো বুঝে নিজে সমাধান করা
(মাঝারি) একটি সমস্যায় n ≤ 18 টি শহর, আর প্রশ্ন সব শহর একবার করে ঘোরার সবচেয়ে কম খরচ। কোন ধরনের সমাধান প্রত্যাশিত?
- সব permutation পরীক্ষা, O(n!)
- bitmask DP, O(2n · n²)
- সবসময় সবচেয়ে কাছের শহরে যাওয়ার greedy
- একবার Dijkstra চালানো
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- নিয়ম জানুন: ICPC-তে সমাধানের সংখ্যা, তারপর penalty — সমাধানের মিনিট আর প্রতিটি ভুল জমার বাড়তি সময়; অসমাধিত সমস্যার ভুল জমা গোনা হয় না।
- রায় পড়ুন ক্রমে: CE → RE → TLE → MLE → WA; আগে সস্তা কারণ, শেষে ধারণা।
- সীমা আগে: n ≤ 10 → n!, ≤ 20 → 2n, ≤ 500 → n³, ≤ 5000 → n², ≤ 106 → n log n, ≤ 1018 → log n বা সূত্র; সাথে সংখ্যার আকার দেখে
long long. - চারটি পরিচিত বাগ: overflow, আগের টেস্টের না-মোছা ডেটা, off-by-one, গভীর recursion — জমার আগে ৩০ সেকেন্ডের চেকলিস্ট।
- শেখা হয় পরে: upsolving, virtual contest, বিষয়ের মই, আর অজানা WA-তে brute force দিয়ে stress test.
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- ডেটা স্ট্রাকচার ও অ্যালগরিদম · কোর্স হোম৭২টি পর্বপুরো কোর্সের রোডম্যাপ — দুর্বল মডিউলে ফিরে যান।
- ড্যাশবোর্ড · মডিউল টেস্ট ও ফাইনালপরীক্ষাঅগ্রগতি, মডিউল টেস্ট আর ফাইনাল পরীক্ষা — লগইন করে।
- পর্ব ৬৭ · ক্যাপস্টোনstress testingএকটি কঠিন সমস্যা শুরু থেকে শেষ, brute force আর ১০০০ টেস্টের stress test.
- পর্ব ৭১ · কোডিং ইন্টারভিউর সমস্যাইন্টারভিউপ্যাটার্ন চিনে দ্রুত সমাধানে পৌঁছানো।
- C++ প্রোগ্রামিং · STL algorithmভাষার দিক
sort,lower_boundআর লাইব্রেরির অ্যালগরিদম। - Python প্রোগ্রামিং · অ্যালগরিদম ও কনটেস্টে Pythonপাশের ট্র্যাকদ্রুত ইনপুট,
heapqআর বড় সংখ্যার সুবিধা।