এই পর্বে যা শিখবেন
- অ্যালগরিদমের সংজ্ঞা আর Knuth-এর পাঁচটি বৈশিষ্ট্য দিয়ে যেকোনো পদ্ধতি যাচাই করা
- সমস্যা, instance, অ্যালগরিদম আর প্রোগ্রাম — চারটির পার্থক্য পরিষ্কারভাবে বলা
- Euclid-এর GCD হাতে চালানো, আর al-Khwarizmi থেকে Turing পর্যন্ত সংক্ষিপ্ত ইতিহাস
- CLRS-ধাঁচের pseudocode লেখা এবং সেটিকে C++ ও Python-এ নির্ভুলভাবে অনুবাদ করা
- RAM model-এ একটি লুপের ধাপ একদম নিখুঁতভাবে গোনা, আর অ্যালগরিদম ডিজাইনের আট ধাপের কর্মপ্রবাহ
১ · অ্যালগরিদম — নিয়ম মেনে চলা ধাপের তালিকা
সকালে আপনি bKash অ্যাপ খুলে মাকে টাকা পাঠালেন। ধাপগুলো ভাবুন: “Send Money” চাপুন → নম্বর দিন → টাকার পরিমাণ দিন → PIN দিন → নিশ্চিত করুন। প্রতিটি ধাপ স্পষ্ট, ক্রম নির্দিষ্ট, আর শেষে একটি ফল আসে — টাকা পৌঁছায়, নয়তো “ব্যালান্স যথেষ্ট নয়” বার্তা। এই ধরনের ধাপের তালিকাকেই গণিত আর কম্পিউটার বিজ্ঞানে বলা হয় অ্যালগরিদম।
একটু আনুষ্ঠানিকভাবে বললে: অ্যালগরিদম হলো সুনির্দিষ্টভাবে বর্ণিত একটি গণনা-পদ্ধতি, যা কিছু মান input হিসেবে নেয়, ধাপে ধাপে কাজ করে, এবং কিছু মান output হিসেবে দেয়। CLRS (Cormen, Leiserson, Rivest ও Stein-এর বিখ্যাত বই Introduction to Algorithms) প্রায় এভাবেই সংজ্ঞা দেয়: অ্যালগরিদম হলো input-কে output-এ রূপান্তরের গণনামূলক ধাপের একটি ক্রম।
পরীক্ষার পর একজন শিক্ষকের হাতে ৬০টি খাতা এলোমেলো পড়ে আছে। তিনি রোল নম্বর অনুযায়ী সাজাতে চান। কেউ একটি একটি খাতা তুলে আগের সাজানো স্তূপে ঠিক জায়গায় ঢোকান; কেউ আগে ১–২০, ২১–৪০, ৪১–৬০ — তিন ভাগে ভাগ করে নেন। দুজনেরই কাজ একই (খাতা সাজানো), কিন্তু পদ্ধতি আলাদা। এই পদ্ধতিটিই অ্যালগরিদম। আর দুই পদ্ধতির মধ্যে কোনটি দ্রুত, কেন দ্রুত — এই প্রশ্নের উত্তরই এই পুরো কোর্সের বিষয়।
অ্যালগরিদম ভাষা-নিরপেক্ষ একটি ধারণা। একই অ্যালগরিদম C++-এ লিখলে এক রকম দেখায়, Python-এ আরেক রকম, বাংলায় খাতায় লিখলে আরেক রকম — কিন্তু তিনটিই একই অ্যালগরিদম। প্রোগ্রাম হলো অ্যালগরিদমের একটি নির্দিষ্ট ভাষায় লেখা রূপ, যা কম্পিউটার চালাতে পারে।
কেন এত জোর দিয়ে এই পার্থক্য বলছি? কারণ ইন্টারভিউ, বিশ্ববিদ্যালয়ের পরীক্ষা বা কনটেস্টে আপনাকে প্রায়ই কোড লেখার আগে অ্যালগরিদমটি বোঝাতে, তার শুদ্ধতা প্রমাণ করতে আর তার খরচ হিসাব করতে বলা হবে। যিনি শুধু কোড মুখস্থ রাখেন, ভাষা বদলালেই তিনি আটকে যান। যিনি অ্যালগরিদম বোঝেন, তিনি যেকোনো ভাষায় সেটি লিখে ফেলতে পারেন।
২ · Knuth-এর পাঁচ শর্ত — কোনটা অ্যালগরিদম, কোনটা নয়
“ধাপের তালিকা” বললে তো রান্নার রেসিপিও পড়ে। তাহলে কি রেসিপিও অ্যালগরিদম? এই প্রশ্নের নিখুঁত উত্তর দিয়েছেন Stanford বিশ্ববিদ্যালয়ের Donald Knuth, তাঁর বিখ্যাত বই The Art of Computer Programming-এর প্রথম খণ্ডে (১৯৬৮)। তাঁর মতে একটি অ্যালগরিদমের পাঁচটি বৈশিষ্ট্য থাকতেই হবে:
১ · সসীমতা (Finiteness)
Finiteness — যেকোনো বৈধ input-এ সসীম সংখ্যক ধাপের পরে অবশ্যই থামবে। “যতক্ষণ না উত্তর পাও, চেষ্টা চালিয়ে যাও” — এটি শর্ত পূরণ করে না, যদি উত্তর কখনো না-ও আসতে পারে।
থামার নিশ্চয়তা২ · স্পষ্টতা (Definiteness)
Definiteness — প্রতিটি ধাপ এত স্পষ্ট যে দুজন আলাদা মানুষ একই কাজই করবেন। “পরিমাণমতো লবণ দিন” স্পষ্ট নয়; “৫ গ্রাম লবণ দিন” স্পষ্ট।
কোনো দ্ব্যর্থতা নেই৩ · Input
শূন্য বা তার বেশি input — যেগুলো শুরুর আগে দেওয়া হয়, আর নির্দিষ্ট একটি সেট থেকে আসে (যেমন “ধনাত্মক পূর্ণসংখ্যা”)। শূন্য input-ও চলে: “প্রথম ১০টি মৌলিক সংখ্যা ছাপো”।
০ বা তার বেশি৪ · Output
এক বা একাধিক output — যার input-এর সাথে নির্দিষ্ট সম্পর্ক আছে। কোনো ফলই না দিলে সেটি দিয়ে কোনো সমস্যার সমাধান হলো না।
১ বা তার বেশি৫ · কার্যকারিতা (Effectiveness)
Effectiveness — প্রতিটি ধাপ এত মৌলিক যে একজন মানুষ কাগজ-কলমে সসীম সময়ে নির্ভুলভাবে করতে পারেন। “π-এর সব অঙ্ক লিখে ফেলো” কোনো একক ধাপ হতে পারে না।
হাতে করা সম্ভবএবার কয়েকটি পদ্ধতি এই পাঁচ শর্তের পাল্লায় মেপে দেখি:
| পদ্ধতি | অ্যালগরিদম? | কারণ |
|---|---|---|
| “পরিমাণমতো চিনি দিয়ে চা বানান” | না | স্পষ্টতা ভাঙে — “পরিমাণমতো” প্রত্যেকের কাছে আলাদা |
| “১ থেকে শুরু করে সংখ্যাগুলো একে একে ছাপো” | না | সসীমতা ভাঙে — কখনো থামে না |
| “সবচেয়ে বড় মৌলিক সংখ্যাটি লেখো” | না | মৌলিক সংখ্যা অসীম, তাই এমন সংখ্যাই নেই — কাজটি করাই অসম্ভব |
| “তালিকার প্রথম সংখ্যাকে সর্বোচ্চ ধরো; বাকিগুলো একে একে দেখে বড় পেলে বদলাও; শেষে সর্বোচ্চটি দাও” | হ্যাঁ | পাঁচটি শর্তই পূরণ করে — এটিই এই পর্বের ARRAY-MAX |
| bKash-এর সার্ভার: “অনুরোধ এলে প্রক্রিয়া করো, তারপর পরের অনুরোধের জন্য অপেক্ষা করো” | কঠোর অর্থে না | ইচ্ছাকৃতভাবেই কখনো থামে না; Knuth এমন পদ্ধতিকে বলেন “computational method” — তবে প্রতিটি অনুরোধ প্রক্রিয়ার অংশটি অ্যালগরিদম |
সবচেয়ে সূক্ষ্ম শর্ত হলো সসীমতা। কোনো পদ্ধতি সব input-এ থামে কি না, তা মাঝে মাঝে কেউই জানে না! বিখ্যাত উদাহরণ Collatz-এর ধাঁধা: “n জোড় হলে অর্ধেক করো, বিজোড় হলে 3n + 1 করো; ১ হলে থামো।” কোটি কোটি সংখ্যায় পরীক্ষা করে দেখা গেছে প্রতিবার ১-এ পৌঁছায়, কিন্তু সব সংখ্যার জন্য এর প্রমাণ আজও কেউ দিতে পারেননি। অর্থাৎ এটি অ্যালগরিদম কি না, সেটাই এখনো খোলা প্রশ্ন (অনুশীলন ৩)।
৩ · সমস্যা, instance, অ্যালগরিদম আর প্রোগ্রাম — চারটি আলাদা জিনিস
নতুনরা প্রায়ই এই চারটি শব্দ গুলিয়ে ফেলেন। অথচ পরীক্ষার খাতায় ঠিক এই পার্থক্যই জিজ্ঞেস করা হয়। একটি সমস্যা (problem) বলে কী চাই; অ্যালগরিদম বলে কীভাবে পাব; প্রোগ্রাম সেই “কীভাবে”-কে একটি নির্দিষ্ট ভাষায় লেখে। আর সমস্যার একটি নির্দিষ্ট input-কে বলে instance. নিচের ডায়াগ্রামে “পরে” চাপুন:
- সমস্যা
MAXকী চাই — সাধারণ বর্ণনা - Instance
[4, 8, 2, 9, 6]একটি নির্দিষ্ট input - অ্যালগরিদম
ARRAY-MAXকীভাবে পাব — ভাষাহীন ধাপ - প্রোগ্রাম
max.cpp / max.pyনির্দিষ্ট ভাষায় লেখা রূপ - চালানো
9একটি instance-এ একটি রান
Input : n টি পূর্ণসংখ্যার একটি অ্যারে A, যেখানে n ≥ 1
Output: এমন একটি মান m, যা A-তে আছে এবং
A-এর প্রতিটি উপাদান x-এর জন্য x ≤ m
A = [4, 8, 2, 9, 6] n = 5 সঠিক উত্তর 9
A = [-3, -7, -1] n = 3 সঠিক উত্তর -1
A = [42] n = 1 সঠিক উত্তর 42
A = [5, 5, 5, 5] n = 4 সঠিক উত্তর 5
ARRAY-MAX(A, n) 1 max ← A[1] 2 for i ← 2 to n 3 if A[i] > max 4 max ← A[i] 5 return max
// C++ int mx = a[0]; for (size_t i = 1; i < a.size(); i++) if (a[i] > mx) mx = a[i]; # Python mx = a[0] for i in range(1, len(a)): if a[i] > mx: mx = a[i]
$ ./max 9 এক সমস্যা → অনেক অ্যালগরিদম → প্রতিটির অনেক প্রোগ্রাম → প্রতিটির অনেক রান
- সমস্যা — কী চাই। সমস্যা শুধু বলে input কেমন হবে আর output-কে কোন শর্ত মানতে হবে। “কীভাবে” নিয়ে একটি শব্দও নেই। খেয়াল করুন, শর্তে
n ≥ 1আছে — খালি অ্যারের সর্বোচ্চ মান অর্থহীন, তাই সমস্যাটিই সেটি বাদ দিয়েছে। - Instance — একটি নির্দিষ্ট input.
[4, 8, 2, 9, 6]হলো MAX সমস্যার একটি instance; উত্তর 9. একই সমস্যার instance অসংখ্য — ঋণাত্মক সংখ্যা, একটি মাত্র উপাদান, সব সমান। ভালো অ্যালগরিদমকে সবগুলোতেই ঠিক উত্তর দিতে হবে। - অ্যালগরিদম — কীভাবে পাব। ARRAY-MAX: প্রথমটিকে সর্বোচ্চ ধরা, বাকিগুলো একে একে দেখে বড় পেলে বদলানো। এখানে কোনো
;নেই,vectorনেই — শুধু যুক্তি। একই সমস্যার অন্য অ্যালগরিদমও হয়, যেমন আগে সাজিয়ে শেষেরটি নেওয়া (ধীর, কিন্তু ঠিক)। - প্রোগ্রাম — নির্দিষ্ট ভাষায় লেখা। একই অ্যালগরিদম C++ আর Python-এ। খেয়াল করুন, দুটিই 0-based: pseudocode-এর
A[1]হয়েছেa[0], আর “2 থেকে n” হয়েছে “1 থেকে n−1”. অনুবাদের এই জায়গাতেই সবচেয়ে বেশি ভুল হয় (অংশ ৫)। - চালানো — একটি instance-এ একটি রান। প্রোগ্রাম চালালে একটি নির্দিষ্ট instance-এর উত্তর আসে: 9. একটি রান ঠিক হলেই অ্যালগরিদম ঠিক প্রমাণ হয় না — প্রতিটি instance-এ ঠিক হতে হবে। সেই প্রমাণের কৌশল আসছে পর্ব ৩-এ।
শেষ ধাপের কথাটি আবার বলি, কারণ এটিই পুরো কোর্সের মেরুদণ্ড। একটি অ্যালগরিদমকে সঠিক (correct) বলা হয় তখনই, যখন সমস্যার প্রতিটি instance-এ এটি থামে এবং সঠিক output দেয়। দশটি উদাহরণে ঠিক চললেও একটিমাত্র instance-এ ভুল হলে অ্যালগরিদমটি ভুল। উল্টো দিকে, ভুল অ্যালগরিদমও মাঝে মাঝে কাজের হয় — যদি ভুলের হার নিয়ন্ত্রণ করা যায় (পর্ব ৬১-এর randomized অ্যালগরিদম) বা উত্তর “প্রায় সেরা” হলেই চলে (পর্ব ৬৬)।
আরেকটি শব্দ এখনই শিখে রাখুন: input size বা input-এর আকার, সাধারণত n লেখা হয়। অ্যারের ক্ষেত্রে এটি উপাদানের সংখ্যা; গ্রাফের ক্ষেত্রে vertex আর edge-এর সংখ্যা; আর একটি বড় সংখ্যার ক্ষেত্রে তার অঙ্কের (বা bit-এর) সংখ্যা। অ্যালগরিদমের খরচ আমরা সবসময় এই আকারের ফাংশন হিসেবে মাপব।
৪ · একটু ইতিহাস — বাগদাদ, আলেকজান্দ্রিয়া আর একটি কাল্পনিক যন্ত্র
“Algorithm” শব্দটি এসেছে নবম শতকের বাগদাদের গণিতবিদ মুহাম্মদ ইবনে মুসা আল-খোয়ারিজমি-র নাম থেকে। তিনি হিন্দু-আরবি সংখ্যাপদ্ধতিতে (০ থেকে ৯ আর স্থানীয় মান) হিসাব করার নিয়ম নিয়ে একটি বই লেখেন। দ্বাদশ শতকে বইটির ল্যাটিন অনুবাদের শুরু হতো “Algoritmi de numero Indorum” দিয়ে — “আল-খোয়ারিজমি বলেন, ভারতীয় সংখ্যা সম্পর্কে…”। নামের ল্যাটিন রূপ “Algoritmi” থেকেই ধীরে ধীরে এল “algorism”, পরে “algorithm”. তাঁর আরেকটি বইয়ের নামের “al-jabr” শব্দ থেকে এসেছে “algebra”.
তবে নামটা নবম শতকের হলেও, অ্যালগরিদমের ধারণা আরও অনেক পুরোনো। খ্রিস্টপূর্ব প্রায় ৩০০ সালে আলেকজান্দ্রিয়ার গণিতবিদ Euclid তাঁর Elements বইয়ের সপ্তম খণ্ডে দুটি সংখ্যার গরিষ্ঠ সাধারণ গুণনীয়ক (GCD, গসাগু) বের করার একটি পদ্ধতি লেখেন। Knuth একে বলেছেন টিকে থাকা সবচেয়ে পুরোনো অ-তুচ্ছ অ্যালগরিদমগুলোর একটি। আজও এটি প্রতিদিন কোটি কোটিবার চলে — আপনার ব্রাউজারের এনক্রিপশন থেকে শুরু করে ভগ্নাংশ লঘিষ্ঠ করা পর্যন্ত।
Euclid-এর মূল যুক্তি
ধরুন a = q·b + r, যেখানে r হলো ভাগশেষ, অর্থাৎ r = a mod b (mod মানে ভাগশেষ)। এখন যে সংখ্যা a আর b দুটিকেই নিঃশেষে ভাগ করে, সে a − q·b = r-কেও ভাগ করে। উল্টো দিকে, যে b আর r দুটিকে ভাগ করে, সে q·b + r = a-কেও ভাগ করে। অর্থাৎ (a, b) জোড়ার সাধারণ গুণনীয়ক আর (b, r) জোড়ার সাধারণ গুণনীয়ক হুবহু একই সেট — তাই সবচেয়ে বড়টিও একই:
প্রতিবার দ্বিতীয় সংখ্যাটি ছোট হয় (r < b), আর ঋণাত্মক হয় না — তাই একসময় শূন্য হতেই হবে। এটাই সসীমতার প্রমাণ। Euclid নিজে অবশ্য ভাগ নয়, বারবার বিয়োগ ব্যবহার করেছিলেন (বড়টি থেকে ছোটটি বিয়োগ); ভাগশেষ হলো অনেকগুলো বিয়োগ একসাথে করার সংক্ষিপ্ত রূপ। এবার Euclid-এর অ্যালগরিদম হাতে চালিয়ে দেখি — gcd(252, 105):
0EUCLID(a, b)1while b ≠ 02 r ← a mod b3 a ← b4 b ← r5return a
| a | |
| b | |
| r | |
| mod-এর সংখ্যা |
- শুরু। a = 252, b = 105. লাইন ১-এর শর্ত: 105 ≠ 0? সত্য, তাই লুপের ভেতরে ঢুকব।
- প্রথম ভাগ। 252 = 2 × 105 + 42, তাই r = 252 mod 105 = 42. এ পর্যন্ত একটি mod হলো।
- জোড়া সরানো। a ← 105, b ← 42. এখন প্রশ্নটি দাঁড়াল gcd(105, 42) — gcd(252, 105)-এর সমান, কিন্তু সংখ্যাগুলো ছোট।
- দ্বিতীয় ভাগ। 42 ≠ 0, তাই আবার লুপে। 105 = 2 × 42 + 21, তাই r = 21. মোট দুটি mod.
- আবার সরানো। a ← 42, b ← 21. প্রশ্ন এখন gcd(42, 21).
- তৃতীয় ভাগ। 21 ≠ 0. 42 = 2 × 21 + 0, তাই r = 0. ভাগশেষ শূন্য মানে 21 নিজেই 42-কে নিঃশেষে ভাগ করে। মোট তিনটি mod.
- শেষ সরানো। a ← 21, b ← 0. প্রশ্ন এখন gcd(21, 0), আর শূন্যের সাথে যেকোনো সংখ্যার গসাগু সেই সংখ্যাটিই।
- থামা। লাইন ১: 0 ≠ 0? মিথ্যা — লুপ শেষ, লাইন ৫ ফেরত দেয় a = 21. যাচাই: 252 = 21 × 12, 105 = 21 × 5, আর 12 ও 5-এর কোনো সাধারণ গুণনীয়ক নেই। মাত্র ৩টি ভাগে উত্তর; ১০৫ থেকে নিচে একটি একটি সংখ্যা পরীক্ষা করলে লাগত ৮৫টি চেষ্টা।
Ada Lovelace (১৮৪৩): Charles Babbage-এর নকশা করা (কিন্তু কখনো পুরো তৈরি না হওয়া) Analytical Engine নিয়ে একটি প্রবন্ধ অনুবাদ করতে গিয়ে তিনি নিজের দীর্ঘ টীকা যোগ করেন। শেষ টীকা “Note G”-তে Bernoulli সংখ্যা বের করার একটি ধাপে-ধাপে পরিকল্পনা ছিল — যাকে প্রায়ই যন্ত্রের জন্য লেখা প্রথম প্রকাশিত প্রোগ্রাম বলা হয়। তিনি আরও বুঝেছিলেন, এমন যন্ত্র শুধু সংখ্যা নয়, যেকোনো প্রতীক (এমনকি সংগীতের সুর) নিয়েও কাজ করতে পারে।
Alan Turing (১৯৩৬): “অ্যালগরিদম” বলতে ঠিক কী বোঝায়, তার গাণিতিক সংজ্ঞা দিতে তিনি একটি কাল্পনিক যন্ত্র কল্পনা করেন — আজ যার নাম Turing machine. প্রায় একই সময়ে Alonzo Church অন্য পথে একই সিদ্ধান্তে পৌঁছান। এই সংজ্ঞা থেকেই Turing প্রমাণ করেন, কিছু সমস্যার কোনো অ্যালগরিদমই নেই — যেমন halting problem (ভাবনার প্রশ্ন ২)।
৫ · Pseudocode — কোনো ভাষার নয়, তবু নিখুঁত
অ্যালগরিদম লিখব কোন ভাষায়? বাংলা গদ্যে লিখলে দ্ব্যর্থতা থাকে; C++-এ লিখলে #include, টাইপ আর সেমিকোলনের ভিড়ে মূল যুক্তি হারিয়ে যায়। মাঝামাঝি সমাধান pseudocode — প্রোগ্রামিং ভাষার মতো কাঠামো, কিন্তু কোনো compiler-এর নিয়ম মানতে হয় না। পাঠ্যবই, বিশ্ববিদ্যালয়ের প্রশ্নপত্র আর গবেষণাপত্র — সবখানে অ্যালগরিদম এভাবেই লেখা হয়।
Pseudocode-এর কোনো একক “সরকারি” নিয়ম নেই। তাই প্রতিটি বই শুরুতেই নিজের নিয়ম বলে দেয়। এই কোর্সের নিয়ম (CLRS-ধাঁচের):
- অ্যারে 1-based:
A[1..n]— প্রথম উপাদানA[1], শেষটিA[n]. কোনো পর্বে 0-based লিখলে সেখানে আলাদা করে বলা থাকবে। - মান রাখা
←, তুলনা=:max ← A[1]মানে “max-এ A[1] রাখো”;if x = yমানে “x আর y সমান কি না”। (CLRS-এর তৃতীয় সংস্করণ থেকে মান রাখতে=আর তুলনায়==লেখা হয় — দুটোই চলে, শুধু একটি লেখায় একটি নিয়মই মানবেন।) - লুপ:
for i ← 2 to nমানে i = 2, 3, …, n — দুই প্রান্তসহ।downtoহলে উল্টো দিকে।while শর্তচেনা নিয়মেই চলে। লুপ শেষ হলে for-এর i-এর মান হয় n + 1 (শেষবার শর্ত মিথ্যা হয়েছিল ওই মানে)। - Indentation-ই block: কোনো
{ }বাendনেই — Python-এর মতো, ভেতরে সরানো লাইনগুলো লুপ বা if-এর অংশ। - লাইন নম্বর আর মন্তব্য: প্রতিটি লাইনের নম্বর থাকে, যাতে ব্যাখ্যায় “লাইন ৩” বলা যায়; মন্তব্য
//দিয়ে। and,orshort-circuit:i ≤ n and A[i] ≠ x-এ প্রথম অংশ মিথ্যা হলে দ্বিতীয় অংশ যাচাইই হয় না — তাইA[n+1]পড়ার ভুল হয় না।- উচ্চস্তরের কথা চলে, যদি সেটা মৌলিক কাজ হয়: “x আর y অদলবদল করো” চলে; কিন্তু “অ্যারেটি সাজাও” লিখলে সেটি একটি পুরো অ্যালগরিদমকে লুকিয়ে ফেলে — খরচ হিসাবের সময় আলাদা করে ধরতে হবে।
এই নিয়মে আমাদের দুই অ্যালগরিদম:
ARRAY-MAX(A, n) // A[1..n], n ≥ 1
1 max ← A[1]
2 for i ← 2 to n
3 if A[i] > max
4 max ← A[i]
5 return max
EUCLID(a, b) // a, b ≥ 0 পূর্ণসংখ্যা, দুটোই শূন্য নয়
1 while b ≠ 0
2 r ← a mod b
3 a ← b
4 b ← r
5 return aPseudocode থেকে C++ আর Python — অনুবাদের অভিধান
| Pseudocode | C++ | Python |
|---|---|---|
x ← 5 | x = 5; | x = 5 |
if x = y | if (x == y) | if x == y: |
A[1..n] | a[0] … a[n-1] | a[0] … a[n-1] |
for i ← 2 to n (A[i] পড়া) | for (i = 1; i < n; i++) (a[i]) | for i in range(1, n): |
for i ← n downto 1 | for (i = n-1; i >= 0; i--) | for i in range(n-1, -1, -1): |
a mod b | a % b | a % b |
⌊a / b⌋ | a / b (দুটোই ধনাত্মক int হলে) | a // b |
a ↔ b (অদলবদল) | swap(a, b); | a, b = b, a |
সবচেয়ে বিপজ্জনক অনুবাদ-ভুল: pseudocode-এর for i ← 1 to n হুবহু C++-এ for (i = 1; i <= n; i++) লিখে a[i] পড়া। এতে a[0] বাদ পড়ে আর a[n] — অ্যারের বাইরের ঘর — পড়া হয়। C++-এ এটি undefined behavior: কখনো ভুল উত্তর, কখনো crash, কখনো কিছুই বোঝা যায় না। Python-এ সাথে সাথে IndexError. নিয়ম মনে রাখুন: 1-based-এর A[k] = 0-based-এর a[k−1]; অনুবাদের পর সবচেয়ে ছোট আর সবচেয়ে বড় index হাতে বসিয়ে মিলিয়ে নিন।
Python-এর একটি সূক্ষ্মতাও খেয়াল রাখুন: ঋণাত্মক সংখ্যায় C++-এর % আর Python-এর % আলাদা ফল দেয় (-7 % 3 C++-এ −1, Python-এ 2). Euclid-এ আমরা অঋণাত্মক সংখ্যা ধরে নিয়েছি, তাই এখানে সমস্যা নেই — কিন্তু অনুবাদের সময় এই ধরনের “দেখতে এক, আচরণে ভিন্ন” জিনিসগুলোই ফাঁদ।
৬ · RAM model — হিসাবের জন্য একটি কাল্পনিক কম্পিউটার
“এই অ্যালগরিদম কত দ্রুত?” — প্রশ্নটির উত্তর সেকেন্ডে দিলে বিপদ। একই কোড পুরোনো ল্যাপটপে ২ সেকেন্ড, নতুন ডেস্কটপে ০.৩ সেকেন্ড, ফোনে আরেক রকম। তাহলে দুটি অ্যালগরিদম তুলনা করব কীভাবে? সমাধান: একটি সরল, কাল্পনিক কম্পিউটার ঠিক করে নেওয়া, আর সেখানে কতগুলো ধাপ লাগে তা গোনা। সবচেয়ে প্রচলিত এমন মডেলের নাম RAM model (Random-Access Machine — মেমরির যেকোনো ঘরে সরাসরি যাওয়া যায় বলে এই নাম; এটি আপনার কম্পিউটারের “RAM” চিপ নয়)।
ঢাকার মানচিত্রে মিরপুর থেকে মতিঝিলের দূরত্ব মাপলে পাবেন কিলোমিটার — জ্যাম, সিগন্যাল, বৃষ্টি কিছুই সেখানে নেই। তবু মানচিত্র অকেজো নয়: ১২ কিলোমিটারের রাস্তা যে ৩ কিলোমিটারের রাস্তার চেয়ে বেশি সময় নেবে, এটুকু সে ঠিকই বলে দেয়। RAM model হলো অ্যালগরিদমের জন্য এমনই একটি মানচিত্র — বাস্তবের অনেক কিছু বাদ দেয়, কিন্তু তুলনার জন্য যা দরকার, তা ঠিক রাখে।
RAM model যা ধরে নেয়
একটি প্রসেসর, নির্দেশ একটির পর একটি চলে — একসাথে দুটি নয়।
মৌলিক কাজ (primitive operation) প্রতিটি এক একক সময় নেয়: যোগ, বিয়োগ, গুণ, ভাগ, ভাগশেষ, তুলনা, মান রাখা, শর্তসাপেক্ষ লাফ, ফাংশন ডাকা ও ফেরা।
মেমরির যেকোনো ঘর পড়া বা লেখা এক একক — A[1] আর A[1000000] পড়তে একই সময়।
RAM model যা উপেক্ষা করে
মেমরির স্তর: cache থেকে পড়া মেইন মেমরির চেয়ে অনেক গুণ দ্রুত; ডিস্ক আরও বহু গুণ ধীর। মডেলে সব সমান।
সমান্তরাল কাজ: একাধিক core, একই নির্দেশে অনেক ডেটা (SIMD), GPU — মডেলে নেই।
অন্যান্য বাস্তবতা: CPU-র ঘড়ির গতি, compiler-এর optimization, অপারেটিং সিস্টেমের বাধা।
মানচিত্রে জ্যাম নেইWord size — একটি ছোট কিন্তু জরুরি শর্ত
“প্রতিটি যোগ এক একক” — কিন্তু যদি সংখ্যা দুটি এক হাজার অঙ্কের হয়? তখনো এক একক ধরলে মডেলটি প্রতারণার সুযোগ দেয়: পুরো একটি অ্যারেকে একটি বিশাল সংখ্যার ভেতরে ঠেসে রেখে এক “ধাপে” যোগ করে ফেলা যায়! তাই RAM model একটি মেমরি ঘরের আকার (word size) সীমিত রাখে: input আকার n হলে প্রায় c·log₂ n bit (c কোনো ছোট ধ্রুবক)। এতে n পর্যন্ত index বা গণনা একটি ঘরে আঁটে, কিন্তু অসীম তথ্য আঁটে না। বাস্তবের ৬৪-bit কম্পিউটারও তা-ই করে — একটি word-এ ৬৪ bit, তার বেশি নয়।
কোনটা “এক ধাপ”, কোনটা নয়
| কাজ | RAM model-এ খরচ | কেন |
|---|---|---|
x + y, x < y, a % b (word-আকারের সংখ্যা) | ১ একক | মৌলিক গাণিতিক কাজ |
A[i] পড়া বা লেখা | ১ একক | random access — সরাসরি ঠিকানায় যাওয়া |
Python-এ x in my_list | n পর্যন্ত | ভেতরে পুরো list একে একে দেখে |
max(my_list), sum(my_list) | n | লাইব্রেরির ভেতরে একটি লুপ — এক লাইন মানে এক ধাপ নয় |
sort(a.begin(), a.end()) | n log n-এর অনুপাতে | পুরো একটি অ্যালগরিদম (পর্ব ২১–২৫) |
দুটি দৈর্ঘ্য-n string তুলনা s == t | n পর্যন্ত | অক্ষর ধরে ধরে মেলায় |
শেষ চারটি সারিই নতুনদের সবচেয়ে বেশি ঠকায়: কোডে এক লাইন দেখে মনে হয় “এক ধাপ”, অথচ ভেতরে লুকিয়ে আছে পুরো একটি লুপ। কনটেস্টে Time Limit Exceeded পাওয়ার একটি বড় কারণ এটাই।
৭ · ধাপ গোনা — ARRAY-MAX-এর নিখুঁত হিসাব
এবার RAM model-এ ARRAY-MAX-এর running time একদম নিখুঁতভাবে বের করি। CLRS-এর পদ্ধতি: pseudocode-এর লাইন k একবার চলতে ck সময় নেয় (ধ্রুবক), আর আমরা গুনব প্রতিটি লাইন কতবার চলে। ধরুন n টি উপাদানের মধ্যে t বার নতুন সর্বোচ্চ পাওয়া যায় (লাইন ৪ চলে)।
| লাইন | খরচ | কতবার চলে | কেন |
|---|---|---|---|
1 max ← A[1] | c1 | 1 | শুরুতে একবার |
2 for i ← 2 to n | c2 | n | i = 2…n-এ n − 1 বার সত্য, আর i = n + 1-এ একবার মিথ্যা |
3 if A[i] > max | c3 | n − 1 | লুপের প্রতিটি ঘোরায় একবার |
4 max ← A[i] | c4 | t | শুধু নতুন সর্বোচ্চ পেলে; 0 ≤ t ≤ n − 1 |
5 return max | c5 | 1 | শেষে একবার |
সব যোগ করলে:
সব c = 1 ধরলে: T(n) = 2n + 1 + t
লাইন ২-এর “n বার” (n − 1 নয়) — এখানেই সবচেয়ে বেশি ভুল হয়। লুপের শর্ত শেষবার একবার মিথ্যা হয়ে লুপ থামায়, সেই যাচাইটিও একটি ধাপ। এবার A = [4, 8, 2, 9, 6] instance-এ গোনাটি চোখে দেখুন:
0ARRAY-MAX(A, n)1max ← A[1]2for i ← 2 to n3 if A[i] > max4 max ← A[i]5return max
| i | |
| A[i] | |
| max | |
| t (নতুন সর্বোচ্চ) | |
| মোট ধাপ |
- লাইন ১। max ← A[1] = 4. মোট ধাপ 1.
- i = 2. লাইন ২-এর শর্ত (2 ≤ 5, সত্য) +১, লাইন ৩-এর তুলনা (8 > 4? হ্যাঁ) +১, লাইন ৪ (max ← 8) +১। মোট 4, t = 1.
- i = 3. শর্ত +১, তুলনা (2 > 8? না) +১। লাইন ৪ চলে না। মোট 6.
- i = 4. শর্ত +১, তুলনা (9 > 8? হ্যাঁ) +১, max ← 9 +১। মোট 9, t = 2.
- i = 5. শর্ত +১, তুলনা (6 > 9? না) +১। মোট 11.
- i = 6 — শেষ শর্ত। 6 ≤ 5? মিথ্যা। এই যাচাইটিও একটি ধাপ: মোট 12. লাইন ২ মোট চলল ৫ বার = n বার।
- লাইন ৫। return 9: মোট 13. সূত্র মিলিয়ে দেখুন: 2n + 1 + t = 2·5 + 1 + 2 = 13 ✓
সবচেয়ে ভালো আর সবচেয়ে খারাপ ক্ষেত্র
T(n) একটি মাত্র সংখ্যা নয় — t-এর ওপর নির্ভর করে, আর t নির্ভর করে input কীভাবে সাজানো তার ওপর:
- সবচেয়ে ভালো (best case): প্রথম উপাদানই সর্বোচ্চ, যেমন
[9, 7, 5, 3, 1]. t = 0, T(n) = 2n + 1. - সবচেয়ে খারাপ (worst case): কঠোরভাবে ঊর্ধ্বক্রম, যেমন
[1, 3, 5, 7, 9]— প্রতিবারই নতুন সর্বোচ্চ। t = n − 1, T(n) = 3n.
দুই ক্ষেত্রেই T(n) n-এর সাথে সরলরেখায় বাড়ে। সময়: Θ(n) — প্রতিটি উপাদান অন্তত একবার দেখতেই হয়, আর একবারের বেশি দেখা হয় না। বাড়তি জায়গা: Θ(1) — শুধু max আর i, n যত বড়ই হোক। Best, worst আর average case নিয়ে পুরো আলোচনা পর্ব ৫-এ।
ধ্রুবকগুলো কেন ফেলে দিই
এবার একটি মজার পরীক্ষা। লাইন না গুনে যদি C++-এর প্রতিটি মৌলিক কাজ আলাদা করে গুনি — অ্যারে পড়া একটি, তুলনা একটি, মান রাখা একটি, i++ একটি?
| C++ অংশ | প্রতিবারে কাজ | কতবার | মোট |
|---|---|---|---|
mx = a[0] | পড়া + রাখা = 2 | 1 | 2 |
i = 1 | 1 | 1 | 1 |
i < n | 1 | n | n |
i++ | 1 | n − 1 | n − 1 |
a[i] > mx | পড়া + তুলনা = 2 | n − 1 | 2n − 2 |
mx = a[i] | পড়া + রাখা = 2 | t | 2t |
return mx | 1 | 1 | 1 |
যোগফল: 2 + 1 + n + (n − 1) + (2n − 2) + 2t + 1 = 4n + 1 + 2t — best case 4n + 1, worst case 6n − 1. আগের হিসাবে ছিল 2n + 1 আর 3n. কোনটা ঠিক? দুটোই — শুধু “এক ধাপ” কাকে বলছি তা আলাদা। বাস্তবে আবার compiler কয়েকটি কাজ একসাথে জুড়ে দেয়, CPU একই চক্রে একাধিক নির্দেশ চালায় — আসল ধ্রুবক আরেক রকম হবে।
ধ্রুবকগুলো (2, 3, 4, 6) নির্ভর করে গোনার নিয়ম, ভাষা আর যন্ত্রের ওপর — অ্যালগরিদমের ওপর নয়। যা বদলায় না তা হলো বৃদ্ধির ধরন: n দ্বিগুণ হলে ধাপও মোটামুটি দ্বিগুণ। তাই আমরা ধ্রুবক গুণক আর ছোট পদ ফেলে দিয়ে বলি “ARRAY-MAX চলে Θ(n) সময়ে”। এই ভাষার — Big-O, Ω আর Θ — পূর্ণ গাণিতিক সংজ্ঞা আসছে পর্ব ৪-এ।
আরও গভীরে: সর্বোচ্চ খুঁজতে যেকোনো তুলনা-ভিত্তিক অ্যালগরিদমের অন্তত n − 1টি তুলনা লাগবেই — কারণ সর্বোচ্চ নয় এমন প্রতিটি উপাদানকে অন্তত একবার কোনো তুলনায় “হারতে” হবে, আর প্রতিটি তুলনায় মাত্র একজন হারে। ARRAY-MAX ঠিক n − 1টি তুলনা করে, তাই এটি শুধু Θ(n) নয়, তুলনার হিসাবে একদম সেরা। মজার ব্যাপার: সর্বোচ্চ আর সর্বনিম্ন একসাথে খুঁজতে আলাদা দুই লুপে 2n − 2 তুলনা লাগে, কিন্তু উপাদানগুলো জোড়ায় জোড়ায় মিলিয়ে মাত্র প্রায় ⌈3n/2⌉ − 2 তুলনায় করা যায় — এটিও প্রমাণিতভাবে সেরা।
৮ · কাজের উদাহরণ — ধাপ গুনতে গুনতে max আর Euclid
এবার দুই অ্যালগরিদমকেই আসল কোডে লিখি — সাথে একটি গণনাকারী (counter), যা RAM model-এর হিসাবটিই করে। প্রথমে ARRAY-MAX: প্রতিটি pseudocode লাইন চললে counter এক বাড়ে। আপনার ভাষার ট্যাবটি বেছে নিন, তারপর চালান:
#include <iostream>
#include <vector>
using namespace std;
// ARRAY-MAX with a counter: +1 for every executed pseudocode line
int arrayMax(const vector<int>& a, int& steps, int& t) {
int mx = a[0];
steps = 1; t = 0; // line 1
for (size_t i = 1; i < a.size(); i++) {
steps += 2; // line 2 (test true) + line 3
if (a[i] > mx) { mx = a[i]; steps++; t++; } // line 4
}
steps += 2; // line 2 (final test) + line 5
return mx;
}
int main() {
vector<vector<int>> tests = {{9, 7, 5, 3, 1}, {1, 3, 5, 7, 9},
{4, 8, 2, 9, 6}, {6, 2, 6, 1, 7, 3, 7, 5}};
for (const auto& a : tests) {
int steps, t;
int mx = arrayMax(a, steps, t);
int n = static_cast<int>(a.size());
cout << "n=" << n << " max=" << mx << " t=" << t << " steps=" << steps
<< " 2n+1+t=" << 2 * n + 1 + t << '\n';
}
}আউটপুট:
n=5 max=9 t=0 steps=11 2n+1+t=11
n=5 max=9 t=4 steps=15 2n+1+t=15
n=5 max=9 t=2 steps=13 2n+1+t=13
n=8 max=7 t=1 steps=18 2n+1+t=18
# ARRAY-MAX with a counter: +1 for every executed pseudocode line
def array_max(a):
mx = a[0]
steps, t = 1, 0 # line 1
for i in range(1, len(a)):
steps += 2 # line 2 (test true) + line 3
if a[i] > mx:
mx = a[i]
steps += 1 # line 4
t += 1
steps += 2 # line 2 (final test) + line 5
return mx, steps, t
tests = [[9, 7, 5, 3, 1], [1, 3, 5, 7, 9], [4, 8, 2, 9, 6], [6, 2, 6, 1, 7, 3, 7, 5]]
for a in tests:
mx, steps, t = array_max(a)
n = len(a)
print(f"n={n} max={mx} t={t} steps={steps} 2n+1+t={2 * n + 1 + t}")আউটপুট C++-এর সাথে হুবহু এক:
n=5 max=9 t=0 steps=11 2n+1+t=11
n=5 max=9 t=4 steps=15 2n+1+t=15
n=5 max=9 t=2 steps=13 2n+1+t=13
n=8 max=7 t=1 steps=18 2n+1+t=18
চারটি লাইনেই গোনা ধাপ আর সূত্র 2n + 1 + t হুবহু মিলেছে। প্রথম লাইন best case (11 = 2·5 + 1), দ্বিতীয়টি worst case (15 = 3·5), তৃতীয়টি স্টেপারের instance (13). চতুর্থটি খেয়াল করুন: 6 দুবার আর 7 দুবার আছে, কিন্তু t = 1 — কারণ শর্তটি >, ≥ নয়; সমান মান পেলে max বদলায় না। ≥ লিখলে উত্তর একই থাকত, কিন্তু t আর ধাপ বাড়ত, আর “প্রথম সর্বোচ্চের index” চাইলে ভুল index আসত। একটি অক্ষরের পার্থক্য — এটাই অ্যালগরিদমের নিখুঁততা।
এবার Euclid. পাশাপাশি রাখছি একটি brute force পদ্ধতি: min(a, b) থেকে শুরু করে নিচে নামতে নামতে প্রথম যে সংখ্যা দুটিকেই ভাগ করে। দুটির উত্তর মিলিয়ে দেখা হবে, আর গোনা হবে কে কতবার ভাগশেষ বের করল:
#include <iostream>
#include <algorithm>
using namespace std;
// EUCLID: gcd(a, b) = gcd(b, a mod b); count the mod operations
long long euclid(long long a, long long b, int& divs, bool show) {
divs = 0;
while (b != 0) {
long long r = a % b; divs++;
if (show) cout << " " << a << " = " << a / b << "*" << b << " + " << r << '\n';
a = b; b = r;
}
return a;
}
// brute force: try d = min(a, b), min(a, b) - 1, ..., 1
long long brute(long long a, long long b, long long& tries) {
tries = 0;
for (long long d = min(a, b); d >= 1; d--) {
tries++;
if (a % d == 0 && b % d == 0) return d;
}
return 1;
}
int main() {
long long pairs[][2] = {{252, 105}, {1071, 462}, {89, 55}, {1000000, 999999}};
for (auto& p : pairs) {
bool show = (p[0] == 252);
if (show) cout << "trace of gcd(252, 105):\n";
int divs; long long tries;
long long g = euclid(p[0], p[1], divs, show), gb = brute(p[0], p[1], tries);
cout << "gcd(" << p[0] << ", " << p[1] << ") = " << g << " euclid_divisions=" << divs
<< " brute_tries=" << tries << (g == gb ? "" : " MISMATCH") << '\n';
}
}আউটপুট:
trace of gcd(252, 105):
252 = 2*105 + 42
105 = 2*42 + 21
42 = 2*21 + 0
gcd(252, 105) = 21 euclid_divisions=3 brute_tries=85
gcd(1071, 462) = 21 euclid_divisions=3 brute_tries=442
gcd(89, 55) = 1 euclid_divisions=9 brute_tries=55
gcd(1000000, 999999) = 1 euclid_divisions=2 brute_tries=999999
# EUCLID: gcd(a, b) = gcd(b, a mod b); count the mod operations
def euclid(a, b, show=False):
divs = 0
while b != 0:
r = a % b
divs += 1
if show:
print(f" {a} = {a // b}*{b} + {r}")
a, b = b, r
return a, divs
# brute force: try d = min(a, b), min(a, b) - 1, ..., 1
def brute(a, b):
tries = 0
for d in range(min(a, b), 0, -1):
tries += 1
if a % d == 0 and b % d == 0:
return d, tries
return 1, tries
for x, y in [(252, 105), (1071, 462), (89, 55), (1000000, 999999)]:
show = x == 252
if show:
print("trace of gcd(252, 105):")
g, divs = euclid(x, y, show)
gb, tries = brute(x, y)
tail = "" if g == gb else " MISMATCH"
print(f"gcd({x}, {y}) = {g} euclid_divisions={divs} brute_tries={tries}{tail}")আউটপুট C++-এর সাথে হুবহু এক:
trace of gcd(252, 105):
252 = 2*105 + 42
105 = 2*42 + 21
42 = 2*21 + 0
gcd(252, 105) = 21 euclid_divisions=3 brute_tries=85
gcd(1071, 462) = 21 euclid_divisions=3 brute_tries=442
gcd(89, 55) = 1 euclid_divisions=9 brute_tries=55
gcd(1000000, 999999) = 1 euclid_divisions=2 brute_tries=999999
কোথাও MISMATCH নেই — দুই পদ্ধতির উত্তর সব জোড়ায় মিলেছে। কিন্তু খরচের পার্থক্য দেখুন: শেষ জোড়ায় Euclid দুটি ভাগে শেষ (1000000 = 1·999999 + 1, তারপর 999999 = 999999·1 + 0), আর brute force-কে প্রায় দশ লাখ সংখ্যা পরীক্ষা করতে হলো। দশ লাখের জায়গায় ১৮ অঙ্কের সংখ্যা হলে সেকেন্ডে একশো কোটি চেষ্টা করেও brute force-এর কয়েক দশক লেগে যেত; Euclid তখনো ১০০-এর কম ভাগে শেষ করত।
Euclid-এর খরচ কত? ১৮৪৪ সালে ফরাসি গণিতবিদ Gabriel Lamé প্রমাণ করেন: ভাগের সংখ্যা কখনো ছোট সংখ্যাটির দশমিক অঙ্কের সংখ্যার ৫ গুণের বেশি হয় না। সবচেয়ে খারাপ ক্ষেত্র আসে পাশাপাশি দুটি Fibonacci সংখ্যায় — যেমন (89, 55): প্রতিটি ভাগফল 1, তাই সংখ্যা সবচেয়ে ধীরে ছোট হয়, লাগে ৯টি ভাগ (৫৫ দুই অঙ্কের, সীমা ৫ × ২ = ১০)। সহজ একটি যুক্তিও আছে: দুই ধাপ পরপর বড় সংখ্যাটি অন্তত অর্ধেক হয়ে যায় (কারণ a ≥ b হলে a mod b < a/2 সবসময় সত্য)। তাই ভাগের সংখ্যা O(log min(a, b)), আর বাড়তি জায়গা Θ(1). brute force-এর খরচ O(min(a, b)) — দুটির পার্থক্য log বনাম সংখ্যাটি নিজে।
আরও গভীরে: “brute force O(min(a, b)) — linear, তাহলে তো ভালোই!” — এখানে একটি সূক্ষ্ম ফাঁদ। সংখ্যার ক্ষেত্রে input size হলো অঙ্ক বা bit-এর সংখ্যা d ≈ log₁₀ a, সংখ্যাটির মান নয়। মানের হিসাবে O(a) মানে আকারের হিসাবে O(10d) — exponential! আর Euclid-এর O(log a) হলো O(d) সংখ্যক ভাগ, যা আকারের polynomial. এই পার্থক্যই পর্ব ৫৩-এর knapsack DP-কে “pseudo-polynomial” বানায়, আর পর্ব ৬২-তে বড় সংখ্যার গণিতে (RSA) Euclid ছাড়া চলে না।
৯ · অ্যালগরিদম ডিজাইনের কর্মপ্রবাহ — আট ধাপ
নতুন একটি সমস্যা হাতে পেলে অনেকে সরাসরি কোড লিখতে বসেন — আর আধঘণ্টা পর বোঝেন সমস্যাটাই ভুল বুঝেছিলেন। অভিজ্ঞ প্রোগ্রামার, কনটেস্টের প্রতিযোগী আর ইন্টারভিউয়ের সফল প্রার্থীরা প্রায় একই ক্রমে ভাবেন। একটি বাস্তব সমস্যায় পুরো কর্মপ্রবাহটি দেখুন: ভর্তি পরীক্ষার রোল-তালিকায় কোনো রোল নম্বর দুবার উঠে গেছে কি না?
Input : n টি পূর্ণসংখ্যা (রোল নম্বর), 0 ≤ n ≤ 200000 Output: কোনো মান দুবার বা তার বেশি থাকলে YES, নইলে NO প্রশ্ন: তালিকা কি বদলানো যাবে? মান কত বড়? n = 0 হলে?
[12, 5, 31, 5] → YES [3, 1, 2] → NO [] → NO (খালি তালিকা) [7] → NO (একটি মাত্র) [4, 4, 4] → YES (সব সমান)
for i ← 1 to n − 1
for j ← i + 1 to n
if A[i] = A[j] return YES
return NO
তুলনা: n(n − 1)/2 → n = 200000 হলে প্রায় 2 × 10^10
SORT(A) // O(n log n) for i ← 1 to n − 1 if A[i] = A[i + 1] return YES return NO বিকল্প: hash set — গড়ে O(n) (পর্ব ৩২)
YES দিলে: A[i] = A[i+1] — সত্যিই দুটি সমান মান আছে। duplicate থাকলে: ধরুন x আছে দুবার, সাজানোর পর অবস্থান p < q. p আর q-এর মাঝের প্রতিটি মান ≥ x এবং ≤ x, অর্থাৎ = x. তাই A[p] = A[p+1] — লুপ এটি ধরবেই। ∎
সময় : sort Θ(n log n) + এক পাস Θ(n) = Θ(n log n)
জায়গা: in-place sort হলে O(log n) বাড়তি (recursion);
মূল তালিকা বদলানো না গেলে কপি — Θ(n)
n = 200000: n log₂ n ≈ 3.5 × 10^6 তুলনা — চোখের পলকে
// C++ sort(a.begin(), a.end()); bool dup = adjacent_find(a.begin(), a.end()) != a.end(); # Python dup = len(set(a)) != len(a)
১. ধাপ ২-এর সব উদাহরণ — খালি, একটি, সব সমান ২. ১০০০ বার: এলোমেলো ছোট তালিকা (n ≤ 8, মান 1..5) brute(A) ≠ fast(A) হলে সেই A ছাপো, থামো ৩. n = 200000 দিয়ে সময় মাপো
- বুঝুন। input কী, output কী, সীমা কত — লিখে ফেলুন। n কত বড় হতে পারে, এটিই ঠিক করে কোন গতির অ্যালগরিদম লাগবে। অস্পষ্ট কিছু থাকলে (তালিকা বদলানো যাবে কি না) এখনই জিজ্ঞেস করুন — ইন্টারভিউতে এই প্রশ্নগুলোই প্রথম নম্বর এনে দেয়।
- উদাহরণ। হাতে কয়েকটি instance আর তার উত্তর লিখুন — সাধারণ ক্ষেত্রের পাশাপাশি edge case: খালি তালিকা, একটি উপাদান, সব সমান। এগুলো পরে পরীক্ষার কাজেও লাগবে।
- Brute force. সবচেয়ে সরল, নিশ্চিতভাবে ঠিক পদ্ধতি: প্রতিটি জোড়া তুলনা। সময় Θ(n²) — n = 200000-এ প্রায় দুই হাজার কোটি তুলনা, অনেক বেশি ধীর। তবু এটি ফেলে দেবেন না: পরে দ্রুত সমাধান যাচাইয়ের মাপকাঠি এটাই।
- উন্নত করুন। brute force কোথায় অপচয় করছে? দূরের জোড়াগুলো অকারণে তুলনা হচ্ছে। সাজিয়ে নিলে সমান মান পাশাপাশি চলে আসে — তখন শুধু পাশের জোড়া দেখলেই চলে। (Hash table দিয়ে গড়ে আরও দ্রুত — পর্ব ৩২।)
- প্রমাণ। “উদাহরণে চলেছে” প্রমাণ নয়। দুই দিক দেখাতে হবে: YES দিলে সত্যিই duplicate আছে, আর duplicate থাকলে অ্যালগরিদম YES দেবেই। দ্বিতীয় দিকের মূল যুক্তি: দুই x-এর মাঝের সব মান x-এর সমান। লুপের প্রমাণের আনুষ্ঠানিক হাতিয়ার loop invariant — পর্ব ৩-এ।
- বিশ্লেষণ। সময় Θ(n log n) — sort-ই প্রধান খরচ, পরের পাসটি Θ(n). জায়গার হিসাবও লিখুন: মূল তালিকা বদলানো নিষেধ হলে কপির জন্য Θ(n) লাগবে। ধাপ ১-এর সীমার সাথে মিলিয়ে দেখুন — ৩৫ লাখের মতো তুলনা, যথেষ্ট দ্রুত।
- কোড। এবার — এবং কেবল এবার — কোড। যেখানে লাইব্রেরি নির্ভরযোগ্য, সেখানে নিজে চাকা বানাবেন না: C++-এ
sort+adjacent_find, Python-এset. খেয়াল করুন, Python-এর সংস্করণটি আসলে অন্য অ্যালগরিদম (hash) — একই সমস্যার আরেকটি সমাধান। - পরীক্ষা। প্রথমে ধাপ ২-এর edge case, তারপর stress testing: হাজারটি এলোমেলো ছোট তালিকায় brute force আর দ্রুত সমাধান মিলিয়ে দেখা — একটি অমিলই বাগের ঠিকানা দেখিয়ে দেয়। শেষে সর্বোচ্চ n দিয়ে সময় মাপা। এই আট ধাপ পুরো কোর্সে বারবার ফিরে আসবে।
পরীক্ষা ও ইন্টারভিউতে — বিশ্ববিদ্যালয়, চাকরি বা কোডিং ইন্টারভিউর পরীক্ষায় এই ধরনের প্রশ্ন আসতে পারে:
- অ্যালগরিদমের বৈশিষ্ট্যগুলো লিখুন। Knuth-এর পাঁচটি: finiteness, definiteness, input (০ বা বেশি), output (১ বা বেশি), effectiveness — প্রতিটির একটি উদাহরণসহ।
- অ্যালগরিদম আর প্রোগ্রামের পার্থক্য কী? অ্যালগরিদম ভাষা-নিরপেক্ষ ধারণা এবং অবশ্যই থামে; প্রোগ্রাম নির্দিষ্ট ভাষায় লেখা রূপ, আর OS বা সার্ভারের মতো প্রোগ্রাম ইচ্ছা করে কখনো না-ও থামতে পারে।
- RAM model-এর অনুমানগুলো কী, আর কী উপেক্ষা করে? একটি প্রসেসর, মৌলিক কাজ ও যেকোনো মেমরি ঘরে প্রবেশ স্থির সময়, সীমিত word size; উপেক্ষা করে cache, মেমরির স্তর আর সমান্তরালতা।
for i ← 2 to nলাইনটি কতবার চলে? n বার — n − 1 বার শর্ত সত্য, আর শেষে একবার মিথ্যা।- gcd(a, b)-এর জটিলতা কত, worst case কখন? O(log min(a, b))টি ভাগ; পাশাপাশি দুটি Fibonacci সংখ্যায় (Lamé-র উপপাদ্য)।
১০ · AI-এর যুগে — অ্যালগরিদম বোঝা কেন আরও জরুরি
আজ যেকোনো AI চ্যাটবটকে “GCD-র কোড দাও” বললে কয়েক সেকেন্ডে পেয়ে যাবেন। তাহলে অ্যালগরিদম শেখার মানে কী? মানে হলো — AI লেখে, কিন্তু সেটি ঠিক কি না, কত দ্রুত, কোন input-এ ভেঙে পড়বে — এগুলো যাচাইয়ের দায় আপনার। আর ঠিক এই পর্বের বিষয়গুলোতেই AI নিয়মিত ভুল করে, খুব আত্মবিশ্বাসের সাথে।
এই বিষয়ে AI-এর যে ভুলগুলো বারবার দেখা যায়
| AI যা বলে বা লেখে | আসল সমস্যা | কীভাবে ধরবেন |
|---|---|---|
| “লুপের শর্ত n − 1 বার যাচাই হয়” | শেষের মিথ্যা যাচাইটি বাদ — off-by-one | n = 1 বা 2 দিয়ে হাতে গুনুন |
1-based pseudocode হুবহু C++-এ: for (i = 1; i <= n; i++) a[i] | a[0] বাদ, a[n] সীমার বাইরে | সবচেয়ে ছোট ও বড় index বসিয়ে দেখুন; sanitizer চালান |
“Python-এ x in list O(1)” | list-এ এটি O(n); শুধু set/dict-এ গড়ে O(1) | এক লাইনের ভেতরের লুকানো লুপ খুঁজুন |
| “brute force GCD O(n), তাই যথেষ্ট দ্রুত” | n এখানে মান, আকার নয় — অঙ্কের হিসাবে exponential | জিজ্ঞেস করুন: “n দিয়ে ঠিক কী বোঝাচ্ছ?” |
| “এই greedy পদ্ধতি সবসময় সেরা উত্তর দেয়” — প্রমাণ ছাড়া | কয়েকটি উদাহরণে ঠিক, কোনো এক instance-এ ভুল | brute force-এর সাথে stress test |
AI-কে শিক্ষকের মতো ব্যবহার করুন, উত্তর-দাতার মতো নয়। নিচের প্রম্পটে খেয়াল করুন — AI-কে হাতে চালাতে আর নিজের দাবির বিরুদ্ধে উদাহরণ খুঁজতে বলা হচ্ছে:
AI-এর দেওয়া অ্যালগরিদম বা complexity হাতে পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলো আপনাকে কোড-লেখক থেকে অ্যালগরিদম-চিন্তক বানাবে।
প্র ০১“অ্যালগরিদম” শব্দটি একজন মানুষের নাম থেকে এল কীভাবে — আর তাঁর আসল অবদানটা কী ছিল?
নবম শতকে বাগদাদ ছিল জ্ঞানচর্চার বড় কেন্দ্র, আর সেখানে কাজ করতেন মুহাম্মদ ইবনে মুসা আল-খোয়ারিজমি। তাঁর দুটি বই পরবর্তী হাজার বছরের গণিতকে বদলে দেয়।
- হিন্দু-আরবি সংখ্যায় হিসাব: ভারতীয় গণিতবিদদের উদ্ভাবিত দশটি অঙ্ক (০ সহ) আর স্থানীয় মান দিয়ে যোগ, বিয়োগ, গুণ, ভাগ করার নিয়ম তিনি ধাপে ধাপে লেখেন। দ্বাদশ শতকে এর ল্যাটিন অনুবাদ ইউরোপে পৌঁছায়, যার শুরুর কথা “Algoritmi de numero Indorum”. এই নিয়মে হিসাব করাকে বলা হতো “algorism” — সেখান থেকেই আজকের “algorithm”.
- বীজগণিত: সমীকরণ সমাধানের পদ্ধতি নিয়ে তাঁর আরেকটি বইয়ের নামে ছিল “al-jabr” — যা থেকে “algebra”.
এর তাৎপর্য বোঝা যায় রোমান সংখ্যার কথা ভাবলে। MCMXCVIII-কে XLVII দিয়ে গুণ করার কোনো সহজ ধাপে-ধাপে নিয়ম নেই; তাই মধ্যযুগের ইউরোপে হিসাব হতো অ্যাবাকাস দিয়ে। স্থানীয় মানের অঙ্কে কিন্তু খাতা-কলমেই যান্ত্রিকভাবে গুণ করা যায় — যেকোনো মানুষ নিয়ম মেনে চললেই ঠিক উত্তর পাবেন। ১২০২ সালে Fibonacci তাঁর Liber Abaci বইয়ে এই পদ্ধতি ইউরোপের ব্যবসায়ীদের কাছে জনপ্রিয় করেন, আর কয়েক শতক ধরে “algorist” (খাতা-কলমের নিয়ম) আর “abacist” (অ্যাবাকাস)-দের মধ্যে প্রতিযোগিতা চলে। শেষে খাতা-কলম জেতে।
খেয়াল করুন, আল-খোয়ারিজমির আসল অবদান কোনো একক কৌশল নয়, বরং একটি দৃষ্টিভঙ্গি: হিসাবকে এমন স্পষ্ট নিয়মে ভাঙা যে মেধা ছাড়াই, শুধু নিয়ম মেনে, যে কেউ ঠিক ফল পান। এটিই Knuth-এর definiteness আর effectiveness — এবং ঠিক এই কারণেই পরে যন্ত্রকে দিয়ে হিসাব করানো সম্ভব হলো।
মূল উপলব্ধি: ভালো উপস্থাপন (representation) অ্যালগরিদমকে সম্ভব করে। রোমান সংখ্যা থেকে দশমিক সংখ্যায় যাওয়া যেমন গুণকে সহজ করেছিল, তেমনি এই কোর্সে বারবার দেখবেন — ঠিক ডেটা স্ট্রাকচার বাছলে কঠিন সমস্যা হঠাৎ সহজ হয়ে যায়।
প্র ০২যেকোনো সুনির্দিষ্ট প্রশ্নের জন্য কি একটি অ্যালগরিদম থাকতেই হবে? নাকি কিছু সমস্যা কম্পিউটার কোনোদিনই সমাধান করতে পারবে না?
বিশ শতকের শুরুতে অনেক গণিতবিদ বিশ্বাস করতেন — সব সুনির্দিষ্ট গাণিতিক প্রশ্নের উত্তর একটি যান্ত্রিক পদ্ধতিতে বের করা সম্ভব। ১৯২৮ সালে David Hilbert আর Wilhelm Ackermann একটি প্রশ্ন সামনে আনেন, যার নাম Entscheidungsproblem (“সিদ্ধান্তের সমস্যা”): এমন কোনো পদ্ধতি কি আছে, যা যেকোনো যৌক্তিক বক্তব্য দেখে বলে দেবে সেটি প্রমাণযোগ্য কি না?
প্রশ্নটির উত্তর দিতে প্রথমে দরকার ছিল “পদ্ধতি” বা “অ্যালগরিদম”-এর একটি গাণিতিক সংজ্ঞা — কারণ “এমন অ্যালগরিদম নেই” প্রমাণ করতে হলে সব সম্ভাব্য অ্যালগরিদমের কথা একসাথে বলতে হয়। ১৯৩৬ সালে Alonzo Church (lambda calculus দিয়ে) আর Alan Turing (তাঁর কাল্পনিক যন্ত্র দিয়ে) আলাদাভাবে সেই সংজ্ঞা দেন, আর দুজনেই দেখান: উত্তর “না”।
Turing-এর যুক্তির সবচেয়ে পরিচিত রূপ halting problem: এমন কোনো প্রোগ্রাম H কি লেখা যায়, যা যেকোনো প্রোগ্রাম P আর input x দেখে বলবে P(x) কখনো থামবে কি না? ধরুন যায়। তাহলে একটি দুষ্টু প্রোগ্রাম D বানাই: D নিজের কোড নিয়ে H-কে জিজ্ঞেস করে, “আমি কি থামব?” H “থামবে” বললে D চিরকাল লুপে ঘোরে; “থামবে না” বললে D সাথে সাথে থেমে যায়। H যা-ই বলুক, ভুল হয়। তাই H থাকতেই পারে না।
এর ব্যবহারিক মানে বিশাল। কোনো tool কখনো নিখুঁতভাবে বলতে পারবে না আপনার কোডে infinite loop আছে কি না, বা কোনো ভ্যারিয়েবল কখনো শূন্য হবে কি না — পরে Rice-এর উপপাদ্য দেখায়, প্রোগ্রামের আচরণ নিয়ে প্রায় যেকোনো অ-তুচ্ছ প্রশ্নই এমন। তাই compiler-এর warning, antivirus আর static analyzer সবাই হয় কিছু বাগ মিস করে, নয়তো মাঝে মাঝে ভুল সতর্কবার্তা দেয়। আর AI-ও এই সীমার বাইরে নয়।
মূল উপলব্ধি: “অ্যালগরিদম নেই” আর “দ্রুত অ্যালগরিদম জানা নেই” — দুটি আলাদা কথা। প্রথমটি Turing-এর জগৎ (গণনাযোগ্যতা), দ্বিতীয়টি P বনাম NP-র জগৎ (পর্ব ৬৪)। একজন ভালো প্রকৌশলী সমস্যা দেখে আগে বোঝেন সেটি কোন জগতের।
প্র ০৩RAM model তো cache আর একাধিক core-কে উপেক্ষা করে — তাহলে এই “ভুল” মডেল দিয়ে হিসাব করার মানে কী?
পরিসংখ্যানবিদ George Box-এর একটি বিখ্যাত কথা আছে: “সব মডেলই ভুল, কিন্তু কিছু মডেল কাজের।” RAM model ঠিক তেমনই।
কেন কাজের: বেশিরভাগ ক্ষেত্রে বৃদ্ধির হারের পার্থক্য এত বড় যে বাস্তবের খুঁটিনাটি সেটি উল্টাতে পারে না। n = দশ লাখ হলে n² অ্যালগরিদম n log n-এর চেয়ে প্রায় পঞ্চাশ হাজার গুণ বেশি কাজ করে। কোনো cache-এর সুবিধা এই ব্যবধান মেটাতে পারে না। তাই RAM model-এ ভালো প্রমাণিত অ্যালগরিদম প্রায় সবসময় বাস্তবেও ভালো।
কোথায় ভেঙে পড়ে: যখন দুই অ্যালগরিদমের বৃদ্ধির হার এক। ধরুন একটি array আর একটি linked list-এর সব উপাদান যোগ করছেন — দুটিই Θ(n). কিন্তু array-র উপাদান মেমরিতে পাশাপাশি থাকে, CPU একবারে একগুচ্ছ এনে cache-এ রাখে; linked list-এর প্রতিটি node মেমরির এলোমেলো জায়গায়, প্রতিবার ধীর মেইন মেমরিতে যেতে হয়। বাস্তবে পার্থক্য অনেক গুণ হতে পারে, অথচ RAM model-এ দুটি একই (পর্ব ১০ ও ১২)।
বিজ্ঞানীরা কী করলেন: প্রয়োজন অনুযায়ী আরও সূক্ষ্ম মডেল বানালেন। ১৯৮৮ সালে Alok Aggarwal আর Jeffrey Vitter-এর external memory (I/O) model গোনে ডিস্ক আর মেমরির মধ্যে কতগুলো ব্লক আনা-নেওয়া হলো — ডেটাবেসের B-tree (পর্ব ৩০) কেন এত ভালো কাজ করে, তা এই মডেলেই সবচেয়ে স্পষ্ট বোঝা যায় (B-tree নিজে অবশ্য এই মডেলের আগেই, ১৯৭০-এর দশকের শুরুতে উদ্ভাবিত)। ১৯৯৯ সালে Frigo, Leiserson, Prokop ও Ramachandran দেখান “cache-oblivious” অ্যালগরিদম — cache-এর আকার না জেনেও যা সব স্তরের cache ভালোভাবে ব্যবহার করে। আর একাধিক প্রসেসরের জন্য আছে PRAM-এর মতো সমান্তরাল মডেল।
মূল উপলব্ধি: মডেল হলো মানচিত্র — প্রশ্ন অনুযায়ী মানচিত্র বাছতে হয়। প্রথমে RAM model দিয়ে বৃদ্ধির হার ঠিক করুন; দুটি সমাধান সমান হলে তখন cache, মেমরির বিন্যাস আর বাস্তব মাপজোখ (benchmark) দেখুন।
প্র ০৪Ada Lovelace-কে “প্রথম প্রোগ্রামার” বলা হয় — কথাটি কতটা ঠিক, আর AI-এর যুগে তাঁর কোন ভাবনাটি আবার আলোচনায়?
১৮৪২ সালে ইতালীয় প্রকৌশলী Luigi Menabrea, Charles Babbage-এর প্রস্তাবিত Analytical Engine নিয়ে ফরাসি ভাষায় একটি প্রবন্ধ লেখেন। Ada Lovelace সেটি ইংরেজিতে অনুবাদ করেন এবং ১৮৪৩ সালে প্রকাশ করেন — সাথে নিজের টীকা, যা মূল প্রবন্ধের প্রায় তিন গুণ লম্বা। শেষ টীকা “Note G”-তে ছিল যন্ত্রটি দিয়ে Bernoulli সংখ্যা বের করার ধাপে-ধাপে পরিকল্পনা: কোন ধাপে কোন ঘরের মান নিয়ে কোন কাজ, কোন ফল কোথায় যাবে — একটি টেবিলে সাজানো।
“প্রথম” নিয়ে বিতর্ক: ইতিহাসবিদেরা দেখিয়েছেন, Babbage নিজেও আগে তাঁর যন্ত্রের জন্য কিছু হিসাবের পরিকল্পনা লিখেছিলেন, যদিও প্রকাশ করেননি। আবার যন্ত্রটি কখনো পুরো তৈরিই হয়নি, তাই কোনো পরিকল্পনাই আসলে চলেনি। তাই সতর্ক বক্তব্য হলো: Note G যন্ত্রের জন্য লেখা প্রথম প্রকাশিত বিস্তারিত অ্যালগরিদমগুলোর একটি। Note G-তে একটি ছোট ভুলও ছিল বলে পরে দেখা গেছে — প্রথম প্রোগ্রামের সাথে প্রথম বাগও!
তাঁর আসল দূরদৃষ্টি: Lovelace লেখেন, যন্ত্রটি শুধু সংখ্যা নয়, যেকোনো প্রতীক নিয়ে কাজ করতে পারে — নিয়ম জানা থাকলে সুরও রচনা করতে পারে। অর্থাৎ কম্পিউটার “হিসাবযন্ত্র” নয়, “প্রতীক-প্রক্রিয়াকরণ যন্ত্র” — আজকের সব সফটওয়্যারের মূল ধারণা।
AI-এর সংযোগ: একই টীকায় তিনি লেখেন, যন্ত্রটি নিজে থেকে কিছু “উদ্ভাবন” করে না — আমরা যা করতে বলি, শুধু তা-ই করে। ১৯৫০ সালে Alan Turing তাঁর বিখ্যাত প্রবন্ধ “Computing Machinery and Intelligence”-এ এই আপত্তিকে “Lady Lovelace’s Objection” নাম দিয়ে আলোচনা করেন। আজকের বড় ভাষা-মডেলগুলো কি সত্যিই নতুন কিছু তৈরি করে, নাকি দেখা জিনিসের পুনর্বিন্যাস — সেই বিতর্ক আসলে Lovelace-এর প্রশ্নেরই নতুন রূপ।
মূল উপলব্ধি: অ্যালগরিদমের ধারণা যন্ত্রের চেয়ে পুরোনো, এবং যন্ত্র ছাড়াও টিকে থাকে। Lovelace এমন একটি যন্ত্রের জন্য লিখেছিলেন, যা কখনো চলেনি — তবু তাঁর পরিকল্পনা আজও পড়ে বোঝা যায়, কারণ অ্যালগরিদম যন্ত্রের নয়, যুক্তির জিনিস।
প্র ০৫২৩০০ বছরের পুরোনো Euclid-এর অ্যালগরিদম আজও কোথায় কোথায় চলছে, আর এর চেয়ে ভালো কিছু কি আবিষ্কার হয়নি?
আপনি প্রতিদিন না জেনেই Euclid-এর অ্যালগরিদম ব্যবহার করেন।
- এনক্রিপশন: RSA-র মতো public-key পদ্ধতিতে key তৈরির সময় একটি সংখ্যার “modular inverse” লাগে — অর্থাৎ এমন d, যাতে
e·d mod φ = 1. এটি বের করা হয় extended Euclid দিয়ে, যা gcd-এর সাথে সাথেa·x + b·y = gcd(a, b)সমীকরণের x আর y-ও দেয় (পর্ব ৬২)। শত শত অঙ্কের সংখ্যাতেও এটি কয়েকশো ধাপে শেষ — Lamé-র সীমার জন্যই। - ভগ্নাংশ লঘিষ্ঠ করা: 252/105-কে 12/5 বানাতে দুটিকেই gcd = 21 দিয়ে ভাগ। Python-এর
fractionsমডিউল ঠিক এটিই করে। - লাইব্রেরি: C++17 থেকে
std::gcd, Python-এmath.gcd— কনটেস্টে প্রতিদিনের হাতিয়ার।
ইতিহাসে এর জায়গা: ১৮৪৪ সালে Gabriel Lamé যখন প্রমাণ করেন ভাগের সংখ্যা ছোট সংখ্যার অঙ্কসংখ্যার পাঁচ গুণের বেশি নয়, সেটিকে প্রায়ই কোনো অ্যালগরিদমের worst case নিয়ে প্রথম দিকের কঠোর বিশ্লেষণগুলোর একটি বলা হয় — আর Fibonacci সংখ্যার প্রথম দিকের ব্যবহারিক প্রয়োগগুলোরও একটি। অর্থাৎ “অ্যালগরিদম বিশ্লেষণ” বিষয়টির জন্মও অনেকটা এই অ্যালগরিদমকে ঘিরে।
উন্নতি কি হয়নি? হয়েছে, তবে মূল ধারণা একই। ১৯৬৭ সালে Josef Stein-এর binary GCD ভাগের বদলে শুধু বিয়োগ আর bit সরানো (২ দিয়ে ভাগ) ব্যবহার করে — যেসব যন্ত্রে ভাগ ধীর, সেখানে দ্রুত। হাজার হাজার অঙ্কের সংখ্যার জন্য আছে Lehmer-এর কৌশল আর আরও উন্নত “half-GCD” পদ্ধতি, যেগুলো বড় সংখ্যার গণিতের লাইব্রেরিতে ব্যবহার হয়। কিন্তু word-আকারের সংখ্যায় সাধারণ Euclid এখনো সহজ, দ্রুত আর যথেষ্ট।
মূল উপলব্ধি: ভালো অ্যালগরিদমের তিনটি গুণ — সহজ, প্রমাণযোগ্যভাবে ঠিক, আর প্রমাণযোগ্যভাবে দ্রুত। Euclid-এর তিনটিই আছে, তাই তা ২৩০০ বছর টিকে আছে। এই কোর্সে যত অ্যালগরিদম দেখবেন, প্রতিটিকে এই তিন প্রশ্নে মেপে দেখুন।
অনুশীলন
খাতা-কলম নিয়ে বসুন। প্রথমে নিজে করুন, তারপর উত্তর মেলান।
-
হাতে trace করুন: EUCLID(270, 192) চালান। প্রতিটি ধাপে a, b, r লিখুন। মোট কয়টি mod লাগল, আর gcd কত?
- 270 = 1 × 192 + 78 → (a, b) = (192, 78)
- 192 = 2 × 78 + 36 → (78, 36)
- 78 = 2 × 36 + 6 → (36, 6)
- 36 = 6 × 6 + 0 → (6, 0) → থামা
মোট ৪টি mod, gcd = 6. যাচাই: 270 = 6 × 45, 192 = 6 × 32, আর 45 ও 32-এর কোনো সাধারণ গুণনীয়ক নেই। Lamé-র সীমা: ছোট সংখ্যা 192 তিন অঙ্কের, তাই ভাগ ≤ ১৫ — আমাদের ৪টি সেই সীমার অনেক নিচে।
-
ধাপ গুনুন: নিচের SUM অ্যালগরিদমের প্রতিটি লাইন কতবার চলে লিখুন, আর সব ck = 1 ধরে T(n) বের করুন। ARRAY-MAX-এর মতো এখানে best আর worst case আলাদা কি?
SUM(A, n) 1 s ← 0 2 for i ← 1 to n 3 s ← s + A[i] 4 return sলাইন ১: ১ বার। লাইন ২: n + 1 বার (i = 1…n-এ সত্য, i = n + 1-এ মিথ্যা)। লাইন ৩: n বার। লাইন ৪: ১ বার। মোট
T(n) = 1 + (n + 1) + n + 1 = 2n + 3.এখানে best আর worst case একই — কোনো শর্তসাপেক্ষ লাইন নেই, তাই input-এর মান যা-ই হোক, ধাপ একই। ARRAY-MAX-এ লাইন ৪ শর্তের ওপর নির্ভর করত বলে t এসেছিল। সময় Θ(n), বাড়তি জায়গা Θ(1). (লক্ষ করুন: এখানে লুপ 1 থেকে, তাই n + 1; ARRAY-MAX-এ 2 থেকে, তাই n.)
-
অ্যালগরিদম, নাকি নয়? Knuth-এর কোন শর্ত ভাঙছে (বা ভাঙছে কি না জানা নেই) বলুন — (ক) “একটি ধনাত্মক পূর্ণসংখ্যা n নিন; n ≠ 1 যতক্ষণ, জোড় হলে n ← n/2, বিজোড় হলে n ← 3n + 1” (খ) “ডাল ভালোভাবে সেদ্ধ হলে নামিয়ে ফেলুন” (গ) “একটি পূর্ণসংখ্যা x নিন; x ← x + 1 করতে থাকুন”
(ক) এটি Collatz-এর পদ্ধতি। প্রতিটি ধাপ স্পষ্ট ও করা সম্ভব, কিন্তু সসীমতা অজানা — সব ধনাত্মক n-এ এটি ১-এ পৌঁছায় কি না, আজও কেউ প্রমাণ করতে পারেননি (অসংখ্য সংখ্যায় পরীক্ষা করে প্রতিবার থেমেছে)। তাই এটি অ্যালগরিদম কি না, সেটাই একটি খোলা গাণিতিক প্রশ্ন। শিক্ষা: “থামবে কি না” প্রমাণ করা সবসময় সহজ নয়।
(খ) স্পষ্টতা ভাঙে — “ভালোভাবে সেদ্ধ” কতক্ষণ বা কোন অবস্থা, দুজন দুরকম বুঝবেন। “ফুটে ওঠার পর ২০ মিনিট” লিখলে স্পষ্ট হয়।
(গ) সসীমতা ভাঙে — কখনো থামে না, আর কোনো output-ও দেয় না।
-
কোড লিখুন: দুটি ধনাত্মক পূর্ণসংখ্যার লসাগু (LCM) বের করার pseudocode লিখুন, EUCLID ব্যবহার করে। তারপর C++ বা Python-এ লিখুন।
a = 1000000,b = 999999হলে কী সাবধানতা লাগবে?মূল সম্পর্ক:
gcd(a, b) × lcm(a, b) = a × b. তাই:LCM(a, b) 1 g ← EUCLID(a, b) 2 return (a / g) × b // আগে ভাগ, পরে গুণlong long lcm_ll(long long a, long long b) { long long g = gcd_euclid(a, b); // the EUCLID loop shown above return a / g * b; // divide first: avoids overflow }Python-এ:
a // g * b(বা সরাসরিmath.lcm, Python 3.9 থেকে)। সাবধানতা: আগেa * bগুণ করলে 1000000 × 999999 ≈ 1012 — ৩২-bitint-এর সীমা (প্রায় 2.1 × 109) ছাড়িয়ে যায়। আগে ভাগ করলে মাঝের মান ছোট থাকে, আরlong longব্যবহার করলে ফল 999999000000 নিরাপদে আঁটে। Python-এর int-এর সীমা নেই, তবু আগে ভাগ করা ভালো অভ্যাস। খরচ: EUCLID-এর O(log min(a, b)) + একটি ভাগ ও একটি গুণ।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমনতুন সমস্যা হাতে পেলে অ্যালগরিদম ডিজাইনের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- সমস্যা বোঝা আর হাতে কয়েকটি উদাহরণ (edge case সহ) লেখা
- সবচেয়ে সরল brute force সমাধান ভাবা
- অপচয় খুঁজে উন্নত ধারণা বের করা
- উন্নত পদ্ধতির শুদ্ধতা প্রমাণ করা
- সময় ও জায়গার complexity বিশ্লেষণ করা
- কোড লেখা আর brute force-এর সাথে stress test করা
একটি পদ্ধতির প্রতিটি ধাপ স্পষ্ট ও করা সম্ভব, কিন্তু কিছু input-এ এটি কখনো থামে না। Knuth-এর কোন শর্ত এটি ভাঙছে?
- Finiteness (সসীমতা)
- Definiteness (স্পষ্টতা)
- Effectiveness (কার্যকারিতা)
- Input
RAM model-এ নিচের কোনটিকে এক একক (স্থির) সময়ের কাজ ধরা হয় না?
- দুটি word-আকারের পূর্ণসংখ্যা যোগ করা
- অ্যারের i-তম ঘর পড়া
- n টি উপাদানের একটি অ্যারে সাজানো
- দুটি word-আকারের সংখ্যা তুলনা করা
Pseudocode-এ for i ← 2 to n লুপের শর্ত-যাচাই (শেষবার মিথ্যা হওয়াসহ) মোট কতবার হয়? (n ≥ 2)
- n − 2
- n − 1
- n
- n + 1
Euclid-এর অ্যালগরিদমে gcd(48, 18) বের করতে কয়টি ভাগশেষ (mod) হিসাব লাগে?
- ২টি
- ৩টি
- ৪টি
- ৬টি
“[5, 2, 7] অ্যারের সবচেয়ে বড় মান খোঁজা” — এটি সর্বোচ্চ-খোঁজার সমস্যার সাপেক্ষে কী?
- একটি অ্যালগরিদম
- একটি প্রোগ্রাম
- সমস্যাটির সাধারণ বর্ণনা
- সমস্যাটির একটি instance
দুজন শিক্ষার্থী একই সর্বোচ্চ-খোঁজার অ্যালগরিদমের worst-case ধাপ গুনে পেলেন 3n আর 6n − 1. এর সবচেয়ে ভালো ব্যাখ্যা কোনটি?
- একজনের হিসাব অবশ্যই ভুল
- ধাপ গোনার নিয়ম আলাদা; দুটোই Θ(n), পার্থক্য শুধু ধ্রুবকে
- 6n − 1 মানে অ্যালগরিদমটি আসলে n²
- RAM model এখানে প্রযোজ্য নয়
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- অ্যালগরিদম = input থেকে output-এ পৌঁছানোর সুনির্দিষ্ট, সসীম ধাপের তালিকা; Knuth-এর পাঁচ শর্ত: finiteness, definiteness, input, output, effectiveness.
- সমস্যা বলে কী চাই, instance একটি নির্দিষ্ট input, অ্যালগরিদম বলে কীভাবে, প্রোগ্রাম সেটিকে কোনো ভাষায় লেখে; সঠিক মানে প্রতিটি instance-এ ঠিক।
- Euclid:
gcd(a, b) = gcd(b, a mod b), O(log min(a, b))টি ভাগ — প্রায় ২৩০০ বছরের পুরোনো, আজও এনক্রিপশনে। - Pseudocode-এর নিয়ম আগে ঠিক করুন (এখানে 1-based,
←, indentation); 0-based ভাষায় অনুবাদে index আর লুপের সীমা আলাদা করে মেলান। - RAM model-এ প্রতিটি লাইন কতবার চলে গুনুন (লুপের শেষ মিথ্যা শর্তসহ): ARRAY-MAX =
2n + 1 + t; ধ্রুবক গোনার নিয়মে বদলায়, বৃদ্ধির হার Θ(n) বদলায় না।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ০৩ · অ্যালগরিদম ঠিক কি না — loop invariant আর inductionপরবর্তী পর্ব“কয়েকটি উদাহরণে চলে” থেকে “সব input-এ ঠিক” প্রমাণে।
- পর্ব ০৪ · বৃদ্ধির হার আর Big-O, Ω, Θএই পর্বের সেতুধ্রুবক কেন ফেলে দিই — তার পূর্ণ গাণিতিক ভাষা।
- ডেটা স্ট্রাকচার ও অ্যালগরিদম · কোর্স হোম৭২টি পর্বপুরো কোর্সের রোডম্যাপ — নয়টি মডিউল।
- C++ প্রোগ্রামিং · STL Algorithmপাশের কোর্স
sort,adjacent_find,std::gcd-এর মতো তৈরি অ্যালগরিদম ঠিকভাবে ব্যবহার। - Python প্রোগ্রামিং · Python দিয়ে অ্যালগরিদম ও কনটেস্টপাশের কোর্সPython-এ অ্যালগরিদম লেখার কৌশল আর সাধারণ ফাঁদ।