সরাসরি পর্বে যান
পর্ব ৬৭ · ৭২-এর মধ্যে · মডিউল ৮

ক্যাপস্টোন — একটি কঠিন সমস্যার সমাধান শুরু থেকে শেষ

Capstone — One Hard Problem End to End: Constraints, Brute Force, Key Insight, Proof, Implementation & Stress Testing

Taught in Bangla, this lesson solves one hard problem about an electric delivery van and charging stations from start to finish. It reads the constraints, writes a correct but slow brute force, finds the key insight about the nearest station, chooses data structures, proves correctness, analyses time and memory, implements the solution in C++ and Python, and stress-tests it against the brute force.

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

  • সমস্যার সীমা (constraints) দেখে কোন complexity চলবে তা আগেই হিসাব করা
  • আগে একটি সঠিক কিন্তু ধীর brute force দাঁড় করানো, তারপর পর্যবেক্ষণ থেকে মূল ধারণায় পৌঁছানো
  • কোর্সের কয়েকটি কৌশল — Dijkstra, sorting, DSU, small-to-large, MST-এর ধর্ম — জোড়া দিয়ে একটি সমাধান বানানো আর তার শুদ্ধতা প্রমাণ করা
  • C++ আর Python-এ পরিষ্কার বাস্তবায়ন, আর brute-force checker দিয়ে হাজার টেস্টের stress test চালানো
  • প্রশ্ন online হলে বা গ্রাফ বদলালে সমাধান কীভাবে বদলায় — আর কোর্সের পুরো মানচিত্রে এরপর কোথায় যাবেন

১ · সমস্যাটি পড়া — ঢাকার ইলেকট্রিক ডেলিভারি ভ্যান আর তার সীমা

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

একটি কুরিয়ার কোম্পানি ঢাকায় ব্যাটারিচালিত ডেলিভারি ভ্যান চালায়। শহরে n টি এলাকা, আর m টি দুই-মুখী রাস্তা। রাস্তা i পার হতে ব্যাটারির wi একক চার্জ খরচ হয়। প্রথম k টি এলাকায় (১ থেকে k) চার্জিং স্টেশন আছে; সেখানে পৌঁছালে ব্যাটারি পুরো ভরে নেওয়া যায়। যাত্রার কোনো মুহূর্তে চার্জ শূন্যের নিচে নামতে পারবে না। কোম্পানি বিভিন্ন ধারণক্ষমতার (capacity) ব্যাটারি কিনতে পারে, তাই q টি প্রশ্ন আসে: “স্টেশন a থেকে পুরো চার্জ নিয়ে রওনা দিয়ে স্টেশন b-তে পৌঁছাতে ব্যাটারির ধারণক্ষমতা c অন্তত কত হতে হবে?”

অংশবিন্যাসসীমা
প্রথম লাইনn m k q2 ≤ n ≤ 105, n − 1 ≤ m ≤ 3 × 105, 1 ≤ k ≤ n, 1 ≤ q ≤ 3 × 105
পরের m লাইনu v w — একটি রাস্তা1 ≤ w ≤ 109; একই জোড়ায় একাধিক রাস্তা বা self-loop থাকতে পারে; পুরো গ্রাফ connected
পরের q লাইনa b — একটি প্রশ্ন1 ≤ a, b ≤ k (দুটিই স্টেশন), a = b-ও হতে পারে
আউটপুটপ্রতিটি প্রশ্নের জন্য একটি লাইনে ন্যূনতম cসময়সীমা ২ সেকেন্ড, মেমরি ২৫৬ MB

নমুনা হিসেবে ছয়টি এলাকা: মিরপুর (১), মতিঝিল (২) আর উত্তরা (৩) স্টেশন (বন্ধনীতে চিহ্নিত); ফার্মগেট (৪), তেজগাঁও (৫), গুলশান (৬) সাধারণ এলাকা। সংখ্যাগুলো চার্জ খরচ (বোঝার জন্য বানানো, আসল নয়)।

[Uttara]──────────── 20 ────────────[Mirpur]
    │                                 │   │
    6                                 4   │
    │                                 │   │
 Gulshan ──2── Tejgaon ──3──── Farmgate   12
                  │               │       │
                  2               5       │
                  │               │       │
                  └───────── [Motijheel]──┘
নমুনা গ্রাফ: ৬টি এলাকা, ৮টি রাস্তা, স্টেশন ৩টি। প্রশ্ন: (মিরপুর, মতিঝিল), (মিরপুর, উত্তরা), (মতিঝিল, উত্তরা), (উত্তরা, উত্তরা), (উত্তরা, মিরপুর). সঠিক উত্তর: 9, 10, 10, 0, 10.

প্রথম উত্তরটি দেখুন। মিরপুর থেকে মতিঝিলের সরাসরি রাস্তায় 12 লাগে, কিন্তু ফার্মগেট হয়ে 4 + 5 = 9. দ্বিতীয়টি আরও মজার: মিরপুর থেকে উত্তরার যেকোনো পথে মোট খরচ অন্তত 15, তবু উত্তর 10 — কারণ পথে মতিঝিলে একবার ঘুরে চার্জ নেওয়া যায়। এই “ঘুরে চার্জ নেওয়া”-ই সমস্যাটিকে কঠিন করেছে, আর এটিই শেষে সমাধানের চাবি হবে।

সীমা থেকে complexity-র বাজেট

অভিজ্ঞ সমাধানকারী কোড লেখার আগে constraints পড়েন, কারণ সীমাই বলে দেয় কোন অ্যালগরিদম চলবে। মোটা দাগের একটি নিয়ম: আধুনিক কম্পিউটারে C++ এক সেকেন্ডে মোটামুটি 108-এর কাছাকাছি সরল কাজ করে (ক্যাশ আর ধ্রুবকের ওপর নির্ভর করে কয়েক গুণ কম-বেশি)। Python সাধারণত এর কয়েক দশ ভাগের এক ভাগ। তাই time limit ১–২ সেকেন্ড হলে:

ইনপুটের আকারযে complexity চলেসাধারণ কৌশল
n ≤ 10–11O(n!)সব permutation, backtracking (পর্ব ৫৬)
n ≤ 20–25O(2n·n)bitmask DP, meet in the middle (পর্ব ৫৫)
n ≤ 400–500O(n³)Floyd-Warshall, interval DP (পর্ব ৪৪, ৫৪)
n ≤ 5,000O(n²)দুই-মাত্রার DP, সব জোড়া
n ≤ 105–106O(n log n), O(n log² n)sorting, heap, Dijkstra, DSU, segment tree
n ≤ 107–108O(n)এক পাস, prefix sum, two pointer
n ≥ 109O(log n) বা O(1)সূত্র, binary search, দ্রুত ঘাত

আমাদের সমস্যায় মোট ইনপুট প্রায় n + m + q ≈ 7 × 105. অর্থাৎ বাজেট মোটামুটি “প্রতিটি জিনিসের জন্য কয়েকটি log” — O((n + m + q) log n) ≈ 107-এর ঘরে। প্রতিটি প্রশ্নে পুরো গ্রাফ ঘাঁটা (q × m ≈ 9 × 1010) বাদ। দ্বিতীয় তথ্যটি সংখ্যার আকার নিয়ে: একটি পথে 105 টি রাস্তা থাকতে পারে, প্রতিটি 109 পর্যন্ত — উত্তর 1014-এর কাছাকাছি হতে পারে। তাই int নয়, long long; নইলে overflow.

২ · আগে brute force — সঠিক, কিন্তু ধীর

দ্রুত সমাধানের আগে একটি brute force চাই — যা দেখেই বিশ্বাস হয় যে ঠিক। এর তিনটি কাজ: ছোট উদাহরণে উত্তর দিয়ে সমস্যা বুঝতে সাহায্য করা, পর্যবেক্ষণের কাঁচামাল দেওয়া, আর শেষে দ্রুত সমাধানকে যাচাই করা।

প্রথম চিন্তা — অনুকরণ। একটি নির্দিষ্ট c-এর জন্য ভ্যানটি চালিয়ে দেখি: প্রতিটি এলাকায় “সবচেয়ে বেশি কত চার্জ নিয়ে পৌঁছানো যায়” মনে রাখি, আর পরিবর্তন থামা পর্যন্ত সব রাস্তা ঘুরে মান বাড়াই। c বাড়লে সম্ভাব্যতা কখনো কমে না — বড় ব্যাটারি ছোট ব্যাটারির সব কাজ পারে। এটি একটি monotonic predicate, তাই পর্ব ১৯-এর উত্তরের ওপর binary search চলে: প্রতিটি প্রশ্নে log(1014) ≈ 47 বার feasibility check. কিন্তু প্রতিটি check-ই বহুবার পুরো গ্রাফ ঘোরে — q টি প্রশ্নে অসম্ভব রকম ধীর।

দ্বিতীয় চিন্তা — স্টেশন থেকে স্টেশনে লাফ। যেকোনো যাত্রাকে স্টেশনগুলোতে কেটে ফেলুন: প্রতিটি টুকরো এক স্টেশন থেকে পরের স্টেশন, মাঝে চার্জ নেওয়ার সুযোগ নেই। s থেকে t-তে এক লাফে যেতে লাগে ঠিক D[s][t] চার্জ — দুটির মধ্যে সবচেয়ে ছোট পথ। তাই উত্তর হলো স্টেশনগুলোর একটি “লাফের গ্রাফে” এমন পথ, যার সবচেয়ে বড় লাফটি যত ছোট সম্ভব। একে বলে minimax path. বাস্তবায়ন: পর্ব ৪৪-এর Floyd-Warshall দিয়ে D, তারপর একই ছাঁচে “যোগ”-এর বদলে “max” নিয়ে আরেকবার:

D[i][j] = min(D[i][j], D[i][l] + D[l][j])  →  M[s][t] = min(M[s][t], max(M[s][l], M[l][t]))

খরচ O(n³ + k³) সময়, O(n²) জায়গা — n = 105-এ 1015 কাজ, আর 1010 ঘরের টেবিল। k টি Dijkstra চালিয়ে k² টি লাফ বানালেও k = 105 হলে 1010 জোড়া। তবু এই brute force অমূল্য: সরল, আর দ্রুত সমাধানের চেয়ে সম্পূর্ণ আলাদা যুক্তিতে দাঁড়িয়ে — অংশ ৭-এ এটিই হবে আমাদের বিচারক।

৩ · পর্যবেক্ষণ আর মূল ধারণা — নিকটতম স্টেশনটিই সব বলে দেয়

সমস্যার কঠিন অংশ হলো “অবস্থা”: একটি এলাকায় আমরা কত চার্জ নিয়ে আছি, তা নির্ভর করে কোন পথে এসেছি তার ওপর। চার্জ 1014 পর্যন্ত যেকোনো সংখ্যা হতে পারে, তাই (এলাকা, চার্জ) জোড়ায় পর্ব ৪৩-এর state-space খোঁজও অসম্ভব। দরকার এমন একটি পর্যবেক্ষণ, যা অবস্থাকে একটিমাত্র সংখ্যায় নামিয়ে আনে। ধরুন d[v] = v থেকে নিকটতম যেকোনো স্টেশনে যেতে যত চার্জ লাগে (স্টেশনে d = 0).

  1. ওপরের সীমা: v-তে চার্জ কখনো c − d[v]-এর বেশি হতে পারে না। কারণ শেষবার চার্জ নেওয়ার পর একটি স্টেশন থেকে v পর্যন্ত এসেছি, আর সেই পথ অন্তত d[v] লম্বা।
  2. নিচের সীমা: v ছেড়ে যাওয়ার পর কোনো না কোনো স্টেশনে পৌঁছাতেই হবে (যাত্রা শেষ হয় b-তে, যেটি স্টেশন)। মাঝে চার্জের সুযোগ নেই, তাই v-তে অন্তত d[v] চার্জ থাকতেই হবে।
  3. ঘুরে আসার কৌশল: v-তে অন্তত d[v] থাকলে নিকটতম স্টেশনে গিয়ে পুরো চার্জ নিয়ে ফিরে আসা যায় — ফেরার পর ঠিক c − d[v]. অর্থাৎ বৈধ যাত্রায় v-তে থাকলে আমরা সবসময় সর্বোচ্চ সম্ভব চার্জ c − d[v] ধরে নিতে পারি। পথের ইতিহাস আর লাগে না।

তিনটি মিলিয়ে: u-তে c − d[u] চার্জ নিয়ে রাস্তা (u, v, w) পার হলে থাকে c − d[u] − w, আর v-তে দরকার অন্তত d[v]. তাই

রাস্তা (u, v, w) ব্যবহারযোগ্য  ⇔  W(u, v) = d[u] + w + d[v] ≤ c

শর্তটি শুধু রাস্তাটির নিজের ওপর নির্ভর করে — কোন পথে এসেছি তার ওপর নয়। নিচের ডায়াগ্রামে নমুনার দ্বিতীয় প্রশ্ন (মিরপুর → উত্তরা, c = 10) চালিয়ে দেখুন। নমুনায় d: মিরপুর, মতিঝিল, উত্তরা 0; ফার্মগেট 4; তেজগাঁও 2 (মতিঝিল কাছে); গুলশান 4 (তেজগাঁও হয়ে মতিঝিল)।

প্রতিটি ধাপের পর ভ্যানের অবস্থা
এখন কোথায়
চার্জ
d[এখানে]
সর্বোচ্চ সম্ভব c − d
যে রাস্তা পার হলাম, তার W = d[u] + w + d[v]
  1. শুরু. মিরপুর স্টেশন, ব্যাটারি পুরো: 10. এখানে d = 0, তাই সর্বোচ্চ সম্ভব চার্জও 10.
  2. ফার্মগেট. 4 খরচ, থাকল 6 — ঠিক c − d[Farmgate] = 10 − 4 = 6. রাস্তাটির W = 8 ≤ 10, তাই বৈধ।
  3. তেজগাঁও. আরও 3 খরচ, থাকল 3. কিন্তু এখানে সর্বোচ্চ সম্ভব 8 — আমরা কম চার্জ নিয়ে আছি। তবু 3 ≥ d = 2, তাই নিকটতম স্টেশনে যাওয়া যাবে।
  4. মতিঝিলে ঘুরে আসা. 2 খরচ করে মতিঝিল, থাকল 1, স্টেশনে ব্যাটারি আবার 10. এটিই অংশ ৩-এর তৃতীয় পর্যবেক্ষণ।
  5. তেজগাঁওয়ে ফেরা. 2 খরচ, থাকল 8 = c − d[Tejgaon]. এখন আমরা সর্বোচ্চ সম্ভব চার্জে — পথের ইতিহাস আর কোনো ভূমিকা রাখে না।
  6. গুলশান. 2 খরচ, থাকল 6 = 10 − 4. আবার সর্বোচ্চে আছি।
  7. উত্তরা. 6 খরচ, ঠিক 0 থাকতে পৌঁছালাম। ব্যবহৃত প্রতিটি রাস্তার W ≤ 10 ছিল; সবচেয়ে বড়টি গুলশান–উত্তরা, W = 10. মোট পথ 4 + 3 + 2 + 2 + 2 + 6 = 19, তবু 10-এর ব্যাটারিতেই হলো।
  8. c = 9 কেন পারে না. গুলশানে কোনোভাবেই 9 − 4 = 5-এর বেশি চার্জ থাকা সম্ভব নয় (নিকটতম স্টেশন 4 দূরে), অথচ উত্তরার রাস্তায় 6 লাগে। গুলশান থেকে উত্তরা ছাড়া অন্য পথে (সরাসরি মিরপুর–উত্তরা) লাগে 20. তাই উত্তর 10.

এখন সমস্যাটি সম্পূর্ণ নতুন চেহারা নিয়েছে: প্রতিটি রাস্তাকে ওজন W দিন; এমন সবচেয়ে ছোট c বের করুন, যাতে শুধু W ≤ c রাস্তা দিয়ে a থেকে b যাওয়া যায়। ব্যাটারি, চার্জ, ঘুরে আসা — কিছুই আর নেই। এটি একটি reduction: অপরিচিত সমস্যাকে পরিচিত সমস্যায় রূপান্তর। আর পর্ব ৪৫ থেকে আমরা জানি, undirected গ্রাফে minimax path একটি minimum spanning tree-তে থাকে, Kruskal-এর মতো হালকা থেকে ভারী ক্রমে edge জুড়লে a আর b যে মুহূর্তে এক দলে আসে, সেই edge-এর ওজনই উত্তর।

৪ · ডেটা স্ট্রাকচার বাছাই, অ্যালগরিদম আর শুদ্ধতার প্রমাণ

বাকি প্রশ্ন একটাই: q টি প্রশ্নের উত্তর একসাথে কীভাবে? সব প্রশ্ন শুরুতেই জানা, আর উত্তর যেকোনো ক্রমে দেওয়া যায় — তাই সমাধানটি offline হতে পারে। রাস্তাগুলো W-এর ছোট থেকে বড় ক্রমে DSU-তে জুড়তে থাকি। প্রতিটি দলের সাথে একটি তালিকা রাখি — “যেসব প্রশ্নের অন্তত একটি প্রান্ত এই দলে, কিন্তু উত্তর এখনো মেলেনি”. দুটি দল মিললে ছোট তালিকাটি ঘুরে দেখি কোন প্রশ্নের দুই প্রান্ত এখন এক দলে, তারপর ছোটটিকে বড়টিতে ঢেলে দিই — পর্ব ৩৩-এর small-to-large.

ধাপকাজযন্ত্রকোথায় শিখেছেন
১প্রতিটি এলাকার d[v]multi-source Dijkstra, binary heap, lazy deletionপর্ব ৪৩, পর্ব ২৩
২প্রতিটি রাস্তার W = d[u] + w + d[v]অ্যারে, tupleপর্ব ১০
৩W অনুযায়ী সাজানোলাইব্রেরির sort, tuple-এর তুলনাপর্ব ২৫
৪রাস্তা জুড়ে দল মেলানোDSU: union by size + path halvingপর্ব ৩৩
৫অপেক্ষমাণ প্রশ্নের তালিকা মেলানোsmall-to-large, amortized হিসাবপর্ব ৩৩, পর্ব ৬০
প্রমাণminimax ⇔ Kruskal-এর মুহূর্তcut property, cycle propertyপর্ব ৪৫
বিচারকধীর কিন্তু নিশ্চিত উত্তরFloyd-Warshall + minimaxপর্ব ৪৪

বিকল্প পথও আছে। W ওজনে MST বানিয়ে পর্ব ৪৮-এর binary lifting দিয়ে tree-পথের সবচেয়ে বড় edge বের করলেও হয় — প্রতিটি প্রশ্ন O(log n), আর প্রশ্নগুলো online-ও হতে পারে (অংশ ৮)। এখানে offline DSU বেছে নিলাম, কারণ কোড ছোট, ভুলের জায়গা কম, আর সব প্রশ্ন আগে থেকেই জানা। প্রতিযোগিতায় “কোনটি সবচেয়ে কম সময়ে নির্ভুল লিখতে পারব” — এটিও একটি নকশার সিদ্ধান্ত।

অ্যালগরিদম আর তিনটি lemma

Pseudocode — battery capacity queries (0-based areas, stations are 0..k-1)

d = multiSourceDijkstra(sources = {0, ..., k-1})       // d[v] = charge to the nearest station
for each road (u, v, w):   W = d[u] + w + d[v]
sort roads by W (ascending)
DSU with every area alone;  ans[i] = 0 if a_i == b_i else -1
pend[x] = list of query ids with an endpoint at x
for each road (u, v) in sorted order, with weight W:
    A = find(u);  B = find(v);  if A == B: continue
    union(A, B) by size, new root R; S = the smaller of the two pending lists
    for each query id i in S:
        if ans[i] == -1 and find(a_i) == find(b_i): ans[i] = W
    append S to the other (bigger) list, which now belongs to R

শুদ্ধতা তিনটি lemma-য় ভাগ করি — বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর লিখিত প্রশ্নে ঠিক এভাবে লিখলে পরীক্ষক প্রতিটি ধাপ মিলিয়ে নিতে পারেন।

  1. Lemma ১ (প্রয়োজনীয়তা). ধারণক্ষমতা c-তে যেকোনো বৈধ যাত্রা শুধু W ≤ c রাস্তা ব্যবহার করে। প্রমাণ: যাত্রায় রাস্তা (u, v, w) পার হওয়ার ঠিক আগে চার্জ x. অংশ ৩-এর ওপরের সীমায় x ≤ c − d[u]; নিচের সীমায় x − w ≥ d[v]. মিলিয়ে d[u] + w + d[v] ≤ c. ∎
  2. Lemma ২ (যথেষ্টতা). a থেকে b পর্যন্ত এমন একটি পথ থাকলে যার সব রাস্তার W ≤ c, তাহলে c-তে যাত্রা সম্ভব। প্রমাণ (পথের দৈর্ঘ্যে induction): invariant — পথের প্রতিটি vertex u-তে চার্জ ঠিক c − d[u]. a-তে সত্য (d[a] = 0, চার্জ c). রাস্তা (u, v, w) পার হলে থাকে c − d[u] − w ≥ d[v] ≥ 0 — চার্জ কখনো ঋণাত্মক হয়নি, আর নিকটতম স্টেশনে ঘুরে এসে v-তে c − d[v] পাওয়া যায়। b-তে পৌঁছালেই শেষ। ∎
  3. Lemma ৩ (offline DSU). W-এর ক্রমে রাস্তা জোড়ার সময় প্রশ্ন i-এর উত্তর সেই রাস্তার W, যার union ai আর bi-কে প্রথমবার এক দলে আনে — আর অ্যালগরিদম ঠিক তখনই উত্তর লেখে। প্রমাণ: Kruskal-এর মতো, W ≤ x-এর সব রাস্তা জোড়ার পর DSU-র দলগুলো ঠিক G≤x-এর component. a, b প্রথম যে x-এ এক component-এ আসে, সেটিই সবচেয়ে ছোট বৈধ c (Lemma ১ ও ২). তালিকার invariant: প্রতিটি root-এর তালিকায় সেই দলে প্রান্ত আছে এমন প্রতিটি অমীমাংসিত প্রশ্ন থাকে। যে প্রশ্ন এই union-এ প্রথম মেটে, তার এক প্রান্ত এক দলে, অন্যটি অন্য দলে — তাই সে দুটি তালিকাতেই আছে, বিশেষ করে ছোটটিতে; ছোট তালিকা ঘুরলেই সে ধরা পড়ে। ∎

Lemma ১ আর ২ মিলে বলে: উত্তর = W-ওজনে a থেকে b-এর minimax path-এর মান। এবার নমুনায় পুরো অ্যালগরিদম দেখুন। W-এর সাজানো ক্রম: মতিঝিল–তেজগাঁও 4, মিরপুর–ফার্মগেট 8, তেজগাঁও–গুলশান 8, ফার্মগেট–মতিঝিল 9, ফার্মগেট–তেজগাঁও 9, গুলশান–উত্তরা 10, মিরপুর–মতিঝিল 12, উত্তরা–মিরপুর 20. প্রশ্ন: q1 (Mir, Mot), q2 (Mir, Utt), q3 (Mot, Utt), q4 (Utt, Utt), q5 (Utt, Mir).

প্রতিটি রাস্তা প্রক্রিয়ার পর (দলের প্রথম নামটি root)
এই রাস্তা (W)
দলগুলো
অপেক্ষমাণ তালিকা
উত্তর q1 … q5
  1. শুরু. প্রতিটি এলাকা নিজের দলে। q4-এর দুই প্রান্ত একই (উত্তরা), তাই উত্তর সরাসরি 0, কোনো তালিকায় নেই। বাকি প্রশ্নগুলো দুই প্রান্তের তালিকায়।
  2. মতিঝিল–তেজগাঁও (4). দুটি একক দল মিলল, root মতিঝিল। তেজগাঁওয়ের তালিকা খালি, তাই কোনো প্রশ্ন দেখার দরকার নেই।
  3. মিরপুর–ফার্মগেট (8). নতুন দল {Mir, Far}, root মিরপুর। কোনো প্রশ্নের দুই প্রান্ত এখনো এক দলে নয়।
  4. তেজগাঁও–গুলশান (8). গুলশান মতিঝিলের দলে ঢুকল (বড় দলের root থাকে)। W সমান হলে ক্রম যা-ই হোক, উত্তর বদলায় না।
  5. ফার্মগেট–মতিঝিল (9). আকার 3 বনাম 2 — মতিঝিল root. কিন্তু তালিকার আকার উল্টো: মতিঝিলের 2টি, মিরপুরের 3টি। তাই তালিকা দুটি অদলবদল (O(1)) করে ছোটটি (q1, q3) ঘুরে দেখা হলো: q1-এর মিরপুর আর মতিঝিল এখন এক দলে — উত্তর 9. q3 এখনো নয়। ছোট তালিকাটি বড়টির শেষে জুড়ে গেল।
  6. ফার্মগেট–তেজগাঁও (9). দুই প্রান্তের root একই — এই রাস্তা কিছু জোড়ে না (MST-তে থাকত না)। বাদ।
  7. গুলশান–উত্তরা (10). উত্তরা বড় দলে ঢুকল। ছোট তালিকা উত্তরার (q2, q3, q5) — তিনটিরই দুই প্রান্ত এখন এক দলে, উত্তর 10. লক্ষ করুন, বড় তালিকার ৫টি প্রবেশ একবারও দেখা হয়নি — small-to-large-এর পুরো সঞ্চয় এখানে।
  8. শেষ দুটি রাস্তা. সব এলাকা আগেই এক দলে, দুটিই বাদ। ফল: 9, 10, 10, 0, 10 — নমুনার উত্তরের সাথে হুবহু মেলে।

৫ · Complexity — সময় আর মেমরি

ধাপসময়কেন
Multi-source DijkstraO((n + m) log n)সর্বোচ্চ 2m + k টি push আর ততগুলো pop, প্রতিটি O(log n)
W হিসাব আর সাজানোO(m log m)m টি tuple-এর তুলনাভিত্তিক sort
DSU-তে m টি রাস্তাO(m α(n))union by size + path halving-এ প্রতি find amortized α(n)
তালিকা মেলানোO(q log q · α(n))নিচে দেখুন

Small-to-large-এর হিসাব. তালিকাগুলোতে মোট 2q টি প্রবেশ, আর কোনোটি কখনো মোছা হয় না। একটি প্রবেশ সরে শুধু ছোট তালিকা থেকে বড়টিতে; নতুন তালিকার আকার তাই আগের তালিকার অন্তত দ্বিগুণ। আকার 2q ছাড়াতে পারে না, তাই প্রতিটি প্রবেশ সর্বোচ্চ log₂(2q) বার সরে — মোট O(q log q) সরানো, প্রতিটিতে দুটি find. এটি পর্ব ৬০-এর aggregate amortized analysis-এর একটি চমৎকার উদাহরণ: কোনো একটি union-এ অনেক কাজ হতে পারে, কিন্তু সব মিলিয়ে সীমিত।

মোট: O((n + m) log n + m log m + q log q · α(n)) সময়, আর O(n + m + q) মেমরি — adjacency list-এ 2m, সাজানো রাস্তা m, তালিকায় 2q, আর d, par, sz-এ n টি করে ঘর। সর্বোচ্চ ইনপুটে আনুমানিক কাজ: 7 × 105 × 17 + 3 × 105 × 18 + 6 × 105 × 19 ≈ 2.9 × 107 — দুই সেকেন্ডের সীমার অনেক নিচে। মেমরি কয়েক দশ MB.

৬ · বাস্তবায়ন — C++ আর Python

নিচের প্রোগ্রাম কনটেস্টের মতো ইনপুট পড়ে। “চালান” চাপলে অংশ ১-এর নমুনা ইনপুটটি নিজে থেকেই দেওয়া হয়:

6 8 3 5
1 4 4
4 2 5
1 2 12
4 5 3
5 6 2
6 3 6
2 5 2
3 1 20
1 2
1 3
2 3
3 3
3 1
#include <algorithm>
#include <functional>
#include <iostream>
#include <numeric>
#include <queue>
#include <tuple>
#include <vector>
using namespace std;
using ll = long long;
int main() {
    ios::sync_with_stdio(false); cin.tie(nullptr);                 // fast I/O
    int n, m, k, q; cin >> n >> m >> k >> q;
    vector<tuple<int, int, ll>> es(m);
    vector<vector<pair<int, ll>>> adj(n);
    for (auto& [u, v, w] : es) { cin >> u >> v >> w; --u; --v; adj[u].push_back({v, w}); adj[v].push_back({u, w}); }
    // 1) multi-source Dijkstra: d[v] = charge needed to reach the nearest station (areas 0..k-1)
    vector<ll> d(n, 1LL << 60);
    priority_queue<pair<ll, int>, vector<pair<ll, int>>, greater<>> pq;
    for (int s = 0; s < k; ++s) { d[s] = 0; pq.push({0, s}); }
    while (!pq.empty()) {
        auto [du, u] = pq.top(); pq.pop();
        if (du != d[u]) continue;                                    // stale pair
        for (auto [v, w] : adj[u]) if (du + w < d[v]) { d[v] = du + w; pq.push({d[v], v}); }
    }
    // 2) key insight: road (u, v, w) is usable with battery c  <=>  d[u] + w + d[v] <= c
    vector<tuple<ll, int, int>> E; for (auto [u, v, w] : es) E.push_back({d[u] + w + d[v], u, v});
    sort(E.begin(), E.end());
    // 3) offline DSU over roads in increasing order; pending queries merged small-to-large
    vector<int> par(n), sz(n, 1), qa(q), qb(q); iota(par.begin(), par.end(), 0);
    vector<ll> ans(q, -1); vector<vector<int>> pend(n);
    for (int i = 0; i < q; ++i) {
        cin >> qa[i] >> qb[i]; --qa[i]; --qb[i];
        if (qa[i] == qb[i]) ans[i] = 0; else { pend[qa[i]].push_back(i); pend[qb[i]].push_back(i); }
    }
    auto find = [&](int x) { while (par[x] != x) x = par[x] = par[par[x]]; return x; };   // path halving
    for (auto [c, u, v] : E) {
        int a = find(u), b = find(v); if (a == b) continue;
        if (sz[a] < sz[b]) swap(a, b);
        par[b] = a; sz[a] += sz[b];                                    // union by size
        if (pend[a].size() < pend[b].size()) swap(pend[a], pend[b]);
        for (int i : pend[b]) {                                        // walk the SMALLER list
            if (ans[i] < 0 && find(qa[i]) == find(qb[i])) ans[i] = c;
            pend[a].push_back(i);
        }
        vector<int>().swap(pend[b]);                                   // free its memory
    }
    for (ll x : ans) cout << x << '\n';
}

আউটপুট:

9
10
10
0
10

অংশ ৪-এর ডায়াগ্রামের সাথে হুবহু এক। কয়েকটি বিষয় লক্ষ করুন। (১) Dijkstra-তে সব স্টেশন একসাথে dist 0 দিয়ে heap-এ — একটিমাত্র multi-source চালানো। (২) tuple<ll, int, int> প্রথম উপাদান W দিয়ে সাজায়, তাই আলাদা comparator লাগেনি। (৩) 1LL << 60 “অসীম” হিসেবে যথেষ্ট বড়, আবার দুটি যোগ করলেও long long-এর সীমা ছাড়ায় না। (৪) find loop দিয়ে, recursion নয়। (৫) swap(pend[a], pend[b]) শুধু ভেতরের pointer বদলায়, O(1); আর vector<int>().swap(pend[b]) খালি তালিকার মেমরিও ফেরত দেয়।

import sys, heapq
def main():
    it = iter(sys.stdin.buffer.read().split())
    n, m, k, q = (int(next(it)) for _ in range(4))
    es = [(int(next(it)) - 1, int(next(it)) - 1, int(next(it))) for _ in range(m)]
    adj = [[] for _ in range(n)]
    for u, v, w in es: adj[u].append((v, w)); adj[v].append((u, w))
    # 1) multi-source Dijkstra: d[v] = charge needed to reach the nearest station
    d = [0] * k + [float("inf")] * (n - k)
    pq = [(0, s) for s in range(k)]
    while pq:
        du, u = heapq.heappop(pq)
        if du != d[u]: continue                # stale pair
        for v, w in adj[u]:
            if du + w < d[v]: d[v] = du + w; heapq.heappush(pq, (d[v], v))
    # 2) key insight: road (u, v, w) is usable with battery c  <=>  d[u] + w + d[v] <= c
    E = sorted((d[u] + w + d[v], u, v) for u, v, w in es)
    # 3) offline DSU over edges in increasing order; pending query lists merged small-to-large
    par, sz = list(range(n)), [1] * n
    qa, qb, ans, pend = [0] * q, [0] * q, [-1] * q, [[] for _ in range(n)]
    for i in range(q):
        qa[i], qb[i] = int(next(it)) - 1, int(next(it)) - 1
        if qa[i] == qb[i]: ans[i] = 0
        else: pend[qa[i]].append(i); pend[qb[i]].append(i)
    def find(x):
        while par[x] != x:
            par[x] = par[par[x]]               # path halving
            x = par[x]
        return x
    for c, u, v in E:
        a, b = find(u), find(v)
        if a == b: continue
        if sz[a] < sz[b]: a, b = b, a
        par[b] = a; sz[a] += sz[b]             # union by size
        if len(pend[a]) < len(pend[b]): pend[a], pend[b] = pend[b], pend[a]
        for i in pend[b]:                      # move the SMALLER list
            if ans[i] < 0 and find(qa[i]) == find(qb[i]): ans[i] = c
            pend[a].append(i)
        pend[b] = []
    print("\n".join(map(str, ans)))

main()

আউটপুট C++-এর সাথে হুবহু এক:

9
10
10
0
10

Python-এর পার্থক্যগুলো গতির জন্য: পুরো ইনপুট একবারে sys.stdin.buffer.read().split() দিয়ে পড়া, সব কাজ একটি ফাংশনের ভেতরে (লোকাল নাম দ্রুত), আর শেষে একটিমাত্র print. find loop দিয়ে লেখা জরুরি — recursive find লম্বা শিকলে Python-এর recursion সীমা (ডিফল্ট ১০০০) পেরিয়ে RecursionError দিত। float("inf") শুধু স্টেশনহীন বিচ্ছিন্ন অংশে থেকে যেতে পারে; সমস্যায় গ্রাফ connected, তাই উত্তরে কখনো আসে না।

৭ · টেস্টিং আর পারফরম্যান্স — edge case, checker, stress test আর সময় মাপা

নমুনা মিলেছে — কিন্তু নমুনা মেলা মানে প্রমাণ নয়। কোড আর প্রমাণের মাঝে প্রায়ই ছোট ফাঁক থাকে: একটি <-এর জায়গায় ≤, ভুল প্রান্তে find, শূন্য-ভিত্তিক সূচকের ভুল। তিন স্তরে যাচাই করি।

স্তর ১ — হাতে বানানো edge case

  • a = b: উত্তর 0 — কোনো রাস্তাই লাগে না। কোডে আলাদা করে ধরা হয়েছে।
  • k = 1: একটিই স্টেশন, তাই সব প্রশ্ন a = b — সব উত্তর 0.
  • সব এলাকাই স্টেশন (k = n): সব d = 0, W = w — সমস্যাটি হয়ে যায় সাধারণ minimax path, MST-এর সরাসরি প্রয়োগ।
  • একই জোড়ায় একাধিক রাস্তা, আর self-loop: self-loop দুই প্রান্ত একই দলে বলে বাদ পড়ে; parallel edges-এর হালকাটি আগে আসে।
  • লম্বা শিকল, বড় ওজন: 105 টি এলাকা এক সারিতে, প্রতিটি রাস্তা 109, স্টেশন শুধু দুই প্রান্তে — d প্রায় 5 × 1013, তাই int-এ overflow ধরা পড়বে।

স্তর ২ ও ৩ — checker আর stress test

Stress testing-এর ছাঁচ সরল: একটি test generator হাজার হাজার ছোট এলোমেলো ইনপুট বানায়, প্রতিটিতে দ্রুত সমাধান আর brute force চলে, উত্তর না মিললেই থেমে ইনপুটটি ছাপে। ছোট ইনপুট (n ≤ 7) ইচ্ছাকৃত: brute force সেখানে তাৎক্ষণিক, আর ভুল ধরা পড়লে ইনপুটটি হাতে trace করা যায়। জেনারেটর একটি নিজে লেখা PRNG — একটি 64-bit linear congruential generator, নির্দিষ্ট seed 2026. এতে দুটি সুবিধা: প্রতিবার হুবহু একই ১০০০টি টেস্ট (reproducibility), আর C++ ও Python একই সংখ্যার ধারা পায়। এলোমেলো tree আগে বানিয়ে তারপর বাড়তি রাস্তা জোড়া হয়, তাই গ্রাফ সবসময় connected; বাড়তি রাস্তায় self-loop আর parallel edge আপনা থেকেই আসে।

লুপের ভেতরে যা ঘটে
টেস্ট
ইনপুট
fast()
brute()
ফল
  1. বানানো. seed 2026 থেকে প্রথম টেস্ট: ৫টি এলাকা, স্টেশন 0 আর 1. জেনারেটর নিজে থেকেই কঠিন জিনিস এনেছে — 0 আর 1-এর মধ্যে দুটি রাস্তা (5 আর 3), আর দুটি self-loop. হাতে টেস্ট লিখলে এগুলো প্রায়ই বাদ পড়ে।
  2. দ্রুত সমাধান. fast() দিল [3, 0, 0, 0, 3]. তিনটি প্রশ্ন a = b, তাই 0; বাকি দুটিতে হালকা সরাসরি রাস্তাটি (3).
  3. বিচারক. brute() — Floyd-Warshall আর minimax — সম্পূর্ণ আলাদা যুক্তিতে একই উত্তর দিল। মিলেছে, পরের টেস্ট।
  4. আরেকটি টেস্ট. test 4-এ স্টেশন চারটি (0–3), এলাকা 4 আর 5 সাধারণ। উত্তর [4, 9, 2], দুই দিকেই এক।
  5. ১০০০টি শেষ. সব মিলেছে — মোট 3072টি প্রশ্ন। এটি প্রমাণ নয়, কিন্তু প্রমাণের সাথে মিলিয়ে আত্মবিশ্বাস অনেক বাড়ায়।
  6. Stress test-কে পরীক্ষা. ইচ্ছে করে ভুল ঢোকালাম: W = d[u] + w + d[v]-এর বদলে শুধু w (mutation testing). test 4-এর তৃতীয় প্রশ্নে fast 1, brute 2. কারণ: 0–4 আর 4–1 দুটি রাস্তাই 1, কিন্তু এলাকা 4-এ স্টেশন নেই — দুটির খরচ যোগ হয়, max নয়। এমন ইনপুট স্থায়ী regression test হিসেবে রেখে দিন।
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
unsigned long long st = 2026;                       // fixed seed: the same tests on every run
int rnd(int lo, int hi) {                           // 64-bit LCG, identical to the Python version
    st = st * 6364136223846793005ULL + 1442695040888963407ULL;
    return lo + (int)((st >> 33) % (unsigned long long)(hi - lo + 1));
}
vector<ll> fast(int n, int k, const auto& es, const auto& qs) {   // the final solution
    vector<vector<pair<int, ll>>> adj(n);
    for (auto [u, v, w] : es) { adj[u].push_back({v, w}); adj[v].push_back({u, w}); }
    vector<ll> d(n, 1LL << 60);
    priority_queue<pair<ll, int>, vector<pair<ll, int>>, greater<>> pq;
    for (int s = 0; s < k; ++s) { d[s] = 0; pq.push({0, s}); }
    while (!pq.empty()) {
        auto [du, u] = pq.top(); pq.pop();
        if (du == d[u]) for (auto [v, w] : adj[u]) if (du + w < d[v]) { d[v] = du + w; pq.push({d[v], v}); }
    }
    vector<tuple<ll, int, int>> E; for (auto [u, v, w] : es) E.push_back({d[u] + w + d[v], u, v});
    sort(E.begin(), E.end());
    vector<int> par(n), sz(n, 1); vector<ll> ans; vector<vector<int>> pend(n);
    iota(par.begin(), par.end(), 0);
    for (auto [a, b] : qs) { pend[a].push_back(ans.size()); pend[b].push_back(ans.size()); ans.push_back(a == b ? 0 : -1); }
    auto find = [&](int x) { while (par[x] != x) x = par[x] = par[par[x]]; return x; };
    for (auto [c, u, v] : E) {
        int a = find(u), b = find(v); if (a == b) continue;
        if (sz[a] < sz[b]) swap(a, b);
        par[b] = a; sz[a] += sz[b];
        if (pend[a].size() < pend[b].size()) swap(pend[a], pend[b]);
        for (int i : pend[b]) if (ans[i] < 0 && find(qs[i].first) == find(qs[i].second)) ans[i] = c;
        pend[a].insert(pend[a].end(), pend[b].begin(), pend[b].end()); pend[b].clear();
    }
    return ans;
}
vector<ll> brute(int n, int k, const auto& es, const auto& qs) {  // other idea: Floyd-Warshall,
    vector<vector<ll>> D(n, vector<ll>(n, 1LL << 60));               // then minimax station hops
    for (int i = 0; i < n; ++i) D[i][i] = 0;
    for (auto [u, v, w] : es) D[u][v] = D[v][u] = min(D[u][v], w);
    for (int l = 0; l < n; ++l) for (int i = 0; i < n; ++i) for (int j = 0; j < n; ++j) D[i][j] = min(D[i][j], D[i][l] + D[l][j]);
    for (int l = 0; l < k; ++l) for (int i = 0; i < k; ++i) for (int j = 0; j < k; ++j) D[i][j] = min(D[i][j], max(D[i][l], D[l][j]));
    vector<ll> ans; for (auto [a, b] : qs) ans.push_back(D[a][b]);
    return ans;
}
int main() {
    int checked = 0;
    for (int t = 0; t < 1000; ++t) {
        int n = rnd(2, 7), k = rnd(1, n);                           // areas 0..k-1 are stations
        vector<tuple<int, int, ll>> es; vector<pair<int, int>> qs;
        for (int v = 1; v < n; ++v) es.push_back({rnd(0, v - 1), v, rnd(1, 9)});   // random tree
        for (int e = rnd(0, n); e > 0; --e) es.push_back({rnd(0, n - 1), rnd(0, n - 1), rnd(1, 9)});
        for (int e = rnd(1, 5); e > 0; --e) qs.push_back({rnd(0, k - 1), rnd(0, k - 1)});
        if (fast(n, k, es, qs) != brute(n, k, es, qs)) { printf("MISMATCH in test %d\n", t); return 1; }
        checked += (int)qs.size();
    }
    printf("all 1000 tests passed (%d queries checked)\n", checked);
}

আউটপুট:

all 1000 tests passed (3072 queries checked)

এখানে কনটেস্টের অভ্যাসমতো <bits/stdc++.h> আর C++20-এর const auto& প্যারামিটার, যাতে প্রোগ্রাম ছোট থাকে। fast() অংশ ৬-এর সমাধানই, শুধু ইনপুট পড়ার বদলে ভেক্টর নেয়। brute() আলাদা যুক্তির: Floyd-Warshall দিয়ে সব জোড়ার সবচেয়ে ছোট চার্জ খরচ, তারপর স্টেশনগুলোর মধ্যে minimax. 1LL << 60-কে “অসীম” ধরায় দুটি অসীম যোগ হলেও overflow হয় না — 1LL << 62 নিলে এখানে হতো। জেনারেটরের {rnd(...), v, rnd(...)} braced list-এ বাঁ থেকে ডানে মান তৈরি হওয়ার নিশ্চয়তা আছে; সাধারণ ফাংশন-আর্গুমেন্টে এই ক্রম নিশ্চিত নয়, তখন C++ আর Python-এর টেস্ট আলাদা হয়ে যেত।

import heapq
st = 2026                                           # fixed seed: the same tests on every run
def rnd(lo, hi):                                    # 64-bit LCG, identical to the C++ version
    global st; st = (st * 6364136223846793005 + 1442695040888963407) % 2**64
    return lo + (st >> 33) % (hi - lo + 1)

def fast(n, k, es, qs):                             # Dijkstra + reweight + offline DSU, small-to-large
    adj = [[] for _ in range(n)]
    for u, v, w in es: adj[u].append((v, w)); adj[v].append((u, w))
    d, pq = [0] * k + [10**18] * (n - k), [(0, s) for s in range(k)]
    while pq:
        du, u = heapq.heappop(pq)
        for v, w in (adj[u] if du == d[u] else []):
            if du + w < d[v]: d[v] = du + w; heapq.heappush(pq, (d[v], v))
    par, sz, ans = list(range(n)), [1] * n, [0 if a == b else -1 for a, b in qs]
    pend = [[i for i, ab in enumerate(qs) if x in ab] for x in range(n)]
    def find(x):
        while par[x] != x: par[x] = par[par[x]]; x = par[x]
        return x
    for c, u, v in sorted((d[u] + w + d[v], u, v) for u, v, w in es):
        a, b = sorted((find(u), find(v)), key=lambda r: -sz[r])     # a = the bigger tree
        if a == b: continue
        par[b] = a; sz[a] += sz[b]
        if len(pend[a]) < len(pend[b]): pend[a], pend[b] = pend[b], pend[a]
        for i in pend[b]:
            if ans[i] < 0 and find(qs[i][0]) == find(qs[i][1]): ans[i] = c
        pend[a] += pend[b]; pend[b] = []
    return ans

def brute(n, k, es, qs):                            # other idea: Floyd-Warshall, then minimax hops
    D = [[0 if i == j else 10**18 for j in range(n)] for i in range(n)]
    for u, v, w in es: D[u][v] = D[v][u] = min(D[u][v], w)
    for l in range(n): D = [[min(D[i][j], D[i][l] + D[l][j]) for j in range(n)] for i in range(n)]
    S = [row[:k] for row in D[:k]]                  # station graph: a hop s -> t needs charge D[s][t]
    for l in range(k): S = [[min(S[i][j], max(S[i][l], S[l][j])) for j in range(k)] for i in range(k)]
    return [S[a][b] for a, b in qs]

checked = 0
for t in range(1000):
    n = rnd(2, 7); k = rnd(1, n)                    # areas 0..k-1 are charging stations
    es = [(rnd(0, v - 1), v, rnd(1, 9)) for v in range(1, n)]        # random tree: connected
    es += [(rnd(0, n - 1), rnd(0, n - 1), rnd(1, 9)) for _ in range(rnd(0, n))]   # extra roads
    qs = [(rnd(0, k - 1), rnd(0, k - 1)) for _ in range(rnd(1, 5))]
    if fast(n, k, es, qs) != brute(n, k, es, qs): print("MISMATCH in test", t, es, qs); break
    checked += len(qs)
else: print(f"all 1000 tests passed ({checked} queries checked)")

আউটপুট C++-এর সাথে হুবহু এক:

all 1000 tests passed (3072 queries checked)

একই LCG, একই seed, একই ক্রমে rnd ডাকা — তাই দুই ভাষা ঠিক একই ১০০০টি ইনপুট দেখে, আর গোনা প্রশ্নের সংখ্যাও (3072) মেলে। এটি নিজেই একটি ছোট যাচাই: দুই বাস্তবায়নের জেনারেটর এক কি না। Floyd-Warshall এখানে প্রতি ধাপে নতুন টেবিল বানায় (list comprehension); ধাপ l-এ D[i][l] আর D[l][j] বদলায় না বলে ফল একই। লুপের else চলে শুধু তখনই, যখন কোনো break হয়নি।

Stress test যা ধরে না। আরেকটি mutation চালিয়ে দেখেছি: তালিকা অদলবদলের লাইন (swap(pend[a], pend[b])) মুছে দিলেও ১০০০টি টেস্টই পাস করে — কারণ উত্তর ঠিকই থাকে, শুধু worst case O(q²) হয়ে যায়। ছোট এলোমেলো টেস্ট শুদ্ধতা ধরে, গতি নয়। তাই আলাদা করে সবচেয়ে বড় ইনপুট বানিয়ে সময় মাপতে হয় — আর এমন ইনপুটও, যা একটি বিশেষ দুর্বলতায় আঘাত করে (যেমন সব প্রশ্ন একটি এলাকায়, আর সেই এলাকাটি বারবার ছোট দলে)।

পারফরম্যান্স — কাজ গোনা আর দ্রুত I/O

অংশ ৫-এর হিসাবে প্রায় 3 × 107 মৌলিক কাজ। C++-এ এটি এক সেকেন্ডের অনেক কম হওয়ার কথা। একটি এলোমেলো বড় ইনপুটে (n = 105, m = q = 2 × 105, k = 1000) Python সংস্করণে সাধারণ ল্যাপটপে মোটামুটি এক সেকেন্ডের ঘরে সময় লাগার কথা (মেশিনভেদে কম-বেশি) — সীমার ভেতরে, তবে C++-এর তুলনায় নিরাপত্তার ব্যবধান কম। লক্ষ করার বিষয়, এমন সমস্যায় সময়ের বড় অংশ যায় ইনপুট পড়তেই: সর্বোচ্চ সীমায় প্রায় 1.5 × 106 টি সংখ্যা।

  • C++: fast I/O — ios::sync_with_stdio(false); cin.tie(nullptr); আর endl-এর বদলে '\n' (endl প্রতিবার buffer খালি করে)। এই দুই লাইন ছাড়া লাখো সংখ্যায় cin কয়েক গুণ ধীর হতে পারে।
  • Python: লাইন ধরে input() নয়, একবারে sys.stdin.buffer.read().split(); আউটপুট একটি "\n".join-এ। heap-এ tuple, আর গ্লোবাল নামের বদলে ফাংশনের লোকাল নাম।
  • মেমরি: vector<vector<int>>-এ লক্ষাধিক ছোট vector — প্রতিটির নিজস্ব খরচ আছে; খালি হওয়া তালিকার মেমরি ফেরত দিলে সর্বোচ্চ ব্যবহার কমে।
  • সবচেয়ে খারাপ আকৃতি: শুধু এলোমেলো নয়, শিকল-গ্রাফ (Dijkstra-য় অনেক relax), তারকা-গ্রাফ, সব প্রশ্ন একই জোড়ায় — প্রতিটিতে আলাদা করে সময় মাপুন।

৮ · ভিন্ন রূপ আর বিস্তার — সমস্যা বদলালে কী হয়

একটি সমাধান সত্যিই বুঝেছেন কি না, তার ভালো পরীক্ষা হলো সমস্যা একটু বদলে দেখা — কোন অংশ টেকে, কোনটি ভাঙে।

বদলকী ভাঙেনতুন পথ
প্রশ্ন online — পরের প্রশ্ন আগের উত্তরের ওপর নির্ভর করেপ্রশ্নগুলো আগে জানা নেই, offline DSU চলে নাW-ওজনে MST, তারপর binary lifting দিয়ে tree-পথের সবচেয়ে বড় edge (পর্ব ৪৮): প্রতিটি প্রশ্ন O(log n). অথবা Kruskal reconstruction tree + LCA.
সবার জন্য একই নির্দিষ্ট c, প্রশ্ন “যাওয়া যায় কি?”কিছুই না — বরং সহজW ≤ c-এর সব রাস্তা একবারে জুড়ে প্রতিটি প্রশ্নে দুটি find: প্রতিটি O(α(n)) amortized
নতুন চার্জিং স্টেশন খোলেd কমে, ফলে অনেক W বদলায়নতুন স্টেশন থেকে Dijkstra চালিয়ে শুধু কমে যাওয়া d-গুলো হালনাগাদ; MST নতুন করে, বা কয়েকটি বদল জমিয়ে একসাথে পুনর্গঠন (পর্ব ৩৭-এর sqrt-ধারণা)
রাস্তা বন্ধ হয় (মোছা)DSU মুছতে পারে নাসব ঘটনা আগে জানা থাকলে সময়ের ওপর segment tree আর rollback DSU — offline dynamic connectivity
একমুখী রাস্তা“ঘুরে এসে চার্জ নেওয়া” আর undirected connectivity — দুটিইঅনুশীলন ৩ দেখুন; সাধারণত প্রতিটি প্রশ্নে binary search + BFS, অনেক ধীর
ব্যাটারি নির্দিষ্ট, কিন্তু মোট সময় কমাতে হবেminimax আর যথেষ্ট নয়দুই রকম খরচ একসাথে — resource-constrained shortest path-এর আত্মীয়; সাধারণ রূপটি NP-hard (পর্ব ৬৪–৬৫)

আরও গভীরে: Kruskal reconstruction tree-তে প্রতিটি union-এ একটি নতুন node বানানো হয়, যার দুই সন্তান মিলে যাওয়া দুই দলের root, আর মান সেই রাস্তার W. ফলে 2n − 1 node-এর একটি tree, যেখানে যেকোনো দুই পাতার LCA-র মান ঠিক তাদের minimax — online প্রশ্নের সবচেয়ে পরিষ্কার উত্তর, আর “c ধারণক্ষমতায় a থেকে কয়টি স্টেশনে যাওয়া যায়” ধরনের প্রশ্নও subtree গুনে মেলে। রাস্তা যোগ-মোছা দুটোই থাকলে সময়-অক্ষে segment tree বানিয়ে প্রতিটি রাস্তাকে তার সক্রিয় সময়ের O(log q) টি node-এ রাখা হয়, আর DFS-এ নামার সময় union, ওঠার সময় rollback — মোট O((m + q) log q · log n).

পরীক্ষা ও ইন্টারভিউতে:

  • n = 2 × 105, সময় ১ সেকেন্ড — কোন complexity নিরাপদ? — O(n log n); O(n²) = 4 × 1010 চলবে না।
  • দুই vertex-এর minimax (bottleneck) path কীভাবে? — MST-এর tree-পথেই থাকে; offline হলে Kruskal-এর ক্রমে DSU, online হলে MST + binary lifting.
  • Small-to-large-এ প্রতিটি উপাদান কতবার সরে? — O(log N), কারণ প্রতিবার তার সংগ্রহ অন্তত দ্বিগুণ হয়।
  • Stress test-এর brute force কেমন হওয়া উচিত? — সরল, নিশ্চিতভাবে ঠিক, আর দ্রুত সমাধানের চেয়ে আলাদা যুক্তিতে; ধীর হলে সমস্যা নেই।
  • র‍্যান্ডম টেস্টে seed নির্দিষ্ট কেন? — ভুল পেলে ঠিক একই ইনপুট আবার তৈরি করে debug করা যায়।

৯ · কোর্সের রোডম্যাপ — নয়টি মডিউল, আর এরপর কোথায়

এই একটি সমস্যার সমাধানে কোর্সের প্রায় প্রতিটি মডিউল কোথাও না কোথাও কাজে লেগেছে। পুরো পথটি একবার ফিরে দেখা যাক:

মডিউলমূল বিষয়এই ক্যাপস্টোনে কোথায়
১ · শুরু ও বিশ্লেষণের ভিত্তি (পর্ব ১–৯)Big-O, invariant, induction, recurrenceসীমা থেকে বাজেট, Lemma ২-এর induction
২ · লিনিয়ার ডেটা স্ট্রাকচার (পর্ব ১০–১৭)array, list, stack, queue, two pointer, prefix sumadjacency list, অপেক্ষমাণ তালিকা
৩ · সার্চিং ও সর্টিং (পর্ব ১৮–২৫)binary search on answer, sorting, heapপ্রথম brute force, রাস্তা সাজানো, Dijkstra-র heap
৪ · ট্রি, হ্যাশ ও DSU (পর্ব ২৬–৩৩)tree, BST, hash table, DSUসমাধানের হৃদয় — DSU আর small-to-large
৫ · রেঞ্জ কোয়েরি (পর্ব ৩৪–৩৮)segment tree, Fenwick, sparse tableরাস্তা মোছা গেলে সময়ের ওপর segment tree
৬ · গ্রাফ অ্যালগরিদম (পর্ব ৩৯–৪৮)BFS, DFS, Dijkstra, Floyd, MST, LCAmulti-source Dijkstra, Floyd checker, MST-এর প্রমাণ, online রূপ
৭ · ডিজাইন কৌশল (পর্ব ৪৯–৫৯)greedy, DP, backtracking, stringKruskal-এর greedy যুক্তি, সীমার টেবিলের কৌশলগুলো
৮ · উন্নত বিশ্লেষণ ও তত্ত্ব (পর্ব ৬০–৬৭)amortized, randomized, P বনাম NPsmall-to-large-এর হিসাব, PRNG, NP-hard রূপের সীমা
৯ · পরীক্ষা, ইন্টারভিউ ও কনটেস্ট (পর্ব ৬৮–৭২)dry-run, MCQ, লিখিত প্রশ্ন, ইন্টারভিউ, কনটেস্টএরপর আপনার পথ

এরপর কী? বিশ্ববিদ্যালয় বা চাকরির পরীক্ষার প্রস্তুতি নিলে পর্ব ৬৮–৭০ (হাতে trace, MCQ, প্রমাণসহ লিখিত প্রশ্ন)। কোডিং ইন্টারভিউর জন্য পর্ব ৭১ — সেখানে প্যাটার্ন চিনে দ্রুত সমাধানে পৌঁছানোর অনুশীলন। ICPC বা অনলাইন কনটেস্টের জন্য পর্ব ৭২-এর সমস্যা সেট, তারপর নিয়মিত Codeforces, AtCoder বা Toph-এর মতো online judge-এ সমস্যা সমাধান। একটি কার্যকর অভ্যাস: প্রতিটি কঠিন সমস্যায় এই পর্বের ছয় ধাপ — সীমা, brute force, পর্যবেক্ষণ, প্রমাণ, কোড, stress test — খাতায় লিখে ফেলুন। কয়েক মাসে দেখবেন “মূল ধারণা” খুঁজে পাওয়ার গতি অনেক বেড়েছে।

১০ · AI-এর যুগে — সমাধান লেখায় নয়, যাচাইয়ে AI

এমন সমস্যা AI-কে দিলে প্রায়ই দ্রুত একটি আত্মবিশ্বাসী উত্তর আসে — কখনো ঠিক, কখনো সূক্ষ্মভাবে ভুল। সবচেয়ে বিপজ্জনক হলো সেই ভুল, যা নমুনা ইনপুটে ধরা পড়ে না। এই পর্বের সমস্যায় আমরা যেসব ভুল দেখেছি বা হতে পারে:

AI-এর সম্ভাব্য ভুলকীভাবে ধরবেন
সাধারণ ওজনে minimax (d বাদ) — “MST-এর সবচেয়ে বড় edge”স্টেশন A, B, মাঝে সাধারণ এলাকা X, রাস্তা A–X 1, X–B 1: আসল উত্তর 2, ভুল কোড বলবে 1
প্রতিটি প্রশ্নে binary search + Dijkstra, আর দাবি “যথেষ্ট দ্রুত”সীমা দিয়ে নিজে গুনুন: q × 47 × m log n ≈ 1014
int দিয়ে d বা উত্তরঅংশ ৭-এর লম্বা শিকল, প্রতিটি রাস্তা 109
Python-এ recursive find105-এর শিকলে চালান — RecursionError
small-to-large-এ অদলবদল ভুলে যাওয়াstress test পাস করবে! সবচেয়ে বড় ইনপুটে সময় মাপুন
complexity “O(m α(n))” — Dijkstra আর sort বাদপ্রতিটি ধাপের খরচ আলাদা করে যোগ করতে বলুন

AI-কে সমাধান লিখতে না দিয়ে, আপনার সমাধান ভাঙার কাজ দিন — আর প্রতিটি দাবির কারণ চান:

আমি DSA শিখছি। নিচে একটি সমস্যা, আমার সমাধানের মূল ধারণা, প্রমাণের খসড়া আর কোড দিলাম: [এখানে সমস্যা, ধারণা, প্রমাণ আর কোড পেস্ট করুন] নতুন সমাধান লিখে দিও না। শুধু এগুলো করো: ১. আমার প্রমাণের প্রতিটি ধাপ যাচাই করো; কোনো ধাপ ফাঁকা থাকলে ঠিক কোথায়, বলো। ২. আমার ধারণা ভুল প্রমাণ করে এমন সবচেয়ে ছোট counterexample খোঁজো (থাকলে), হাতে trace সহ। ৩. সমস্যার সীমা ধরে প্রতিটি ধাপের time ও space complexity আলাদা করে হিসাব করো। ৪. ছোট এলোমেলো ইনপুটের একটি generator আর একটি সরল brute force লিখে দাও — brute force যেন আমার ধারণা ব্যবহার না করে। ৫. কোন edge case আর কোন বড় ইনপুটে আমার কোডের সময় মাপা উচিত, তালিকা দাও।

AI-এর সাহায্যে লেখা সমাধান যাচাইয়ের চেকলিস্ট

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

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।

প্র ০১প্রায় আশি বছর আগের একটি গণিতের বই কেন আজও প্রোগ্রামিং কনটেস্টের সেরা পরামর্শ?

১৯৪৫ সালে হাঙ্গেরীয় গণিতবিদ George Pólya প্রকাশ করেন How to Solve It — স্কুল-কলেজের গণিতের সমস্যা কীভাবে ভাবতে হয়, তার একটি ছোট বই। তিনি সমাধানকে চার ধাপে ভাগ করেন: সমস্যাটি বোঝা, একটি পরিকল্পনা বানানো, পরিকল্পনা কাজে লাগানো, আর ফিরে দেখা। বইটি বহু ভাষায় অনূদিত হয়েছে, লক্ষ লক্ষ কপি বিক্রি হয়েছে, আর আজও শিক্ষকেরা এটি পড়তে বলেন।

এই ক্যাপস্টোন আসলে Pólya-র চার ধাপই। “বোঝা” মানে সীমা পড়া, নমুনা হাতে মেলানো, আর একটি brute force লেখা — যে প্রোগ্রাম লিখতে পারেন না, সেই সমস্যা আপনি এখনো পুরো বোঝেননি। “পরিকল্পনা”-র জন্য Pólya-র প্রিয় প্রশ্ন ছিল: আপনি কি এমন কোনো সম্পর্কিত সমস্যা জানেন, যার সমাধান আগে দেখেছেন? আমাদের ক্ষেত্রে সেটি minimax path আর MST — একবার W = d[u] + w + d[v] রূপান্তর চোখে পড়লে অপরিচিত সমস্যাটি পরিচিত হয়ে গেল। তিনি আরও বলতেন, সমস্যা খুব কঠিন হলে একটি সহজ রূপ সমাধান করুন — যেমন “সব এলাকাই স্টেশন” রূপটি, যেখানে সরাসরি MST খাটে; তারপর ভাবুন সাধারণ রূপে কী আলাদা।

সবচেয়ে অবহেলিত ধাপ হলো চতুর্থটি — ফিরে দেখা। Pólya জিজ্ঞেস করতে বলতেন: ফলটি কি অন্যভাবে পাওয়া যায়? পদ্ধতিটি কি অন্য সমস্যায় কাজে লাগে? প্রোগ্রামিংয়ে এর আধুনিক রূপ হলো stress test (অন্য উপায়ে একই উত্তর), ভিন্ন রূপ নিয়ে ভাবা (online হলে কী হয়), আর নিজের খাতায় কৌশলটি লিখে রাখা। কনটেস্টে যারা দ্রুত উন্নতি করেন, তাঁরা সাধারণত প্রতিটি সমস্যার পর ব্যাখ্যা (editorial) পড়ে নিজের পথের সাথে মেলান — ঠিক এই ফিরে দেখা।

মূল উপলব্ধি: সমস্যা সমাধান একটি শেখা যায় এমন প্রক্রিয়া, প্রতিভার ঝলক নয়। পরিচিত সমস্যায় রূপান্তর আর ফিরে দেখা — এই দুটি অভ্যাসই সবচেয়ে বেশি কাজ দেয়।

প্র ০২“সবচেয়ে চওড়া পথ” কেন সবসময় spanning tree-র ভেতরেই থাকে — আর বাস্তবে এটি কোথায় লাগে?

Minimax path-এর উল্টো রূপ হলো maximin বা “widest path”: এমন পথ যার সবচেয়ে সরু অংশটি যতটা সম্ভব চওড়া — যেমন সবচেয়ে ভারী ট্রাক কোন সেতু-পথে যেতে পারে, বা নেটওয়ার্কে কোন পথে সবচেয়ে বেশি bandwidth পাওয়া যায়। অপারেশনস রিসার্চে এটি “maximum capacity route problem” নামে ১৯৬০-এর দশকের শুরু থেকেই আলোচিত; T. C. Hu ১৯৬১ সালে এ বিষয়ে একটি পরিচিত প্রবন্ধ লেখেন, আর দেখা যায় সমাধান maximum spanning tree-র সাথে গভীরভাবে যুক্ত।

কেন tree-তেই থাকে? পর্ব ৪৫-এর cycle property দিয়ে সরল যুক্তি: ধরুন a থেকে b-এর একটি সেরা minimax path P একটি edge e ব্যবহার করে যা MST T-তে নেই। T-তে e-এর দুই প্রান্তের মধ্যে একটি পথ আছে, আর e যোগ করলে একটি cycle তৈরি হয়। MST-র ধর্মে সেই cycle-এ e-ই সবচেয়ে ভারী (বা সমান)। তাই e-কে T-এর সেই পথ দিয়ে বদলালে পথের সবচেয়ে বড় edge বাড়ে না। প্রতিটি এমন edge বদলে শেষে একটি walk পাই যা পুরোপুরি T-এর ভেতরে, আর তার ভেতর থেকে tree-পথটি বের করা যায় — minimax মান একই বা কম। অর্থাৎ একটি MST একসাথে সব জোড়ার minimax উত্তর ধরে রাখে — n² টি উত্তর মাত্র n − 1 টি edge-এ। এজন্যই প্রতিটি MST একটি minimum bottleneck spanning tree.

বাস্তব প্রয়োগ অনেক: ডেটা ক্লাস্টারিং-এ single-linkage (MST-এর সবচেয়ে ভারী edge কাটলে দল ভাগ হয়), ছবি ভাগ করা, নেটওয়ার্কে bandwidth-সচেতন routing, আর ভারী যানবাহনের পথ পরিকল্পনা। আমাদের ক্যাপস্টোনের কৌশল এই পুরোনো ধারণাটির ওপরই দাঁড়িয়ে — শুধু ওজনগুলো আগে d দিয়ে বদলে নিতে হয়েছে।

মূল উপলব্ধি: একটি ভালো কাঠামো (MST) অনেক প্রশ্নের উত্তর একসাথে সংকুচিত করে রাখে। কঠিন প্রশ্নে প্রায়ই কাজ হলো সমস্যাটিকে সেই কাঠামোর ভাষায় অনুবাদ করা।

প্র ০৩Offline সমাধান কি “ঠকানো”? বাস্তব সিস্টেমে প্রশ্ন আগে জানা থাকে কখন — আর কনটেস্ট কীভাবে online-এ বাধ্য করে?

Offline মানে সব প্রশ্ন হাতে পেয়ে নিজের সুবিধামতো ক্রমে উত্তর দেওয়া। এটি ঠকানো নয় — বাস্তবে অনেক কাজই এমন। ব্যাংক দিনের শেষে সব লেনদেন একসাথে হিসাব করে (batch processing), ডেটাবেস একসাথে আসা অনেক প্রশ্ন সাজিয়ে চালায় যাতে ডিস্ক কম পড়তে হয়, আর রাতের রিপোর্ট তৈরিতে প্রশ্নগুলো আগে থেকেই জানা। অ্যালগরিদমের ইতিহাসেও offline কৌশলের সুন্দর উদাহরণ আছে: ১৯৭০-এর দশকে Robert Tarjan DSU দিয়ে tree-তে অনেক LCA প্রশ্নের offline উত্তর দেওয়ার একটি পদ্ধতি দেন (পর্ব ৪৮), যা প্রায় linear সময়ে চলে। পর্ব ৩৭-এর Mo’s algorithm-ও প্রশ্নগুলো বিশেষ ক্রমে সাজিয়ে কাজ করে।

কিন্তু অনেক ক্ষেত্রে উত্তর তখনই লাগে: ম্যাপ অ্যাপে প্রতিটি ব্যবহারকারী সঙ্গে সঙ্গে পথ চান, পরের প্রশ্ন কী হবে কেউ জানে না। তখন online সমাধান লাগে — আমাদের সমস্যায় MST + binary lifting, বা Kruskal reconstruction tree. সাধারণত online সমাধান বেশি preprocessing আর বেশি কোড চায়।

কনটেস্টে সমস্যা-নির্মাতারা offline কৌশল বন্ধ করতে একটি পরিচিত কৌশল ব্যবহার করেন: প্রশ্নগুলো “এনকোড” করা থাকে — যেমন আসল a পেতে ইনপুটের সংখ্যার সাথে আগের উত্তর XOR করতে হয়। আগের উত্তর না জেনে পরের প্রশ্নই জানা যায় না, তাই প্রশ্নগুলো সাজানো অসম্ভব। Persistent data structure (যেমন পর্ব ৩৫-এ উল্লেখ করা persistent segment tree) অনেক offline সমাধানকে online-এ রূপান্তর করার একটি সাধারণ উপায় — প্রতিটি “সময়ের” কাঠামোর একটি কপি সস্তায় রেখে দেওয়া।

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

প্র ০৪প্রমাণ থাকলে আবার টেস্ট কেন? বিখ্যাত বাগগুলো এ নিয়ে কী শেখায়?

Donald Knuth-এর একটি বহুল উদ্ধৃত রসিকতা আছে, ১৯৭০-এর দশকের একটি চিঠি থেকে: “Beware of bugs in the above code; I have only proved it correct, not tried it.” কথাটি মজার, কিন্তু গভীর: প্রমাণ করা হয় একটি ধারণার জন্য, আর চালানো হয় একটি কোড — দুটির মাঝে অনুবাদের ভুল থাকতেই পারে।

সবচেয়ে বিখ্যাত উদাহরণ binary search. ২০০৬ সালে Joshua Bloch Google-এর একটি ব্লগ লেখায় দেখান যে Java-র লাইব্রেরির binary search-এ (আর আরও অনেক বাস্তবায়নে) (low + high) / 2 লাইনটি খুব বড় অ্যারেতে integer overflow ঘটায়। অ্যালগরিদমটি নিখুঁত, প্রমাণও ঠিক — কিন্তু প্রমাণে ধরে নেওয়া হয়েছিল পূর্ণসংখ্যা অসীম বড় হতে পারে। বাগটি প্রায় এক দশক লাইব্রেরিতে ছিল, কারণ কেউ এত বড় অ্যারে দিয়ে পরীক্ষা করেনি। আমাদের ক্যাপস্টোনে একই ঝুঁকি ছিল d-এর 1014 মানে, আর stress test-এর brute force-এ “অসীম + অসীম”-এ।

এলোমেলো টেস্টের ধারণাটিকে একটি সাধারণ পদ্ধতিতে রূপ দেয় property-based testing: ২০০০ সালে Koen Claessen আর John Hughes Haskell-এর জন্য QuickCheck তৈরি করেন, যেখানে প্রোগ্রামার একটি নিয়ম লেখেন (“দুই সমাধানের উত্তর সমান”), আর টুল অসংখ্য এলোমেলো ইনপুটে সেটি পরীক্ষা করে; ভুল পেলে ইনপুটটিকে ছোট করে (shrinking) দেখায়। আজ প্রায় সব বড় ভাষায় এর রূপ আছে। প্রতিযোগিতামূলক প্রোগ্রামিংয়ে stress test হলো এরই হাতে-লেখা রূপ।

উল্টো দিকটিও সত্য: টেস্ট কখনো সব ইনপুট দেখে না। আমাদের small-to-large-এর অদলবদল মুছে দিলেও সব টেস্ট পাস করেছিল। তাই প্রমাণ আর টেস্ট একে অপরের বিকল্প নয়, পরিপূরক।

মূল উপলব্ধি: প্রমাণ বলে ধারণা ঠিক, টেস্ট বলে কোড ধারণাটি ঠিকমতো অনুবাদ করেছে, আর সময় মাপা বলে সেটি যথেষ্ট দ্রুত — তিনটিই লাগে।

প্র ০৫বাস্তবের ইলেকট্রিক গাড়ির রুট পরিকল্পনা কি এই পর্বের মতো সহজ? কোথায় সমস্যাটি হঠাৎ NP-hard হয়ে যায়?

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

শর্তগুলো একটু বদলালেই ছবি পাল্টে যায়। ধরুন ব্যাটারি নির্দিষ্ট, আর চাই সবচেয়ে কম সময় — এখন প্রতিটি পথের দুটি খরচ (সময় আর চার্জ)। চার্জের পথে কোনো স্টেশন না থাকলে এটি হয় একটি সীমাবদ্ধ shortest path সমস্যা: “চার্জ খরচ ≤ c রেখে সবচেয়ে কম সময়ের পথ”. এর সাধারণ রূপটি NP-hard বলে প্রমাণিত (পর্ব ৬৪-এর reduction-এর ভাষায়, knapsack-এর মতো সমস্যা এতে রূপান্তর করা যায়) — যদিও সংখ্যাগুলো ছোট পূর্ণসংখ্যা হলে pseudo-polynomial DP চলে। আংশিক চার্জ নেওয়া যায়, স্টেশনে অপেক্ষার সময় আছে, চার্জের দাম সময়ভেদে আলাদা — প্রতিটি শর্ত সমস্যাকে আরও কঠিন করে।

বাস্তব রুটিং ব্যবস্থাগুলো তাই পর্ব ৬৫-এর কৌশল নেয়: প্রতিটি vertex-এ একাধিক “label” (সময়, চার্জ) রাখা আর যে label অন্যটির চেয়ে সব দিকে খারাপ তা ছেঁটে ফেলা, ভালো heuristic দিয়ে A*-এর মতো খোঁজ, আর আগে থেকে হিসাব করা কাঠামো। আর চার্জিং স্টেশন কোথায় বসাবেন — সেটি facility location ধাঁচের সমস্যা, যার অনেক রূপ NP-hard; সেখানে approximation অ্যালগরিদম (পর্ব ৬৬) কাজে লাগে।

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

অনুশীলন

খাতা-কলম নিন। প্রথমটি হাতে trace, দ্বিতীয়টিতে কোড লিখতে হবে, তৃতীয় আর চতুর্থটি ভাবনার।

  1. হাতে trace করুন: স্টেশন A, B, E; সাধারণ এলাকা C, D. রাস্তা: A–C 4, C–D 5, D–B 4, C–E 2, D–E 3, A–B 15. প্রতিটি এলাকার d, প্রতিটি রাস্তার W, সাজানো ক্রম আর DSU-র ধাপ লিখে (A, B), (A, E), (E, B)-এর উত্তর বের করুন। (A, B)-এর উত্তরে আসল যাত্রাটিও দেখান।

    d: A = B = E = 0; C = min(4, 2) = 2; D = min(4, 3) = 3. W: A–C 0 + 4 + 2 = 6; C–D 2 + 5 + 3 = 10; D–B 3 + 4 + 0 = 7; C–E 2 + 2 + 0 = 4; D–E 3 + 3 + 0 = 6; A–B 15. ক্রম: C–E 4, A–C 6, D–E 6, D–B 7, C–D 10, A–B 15.

    C–E → {C, E}; A–C → {A, C, E}: (A, E) = 6; D–E → D যোগ; D–B → B যোগ: (A, B) = 7, (E, B) = 7. যাত্রা c = 7: A থেকে 7 → C-তে 3 → E-তে 1, চার্জে 7 → D-তে 4 → B-তে 0. অথচ A থেকে B-এর সবচেয়ে ছোট পথ 13 — মাঝের স্টেশন E-র জন্যই উত্তর এত কম।

  2. কোড লিখুন: সব ভ্যানের ব্যাটারি একই, c (ইনপুটে দেওয়া)। প্রতিটি প্রশ্ন (a, b): যাওয়া যায় কি না, YES বা NO. মূল সমাধান থেকে কোন অংশ রাখবেন, কোনটি বাদ দেবেন? complexity কত?

    Dijkstra আর W রাখুন; সাজানো, অপেক্ষমাণ তালিকা আর small-to-large বাদ। W ≤ c এমন প্রতিটি রাস্তা একবারে union, তারপর প্রতিটি প্রশ্নে দুটি find.

    for (auto [u, v, w] : es)
        if (d[u] + w + d[v] <= c) unite(u, v);        // Lemma 1 + 2: exactly the usable roads
    for (auto [a, b] : qs)
        cout << (find(a) == find(b) ? "YES" : "NO") << '\n';

    সময় O((n + m) log n + (m + q) α(n)) — সাজানোর দরকারই নেই, কারণ সীমা একটিই। জায়গা O(n + m).

  3. ভেবে বলুন: রাস্তাগুলো একমুখী হলে মূল ধারণার কোন অংশ টেকে, কোনটি ভাঙে?

    দূরত্ব দুই রকম হয়ে যায়: din[u] = নিকটতম স্টেশন থেকে u, আর dout[v] = v থেকে নিকটতম স্টেশন পর্যন্ত (উল্টো গ্রাফে Dijkstra). প্রয়োজনীয়তা টেকে: রাস্তা u → v ব্যবহারে din[u] + w + dout[v] ≤ c লাগে।

    কিন্তু যথেষ্টতা ভাঙে: u থেকে নিকটতম স্টেশন s-এ গিয়ে চার্জ নিলেও s থেকে u-তে ফেরার পথ din[u]-এর চেয়ে অনেক লম্বা হতে পারে, বা না-ও থাকতে পারে — “ঘুরে এসে সর্বোচ্চ চার্জ” আর নিশ্চিত নয়। তার ওপর directed গ্রাফে পৌঁছানো (reachability) সমমিতিক নয়, তাই DSU-ও চলে না। সাধারণত প্রতিটি প্রশ্নে উত্তরের ওপর binary search আর স্টেশন-লাফের গ্রাফে খোঁজ লাগে — অনেক ধীর।

  4. ভুল ধারণা ভাঙুন: রাফি বলল, “d-এর ঝামেলা দরকার নেই — আসল ওজন w-এ MST বানিয়ে tree-পথের সবচেয়ে বড় রাস্তাই উত্তর।” সবচেয়ে ছোট একটি counterexample দিন, আর বলুন stress test এটি কীভাবে ধরত।

    স্টেশন A আর B, মাঝে স্টেশনহীন এলাকা X; রাস্তা A–X 1 আর X–B 1. রাফির উত্তর max(1, 1) = 1. কিন্তু X-এ চার্জ নেওয়া যায় না, তাই দুটি রাস্তার খরচ যোগ হয়: আসল উত্তর 2. W দিয়ে: d[X] = 1, তাই দুটি রাস্তারই W = 0 + 1 + 1 = 2 — সঠিক।

    অংশ ৭-এর stress test-এ এই ভুল সংস্করণটি ঠিক এমন একটি অংশ পেয়ে test 4-এ থেমেছিল: “MISMATCH in test 4”. রাফির ধারণা সঠিক শুধু তখন, যখন সব এলাকা স্টেশন (k = n).

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

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

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

  1. সমস্যা আর সীমা পড়ে complexity-র বাজেট ঠিক করা
  2. একটি সরল, নিশ্চিতভাবে সঠিক brute force লেখা
  3. পর্যবেক্ষণ থেকে মূল ধারণা খুঁজে সমস্যাকে পরিচিত সমস্যায় রূপান্তর
  4. অ্যালগরিদম আর ডেটা স্ট্রাকচার ঠিক করে শুদ্ধতা প্রমাণ করা
  5. দ্রুত সমাধানের কোড লেখা
  6. brute force-এর সাথে stress test আর সবচেয়ে বড় ইনপুটে সময় মাপা

একটি সমস্যায় n ≤ 2 × 105 আর সময়সীমা ১ সেকেন্ড। কোন time complexity-র সমাধান নিরাপদ?

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

মোট N টি উপাদান কয়েকটি তালিকায় ভাগ করা। দুটি তালিকা জোড়ার সময় সবসময় ছোটটির উপাদান বড়টিতে সরানো হয়। পুরো প্রক্রিয়ায় একটি উপাদান সর্বোচ্চ কতবার সরে?

  • ঠিক ১ বার
  • O(N) বার
  • O(log N) বার
  • O(α(N)) বার

একটি connected undirected weighted গ্রাফে u থেকে v-এর এমন পথ চাই, যার সবচেয়ে ভারী edge যতটা সম্ভব হালকা। এমন একটি পথ সবসময় কোথায় পাওয়া যায়?

  • যেকোনো minimum spanning tree-তে u থেকে v-এর পথে
  • u থেকে v-এর shortest path-এ
  • BFS tree-তে u থেকে v-এর পথে
  • শুধু সব সরল পথ গুনে দেখলেই পাওয়া যায়

Stress testing-এ দ্রুত সমাধানের সাথে তুলনার জন্য যে brute force লেখা হয়, তার সবচেয়ে জরুরি গুণ কোনটি?

  • দ্রুত সমাধানের মতোই দ্রুত হওয়া
  • দ্রুত সমাধানের মূল ধারণাটিই ব্যবহার করা
  • সবচেয়ে বড় ইনপুটেও চলতে পারা
  • সরল, নিশ্চিতভাবে সঠিক, আর আলাদা যুক্তিতে লেখা

ইলেকট্রিক ভ্যানের সমস্যায় (স্টেশনে ব্যাটারি পুরো ভরে, d[x] = x থেকে নিকটতম স্টেশনের দূরত্ব) একটি রাস্তা u–v-এর খরচ 5, d[u] = 3 আর d[v] = 4. এই রাস্তা ব্যবহার করতে ব্যাটারির ধারণক্ষমতা অন্তত কত লাগবে?

  • 5
  • 12
  • 9
  • 8

ছোট এলোমেলো ইনপুটে (n ≤ 7) ১০০০টি টেস্টের stress test কোন ধরনের ভুল সাধারণত ধরতে পারে না?

  • ভুল সূত্রের কারণে ভুল উত্তর
  • a = b-এর মতো edge case-এ ভুল উত্তর
  • উত্তর ঠিক রেখে worst case O(n²) হয়ে যাওয়া — যেমন small-to-large-এ অদলবদল ভুলে যাওয়া
  • parallel edge বা self-loop-এ ভুল

র‍্যান্ডম টেস্ট জেনারেটরে নির্দিষ্ট seed (যেমন 2026) ব্যবহারের মূল কারণ কী?

  • প্রতিবার হুবহু একই টেস্ট পাওয়া, যাতে ভুল আবার ঘটিয়ে দেখা যায়
  • সংখ্যাগুলো আরও বেশি এলোমেলো করা
  • জেনারেটরকে দ্রুত করা
  • নিশ্চিত করা যে সব সম্ভাব্য ইনপুট পরীক্ষা হবে

শব্দকোষ

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

মনে রাখুন

  1. সীমা আগে: n, m, q থেকে বাজেট — 105-এর ঘরে O(n log n); উত্তরের আকার দেখে long long.
  2. Brute force আগে: সরল আর আলাদা যুক্তির — এখানে Floyd-Warshall আর স্টেশনের মধ্যে minimax. এটি বোঝায়, ধারণা দেয়, শেষে বিচার করে।
  3. মূল ধারণা: d[v] = নিকটতম স্টেশনের দূরত্ব; রাস্তা ব্যবহারযোগ্য ⇔ d[u] + w + d[v] ≤ c. সমস্যা হয়ে যায় minimax path — MST-এর ধর্ম।
  4. সমাধান: multi-source Dijkstra → W সাজানো → DSU-তে জোড়া, small-to-large তালিকা। O((n + m) log n + m log m + q log q · α(n)), প্রমাণ তিন lemma-য়।
  5. যাচাই: edge case, নির্দিষ্ট seed-এর ১০০০টি stress test, একটি ইচ্ছাকৃত ভুল দিয়ে টেস্টের পরীক্ষা, আর সবচেয়ে বড় ইনপুটে সময় — কারণ ছোট টেস্ট গতি ধরে না।

পরের পর্বে: মডিউল ৯ শুরু — complexity আর dry-run: পরীক্ষার খাতায় হাতে চালিয়ে কোডের আউটপুট আর সময়ের হিসাব বের করার অনুশীলন।

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

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