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

অ্যালগরিদম ও কনটেস্টে Python — constraints থেকে সমাধান

Algorithms and Contests in Python: From Constraints to Complexity, Fast Input and Classic Patterns

Taught in Bangla, this lesson applies Python to algorithms and programming contests: choosing complexity from constraints, fast input with sys.stdin, recursion limits and big integers, patterns like prefix sums, two pointers, bisect, binary search on the answer, greedy, Counter, BFS and DFS, DP with functools.cache, heapq and sieve, plus stress testing, PyPy and contest ethics.

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

  • সমস্যার constraints দেখে কোন complexity লাগবে, তা আগেই হিসাব করা — আর Python-এর বিল্ট-ইন কাজগুলোর খরচ
  • কনটেস্টে Python-এর কারিগরি দিক: দ্রুত ইনপুট-আউটপুট, recursion limit, বড় সংখ্যা
  • প্রায় ডজনখানেক চেনা ছাঁচ, প্রতিটি ছোট Python উদাহরণসহ — prefix sum থেকে Dijkstra আর sieve পর্যন্ত
  • stress testing দিয়ে নিজের সমাধানের বাগ ধরা, আর greedy কোথায় ভুল করে তা প্রমাণ করা
  • প্ল্যাটফর্ম, PyPy, কনটেস্টের নৈতিকতা — আর AI-এর সমাধান কেন নমুনায় পাস করে বড় ইনপুটে আটকে যায়

১ · অ্যালগরিদম মানে রেসিপি — আর Python কনটেস্টে কোথায় আলাদা

রাজশাহীর তানভীর একটি অনলাইন দোকানের হিসাব দেখেন। মালিক জানতে চান, “গত বছরের ১২০তম দিন থেকে ২০০তম দিন পর্যন্ত মোট বিক্রি কত?” তানভীর একটি লুপ লিখে ৮১টি দিনের বিক্রি যোগ করে দিলেন। পরদিন মালিক চাইলেন এমন পঞ্চাশটি প্রশ্নের উত্তর। তারপর অ্যাপের ড্যাশবোর্ড প্রতিটি গ্রাহকের জন্য প্রতি মিনিটে এমন হাজারো প্রশ্ন পাঠাতে শুরু করল। একই লুপ, কিন্তু এবার সার্ভার হাঁপিয়ে উঠল।

সমাধানটি কোনো দ্রুত কম্পিউটার নয়, একটি ভালো algorithm. পর্ব ০২-এ দেখেছেন, অ্যালগরিদম হলো একটি সমস্যা সমাধানের নির্দিষ্ট রেসিপি — ধাপগুলোর একটি স্পষ্ট ক্রম। এই পর্বের মূল কথাটি তার সাথে যোগ হয়: প্রতিটি রেসিপির একটি খরচ আছে, আর সেই খরচ ইনপুটের আকারের সাথে বাড়ে. কোনো রেসিপির খরচ দ্বিগুণ ইনপুটে দ্বিগুণ হয়, কোনোটির চার গুণ, কোনোটির প্রায় একই থাকে। পর্ব ৪৩-এ এই বৃদ্ধি মাপার ভাষা দেখেছেন — Big-O. এখন সেই ভাষা কাজে লাগবে আগে থেকে সিদ্ধান্ত নিতে, কোড লেখার আগেই।

বিয়েবাড়ির রান্নার কথা ভাবুন। পাঁচজনের জন্য রান্না হয় একটি হাঁড়িতে, যেকোনো পদ্ধতিতে। পাঁচশো জনের জন্য একই পদ্ধতি চলে না — প্রতিটি প্লেট আলাদা করে রান্না করলে ভোর হয়ে যাবে। বাবুর্চি তাই আগেই অতিথির সংখ্যা জেনে নেন, তারপর ঠিক করেন ডেকচি কয়টি, চুলা কয়টি, কাজ কীভাবে ভাগ হবে। কনটেস্টের সমস্যায় “অতিথির সংখ্যা” লেখা থাকে constraints-এ, আর বাবুর্চির পরিকল্পনাটিই আপনার অ্যালগরিদম।

Python-এর দুই দিক: হাতিয়ার প্রচুর, লুপ ধীর

অ্যালগরিদম শেখা আর অনুশীলনের জন্য Python খুব আরামদায়ক। কোড পড়তে প্রায় pseudocode-এর মতো, আর স্ট্যান্ডার্ড লাইব্রেরিতে তৈরি আছে কনটেস্টের প্রায় সব মৌলিক হাতিয়ার: sorted (Timsort), heapq, bisect, collections-এর deque, Counter, defaultdict, itertools-এর accumulate, permutations, combinations, আর functools.cache. সংখ্যার আকার নিয়েও চিন্তা নেই — Python-এর int যত বড় দরকার, তত বড় হয়।

অন্য দিকটিও সৎভাবে জানা দরকার। পর্ব ০৫ আর ৪৩-এ দেখেছেন, Python-এর প্রতিটি নির্দেশ interpreter-এর মধ্য দিয়ে যায়, প্রতিটি সংখ্যা একটি object. তাই একই সরল লুপ C++-এর চেয়ে সাধারণত কয়েক দশ গুণ ধীর চলে। কনটেস্টের সময়সীমা প্রায়ই ১-২ সেকেন্ড, আর অনেক সমস্যার সময়সীমা C++-এর কথা ভেবে ঠিক করা। ফলে Python-এ কনটেস্ট করার কৌশল দাঁড়ায় তিনটি: (১) ঠিক complexity — এখানে কোনো ছাড় নেই; (২) ভারী লুপ বিল্ট-ইন আর লাইব্রেরিকে দিয়ে করানো, যাদের ভেতরের লুপ C-তে চলে; (৩) জাজে PyPy থাকলে সেটি বেছে নেওয়া (অংশ ৮)।

এই পর্বের ভাবনা

কনটেস্টের সমস্যা দেখে প্রথম প্রশ্ন “কীভাবে লিখব?” নয়, বরং “কত বড় ইনপুট, আর কত সময়?” এর উত্তর থেকে বের হয় কোন complexity চলবে; complexity থেকে বের হয় কোন ছাঁচ; আর ছাঁচ জানা থাকলে Python-এ কোড প্রায়ই দশ-পনেরো লাইন। বাকি পর্বটি এই ক্রমেই সাজানো।

২ · constraints পড়ে complexity ঠিক করা

কনটেস্টের প্রতিটি সমস্যার নিচে থাকে constraints — যেমন “1 ≤ n ≤ 2·10^5, 1 ≤ a_i ≤ 10^9”, আর ওপরে সময়সীমা, যেমন “2 seconds”. নতুনরা প্রায়ই এগুলো এড়িয়ে যান, অথচ এটাই সমস্যার সবচেয়ে বড় ইঙ্গিত। সমস্যা-লেখক n-এর সীমা বেছে নেন ঠিক এমনভাবে, যাতে প্রত্যাশিত সমাধান সময়ে শেষ হয় আর সরল কিন্তু ধীর সমাধান আটকে যায়।

হিসাবের জন্য একটি মোটা দাগের সংখ্যা মনে রাখুন: CPython মোটামুটি প্রতি সেকেন্ডে ১০^7 (এক কোটি) সরল কাজ করে. এটা কোনো নির্দিষ্ট মাপ নয় — কাজের ধরন, মেশিন আর Python version ভেদে কয়েক গুণ কম-বেশি হয়; বিল্ট-ইনের ভেতরের কাজ আরও অনেক দ্রুত। C++-এর জন্য একই হিসাব সাধারণত ১০^8 থেকে ১০^9. এই অনুমান দিয়ে constraints থেকে প্রয়োজনীয় time complexity বের করার চলতি টেবিল:

n-এর সীমাযে complexity চলবেসাধারণ কৌশল
n ≤ 10–12O(n!) বা O(n · 2^n)সব বিন্যাস পরীক্ষা (itertools.permutations), backtracking
n ≤ 20O(2^n)সব উপসেট, bitmask
n ≤ 500O(n³)তিন লুপ, ছোট DP (Python-এ সীমান্তে)
n ≤ 3000–5000O(n²)দুই লুপ, 2D DP (Python-এ সতর্ক থাকুন)
n ≤ 10^5 – 2·10^5O(n log n) বা O(n)sort, binary search, heap, prefix sum, two pointers
n ≤ 10^6O(n), সাবধানে O(n log n)এক পাসের লুপ, sieve; Python-এ বিল্ট-ইন ছাড়া কঠিন
n ≤ 10^18O(log n) বা O(1)গণিত সূত্র, binary search, দ্রুত ঘাত (pow(a, b, m))

টেবিলটি C++-এর জন্য লেখা টেবিলের চেয়ে একটু সতর্ক। যেমন n ≤ 5000-এ O(n²) মানে আড়াই কোটি ধাপ — C++-এ নিরাপদ, Python-এ প্রায় সীমান্তে। এবার একটি বাস্তব সিদ্ধান্ত ধাপে ধাপে দেখুন:

  1. পড়াn ≤ 2·10^5২ সেকেন্ড
  2. সরল পথO(n²)সব জোড়া
  3. গুনে দেখা2·10^10অসম্ভব
  4. লক্ষ্যO(n log n)~৩৬ লাখ
  5. ছাঁচsort + দুই আঙুলদ্রুত ইনপুট
ধাপ ১সমস্যা আর constraints
n টি পণ্যের দাম দেওয়া আছে। কতগুলো জোড়া (i < j)
আছে, যাদের দামের যোগফল বাজেট B-এর বেশি নয়?

1 ≤ n ≤ 2·10^5    1 ≤ দাম, B ≤ 10^9    সময়: 2 s
ধাপ ২প্রথম যে সমাধান মাথায় আসে
count = 0
for i in range(n):
    for j in range(i + 1, n):
        if price[i] + price[j] <= B:
            count += 1          # নমুনায় ঠিক উত্তর
ধাপ ৩জোড়ার সংখ্যা গুনে দেখা
n(n − 1) / 2 ≈ (2·10^5)² / 2 = 2·10^10 তুলনা
Python: ~10^7 প্রতি সেকেন্ডে → ~2000 সেকেন্ড
C++   : ~10^9 প্রতি সেকেন্ডে → ~20 সেকেন্ড
# দুই ভাষাতেই TLE — ভাষা নয়, পদ্ধতি বদলাতে হবে
ধাপ ৪কোন complexity সময়ে আঁটে?
O(n)       = 2·10^5           # খুব আরামে
O(n log n) ≈ 2·10^5 × 18 ≈ 3.6·10^6  # ১ সেকেন্ডের কম
O(n√n)     ≈ 9·10^7           # Python-এ ঝুঁকি
O(n²)      ≈ 4·10^10          # বাদ
ধাপ ৫সিদ্ধান্ত
price.sort()                   # O(n log n)
lo, hi = 0, n - 1              # দুই আঙুল, O(n)
while lo < hi:
    if price[lo] + price[hi] <= B:
        count += hi - lo       # একসাথে অনেক জোড়া
        lo += 1
    else:
        hi -= 1
  1. আগে পড়ুন সীমা। সমস্যার গল্প যতই সহজ হোক, চোখ যাক সংখ্যাগুলোতে: n দুই লাখ পর্যন্ত, দাম আর বাজেট একশো কোটি পর্যন্ত, সময় ২ সেকেন্ড। n-এর সীমাই বলে দেবে কোন পথ খোলা।
  2. সরল সমাধান — নমুনায় পাস। সব জোড়া পরীক্ষা করা দুই লুপ। পাঁচ-ছয়টি সংখ্যার নমুনা ইনপুটে নির্ভুল উত্তর দেয়, তাই মনে হয় কাজ শেষ। কিন্তু জাজের গোপন টেস্টে n দুই লাখ।
  3. গুনে দেখুন — লেখার আগেই। প্রায় ২·১০^10টি জোড়া। Python-এ মোটামুটি ২০০০ সেকেন্ড, C++-এও প্রায় ২০ সেকেন্ড। এখানে ভাষা বদলে লাভ নেই; O(n²) পদ্ধতিটিই বাতিল। এই হিসাব করতে লাগে দশ সেকেন্ড, আর বাঁচায় একটি ভুল জমা।
  4. লক্ষ্য ঠিক করুন। log₂(2·10^5) প্রায় ১৮, তাই O(n log n) মানে প্রায় ৩৬ লাখ ধাপ — Python-এও আরামে। অর্থাৎ sort করা চলবে, binary search চলবে, heap চলবে। O(n√n) প্রায় নয় কোটি — Python-এ ঝুঁকিপূর্ণ।
  5. ছাঁচ বেছে নিন। সাজানোর পর দুই প্রান্তে দুটি আঙুল: সবচেয়ে সস্তা আর সবচেয়ে দামি জোড়া বাজেটে আঁটলে, সস্তাটির সাথে মাঝের সবগুলোও আঁটবে — একসাথে hi - loটি জোড়া গোনা হলো। মোট O(n log n). সাথে দ্রুত ইনপুট (অংশ ৩), কারণ দুই লাখ সংখ্যা input() দিয়ে পড়াও সময় খায়।

Python-এর বিল্ট-ইন কাজগুলোর খরচ

complexity হিসাবের সময় একটি ফাঁদ আছে: Python-এর এক লাইন মানেই এক ধাপ নয়। x in items দেখতে একটি কাজ, কিন্তু list-এ সেটি পুরো list ঘুরে দেখতে পারে। কনটেস্টে সবচেয়ে কাজের খরচগুলো (amortized মানে অনেকবারের গড়):

কাজখরচমনে রাখুন
a.append(x), a.pop(), a[i], len(a)O(1) (append amortized)list-এর শেষ প্রান্তে কাজ সস্তা
a.insert(0, x), a.pop(0)O(n)বাকি সব উপাদান এক ঘর সরে; সারির জন্য deque
x in a (list), a.index(x), a.count(x)O(n)লুপের ভেতরে হলে লুকানো O(n²)
x in s (set), d[k], d[k] = v (dict)গড়ে O(1)hash table; খোঁজার কাজে প্রথম পছন্দ
a[i:j], a + b, a.copy()O(j − i), O(len)slice নতুন list বানায় — লুপে বারবার নয়
sorted(a), a.sort()O(n log n)Timsort; আগে থেকে প্রায় সাজানো হলে আরও দ্রুত
min, max, sumO(n)লুপের ভেতরে বারবার ডাকবেন না
heappush, heappopO(log n)heapify পুরো list-এ O(n)
bisect_left / bisect_rightO(log n)কিন্তু insort O(n), কারণ ঢোকাতে সরাতে হয়
dq.append, dq.popleft (deque)O(1)BFS-এর queue
লুপে s += piece (string)মোট O(n²) পর্যন্তটুকরো list-এ রেখে শেষে "".join

৩ · কনটেস্টে Python-এর কারিগরি — দ্রুত ইনপুট, recursion limit আর বড় সংখ্যা

সঠিক complexity-র পরেও Python-এ তিনটি জায়গায় অনেকে আটকে যান। তিনটিই অল্প কয়েক লাইনের অভ্যাসে মেটে।

দ্রুত ইনপুট-আউটপুট

পর্ব ১০-এর input() সাধারণ প্রোগ্রামের জন্য চমৎকার, কিন্তু প্রতিটি ডাকের নিজস্ব খরচ আছে। দুই লাখ লাইনের ইনপুটে শুধু পড়তেই যথেষ্ট সময় চলে যেতে পারে, অ্যালগরিদম শুরুর আগেই। online judge-এ Python ব্যবহারকারীদের দুটি প্রচলিত fast I/O পদ্ধতি:

import sys
input = sys.stdin.readline        # পদ্ধতি ১: লাইন ধরে, কিন্তু দ্রুত

def main():
    n = int(input())
    a = list(map(int, input().split()))
    name = input().strip()        # লাইনের শেষে "\n" থাকে — string-এ strip জরুরি
    print(sum(a), name)

main()
import sys

def main():
    data = sys.stdin.buffer.read().split()   # পদ্ধতি ২: পুরো ইনপুট একবারে, টুকরো করে
    t = int(data[0])
    ...
    sys.stdout.write("\n".join(answers) + "\n")   # সব উত্তর একবারে লেখা

main()

প্রথম পদ্ধতিতে sys.stdin.readline লাইনের শেষের newline সহ string দেয়। int() আর split() এই newline নিজেই উপেক্ষা করে, কিন্তু শব্দ তুলনার আগে .strip() ভুলে গেলে "yes\n" == "yes" মিথ্যা হয়ে উত্তর ভুল আসে — খুব পরিচিত বাগ। দ্বিতীয় পদ্ধতি সবচেয়ে দ্রুত: পুরো ইনপুট bytes হিসেবে এক ডাকে আসে, split() সব ফাঁকা জায়গায় ভাগ করে, আর আপনি একটি সূচক এগিয়ে এগিয়ে সংখ্যা নেন। (int() bytes থেকেও সরাসরি সংখ্যা বানাতে পারে।)

আউটপুটেও একই নীতি। এক লাখ উত্তরের জন্য এক লাখবার print ডাকার বদলে উত্তরগুলো একটি list-এ জমিয়ে শেষে "\n".join(...) দিয়ে একবারে লিখুন। join-এর আগে সংখ্যাগুলোকে string বানাতে হয় — map(str, answers). সব কোড main() ফাংশনের ভেতরে রাখার একটি ছোট বাড়তি সুবিধাও আছে: CPython-এ local ভ্যারিয়েবল পড়া global-এর চেয়ে সামান্য সস্তা (পর্ব ৪৩)।

recursion limit — আর কেন নিজের stack নিরাপদ

পর্ব ১৩-এর recursion অনেক অ্যালগরিদমকে সুন্দর করে লেখে — গাছে নামা, DFS, DP. কিন্তু CPython-এ ডিফল্ট সীমা প্রায় ১০০০ স্তর। এক লাখ node-এর একটি লম্বা শিকলের মতো গাছে recursive DFS চালালে আসে RecursionError: maximum recursion depth exceeded. সাধারণ প্রতিকার:

import sys
sys.setrecursionlimit(300_000)     # শুধু Python-এর গোনার সীমা বাড়ে

সমস্যা হলো, এই সীমা শুধু Python-এর হিসাবের খাতা। প্রতিটি ডাক নতুন একটি frame নেয় (পর্ব ১৩-এর call stack), আর খুব গভীর recursion-এ প্রোগ্রামের আসল (অপারেটিং সিস্টেমের দেওয়া) stack মেমরি ফুরিয়ে যেতে পারে — বিশেষত পুরোনো Python version-এ, PyPy-তে, বা functools.cache-এর মতো C-তে লেখা মোড়কের ভেতর দিয়ে ডাক গেলে। তখন সুন্দর traceback-ও আসে না — প্রোগ্রাম হঠাৎ crash করে, জাজ দেখায় Runtime Error. কেউ কেউ threading.stack_size দিয়ে বড় stack-এর একটি thread-এ পুরো সমাধান চালান; এটি কাজ করে, তবে প্ল্যাটফর্মভেদে আচরণ আলাদা। সবচেয়ে নিরাপদ পথ: গভীর হতে পারে এমন DFS নিজের stack দিয়ে iterative লেখা (অংশ ৬)। একটি list-এর append আর pop দিয়েই কাজ চলে, আর গভীরতার কোনো সীমা থাকে না।

বড় সংখ্যা — Python-এ বিনামূল্যে

C বা C++-এ ১০^9 পর্যন্ত দুটি সংখ্যা গুণ করলে ৩২-বিটের int উপচে পড়ে (overflow), আর উত্তর চুপচাপ ভুল হয়ে যায়; কনটেস্টে Wrong Answer-এর সবচেয়ে চেনা কারণগুলোর একটি এটি। Python-এর int arbitrary precision (পর্ব ০৭): 2 ** 200, ১০০-এর ফ্যাক্টোরিয়াল, বিশাল যোগফল — সব নির্ভুল। এটা কনটেস্টে Python-এর একটি সত্যিকারের সুবিধা। তিনটি সতর্কতা মনে রাখুন:

  • বড় সংখ্যা ধীর: হাজার অঙ্কের সংখ্যার গুণ এক ধাপ নয়। “উত্তর 10^9 + 7 দিয়ে mod করে দিন” লেখা থাকলে প্রতিটি ধাপেই mod নিন, শেষে একবার নয়। দ্রুত ঘাতের জন্য বিল্ট-ইন pow(a, b, m); Python 3.8 থেকে pow(a, -1, m) দেয় modular inverse.
  • বিশাল সংখ্যা string-এ: Python 3.11 থেকে (আর কিছু পুরোনো version-এর নিরাপত্তা আপডেটে) প্রায় ৪৩০০ অঙ্কের বেশি int-কে str() বা print করতে গেলে ডিফল্টে ValueError আসে। সত্যিই দরকার হলে sys.set_int_max_str_digits(0) দিয়ে সীমা তোলা যায়।
  • float-এ নয়: int(n ** 0.5) বড় n-এ float-এর কারণে ১ কম-বেশি হতে পারে। পূর্ণসংখ্যার বর্গমূলের জন্য math.isqrt(n), ভাগের জন্য //.

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

prefix sum: একবার হিসাব, বারবার উত্তর

তানভীরের সমস্যায় ফিরে যাই। প্রতিটি প্রশ্নে l থেকে r দিনের বিক্রি নতুন করে যোগ করলে একেকটি প্রশ্নে O(n), q টি প্রশ্নে O(n · q) — দুই লাখ দিন আর দুই লাখ প্রশ্নে চার হাজার কোটি ধাপ। prefix sum-এর ভাবনা: আগে একবার একটি তালিকা বানান, যার i-তম ঘরে থাকবে প্রথম i দিনের মোট বিক্রি। তাহলে l থেকে r-এর যোগফল = (প্রথম r দিনের যোগ) − (প্রথম l−1 দিনের যোগ) — একটি বিয়োগ, O(1).

ক্রিকেটের স্কোরকার্ডে প্রতি ওভার শেষে দলের মোট রান লেখা থাকে। “১১ থেকে ২০ ওভারে কত রান হলো?” জানতে কেউ দশটি ওভারের রান আলাদা করে যোগ করেন না — ২০ ওভার শেষের মোট থেকে ১০ ওভার শেষের মোট বিয়োগ করেন। স্কোরকার্ডের ওই “মোট” কলামটিই prefix sum.

1sales = [1200, 800, 1500, 900, 2000]2prefix = [0]3for s in sales:4    prefix.append(prefix[-1] + s)5# দিন l থেকে r (1-ভিত্তিক)6print(prefix[r] - prefix[l - 1])
ভ্যারিয়েবল
s
prefix
l, r
  1. শুরুতে একটি শূন্য। পাঁচ দিনের বিক্রি, আর prefix-এর শুরুতে 0 — “প্রথম ০ দিনের যোগফল”. এই শূন্যটির জন্যই পরে l = 1 হলে আলাদা কোনো শর্ত লাগবে না। edge case সামলানোর এই ছোট কৌশল খুব কাজের।
  2. প্রথম দিন যোগ। আগের মোট (০) + আজকের বিক্রি (১২০০) = ১২০০. প্রতিটি নতুন ঘর আগের ঘরের ওপর দাঁড়ায় — তাই পুরো তালিকা এক পাসে, O(n).
  3. দ্বিতীয় দিন। ১২০০ + ৮০০ = ২০০০. লক্ষ করুন, আমরা কখনো পেছনের সব দিন আবার যোগ করছি না; শুধু শেষ ঘরটি (prefix[-1]) দেখছি।
  4. তালিকা তৈরি। বাকি তিন দিনও একইভাবে: ৩৫০০, ৪৪০০, ৬৪০০. prefix-এর দৈর্ঘ্য n + 1 = ৬. Python-এ পুরো কাজটি এক লাইনেও হয়: [0, *itertools.accumulate(sales)] — আর accumulate-এর লুপ C-তে চলে।
  5. প্রশ্ন: দিন ২ থেকে ৪। prefix[4] - prefix[1] = ৪৪০০ − ১২০০ = ৩২০০. মিলিয়ে দেখুন: ৮০০ + ১৫০০ + ৯০০ = ৩২০০. তিনটি দিন হোক বা তিন লাখ — একটিই বিয়োগ।
  6. প্রশ্ন: পুরো সময়। l = 1 হলে prefix[0] = ০ বিয়োগ হয়, তাই উত্তর ৬৪০০ — শুরুর শূন্যটির সুফল। মোট খরচ: বানাতে O(n), q টি প্রশ্নে O(q). চার হাজার কোটি ধাপ নেমে এল প্রায় চার লাখে।

এবার একই সমাধান জাজের ঢঙে — ইনপুট আসে standard input থেকে, আর উত্তর যায় একবারে। প্রথম লাইনে n (দিন) আর q (প্রশ্ন), দ্বিতীয় লাইনে প্রতিদিনের বিক্রি, তারপর প্রতিটি প্রশ্নের l আর r:

import sys
from itertools import accumulate

def main():
    data = sys.stdin.buffer.read().split()      # পুরো ইনপুট একবারে
    n, q = int(data[0]), int(data[1])
    sales = list(map(int, data[2:2 + n]))
    prefix = [0, *accumulate(sales)]            # prefix[i] = প্রথম i দিনের যোগ
    out, pos = [], 2 + n
    for _ in range(q):
        l, r = int(data[pos]), int(data[pos + 1])
        pos += 2
        out.append(prefix[r] - prefix[l - 1])   # প্রতিটি প্রশ্ন O(1)
    sys.stdout.write("\n".join(map(str, out)) + "\n")

main()

ইনপুট হিসেবে দেওয়া হয়েছে: প্রথম লাইনে 7 3, দ্বিতীয় লাইনে সাত দিনের বিক্রি 1200 800 1500 900 2000 700 1100, তারপর তিনটি প্রশ্ন 1 3, 2 5, 4 7. আউটপুট তিন লাইন: 3500, 5200, 4700. data হলো সব টুকরোর একটি list (bytes হিসেবে), আর pos সূচকটি পড়ার জায়গা মনে রাখে — লাইন কোথায় ভাঙল, তা নিয়ে আর ভাবতে হয় না। [0, *accumulate(sales)] স্টেপারের লুপটিরই এক লাইনের রূপ। শেষে সব উত্তর একটি string হয়ে একবারে লেখা হলো। দুই লাখ দিন আর দুই লাখ প্রশ্নেও এই প্রোগ্রাম Python-এ সাধারণত এক সেকেন্ডের মধ্যে শেষ হওয়ার কথা — নিজে বড় ইনপুট বানিয়ে মেপে দেখতে পারেন।

two pointers আর sliding window: দুই আঙুলের কৌশল

অংশ ২-এর স্টেপারে দেখেছেন দুই আঙুলের এক ঝলক। two pointers কৌশলে একটি সারিতে দুটি সূচক থাকে, আর প্রতিটি ধাপে একটি নিয়ম দেখে ঠিক করা হয় কোনটি সরবে। কোনো সূচক কখনো পেছনে ফেরে না, তাই মোট সরানো সর্বোচ্চ 2n বার — O(n). বাস্তব প্রশ্ন: একটি দোকানে ছয়টি পণ্য, আপনার বাজেট ঠিক ৭৫০ টাকা — কোন দুটি পণ্য কিনলে পুরো টাকা খরচ হয়?

1prices = [120, 250, 300, 450, 600, 800]  # সাজানো2lo, hi = 0, len(prices) - 13while lo < hi:4    total = prices[lo] + prices[hi]5    if total == budget: break6    elif total < budget: lo += 17    else: hi -= 1
budget = 750
lo → দাম
hi → দাম
total
সিদ্ধান্ত
  1. দুই প্রান্ত থেকে শুরু। সবচেয়ে সস্তা (১২০) + সবচেয়ে দামি (৮০০) = ৯২০, বাজেটের বেশি। ৮০০ টাকার পণ্যটি সবচেয়ে সস্তাটির সাথেও বেশি হয়, তাই অন্য কারও সাথে তো আরও বেশি — ওটিকে চিরতরে বাদ দেওয়া নিরাপদ। hi এক ঘর বাঁয়ে।
  2. এবার কম। ১২০ + ৬০০ = ৭২০, বাজেটের কম। ১২০ টাকার পণ্যটি সবচেয়ে দামি বাকিটির সাথেও কম পড়ে — ওটিও বাদ। lo এক ঘর ডানে। প্রতিটি ধাপে একটি পণ্য নিশ্চিতভাবে বাদ যাচ্ছে, এটাই কৌশলের প্রাণ।
  3. আবার বেশি। ২৫০ + ৬০০ = ৮৫০ → ৬০০ বাদ। দুই লুপে সব জোড়া দেখলে লাগত ১৫টি তুলনা; এখানে প্রতি ধাপে একটি করে।
  4. আবার কম। ২৫০ + ৪৫০ = ৭০০ → ২৫০ বাদ। লক্ষ করুন, কোনো সূচক পেছনে ফেরেনি — তাই n উপাদানে সর্বোচ্চ n − 1টি ধাপ।
  5. মিলে গেছে। ৩০০ + ৪৫০ = ৭৫০. দুই লাখ পণ্যেও একই কৌশলে প্রায় দুই লাখ ধাপ, আর আগে sort-এর O(n log n). দাম সাজানো না থাকলে, আর মূল সূচক দরকার হলে, অংশ ৬-এর dict-পদ্ধতি আরও সরাসরি।

sliding window একই পরিবারের কৌশল: দুটি সূচক একটি জানালার দুই কিনারা, আর জানালাটি ডানে সরে। যেমন “পরপর ৭ দিনের সর্বোচ্চ মোট বিক্রি” — প্রতিবার সাত দিন নতুন করে যোগ না করে, নতুন দিন যোগ আর সবচেয়ে পুরোনো দিন বাদ:

def best_week(sales, k=7):
    window = sum(sales[:k])                 # প্রথম জানালা
    best = window
    for i in range(k, len(sales)):
        window += sales[i] - sales[i - k]   # নতুন দিন যোগ, পুরোনো দিন বাদ
        best = max(best, window)
    return best

জানালার আকার বদলাতে পারে এমন সমস্যাতেও (যেমন “যোগফল S-এর বেশি না হওয়া দীর্ঘতম টানা অংশ”) একই ভাবনা: ডান কিনারা সবসময় এগোয়, শর্ত ভাঙলে বাম কিনারা এগোয় যতক্ষণ না শর্ত ফেরে।

৫ · ছাঁচ ২ — খোঁজা আর সাজানো: bisect, উত্তরের ওপর binary search, sort key, greedy

bisect — সাজানো list-এ binary search, তৈরি অবস্থায়

পর্ব ০২-এ linear search আর binary search-এর পার্থক্য দেখেছেন। নিজে binary search লিখলে সীমানায় ১ কম-বেশির ভুল খুব সহজে হয়। Python-এর bisect module এটা নির্ভুলভাবে করে দেয়:

from bisect import bisect_left, bisect_right

prices = [120, 250, 250, 300, 450, 800]      # অবশ্যই সাজানো
bisect_left(prices, 250)     # 1 — প্রথম 250 যে ঘরে
bisect_right(prices, 250)    # 3 — শেষ 250-এর ঠিক পরের ঘর
bisect_right(prices, 300)    # 4 — 300 টাকা বা কম দামের পণ্য 4টি
bisect_right(prices, 250) - bisect_left(prices, 250)   # 2 — 250 কয়বার আছে

দুটির পার্থক্য শুধু সমান মান পেলে কোন দিকে থামবে। “x-এর চেয়ে ছোট কয়টি” = bisect_left(a, x); “x বা তার চেয়ে ছোট কয়টি” = bisect_right(a, x). প্রতিটি O(log n). Python 3.10 থেকে key= parameter-ও আছে। সতর্কতা: list সাজানো না থাকলে bisect কোনো ভুল জানায় না, শুধু ভুল উত্তর দেয়।

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

কনটেস্টের একটি প্রিয় কৌশল binary search on the answer. ধরুন একটি গার্মেন্টস কারখানায় পাঁচটি অর্ডার: ৩০, ১১, ২৩, ৪, ২০ ডজন। একটি লাইন প্রতি ঘণ্টায় k ডজন সেলাই করে, তবে এক ঘণ্টায় একটির বেশি অর্ডার ধরে না। ৬ ঘণ্টায় সব শেষ করতে k-এর সবচেয়ে ছোট মান কত? সরাসরি সূত্র নেই। কিন্তু “k দিয়ে কি ৬ ঘণ্টায় হবে?” — এই হ্যাঁ/না প্রশ্নের উত্তর সহজ, আর k বাড়লে উত্তর একবার “হ্যাঁ” হলে আর কখনো “না” হয় না। এই একমুখী (monotonic) বৈশিষ্ট্য থাকলেই সম্ভাব্য উত্তরের পরিসরে binary search চলে:

def hours_needed(orders, k):
    return sum((x + k - 1) // k for x in orders)    # প্রতিটি অর্ডারে ceil(x / k) ঘণ্টা

def min_speed(orders, h):
    lo, hi = 1, max(orders)          # উত্তর এই পরিসরেই
    while lo < hi:
        mid = (lo + hi) // 2
        if hours_needed(orders, mid) <= h:
            hi = mid                 # mid চলে — আরও ছোট চেষ্টা
        else:
            lo = mid + 1             # mid চলে না — বাড়াতে হবে
    return lo

# min_speed([30, 11, 23, 4, 20], 6) → 23

খরচ: প্রতিটি যাচাই O(n), আর যাচাই হয় log(max) বার — মোট O(n log max). ceil-এর জন্য (x + k - 1) // k পূর্ণসংখ্যার হিসাব; math.ceil(x / k) বড় সংখ্যায় float-এর কারণে ভুল করতে পারে।

key দিয়ে sort, আর কাস্টম তুলনা

Python-এর sort stable আর key function নেয় (পর্ব ১৪, ৩৮)। একাধিক নিয়মে সাজাতে tuple-key সবচেয়ে সহজ — tuple আগে প্রথম উপাদানে তুলনা হয়, সমান হলে দ্বিতীয়তে:

students = [("Rafi", 88), ("Mim", 95), ("Arif", 88)]
students.sort(key=lambda s: (-s[1], s[0]))    # নম্বর বেশি আগে, সমান হলে নামের ক্রমে
# [('Mim', 95), ('Arif', 88), ('Rafi', 88)]

from functools import cmp_to_key
nums = ["3", "30", "34", "5", "9"]            # জুড়ে সবচেয়ে বড় সংখ্যা বানাতে হবে
nums.sort(key=cmp_to_key(lambda a, b: (b + a > a + b) - (b + a < a + b)))
"".join(nums)    # '9534330'

সংখ্যার ক্ষেত্রে -s[1] দিয়ে উল্টো ক্রম পাওয়া যায়। কিছু ক্রম কোনো key দিয়ে প্রকাশ করা কঠিন — যেমন দ্বিতীয় উদাহরণে “a আগে বসবে যদি a + b জোড়া b + a-এর চেয়ে বড় হয়”. তখন দুটি উপাদান তুলনার একটি ফাংশন লিখে cmp_to_key দিয়ে key বানাতে হয়। ফাংশনটি ঋণাত্মক ফেরত দিলে a আগে, ধনাত্মক হলে b আগে।

greedy — প্রতিবার সেরা বাছাই, কিন্তু প্রমাণসহ

greedy algorithm প্রতিটি ধাপে তখনকার সবচেয়ে ভালো দেখানো সিদ্ধান্ত নেয়, পেছনে ফেরে না। একটি ক্লাসিক উদাহরণ: একটি সেমিনার হলে সারাদিনের কয়েকটি বক্তৃতার অনুরোধ, সময় ওভারল্যাপ করে। সবচেয়ে বেশি কয়টি বক্তৃতা রাখা যায়? সঠিক greedy নিয়ম: যেটি সবচেয়ে আগে শেষ হয়, সেটি আগে নিন.

talks = [(9, 11), (10, 12), (11, 13), (12, 14), (13, 15)]   # (শুরু, শেষ)
talks.sort(key=lambda t: t[1])         # আগে যেটি শেষ হয়
end, chosen = 0, []
for start, finish in talks:
    if start >= end:                   # আগের বক্তৃতার সাথে ধাক্কা নেই
        chosen.append((start, finish))
        end = finish
# chosen → [(9, 11), (11, 13), (13, 15)]

এই নিয়মটি প্রমাণসহ সঠিক: আগে শেষ হওয়াটি বাছলে বাকি দিনের জন্য সবচেয়ে বেশি সময় থাকে। কিন্তু “সবচেয়ে ছোট বক্তৃতা আগে” বা “সবচেয়ে আগে শুরু হওয়াটি আগে” — শুনতে যুক্তিসংগত, অথচ ভুল উত্তর দেয়। greedy-র বিপদ এখানেই: অনেক greedy নিয়ম বিশ্বাসযোগ্য শোনায়। সবচেয়ে পরিচিত উদাহরণ নোট বা কয়েন। বাংলাদেশের টাকার নোটে (১, ২, ৫, ১০, ২০, ৫০, ১০০, ২০০, ৫০০, ১০০০) “সবচেয়ে বড় নোট আগে” নিয়মে সবসময় সবচেয়ে কম নোট পাওয়া যায়। কিন্তু নোটগুলো ১, ৬ আর ৭ হলে ১২ টাকার জন্য greedy দেয় ৭ + ১×৫ = ৬টি নোট, অথচ ৬ + ৬ = মাত্র ২টি। এই ভুলটি অংশ ৮-এ একটি প্রোগ্রাম নিজেই খুঁজে বের করবে।

৬ · ছাঁচ ৩ — dict, set আর Counter; stack; গ্রিডে BFS; DFS

hashing: “আগে দেখেছি কি?” প্রশ্নের উত্তর O(1)-এ

অনেক O(n²) সমস্যার ভেতরে লুকিয়ে থাকে একটি প্রশ্ন: “এই মানটি কি আগে এসেছে?” পর্ব ১৬-১৭-এর dict আর set সেই প্রশ্নের উত্তর গড়ে O(1)-এ দেয়। অংশ ৪-এর বাজেট সমস্যাটিই দাম না সাজিয়ে, মূল ঘরসহ:

def two_sum(prices, budget):
    seen = {}                          # দাম → কোন ঘরে দেখেছি
    for i, p in enumerate(prices):
        if budget - p in seen:         # জোড়ার সঙ্গী আগে এসেছে?
            return seen[budget - p], i
        seen[p] = i
    return None

# two_sum([450, 120, 800, 300, 250], 750) → (0, 3)

একবারের পাসে O(n). শব্দ নিয়ে কাজেও একই ভাবনা: দুটি শব্দ একে অপরের anagram (একই অক্ষর, ভিন্ন ক্রম) কি না — Counter(a) == Counter(b) (পর্ব ২৭-এর Counter), বা sorted(a) == sorted(b). অনেক শব্দকে anagram-দলে ভাগ করতে চাবি হিসেবে "".join(sorted(word)) নিয়ে একটি defaultdict(list). সবচেয়ে বেশিবার আসা জিনিস চাইলে Counter(items).most_common(3).

stack: বন্ধনী মেলানো

“{[()]}” ঠিক আছে, “([)]” নেই। প্রতিটি বন্ধ-বন্ধনীকে মিলতে হবে সবচেয়ে শেষে খোলা বন্ধনীর সাথে — শেষে ঢোকা আগে বের হওয়া, অর্থাৎ stack. Python-এ list-ই stack:

def is_valid(s):
    pairs = {")": "(", "]": "[", "}": "{"}
    stack = []
    for ch in s:
        if ch in "([{":
            stack.append(ch)                       # খোলা — রেখে দিন
        elif not stack or stack.pop() != pairs[ch]:
            return False                           # সঙ্গী নেই, বা ভুল সঙ্গী
    return not stack                               # খোলা কিছু বাকি থাকলে ভুল

গ্রিডে BFS: গোলকধাঁধার সবচেয়ে ছোট পথ

একটি গোলকধাঁধা — কিছু ঘরে দেয়াল (#), শুরু S, শেষ E; প্রতি ধাপে ওপরে, নিচে, বাঁয়ে বা ডানে এক ঘর যাওয়া যায়। সবচেয়ে কম কয় ধাপে E-তে পৌঁছানো যায়? গ্রিডটি আসলে একটি graph: প্রতিটি খোলা ঘর একটি বিন্দু, পাশাপাশি খোলা ঘর সংযুক্ত। ওজনহীন গ্রাফে shortest path-এর অ্যালগরিদম BFS: পুকুরে ঢিল ফেললে যেমন ঢেউ বৃত্তাকারে ছড়ায় — আগে ১ ধাপ দূরের সব ঘর, তারপর ২ ধাপ দূরের সব। এর জন্য লাগে একটি queue, আর Python-এ সেটি collections.deque (list-এর pop(0) O(n), deque-এর popleft() O(1)).

from collections import deque

def shortest_path(grid, start, goal):
    rows, cols = len(grid), len(grid[0])
    dist = {start: 0}                          # দেখা ঘর → দূরত্ব
    q = deque([start])
    while q:
        r, c = q.popleft()                     # সবচেয়ে পুরোনোটি আগে
        if (r, c) == goal:
            return dist[(r, c)]
        for dr, dc in ((-1, 0), (1, 0), (0, -1), (0, 1)):   # ওপর, নিচ, বাঁ, ডান
            nr, nc = r + dr, c + dc
            if (0 <= nr < rows and 0 <= nc < cols
                    and grid[nr][nc] != "#" and (nr, nc) not in dist):
                dist[(nr, nc)] = dist[(r, c)] + 1
                q.append((nr, nc))
    return -1                                  # পৌঁছানো যায় না

৩ সারি × ৪ কলামের একটি ছোট গোলকধাঁধায় এই কোড কীভাবে ছড়ায়, ধাপে ধাপে দেখুন। ঘরের সংখ্যাটি শুরু থেকে দূরত্ব; . মানে এখনো দেখা হয়নি।

ধাপ ১শুরু: S = (0,0), E = (2,0)
     কলাম 0  1  2  3
সারি 0  0  .  .  #
সারি 1  #  .  #  .
সারি 2  E  .  .  .

q = deque([(0,0)])
ধাপ ২popleft (0,0) — প্রতিবেশী যোগ
     কলাম 0  1  2  3
সারি 0  0  1  .  #
সারি 1  #  .  #  .
সারি 2  E  .  .  .

q = deque([(0,1)])      # নিচে দেয়াল, শুধু ডানে
ধাপ ৩popleft (0,1) — দুটি নতুন ঘর
     কলাম 0  1  2  3
সারি 0  0  1  2  #
সারি 1  #  2  #  .
সারি 2  E  .  .  .

q = deque([(1,1), (0,2)])  # নিচে, তারপর ডানে
ধাপ ৪popleft (1,1), তারপর (0,2)
     কলাম 0  1  2  3
সারি 0  0  1  2  #
সারি 1  #  2  #  .
সারি 2  E  3  .  .

q = deque([(2,1)])      # (0,2) থেকে পথ বন্ধ
ধাপ ৫popleft (2,1) — E পাওয়া গেল
     কলাম 0  1  2  3
সারি 0  0  1  2  #
সারি 1  #  2  #  .
সারি 2  4  3  4  .

q = deque([(2,0), (2,2)])  # পরের popleft-এ E → উত্তর 4
  1. শুরুর ঘরটি queue-তে। S-এর দূরত্ব ০, সেটি dist-এ লেখা আর queue-তে রাখা। dist একসাথে দুটি কাজ করে: দূরত্ব মনে রাখে, আর কোন ঘর আগে দেখা হয়েছে তা জানায় — যাতে কোনো ঘর দুবার queue-তে না ঢোকে।
  2. প্রথম ঢেউ। (0,0) বের করে চার দিকে দেখা: ওপরে আর বাঁয়ে গ্রিডের বাইরে, নিচে দেয়াল। শুধু ডানের (0,1) খোলা — দূরত্ব ১, queue-র পেছনে যোগ।
  3. দ্বিতীয় ঢেউ। (0,1) থেকে নিচে (1,1) আর ডানে (0,2) — দুটিরই দূরত্ব ২. বাঁয়ের (0,0) আগেই dist-এ আছে, তাই বাদ। queue-তে এখন দুটি ঘর, দুটিই ২ ধাপ দূরের। BFS-এর মূল বৈশিষ্ট্য এটাই: queue-তে সবসময় একই বা পাশাপাশি দূরত্বের ঘর থাকে।
  4. তৃতীয় ঢেউ। (1,1) থেকে নিচে (2,1), দূরত্ব ৩. তারপর (0,2) বের হলো, কিন্তু তার নিচে আর ডানে দেয়াল — কোনো নতুন ঘর নেই। একটি কানাগলি; BFS কোনো আফসোস ছাড়াই অন্য পথে এগোয়।
  5. লক্ষ্যে পৌঁছানো। (2,1) থেকে বাঁয়ে E আর ডানে (2,2), দুটির দূরত্ব ৪. পরের popleft-এ E বের হলে ফাংশন ৪ ফেরত দেয়। যেহেতু ঘরগুলো দূরত্বের ক্রমে বের হয়, প্রথমবার E-তে পৌঁছানোর দূরত্বই সবচেয়ে কম। প্রতিটি ঘর একবারই queue-তে ঢোকে, তাই মোট খরচ O(সারি × কলাম)।

DFS — recursion দিয়ে, আর নিজের stack দিয়ে

DFS একটি পথ ধরে যত গভীরে সম্ভব যায়, আটকে গেলে ফিরে আসে — “কোন কোন শহরে যাওয়া সম্ভব”, “গ্রাফে কয়টি আলাদা অংশ”, গাছের হিসাব, backtracking. recursion দিয়ে লিখলে প্রায় সংজ্ঞার মতো দেখায়, কিন্তু অংশ ৩-এর গভীরতার সমস্যা আছে। তাই কনটেস্টে Python-এ নিরাপদ রূপটি নিজের stack দিয়ে। গ্রাফটি এখানে একটি adjacency list — প্রতিটি বিন্দুর প্রতিবেশীদের তালিকা:

def reachable(graph, start):          # graph[u] = u-এর প্রতিবেশীদের list
    seen = {start}
    stack = [start]
    while stack:
        u = stack.pop()                # শেষে রাখা আগে — তাই গভীরে আগে
        for v in graph[u]:
            if v not in seen:
                seen.add(v)
                stack.append(v)
    return seen

BFS কোডের সাথে মিলিয়ে দেখুন: পার্থক্য প্রায় শুধু popleft() বনাম pop() — queue বনাম stack. এই এক বদলেই খোঁজার ধরন “স্তরে স্তরে” থেকে “গভীরে আগে” হয়ে যায়। কোন কোন বিন্দুতে পৌঁছানো যায় — এমন প্রশ্নে দুটিই একই উত্তর দেয়; সবচেয়ে ছোট পথের জন্য লাগে BFS.

৭ · ছাঁচ ৪ — DP, heapq আর sieve

dynamic programming: একবার হিসাব, খাতায় রাখা

একটি সিঁড়িতে n ধাপ; একবারে ১ বা ২ ধাপ ওঠা যায়। কত ভাবে ওপরে ওঠা যায়? শেষ লাফটি হয় ১ ধাপের (তার আগে n−1 ধাপে ওঠার সব উপায়) নয়তো ২ ধাপের (n−2-এর সব উপায়) — তাই ways(n) = ways(n-1) + ways(n-2). সরাসরি recursion লিখলে একই উপ-সমস্যা বারবার হিসাব হয়; ডাকের সংখ্যা n-এর সাথে প্রায় দ্বিগুণ হারে বাড়ে। dynamic programming-এর মূল ভাবনা: প্রতিটি উপ-সমস্যা একবারই সমাধান করে উত্তর রেখে দেওয়া। Python-এ সবচেয়ে সহজ পথ functools.cache (পর্ব ৩৫, ৩৮) — এক লাইনেই memoization:

from functools import cache

@cache
def ways(n):                  # n ধাপের সিঁড়ি, একবারে 1 বা 2 ধাপ
    if n <= 1:
        return 1              # base case
    return ways(n - 1) + ways(n - 2)

print(ways(90))               # 4660046610375530309 — চোখের পলকে

@cache ছাড়া ways(90) শেষ হতে হাজার হাজার বছর লাগার কথা; cache-সহ মাত্র ৯১টি আলাদা হিসাব। উত্তরটি ৬৪-বিট সীমার কাছাকাছি, আর Python-এ তা নিয়ে চিন্তাই নেই। তবে লক্ষ করুন — এটি recursion. ways(5000) ডাকলে আবার RecursionError. তখন bottom-up টেবিল: ছোট থেকে বড় ক্রমে একটি list ভরা, কোনো recursion ছাড়া। Python-এ কনটেস্টে bottom-up সাধারণত দ্রুতও, কারণ ফাংশন ডাকের খরচ নেই। অংশ ৮-এর প্রোগ্রামে কয়েন সমস্যার bottom-up DP দেখবেন: best[x] = x টাকা বানাতে সবচেয়ে কম নোট, আর প্রতিটি x-এর জন্য সব নোট চেষ্টা করে best[x - c] + 1-এর সবচেয়ে ছোটটি।

DP চেনার উপায়

দুটি লক্ষণ: (১) বড় উত্তরটি একই ধরনের ছোট উত্তর থেকে বানানো যায় (একটি সূত্র বা “recurrence”); (২) একই ছোট উপ-সমস্যা বারবার আসে। greedy-র সাথে পার্থক্য: greedy একটি পছন্দ করে এগিয়ে যায়, DP সব পছন্দ বিবেচনা করে — কিন্তু প্রতিটি উপ-সমস্যা মাত্র একবার। state কী (এখানে “x টাকা”), আর state কয়টি (amount + ১টি) — এটা ঠিক করাই DP-র আসল কাজ; খরচ = state-এর সংখ্যা × প্রতি state-এর কাজ।

heapq: top-k আর Dijkstra

পর্ব ২৭-এ দেখা heap সবসময় সবচেয়ে ছোট উপাদানটি হাতের কাছে রাখে। Python-এর heapq একটি সাধারণ list-কে min-heap হিসেবে চালায়। একটি অনলাইন কুইজের সবচেয়ে বড় ৩টি স্কোর চাই — পুরো list sort না করেই (top-k, O(n log k)):

import heapq
scores = [78, 95, 62, 88, 91, 70]
heapq.nlargest(3, scores)               # [95, 91, 88]
heapq.nsmallest(2, scores)              # [62, 70]

heap-এর সবচেয়ে বিখ্যাত ব্যবহার একটি priority queue হিসেবে Dijkstra-র অ্যালগরিদমে — রাস্তার দৈর্ঘ্য (ওজন) ভিন্ন হলে সবচেয়ে ছোট পথ। মূল ভাবনা: যে বিন্দুর বর্তমান জানা দূরত্ব সবচেয়ে কম, সেটি আগে চূড়ান্ত করুন। heap সেই “সবচেয়ে কম” প্রতিবার O(log n)-এ দেয়:

import heapq

def dijkstra(graph, src):               # graph[u] = [(v, ওজন), ...]
    dist = {src: 0}
    pq = [(0, src)]                     # (দূরত্ব, বিন্দু) — tuple আগে দূরত্বে তুলনা হয়
    while pq:
        d, u = heapq.heappop(pq)
        if d > dist[u]:
            continue                    # পুরোনো, বাতিল এন্ট্রি
        for v, w in graph[u]:
            if d + w < dist.get(v, float("inf")):
                dist[v] = d + w
                heapq.heappush(pq, (d + w, v))
    return dist

Python-এর heapq-তে “দূরত্ব কমানো” কাজটি নেই, তাই নতুন কম দূরত্ব পেলে আরেকটি এন্ট্রি ঢোকানো হয়, আর পুরোনো এন্ট্রি বের হলে d > dist[u] দেখে বাদ দেওয়া হয় — Python-এ Dijkstra-র এটাই প্রচলিত রূপ। শর্ত মনে রাখুন: কোনো ওজন ঋণাত্মক হলে Dijkstra ভুল উত্তর দিতে পারে। বিস্তারিত গ্রাফ অ্যালগরিদম ABCL TECH-এর ডেটা স্ট্রাকচার ও অ্যালগরিদম কোর্সে.

sieve: সব মৌলিক সংখ্যা একসাথে

১০^6 পর্যন্ত বহু সংখ্যা মৌলিক কি না জানতে হলে প্রতিটি আলাদাভাবে পরীক্ষা না করে sieve of Eratosthenes: ২ থেকে শুরু করে প্রতিটি মৌলিক সংখ্যার সব গুণিতক কেটে দিন; যা বাকি থাকে, সবই মৌলিক। Python-এ গুণিতক কাটার লুপটি slice-assignment দিয়ে C-তে ঠেলে দেওয়া যায়:

from math import isqrt

def primes_upto(n):
    is_prime = [True] * (n + 1)
    is_prime[0] = is_prime[1] = False
    for p in range(2, isqrt(n) + 1):
        if is_prime[p]:
            is_prime[p * p :: p] = [False] * len(range(p * p, n + 1, p))   # সব গুণিতক একবারে
    return [i for i, ok in enumerate(is_prime) if ok]

p × p থেকে শুরু, কারণ তার ছোট গুণিতকগুলো আগের মৌলিক সংখ্যারা ইতিমধ্যে কেটেছে। খরচ প্রায় O(n log log n) — কার্যত রৈখিক। ভেতরের লাইনটি Python-এর নিজস্ব কৌশল: হাতে লেখা for লুপের বদলে একটি slice-এ একসাথে False বসানো, যা অনেক দ্রুত।

৮ · কনটেস্টে ডিবাগিং, প্ল্যাটফর্ম, PyPy আর নৈতিকতা

stress testing — নিজের সমাধানের বিরুদ্ধে প্রমাণ খোঁজা

জাজ বলল Wrong Answer, কিন্তু কোন ইনপুটে — বলে না। নমুনাগুলোয় সব ঠিক। এখানে সবচেয়ে কার্যকর কৌশল stress testing: (১) একটি brute force সমাধান লিখুন — ধীর, কিন্তু এত সরল যে ভুলের সুযোগ কম; (২) একটি নির্দিষ্ট seed দিয়ে হাজারো এলোমেলো ছোট ইনপুট বানান; (৩) দুটি সমাধান চালিয়ে উত্তর মেলান; প্রথম অমিলেই থামুন — সেই ছোট ইনপুটটিই আপনার বাগের ঠিকানা। ছোট ইনপুট কেন? কারণ পাঁচটি সংখ্যার ভুল ইনপুট হাতে কাগজে মেলানো যায়, পাঁচ হাজারের নয়।

অংশ ৫-এর কয়েন প্রশ্নটিই দেখুন। greedy সমাধানটি দ্রুত, আর বাংলাদেশের নোটে সঠিক উত্তরও দেয় — তাই মনে হয় এটাই সমাধান। নিশ্চিত সমাধান হিসেবে bottom-up DP রাখা হলো, আর দুটিকে এলোমেলো নোট-সেটে মেলানো হলো:

import random
def greedy(coins, amount):                  # দ্রুত: সবচেয়ে বড় নোট আগে
    count = 0
    for c in sorted(coins, reverse=True):
        count, amount = count + amount // c, amount % c
    return count
def dp(coins, amount):                      # নিশ্চিত: bottom-up টেবিল
    best = [0] + [amount + 1] * amount      # best[x] = x টাকায় সবচেয়ে কম নোট
    for x in range(1, amount + 1):
        for c in coins:
            if c <= x:
                best[x] = min(best[x], best[x - c] + 1)
    return best[amount]

notes = [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000]
print("1880 টাকা:", greedy(notes, 1880), dp(notes, 1880))
random.seed(45)
for t in range(1, 1001):                    # stress test: এলোমেলো ছোট টেস্ট
    coins, amount = [1, *random.sample(range(2, 13), 2)], random.randint(1, 30)
    if greedy(coins, amount) != dp(coins, amount):
        print(f"টেস্ট {t}: নোট {sorted(coins)}, {amount} টাকা -> greedy {greedy(coins, amount)}, dp {dp(coins, amount)}")
        break

আউটপুট দুই লাইন: 1880 টাকা: 7 7 আর টেস্ট 12: নোট [1, 6, 7], 12 টাকা -> greedy 6, dp 2. প্রথম লাইনে বাংলাদেশের নোটে দুটি সমাধান একমত — ১০০০ + ৫০০ + ২০০ + ১০০ + ৫০ + ২০ + ১০, মোট ৭টি নোট। শুধু এটুকু দেখে greedy জমা দিলে গোপন টেস্টে Wrong Answer আসত। দ্বিতীয় লাইনে stress test মাত্র ১২তম এলোমেলো টেস্টেই একটি ছোট, হাতে মেলানো যায় এমন প্রতি-উদাহরণ খুঁজে দিয়েছে: নোট ১, ৬, ৭ আর ১২ টাকা — greedy ৭ + ১ + ১ + ১ + ১ + ১ = ৬টি নোট নেয়, DP দেখায় ৬ + ৬ = ২টিই যথেষ্ট। random.seed(45) থাকায় প্রতিবার চালালে ঠিক একই টেস্ট আর একই ফল আসে — বাগ খুঁজতে পুনরুৎপাদনযোগ্যতা জরুরি। [amount + 1] * amount একটি “অসীম” মান: কোনো উত্তর কখনো amount-এর বেশি নোট লাগে না, কারণ ১ টাকার নোট আছে। DP-র খরচ O(amount × নোটের সংখ্যা)।

কনটেস্টে Wrong Answer-এর আরও কিছু চেনা কারণ, জমা দেওয়ার আগে একবার দেখুন: n = 1 বা খালি ইনপুটের মতো edge case; একাধিক test case-এ আগের টেস্টের ভ্যারিয়েবল পরিষ্কার না করা; readline-এর newline (.strip()); / দিয়ে ভাগ করে float পাওয়া যেখানে // দরকার ছিল; আর আউটপুটের বিন্যাস — “YES” না “Yes”.

প্ল্যাটফর্ম আর PyPy

শুরু করার জন্য পরিচিত কয়েকটি competitive programming প্ল্যাটফর্ম:

🏁 Codeforces

নিয়মিত রেটেড কনটেস্ট, বিশাল সমস্যা-সংগ্রহ, প্রতিটি কনটেস্টের পরে editorial. ভাষার তালিকায় CPython আর PyPy দুটিই থাকে।

নিয়মিত কনটেস্ট

🎌 AtCoder

জাপানের প্ল্যাটফর্ম; সাপ্তাহিক Beginner Contest নতুনদের জন্য চমৎকার, সমস্যার বিবৃতি পরিষ্কার। Python ব্যবহারকারীদের মধ্যে জনপ্রিয়।

নতুনদের জন্য ভালো

💼 LeetCode

ইন্টারভিউ-ধাঁচের সমস্যা: একটি ফাংশন লিখতে হয়, ইনপুট পড়তে হয় না। চাকরির কোডিং ইন্টারভিউর প্রস্তুতিতে বহুল ব্যবহৃত (পর্ব ৫৫)।

ইন্টারভিউ প্রস্তুতি

🍳 CodeChef

মাসিক ও সাপ্তাহিক কনটেস্ট, বিভিন্ন স্তরের সমস্যা; দক্ষিণ এশিয়ার শিক্ষার্থীদের মধ্যে পরিচিত।

নিয়মিত অনুশীলন

পর্ব ৪৩-এ দেখা PyPy কনটেস্টে খুব কাজের। এর JIT গরম লুপকে চলার সময় machine code-এ রূপান্তর করে, তাই বিশুদ্ধ-Python লুপ-ভারী সমাধান প্রায়ই CPython-এর চেয়ে কয়েক গুণ দ্রুত চলে। অনেক জাজে (যেমন Codeforces, AtCoder) ভাষা বেছে নেওয়ার সময় PyPy থাকে; আবার কিছু প্ল্যাটফর্মে শুধু CPython — প্রতিটি প্ল্যাটফর্মে ভাষার তালিকা নিজে দেখে নিন। কয়েকটি সতর্কতা: PyPy-তে গভীর recursion তুলনামূলক ধীর আর বেশি মেমরি নেয়; খুব ছোট ইনপুটে JIT “গরম” হওয়ার আগেই প্রোগ্রাম শেষ, লাভ কম; আর PyPy-র Python version সাধারণত সর্বশেষ CPython-এর চেয়ে পিছিয়ে থাকে, তাই একদম নতুন syntax সেখানে না-ও চলতে পারে। নিয়মটি সহজ: একই কোড দুটিতেই চলে, তাই TLE এলে আগে complexity দেখুন, তারপর PyPy-তে জমা দিয়ে দেখুন।

কনটেস্টের নৈতিকতা

  • অন্যের কোড নিজের বলে জমা নয়: চলমান কনটেস্টে সমাধান শেয়ার করা বা অন্যের সমাধান জমা দেওয়া প্রতারণা। প্ল্যাটফর্মগুলো কোডের মিল খুঁজে বের করে, আর এর শাস্তি ফল বাতিল থেকে অ্যাকাউন্ট বন্ধ পর্যন্ত।
  • প্রতিটি কনটেস্টের AI-নিয়ম পড়ুন ও মানুন: অনেক কনটেস্ট এখন চলাকালীন AI দিয়ে সমাধান লেখা বা সমস্যা বোঝানো নিষিদ্ধ করেছে; কোথাও নিয়ম আলাদা। নিয়ম না জানলে ধরে নিন নিষিদ্ধ।
  • কনটেস্টের পরে শিখুন: না পারা সমস্যার editorial পড়া, অন্যদের সমাধান দেখা — কনটেস্ট শেষ হওয়ার পরে এটাই উন্নতির সবচেয়ে বড় পথ।
C/C++ জানলে

ছাঁচগুলো হুবহু এক — prefix sum, two pointers, BFS, DP, Dijkstra; C++-এর lower_bound মানে Python-এর bisect_left, priority_queue মানে heapq (তবে heapq min-heap, C++-এর ডিফল্ট max-heap). বড় পার্থক্য তিনটি: Python-এ overflow নেই, কিন্তু লুপ কয়েক দশ গুণ ধীর, আর ইনপুট পড়ার দ্রুত পদ্ধতি আলাদা (ios::sync_with_stdio(false)-এর বদলে sys.stdin). C++-এর দিক থেকে পুরো ছবি দেখতে C++-এর কনটেস্ট প্রোগ্রামিং পর্ব.

পরীক্ষা ও ইন্টারভিউতে
  • Python-এ list-এর pop(0) আর deque-এর popleft()-এর খরচ? — list-এ O(n), কারণ বাকি সব উপাদান সরে; deque-এ O(1). তাই BFS-এর queue হিসেবে deque.
  • BFS আর DFS-এর পার্থক্য? — BFS queue দিয়ে স্তরে স্তরে খোঁজে, ওজনহীন গ্রাফে সবচেয়ে ছোট পথ দেয়; DFS stack বা recursion দিয়ে গভীরে আগে যায়।
  • Python-এ গভীর recursion-এ কী সমস্যা, সমাধান কী? — ডিফল্ট সীমা প্রায় ১০০০, ছাড়ালে RecursionError; sys.setrecursionlimit বাড়ানো যায়, কিন্তু নিরাপদ পথ নিজের stack দিয়ে iterative লেখা।
  • memoization আর bottom-up DP-র পার্থক্য? — memoization recursion-এর ফল cache করে (ওপর থেকে নিচে); bottom-up ছোট থেকে বড় ক্রমে টেবিল ভরে, recursion ছাড়া।
  • greedy সবসময় কাজ করে না — একটি উদাহরণ? — নোট ১, ৬, ৭ দিয়ে ১২ টাকা: greedy দেয় ৬টি নোট, সঠিক উত্তর ২টি (৬ + ৬)।

৯ · AI-এর যুগে — নমুনায় পাস, বড় ইনপুটে TLE

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

AI প্রায়ই যা করেফলকী যাচাই করবেন
দুই লুপের সরল সমাধান, constraints উপেক্ষা করেনমুনায় পাস, n = 2·10^5-এ TLEcomplexity লিখিয়ে নিন, নিজে constraints-এর সাথে মেলান
লুপের ভেতরে x in list, list.pop(0), min(...)দেখতে O(n), আসলে O(n²)অংশ ২-এর খরচের টেবিল ধরে প্রতিটি লাইন দেখুন
সুন্দর recursive DFS বা DPগভীর ইনপুটে RecursionError বা crashসর্বোচ্চ গভীরতা কত? iterative বা bottom-up রূপ চান
প্রতি লাইনে input() আর প্রতি উত্তরে printঅ্যালগরিদম ঠিক, তবু TLEsys.stdin আর একবারে join
বিশ্বাসযোগ্য greedy, কোনো প্রমাণ ছাড়াকিছু গোপন টেস্টে Wrong Answerbrute force-এর সাথে stress test
কোনো নামকরা সমস্যার সমাধান “মনে করে” লেখাপ্রায় একই কিন্তু আপনার সমস্যার শর্তে ভুলনিজের সমস্যার প্রতিটি শর্ত কোডে মিলিয়ে দেখুন

AI-এর সবচেয়ে ভালো ব্যবহার এই বিষয়ে শেখায়, প্রতিযোগিতায় নয়। কনটেস্ট শেষ হওয়ার পরে যে সমস্যা পারেননি, তার editorial-এর কোনো ধাপ না বুঝলে AI-কে ব্যাখ্যা করতে বলুন, ছোট উদাহরণে হাতে চালিয়ে দেখাতে বলুন, নিজের ভুল সমাধান কেন ভুল তা জিজ্ঞেস করুন। কিন্তু যে কনটেস্টের নিয়মে AI নিষিদ্ধ, সেখানে চলাকালীন কখনো নয় — সেটা অন্য প্রতিযোগীদের প্রতি অন্যায়, আর আপনার নিজের শেখারও ক্ষতি।

কনটেস্টের পরে একটি editorial বুঝতে AI-কে এভাবে বলুন — উত্তর নয়, বোঝাপড়া চাইছেন:

আমি Python দিয়ে কনটেস্ট প্রোগ্রামিং শিখছি। একটি কনটেস্ট শেষ হয়েছে; নিচে একটি সমস্যা, তার constraints, editorial-এর একটি অংশ আর আমার নিজের সমাধান (যেটা TLE পেয়েছে) দিলাম। শর্ত: ১. আগে constraints থেকে হিসাব করে বলো কোন time complexity দরকার, আর আমার সমাধানের complexity কত — কোন লাইনের কারণে। ২. editorial-এর মূল ভাবনাটি সহজ ভাষায় বুঝিয়ে দাও, তারপর ৫-৬টি সংখ্যার একটি ছোট উদাহরণে ধাপে ধাপে হাতে চালিয়ে দেখাও। ৩. পুরো সমাধানের কোড এখনই দিও না; আমি নিজে লিখব। শুধু কোন Python হাতিয়ার (bisect, heapq, deque, prefix sum ইত্যাদি) কাজে লাগবে, তা বলো। ৪. Python-এ কোথায় TLE বা RecursionError-এর ঝুঁকি আছে (ইনপুট পড়া, recursion-এর গভীরতা, লুপের ভেতরে O(n) কাজ), তা আলাদা করে দেখাও। ৫. আমার সমাধান যাচাইয়ের জন্য একটি brute force আর seed-সহ একটি stress test-এর কাঠামো দাও। [এখানে সমস্যা, constraints, editorial-এর অংশ আর আমার কোড]

AI-এর অ্যালগরিদম-কোড যাচাইয়ের চেকলিস্ট

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

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

প্র ০১কনটেস্টে Python যদি C++-এর চেয়ে এত ধীর হয়, তবু এত মানুষ Python দিয়ে কনটেস্ট করেন কেন? PyPy-র JIT এই হিসাব কতটা বদলায়?

প্রথমে ধীরতার কারণটা পরিষ্কার করি। C++ কোড আগেই machine code-এ compile হয়; একটি int যোগ মানে CPU-র একটি নির্দেশ। CPython-এ একই যোগের জন্য interpreter একটি bytecode নির্দেশ পড়ে, দুটি object-এর type দেখে, ফলাফলের জন্য একটি নতুন int object বানায় (ছোট সংখ্যা ছাড়া), reference গোনে। প্রতিটি ধাপ ছোট, কিন্তু মিলিয়ে সরল লুপে C++-এর চেয়ে সাধারণত কয়েক দশ গুণ ধীর। কনটেস্টের সময়সীমা অনেক সময় C++-এর প্রত্যাশিত সমাধান ধরে ঠিক করা হয়, ফলে কিছু কঠিন সমস্যা Python-এ সময়ে আঁটানো সত্যিই কঠিন বা অসম্ভব।

তবু Python জনপ্রিয় কয়েকটি বাস্তব কারণে। এক, লেখার গতি: কনটেস্টে সময় শুধু CPU-র নয়, আপনারও। Python-এ একটি সমাধান প্রায়ই অর্ধেক লাইনে লেখা যায়, আর overflow, মেমরি বা টাইপের ঝামেলা নেই। সহজ থেকে মাঝারি সমস্যায়, যেখানে সময়সীমা ঢিলেঢালা, এটা বড় সুবিধা। দুই, বড় সংখ্যা: কিছু সমস্যায় C++-এ নিজে বড় সংখ্যার গাণিতিক কাজ লিখতে হয়, Python-এ তা বিনামূল্যে। তিন, শেখা আর ইন্টারভিউ: অনেকের কাছে অ্যালগরিদম শেখার লক্ষ্য রেটিং নয়, ভাবনার দক্ষতা আর কোডিং ইন্টারভিউ — যেখানে Python বহুল ব্যবহৃত, কারণ কোড পড়তে প্রায় pseudocode-এর মতো। চার, বিল্ট-ইন: sort, heap, Counter, বড় অংশ C-তে চলে, তাই ভালোভাবে লেখা Python সমাধান সরল লুপের হিসাবের চেয়ে দ্রুত হয়।

এখানে আসে PyPy. এটি Python-এর একটি বিকল্প implementation, যার মূল শক্তি একটি tracing JIT: চলার সময় দেখে কোন লুপ বারবার চলছে, সেই লুপে বাস্তবে কোন type আসছে, তারপর শুধু সেই পথের জন্য machine code বানায়। ফলে অস্থায়ী int object প্রায়ই তৈরিই হয় না, type যাচাই একবারে নেমে আসে। লুপ-ভারী কনটেস্ট সমাধানে এর লাভ প্রায়ই কয়েক গুণ — কখনো CPython-এ TLE পাওয়া সমাধান PyPy-তে আরামে পাস করে। Codeforces বা AtCoder-এর মতো জাজে PyPy থাকায় অনেক Python প্রতিযোগী সাধারণত PyPy-তেই জমা দেন।

তবে PyPy জাদু নয়। O(n²) সমাধান PyPy-তেও O(n²); কয়েক গুণ লাভ হাজার গুণের ফাঁক পূরণ করে না। গভীর recursion PyPy-তে তুলনামূলক দুর্বল, আর কিছু ক্ষেত্রে (যেমন খুব বড় int-এর হিসাব বা কিছু string কাজ) CPython সমান বা বেশি দ্রুত হতে পারে। তাই অভিজ্ঞরা দুটোই চেনেন, আর মেপে বেছে নেন। এটাও মনে রাখার মতো যে C++-এর প্রতিযোগীদের জন্যও নিয়ম একই: ভুল complexity কোনো ভাষাকেই বাঁচায় না।

মূল উপলব্ধি: Python-এর ধীরতা একটি বাস্তব সীমা, কিন্তু অ্যালগরিদম শেখার মূল দক্ষতা — constraints পড়া, ঠিক complexity বেছে নেওয়া, চেনা ছাঁচ চেনা — ভাষার ওপর নির্ভর করে না। PyPy সেই সীমাকে অনেকটা ঠেলে দেয়, আর ঠিক complexity-ই থাকে আসল শর্ত।

প্র ০২Python-এর sorted()-এর ভেতরে Timsort নামের একটি অ্যালগরিদম। এটি কে, কেন বানালেন, আর বাস্তবের ডেটায় এটি এত দ্রুত কেন?

২০০২ সালে Python-এর পুরোনো core developer Tim Peters Python 2.3-এর জন্য একটি নতুন sort লেখেন, যা পরে তাঁর নামেই Timsort নামে পরিচিত হয়। তাঁর পর্যবেক্ষণ ছিল খুব ব্যবহারিক: পাঠ্যবইয়ের sort অ্যালগরিদমগুলো ধরে নেয় ডেটা সম্পূর্ণ এলোমেলো। কিন্তু বাস্তবের ডেটা প্রায়ই আংশিক সাজানো থাকে — একটি সাজানো তালিকার শেষে কয়েকটি নতুন উপাদান যোগ হয়েছে, দুটি সাজানো তালিকা জোড়া লাগানো হয়েছে, অথবা ডেটা উল্টো ক্রমে আছে। এই বিদ্যমান শৃঙ্খলা কাজে লাগানো গেলে অনেক কাজ বাঁচে।

Timsort একটি সংকর (hybrid) অ্যালগরিদম, merge sort আর insertion sort মিলিয়ে। প্রথমে এটি ডেটায় আগে থেকেই সাজানো টানা অংশ খোঁজে, যাদের বলে “run”; উল্টো ক্রমের run পেলে উল্টে নেয়। run খুব ছোট হলে insertion sort দিয়ে একটি ন্যূনতম দৈর্ঘ্যে বাড়ায় — ছোট অংশে insertion sort সরল আর cache-বান্ধব বলে দ্রুত। তারপর run-গুলোকে একটি stack-এ রেখে নির্দিষ্ট নিয়মে জোড়ায় জোড়ায় merge করে, যাতে merge-গুলো ভারসাম্যপূর্ণ থাকে। merge-এর সময় “galloping” নামের একটি কৌশল আছে: একটি run থেকে টানা অনেক উপাদান জিততে থাকলে সে একটি একটি করে না দেখে লাফিয়ে লাফিয়ে (exponential search) খোঁজে।

ফল: সবচেয়ে খারাপ ক্ষেত্রে O(n log n), কিন্তু আগে থেকে সাজানো বা প্রায় সাজানো ডেটায় প্রায় O(n). আর এটি stable — সমান key-র উপাদানের মূল ক্রম বজায় থাকে। এই stability-র কারণেই Python-এ একাধিক নিয়মে সাজানো যায় দুই ধাপে: আগে দ্বিতীয় নিয়মে, তারপর প্রথম নিয়মে sort. Python-এর অফিসিয়াল Sorting HOWTO-তে এই কৌশল বর্ণিত আছে।

Timsort এত সফল হয় যে Python-এর বাইরেও ছড়িয়ে পড়ে: Java 7 থেকে object-এর array sort করতে Timsort-ভিত্তিক অ্যালগরিদম ব্যবহার হয়, Android-এও, আর V8 (Chrome-এর JavaScript ইঞ্জিন) পরে তার Array.prototype.sort-এ Timsort নেয়। একটি মজার ঘটনা: ২০১৫ সালে গবেষকেরা formal verification-এর টুল দিয়ে Timsort-এর merge-নিয়মে একটি সূক্ষ্ম বাগ খুঁজে পান, যা খুব বড় ও বিশেষ ধরনের ইনপুটে run-stack-এর ধারণা ভাঙতে পারত; Python আর Java দুটিতেই এটি ঠিক করা হয়। পরে Python 3.11-এ run মেলানোর নিয়মটি “powersort”-এর পদ্ধতিতে বদলানো হয়, যা merge-এর ক্রম আরও ভালোভাবে ঠিক করে।

কনটেস্টের দিক থেকে শিক্ষা: Python-এ নিজে sort লেখার কোনো কারণ নেই — sorted C-তে লেখা, বাস্তব ডেটার জন্য টিউন করা, আর key দিয়ে প্রায় যেকোনো ক্রম প্রকাশ করা যায়। sort করে তারপর two pointers বা bisect — এটাই Python-এ O(n log n) সমাধানের সবচেয়ে চেনা রূপ।

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

প্র ০৩Dijkstra-র shortest path অ্যালগরিদম ১৯৫৯ সালে প্রকাশিত। এটি কীভাবে জন্মাল, আর আজকের ম্যাপ-অ্যাপের পথ খোঁজার সাথে এর সম্পর্ক কী?

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

উদ্দেশ্য ছিল ARMAC নামের একটি নতুন কম্পিউটারের ক্ষমতা দেখানো, এমন একটি সমস্যা দিয়ে যা কম্পিউটার-না-জানা মানুষও বোঝেন: নেদারল্যান্ডসের কয়েকটি শহরের মধ্যে সবচেয়ে ছোট পথ। অ্যালগরিদমটি প্রকাশিত হয় ১৯৫৯ সালে Numerische Mathematik পত্রিকায়, মাত্র তিন পৃষ্ঠার একটি প্রবন্ধে: “A note on two problems in connexion with graphs”. দুটি সমস্যার একটি shortest path, অন্যটি minimum spanning tree.

মূল ভাবনাটি অংশ ৭-এর কোডেই আছে: উৎস থেকে যে বিন্দুর জানা দূরত্ব এখন সবচেয়ে কম, তার দূরত্ব আর কখনো কমবে না — কারণ অন্য যেকোনো পথ আরও বড় দূরত্বের কোনো বিন্দু ঘুরে আসবে, আর ওজন ঋণাত্মক নয় বলে তাতে দূরত্ব শুধু বাড়ে। তাই সেটিকে চূড়ান্ত করে তার প্রতিবেশীদের দূরত্ব হালনাগাদ করা যায়। এই যুক্তি থেকেই শর্তটিও বোঝা যায়: ঋণাত্মক ওজন থাকলে “আর কমবে না” দাবিটি ভেঙে যায়, তখন Bellman–Ford-এর মতো অন্য অ্যালগরিদম লাগে। Dijkstra-র মূল রূপে সবচেয়ে কম দূরত্বের বিন্দু খুঁজতে প্রতিবার সব বিন্দু দেখা হতো (O(V²)); পরে heap-ভিত্তিক priority queue যোগ হয়ে আজকের O((V + E) log V) রূপটি প্রচলিত হয় — আমাদের heapq-এর সংস্করণ সেটিই।

আজকের ম্যাপ-অ্যাপ বা রাইড-শেয়ারিং অ্যাপ যখন ঢাকার রাস্তায় পথ দেখায়, তার ভেতরের ভাবনার শিকড় এখানেই, যদিও কোটি কোটি রাস্তার মহাদেশীয় মানচিত্রে সরল Dijkstra যথেষ্ট দ্রুত নয়। তাই সেখানে ব্যবহার হয় এর উন্নত বংশধর: A* (লক্ষ্যের দিকে একটি অনুমান দিয়ে খোঁজাকে দিক দেখানো), আর আগে থেকে মানচিত্র প্রক্রিয়া করে রাখার নানা কৌশল। ইন্টারনেটের কিছু routing protocol-ও (যেমন OSPF) shortest path হিসাবে Dijkstra-ধাঁচের অ্যালগরিদম ব্যবহার করে।

Dijkstra পরে structured programming, concurrency (semaphore) আর প্রোগ্রামের সঠিকতা প্রমাণে মৌলিক অবদান রাখেন এবং ১৯৭২ সালে Turing Award পান। কিন্তু তাঁর সবচেয়ে বেশি ব্যবহৃত কাজটি সম্ভবত বিশ মিনিটের এই ক্যাফে-ভাবনাই।

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

প্র ০৪“Dynamic programming” — নামটিতে “dynamic” বা “programming” কোনোটিই আজকের অর্থে নেই। নামটি কোথা থেকে এল?

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

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

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

আরেকটি মজার যোগসূত্র: Bellman-এর নাম আরেকটি অ্যালগরিদমেও আছে — Bellman–Ford, যা ঋণাত্মক ওজনের গ্রাফেও shortest path দেয় (প্রশ্ন ০৩-এ যার উল্লেখ আছে)। সেটিও আসলে একটি DP: “সর্বোচ্চ k-টি রাস্তা ব্যবহার করে প্রতিটি বিন্দুর সবচেয়ে ছোট দূরত্ব” — k বাড়িয়ে বাড়িয়ে টেবিল ভরা।

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

প্র ০৫ICPC আর IOI কী? বাংলাদেশে প্রোগ্রামিং কনটেস্টের সংস্কৃতি কীভাবে গড়ে উঠেছে, আর কনটেস্ট থেকে আসল লাভটা কী?

ICPC (International Collegiate Programming Contest) বিশ্ববিদ্যালয়ের শিক্ষার্থীদের সবচেয়ে পুরোনো ও মর্যাদাপূর্ণ প্রোগ্রামিং প্রতিযোগিতা। এর শিকড় ১৯৭০ সালে যুক্তরাষ্ট্রের Texas A&M বিশ্ববিদ্যালয়ের একটি প্রতিযোগিতায়, আর প্রথম World Finals হয় ১৯৭৭ সালে। বহু বছর এটি “ACM ICPC” নামে পরিচিত ছিল। এর বৈশিষ্ট্য দলগত খেলা: তিনজনের একটি দল, কিন্তু কম্পিউটার একটি। তাই কে কখন কোড লিখবে, কে কাগজে পরের সমস্যা ভাববে, কে অন্যের কোড যাচাই করবে — এই সমন্বয়ই প্রতিযোগিতার বড় অংশ। আঞ্চলিক পর্ব (regional) থেকে বাছাই হয়ে দলগুলো World Finals-এ যায়।

IOI (International Olympiad in Informatics) স্কুল-কলেজ পর্যায়ের শিক্ষার্থীদের জন্য, গণিত অলিম্পিয়াডের মতো। প্রথম IOI হয় ১৯৮৯ সালে বুলগেরিয়ায়। প্রতিটি দেশ জাতীয় পর্যায়ের প্রতিযোগিতা থেকে কয়েকজনকে বাছাই করে পাঠায়, আর এখানে প্রতিযোগিতা ব্যক্তিগত। বাংলাদেশে জাতীয় পর্যায়ে ইনফরমেটিক্স অলিম্পিয়াড আয়োজিত হয়, যেখান থেকে IOI-এর দল বাছাই হয়।

বাংলাদেশের বিশ্ববিদ্যালয়গুলো বহু বছর ধরে ICPC-র আঞ্চলিক ও বিশ্ব পর্যায়ে নিয়মিত অংশ নিচ্ছে, আর দেশে আঞ্চলিক পর্বও আয়োজিত হয়েছে। এই সংস্কৃতির পেছনে বড় ভূমিকা রেখেছেন অগ্রজ প্রতিযোগী আর শিক্ষকেরা — যাঁরা ক্যাম্প করেছেন, বাংলায় অ্যালগরিদমের বই ও ব্লগ লিখেছেন, অনলাইন judge-এর জন্য সমস্যা বানিয়েছেন, আর বিশ্ববিদ্যালয়ে প্রোগ্রামিং ক্লাব গড়েছেন। আজ দেশের বহু বিশ্ববিদ্যালয়ে নিয়মিত intra-university কনটেস্ট হয়, আর স্কুলের শিক্ষার্থীরাও অলিম্পিয়াডের মাধ্যমে শুরু করছেন। কনটেস্টে অভিজ্ঞ অনেক বাংলাদেশি দেশে-বিদেশে বড় প্রযুক্তি প্রতিষ্ঠানে কাজ করছেন — এটা অনুপ্রেরণা, তবে কনটেস্টই একমাত্র পথ নয়।

লক্ষ করুন, ICPC আর IOI-তে ঐতিহ্যগতভাবে C++ প্রধান ভাষা; IOI-তে বহু বছর ধরে মূলত C++ (কিছু সময় অন্য ভাষাও) সমর্থিত হয়েছে, আর ICPC-তে Python যোগ হয়েছে তুলনামূলক পরে। তাই এসব প্রতিযোগিতায় গুরুতরভাবে অংশ নিতে চাইলে একসময় C++ শিখে নেওয়া বুদ্ধিমানের কাজ — ভালো খবর হলো, এই পর্বে শেখা ছাঁচগুলো হুবহু সেখানে চলে যায়।

কনটেস্টের আসল লাভ ট্রফি নয়। চাপের মধ্যে একটি অচেনা সমস্যা ভাঙা, constraints থেকে পদ্ধতি বের করা, নিজের কোডের প্রতিটি edge case ভাবা, ভুল হলে দ্রুত খুঁজে বের করা — এই দক্ষতাগুলো সাধারণ সফটওয়্যার কাজেও লাগে, আর কোডিং ইন্টারভিউতে সরাসরি পরীক্ষা হয়। তবে উল্টো সতর্কতাও আছে: কনটেস্টের কোড লেখা হয় একবার চালানোর জন্য — এক-অক্ষরের নাম, কোনো test নেই। বাস্তব প্রজেক্টে দরকার পর্ব ৪৪-এর পরিষ্কার কোড আর পর্ব ৩৯-এর test. দুটি দক্ষতা আলাদা, দুটিই মূল্যবান।

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

অনুশীলন

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

  1. constraints থেকে সিদ্ধান্ত: তিনটি সমস্যার সীমা — (ক) n ≤ 18, (খ) n ≤ 2000, (গ) n ≤ 10^6. প্রতিটির জন্য কোন complexity পর্যন্ত Python-এ চলবে বলে মনে করেন, আর কোন ধরনের কৌশল মাথায় আসে?

    (ক) n ≤ 18: 2^18 প্রায় ২.৬ লাখ, তাই O(2^n) বা এমনকি O(n · 2^n) (প্রায় ৪৭ লাখ) চলবে — সব উপসেট পরীক্ষা, bitmask DP, backtracking. সমস্যা-লেখক n এত ছোট রেখেছেন মানেই প্রায় নিশ্চিতভাবে সূচকীয় কিছু প্রত্যাশিত। (খ) n ≤ 2000: n² = ৪০ লাখ, Python-এ আরামে; O(n² log n) প্রায় সাড়ে চার কোটি — ঝুঁকিপূর্ণ। দুই লুপ বা n × n-এর DP টেবিল। (গ) n ≤ 10^6: শুধু O(n) বা খুব হালকা O(n log n). আর এখানে Python-এ শুধু অ্যালগরিদম যথেষ্ট নয় — ইনপুট পড়তে হবে sys.stdin.buffer.read().split() দিয়ে, লুপ যতটা সম্ভব বিল্ট-ইনে (accumulate, sorted, slice), আর সম্ভব হলে PyPy-তে জমা।

  2. লুকানো খরচ খুঁজুন: একটি সমাধানে লুপের ভেতরে এই লাইনগুলো আছে — if x not in visited_list:, queue.pop(0), best = max(scores), ans += str(v) + " ". n = 10^5 হলে প্রতিটি কেন সমস্যা, আর কী দিয়ে বদলাবেন?

    চারটিই দেখতে এক ধাপ, আসলে O(n) — লুপের ভেতরে মোট O(n²), অর্থাৎ প্রায় ১০^10. (১) not in visited_list → visited একটি set বানান, খোঁজ গড়ে O(1). (২) queue.pop(0) → collections.deque আর popleft(), O(1). (৩) লুপের ভেতরে max(scores) → সর্বোচ্চটি একটি ভ্যারিয়েবলে ধরে রাখুন আর প্রতিটি নতুন মানে হালনাগাদ করুন; বারবার সবচেয়ে বড়/ছোট চাই আর মান বদলায়, তাহলে heapq. (৪) string-এ বারবার += → উত্তরগুলো একটি list-এ রাখুন, শেষে " ".join(map(str, answers)). চারটি বদলের পর পুরো সমাধান O(n) বা O(n log n).

  3. হাতে BFS চালান: এই গ্রিডে (S শুরু, E শেষ, # দেয়াল) অংশ ৬-এর BFS হাতে চালান, প্রতিটি ঘরে দূরত্ব লিখুন। E-তে সবচেয়ে কম কয় ধাপ? দেয়ালের অবস্থান বদলে কি এমন গ্রিড বানানো যায় যেখানে উত্তর -1?
    S . # .
    . . # E
    # . . .

    দূরত্বগুলো (সারি ধরে): প্রথম সারি 0 1 # ., দ্বিতীয় সারি 1 2 # ., তৃতীয় সারি # 3 4 5. তারপর (2,3) থেকে ওপরে E = (1,3), দূরত্ব ৬; আর (0,3)-এর দূরত্ব হবে ৭. তাই উত্তর ৬. লক্ষ করুন, (0,1) আর (1,0) দুটিই দূরত্ব ১ — দুটি ঘর একই ঢেউয়ে। উত্তর -1 পেতে E-কে ঘিরে ফেলুন: যেমন (2,3)-এ দেয়াল দিলে E-তে পৌঁছানোর পথ থাকে শুধু (0,3) দিয়ে, যা নিজেও শুধু E আর দেয়ালের পাশে — অর্থাৎ E আর (0,3) মিলে একটি বিচ্ছিন্ন অংশ। BFS তখন queue খালি হওয়া পর্যন্ত চলে, E কখনো dist-এ আসে না, ফাংশন -1 ফেরত দেয়।

  4. নিজের stress test লিখুন (কোড লিখতে হবে): অংশ ৪-এর prefix sum সমাধানের জন্য একটি brute force (প্রতিটি প্রশ্নে sum(sales[l-1:r])) লিখুন, তারপর random.seed দিয়ে ৫০০টি এলোমেলো ছোট টেস্টে দুটি মেলান। তারপর ইচ্ছা করে prefix সমাধানে একটি বাগ ঢোকান (যেমন prefix[l]) — stress test কি ধরে?

    একটি কাঠামো:

    import random
    from itertools import accumulate
    
    def fast(sales, queries):
        prefix = [0, *accumulate(sales)]
        return [prefix[r] - prefix[l - 1] for l, r in queries]
    
    def brute(sales, queries):
        return [sum(sales[l - 1:r]) for l, r in queries]
    
    random.seed(1)
    for t in range(500):
        n = random.randint(1, 8)
        sales = [random.randint(0, 20) for _ in range(n)]
        queries = [tuple(sorted(random.choices(range(1, n + 1), k=2))) for _ in range(5)]
        if fast(sales, queries) != brute(sales, queries):
            print("অমিল:", sales, queries)
            break
    else:
        print("৫০০টি টেস্টে সব মিলেছে")

    সঠিক সমাধানে শেষ লাইনটি ছাপা হবে। prefix[l - 1]-এর জায়গায় prefix[l] লিখলে প্রথম কয়েকটি টেস্টেই অমিল ধরা পড়বে, আর ছাপা ছোট ইনপুটটি হাতে মিলিয়ে বুঝতে পারবেন উত্তর ঠিক এক দিনের বিক্রি কম আসছে। লক্ষ করুন for ... else: break না হলে else চলে (পর্ব ১২)। n ছোট (১-৮) আর মান ছোট রাখা ইচ্ছাকৃত — অমিল পেলে হাতে মেলানো সহজ। একটি সতর্কতা: brute force-ও যেন সত্যিই সরল হয়; দুটিতে একই ভুল ধারণা থাকলে stress test সেটা ধরতে পারবে না।

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

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

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

  1. সমস্যা আর constraints মন দিয়ে পড়া
  2. constraints থেকে প্রয়োজনীয় complexity হিসাব করা
  3. মানানসই ছাঁচ বা অ্যালগরিদম বেছে নেওয়া
  4. দ্রুত ইনপুটসহ Python-এ সমাধান লেখা
  5. নমুনা, edge case আর দরকারে stress test দিয়ে যাচাই
  6. জমা দেওয়া (প্রয়োজনে PyPy বেছে নিয়ে)

একটি সমস্যায় n ≤ 2·10^5 আর সময়সীমা ২ সেকেন্ড। Python-এ কোন complexity-র সমাধান লক্ষ্য করা সবচেয়ে যুক্তিসংগত?

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

Python-এ BFS-এর queue হিসেবে সাধারণ list আর pop(0)-এর বদলে collections.deque আর popleft() ব্যবহার করা হয় কেন?

  • list-এ tuple রাখা যায় না
  • list-এর pop(0) O(n), deque-এর popleft() O(1)
  • deque উপাদানগুলো স্বয়ংক্রিয়ভাবে দূরত্ব অনুযায়ী সাজায়
  • deque একই ঘর দুবার ঢুকতে দেয় না

একটি recursive DFS সমাধান এক লাখ node-এর একটি লম্বা শিকলের মতো গাছে Python-এ RecursionError দিচ্ছে। সবচেয়ে নির্ভরযোগ্য সমাধান কোনটি?

  • RecursionError-কে try/except দিয়ে উপেক্ষা করা
  • @functools.cache যোগ করা
  • শুধু sys.setrecursionlimit(10**9) লেখা
  • নিজের stack (list-এর append/pop) দিয়ে DFS-কে iterative করা

Python-এ সাজানো list a = [10, 20, 20, 30]-এ bisect_left(a, 20) আর bisect_right(a, 20)-এর মান কত?

  • 1 আর 3
  • 2 আর 3
  • 1 আর 1
  • 2 আর 2

নোটের মান ১, ৬ আর ৭ হলে ১২ টাকা বানাতে “সবচেয়ে বড় নোট আগে” greedy নিয়ম আর সঠিক (সবচেয়ে কম নোটের) উত্তর যথাক্রমে কত নোট?

  • ২ আর ২
  • ৬ আর ৩
  • ৬ আর ২
  • ৫ আর ২

কনটেস্টে stress testing-এ এলোমেলো ইনপুট সাধারণত ছোট রাখা হয় আর random.seed দেওয়া হয় কেন?

  • ছোট ইনপুটে সব বাগ নিশ্চিতভাবে ধরা পড়ে
  • অমিলের ইনপুট হাতে মেলানো যায়, আর seed থাকলে একই টেস্ট আবার পাওয়া যায়
  • seed দিলে Python দ্রুত চলে
  • জাজ শুধু ছোট আর seed-সহ টেস্ট গ্রহণ করে

n দিনের বিক্রির একটি list-এ q টি প্রশ্ন, প্রতিটি “দিন l থেকে r-এর মোট বিক্রি কত?” prefix sum ব্যবহার করলে মোট খরচ কত?

  • O(n + q)
  • O(n · q)
  • O(q log n)
  • মোট O(1)

শব্দকোষ

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

মনে রাখুন

  1. constraints আগে, কোড পরে। n-এর সীমা থেকে complexity: n ≤ 10^5–2·10^5 মানে O(n log n) বা O(n). Python মোটামুটি প্রতি সেকেন্ডে ১০^7 সরল কাজ — হিসাবটা লেখার আগেই করুন।
  2. Python-এর কনটেস্ট-অভ্যাস: sys.stdin.readline বা sys.stdin.buffer.read().split(), উত্তর একবারে "\n".join; গভীর recursion-এর বদলে নিজের stack; বড় int বিনামূল্যে, কিন্তু mod প্রতি ধাপে।
  3. খরচ চিনুন: list-এ in, pop(0), insert(0) O(n); set/dict গড়ে O(1); sort O(n log n); heap O(log n); deque-এর দুই প্রান্ত O(1).
  4. চেনা ছাঁচ: prefix sum, two pointers/sliding window, bisect আর উত্তরের ওপর binary search, key দিয়ে sort, প্রমাণসহ greedy, dict/Counter, stack, deque-এ BFS, iterative DFS, @cache বা bottom-up DP, heapq-এ top-k ও Dijkstra, sieve.
  5. যাচাই আর সততা: brute force-এর সাথে seed-সহ stress test; TLE-তে আগে complexity, তারপর PyPy. কনটেস্টের AI-নিয়ম মানুন — AI কনটেস্টের পরে editorial বোঝার সঙ্গী।

পরের পর্বে: এই পর্বে বারবার দেখেছেন, Python-এ গতি আসে যখন লুপটা C-তে চলে। এই ভাবনার সবচেয়ে বড় রূপ হলো ডেটা সায়েন্সের প্রথম ধাপ — NumPy আর pandas: লাখো সংখ্যার ওপর এক লাইনের হিসাব, টেবিলের মতো ডেটা পড়া, ছাঁকা আর দলবদ্ধ করা।

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

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