Mathematical Induction — অসীমকে সসীম যুক্তিতে ধরা
Mathematical Induction
Weak, strong আর structural induction — একমাত্র হাতিয়ার যা অসীম সংখ্যক ক্ষেত্র সসীম যুক্তিতে প্রমাণ করে। আর কেন recursion আর induction আসলে একই জিনিস।
আগে এটা বুঝি
গত লেসনে আমরা পাঁচটা proof technique শিখেছি। কিন্তু এই দাবিটা তাদের কোনোটা দিয়ে ধরা যায় না:
প্রতিটা
n ≥ 1-এর জন্য:1 + 2 + 3 + ⋯ + n = n(n+1)/2
কেন? কারণ এখানে অসীম সংখ্যক দাবি আছে — n = 1-এর জন্য একটা,
n = 2-এর জন্য একটা, n = 3… অনন্তকাল।
আপনি একটা একটা করে প্রমাণ করতে পারবেন না। কখনো শেষ হবে না।
কিন্তু লক্ষ্য করুন একটা জিনিস: n = 5 -এর দাবি প্রমাণ করা সহজ যদি
n = 4 -এর দাবি ইতিমধ্যে জানা থাকে।
1+2+3+4+5 = (1+2+3+4) + 5 = 10 + 5 = 15 = 5·6/2 ✓এই সম্পর্কটাই — “আগেরটা জানলে পরেরটা পাওয়া যায়” — induction-এর হৃদয়।
দুইটা জিনিস প্রমাণ করলেই যথেষ্ট:
- প্রথমটা সত্য
- যেকোনোটা সত্য হলে তার পরেরটাও সত্য
তাহলে সবগুলোই সত্য — অসীম সংখ্যক দাবি, দুইটা যুক্তি দিয়ে।
মূল ধারণা
Weak induction — মৌলিক রূপ
P(n) একটা predicate। প্রমাণ করতে হবে ∀n ≥ n₀, P(n)।
দুইটা ধাপ:
| ধাপ | কী প্রমাণ করবেন |
|---|---|
| Base case | P(n₀) সত্য |
| Inductive step | ∀k ≥ n₀ : P(k) → P(k+1) |
Inductive step-এ P(k) কে বলা হয় inductive hypothesis (IH) —
এটা আপনি ধরে নিচ্ছেন, প্রমাণ করছেন না।
Formally, induction axiom:
প্রথম উদাহরণ
দাবি:
∀n ≥ 1:1 + 2 + ⋯ + n = n(n+1)/2
প্রমাণ.
Base case (n = 1):
বাঁ পাশ = 1। ডান পাশ = 1·2/2 = 1। সমান ✓
Inductive step: ধরি P(k) সত্য কোনো k ≥ 1-এর জন্য, অর্থাৎ
দেখাতে হবে P(k+1) সত্য:
এটাই P(k+1)। ∎
Strong induction — যখন আগেরটা যথেষ্ট নয়
কখনো P(k+1) প্রমাণ করতে শুধু P(k) যথেষ্ট নয় — আরো আগের কিছু লাগে।
Strong induction-এ IH বদলে যায়:
| Inductive hypothesis | |
|---|---|
| Weak | P(k) সত্য |
| Strong | P(n₀), P(n₀+1), …, P(k) — সবগুলো সত্য |
কেন এটা দরকার
দাবি:
n ≥ 2-এর প্রতিটা পূর্ণসংখ্যা মৌলিক সংখ্যার গুণফল হিসেবে লেখা যায়।
Weak induction চেষ্টা করুন: P(k) জানি, P(k+1) চাই।
k+1 যদি যৌগিক হয়, ধরি k+1 = a·b যেখানে 2 ≤ a, b ≤ k।
এখন আমার P(a) আর P(b) দরকার। কিন্তু weak IH শুধু P(k) দেয় —
a আর b তো k-এর চেয়ে ছোট হতে পারে যেকোনো মান!
Strong induction দিয়ে:
Base case (n = 2): 2 নিজেই মৌলিক ✓
Inductive step: ধরি 2 থেকে k পর্যন্ত সব সংখ্যার জন্য দাবি সত্য।
k+1 বিবেচনা করি:
- Case 1:
k+1মৌলিক। তাহলে সেটা নিজেই একটা (একক) মৌলিক গুণফল ✓ - Case 2:
k+1যৌগিক। তাহলেk+1 = a·bযেখানে2 ≤ a, b \< k+1। যেহেতুa, b ≤ k, strong IH প্রযোজ্য — দুটোই মৌলিকের গুণফল। তাদের গুণফলও তাই ✓ ∎
Structural induction — CS-এর সবচেয়ে দরকারি রূপ
সংখ্যার বদলে recursively সংজ্ঞায়িত structure-এর উপর induction।
একটা binary tree-র recursive সংজ্ঞা:
Tree ::= Leaf
| Node(Tree, value, Tree)Structural induction-এ:
- Base case: সবচেয়ে সরল constructor (
Leaf) - Inductive step: যৌগিক constructor, ধরে নিয়ে যে উপাদানগুলোর জন্য দাবি সত্য
দাবি: যেকোনো binary tree-তে (Leaf বাদে) node সংখ্যা
nহলে edge সংখ্যাn − 1।
প্রমাণ (structural induction).
P(T): “tree T-তে nodes(T) − 1 = edges(T)”, যেখানে খালি tree-কে
আলাদা করে ধরছি।
Base case (Leaf): একটা মাত্র node, শূন্য edge। 1 − 1 = 0 ✓
Inductive step (Node(L, v, R)): ধরি P(L) আর P(R) সত্য।
nodes(Node(L,v,R)) = nodes(L) + nodes(R) + 1
edges(Node(L,v,R)) = edges(L) + edges(R) + 2 ← v থেকে দুই সন্তানেIH থেকে edges(L) = nodes(L) − 1 এবং edges(R) = nodes(R) − 1:
edges = (nodes(L) − 1) + (nodes(R) − 1) + 2
= nodes(L) + nodes(R)
= (nodes(L) + nodes(R) + 1) − 1
= nodes(Node(L,v,R)) − 1 ✓ ∎Well-ordering principle
Induction-এর যমজ ভাই:
অ-ঋণাত্মক পূর্ণসংখ্যার যেকোনো অ-খালি উপসেটে একটা সর্বনিম্ন উপাদান আছে।
দেখতে তুচ্ছ, কিন্তু এটা induction-এর সাথে logically equivalent — একটা থেকে অন্যটা প্রমাণ করা যায়।
ব্যবহারিক রূপ — termination proof:
গত লেসনে আমরা Euclid’s algorithm-এর termination প্রমাণ করেছি
“b কমছে, অ-ঋণাত্মক, তাই থামবে” বলে। সেই যুক্তির ভিত্তি এটাই।
অ-ঋণাত্মক পূর্ণসংখ্যার একটা কঠোরভাবে হ্রাসমান অসীম ক্রম থাকতে পারে না — কারণ থাকলে সেই ক্রমের সেট-এর কোনো সর্বনিম্ন উপাদান থাকত না, যা well-ordering-এর বিরোধী।
ভেতরে কী ঘটছে
Recursion আর induction — একই মুদ্রার দুই পিঠ
এটাই এই লেসনের সবচেয়ে গুরুত্বপূর্ণ অন্তর্দৃষ্টি।
| Induction proof | Recursive function |
|---|---|
P(n) — প্রমাণ করার দাবি | Function-এর specification |
| Base case | Base case (if n == 0: return …) |
| Inductive hypothesis | Recursive call-টা ঠিক কাজ করে — এই বিশ্বাস |
| Inductive step | Recursive call-এর ফল ব্যবহার করে উত্তর বানানো |
| Well-ordering (termination) | Argument ছোট হচ্ছে, base-এ পৌঁছাবে |
উদাহরণ দিয়ে দেখি:
def factorial(n):
if n == 0: # ← base case
return 1
return n * factorial(n - 1) # ← inductive stepপ্রমাণ. P(n): “factorial(n) n! ফেরত দেয়।”
Base: factorial(0) = 1 = 0! ✓
Step: ধরি factorial(k) = k! (IH — অর্থাৎ recursive call-টা ঠিক কাজ করে)।
factorial(k+1) = (k+1) * factorial(k)
= (k+1) * k! [IH]
= (k+1)! ✓Termination: প্রতিবার argument 1 কমে, অ-ঋণাত্মক, তাই 0-তে পৌঁছাবে ∎
Recurrence relation
Recursive algorithm-এর running time প্রায়ই recurrence আকারে আসে।
Merge sort:
def merge_sort(A):
if len(A) <= 1:
return A
mid = len(A) // 2
left = merge_sort(A[:mid]) # T(n/2)
right = merge_sort(A[mid:]) # T(n/2)
return merge(left, right) # Θ(n)দাবি: T(n) = O(n log n)
প্রমাণ (strong induction). দেখাব T(n) ≤ c·n·log₂(n) + d·n উপযুক্ত
c, d-এর জন্য। সরলতার জন্য n দুইয়ের ঘাত ধরি আর T(n) = 2T(n/2) + n,
T(1) = 1 নিই।
Base (n = 1): T(1) = 1 ≤ c·1·0 + d·1 = d। d ≥ 1 নিলে ✓
Step: ধরি n-এর চেয়ে ছোট সব দুইয়ের ঘাতের জন্য দাবি সত্য (strong IH)।
c ≥ 1 নিলে n(1-c) ≤ 0, তাই T(n) ≤ c n log₂ n + dn ✓ ∎
Induction-এর সাধারণ ফাঁদ
- Base case ভুল বা বাদশৃঙ্খল শুরুই হয় না
- ভুল base case নির্বাচনn₀ = 1 লিখেছেন কিন্তু step n ≥ 2 ধরে নেয়
- IH ব্যবহার না করাতাহলে এটা induction নয় — কিছু গোলমাল আছে
- IH ভুল জায়গায় প্রয়োগk-এর বদলে k+1-এর জন্য ধরে নেওয়া — circular
- Strong লাগলে weak ব্যবহারযে অংশটা লাগে সেটা IH-তে নেই
- লুকানো অনুমানঘোড়ার প্রমাণের overlap — n ≥ 2 ধরে নেওয়া
- Termination না দেখানোRecursive function-এ argument ছোট হচ্ছে কি?
উদাহরণ
একটা সম্পূর্ণ correctness প্রমাণ
def power(base, exp):
"""base^exp হিসাব করে, O(log exp) গুণে"""
if exp == 0:
return 1
half = power(base, exp // 2)
if exp % 2 == 0:
return half * half
else:
return half * half * baseদাবি: সব exp ≥ 0-এর জন্য power(b, exp) = b^exp।
Strong induction লাগবে — কারণ recursive call exp // 2 -এ যায়,
exp − 1 -এ নয়।
প্রমাণ.
Base case (exp = 0): power(b, 0) = 1 = b⁰ ✓
Inductive step: ধরি 0 ≤ j \< e-এর সব j-এর জন্য power(b, j) = bʲ
(strong IH)।
e ≥ 1 নিন। যেহেতু e // 2 \< e (কারণ e ≥ 1), IH প্রযোজ্য:
half = power(b, e // 2) = b^(e // 2)Case 1: e জোড়। তাহলে e = 2m যেখানে m = e // 2।
Case 2: e বিজোড়। তাহলে e = 2m + 1 যেখানে m = e // 2
(পূর্ণসংখ্যা ভাগে ভগ্নাংশ বাদ যায়)।
দুই case-ই ঢাকা পড়েছে (প্রতিটা পূর্ণসংখ্যা হয় জোড় নয় বিজোড়)। ∎
Termination: প্রতিবার exp অন্তত অর্ধেক হয়। exp ≥ 1 হলে
exp // 2 \< exp, তাই ক্রমটা কঠোরভাবে হ্রাসমান আর অ-ঋণাত্মক।
Well-ordering অনুযায়ী 0-তে পৌঁছাবে ∎
Complexity: প্রতিটা call exp অর্ধেক করে, তাই recursion-এর গভীরতা
⌊log₂ exp⌋ + 1। প্রতিটা level-এ ধ্রুবক কাজ, তাই Θ(log exp) গুণ।
তুলনা করুন naive পদ্ধতির সাথে (exp বার গুণ):
exp | Naive | Fast |
|---|---|---|
| 10 | 10 | 4 |
| 1,000 | 1,000 | 10 |
| 10⁶ | 10⁶ | 20 |
| 10⁹ | 10⁹ | 30 |
যেখানে induction ব্যর্থ হয় — একটা সাবধানবাণী
“দাবি”: সব
n ≥ 1-এর জন্যn \< 100
“প্রমাণ.”
Base: 1 \< 100 ✓
Step: ধরি k \< 100। তাহলে… k + 1 \< 101। 😐
Step-টা ভেঙে গেল — k + 1 \< 100 প্রমাণ করা গেল না (k = 99 হলে
k+1 = 100, যা \< 100 নয়)।
ভালো — মিথ্যা দাবি প্রমাণ করা যায়নি। Induction মিথ্যা জিনিস প্রমাণ করতে দেয় না, যদি আপনি নিয়ম মেনে চলেন।
কিন্তু নিয়ম না মানলে? গত লেসনের ঘোড়ার প্রমাণটা মনে করুন — সেখানে
inductive step-এ একটা লুকানো অনুমান (n ≥ 2) ছিল যা base case-এ
সত্য নয়। ফলে মিথ্যা দাবি “প্রমাণিত” হয়ে গিয়েছিল।
শিক্ষা: inductive step-এ যা যা ধরে নিচ্ছেন সব স্পষ্ট করে লিখুন, আর যাচাই করুন সেগুলো base case থেকেই সত্য কি না।
নিজে চালিয়ে দেখুন
Recursion depth-এর দেয়ালে ধাক্কা
import sys
def depth(n=0):
return depth(n + 1)
print("Python-এর recursion limit:", sys.getrecursionlimit())
try:
depth()
except RecursionError:
print("RecursionError — Python নিজেই থামিয়ে দিল")
# limit বাড়িয়ে দিলে?
sys.setrecursionlimit(100000)
try:
depth()
except RecursionError as e:
print("আবার RecursionError:", e)Python নিজে থেকে একটা limit রাখে (সাধারণত ১০০০) যাতে C-স্তরের stack overflow হয়ে interpreter crash না করে। Limit অনেক বাড়িয়ে দিলে আসল segfault হতে পারে।
C-তে সীমাটা আসল:
#include <stdio.h>
int depth(int n) {
if (n % 10000 == 0) printf("depth = %d\n", n);
return depth(n + 1);
}
int main(void) {
depth(0);
return 0;
}gcc -O0 -o depth depth.c # -O0 জরুরি, নাহলে tail call optimize হতে পারে
./depth
# ... depth = 260000
# Segmentation faultStack size দেখুন আর বদলান:
ulimit -s # সাধারণত 8192 (KB) = 8 MB
ulimit -s 65536 # 64 MB করুন
./depth # এখন অনেক গভীরে যাবেহিসাব মিলিয়ে দেখুন: প্রতিটা stack frame-এ return address (8 byte),
saved registers, আর local variable থাকে — সাধারণত ৩২ থেকে ৪৮ byte।
8 MB stack-এ তাই প্রায় 8388608 / 32 ≈ 260000 frame আঁটে।
উপরের output-টার সাথে মিলে গেল।
Tail call optimization দেখুন:
gcc -O2 -o depth_opt depth.c
./depth_opt # চিরকাল চলবে — crash করবে না!-O2-তে GCC দেখে যে return depth(n+1) -এর পরে আর কোনো কাজ নেই,
তাই নতুন frame না বানিয়ে বর্তমান frame পুনর্ব্যবহার করে — recursion
কে loop-এ পরিণত করে।
গাণিতিক induction অসীম পর্যন্ত চলে, কিন্তু বাস্তব recursion সসীম stack-এ চলে। এই ফাঁকটাই stack overflow — আর এটা Level 4-এ process memory layout-এর সাথে সরাসরি যুক্ত।
Recurrence-এর তাত্ত্বিক ও পরিমাপকৃত রূপ
import time, math
calls = 0
def merge_sort(A):
global calls
calls += 1
if len(A) <= 1:
return A
mid = len(A) // 2
L = merge_sort(A[:mid])
R = merge_sort(A[mid:])
out, i, j = [], 0, 0
while i \< len(L) and j \< len(R):
if L[i] <= R[j]: out.append(L[i]); i += 1
else: out.append(R[j]); j += 1
out.extend(L[i:]); out.extend(R[j:])
return out
print(f"{'n':>8} {'calls':>10} {'2n-1':>10} {'time(ms)':>10} {'t/(n log n)':>14}")
import random
for n in [1000, 2000, 4000, 8000, 16000, 32000, 64000]:
A = [random.random() for _ in range(n)]
calls = 0
t0 = time.perf_counter()
merge_sort(A)
dt = (time.perf_counter() - t0) * 1000
nlogn = n * math.log2(n)
print(f"{n:>8} {calls:>10} {2*n-1:>10} {dt:>10.2f} {dt/nlogn*1e6:>14.3f}")দুইটা জিনিস লক্ষ্য করুন:
১. Call সংখ্যা ঠিক 2n − 1। এটা induction দিয়ে প্রমাণযোগ্য:
n element-এর merge sort একটা binary tree বানায় যার n টা leaf,
আর n leaf-এর একটা full binary tree-তে মোট 2n − 1 টা node।
২. t / (n log n) অনুপাতটা মোটামুটি স্থির। এটাই Θ(n log n)-এর
পরীক্ষামূলক প্রমাণ। পুরোপুরি স্থির নয় — বড় n-এ সামান্য বাড়ে, কারণ
cache miss বাড়ে আর memory allocation বেশি হয়।
এবার একটা তুলনা চালান:
def insertion_sort(A):
A = A[:]
for i in range(1, len(A)):
key, j = A[i], i - 1
while j >= 0 and A[j] > key:
A[j+1] = A[j]; j -= 1
A[j+1] = key
return A
for n in [100, 500, 1000, 2000, 4000]:
A = [random.random() for _ in range(n)]
t0 = time.perf_counter(); insertion_sort(A)
t_ins = (time.perf_counter() - t0) * 1000
t0 = time.perf_counter(); merge_sort(A)
t_mrg = (time.perf_counter() - t0) * 1000
print(f"n={n:>5} insertion={t_ins:>8.2f}ms merge={t_mrg:>7.2f}ms "
f"অনুপাত={t_ins/t_mrg:>6.1f}×")ছোট n-এ insertion sort দ্রুত হতে পারে — যদিও তার complexity
O(n²)। কারণ তার ধ্রুবক অনেক ছোট, আর memory access sequential।
এই কারণেই বাস্তব sorting library (Python-এর Timsort, C++-এর introsort) ছোট subarray-তে insertion sort ব্যবহার করে। Asymptotic analysis গল্পের অর্ধেক মাত্র — Level 11-এ বাকি অর্ধেক।
Induction যে recurrence প্রমাণ করে, সেটা বাস্তবে মাপা যায় — কিন্তু ধ্রুবক আর cache effect-এর কারণে ঠিক মেলে না। এই ফাঁকটাই Level 11-এর বিষয়।
নিজে বানান
Induction-এর নিয়মে recursive function লেখা
- প্রতিটা function-এর জন্য আগে P(n) লিখুন — কী দাবি করছেন
- Base case লিখুন এবং যাচাই করুন
- IH ধরে নিয়ে recursive case লিখুন
- Termination measure বলুন — কোন রাশি কমছে
- Contract দিয়ে runtime-এ যাচাই করুন
লক্ষ্য: recursion লেখার সময় induction-এর কাঠামোটা সচেতনভাবে অনুসরণ করা।
import functools
def induction(P, measure, base_msg=""):
"""
P : (result, *args) -> bool — postcondition
measure : (*args) -> int — termination measure (কমতে হবে)
"""
def decorate(fn):
stack = []
@functools.wraps(fn)
def wrapper(*args):
m = measure(*args)
assert m >= 0, f"{fn.__name__}: measure ঋণাত্মক ({m}) — well-ordering ভাঙল"
if stack:
assert m \< stack[-1], (
f"{fn.__name__}: measure কমেনি ({stack[-1]} → {m}) — termination ঝুঁকি")
stack.append(m)
try:
r = fn(*args)
finally:
stack.pop()
assert P(r, *args), f"{fn.__name__}: postcondition ভাঙল args={args} → {r!r}"
return r
return wrapper
return decorate
# ── ১. Fast exponentiation ───────────────────────────────────────
@induction(
P = lambda r, b, e: r == b ** e,
measure = lambda b, e: e,
)
def power(base, exp):
if exp == 0:
return 1
half = power(base, exp // 2)
return half * half if exp % 2 == 0 else half * half * base
# ── ২. Merge sort ────────────────────────────────────────────────
from collections import Counter
@induction(
P = lambda r, A: (
all(r[i] <= r[i+1] for i in range(len(r)-1))
and Counter(r) == Counter(A)
),
measure = lambda A: len(A),
)
def msort(A):
if len(A) <= 1:
return list(A)
mid = len(A) // 2
L, R = msort(A[:mid]), msort(A[mid:])
out, i, j = [], 0, 0
while i \< len(L) and j \< len(R):
if L[i] <= R[j]: out.append(L[i]); i += 1
else: out.append(R[j]); j += 1
out.extend(L[i:]); out.extend(R[j:])
return out
# ── ৩. Tree height ───────────────────────────────────────────────
class Node:
def __init__(self, v, l=None, r=None):
self.v, self.l, self.r = v, l, r
@induction(
P = lambda r, t: r >= 0 and (r == 0) == (t is None),
measure = lambda t: 0 if t is None else 1 + max(
(0 if t.l is None else 1), (0 if t.r is None else 1)),
)
def height(t):
if t is None:
return 0
return 1 + max(height(t.l), height(t.r))
print("power(2, 10) =", power(2, 10))
print("power(3, 0) =", power(3, 0))
print("power(7, 13) =", power(7, 13))
print("msort([5,2,8,1,9,3]) =", msort([5, 2, 8, 1, 9, 3]))
tree = Node(1, Node(2, Node(4)), Node(3))
print("height =", height(tree))
# ── ইচ্ছে করে ভুল ঢোকান ─────────────────────────────────────────
@induction(
P = lambda r, b, e: r == b ** e,
measure = lambda b, e: e,
)
def power_buggy(base, exp):
if exp == 0:
return 1
half = power_buggy(base, exp // 2)
return half * half # বিজোড় case ভুলে গেছি!
try:
power_buggy(2, 5)
except AssertionError as e:
print("\nধরা পড়ল:", e)শেষ example-টা গুরুত্বপূর্ণ: bug-টা বিজোড় exponent-এ, তাই
power_buggy(2, 4) ঠিক উত্তর দেবে কিন্তু power_buggy(2, 5) দেবে না।
Contract সেটা সাথে সাথে ধরে ফেলে — আর সবচেয়ে ভেতরের ব্যর্থ call-টা
দেখায়, যা debugging সহজ করে।
নিজে বাড়ান:
- Fibonacci-র naive আর memoized version লিখুন, দুটোতেই contract দিন, আর call সংখ্যা তুলনা করুন
reverse(list)লিখুন এই postcondition সহ:reverse(reverse(x)) == x- Binary search-এর recursive version লিখুন contract সহ
- Tower of Hanoi লিখুন এবং প্রমাণ করুন চাল সংখ্যা ঠিক
2ⁿ − 1 - একটা function লিখুন যার measure কমে না এবং দেখুন contract কীভাবে infinite recursion-এর আগেই ধরে ফেলে
বাস্তব সিস্টেমে
Induction কোথায় কোথায় লুকিয়ে আছে
প্রতিটা recursive algorithm। Merge sort, quicksort, binary search, tree traversal, DFS, divide-and-conquer — সবগুলোর correctness argument induction। আপনি লেখেন না, কিন্তু যুক্তিটা সেখানেই আছে।
Compiler-এর প্রতিটা AST pass। Type checking, constant folding, code generation — প্রতিটা recursive traversal-এর correctness structural induction দিয়ে প্রমাণিত। “এই pass সব node ঠিকভাবে হ্যান্ডল করে” — এটা induction-এর দাবি।
Type system-এর soundness। “যদি একটা program type-check করে, তাহলে সেটা runtime-এ type error দেবে না” — এই দাবিটা (progress + preservation) প্রমাণিত হয় derivation tree-র উপর structural induction দিয়ে।
Parser-এর correctness। Recursive descent parser গ্রামারের গঠনের সাথে হুবহু মেলে; তার correctness proof-ও গ্রামারের গঠনের উপর induction।
Distributed protocol। Raft-এর safety proof-এ একটা কেন্দ্রীয় দাবি: “যদি একটা log entry কোনো term-এ commit হয়, তাহলে সব উচ্চতর term-এর leader-এর log-এ সেটা থাকবে।” প্রমাণটা term সংখ্যার উপর induction। Level 9-এ আমরা এটা করব।
Blockchain। “Chain-এর প্রতিটা block বৈধ” — genesis block (base case) থেকে শুরু করে প্রতিটা নতুন block-এর validation (inductive step)। আক্ষরিকভাবে induction, প্রতি ১০ মিনিটে একবার।
Loop invariant। গত লেসনে দেখেছি — maintenance step আসলে inductive step, আর initialization হলো base case। প্রতিটা loop invariant proof একটা ছদ্মবেশী induction।
Amortized analysis। Dynamic array-র doubling কৌশল কেন O(1)
amortized — এই প্রমাণটা potential function-এর উপর induction।
Level 6-এ দেখব।
যে ভুলগুলো সবাই করে
“Induction হলো 'কয়েকটা উদাহরণ দেখে সাধারণীকরণ করা'।”
এটা inductive reasoning (আরোহী যুক্তি) — বিজ্ঞানের পদ্ধতি, আর এটা প্রমাণ নয়।
Mathematical induction সম্পূর্ণ ভিন্ন — এটা deductive, অর্থাৎ নিগমনমূলক ও নিশ্চিত।
| Inductive reasoning | Mathematical induction | |
|---|---|---|
| পদ্ধতি | কয়েকটা কেস দেখে সাধারণীকরণ | দুইটা দাবি প্রমাণ করে সব কেস |
| নিশ্চয়তা | সম্ভাব্য | নিশ্চিত |
| উদাহরণ | “১০০০টা রাজহাঁস সাদা, তাই সব সাদা” | P(1) ও P(k)→P(k+1) |
| ভুল হতে পারে? | হ্যাঁ | না (নিয়ম মানলে) |
নামের মিলটা দুর্ভাগ্যজনক ঐতিহাসিক দুর্ঘটনা।
Euler-এর n² + n + 41 মনে করুন — ৪০টা কেস সত্য, তবু দাবিটা মিথ্যা।
সেটা ছিল inductive reasoning। Mathematical induction-এ এমন হতে পারে না।
“Inductive step-এ P(k) ধরে নেওয়া মানে circular reasoning — যা প্রমাণ করছি তাই ধরে নিচ্ছি।”
না, এটা সবচেয়ে সাধারণ বিভ্রান্তি — আর বোঝাটা গুরুত্বপূর্ণ।
আপনি P(k) সত্য বলে ধরে নিচ্ছেন না। আপনি প্রমাণ করছেন একটা
implication: P(k) → P(k+1)।
Implication প্রমাণ করার নিয়মই হলো premise ধরে conclusion-এ পৌঁছানো। এতে premise-টা সত্য কি না সে বিষয়ে কোনো দাবি করা হয় না।
উপমা: “যদি বৃষ্টি হয়, রাস্তা ভিজবে” — এটা প্রমাণ করতে আমি ধরে নিই বৃষ্টি হচ্ছে, এবং দেখাই রাস্তা ভিজবে। এতে আমি দাবি করছি না যে বৃষ্টি হচ্ছে।
Base case-টাই একমাত্র জায়গা যেখানে আমরা কিছু সত্য প্রমাণ করি। তারপর implication-এর শৃঙ্খল সেই সত্যকে অসীম পর্যন্ত বহন করে নেয়।
Programming-এর ভাষায়: recursive call ঠিক কাজ করে ধরে নেওয়া circular নয়, কারণ base case-এ পৌঁছানো নিশ্চিত।
“Base case সবসময় n = 0 বা n = 1।”
না — base case সেখানে যেখানে দাবিটা শুরু হয়।
উদাহরণ: 2ⁿ > n² — এটা n = 5 থেকে সত্য।
n | 2ⁿ | n² | 2ⁿ > n²? |
|---|---|---|---|
| 1 | 2 | 1 | ✓ |
| 2 | 4 | 4 | ✗ |
| 3 | 8 | 9 | ✗ |
| 4 | 16 | 16 | ✗ |
| 5 | 32 | 25 | ✓ |
| 6 | 64 | 36 | ✓ |
তাই base case n = 5, আর প্রমাণটা ∀n ≥ 5-এর জন্য।
আরো একটা সূক্ষ্মতা: কখনো একাধিক base case লাগে। Fibonacci-র
কোনো দাবি প্রমাণ করতে হলে সাধারণত n = 0 আর n = 1 — দুটোই
base case করতে হয়, কারণ recursive সংজ্ঞা দুইটা আগের মানের উপর নির্ভর করে।
কোডেও একই: fib(n) = fib(n-1) + fib(n-2) লিখলে দুইটা base case
না দিলে infinite recursion।
“Structural induction আর সংখ্যার উপর induction আলাদা দুইটা জিনিস।”
এরা একই নীতির দুইটা রূপ।
Structural induction চলে যেকোনো well-founded সম্পর্কের উপর — এমন সম্পর্ক যাতে অসীম নিম্নগামী শৃঙ্খল নেই।
- সংখ্যায়:
n−1 \< n, আর0-তে থামে - List-এ:
tail(L)ছোট, আর[]-তে থামে - Tree-তে: subtree ছোট, আর
Leaf-এ থামে - Grammar-এ: derivation ছোট, আর terminal-এ থামে
সব ক্ষেত্রেই একই যুক্তি: “ছোটগুলোর জন্য সত্য ধরে নিয়ে বড়টার জন্য প্রমাণ, আর সবচেয়ে ছোটগুলোর জন্য সরাসরি প্রমাণ।”
গণিতে এই সাধারণীকরণকে বলে well-founded induction, আর এটাই সবচেয়ে মৌলিক রূপ। সংখ্যার induction তার একটা বিশেষ ক্ষেত্র মাত্র।
বুঝেছেন কি না দেখুন
1Induction দিয়ে প্রমাণ করুন: ∀n ≥ 0, n উপাদানের একটা সেটের
উপসেট সংখ্যা 2ⁿ।
প্রয়োগ
∀n ≥ 0, n উপাদানের একটা সেটের
উপসেট সংখ্যা 2ⁿ।P(n): “|S| = n হলে |𝒫(S)| = 2ⁿ”, যেখানে 𝒫(S) = power set।
Base case (n = 0): খালি সেট ∅-এর একমাত্র উপসেট ∅ নিজে।
তাই |𝒫(∅)| = 1 = 2⁰ ✓
Inductive step: ধরি P(k) সত্য — k উপাদানের যেকোনো সেটের
2ᵏ টা উপসেট আছে।
এবার |S| = k + 1 নিন। একটা উপাদান x ∈ S বেছে নিন, আর ধরি
S' = S \ {x}, তাই |S'| = k।
S-এর প্রতিটা উপসেট T -কে দুই দলে ভাগ করা যায়:
- যেগুলোতে
xনেই: এরা ঠিকS'-এর উপসেট। সংখ্যা= |𝒫(S')| = 2ᵏ(IH) - যেগুলোতে
xআছে: প্রতিটা এমনT-কেT = T' ∪ {x}আকারে লেখা যায় যেখানেT' ⊆ S'। এই সঙ্গতিটা একের সাথে এক, তাই সংখ্যাও2ᵏ
দুইটা দল পরস্পর বিচ্ছিন্ন এবং একসাথে সব উপসেট ঢাকে (প্রতিটা উপসেটে
হয় x আছে নয় নেই — exhaustive)।
একটা সুন্দর বিকল্প দৃষ্টি: প্রতিটা উপসেটকে একটা n-bit string
হিসেবে ভাবুন — bit i বলে i-তম উপাদানটা আছে কি নেই। n bit-এর
সম্ভাব্য string 2ⁿ টা, আর প্রতিটা string ঠিক একটা উপসেট বোঝায়।
এই bit-string দৃষ্টিটাই বাস্তবে ব্যবহার হয় — bitmask দিয়ে subset enumeration:
def subsets(items):
n = len(items)
for mask in range(1 \<\< n):
yield [items[i] for i in range(n) if mask & (1 \<\< i)]Level 1-এ bit manipulation, Level 6-এ subset enumeration আর bitmask DP — দুটোই এই ধারণার উপর দাঁড়ানো।
2এই function-টার correctness প্রমাণ করতে weak না strong induction লাগবে?
কেন?
def fib(n):
if n \<= 1: return n
return fib(n-1) + fib(n-2)
যুক্তি
def fib(n):
if n \<= 1: return n
return fib(n-1) + fib(n-2)Strong induction লাগবে।
কারণ fib(n) -এর correctness প্রমাণ করতে আপনার fib(n-1) এবং
fib(n-2) — দুটোরই correctness লাগে। Weak IH শুধু P(n-1) দেয়,
P(n-2) দেয় না।
প্রমাণ. P(n): “fib(n) = Fₙ”, যেখানে F₀ = 0, F₁ = 1, Fₙ = Fₙ₋₁ + Fₙ₋₂।
Base cases (দুইটা লাগবে):
fib(0) = 0 = F₀✓fib(1) = 1 = F₁✓
Inductive step: n ≥ 2 নিন। ধরি 0 ≤ j \< n-এর সব j-এর জন্য
fib(j) = Fⱼ (strong IH)।
যেহেতু n-1 \< n এবং n-2 \< n, IH দুটোতেই প্রযোজ্য:
fib(n) = fib(n-1) + fib(n-2) = Fₙ₋₁ + Fₙ₋₂ = Fₙ ✓ ∎দুইটা base case কেন জরুরি: যদি শুধু n = 0 base case রাখতেন,
তাহলে n = 1-এর জন্য inductive step-এ fib(-1) লাগত — যা সংজ্ঞায়িত নয়।
কোডেও একই: if n == 0: return 0 লিখে n == 1-এর case ভুলে গেলে
fib(1) → fib(0) + fib(-1) → অসীম recursion (negative-এ নেমে যাবে)।
পার্শ্ব-পর্যবেক্ষণ: এই naive version-এর complexity O(φⁿ) যেখানে
φ ≈ 1.618। fib(50) চালাতে মিনিট লাগে। Memoization দিয়ে O(n),
matrix exponentiation দিয়ে O(log n) — Level 6-এ দেখব।
3Tower of Hanoi: n টা চাকতি, ৩টা খুঁটি। প্রমাণ করুন যে সমাধানে ঠিক
2ⁿ − 1 টা চাল লাগে, আর এটাই সর্বনিম্ন।
ডিজাইন
n টা চাকতি, ৩টা খুঁটি। প্রমাণ করুন যে সমাধানে ঠিক
2ⁿ − 1 টা চাল লাগে, আর এটাই সর্বনিম্ন।def hanoi(n, src, dst, aux, moves):
if n == 0: return
hanoi(n-1, src, aux, dst, moves) # উপরের n-1 টা সরাও
moves.append((src, dst)) # সবচেয়ে বড়টা সরাও
hanoi(n-1, aux, dst, src, moves) # n-1 টা ফিরিয়ে আনোঅংশ ১ — algorithm ঠিক 2ⁿ − 1 চাল দেয়।
M(n) = n চাকতির জন্য চাল সংখ্যা। Recurrence:
P(n): M(n) = 2ⁿ − 1
Base: M(0) = 0 = 2⁰ − 1 = 0 ✓
Step: ধরি M(k) = 2ᵏ − 1।
অংশ ২ — এর চেয়ে কম চালে সম্ভব নয়।
L(n) = সর্বনিম্ন প্রয়োজনীয় চাল। দেখাব L(n) ≥ 2ⁿ − 1।
Base: L(0) = 0 ≥ 0 ✓
Step: ধরি L(k) ≥ 2ᵏ − 1।
যেকোনো বৈধ সমাধানে, সবচেয়ে বড় চাকতিটাকে অন্তত একবার সরাতেই হবে। কিন্তু সেটা সরানোর মুহূর্তে:
- তার উপরে কোনো চাকতি থাকতে পারে না
- গন্তব্য খুঁটিও খালি থাকতে হবে
অর্থাৎ বাকি k টা চাকতি সবগুলো তৃতীয় খুঁটিতে থাকতে হবে — একটা
সম্পূর্ণ বৈধ স্তূপ হিসেবে। সেখানে পৌঁছাতে অন্তত L(k) চাল।
তারপর বড় চাকতিটা সরানো: ১ চাল।
তারপর সেই k টা চাকতিকে গন্তব্যে আনা: আবার অন্তত L(k) চাল।
দুই অংশ মিলিয়ে: algorithm 2ⁿ − 1 দেয়, আর 2ⁿ − 1-এর কম সম্ভব নয়।
অতএব algorithm-টা optimal।
তাৎপর্য: এটা একটা lower bound proof — algorithm design-এর সবচেয়ে গুরুত্বপূর্ণ ধরনের প্রমাণ। এটা বলে “আরো ভালো algorithm খুঁজে লাভ নেই”।
কিংবদন্তির ৬৪ চাকতির Hanoi tower: 2⁶⁴ − 1 ≈ 1.8 × 10¹⁹ চাল।
সেকেন্ডে একটা চাল দিলে প্রায় ৫৮৫ বিলিয়ন বছর — মহাবিশ্বের বয়সের
৪২ গুণ।
Level 6-এ আমরা comparison sort-এর Ω(n log n) lower bound দেখব,
যেটা একই ধরনের যুক্তি।
4নিচের “প্রমাণ”-এ ভুল ধরুন:
“দাবি”: সব n ≥ 1-এর জন্য, n টা মুদ্রা দিয়ে যেকোনো পরিমাণ
≥ n টাকা বানানো যায় (৩ ও ৫ টাকার মুদ্রা দিয়ে)।
Base: n = 1… ৩ টাকা বানাতে ১টা মুদ্রা লাগে ✓
Step: ধরি k টা মুদ্রায় k টাকা বানানো যায়। আরেকটা মুদ্রা
যোগ করলে k+1 টাকাও বানানো যাবে ∎
যুক্তি
“দাবি”: সব n ≥ 1-এর জন্য, n টা মুদ্রা দিয়ে যেকোনো পরিমাণ
≥ n টাকা বানানো যায় (৩ ও ৫ টাকার মুদ্রা দিয়ে)।
Base: n = 1… ৩ টাকা বানাতে ১টা মুদ্রা লাগে ✓
Step: ধরি k টা মুদ্রায় k টাকা বানানো যায়। আরেকটা মুদ্রা
যোগ করলে k+1 টাকাও বানানো যাবে ∎
একাধিক ভুল আছে।
১. দাবিটাই অস্পষ্ট। “n টা মুদ্রা দিয়ে যেকোনো পরিমাণ ≥ n” —
এটা কী বলছে পরিষ্কার নয়। মুদ্রা সংখ্যা ঠিক n হতে হবে? নাকি
সর্বোচ্চ n?
২. Base case ভুল। n = 1-এ দাবি হলো “১টা মুদ্রায় যেকোনো
পরিমাণ ≥ 1 বানানো যায়”। কিন্তু ১টা মুদ্রায় শুধু ৩ বা ৫ টাকা হয় —
১, ২, ৪ টাকা হয় না। Base case মিথ্যা।
৩. Inductive step অর্থহীন। “আরেকটা মুদ্রা যোগ করলে k+1 টাকা
হবে” — কোন মুদ্রা? ৩ যোগ করলে k+3, ৫ যোগ করলে k+5। k+1 পাওয়ার
কোনো উপায় বলা হয়নি। IH ব্যবহারই হয়নি ঠিকভাবে।
সঠিক দাবি ও প্রমাণ:
দাবি:
n ≥ 8-এর প্রতিটা পূর্ণসংখ্যা টাকা ৩ আর ৫ টাকার মুদ্রা দিয়ে বানানো যায়।
Base cases (তিনটা লাগবে):
8 = 3 + 5✓9 = 3 + 3 + 3✓10 = 5 + 5✓
Inductive step: ধরি 8 ≤ j ≤ k-এর সব j বানানো যায় (strong IH),
আর k ≥ 10 নিন।
k + 1 ≥ 11, তাই (k+1) − 3 = k − 2 ≥ 8। Strong IH অনুযায়ী k−2
বানানো যায়। তার সাথে একটা ৩ টাকার মুদ্রা যোগ করলে k+1 ✓ ∎
কেন n = 8 থেকে: 1, 2, 4, 7 — এই চারটা বানানো যায় না।
(7: 3+3=6, 3+5=8 — মাঝে কিছু নেই।)
কেন তিনটা base case: inductive step k−2-এ ফিরে যায়, তাই
পরপর তিনটা মান জানা থাকতে হবে যাতে শৃঙ্খল ভাঙে না।
এটা Chicken McNugget theorem (বা Frobenius coin problem) নামে
পরিচিত: a আর b সহমৌলিক হলে বানানো যায় না এমন সর্বোচ্চ সংখ্যা
ab − a − b। এখানে 3·5 − 3 − 5 = 7 ✓
5Structural induction দিয়ে প্রমাণ করুন: যেকোনো binary tree-তে
leaves ≤ 2^height।
Tree-র সংজ্ঞা: Leaf -এর height 0; Node(L, R) -এর height
1 + max(height(L), height(R))।
প্রয়োগ
leaves ≤ 2^height।Leaf -এর height 0; Node(L, R) -এর height
1 + max(height(L), height(R))।P(T): “leaves(T) ≤ 2^height(T)”
Base case (T = Leaf):
leaves = 1, height = 0。
1 ≤ 2⁰ = 1 ✓
Inductive step (T = Node(L, R)):
ধরি P(L) আর P(R) সত্য (structural IH):
leaves(L) ≤ 2^height(L)
leaves(R) ≤ 2^height(R)ধরি h = height(T) = 1 + max(height(L), height(R))।
তাহলে height(L) ≤ h − 1 এবং height(R) ≤ h − 1।
তাৎপর্য — এটাই balanced tree-র ভিত্তি।
অসমতাটা উল্টে লিখুন:
অর্থাৎ n টা leaf রাখতে হলে height অন্তত log₂ n হতেই হবে।
এই সীমাটা বলে দেয়:
- কোনো binary tree
log₂ n-এর চেয়ে কম height-এnটা leaf রাখতে পারে না — তাই balanced BST-রO(log n)search-ই সর্বোত্তম সম্ভব - Comparison sort-এর decision tree-তে
n!টা সম্ভাব্য ফলাফল (leaf), তাই height≥ log₂(n!) = Θ(n log n)। এটাই comparison sort-এর বিখ্যাত lower bound - B-tree-তে branching factor
bহলে height≥ log_b n— এই কারণেই database index-এbবড় রাখা হয় (page-এর সমান), যাতে disk seek কম লাগে
Level 6-এ এই তিনটাই বিস্তারিত আসবে, আর Level 8-এ B-tree-র page আকার নির্বাচনের হিসাবটা এই অসমতা থেকেই আসবে।
এরপর কী
Logic আর proof — আমাদের ভিত্তি তৈরি। এখন আমরা যেকোনো দাবি প্রকাশ করতে পারি এবং প্রমাণ করতে পারি।
পরের কয়েকটা লেসনে আসছে সেই বস্তুগুলো যাদের নিয়ে আমরা কথা বলব:
- Sets — সব data structure-এর গাণিতিক পূর্বপুরুষ
- Relations — database-এর “relational” শব্দটা এখান থেকেই, আর dependency graph ও topological sort-ও
- Functions — hash function, injection, pigeonhole
- Combinatorics — গোনার শিল্প, যা complexity analysis-এর ভিত্তি
- Probability — hash collision, randomized algorithm, ML
এরপর graph theory — যেটা এই কারিকুলামের সবচেয়ে বেশি ব্যবহৃত গাণিতিক কাঠামো হতে চলেছে: network topology, dependency, compiler CFG, distributed system, filesystem — সব জায়গায় graph।
আরও পড়ুন
- Mathematics for Computer Science, Chapter 5 — Lehman, Leighton, Meyer
- Concrete Mathematics, Chapter 1 — Graham, Knuth, Patashnik · Recurrence আর summation-এর জন্য অতুলনীয়