সঠিকতা প্রমাণ — লুপ ইনভেরিয়েন্ট ও ইনডাকশন
এই পাঠে যা শিখবেন
- লুপ ইনভেরিয়েন্ট কী এবং কেন এটি সঠিকতা প্রমাণের একটি শক্তিশালী টুল
- Initialization / Maintenance / Termination — ইনভেরিয়েন্ট-ভিত্তিক প্রমাণের তিনটি ধাপ
- ইনসার্শন সর্টের জন্য একটি সম্পূর্ণ, রিগোরাস সঠিকতা প্রমাণ
- কীভাবে প্রমাণের "Maintenance" ধাপ কোডে সত্যিকারের
assertদিয়ে verify করা যায়
১ · লুপ ইনভেরিয়েন্ট কী, এবং ইনডাকশনের সাথে সম্পর্ক
একটি অ্যালগরিদম "সঠিক" — এই দাবিটি শুধু কয়েকটি উদাহরণে সঠিক আউটপুট দেখে প্রমাণ করা যায় না (উদাহরণে ঠিক থাকলেও অন্য কোনো ইনপুটে ভুল হতে পারে)। প্রয়োজন একটি সাধারণ (general) প্রমাণ, যা সব বৈধ ইনপুটের জন্য একসাথে কাজ করে। লুপ-ভিত্তিক অ্যালগরিদমের জন্য এই কাজে ব্যবহৃত হয় একটি লুপ ইনভেরিয়েন্টLoop invariantএমন একটি প্রপার্টি (দাবি) যা লুপের প্রতিটি ইটারেশন শুরু হওয়ার আগমুহূর্তে সত্য থাকে — লুপ ঠিক কতবার চলবে তা নির্বিশেষে।।
লুপ ইনভেরিয়েন্ট দিয়ে সঠিকতা প্রমাণ করা গাণিতিক ইনডাকশনের সাথে হুবহু সমান্তরাল — শুধু "$n$-তম ধাপ" এর জায়গায় "লুপের $k$-তম ইটারেশন" বসে:
প্রথম ইটারেশন শুরু হওয়ার আগে ইনভেরিয়েন্ট সত্য — ইনডাকশনের base case-এর সমতুল্য।
যদি ইনভেরিয়েন্ট কোনো ইটারেশন শুরুর আগে সত্য থাকে, তাহলে পরবর্তী ইটারেশন শুরুর আগেও সত্য থাকবে — ইনডাকশনের inductive step-এর সমতুল্য।
লুপ শেষ হলে (কোনো একটি নির্দিষ্ট শর্তে), ইনভেরিয়েন্টটি — লুপ-শেষের শর্তের সাথে মিলিয়ে — অ্যালগরিদমের সামগ্রিক সঠিকতা প্রমাণ করে।
এই তিনটি ধাপ একসাথে প্রমাণ করে যে ইনভেরিয়েন্টটি লুপের প্রতিটি ইটারেশনের শুরুতে সত্য (Initialization + Maintenance = ইনডাকশন দ্বারা সব ইটারেশনে সত্য), এবং লুপ শেষে এই সত্যতা থেকেই কাঙ্ক্ষিত ফলাফল বেরিয়ে আসে (Termination)।
২ · উদাহরণ অ্যালগরিদম — ইনসার্শন সর্ট
ইনসার্শন সর্ট ইতিমধ্যে DSA কোর্সে কীভাবে কাজ করে ও ইমপ্লিমেন্ট করতে হয় তা কভার করা হয়েছে — এখানে আমরা সরাসরি এর রিগোরাস সঠিকতা প্রমাণে যাচ্ছি। নিচে অ্যালগরিদমের সিউডোকোড দেওয়া হলো (0-ইনডেক্সিং-এ, M1/L02-এর কনভেনশন অনুসরণ করে) — এটি শুধু ব্যাখ্যামূলক টেক্সট, সরাসরি চালানো যায় না:
INSERTION-SORT(A)
for j ← 1 to length(A) - 1
key ← A[j]
i ← j - 1
while i ≥ 0 and A[i] > key
A[i + 1] ← A[i]
i ← i - 1
A[i + 1] ← key
ধারণাটি সহজ: প্রতিটি ইটারেশনে A[j]-কে "key" হিসেবে ধরা হয়, এবং ইতিমধ্যে সাজানো অংশ
$A[0 \,..\, j-1]$-এর মধ্যে key-এর সঠিক অবস্থানে সেটি বসিয়ে দেওয়া হয় (key-এর চেয়ে বড় এলিমেন্টগুলো এক ঘর ডানে
সরিয়ে জায়গা করে দেওয়া হয়)।
৩ · ইনভেরিয়েন্ট বিবৃতি
আমরা দাবি করছি:
বহিরাগত for লুপের প্রতিটি ইটারেশন শুরু হওয়ার ঠিক আগে, ইনডেক্স $j$-এর যে মানই থাকুক না কেন,
subarray $A[0 \,..\, j-1]$-এ ঠিক সেই এলিমেন্টগুলোই আছে যা মূল অ্যারের ওই অবস্থানগুলোতে (শুরুতে) ছিল —
কিন্তু এখন সাজানো (sorted) ক্রমে।
৪ · Initialization — প্রথম ইটারেশনের আগে
প্রথম ইটারেশন শুরু হয় $j = 1$ দিয়ে। তখন subarray $A[0 \,..\, j-1] = A[0 \,..\, 0]$ — অর্থাৎ একটিমাত্র এলিমেন্ট, $A[0]$। একটিমাত্র এলিমেন্টের যেকোনো ক্রম স্বতঃসিদ্ধভাবেই সাজানো (একটি এলিমেন্টকে অন্য কোনো এলিমেন্টের সাথে তুলনা করার প্রয়োজনই নেই)। সুতরাং ইনভেরিয়েন্টটি প্রথম ইটারেশন শুরুর আগে সত্য। এটাই ইনডাকশনের base case।
৫ · Maintenance — প্রতিটি ইটারেশন ইনভেরিয়েন্ট বজায় রাখে
ধরে নিই ইনভেরিয়েন্টটি কোনো ইটারেশন $j$ শুরুর আগে সত্য — অর্থাৎ $A[0 \,..\, j-1]$ ইতিমধ্যে সাজানো। ইটারেশনের
শরীরে কী ঘটে তা দেখা যাক: key ← A[j] নেওয়া হয়, তারপর while লুপ $A[0 \,..\, j-1]$-এর
মধ্যে key-এর চেয়ে বড় প্রতিটি এলিমেন্ট এক ঘর ডানে সরায় (যতক্ষণ না key-এর চেয়ে ছোট বা সমান কোনো এলিমেন্ট বা
অ্যারের শুরু পাওয়া যায়), এবং শেষে key-কে সেই ফাঁকা জায়গায় বসিয়ে দেয়। যেহেতু $A[0 \,..\, j-1]$ ইতিমধ্যে
সাজানো ছিল (ইনডাকটিভ hypothesis অনুযায়ী), key-এর সঠিক অবস্থান একটিই এবং এই শিফটিং প্রক্রিয়া ঠিক সেই
অবস্থানেই key-কে বসায়। ফলে ইটারেশন শেষে $A[0 \,..\, j]$ সাজানো থাকে — যা পরের ইটারেশন ($j+1$) শুরুর আগে
ঠিক ইনভেরিয়েন্টেরই দাবি। এলিমেন্টগুলোর সেট অপরিবর্তিত থাকে কারণ শুধু শিফট ও একটি বসানো হয়েছে, কোনো
এলিমেন্ট হারিয়ে যায়নি বা নতুন যোগ হয়নি। এটাই ইনডাকটিভ স্টেপ।
৬ · Termination — লুপ শেষে
বহিরাগত for লুপটি থামে যখন $j$, $\text{length}(A) - 1$ অতিক্রম করে — অর্থাৎ $j = n$ হয়ে যায়
($n$ = অ্যারের দৈর্ঘ্য)। Initialization ও Maintenance ধাপ দুটো একসাথে (ইনডাকশন দ্বারা) প্রমাণ করে যে
ইনভেরিয়েন্টটি $j = n$ হওয়ার মুহূর্তেও সত্য — অর্থাৎ subarray $A[0 \,..\, n-1]$ সাজানো। কিন্তু
$A[0 \,..\, n-1]$ মানেই তো পুরো অ্যারে! সুতরাং লুপ শেষ হওয়ার মুহূর্তে সম্পূর্ণ অ্যারেটি
সাজানো থাকে — যা প্রমাণ করে ইনসার্শন সর্ট সঠিক। $\blacksquare$
৭ · কোডে প্রমাণ যাচাই — প্রতিটি ইটারেশনে সত্যিকারের assert
উপরের "Maintenance" প্রমাণটি প্রতিবার একটি নির্দিষ্ট দাবি করে: প্রতিটি ইটারেশনের পর, $A[0 \,..\, j]$
সাজানো থাকতে হবে। এই দাবিটি নিছক প্রমাণেই সীমাবদ্ধ না রেখে, নিচের কোডে সত্যিকারের Python
assert দিয়ে প্রতিটি ইটারেশনের পর সরাসরি চেক করা হয়েছে — যদি প্রমাণে কোনো ভুল থাকত, নিচের কোডটি
কোনো না কোনো ইনপুটে AssertionError দিয়ে ক্র্যাশ করত:
def insertion_sort_with_invariant_check(arr):
a = arr[:]
n = len(a)
checks_passed = 0
for j in range(1, n):
key = a[j]
i = j - 1
while i >= 0 and a[i] > key:
a[i + 1] = a[i]
i -= 1
a[i + 1] = key
# --- ইনভেরিয়েন্ট চেক (Maintenance ধাপ, সত্যিকারের assert) ---
# দাবি: এই ইটারেশনের পর A[0..j] অবশ্যই সাজানো থাকবে
prefix = a[:j + 1]
assert prefix == sorted(prefix), f"j={j} এ ইনভেরিয়েন্ট ভঙ্গ হয়েছে!"
checks_passed += 1
return a, checks_passed
arr = [5, 2, 9, 1, 5, 6, 3]
sorted_arr, checks = insertion_sort_with_invariant_check(arr)
print("ইনপুট: ", arr)
print("আউটপুট (সাজানো): ", sorted_arr)
print(f"মোট {checks}টি ইনভেরিয়েন্ট চেক করা হলো — একটিও fail করেনি (assert ব্যতিক্রম ছাড়াই সম্পন্ন)")
print("পাইথনের sorted() এর সাথে মিলছে:", sorted_arr == sorted(arr))
ইনপুট: [5, 2, 9, 1, 5, 6, 3] থেকে আউটপুট: [1, 2, 3, 5, 5, 6, 9]
পাওয়া গেছে, এবং ৬টি ইনভেরিয়েন্ট চেক (অ্যারের দৈর্ঘ্য $7$, তাই $j = 1$ থেকে $j = 6$ পর্যন্ত ৬টি
ইটারেশন) সবগুলোই পাস করেছে — কোনো AssertionError ছাড়াই। এই কোড শুধু "insertion sort কাজ করে"
তা দেখায় না — এটি উপরে লেখা Maintenance প্রমাণের প্রতিটি দাবি ধাপে ধাপে, প্রতিটি ইটারেশনে
সত্যিই যাচাই করে দেখায়।
লুপ ইনভেরিয়েন্ট কৌশলটি এই কোর্স জুড়ে বারবার ফিরে আসবে — বাইনারি সার্চের সঠিকতা (M4), গ্রিডি অ্যালগরিদমের greedy-choice প্রমাণ (M5), এমনকি গ্রাফ অ্যালগরিদমের correctness invariant (M7) — সবকিছুরই ভিত্তি একই তিন-ধাপ কাঠামো: Initialization, Maintenance, Termination। একবার এই প্যাটার্নটি আয়ত্ত করলে, যেকোনো লুপ-ভিত্তিক অ্যালগরিদমের সঠিকতা প্রমাণের জন্য একটি নির্ভরযোগ্য টেমপ্লেট হাতে থাকবে।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১ যদি আমরা শুধু Initialization ও Maintenance প্রমাণ করতাম, কিন্তু Termination ধাপটি বাদ দিতাম, তাহলে কি প্রমাণটি সম্পূর্ণ হতো?
না। Initialization + Maintenance একসাথে শুধু এটুকু প্রমাণ করে যে ইনভেরিয়েন্টটি প্রতিটি ইটারেশনের শুরুতে সত্য থাকে — কিন্তু এটি বলে না যে এই সত্যতা থেকে কাঙ্ক্ষিত চূড়ান্ত ফলাফল (যেমন, পুরো অ্যারে সাজানো) কীভাবে বেরিয়ে আসে। Termination ধাপটিই লুপ-শেষের নির্দিষ্ট শর্তকে ($j = n$) ইনভেরিয়েন্টের সাথে মিলিয়ে চূড়ান্ত উপসংহারে পৌঁছায়। এই ধাপ ছাড়া প্রমাণটি অসম্পূর্ণ।
প্র ০২ ইনভেরিয়েন্ট বিবৃতিতে শুধু "$A[0..j-1]$ সাজানো" বললেই কি যথেষ্ট হতো, নাকি "মূল এলিমেন্টগুলোরই একটি পার্মুটেশন" অংশটুকু যোগ করা জরুরি ছিল?
"মূল এলিমেন্টগুলোরই পার্মুটেশন" অংশটুকু বাদ দেওয়া যাবে না। শুধু "সাজানো" বললে একটি ভুল অ্যালগরিদমও
(যেমন, যা পুরো subarray-কে [0, 0, 0, ...] দিয়ে ওভাররাইট করে দেয়) ইনভেরিয়েন্ট সিদ্ধ করত —
কারণ [0, 0, 0]-ও তো "সাজানো"! ইনভেরিয়েন্টে "একই এলিমেন্টের পার্মুটেশন" শর্তটি নিশ্চিত করে যে
সাজানোর প্রক্রিয়ায় কোনো এলিমেন্ট হারিয়ে যায়নি বা বিকৃত হয়নি — এটিই "সঠিকতা"-র প্রকৃত সংজ্ঞা।
প্র ০৩
কোড সেলে assert prefix == sorted(prefix) ব্যবহার করা হয়েছে "পার্মুটেশন" শর্তটি সরাসরি
চেক না করেই — তাহলে কি এই কোড Maintenance প্রমাণের পুরো দাবিটি যাচাই করছে?
আংশিকভাবে — এই নির্দিষ্ট কোডটি শুধু "সাজানো" অংশটুকু সরাসরি assert করে। "পার্মুটেশন" শর্তটি এক্ষেত্রে
পরোক্ষভাবে সত্য থাকে কারণ অ্যালগরিদমটি ইন-প্লেস শুধু শিফট ও অ্যাসাইনমেন্ট করে (কোনো এলিমেন্ট মুছে ফেলে না
বা নতুন তৈরি করে না) — কিন্তু চাইলে আরও কঠোরভাবে
assert sorted(prefix) == sorted(arr[:j + 1])-এর মতো একটি চেক যোগ করে পার্মুটেশন শর্তটিও
স্পষ্টভাবে যাচাই করা যেত। রিগোরাস টেস্টিং-এ যত বেশি দাবি সরাসরি চেক করা যায়, প্রমাণের প্রতি আস্থা তত
বাড়ে।
অনুশীলন
-
চিন্তা করুন: যদি ইনপুট অ্যারেটি ইতিমধ্যে সম্পূর্ণ সাজানো থাকে (যেমন
[1, 2, 3, 4, 5]), তাহলেwhileলুপের ভেতরের শর্ত (a[i] > key) কতবার সত্য হবে বলে আপনার ধারণা? এটি কি ইনভেরিয়েন্টের প্রমাণে কোনো প্রভাব ফেলে?ইতিমধ্যে সাজানো অ্যারেতে প্রতিটি
keyইতিমধ্যে তার সঠিক অবস্থানে থাকে, তাইa[i] > keyশর্তটি কখনোই সত্য হবে না (while লুপ প্রতিবার শূন্যবার চলবে)। এটি প্রমাণে কোনো প্রভাব ফেলে না — Maintenance প্রমাণটি সাধারণভাবে (any valid state-এর জন্য) কাজ করে, লুপ কতবার চলছে তার উপর নির্ভর করে না। এই ইনপুটেই দ্রুততম রান-টাইম হয় (best case, M2/L08-এ বিস্তারিত)। -
পরীক্ষা করুন: উপরের কোড সেলে
arr-কে একটি ইতিমধ্যে-উল্টো-সাজানো অ্যারে (যেমনarr = [9, 7, 5, 3, 1]) দিয়ে বদলে Run চাপুন। আউটপুটেরchecks-এর মান কত হবে, এবং সব assert কি এখনও পাস করবে?checks-এর মান হবে4(অ্যারের দৈর্ঘ্য $5$, তাই $j=1$ থেকে $j=4$ পর্যন্ত ৪টি ইটারেশন — অ্যারের আকারের উপর নির্ভরশীল, উপাদানের ক্রমের উপর নয়)। হ্যাঁ, সব assert এখনও পাস করবে এবং চূড়ান্ত আউটপুট হবে[1, 3, 5, 7, 9]— কারণ প্রমাণটি যেকোনো বৈধ ইনপুট বিন্যাসের জন্য প্রযোজ্য, শুধু একটি নির্দিষ্ট বিন্যাসের জন্য নয়।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৭টি পাঠ অ্যাসিম্পটোটিক অ্যানালাইসিস, রিকারেন্স, ডিভাইড অ্যান্ড কনকার, গ্রিডি, DP, গ্রাফ অ্যালগরিদম, ব্যাকট্র্যাকিং, স্ট্রিং অ্যালগরিদম, অ্যামর্টাইজড অ্যানালাইসিস, NP-কমপ্লিটনেস ও অ্যাপ্রক্সিমেশন — বাকি পাঠগুলো শীঘ্রই যুক্ত হবে।
- Data Structures & Algorithms কোর্স সহোদর কোর্স ইনসার্শন সর্ট সহ অন্যান্য সর্টিং অ্যালগরিদম কীভাবে কাজ করে ও ইমপ্লিমেন্ট করতে হয় তার ভিত্তি সেই কোর্সেই তৈরি হয়েছে — এই কোর্স সঠিকতা প্রমাণ ও অ্যানালাইসিসে গভীরে যায়।
- সব Courses দেখুন ABCL TECH C, C++, Python, Java, JavaScript, DSA, DBMS, Discrete Mathematics, System Design, Cybersecurity, Cloud Computing & DevOps, Computer Networks, Operating Systems, Computer Architecture, Programming Languages & Compiler Design, Software Engineering & Git, Theory of Computation, Engineering Economics, Full-Stack Web Frameworks, Mobile App Development, Ethics in Computing & AI Safety, Software Testing & Quality Assurance ও Design and Analysis of Algorithms — সব এক জায়গায়।