Foundationপ্রথম নীতি থেকে
LEVEL 0লেসন ৬/১৬কঠিন১ ঘণ্টা ১৫ মিনিট

Mathematical Induction — অসীমকে সসীম যুক্তিতে ধরা

Mathematical Induction

Weak, strong আর structural induction — একমাত্র হাতিয়ার যা অসীম সংখ্যক ক্ষেত্র সসীম যুক্তিতে প্রমাণ করে। আর কেন recursion আর induction আসলে একই জিনিস।

এই লেসন শেষে আপনি পারবেন

  • Weak induction দিয়ে সংখ্যাগত দাবি প্রমাণ করতে পারবেন
  • কখন strong induction লাগে সেটা চিনতে পারবেন
  • Structural induction দিয়ে tree, list ও grammar-এর উপর দাবি প্রমাণ করতে পারবেন
  • একটা recursive function-এর correctness ও termination প্রমাণ করতে পারবেন
  • 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-এর হৃদয়।

দুইটা জিনিস প্রমাণ করলেই যথেষ্ট:

  1. প্রথমটা সত্য
  2. যেকোনোটা সত্য হলে তার পরেরটাও সত্য

তাহলে সবগুলোই সত্য — অসীম সংখ্যক দাবি, দুইটা যুক্তি দিয়ে।

মূল ধারণা

Weak induction — মৌলিক রূপ

P(n) একটা predicate। প্রমাণ করতে হবে ∀n ≥ n₀, P(n)

দুইটা ধাপ:

ধাপকী প্রমাণ করবেন
Base caseP(n₀) সত্য
Inductive step∀k ≥ n₀ : P(k) → P(k+1)

Inductive step-এ P(k) কে বলা হয় inductive hypothesis (IH) — এটা আপনি ধরে নিচ্ছেন, প্রমাণ করছেন না।

Formally, induction axiom:

[P(n0)    kn0(P(k)P(k+1))]    nn0P(n)\big[P(n_0) \;\wedge\; \forall k \ge n_0\,(P(k) \to P(k+1))\big] \;\to\; \forall n \ge n_0\, P(n)

প্রথম উদাহরণ

দাবি: ∀n ≥ 1: 1 + 2 + ⋯ + n = n(n+1)/2

প্রমাণ.

Base case (n = 1): বাঁ পাশ = 1। ডান পাশ = 1·2/2 = 1। সমান ✓

Inductive step: ধরি P(k) সত্য কোনো k ≥ 1-এর জন্য, অর্থাৎ

1+2++k=k(k+1)2(IH)1 + 2 + \cdots + k = \frac{k(k+1)}{2} \qquad \text{(IH)}

দেখাতে হবে P(k+1) সত্য:

1+2++k+(k+1)=(1++k)IH প্রয়োগ+(k+1)=k(k+1)2+(k+1)=(k+1)(k2+1)=(k+1)k+22=(k+1)((k+1)+1)2\begin{aligned} 1 + 2 + \cdots + k + (k+1) &= \underbrace{\big(1 + \cdots + k\big)}_{\text{IH প্রয়োগ}} + (k+1) \\[4pt] &= \frac{k(k+1)}{2} + (k+1) \\[4pt] &= (k+1)\left(\frac{k}{2} + 1\right) \\[4pt] &= (k+1)\cdot\frac{k+2}{2} \\[4pt] &= \frac{(k+1)\big((k+1)+1\big)}{2} \end{aligned}

এটাই P(k+1)। ∎

Strong induction — যখন আগেরটা যথেষ্ট নয়

কখনো P(k+1) প্রমাণ করতে শুধু P(k) যথেষ্ট নয় — আরো আগের কিছু লাগে।

Strong induction-এ IH বদলে যায়:

Inductive hypothesis
WeakP(k) সত্য
StrongP(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 proofRecursive function
P(n) — প্রমাণ করার দাবিFunction-এর specification
Base caseBase case (if n == 0: return …)
Inductive hypothesisRecursive call-টা ঠিক কাজ করে — এই বিশ্বাস
Inductive stepRecursive call-এর ফল ব্যবহার করে উত্তর বানানো
Well-ordering (termination)Argument ছোট হচ্ছে, base-এ পৌঁছাবে
Induction প্রমাণের প্রতিটা অংশ recursive function-এর একটা অংশের সাথে হুবহু মেলে।

উদাহরণ দিয়ে দেখি:

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)=2T(n/2)+Θ(n),T(1)=Θ(1)T(n) = 2T(n/2) + \Theta(n), \qquad T(1) = \Theta(1)

দাবি: 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 = dd ≥ 1 নিলে ✓

Step: ধরি n-এর চেয়ে ছোট সব দুইয়ের ঘাতের জন্য দাবি সত্য (strong IH)।

T(n)=2T(n/2)+n2[cn2log2n2+dn2]+n[IH]=cn(log2n1)+dn+n=cnlog2ncn+dn+n=cnlog2n+dn+n(1c)\begin{aligned} T(n) &= 2T(n/2) + n \\ &\le 2\left[c\cdot\frac{n}{2}\log_2\frac{n}{2} + d\cdot\frac{n}{2}\right] + n \quad \text{[IH]}\\ &= c\,n(\log_2 n - 1) + dn + n \\ &= c\,n\log_2 n - cn + dn + n \\ &= c\,n\log_2 n + dn + n(1 - c) \end{aligned}

c ≥ 1 নিলে n(1-c) ≤ 0, তাই T(n) ≤ c n log₂ n + dn ✓ ∎

Induction-এর সাধারণ ফাঁদ

একটা induction proof কোথায় ভাঙতে পারে
  1. Base case ভুল বা বাদশৃঙ্খল শুরুই হয় না
  2. ভুল base case নির্বাচনn₀ = 1 লিখেছেন কিন্তু step n ≥ 2 ধরে নেয়
  3. IH ব্যবহার না করাতাহলে এটা induction নয় — কিছু গোলমাল আছে
  4. IH ভুল জায়গায় প্রয়োগk-এর বদলে k+1-এর জন্য ধরে নেওয়া — circular
  5. Strong লাগলে weak ব্যবহারযে অংশটা লাগে সেটা IH-তে নেই
  6. লুকানো অনুমানঘোড়ার প্রমাণের overlap — n ≥ 2 ধরে নেওয়া
  7. 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

half×half=bmbm=b2m=be\texttt{half} \times \texttt{half} = b^m \cdot b^m = b^{2m} = b^e

Case 2: e বিজোড়। তাহলে e = 2m + 1 যেখানে m = e // 2 (পূর্ণসংখ্যা ভাগে ভগ্নাংশ বাদ যায়)।

half×half×b=bmbmb=b2m+1=be\texttt{half} \times \texttt{half} \times b = b^m \cdot b^m \cdot b = b^{2m+1} = b^e

দুই case-ই ঢাকা পড়েছে (প্রতিটা পূর্ণসংখ্যা হয় জোড় নয় বিজোড়)। ∎

Termination: প্রতিবার exp অন্তত অর্ধেক হয়। exp ≥ 1 হলে exp // 2 \< exp, তাই ক্রমটা কঠোরভাবে হ্রাসমান আর অ-ঋণাত্মক। Well-ordering অনুযায়ী 0-তে পৌঁছাবে ∎

Complexity: প্রতিটা call exp অর্ধেক করে, তাই recursion-এর গভীরতা ⌊log₂ exp⌋ + 1। প্রতিটা level-এ ধ্রুবক কাজ, তাই Θ(log exp) গুণ।

তুলনা করুন naive পদ্ধতির সাথে (exp বার গুণ):

expNaiveFast
10104
1,0001,00010
10⁶10⁶20
10⁹10⁹30

যেখানে induction ব্যর্থ হয় — একটা সাবধানবাণী

“দাবি”: সব n ≥ 1-এর জন্য n \< 100

“প্রমাণ.” Base: 1 \< 100Step: ধরি 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 থেকেই সত্য কি না।

নিজে চালিয়ে দেখুন

EXPERIMENT

Recursion depth-এর দেয়ালে ধাক্কা

Python 3 / C· ১৫ মিনিট
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 fault

Stack 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-এর সাথে সরাসরি যুক্ত।

EXPERIMENT

Recurrence-এর তাত্ত্বিক ও পরিমাপকৃত রূপ

Python 3· ১৫ মিনিট
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-এর বিষয়।

নিজে বানান

BUILD IT

Induction-এর নিয়মে recursive function লেখা

Python · ●●●○○
  1. প্রতিটা function-এর জন্য আগে P(n) লিখুন — কী দাবি করছেন
  2. Base case লিখুন এবং যাচাই করুন
  3. IH ধরে নিয়ে recursive case লিখুন
  4. Termination measure বলুন — কোন রাশি কমছে
  5. 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 সহজ করে।

নিজে বাড়ান:

  1. Fibonacci-র naive আর memoized version লিখুন, দুটোতেই contract দিন, আর call সংখ্যা তুলনা করুন
  2. reverse(list) লিখুন এই postcondition সহ: reverse(reverse(x)) == x
  3. Binary search-এর recursive version লিখুন contract সহ
  4. Tower of Hanoi লিখুন এবং প্রমাণ করুন চাল সংখ্যা ঠিক 2ⁿ − 1
  5. একটা 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 reasoningMathematical 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 থেকে সত্য।

n2ⁿ2ⁿ > n²?
121
244
389
41616
53225
66436

তাই 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 তার একটা বিশেষ ক্ষেত্র মাত্র।

বুঝেছেন কি না দেখুন

1

Induction দিয়ে প্রমাণ করুন: ∀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)।

P(S)=2k+2k=22k=2k+1|\mathcal{P}(S)| = 2^k + 2^k = 2 \cdot 2^k = 2^{k+1} \quad\blacksquare

একটা সুন্দর বিকল্প দৃষ্টি: প্রতিটা উপসেটকে একটা 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)
যুক্তি

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.618fib(50) চালাতে মিনিট লাগে। Memoization দিয়ে O(n), matrix exponentiation দিয়ে O(log n) — Level 6-এ দেখব।

3

Tower of Hanoi: 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:

M(0)=0,M(n)=2M(n1)+1M(0) = 0, \qquad M(n) = 2M(n-1) + 1

P(n): M(n) = 2ⁿ − 1

Base: M(0) = 0 = 2⁰ − 1 = 0

Step: ধরি M(k) = 2ᵏ − 1

M(k+1)=2M(k)+1=2(2k1)+1=2k+12+1=2k+11M(k+1) = 2M(k) + 1 = 2(2^k - 1) + 1 = 2^{k+1} - 2 + 1 = 2^{k+1} - 1 \quad\blacksquare

অংশ ২ — এর চেয়ে কম চালে সম্ভব নয়।

L(n) = সর্বনিম্ন প্রয়োজনীয় চাল। দেখাব L(n) ≥ 2ⁿ − 1

Base: L(0) = 0 ≥ 0

Step: ধরি L(k) ≥ 2ᵏ − 1

যেকোনো বৈধ সমাধানে, সবচেয়ে বড় চাকতিটাকে অন্তত একবার সরাতেই হবে। কিন্তু সেটা সরানোর মুহূর্তে:

  • তার উপরে কোনো চাকতি থাকতে পারে না
  • গন্তব্য খুঁটিও খালি থাকতে হবে

অর্থাৎ বাকি k টা চাকতি সবগুলো তৃতীয় খুঁটিতে থাকতে হবে — একটা সম্পূর্ণ বৈধ স্তূপ হিসেবে। সেখানে পৌঁছাতে অন্তত L(k) চাল।

তারপর বড় চাকতিটা সরানো: ১ চাল।

তারপর সেই k টা চাকতিকে গন্তব্যে আনা: আবার অন্তত L(k) চাল।

L(k+1)2L(k)+12(2k1)+1=2k+11L(k+1) \ge 2L(k) + 1 \ge 2(2^k - 1) + 1 = 2^{k+1} - 1 \quad\blacksquare

দুই অংশ মিলিয়ে: 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 টা মুদ্রা দিয়ে যেকোনো পরিমাণ ≥ n” — এটা কী বলছে পরিষ্কার নয়। মুদ্রা সংখ্যা ঠিক n হতে হবে? নাকি সর্বোচ্চ n?

২. Base case ভুল। n = 1-এ দাবি হলো “১টা মুদ্রায় যেকোনো পরিমাণ ≥ 1 বানানো যায়”। কিন্তু ১টা মুদ্রায় শুধু ৩ বা ৫ টাকা হয় — ১, ২, ৪ টাকা হয় না। Base case মিথ্যা।

৩. Inductive step অর্থহীন। “আরেকটা মুদ্রা যোগ করলে k+1 টাকা হবে” — কোন মুদ্রা? ৩ যোগ করলে k+3, ৫ যোগ করলে k+5k+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

5

Structural induction দিয়ে প্রমাণ করুন: যেকোনো binary tree-তে leaves ≤ 2^height

Tree-র সংজ্ঞা: 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 = 01 ≤ 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

leaves(T)=leaves(L)+leaves(R)2height(L)+2height(R)[IH]2h1+2h1=22h1=2h\begin{aligned} \text{leaves}(T) &= \text{leaves}(L) + \text{leaves}(R) \\ &\le 2^{\text{height}(L)} + 2^{\text{height}(R)} \quad \text{[IH]} \\ &\le 2^{h-1} + 2^{h-1} \\ &= 2 \cdot 2^{h-1} \\ &= 2^h \quad\blacksquare \end{aligned}

তাৎপর্য — এটাই balanced tree-র ভিত্তি।

অসমতাটা উল্টে লিখুন:

height(T)log2(leaves(T))\text{height}(T) \ge \log_2(\text{leaves}(T))

অর্থাৎ 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।

আরও পড়ুন