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

অ্যালগরিদম ও কনটেস্টে Java — সেকেন্ড না বছর, সিদ্ধান্ত পদ্ধতিতে

Algorithms and Competitive Programming in Java — Big-O, Fast I/O and Core Techniques

Taught in Bangla, this lesson applies Java to algorithms and competitive programming: estimating complexity from constraints, fast input and output with BufferedReader and PrintWriter, a contest template, prefix sums, two pointers, binary search on the answer, greedy, hashing, recursion and backtracking, dynamic programming, BFS, DFS and Dijkstra, and online judge practice.

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

  • constraints দেখে কোন complexity চলবে তা আগেভাগে আন্দাজ করা
  • Java-তে কনটেস্টের টেমপ্লেট — দ্রুত ইনপুট, এক জায়গায় আউটপুট, আর চেনা ফাঁদ এড়ানো
  • prefix sum, two pointers, উত্তরের ওপর binary search, greedy আর hashing — বাস্তব উদাহরণে
  • recursion থেকে backtracking, আর memoization থেকে bottom-up dynamic programming
  • adjacency list-এ BFS, iterative DFS আর Dijkstra; online judge-এ অনুশীলনের পথ

১ · অ্যালগরিদম মানে রেসিপি — একই সমস্যা, সেকেন্ড না বছর

একটি অ্যালগরিদম হলো একটি নির্ভুল রেসিপি: কোন ক্রমে কী কী ধাপ করলে একটি সমস্যার উত্তর নিশ্চিতভাবে পাওয়া যায়। একই খাবার যেমন নানা পথে রাঁধা যায়, একই সমস্যারও নানা সমাধান — আর সময়ের পার্থক্য বিশাল হতে পারে।

একটি বাস্তব প্রশ্ন ভাবুন। একটি অনলাইন দোকানের এক বছরের প্রতিদিনের বিক্রির হিসাব আছে, আর ম্যানেজার দিনভর জিজ্ঞেস করছেন: “১২ থেকে ৪৫ নম্বর দিনে মোট বিক্রি কত?”, “২০০ থেকে ৩০০?” — এমন দুই লাখ প্রশ্ন। প্রথম পদ্ধতি: প্রতিটি প্রশ্নে সেই দিনগুলো লুপ চালিয়ে যোগ করা। দ্বিতীয় পদ্ধতি: একবার একটি চলমান যোগফলের তালিকা বানিয়ে প্রতিটি প্রশ্নের উত্তর একটি বিয়োগে দেওয়া (অংশ ৩-এ দেখবেন)। দিন দুই লাখ আর প্রশ্নও দুই লাখ হলে প্রথম পদ্ধতিতে প্রায় ৪ × ১০১০ যোগ — কয়েক মিনিট; দ্বিতীয়টিতে কয়েক লাখ — চোখের পলক। ভাষা, কম্পিউটার, প্রোগ্রামার একই; পার্থক্য শুধু পদ্ধতিতে।

Big-O — কাজ কীভাবে বাড়ে

পর্ব ৩০–৩২-এ Big-O চিহ্নগুলো দেখেছেন। মূল কথা আবার মনে করি: Big-O সেকেন্ড মাপে না, মাপে ইনপুটের আকার n বাড়লে কাজের পরিমাণ কোন হারে বাড়ে। ধ্রুবক গুণক আর ছোট পদ বাদ দেওয়া হয়, কারণ n বড় হলে বৃদ্ধির ধরনটাই সব ঠিক করে দেয়। একে বলে time complexity. n = ১০৫ হলে ছবিটা এমন:

complexityn = ১০৫-এ মোটামুটি কাজএক সেকেন্ডের সীমায়চেনা উদাহরণ
O(1)১নিশ্চিন্তarray-এর ঘর পড়া, prefix sum-এ একটি প্রশ্ন
O(log n)প্রায় ১৭নিশ্চিন্তbinary search
O(n)১০৫নিশ্চিন্তএকবার পুরো তালিকা দেখা
O(n log n)প্রায় ১.৭ × ১০৬সাধারণত ঠিক আছেsort, heap-এ n বার কাজ
O(n২)১০১০অনেক বেশি — TLEদুটি nested লুপ
O(2n)কল্পনাতীতশুধু n ২০–২৫-এর মতো ছোট হলেসব subset দেখা

কনটেস্ট আর ইন্টারভিউতে সবচেয়ে কাজের আন্দাজ: সাধারণ একটি কম্পিউটার এক সেকেন্ডে মোটামুটি ১০৮-এর কাছাকাছি সরল কাজ করে (যোগ, তুলনা, array-এর ঘর পড়া)। এটা মোটা দাগের হিসাব — CPU, cache, ভাষা আর কাজের ধরন অনুযায়ী কয়েক গুণ এদিক-ওদিক হয়। Java-তে JVM চালু হওয়ার একটি স্থির সময়ও যোগ হয়। তবু এই আন্দাজ দিয়েই কোড লেখার আগে বলা যায় একটি পদ্ধতি চলবে কি না।

constraints পড়ে পদ্ধতি বেছে নেওয়া

প্রতিটি কনটেস্ট-সমস্যার নিচে ইনপুটের সীমা (constraints) লেখা থাকে — যেমন “1 ≤ n ≤ 2·105”. অভিজ্ঞ প্রতিযোগীরা এটা আগে পড়েন, কারণ সীমাই প্রায় বলে দেয় কোন পদ্ধতি লাগবে:

n-এর সীমাযে complexity সাধারণত চলেইঙ্গিত
≤ ২০O(2n), O(n!) — ছোট হলেসব সম্ভাবনা দেখা, backtracking
≤ ৫০০O(n৩)তিন স্তরের লুপ, ছোট DP
≤ ৫০০০O(n২)দুই স্তরের লুপ, দুই মাত্রার DP
≤ ২ × ১০৫ থেকে ১০৬O(n log n) বা O(n)sort, binary search, prefix sum, heap, BFS
≤ ১০১৮O(log n) বা O(1)গাণিতিক সূত্র, উত্তরের ওপর binary search

এই কৌশলের পুরো নাম competitive programming: নির্দিষ্ট সময় আর মেমরির সীমায় স্পষ্টভাবে বলা সমস্যার সমাধান জমা দেওয়া, আর একটি online judge গোপন টেস্টে সেটা চালিয়ে রায় দেয়। কোডিং ইন্টারভিউও এই দক্ষতাই পরীক্ষা করে। Java কনটেস্টের পুরোপুরি স্বীকৃত ভাষা; শুধু কয়েকটি পারফরম্যান্স-অভ্যাস শিখে নিতে হয়।

২ · কনটেস্টে Java — দ্রুত ইনপুট-আউটপুট আর একটি টেমপ্লেট

পর্ব ০৯-এ Scanner দিয়ে ইনপুট পড়েছেন — শেখার জন্য চমৎকার। কিন্তু দশ লাখ সংখ্যা পড়তে হলে সেটা ধীর, কারণ Scanner প্রতিটি token চিনতে regular expression ব্যবহার করে আর অনেক যাচাই করে। অনেক সময় সঠিক অ্যালগরিদমও শুধু ইনপুট পড়তে গিয়েই সময়সীমা পেরিয়ে যায় — রায় আসে TLE.

গার্মেন্টস কারখানার গুদাম থেকে বোতাম আনার কথা ভাবুন। Scanner যেন প্রতিটি বোতামের জন্য আলাদা করে গুদামে যাওয়া আর প্রতিবার যন্ত্রে মাপা। BufferedReader একবারে এক বস্তা আনে, আর StringTokenizer শুধু ফাঁকা জায়গা দেখে বোতাম আলাদা করে।

  • ইনপুট: BufferedReader পুরো লাইন একবারে পড়ে (readLine()), আর StringTokenizer লাইনটিকে ফাঁকা জায়গায় ভেঙে token দেয়; তারপর Integer.parseInt. StringTokenizer পুরোনো (legacy) class — Java-র documentation সাধারণ কোডে split ব্যবহার করতে বলে — কিন্তু এটা হালকা আর দ্রুত বলে কনটেস্টে খুব প্রচলিত। আরও দ্রুত চাইলে অনেকে DataInputStream থেকে বাইট পড়ে নিজেই সংখ্যা বানানোর একটি ছোট reader লেখেন।
  • আউটপুট: লাখ লাইন ছাপতে প্রতিবার System.out.println ডাকা ধীর, কারণ System.out সাধারণত প্রতিটি লাইনের পর flush করে — প্রতিবার অপারেটিং সিস্টেমের কাছে ছোট একটি লেখা পাঠায়। তার বদলে StringBuilder-এ সব জমান, অথবা PrintWriter (ভেতরে BufferedWriter) ব্যবহার করুন, আর শেষে একবার flush. flush ভুলে গেলে কিছুই ছাপা না হতে পারে — কনটেস্টে খুব পরিচিত “আমার আউটপুট কই গেল” মুহূর্ত।

এই দুটি মিলিয়ে একটি টেমপ্লেট, যা অনেক Java প্রতিযোগী প্রতিটি সমস্যার শুরুতে বসান:

import java.io.*;
import java.util.*;

public class Main {                    // judge-এ public চলে; আমাদের runner-এ public বাদ
    static BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    static StringTokenizer st;
    static String next() throws IOException {
        while (st == null || !st.hasMoreTokens()) st = new StringTokenizer(br.readLine());
        return st.nextToken();
    }
    static int nextInt() throws IOException { return Integer.parseInt(next()); }
    static long nextLong() throws IOException { return Long.parseLong(next()); }

    public static void main(String[] args) throws IOException {
        PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out)));
        int t = nextInt();                 // কতগুলো test case
        while (t-- > 0) solve(out);
        out.flush();                       // শেষে একবার
    }
    static void solve(PrintWriter out) throws IOException {
        // এখানে আসল সমাধান
    }
}

next()-এর while লুপটি খেয়াল করুন: বর্তমান লাইনের token শেষ হলে পরের লাইন পড়ে। তাই সংখ্যাগুলো এক লাইনে থাকুক বা আলাদা লাইনে — কিছু যায় আসে না। Codeforces, AtCoder-এর মতো judge-এ সাধারণত public class Main লেখা হয় (অনেক judge-এ class-এর নাম Main হওয়া বাধ্যতামূলক; প্রতিটি প্ল্যাটফর্মের নির্দেশনা দেখে নিন)। এই কোর্সের ব্রাউজার-runner ফাইলের নাম আলাদা রাখে বলে এখানে চালানো কোডে class Main লেখা হয় public ছাড়া — পর্ব ০৪-এ যেমন বলা হয়েছিল।

Java-র চেনা কনটেস্ট-ফাঁদ

সঠিক অ্যালগরিদম লিখেও Java-তে কয়েকটি জায়গায় উত্তর ভুল (WA) বা সময় পার (TLE) হয়। এগুলো আগে থেকে চিনে রাখুন:

  1. int overflow. int-এর সীমা প্রায় ২.১ × ১০৯. int a = 100000, b = 100000; হলে a * b ছাপে 1410065408 — দশ বিলিয়ন নয়। long c = a * b; লিখলেও একই ফল, কারণ গুণটা আগে int-এ হয়। ঠিক উপায় (long) a * b → 10000000000 (তিনটিই চালিয়ে যাচাই করা)। যোগফল, গুণফল আর দূরত্বে শুরু থেকেই long নিন। পর্ব ০৬ আর ০৭-এ এর বিস্তারিত।
  2. Integer-এ ==. List<Integer>-এর দুটি উপাদান == দিয়ে তুলনা করলে মান নয়, object-এর ঠিকানা তুলনা হয় (reference equality). ১২৭-এর মতো ছোট সংখ্যায় Java-র Integer cache-এর কারণে true আসে, কিন্তু ১০০০ আর ১০০০-এ false (যাচাই করা)। equals ব্যবহার করুন, বা int-এ নামিয়ে তুলনা করুন। পর্ব ০৬ দেখুন।
  3. গভীর recursion-এ StackOverflowError. প্রতিটি method-ডাক call stack-এ একটি ফ্রেম নেয় (পর্ব ১২)। একটি শিকলের মতো গ্রাফে দশ লাখ গভীর DFS বা দশ লাখ ধাপের recursion সাধারণ thread-এর stack ভরিয়ে ফেলে।
  4. Arrays.sort(int[])-এর worst case. primitive array-এ Java Dual-Pivot Quicksort ব্যবহার করে (পর্ব ৩৩)। পুরোনো JDK-তে (যেমন বহু বছর judge-এ থাকা Java 8) ইচ্ছে করে বানানো adversarial input-এ এটা O(n২)-এর দিকে যেতে পারত, আর Codeforces-এর মতো প্ল্যাটফর্মে অন্যের সমাধান “hack” করতে এমন anti-quicksort টেস্ট ব্যবহার হতো। JDK 14-এ বাস্তবায়নটি নতুন করে লেখা হয়, আর সাধারণভাবে বলা হয় যে তাতে recursion অস্বাভাবিক গভীর হলে heap sort-এ সরে যাওয়ার ব্যবস্থা যোগ হয়েছে — ফলে ঝুঁকি অনেক কমেছে। তবু judge কোন JDK চালায় জানা না থাকলে নিরাপদ অভ্যাস: sort-এর আগে array এলোমেলো (shuffle) করা, অথবা Integer[] বা ArrayList sort করা — object-এর sort হলো TimSort, যার worst case নিশ্চিতভাবে O(n log n), যদিও boxing-এর খরচ আছে।
  5. HashMap আর anti-hash টেস্ট. অনেক key একই bucket-এ ফেলার মতো করে বানানো টেস্টে HashMap ধীর হয়ে যায় (hash flooding). Java 8 থেকে বড় bucket একটি গাছে রূপান্তরিত হয়, তাই ক্ষতি সীমিত, কিন্তু ধীরগতি থেকেই যায়। মানের পরিসর ছোট হলে int[] দিয়ে গণনা প্রায়ই দ্রুততর।
  6. Boxing-এর খরচ. ArrayList<Integer>, HashMap<Integer, Integer>-এর প্রতিটি সংখ্যা একটি আলাদা object — মেমরি বেশি, cache-এ ছড়ানো, garbage collector-এর কাজ বাড়ে। দশ লাখ সংখ্যার জন্য সম্ভব হলে int[] আর long[].

সমাধান দুটি। প্রথমটি পরিষ্কার: recursion-কে একটি নিজের ArrayDeque stack দিয়ে iterative করুন (অংশ ৭-এর DFS). দ্বিতীয়টি কনটেস্টের পরিচিত কৌশল: বড় stack দিয়ে একটি নতুন thread চালানো। Thread-এর constructor-এ চতুর্থ argument হলো চাওয়া stack-এর আকার (বাইটে), যা JVM একটি ইঙ্গিত হিসেবে নেয়:

class Main {
    static long sum(int n) { return n == 0 ? 0 : n + sum(n - 1); }
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(null, () -> System.out.println(sum(1_000_000)), "solver", 1L << 28);
        t.start();                          // ২৫৬ MB stack চেয়ে নতুন thread
        t.join();                           // ছাপে 500000500000
        System.out.println(sum(1_000_000)); // main thread-এ: StackOverflowError
    }
}
500000500000
Exception in thread "main" java.lang.StackOverflowError
	at Main.sum(Main.java:2)
	at Main.sum(Main.java:2)
	... (একই লাইন হাজার হাজার বার)

একই method, একই দশ লাখ গভীরতা: বড় stack-এর thread-এ উত্তর এল, সাধারণ main thread-এ StackOverflowError (Wandbox-এর OpenJDK 21-এ চালিয়ে যাচাই করা; সেখানে ফাইলের নাম আলাদা দেখায়, আর stack-এর আকার যন্ত্রভেদে আলাদা হতে পারে)। তবে মনে রাখবেন, stack-এর আকার JVM-এর কাছে অনুরোধ মাত্র, আর judge-এর মেমরি-সীমাও আছে — iterative সমাধানই বেশি নির্ভরযোগ্য।

৩ · ছাঁচ ১ — prefix sum আর two pointers

Prefix sum — রেঞ্জের যোগফল এক বিয়োগে

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

1long[] pre = new long[n + 1];  // pre[0] = 02for (int i = 1; i <= n; i++)3    pre[i] = pre[i - 1] + a[i];4// প্রশ্ন: দিন l থেকে r5sum = pre[r] - pre[l - 1];
বিক্রি a আর prefix pre
i
a[i]
pre[0..6]
  1. শুরু। pre array-এর আকার n+1, আর pre[0] = 0 — “কোনো দিনই না নিলে যোগফল শূন্য”.
  2. দিন ১। আগের মোটের সাথে আজকের বিক্রি: ০ + ৫ = ৫.
  3. দিন ২। ৫ + ৩ = ৮.
  4. দিন ৩। ৮ + ৮ = ১৬.
  5. দিন ৪। ১৬ + ২ = ১৮.
  6. দিন ৫। ১৮ + ৭ = ২৫.
  7. দিন ৬ — তালিকা তৈরি। ২৫ + ৪ = ২৯, পুরো ছয় দিনের মোট। এই পুরো কাজ একবারই, O(n).
  8. প্রশ্ন: দিন ২ থেকে ৫। প্রথম ৫ দিনের মোট (২৫) থেকে প্রথম ১ দিনের মোট (৫) বাদ = ২০. প্রশ্ন যত লম্বা রেঞ্জেরই হোক, উত্তর একটি বিয়োগে — O(1).

এবার অংশ ২-এর টেমপ্লেটে বসিয়ে চালিয়ে দেখুন। ইনপুটের প্রথম লাইনে দিনের সংখ্যা আর প্রশ্নের সংখ্যা, তারপর বিক্রি, তারপর প্রতিটি প্রশ্নের l আর r:

import java.io.*;
import java.util.*;

class Main {
    static BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    static StringTokenizer st;
    static String next() throws IOException {
        while (st == null || !st.hasMoreTokens()) st = new StringTokenizer(br.readLine());
        return st.nextToken();
    }
    static int nextInt() throws IOException { return Integer.parseInt(next()); }

    public static void main(String[] args) throws IOException {
        int n = nextInt(), q = nextInt();
        long[] pre = new long[n + 1];                 // pre[i] = প্রথম i দিনের মোট
        for (int i = 1; i <= n; i++) pre[i] = pre[i - 1] + nextInt();
        StringBuilder out = new StringBuilder();
        while (q-- > 0) {
            int l = nextInt(), r = nextInt();
            out.append(pre[r] - pre[l - 1]).append('\n');   // প্রতিটি প্রশ্ন O(1)
        }
        PrintWriter pw = new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out)));
        pw.print(out);
        pw.flush();                                   // শেষে একবার
    }
}

ইনপুট দেওয়া আছে: 6 3, তারপর 5 3 8 2 7 4, তারপর তিনটি প্রশ্ন 2 5, 1 6, 4 4. আউটপুট তিন লাইন: 20, 29 আর 2 — দিন ২–৫, পুরো ছয় দিন, আর শুধু দিন ৪. তিনটি অভ্যাস খেয়াল করুন। প্রথম, pre হলো long[]: দুই লাখ দিনে প্রতিদিন ১০৯ পর্যন্ত বিক্রি হলে মোট ২ × ১০১৪, যা int-এ ধরে না। দ্বিতীয়, প্রতিটি উত্তর সাথে সাথে না ছেপে StringBuilder-এ জমানো হয়েছে, আর শেষে PrintWriter দিয়ে একবারে flush. তৃতীয়, nextInt() লাইন ভাঙা নিয়ে মাথা ঘামায় না — প্রশ্নগুলো এক লাইনে দিলেও একই ফল। নিজের ইনপুট বদলে চালিয়ে দেখুন।

Two pointers আর sliding window

আরেকটি প্রশ্ন: “সবচেয়ে বেশি কত দিন পরপর এমন আছে, যেখানে মোট বিক্রি ১৫ হাজারের বেশি নয়?” প্রতিটি শুরু আর প্রতিটি শেষ জোড়া দেখলে O(n২). Two pointers কৌশলে দুটি index — l আর r — একটি “জানালা” ধরে রাখে। r ডানে এগিয়ে নতুন দিন যোগ করে; যোগফল সীমা ছাড়ালে l ডানে সরে পুরোনো দিন বাদ দেয়। দুটি pointer-ই শুধু সামনে যায়, প্রত্যেকে সর্বোচ্চ n ধাপ — মোট O(n). চলমান এই জানালার নাম sliding window.

int best = 0, l = 0; long sum = 0;
for (int r = 0; r < a.length; r++) {
    sum += a[r];                          // ডানে নতুন দিন
    while (sum > limit) sum -= a[l++];    // সীমা ছাড়ালে বাঁ থেকে বাদ
    best = Math.max(best, r - l + 1);
}

a = {5, 3, 8, 2, 7, 4}, limit = 15 হলে best হয় 3 (যেমন ৩ + ৮ + ২ = ১৩), চালিয়ে যাচাই করা। এই কৌশল কাজ করে কারণ সব মান ধনাত্মক: জানালা বড় করলে যোগফল শুধু বাড়ে, ছোট করলে শুধু কমে। ঋণাত্মক মান থাকলে এই একমুখী যুক্তি ভেঙে যায়। আরেকটি রূপ: সাজানো দামের তালিকায় এমন দুটি জিনিস খোঁজা যাদের দাম মিলে ঠিক বাজেট — এক pointer শুরুতে, আরেকটি শেষে; যোগফল কম হলে বাঁ pointer ডানে, বেশি হলে ডান pointer বাঁয়ে। প্রতি ধাপে একটি সম্ভাবনা বাদ যায়, তাই O(n). স্থির আকারের জানালার সর্বোচ্চ মান খোঁজার Deque-কৌশল পর্ব ৩২-এ দেখেছেন।

৪ · ছাঁচ ২ — উত্তরের ওপর binary search আর greedy

উত্তরের ওপর binary search

পর্ব ১৩-এ binary search দেখেছেন: সাজানো array-এ একটি মান খুঁজতে প্রতিবার অর্ধেক বাদ। এর একটি শক্তিশালী রূপ আছে, যেখানে আমরা array-তে নয়, সম্ভাব্য উত্তরের পরিসরে খুঁজি — binary search on the answer.

একটি বাস্তব সমস্যা: একটি গুদাম থেকে ৭টি চালান ক্রম মেনে পাঠাতে হবে, ওজন (টনে) ৪, ৮, ৩, ৬, ৫, ৭, ২. প্রতিদিন একটি ট্রাক একবার যায়; ক্রম ভাঙা যাবে না। ৩ দিনে সব পাঠাতে ট্রাকের ক্ষমতা সবচেয়ে কম কত হলে চলে? সরাসরি “সেরা উত্তর” বের করা কঠিন। কিন্তু উল্টো প্রশ্নটা সহজ: “ক্ষমতা X হলে ৩ দিনে হবে কি?” — চালানগুলো ক্রমে ট্রাকে তুলুন, না ধরলে পরের দিন নতুন ট্রাক; দিন গুনুন। এটা O(n).

মূল পর্যবেক্ষণ হলো একঘেয়েমি (monotonicity): ক্ষমতা ১৪-তে হলে ১৫, ১৬-তেও হবে; ১৩-তে না হলে ১২-তেও হবে না। তাই সম্ভাব্য উত্তরের সারি দেখতে এমন — ✘ ✘ ✘ … ✘ ✔ ✔ ✔ … — আর আমাদের খুঁজতে হবে প্রথম ✔. সবচেয়ে ভারী চালান (৮) ধরতে হবেই, তাই নিচের সীমা ৮; সব একদিনে নিলে মোট ৩৫, তাই ওপরের সীমা ৩৫.

1lo = max(w) = 8;  hi = sum(w) = 35;2while (lo < hi) {3    mid = lo + (hi - lo) / 2;4    if (canShip(mid)) hi = mid;5    else lo = mid + 1;6}   // উত্তর = lo
খোঁজার পরিসর
lo
hi
mid
canShip
  1. পরিসর ঠিক করা। উত্তর ৮-এর কম হতে পারে না (৮ টনের চালান তুলতেই হবে), আর ৩৫-এর বেশি লাগে না (সব একদিনে)। এই দুই সীমার মধ্যে কোথাও প্রথম ✔.
  2. mid = ২১ — পারা যায়। মাত্র ২ দিনে হয়ে যায়। তাহলে উত্তর ২১ বা তার কম — hi = 21. ২১ নিজেও উত্তর হতে পারে, তাই hi = mid.
  3. mid = ১৪ — এখনো পারা যায়। ঠিক ৩ দিন। আরও কম ক্ষমতায় চলে কি না দেখতে hi = 14.
  4. mid = ১১ — পারা যায় না। ৪ দিন লাগে। তাহলে ১১ বা তার কম কোনো মানই চলবে না — lo = 12.
  5. mid = ১৩ — পারা যায় না। ৫ + ৭ = ১২-এর পর ২ যোগ করলে ১৪, যা ১৩ ছাড়ায় — চতুর্থ দিন লাগে। lo = 14.
  6. থামা। lo আর hi মিলে গেল: উত্তর ১৪. পরিসর ২৮ হলেও check লেগেছে ৪টি; পরিসর ১০১৮ হলেও লাগত মাত্র প্রায় ৬০টি — log₂ করে কমে। মোট কাজ O(n log S), যেখানে S হলো পরিসরের আকার।
import java.util.StringTokenizer;

class Main {
    static int[] w;
    static boolean canShip(long cap, int days) {      // এই ক্ষমতায় days দিনে হবে?
        int used = 1; long load = 0;
        for (int x : w) {
            if (load + x > cap) { used++; load = 0; }  // আজ আর ধরবে না, কাল নতুন ট্রাক
            load += x; }
        return used <= days;
    }
    public static void main(String[] args) throws Exception {
        StringTokenizer in = new StringTokenizer(new String(System.in.readAllBytes()));
        int n = Integer.parseInt(in.nextToken()), days = Integer.parseInt(in.nextToken());
        w = new int[n];
        long lo = 0, hi = 0;
        for (int i = 0; i < n; i++) {
            w[i] = Integer.parseInt(in.nextToken());
            lo = Math.max(lo, w[i]); hi += w[i];       // উত্তর এই দুইয়ের মাঝে
        }
        while (lo < hi) {
            long mid = lo + (hi - lo) / 2;
            boolean ok = canShip(mid, days);  System.out.println("check(" + mid + ") = " + ok);
            if (ok) hi = mid; else lo = mid + 1;
        }
        System.out.println("সর্বনিম্ন ক্ষমতা: " + lo + " টন");
    }
}

ইনপুট: 7 3 (৭টি চালান, ৩ দিন), তারপর ওজনগুলো। আউটপুট পাঁচ লাইন: check(21) = true, check(14) = true, check(11) = false, check(13) = false, আর সর্বনিম্ন ক্ষমতা: 14 টন — stepper-এর ধাপগুলোর সাথে হুবহু মেলে। এখানে পুরো ইনপুট একবারে পড়ার আরেকটি কৌশল দেখুন: System.in.readAllBytes() (Java 9 থেকে) সব বাইট পড়ে নেয়, আর একটি StringTokenizer পুরোটা ভাঙে। mid = lo + (hi - lo) / 2 লেখার কারণ: (lo + hi) / 2-তে যোগফল সীমা ছাড়িয়ে overflow করতে পারে — বহু পুরোনো এই অ্যালগরিদমের এই bug Java-র নিজের library-তেও বহু বছর লুকিয়ে ছিল (ভাবনার প্রশ্ন ১ দেখুন)। days বদলে ২ বা ৭ দিয়ে চালিয়ে দেখুন।

Greedy — প্রতি ধাপে এখনকার সেরা

Greedy পদ্ধতি প্রতিটি ধাপে সেই মুহূর্তের সবচেয়ে ভালো দেখানো সিদ্ধান্ত নেয়, আর কখনো ফিরে তাকায় না। দোকানদার ১৮৮৮ টাকা ফেরত দিতে চান, সবচেয়ে কম নোট-কয়েনে। সরলতার জন্য ধরুন নোট-কয়েন ১, ২, ৫, ১০, ২০, ৫০, ১০০, ৫০০, ১০০০ টাকা (বাংলাদেশের বেশি প্রচলিতগুলো; ২০০ টাকার নোট এখানে বাদ রাখা হয়েছে)। স্বাভাবিক কৌশল: যত বড় নোট দেওয়া যায় দিন, তারপর বাকিটার জন্য আবার একই কাজ।

int[] notes = {1000, 500, 100, 50, 20, 10, 5, 2, 1};
int amount = 1888, count = 0;
for (int nt : notes) {
    count += amount / nt;        // এই নোট কয়টি দেওয়া যায়
    amount %= nt;                // বাকি টাকা
}
// count = 11: 1000x1 500x1 100x3 50x1 20x1 10x1 5x1 2x1 1x1

ফল ১১টি নোট (যাচাই করা)। এই নোট-ব্যবস্থায় greedy সবসময় সেরা উত্তর দেয় — এমন ব্যবস্থাকে বলে “canonical”, আর বেশিরভাগ দেশের মুদ্রা এভাবেই নকশা করা। কিন্তু সাবধান: greedy সব জায়গায় ঠিক নয়। ধরুন কোনো দেশে কয়েন ১, ৩ আর ৪. ৬ টাকা দিতে greedy নেবে ৪ + ১ + ১ = ৩টি কয়েন, অথচ ৩ + ৩ = ২টিই যথেষ্ট (যাচাই করা)। প্রথম ধাপের “সেরা” সিদ্ধান্ত পরে ক্ষতি করল। তাই greedy লেখার আগে একটি ছোট প্রমাণ বা অন্তত একটি পাল্টা-উদাহরণ খোঁজা জরুরি।

একটি ক্লাসিক জায়গা যেখানে greedy প্রমাণসহ সঠিক: activity selection — একটি অডিটোরিয়ামে না মিলিয়ে সবচেয়ে বেশি অনুষ্ঠান রাখতে অনুষ্ঠানগুলো শেষ হওয়ার সময় অনুযায়ী sort করে যেটা আগে শেষ হয় সেটা নিন, কারণ সে বাকিদের জন্য সবচেয়ে বেশি সময় ছেড়ে দেয়। অনেক greedy সমস্যার আসল কাজ হলো “কোন ক্রমে sort করব”.

৫ · ছাঁচ ৩ — hashing, recursion আর backtracking

Hashing দিয়ে গণনা

“কোন শহর থেকে কতগুলো অর্ডার এসেছে?”, “কোন সংখ্যা সবচেয়ে বেশিবার আছে?”, “আগে দেখেছি কি?” — এমন প্রশ্নে HashMap গড়ে O(1)-এ কাজ করে (পর্ব ৩১)। গণনার সবচেয়ে পরিষ্কার লেখা merge:

Map<String, Integer> freq = new TreeMap<>();       // ছাপার ক্রম নির্দিষ্ট রাখতে TreeMap
for (String city : "dhaka khulna dhaka sylhet dhaka khulna".split(" "))
    freq.merge(city, 1, Integer::sum);                // না থাকলে 1, থাকলে +1
System.out.println(freq);                             // {dhaka=3, khulna=2, sylhet=1}

গতি দরকার হলে HashMap নিন (গড়ে O(1)), নির্দিষ্ট ক্রম দরকার হলে TreeMap (O(log n), সাজানো)। আর key যদি ০ থেকে ১০৬-এর মতো ছোট পরিসরের পূর্ণসংখ্যা হয়, একটি int[] count HashMap-এর চেয়ে অনেক দ্রুত — boxing নেই, hashing নেই।

Recursion থেকে backtracking

পর্ব ১২-এ recursion দেখেছেন: একটি method সমস্যাকে ছোট করে নিজেকে ডাকে, আর base case-এ থামে। Backtracking হলো recursion দিয়ে সব সম্ভাবনা গুছিয়ে খোঁজা: একটি সিদ্ধান্ত নাও, বাকিটা recursion-কে দাও, ফিরে এসে সিদ্ধান্তটি উল্টে দাও, তারপর পরের বিকল্প। গোলকধাঁধায় হাঁটার মতো — কানাগলিতে পৌঁছালে শেষ মোড়ে ফিরে অন্য পথ। একটি তালিকার সব subset বানানো এর সবচেয়ে সরল রূপ: প্রতিটি উপাদানে দুটি পথ — “নেব না” বা “নেব”.

static void subsets(int[] a, int i, Deque<Integer> cur, List<List<Integer>> all) {
    if (i == a.length) { all.add(new ArrayList<>(cur)); return; }   // base case: একটি subset তৈরি
    subsets(a, i + 1, cur, all);                 // a[i] নেব না
    cur.addLast(a[i]);                           // সিদ্ধান্ত: নেব
    subsets(a, i + 1, cur, all);
    cur.removeLast();                            // ফিরে এসে সিদ্ধান্ত উল্টানো
}
// {1, 2, 3} → [[], [3], [2], [2, 3], [1], [1, 3], [1, 2], [1, 2, 3]]

n টি উপাদানে 2nটি subset — তাই অংশ ১-এর টেবিল অনুযায়ী এটা শুধু n ২০-এর আশেপাশে পর্যন্ত চলে। new ArrayList<>(cur) লক্ষ করুন: cur নিজেকে যোগ করলে সব জায়গায় একই object যেত, আর শেষে সব খালি দেখাত — backtracking-এর খুব পরিচিত bug. N-Queens, Sudoku সমাধান, সব বিন্যাস (permutation) — সবই এই ছাঁচ, শুধু “এই সিদ্ধান্ত বৈধ কি না” যাচাই যোগ হয়, যাতে অবৈধ পথ শুরুতেই কেটে দেওয়া যায় (pruning).

৬ · ছাঁচ ৪ — dynamic programming: একবার হিসাব, বারবার ব্যবহার

Fibonacci সংখ্যা ভাবুন: fib(n) = fib(n-1) + fib(n-2). সরাসরি recursion লিখলে fib(50) হিসাব করতে fib(48) দুইবার, fib(47) তিনবার, fib(46) পাঁচবার… হিসাব হয় — ডাকের সংখ্যা বাড়ে প্রায় সূচকীয় হারে, আর সাধারণ কম্পিউটারে উত্তর আসতে বহু মিনিট লেগে যায়। অথচ আলাদা উপ-সমস্যা মাত্র ৫১টি! সমাধান: প্রতিটি উত্তর প্রথমবার হিসাব করে একটি টেবিলে লিখে রাখা, পরের বার টেবিল থেকে পড়া। একে বলে memoization.

static long[] memo = new long[91];
static long fib(int n) {
    if (n <= 1) return n;
    if (memo[n] != 0) return memo[n];           // আগেই জানা
    return memo[n] = fib(n - 1) + fib(n - 2);   // প্রথমবার: হিসাব করে লিখে রাখা
}
// fib(50) = 12586269025, fib(90) = 2880067194370816120 — মুহূর্তে

এটাই dynamic programming (DP)-এর হৃদয়: বড় সমস্যার উত্তর ছোট উপ-সমস্যার উত্তর দিয়ে গড়া যায়, আর একই উপ-সমস্যা বারবার আসে — তাই প্রতিটি একবারই সমাধান করো। দুটি লেখার ধরন: top-down (recursion + memo, যেমন ওপরে) আর bottom-up (ছোট থেকে বড় ক্রমে একটি লুপে টেবিল ভরা)। Bottom-up-এ recursion নেই, তাই StackOverflowError-এর ঝুঁকিও নেই — Java-তে কনটেস্টে এটা বড় সুবিধা। (খেয়াল করুন fib(90): প্রায় ২.৯ × ১০১৮, long-এর সীমার কাছাকাছি; fib(93) আর long-এ ধরে না।)

কয়েন দিয়ে টাকা দেওয়ার কয়টি উপায় — coin change DP

এবার greedy-র উল্টো ধরনের প্রশ্ন: ১, ২ আর ৫ টাকার কয়েন দিয়ে ৬ টাকা কতভাবে দেওয়া যায় (ক্রম গোনা হবে না)? ways[s] = s টাকা বানানোর উপায়ের সংখ্যা। কয়েনগুলো একটি একটি করে “চালু” করি: প্রতিটি কয়েন c-এর জন্য, s টাকা বানানোর নতুন উপায় = আগের উপায় + (s − c টাকা বানানোর উপায়, তারপর একটি c যোগ)। কয়েন বাইরের লুপে থাকায় একই সংমিশ্রণ দুইবার গোনা হয় না।

1long[] ways = new long[amount + 1];2ways[0] = 1;            // শূন্য টাকা: একটিই উপায়3for (int c : coins)4    for (int s = c; s <= amount; s++)5        ways[s] += ways[s - c];
ways[0..6]
কয়েন
s =
ways
  1. ভিত্তি। ways[0] = 1 — শূন্য টাকা দেওয়ার একটিই উপায়: কিছুই না দেওয়া। বাকি ঘর ০.
  2. শুধু ১ টাকার কয়েন। প্রতিটি পরিমাণ ঠিক একভাবে: সব এক টাকা।
  3. ২ টাকার কয়েন যোগ, মাঝপথে। ways[4] = 1 + ways[2], আর ways[2] এই কয়েনের লুপেই একটু আগে ২ হয়েছে। অর্থাৎ “২ টাকা বানাও (২-এর কয়েন ব্যবহার করেও), তারপর আরেকটি ২” — একই কয়েন বারবার নেওয়ার সুযোগ এভাবেই আসে। s ছোট থেকে বড় ক্রমে চলে বলেই এটা সম্ভব।
  4. ২ টাকার কয়েন শেষ। ৬ টাকা এখন ৪ ভাবে: ১×৬, ২+১×৪, ২+২+১+১, ২+২+২.
  5. ৫ টাকার কয়েন। ways[6] += ways[1] — “১ টাকা বানাও, তারপর একটি ৫” — নতুন উপায় ৫+১. মোট ৫টি উপায়। সময় O(কয়েন × টাকা)। একই কোডে ১, ২, ৫, ১০, ২০, ৫০, ১০০ টাকা দিয়ে ১০০ টাকা দেওয়ার উপায় আসে ৪৫৬৩টি (যাচাই করা)।

একটি সূক্ষ্ম পার্থক্য খেয়াল করুন: লুপ দুটির ক্রম উল্টালে (বাইরে টাকা, ভেতরে কয়েন) ক্রমসহ উপায় গোনা হয় — ১+২ আর ২+১ আলাদা ধরা হয়। DP-তে “অবস্থা কী, আর কোন ক্রমে ভরব” — এই দুটি প্রশ্নের উত্তরই আসল নকশা। একই ছাঁচে আসে 0/1 knapsack (প্রতিটি জিনিস একবারই নেওয়া যায় — তখন s-এর লুপ উল্টো দিকে, বড় থেকে ছোট চালাতে হয়), সবচেয়ে কম কয়েনে টাকা দেওয়া (যেখানে greedy ব্যর্থ, সেখানে DP সঠিক), আর দুটি লেখার মধ্যে মিল খোঁজা।

৭ · গ্রাফ — adjacency list, BFS, DFS আর Dijkstra

একটি graph হলো কিছু বিন্দু (node) আর তাদের সংযোগ (edge) — ঢাকার থানা আর তাদের মধ্যে বাস-রুট, মানুষ আর বন্ধুত্ব। কনটেস্টে graph রাখার সবচেয়ে প্রচলিত উপায় adjacency list: প্রতিটি node-এর জন্য তার প্রতিবেশীদের একটি তালিকা — List<List<Integer>>. n × n টেবিল (adjacency matrix)-এর বদলে এতে মেমরি লাগে শুধু node আর edge-এর সংখ্যার সমানুপাতে, তাই দুই লাখ node-এর graph-ও রাখা যায়। দূরত্ব বা ভাড়ার মতো ওজন থাকলে প্রতিটি প্রতিবেশীর সাথে ওজনও রাখুন — যেমন List<List<int[]>>, যেখানে int[]{প্রতিবেশী, ওজন}.

BFS আর DFS — queue বনাম stack

পর্ব ৩২-এ গ্রিডে BFS দেখেছেন: একটি queue দিয়ে স্তরে স্তরে ছড়ানো, তাই প্রতিটি ধাপের দাম সমান হলে সবচেয়ে কম ধাপের পথ দেয় — যেমন “মিরপুর থেকে মতিঝিল সবচেয়ে কম কয়টি বাস বদলে”. Queue-এর জায়গায় stack বসালে পাওয়া যায় DFS (Depth-First Search): একটি পথ ধরে যত গভীরে যাওয়া যায় যাওয়া, আটকে গেলে পিছিয়ে অন্য পথ। DFS দিয়ে বোঝা যায় দুটি জায়গা যুক্ত কি না, graph কয়টি বিচ্ছিন্ন টুকরোয় ভাগ, বা কোথাও চক্র (cycle) আছে কি না। Recursion দিয়ে DFS লেখা সবচেয়ে ছোট, কিন্তু অংশ ২-এর StackOverflowError-এর ঝুঁকি আছে; তাই Java-তে iterative রূপটি জানা জরুরি:

boolean[] seen = new boolean[n];
Deque<Integer> stack = new ArrayDeque<>();
stack.push(0);
while (!stack.isEmpty()) {
    int u = stack.pop();
    if (seen[u]) continue;                    // একাধিক পথে ঢুকে থাকতে পারে
    seen[u] = true;
    System.out.print(u + " ");
    for (int v : adj.get(u)) if (!seen[v]) stack.push(v);
}
// edge 0-1, 0-2, 1-3, 2-4, 4-5 হলে: 0 2 4 5 1 3

আউটপুট 0 2 4 5 1 3 (যাচাই করা): ০-এর প্রতিবেশী ১ আর ২ stack-এ গেল, ২ ওপরে থাকায় আগে বের হলো, তারপর তার গভীরে ৪ আর ৫ — শেষে ফিরে এসে ১ আর ৩. BFS আর DFS দুটিই O(V + E): প্রতিটি node আর প্রতিটি edge একবার করে দেখা।

Dijkstra — ওজনওয়ালা রাস্তায় সবচেয়ে ছোট পথ

BFS গোনে বাস বদলের সংখ্যা, কিন্তু যাত্রী জানতে চান মোট সময়। পাঁচটি থানার একটি ছোট মানচিত্র ধরুন (বাসে মিনিট, উদাহরণের জন্য বানানো): মিরপুর–ফার্মগেট ২৫, মিরপুর–মহাখালী ৪০, ফার্মগেট–মহাখালী ১০, ফার্মগেট–মতিঝিল ৪০, মহাখালী–গুলশান ১০, মহাখালী–মতিঝিল ২৫, গুলশান–মতিঝিল ৩০. BFS-এর হিসাবে মিরপুর থেকে মতিঝিল দুই ধাপ (৬৫ মিনিট), কিন্তু তিন ধাপের একটি পথ আরও দ্রুত। এখানে লাগে Dijkstra-র অ্যালগরিদম: যার দূরত্ব এখনো চূড়ান্ত নয়, তাদের মধ্যে সবচেয়ে কাছেরটিকে বের করো — তার দূরত্ব এখন চূড়ান্ত (সব ওজন শূন্য বা ধনাত্মক হলে) — তারপর তার প্রতিবেশীদের দূরত্ব কমানো যায় কি না দেখো। এই “কমানো যায় কি না দেখা”-র নাম relaxation, আর “সবচেয়ে কাছেরটি বের করা”-র জন্য PriorityQueue.

1dist[Mirpur] = 0; pq.offer({0, Mirpur});2top = pq.poll();                // সবচেয়ে কাছের3if (top.d > dist[u]) continue;  // পুরোনো রেকর্ড4for (u → v, ওজন w)5    if (dist[u] + w < dist[v]) { dist[v] = ...; pq.offer }
dist (মিনিট) আর priority queue
Mirpur
Farmgate
Mohakhali
Gulshan
Motijheel
pq
  1. শুরু। মিরপুরের দূরত্ব ০, বাকি সবার ∞ (এখনো অজানা)। heap-এ শুধু {0, মিরপুর}.
  2. মিরপুর বের হলো। তার দুই প্রতিবেশীর দূরত্ব ∞ থেকে কমে ২৫ আর ৪০; দুজনেই heap-এ।
  3. ফার্মগেট (২৫) — চূড়ান্ত। বাকি সব পথ ইতিমধ্যে ২৫-এর বেশি থেকে শুরু, তাই এর চেয়ে কম অসম্ভব। ফার্মগেট হয়ে মহাখালী ৩৫ — আগের ৪০-এর চেয়ে কম, তাই relaxation সফল। মতিঝিল প্রথমবার ৬৫.
  4. মহাখালী (৩৫) — চূড়ান্ত। গুলশান ৪৫; আর মহাখালী হয়ে মতিঝিল ৬০ — ৬৫ থেকে কমল।
  5. পুরোনো রেকর্ড বাদ। heap-এ মহাখালীর পুরোনো {40} রেকর্ড রয়ে গিয়েছিল। Java-র PriorityQueue-তে মান কমানোর (decrease-key) সুবিধা নেই, তাই আমরা নতুন রেকর্ড যোগ করেছিলাম; এখন 40 > dist = 35 দেখে বাদ দিই (পর্ব ৩২-এর ভাবনার প্রশ্ন ৪)।
  6. গুলশান (৪৫) — চূড়ান্ত। গুলশান হয়ে মতিঝিল ৭৫, যা ৬০-এর চেয়ে বেশি — কোনো বদল নেই।
  7. মতিঝিল (৬০) — চূড়ান্ত। prev array পেছন দিকে ধরে পথ: মিরপুর → ফার্মগেট → মহাখালী → মতিঝিল. heap-এ থাকা {65} পরে বের হয়ে বাদ যাবে। মোট সময় O((V + E) log V)-এর কাছাকাছি।

পুরো প্রোগ্রাম (ইনপুট: প্রথম লাইনে 5 7, তারপর প্রতিটি রাস্তা u v মিনিট; থানার নম্বর ০ মিরপুর, ১ ফার্মগেট, ২ মহাখালী, ৩ গুলশান, ৪ মতিঝিল):

List<List<int[]>> adj = new ArrayList<>();
for (int i = 0; i < n; i++) adj.add(new ArrayList<>());
// প্রতিটি রাস্তা u v w: adj.get(u).add(new int[]{v, w}); adj.get(v).add(new int[]{u, w});

long[] dist = new long[n];
int[] prev = new int[n];
Arrays.fill(dist, Long.MAX_VALUE);
Arrays.fill(prev, -1);
dist[0] = 0;
PriorityQueue<long[]> pq = new PriorityQueue<>(Comparator.comparingLong(a -> a[0]));
pq.offer(new long[]{0, 0});                       // {দূরত্ব, থানা}
while (!pq.isEmpty()) {
    long[] top = pq.poll();
    int u = (int) top[1];
    if (top[0] > dist[u]) continue;               // পুরোনো রেকর্ড
    for (int[] e : adj.get(u))
        if (dist[u] + e[1] < dist[e[0]]) {
            dist[e[0]] = dist[u] + e[1];
            prev[e[0]] = u;
            pq.offer(new long[]{dist[e[0]], e[0]});
        }
}
Deque<String> path = new ArrayDeque<>();
for (int v = 4; v != -1; v = prev[v]) path.push(name[v]);
Mirpur: 0 min
Farmgate: 25 min
Mohakhali: 35 min
Gulshan: 45 min
Motijheel: 60 min
Mirpur -> Farmgate -> Mohakhali -> Motijheel

এই আউটপুট পুরো প্রোগ্রামটি OpenJDK 21-এ চালিয়ে যাচাই করা। heap-এ long[]{দূরত্ব, node} রাখা হালকা আর দ্রুত; চাইলে একটি record আর Comparator দিয়ে বেশি পরিষ্কার করে লিখতে পারেন। দূরত্ব long, আর Long.MAX_VALUE-কে ∞ ধরা হয়েছে — তার সাথে কখনো কিছু যোগ করবেন না, overflow হয়ে ঋণাত্মক হয়ে যাবে। ঋণাত্মক ওজনে Dijkstra ভুল উত্তর দিতে পারে; সেখানে Bellman–Ford-এর মতো অন্য অ্যালগরিদম লাগে।

৮ · online judge, রায় আর অনুশীলনের পথ

Online judge আপনার কোড গোপন টেস্টে চালিয়ে একটি রায় দেয়। রায়গুলো চিনে নিলে ভুল খোঁজার দিক পাওয়া যায়:

রায়মানেJava-তে চেনা কারণ
AC (Accepted)সব টেস্টে সঠিক, সময়ের মধ্যে—
WA (Wrong Answer)কোনো টেস্টে উত্তর ভুলint overflow, Integer ==, edge case (n = 1, সব সমান), ভুল greedy
TLE (Time Limit Exceeded)সময়সীমা পারভুল complexity, Scanner, প্রতি লাইনে println, অতিরিক্ত boxing
MLE (Memory Limit Exceeded)মেমরি-সীমা পারবিশাল long[][], লাখ লাখ ছোট object
RE (Runtime Error)প্রোগ্রাম মাঝপথে ভেঙেছেArrayIndexOutOfBounds, StackOverflowError, NullPointerException

কোথায় অনুশীলন করবেন

  • Codeforces — নিয়মিত rated কনটেস্ট, আর হাজারো সমস্যা কঠিনতার rating অনুযায়ী সাজানো।
  • AtCoder — জাপানের প্ল্যাটফর্ম; “Beginner Contest” শুরু করার জন্য চমৎকার।
  • LeetCode — কোডিং ইন্টারভিউর ধাঁচের সমস্যা; সেখানে ইনপুট-আউটপুট নয়, একটি method লিখতে হয়।
  • দেশের ভেতরে — Toph-এর মতো বাংলাদেশি প্ল্যাটফর্মে দেশি কনটেস্ট আর সমস্যা পাওয়া যায়। বিশ্ববিদ্যালয়গুলোর মধ্যে IUPC (inter-university) আর জাতীয় পর্যায়ের NCPC নিয়মিত হয়, আর ঢাকায় বহু বছর ধরে ICPC-র এশীয় আঞ্চলিক পর্ব আয়োজিত হচ্ছে।

যে অভ্যাসে সত্যিকারের উন্নতি হয়

  1. নিজের স্তরের একটু ওপরের সমস্যা। rating অনুযায়ী এমন সমস্যা বাছুন, যা ৩০–৬০ মিনিট লড়ে প্রায় পারেন।
  2. Upsolving. কনটেস্টে যে সমস্যা পারেননি, শেষে সেটা সমাধান করুন। সবচেয়ে বেশি শেখা হয় এখানে।
  3. Editorial পড়ার নিয়ম। আটকে গেলে editorial-এর শুধু প্রথম ইঙ্গিত পড়ুন, তারপর আবার নিজে চেষ্টা করুন। পুরোটা পড়লে কোড না দেখে নিজে লিখুন।
  4. Stress testing. WA পেলে কিন্তু ভুল খুঁজে না পেলে: একটি ধীর কিন্তু নিশ্চিতভাবে সঠিক সমাধান (যেমন nested লুপ) লিখুন, ছোট এলোমেলো ইনপুট বানিয়ে দুটি সমাধানের উত্তর মেলান। যে ইনপুটে উত্তর আলাদা, সেটাই আপনার bug-এর ঠিকানা।
C/C++/Python জানলে

C++-এ দ্রুত I/O মানে ios::sync_with_stdio(false) আর cin.tie(nullptr); Java-তে একই কাজ করে BufferedReader + StringTokenizer আর PrintWriter. C++-এর long long মানে Java-র long; C++-এর priority_queue ডিফল্টে max-heap, Java-রটা min-heap (কনটেস্ট প্রোগ্রামিংয়ে C++). Python-এ পূর্ণসংখ্যা কখনো overflow করে না আর recursion-এর একটি আলাদা সীমা আছে; Java-তে overflow নীরবে ঘটে, আর বড় সংখ্যার জন্য BigInteger লাগে (অ্যালগরিদম ও কনটেস্টে Python). ধারণাগুলো — prefix sum থেকে Dijkstra — তিন ভাষাতেই হুবহু এক।

পরীক্ষা ও ইন্টারভিউতে
  • n = ১০৫ হলে O(n২) চলবে না কেন? — প্রায় ১০১০ কাজ; মোটা দাগে সেকেন্ডে ১০৮ ধরলে শত সেকেন্ড লাগবে; O(n log n) দরকার।
  • Java-তে লাখ লাখ সংখ্যা দ্রুত পড়বেন কীভাবে? — BufferedReader + StringTokenizer (বা নিজের বাইট-reader), আউটপুট StringBuilder/PrintWriter-এ জমিয়ে শেষে একবার flush.
  • Greedy আর DP-র পার্থক্য? — greedy প্রতি ধাপে এখনকার সেরা নিয়ে আর ফিরে দেখে না; DP সব উপ-সমস্যার উত্তর হিসাব করে মনে রাখে। greedy সঠিক হলে দ্রুত, কিন্তু সবসময় সঠিক নয় (যেমন কয়েন ১, ৩, ৪).
  • Dijkstra-তে PriorityQueue কেন, আর ঋণাত্মক ওজনে কী হয়? — বারবার সবচেয়ে কম দূরত্বের node বের করতে O(log n)-এ; ঋণাত্মক ওজনে “বের হলেই চূড়ান্ত” যুক্তি ভাঙে, তাই Bellman–Ford.
  • Recursive DFS-এ StackOverflowError এলে? — ArrayDeque দিয়ে iterative DFS, অথবা বড় stack-এর Thread.

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

AI-এর লেখা কনটেস্ট-সমাধান প্রায়ই নমুনা ইনপুটে নিখুঁত চলে, কিন্তু গোপন টেস্টে n = ২ × ১০৫, মান ১০৯, আর ইচ্ছে করে রাখা কঠিন ক্ষেত্র থাকে। বারবার দেখা ভুলগুলো:

AI যা করেকী হয়আপনি যা করবেন
প্রতিটি প্রশ্নে লুপ চালিয়ে যোগ (O(n·q))নমুনায় AC, বড় টেস্টে TLEconstraints থেকে complexity হিসাব; prefix sum
যোগফল বা গুণফল int-এবড় মানে নীরবে WAসর্বোচ্চ মান গুণ করে দেখুন; long
১০৬ সংখ্যা Scanner দিয়ে, প্রতি লাইনে printlnঅ্যালগরিদম ঠিক, তবু TLEBufferedReader + StringTokenizer, StringBuilder
গভীর recursive DFSশিকল-আকৃতির graph-এ REiterative DFS বা বড় stack-এর thread
“স্বাভাবিক মনে হওয়া” greedy, প্রমাণ ছাড়াকিছু টেস্টে ভুল উত্তরছোট পাল্টা-উদাহরণ খুঁজুন; দরকারে DP
List<Integer>-এর মান == দিয়ে তুলনা১২৭-এর ওপরে WAequals বা int-এ নামানো

নিয়ম আর সততা। Codeforces, AtCoder-এর মতো প্ল্যাটফর্ম আর ICPC-র মতো প্রতিযোগিতা লাইভ কনটেস্টে AI দিয়ে সমাধান লেখানো নিষিদ্ধ বা কঠোরভাবে সীমিত করেছে। নিয়ম সময়ের সাথে বদলায়, তাই প্রতিটি কনটেস্টের নিয়ম নিজে পড়ুন। নিয়ম ভাঙলে অ্যাকাউন্ট বাতিল হতে পারে — আর তার চেয়েও বড় ক্ষতি, যে দক্ষতার জন্য কনটেস্ট, সেটাই গড়ে ওঠে না। ইন্টারভিউতে AI পাশে থাকবে না; বোঝা ছাড়া কপি করা সমাধান সেখানে প্রথম প্রশ্নেই ধরা পড়ে।

AI-এর সঠিক ব্যবহার কনটেস্ট শেষ হওয়ার পর শেখার সঙ্গী হিসেবে: editorial ব্যাখ্যা করানো, নিজের সমাধানের complexity যাচাই, আর stress test-এর জন্য ধীর সমাধান ও ইনপুট-জেনারেটর লেখানো। নিচের prompt সমাধান চায় না, বোঝা চায়:

কনটেস্টের পরে (upsolving-এর সময়) এই কাঠামো ব্যবহার করুন:

আমি Java-তে কনটেস্ট প্রোগ্রামিং শিখছি। কনটেস্ট শেষ, এখন upsolve করছি। সমস্যা: [সমস্যার বিবরণ আর constraints] আমার সমাধান: [আমার কোড] রায়: [WA / TLE, কোন টেস্টে] শর্ত: ১. পুরো সঠিক কোড লিখে দেবে না। আগে শুধু একটি ইঙ্গিত দাও — কোন ধারণা বা ছাঁচ আমি মিস করছি। ২. constraints থেকে কোন complexity দরকার, হিসাব করে দেখাও; আমার কোডের complexity কত, তাও বলো। ৩. আমার কোডে Java-র চেনা ফাঁদ খোঁজো: int overflow, Integer ==, Scanner বা প্রতি লাইনে println, গভীর recursion, boxing. ৪. stress test-এর জন্য একটি ধীর কিন্তু নিশ্চিতভাবে সঠিক সমাধান আর ছোট এলোমেলো ইনপুট বানানোর একটি জেনারেটর লেখো (Random-এর seed নির্দিষ্ট রেখো)। ৫. editorial-এর এই লাইনটি সহজ ভাষায় ছোট একটি উদাহরণ দিয়ে বোঝাও: [লাইন]

কনটেস্ট-কোড জমা দেওয়ার আগে — চেকলিস্ট

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

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

প্র ০১“Analysis of algorithms” কথাটি কোথা থেকে এল, আর Donald Knuth-এর “The Art of Computer Programming” কেন আজও কিংবদন্তি?

১৯৬০-এর দশকে প্রোগ্রাম লেখা ছিল অনেকটা কারিগরি: কোন পদ্ধতি দ্রুত, তা মানুষ জানতেন অভিজ্ঞতা আর মেপে দেখে। মার্কিন গণিতবিদ ও কম্পিউটার বিজ্ঞানী Donald E. Knuth একটি বই লিখতে শুরু করেন, যা পরে হয়ে দাঁড়ায় বহু খণ্ডের The Art of Computer Programming (সংক্ষেপে TAOCP). প্রথম খণ্ড, “Fundamental Algorithms”, প্রকাশিত হয় ১৯৬৮ সালে; তারপর ১৯৬৯ সালে দ্বিতীয় আর ১৯৭৩ সালে তৃতীয় খণ্ড (সাজানো আর খোঁজা)। চতুর্থ খণ্ডের প্রথম অংশ আসে অনেক পরে, ২০১১ সালে, আর কাজ এখনো চলছে।

Knuth-এর বড় অবদান হলো অ্যালগরিদমকে গাণিতিক বিশ্লেষণের বিষয় বানানো: একটি অ্যালগরিদম গড়ে আর সবচেয়ে খারাপ ক্ষেত্রে ঠিক কতগুলো তুলনা বা কাজ করে, তা সূত্র দিয়ে বের করা। এই ধারার নাম “analysis of algorithms”, আর শব্দবন্ধটি জনপ্রিয় করার কৃতিত্ব সাধারণত Knuth-কেই দেওয়া হয়। ১৯৭৬ সালে তিনি একটি ছোট প্রবন্ধে Big-O-এর পাশাপাশি Big-Omega (Ω) আর Big-Theta (Θ) চিহ্নের ব্যবহার সুনির্দিষ্ট করার প্রস্তাব দেন — আজ পাঠ্যবইয়ে যে অর্থে এগুলো পড়েন, তার বড় অংশ সেখান থেকে। ১৯৭৪ সালে তিনি কম্পিউটার বিজ্ঞানের সর্বোচ্চ সম্মান Turing Award পান।

দ্বিতীয় খণ্ডের নতুন সংস্করণের ছাপার মান দেখে অসন্তুষ্ট হয়ে তিনি নিজেই TeX নামের টাইপসেটিং প্রোগ্রাম লেখেন, যা আজও গণিত আর বিজ্ঞানের লেখায় মানদণ্ড। আর তাঁর বইয়ে প্রথম কেউ একটি ভুল খুঁজে পেলে তিনি ২.৫৬ ডলারের একটি চেক পাঠান — তাঁর ভাষায় “এক hexadecimal ডলার”.

এই পর্বের সাথে যোগ: binary search-এর মতো “সহজ” অ্যালগরিদমও সূক্ষ্ম। Knuth TAOCP-তে লিখেছেন যে binary search-এর ধারণা প্রথম প্রকাশিত হয় ১৯৪৬ সালে, কিন্তু সব n-এর জন্য নির্ভুল সংস্করণ আসে আরও অনেক বছর পরে। ২০০৬ সালে Google-এর Joshua Bloch দেখান যে Java-র Arrays.binarySearch-এ (low + high) / 2 বহু বছর ধরে লুকিয়ে ছিল — খুব বড় array-তে যোগফল int ছাড়িয়ে ঋণাত্মক হয়ে যেত। অংশ ৪-এর lo + (hi - lo) / 2 সেই শিক্ষারই ফল।

মূল উপলব্ধি: অ্যালগরিদম শুধু “চলে কি না” নয়, “কতটা কাজ করে আর সব ক্ষেত্রে ঠিক কি না” — এই দুই প্রশ্ন গাণিতিকভাবে জিজ্ঞেস করার অভ্যাসই Knuth-এর উত্তরাধিকার, আর Big-O সেই অভ্যাসের সবচেয়ে ছোট রূপ।

প্র ০২Dijkstra নাকি ২০ মিনিটে সবচেয়ে ছোট পথের অ্যালগরিদম ভেবেছিলেন — কাগজ-কলম ছাড়া। গল্পটি কী, আর এটা আমাদের কী শেখায়?

পর্ব ৩২-এর ভাবনার প্রশ্নে গল্পটির উল্লেখ ছিল; এবার একটু বিস্তারিত। ১৯৫৬ সালে ডাচ কম্পিউটার বিজ্ঞানী Edsger W. Dijkstra আমস্টারডামের Mathematical Centre-এ কাজ করতেন। প্রতিষ্ঠানটির নতুন কম্পিউটার ARMAC-এর একটি প্রদর্শনী হবে, আর তাঁর দরকার ছিল এমন একটি সমস্যা, যা কম্পিউটার-না-জানা দর্শকও বুঝবেন। তিনি বেছে নিলেন: নেদারল্যান্ডসের দুটি শহরের মধ্যে সবচেয়ে ছোট পথ। পরে এক সাক্ষাৎকারে তিনি বলেছেন, একদিন বাগদত্তার সাথে কেনাকাটা করতে বেরিয়ে ক্লান্ত হয়ে তাঁরা একটি ক্যাফের বারান্দায় বসেন, আর সেখানে প্রায় বিশ মিনিটে তিনি অ্যালগরিদমটি ভেবে ফেলেন — কোনো কাগজ-কলম ছাড়াই। প্রদর্শনীর জন্য তিনি নেদারল্যান্ডসের একটি সরলীকৃত মানচিত্র নেন, ৬৪টি শহরের, যাতে প্রতিটি শহরের নম্বর মাত্র ৬টি bit-এ ধরে।

অ্যালগরিদমটি প্রকাশিত হয় তিন বছর পরে, ১৯৫৯ সালে, “A note on two problems in connexion with graphs” নামের প্রায় তিন পাতার একটি লেখায়। একই লেখায় দ্বিতীয় সমস্যাটি ছিল সবচেয়ে কম মোট দৈর্ঘ্যে সব বিন্দু জোড়া দেওয়া (minimum spanning tree). Dijkstra নিজে বলেছেন, কাগজ-কলম না থাকাটাই সুবিধা ছিল — এতে অপ্রয়োজনীয় জটিলতা এড়াতে বাধ্য হয়েছিলেন। মজার ব্যাপার হলো, মূল লেখায় কোনো heap ছিল না; প্রতিবার সবচেয়ে কাছের বিন্দু খোঁজা হতো সরাসরি দেখে, O(V২)। PriorityQueue দিয়ে আজকের যে রূপ লিখলেন, সেটা পরে heap-এর সাথে মিলিয়ে উন্নত করা।

Dijkstra পরে structured programming, semaphore আর প্রোগ্রামের সঠিকতা প্রমাণ নিয়ে মৌলিক কাজ করেন, আর ১৯৭২ সালে Turing Award পান। আজ মানচিত্র-অ্যাপের পথ খোঁজা আর নেটওয়ার্কের রুট বাছাইয়ে এই ধারণা বা তার উন্নত রূপ কাজ করে; বিশাল রাস্তার নেটওয়ার্কে বাস্তব সিস্টেম আগেভাগে হিসাব করা কাঠামো আর A*-এর মতো কৌশল যোগ করে।

মূল উপলব্ধি: সবচেয়ে টেকসই অ্যালগরিদমগুলো প্রায়ই সরল একটি পর্যবেক্ষণ থেকে আসে — “যে সবচেয়ে কাছে, তার দূরত্ব আর বদলাবে না” — আর সেই সরলতার খোঁজে কখনো কখনো কম সরঞ্জামই বেশি সাহায্য করে।

প্র ০৩“Dynamic programming” নামে না আছে আজকের অর্থে programming, না dynamic-এর স্পষ্ট মানে। Richard Bellman নামটি কেন বেছেছিলেন?

পদ্ধতিটি গড়ে তোলেন মার্কিন গণিতবিদ Richard Bellman, ১৯৫০-এর দশকে, RAND Corporation-এ কাজ করার সময়। RAND তখন মূলত মার্কিন বিমানবাহিনীর জন্য গবেষণা করত। Bellman-এর আগ্রহ ছিল বহু-ধাপের সিদ্ধান্তের সমস্যায়: প্রতিটি ধাপের সিদ্ধান্ত পরের ধাপের অবস্থা বদলায় — পুরো প্রক্রিয়ার সেরা নীতি কীভাবে খুঁজবেন? তাঁর মূল অন্তর্দৃষ্টি পরে “principle of optimality” নামে পরিচিত হয়: একটি সেরা নীতির যেকোনো শেষ অংশও, সেই অংশের শুরুর অবস্থা থেকে, নিজেই সেরা। অংশ ৬-এর coin change টেবিল ঠিক এই ভাবনায় দাঁড়ানো — বড় পরিমাণের উত্তর ছোট পরিমাণের উত্তর থেকে।

নামের গল্পটি Bellman নিজেই লিখেছেন তাঁর আত্মজীবনী Eye of the Hurricane-এ (১৯৮৪)। তাঁর বর্ণনা অনুযায়ী, তখনকার মার্কিন প্রতিরক্ষা সচিব Charles Wilson “research” শব্দটিই প্রচণ্ড অপছন্দ করতেন, আর RAND-এর অর্থের উৎস ছিল সেই বিভাগ। তাই নিজের গণিত-গবেষণার জন্য তাঁর এমন একটি নাম দরকার ছিল, যা গবেষণার মতো শোনায় না। “Programming” শব্দটি তখন কম্পিউটারের কোড অর্থে নয়, পরিকল্পনা বা সময়সূচি তৈরি অর্থে চলত — “linear programming”-ও একই অর্থে। আর “dynamic” সময়ের সাথে বদলানো, বহু-ধাপের প্রক্রিয়া বোঝায়; Bellman-এর ভাষায়, শব্দটিকে নেতিবাচক অর্থে ব্যবহার করা প্রায় অসম্ভব। ফল: এমন একটি নাম, যার বিরুদ্ধে কোনো কংগ্রেস সদস্যও আপত্তি তুলতে পারবেন না।

ইতিহাসবিদেরা গল্পটির কিছু খুঁটিনাটি, বিশেষ করে তারিখের মিল নিয়ে প্রশ্ন তুলেছেন, তবে মূল কথায় দ্বিমত কম: নামটি কৌশলগতভাবে বেছে নেওয়া, প্রযুক্তিগত বর্ণনা হিসেবে নয়। তাই নাম শুনে পদ্ধতিটি বোঝা যায় না; অনেকে মনে রাখেন “উপ-সমস্যার উত্তর মনে রাখা” হিসেবে।

Bellman-এর নাম আছে Bellman–Ford অ্যালগরিদমেও (ঋণাত্মক ওজনের graph-এ সবচেয়ে ছোট পথ), যা নিজেও একটি DP; আর তাঁর “Bellman equation” আজ reinforcement learning-এ কেন্দ্রীয়।

মূল উপলব্ধি: নামটি ভুলে যান, ভাবনাটি মনে রাখুন: বড় সমস্যার সেরা উত্তর ছোট উপ-সমস্যার সেরা উত্তর দিয়ে গড়া, আর প্রতিটি উপ-সমস্যা একবারই সমাধান করা। প্রযুক্তির অনেক নামই ইতিহাসের দুর্ঘটনা — অর্থ খুঁজতে হয় পদ্ধতিতে, শব্দে নয়।

প্র ০৪P বনাম NP — সহজ ভাষায় প্রশ্নটি কী, আর এর উত্তরের জন্য কেন দশ লাখ ডলারের পুরস্কার রাখা আছে?

দুটি কাজের পার্থক্য ভাবুন: একটি Sudoku সমাধান করা, আর কেউ সমাধান দিলে সেটা ঠিক কি না যাচাই করা। যাচাই সহজ — প্রতিটি সারি, কলাম আর বাক্স দেখলেই হলো। সমাধান খোঁজা অনেক কঠিন মনে হয়। P বনাম NP প্রশ্নটি ঠিক এই অনুভূতিকে গাণিতিকভাবে জিজ্ঞেস করে।

P হলো সেই সব সমস্যার শ্রেণি, যা polynomial সময়ে — অর্থাৎ O(n), O(n২), O(n৩)-এর মতো সময়ে — সমাধান করা যায়: sort, সবচেয়ে ছোট পথ (Dijkstra), prefix sum. NP হলো সেই সব সমস্যা, যার একটি প্রস্তাবিত উত্তর polynomial সময়ে যাচাই করা যায়। প্রতিটি P সমস্যা NP-তেও আছে (সমাধান করতে পারলে যাচাইও পারেন)। প্রশ্ন হলো উল্টোটা: যা দ্রুত যাচাই করা যায়, তা কি সবসময় দ্রুত সমাধানও করা যায়? অর্থাৎ P = NP কি না।

১৯৭১ সালে Stephen Cook দেখান যে একটি নির্দিষ্ট যুক্তির সমস্যা (SAT) এমন বিশেষ: NP-র যেকোনো সমস্যা একে দিয়ে প্রকাশ করা যায়। সোভিয়েত ইউনিয়নে Leonid Levin স্বাধীনভাবে একই ধারণায় পৌঁছান। পরের বছর, ১৯৭২ সালে, Richard Karp দেখান যে আরও ২১টি পরিচিত সমস্যাও এমন “সবচেয়ে কঠিন” — এদের বলা হয় NP-complete. এদের একটির জন্যও দ্রুত অ্যালগরিদম পেলে সবগুলোর জন্য পাওয়া যাবে। ভ্রমণকারী বিক্রেতার সবচেয়ে ছোট সফর, বড় আকারের Sudoku, আর knapsack-এর সিদ্ধান্ত-রূপ এই দলে।

এই পর্বের সাথে একটি সূক্ষ্ম যোগ আছে। অংশ ৬-এর DP দিয়ে knapsack বা coin change O(জিনিস × টাকা)-এ চলে — তাহলে কি NP-complete সমস্যা সহজে মিটে গেল? না। ইনপুটের আকার মাপা হয় সংখ্যাগুলো লিখতে কত অঙ্ক লাগে তা দিয়ে; টাকার পরিমাণ ১০১৮ হলে টেবিলও ততটাই বড়। এমন অ্যালগরিদমকে বলে pseudo-polynomial — ছোট সংখ্যায় চমৎকার, বড় সংখ্যায় অচল।

২০০০ সালে Clay Mathematics Institute সাতটি “Millennium Prize Problem” ঘোষণা করে, প্রতিটির জন্য দশ লাখ ডলার; P বনাম NP তার একটি, আর এখনো অমীমাংসিত। বেশিরভাগ গবেষক বিশ্বাস করেন P ≠ NP; উল্টোটা প্রমাণ হলে বহু cryptography-র ভিত্তি দুর্বল হয়ে যেতে পারত।

মূল উপলব্ধি: কনটেস্টে বা কাজে কোনো সমস্যা NP-complete চেনা গেলে নিখুঁত দ্রুত সমাধানের পেছনে না ছুটে ছোট n-এ backtracking, সংখ্যা ছোট হলে DP, বা যথেষ্ট ভালো আনুমানিক উত্তরের দিকে যাওয়াই বুদ্ধিমানের কাজ।

প্র ০৫ICPC কীভাবে টেক্সাসের একটি বিশ্ববিদ্যালয়ের প্রতিযোগিতা থেকে বিশ্বজুড়ে ছড়াল, আর বাংলাদেশের প্রোগ্রামিং সমাজে এর ভূমিকা কী?

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

নিয়মগুলো অনন্য। তিনজনের দল, কিন্তু কম্পিউটার একটি — তাই কে কখন কিবোর্ডে বসবে, কে কাগজে পরের সমস্যা ভাববে, কে অন্যের কোড পড়ে ভুল খুঁজবে, এই সমন্বয়ই কৌশলের বড় অংশ। ফল ঠিক হয় সমাধান করা সমস্যার সংখ্যায়; সমান হলে কম penalty সময়ে, যেখানে প্রতিটি ভুল জমার জন্য শাস্তি যোগ হয়। তাই “জমা দিয়ে দেখি” অভ্যাসের দাম আছে — এই পর্বের চেকলিস্ট ঠিক সেই কারণে। ICPC-তে C, C++, Java, Python-এর মতো কয়েকটি ভাষা সাধারণত অনুমোদিত থাকে, আর বহু বছর ধরে Java এর অন্যতম প্রধান ভাষা; বিশেষ করে বড় সংখ্যার হিসাবে BigInteger-এর জন্য এটি জনপ্রিয় ছিল।

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

একটি ভারসাম্যপূর্ণ দৃষ্টিও দরকার: কনটেস্টের কোড একবার চালানোর জন্য লেখা, আর পেশাদার কাজে দরকার পর্ব ৪৭-এর test আর পর্ব ৪৮-এর পরিষ্কার নকশা। দুটি দক্ষতা আলাদা, দুটিই মূল্যবান।

মূল উপলব্ধি: ICPC-র সাফল্য একটি সরল কাঠামোয় — স্পষ্ট সমস্যা, নিরপেক্ষ স্বয়ংক্রিয় বিচার, সাথে সাথে ফল; বাংলাদেশে এর চারপাশে গড়ে ওঠা সহযোগিতার সংস্কৃতিই আসল সম্পদ, পদক তার উপজাত।

অনুশীলন

বেশিরভাগ কাজে নতুন কোড লিখতে হবে না — হাতে-কলমে অবস্থা অনুসরণ করা আর ভাবাই আসল কাজ।

  1. Prefix sum হাতে বানান: বিক্রি a = 2, 7, 1, 8, 2, 8 (দিন ১–৬)। pre[0..6] লিখুন, তারপর দিন ২–৪ আর দিন ৩–৬-এর মোট বের করুন শুধু একটি করে বিয়োগে।

    pre = 0, 2, 9, 10, 18, 20, 28. দিন ২–৪ = pre[4] - pre[1] = ১৮ − ২ = ১৬ (যাচাই: ৭ + ১ + ৮ = ১৬)। দিন ৩–৬ = pre[6] - pre[2] = ২৮ − ৯ = ১৯ (১ + ৮ + ২ + ৮ = ১৯)। বিয়োগ হয় pre[l-1], pre[l] নয় — সবচেয়ে প্রচলিত off-by-one ভুল।

  2. Binary search on answer অনুসরণ করুন: একই চালান 4 8 3 6 5 7 2, কিন্তু এবার মাত্র ২ দিন। lo আর hi কত থেকে শুরু, প্রতিটি mid-এ canShip কী দেয়, আর উত্তর কত? ৭ দিন হলে উত্তর কত হবে, আর কেন সেটা চিন্তা না করেই বলা যায়?

    lo = ৮, hi = ৩৫. mid = ২১: ৪+৮+৩+৬ = ২১ | ৫+৭+২ = ১৪ → ২ দিন ✔, hi = ২১. mid = ১৪: ৪+৮ | ৩+৬+৫ | ৭+২ → ৩ দিন ✘, lo = ১৫. mid = ১৮: ৪+৮+৩ = ১৫ | ৬+৫+৭ = ১৮ | ২ → ৩ দিন ✘, lo = ১৯. mid = ২০: ৪+৮+৩ = ১৫ | ৬+৫+৭+২ = ২০ → ২ দিন ✔, hi = ২০. mid = ১৯: ১৫ | ১৮ | ২ → ৩ দিন ✘, lo = ২০. উত্তর ২০. অংশ ৪-এর runnable কোডে 7 2 দিয়ে চালালে ঠিক এই check-গুলো আসে। ৭ দিনে প্রতিটি চালান আলাদা দিনে যেতে পারে, তাই উত্তর সবচেয়ে ভারী চালান, অর্থাৎ ৮ — নিচের সীমাই উত্তর।

  3. Greedy কখন ঠিক: (ক) অংশ ৪-এর নোট-কয়েনে (১, ২, ৫, ১০, ২০, ৫০, ১০০, ৫০০, ১০০০) ২৭৪০ টাকা greedy দিয়ে কয়টি নোটে দেবেন? (খ) কয়েন যদি শুধু ১, ৩, ৪ হয়, ৬ টাকার greedy উত্তর আর সেরা উত্তর কত? (গ) সেরা উত্তর নিশ্চিতভাবে পেতে কোন পদ্ধতি নেবেন?

    (ক) ১০০০ × ২, ৫০০ × ১, ১০০ × ২, ২০ × ২ = মোট ৭টি নোট (২০০০ + ৫০০ + ২০০ + ৪০ = ২৭৪০)। (খ) greedy: ৪ + ১ + ১ = ৩টি; সেরা: ৩ + ৩ = ২টি। (গ) DP: minCoins[s] = min(minCoins[s - c] + 1), সব কয়েন c-এর ওপর, s ছোট থেকে বড় ক্রমে। এটা যেকোনো কয়েন-ব্যবস্থায় সঠিক, সময় O(কয়েন × টাকা)।

  4. AI-এর কোড পর্যালোচনা: n আর q দুটিই সর্বোচ্চ ২ × ১০৫, প্রতিটি বিক্রি সর্বোচ্চ ১০৯. একটি AI লিখেছে: Scanner দিয়ে পড়া, প্রতিটি প্রশ্নে for (int i = l; i <= r; i++) sum += a[i]; যেখানে int sum, আর প্রতিটি উত্তর System.out.println দিয়ে। নমুনায় AC. অন্তত তিনটি সমস্যা খুঁজুন আর প্রতিটির সমাধান লিখুন।

    (১) Complexity: সবচেয়ে খারাপ ক্ষেত্রে প্রতিটি প্রশ্নে ২ × ১০৫ যোগ, মোট ৪ × ১০১০ — নিশ্চিত TLE; সমাধান prefix sum, প্রতিটি প্রশ্ন O(1). (২) Overflow: যোগফল ২ × ১০১৪ পর্যন্ত, int-এর সীমা প্রায় ২.১ × ১০৯ — নীরবে ভুল উত্তর; সমাধান long[] pre. (৩) I/O: চার লাখের বেশি সংখ্যা Scanner দিয়ে আর দুই লাখ println — অ্যালগরিদম ঠিক করার পরেও সময় নষ্ট; সমাধান BufferedReader + StringTokenizer আর StringBuilder, শেষে একবার flush. অংশ ৩-এর runnable কোডটি এই তিনটিরই সমাধান।

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

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

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

  1. 📖 সমস্যা আর constraints মন দিয়ে পড়া
  2. 🧮 constraints থেকে প্রয়োজনীয় complexity আন্দাজ করা
  3. ✏️ কাগজে পদ্ধতি ঠিক করা, ছোট উদাহরণে হাতে চালানো
  4. ⌨️ দ্রুত I/O-সহ টেমপ্লেটে কোড লেখা, long আর edge case মাথায় রেখে
  5. 🧪 নমুনা আর নিজের বানানো কঠিন ইনপুটে চালিয়ে দেখা
  6. 📤 জমা দেওয়া; WA বা TLE হলে stress test দিয়ে ভুল খোঁজা

একটি সমস্যায় n ≤ 2 × 10^5 আর সময়সীমা ১ সেকেন্ড। মোটা দাগে সেকেন্ডে ~10^8 সরল কাজ ধরলে কোন complexity-র সমাধান নিরাপদ?

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

Java-তে int a = 100000, b = 100000; long c = a * b; System.out.println(c); — কী ছাপা হয়?

  • 10000000000
  • 1410065408
  • ArithmeticException ছোড়ে
  • compile error

List<Integer> xs = new ArrayList<>(List.of(1000, 1000)); তারপর xs.get(0) == xs.get(1) — ফল কী, আর কেন?

  • true, কারণ দুটির মান সমান
  • compile error, Integer-এ == চলে না
  • কখনো true, কখনো false — এলোমেলো
  • false, কারণ == দুটি আলাদা object-এর reference তুলনা করে

১, ২ আর ৫ টাকার কয়েন (প্রতিটি যতবার খুশি) দিয়ে ৬ টাকা কতভাবে দেওয়া যায়, যদি কয়েনের ক্রম গোনা না হয়?

  • ৫
  • ৪
  • ৮
  • ২

কোন কয়েন-ব্যবস্থায় “যত বড় কয়েন দেওয়া যায় দাও” greedy পদ্ধতি সবচেয়ে কম কয়েনের উত্তর দিতে ব্যর্থ হয়?

  • ১, ২, ৫, ১০
  • ১, ২, ৪, ৮
  • ১, ৩, ৪
  • ১, ১০, ১০০

Java-র PriorityQueue দিয়ে লেখা Dijkstra-তে poll করার পর if (top[0] > dist[u]) continue; লাইনটি কেন রাখা হয়?

  • ঋণাত্মক ওজনের রাস্তা বাদ দিতে
  • একই node-এর পুরোনো, বড় দূরত্বের রেকর্ড বাদ দিতে
  • খালি queue-তে NullPointerException এড়াতে
  • heap-এর জিনিসগুলো সাজানো ক্রমে রাখতে

শব্দকোষ

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

মনে রাখুন

  1. আগে হিসাব, পরে কোড: constraints থেকে complexity আন্দাজ করুন; মোটা দাগে সেকেন্ডে ~১০৮ সরল কাজ, n = ১০৫-এ O(n log n) চলে, O(n২) চলে না।
  2. Java-র কনটেস্ট-অভ্যাস: BufferedReader + StringTokenizer, আউটপুট জমিয়ে শেষে একবার flush, যোগফলে long, Integer-এ equals, গভীর recursion-এর বদলে iterative বা বড় stack.
  3. চার ছাঁচ: prefix sum (রেঞ্জের যোগফল O(1)), two pointers (একমুখী জানালা O(n)), উত্তরের ওপর binary search (একঘেয়ে check), greedy (প্রমাণ বা পাল্টা-উদাহরণ সহ)।
  4. DP: অবস্থা ঠিক করুন, প্রতিটি উপ-সমস্যা একবার হিসাব করুন; memo দিয়ে top-down বা টেবিল দিয়ে bottom-up — Java-তে bottom-up-এ stack-এর ঝুঁকি নেই।
  5. গ্রাফ: adjacency list; সমান দামের ধাপে BFS (queue), পৌঁছানো আর টুকরো খুঁজতে DFS (stack), ওজনে Dijkstra (PriorityQueue + পুরোনো রেকর্ড বাদ)। AI-কে কনটেস্টের পরে শিক্ষক বানান, কনটেস্টের ভেতরে নয়।

পরের পর্বে: ক্যাপস্টোন — একটা আসল Java সিস্টেম। এ পর্যন্ত শেখা সবকিছু — object-ভিত্তিক নকশা, collections, exception, ফাইল আর ডেটাবেস, test, Spring Boot-এর REST API — এক জায়গায় জোড়া দিয়ে শুরু থেকে শেষ পর্যন্ত একটি পূর্ণাঙ্গ প্রজেক্ট, যেখানে এই পর্বের complexity-ভাবনাও কাজে লাগবে।

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

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