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

ক্যাপস্টোন — বাজারঘরের সমস্যায় discrete math

Capstone — Logic, Invariants, Check Digits, Counting, Graphs, Codes and Automata in One Project

Taught in Bangla, this practice page applies the whole course to seven questions about an invented shop. It finds conflicting delivery rules, proves a stock-loop invariant, shows a mod 11 check digit catches every single error and adjacent swap, counts coupon codes and collision risk, and compares a shortest-path tree with an MST, colours shifts and applies Hall's condition. Huffman, Hamming(7,4), a DFA and 2-SAT follow; in-page Python checks each answer, and a real Z3 run on a desktop proves two claims.

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

  • একটি ব্যবসায়িক প্রশ্নকে গাণিতিক বস্তুতে (সূত্র, সেট, graph, code, automaton) লেখা, আর সঠিক উপপাদ্য বাছা
  • প্রতিটি উত্তরের সঙ্গে ছোট প্রমাণ বা প্রমাণের খসড়া লেখা, তার শর্তগুলো মিলিয়ে
  • ব্রাউজারের Python-এ brute force আর হিসাব দিয়ে উত্তর যাচাই, আর brute force কোথায় থামে তা বোঝা
  • খেলনা উদাহরণ আর বাস্তব ব্যবস্থার পার্থক্য স্পষ্ট লেখা (বিশেষ করে RSA)
  • Z3 দিয়ে একটি invariant-এর সত্যিকারের প্রমাণ, নিজের কাজের মূল্যায়ন আর পরের ধাপের পরিকল্পনা
প্রকল্পের ব্রিফ

বাজারঘর একটি কাল্পনিক অনলাইন দোকান: এক শহরে ছয়টি হাব, কয়েকজন রাইডার, প্রতিদিন কয়েকশো অর্ডার। নতুন ব্যবস্থাপক সাতটি প্রশ্ন নিয়ে এসেছেন, আর প্রতিটির উত্তর চান তিন রূপে: একটি সংখ্যা বা সিদ্ধান্ত, তা কেন ঠিক তার যুক্তি, আর কোড যা যে কেউ আবার চালিয়ে মেলাতে পারেন। এই পাতার সব নাম, দূরত্ব, নিয়ম আর সংখ্যা বানানো; কোনো বাস্তব দোকান, রাস্তা বা মানুষ নয়।

এটি অনুশীলনের পর্ব: আগের ৭৭টি পর্বের অনেক অস্ত্র একটি প্রকল্পে। কাঠামো মেশিন লার্নিং কোর্সের পর্ব ৭২ আর AI-এর ভিত্তি কোর্সের পর্ব ৭৩-এর ক্যাপস্টোনের মতো, তবে এখানে মূল কাজ প্রমাণ আর গণনা। পাতার ১২টি ব্লক ব্রাউজারের Python-এ (Pyodide) চলে, শুধু standard library দিয়ে; প্রথমবার Python নামতে কয়েক সেকেন্ড লাগে। ব্লক ৯ আর ১০ একই namespace ভাগ করে (১০ চাপলে ৯ নিজেই আগে চলে), বাকিগুলো স্বাধীন।

১ · প্রকল্পের ছক — সাতটি প্রশ্ন, সাতটি হাতিয়ার

পাড়ার মিস্ত্রির ব্যাগে হাতুড়ি, স্ক্রু-ড্রাইভার, করাত, মাপার ফিতা থাকে। ভালো মিস্ত্রি সমস্যা দেখে আগে ঠিক করেন কোন যন্ত্র লাগবে, তারপর কাজ করেন, আর শেষে ফিতা দিয়ে মেপে দেখেন। এই প্রকল্পেও তা-ই: প্রশ্ন দেখে হাতিয়ার বাছা, প্রমাণ দিয়ে কাজ, আর কোড দিয়ে মাপা।

ধাপব্যবস্থাপকের প্রশ্নহাতিয়ার (পর্ব)
১ডেলিভারির নিয়মগুলো কি একসঙ্গে মানা যায়? কোন অর্ডারে ভাঙে?Propositional logic, consistency, entailment (০২, ০৭)
২মজুতের হিসাবের কোড কি কখনো ঋণাত্মক মজুত দেখাবে?Loop invariant, induction (১০, ১৩)
৩অর্ডার-ID-তে টাইপের ভুল ধরা, আর দিনের সারাংশে সইCheck digit, RSA (২৬, ২৮)
৪কুপন-কোড কতগুলো সম্ভব, আর ছোট কোডে মিলে যাওয়ার ঝুঁকি কত?গণনা, birthday (৩১–৩৫, ৪২)
৫হাবের পথ, কেবল, অডিটের শিফট আর রাইডার বণ্টনGraph (৬১, ৬৩, ৬৪, ৬৭)
৬অর্ডার-লগ ছোট করা আর পথের ভুল থেকে রক্ষাHuffman, Hamming (৬৮, ৭২)
৭অর্ডার-কোডের ফরম্যাট যাচাই, আর শিফটের অনুরোধ মেলানোDFA, 2-SAT (৭৪, ৭৭)

প্রতিটি ধাপ একই ছাঁচে চলবে। নিচের stepper ছাঁচটি ধাপ ৩-এর check digit দিয়ে দেখায়:

ছাঁচ আর বাজারঘরের উদাহরণ
ধাপ
উদাহরণে
  1. প্রশ্ন। ব্যবস্থাপকের ভাষায়: “গুদামের কর্মী ID ভুল টাইপ করলে সিস্টেম কি সঙ্গে সঙ্গে ধরবে?” এখানে এখনো কোনো গণিত নেই।
  2. মডেল। ভুলের ধরন ঠিক করা (একটি অঙ্ক বদল, পাশাপাশি দুটি অঙ্কের অদলবদল), আর নিয়মটি একটি congruence হিসেবে লেখা।
  3. প্রমাণ। কোন উপপাদ্য খাটে, আর তার শর্ত (এখানে modulus মৌলিক, ওজন 1–10-এর মধ্যে) মেলে কি না দেখা।
  4. যাচাই। কোডে সব সম্ভাব্য ভুল চেষ্টা করে দেখা যে প্রমাণের দাবি সংখ্যায়ও মেলে। কোড প্রমাণের বিকল্প নয়, পাহারাদার।
  5. সীমা। উত্তর কোথায় খাটে না, তা লিখে রাখা: দুটি আলাদা ভুল একসঙ্গে হলে check digit-এর কোনো নিশ্চয়তা নেই।

২ · ডেলিভারির নিয়ম — consistency, entailment আর বিরোধের মূল

বাজারঘরের ডেলিভারি দলের পাঁচটি নিয়ম। Variable: f = “ঠান্ডা পণ্য”, e = “এক্সপ্রেস”, b = “বাইকে যাবে”, v = “ফ্রিজ-ভ্যানে যাবে”, n = “রাতের স্লট”।

  • R1 ঠান্ডা পণ্য শুধু ফ্রিজ-ভ্যানে: f → v
  • R2 এক্সপ্রেস মানে বাইক: e → b
  • R3 প্রতিটি অর্ডার কোনো একটি বাহনে: b ∨ v
  • R4 একসঙ্গে দুই বাহনে নয়: ¬(b ∧ v)
  • R5 রাতে ফ্রিজ-ভ্যান চলে না: n → ¬v

তিনটি প্রশ্ন। (ক) নিয়মগুলো কি consistent, অর্থাৎ একসঙ্গে সত্য হওয়া সম্ভব? (খ) “ঠান্ডা পণ্য রাতে যায় না” কথাটি কি আলাদা করে লিখতে হবে, নাকি নিয়মগুলো থেকেই আসে (entailment)? (গ) একজন ক্রেতা ঠান্ডা পণ্য এক্সপ্রেসে চাইলে (F1: f, F2: e) কী হয়? পাঁচটি variable-এ মাত্র 25 = 32টি সারি, তাই পুরো truth table দেখাই একটি সম্পূর্ণ প্রমাণ।

from itertools import product, combinations
imp = lambda a, b: (not a) or b
# f ঠান্ডা পণ্য, e এক্সপ্রেস, b বাইক, v ভ্যান, n রাতের স্লট
R = {"R1": lambda f, e, b, v, n: imp(f, v),
     "R2": lambda f, e, b, v, n: imp(e, b),
     "R3": lambda f, e, b, v, n: b or v,
     "R4": lambda f, e, b, v, n: not (b and v),
     "R5": lambda f, e, b, v, n: imp(n, not v)}
F = {"F1": lambda f, e, b, v, n: f,       # অর্ডারটি ঠান্ডা পণ্যের
     "F2": lambda f, e, b, v, n: e}       # আর এক্সপ্রেস
rows = list(product([True, False], repeat=5))
def models(rules):
    return [r for r in rows if all(g(*r) for g in rules)]

print("R1-R5 সত্য করে", len(models(R.values())), "/ 32 সারি")
def entails(goal):                        # R ⊨ goal ⇔ R ∧ ¬goal-এর model নেই
    return all(goal(*r) for r in models(R.values()))
print("R ⊨ f -> ¬n ?", entails(lambda f, e, b, v, n: imp(f, not n)))
print("R ⊨ e -> ¬v ?", entails(lambda f, e, b, v, n: imp(e, not v)))
print("R ⊨ n -> e  ?", entails(lambda f, e, b, v, n: imp(n, e)))

allr = {**R, **F}
print("ঠান্ডা + এক্সপ্রেস অর্ডারে model:", len(models(allr.values())))
for k in range(1, len(allr) + 1):
    cores = [c for c in combinations(allr, k) if not models([allr[x] for x in c])]
    if cores:
        print("ক্ষুদ্রতম বিরোধ:", cores)
        break

আসল আউটপুট (ব্রাউজারের Python):

R1-R5 সত্য করে 6 / 32 সারি
R ⊨ f -> ¬n ? True
R ⊨ e -> ¬v ? True
R ⊨ n -> e  ? False
ঠান্ডা + এক্সপ্রেস অর্ডারে model: 0
ক্ষুদ্রতম বিরোধ: [('R1', 'R2', 'R4', 'F1', 'F2')]

৩২টি সারির ৬টিতে পাঁচ নিয়মই সত্য, তাই নিয়মগুলো consistent. “ঠান্ডা পণ্য রাতে নয়” আর “এক্সপ্রেস ভ্যানে নয়” নিয়ম থেকেই আসে, আলাদা লেখার দরকার নেই; কিন্তু “রাতের অর্ডার মানেই এক্সপ্রেস” আসে না। ঠান্ডা এক্সপ্রেস অর্ডারে কোনো model নেই, আর ক্ষুদ্রতম বিরোধ (unsat core) পাঁচটি শর্তের: R1, R2, R4, F1, F2. R3 আর R5 দোষী নয়।

কেন ঠিক (প্রমাণের খসড়া):

  1. Entailment হাতে: R1 দেয় f → v; R5-এর contrapositive v → ¬n. দুটি জুড়ে f → ¬n. — hypothetical syllogism
  2. বিরোধ হাতে: F1 আর R1 থেকে v; F2 আর R2 থেকে b; তাহলে b ∧ v, যা R4 ভাঙে। — modus ponens, দুবার
  3. Core ক্ষুদ্রতম: কোড ছোট সব উপসেট (১ থেকে ৪টি শর্তের) পরীক্ষা করেছে, কোনোটি বিরোধী নয়; তাই পাঁচটির যেকোনো একটি বাদ দিলেই বিরোধ মেটে। ∎ — সসীম ক্ষেত্রে brute force নিজেই প্রমাণ

সমাধান নিয়মের মালিকের হাতে: যেমন R2-কে “এক্সপ্রেস মানে বাইক, ঠান্ডা পণ্য বাদে” করা, অর্থাৎ (e ∧ ¬f) → b. শতাধিক নিয়মে brute force অচল, তখন SAT solver লাগে; অংশ ৯-এ Z3 একই core খুঁজে দেবে।

৩ · মজুতের হিসাব — loop invariant আর termination

গুদামে দিনের শুরুতে মজুত S0 ≥ 0. দিনভর ঘটনা আসে: মাল এলে (“in”, q) মজুত বাড়ে; অর্ডার এলে (“out”, q) মজুত থাকলে পাঠানো হয়, নইলে অর্ডার ফেরত যায়; প্রতিটি q ≥ 1. প্রশ্ন: কোডটি কি কখনো ঋণাত্মক মজুত দেখাতে পারে, আর শেষ মজুত কি সবসময় S0 + এল − গেল?

দাবি ৩.১ (মজুতের লুপ)

ব্লক ২-এর run লুপে loop invariant I = (stock = S0 + got − sent ∧ stock ≥ 0). তাই লুপ শেষে stock = S0 + got − sent আর stock ≥ 0.

  1. শুরুতে stock = S0, got = sent = 0: I সত্য, কারণ S0 ≥ 0. — initialization
  2. ধরি একটি ঘটনার আগে I সত্য। “in”: stock আর got দুটোই q বাড়ে, সমীকরণ টেকে, আর stock + q ≥ 0. — বীজগণিত
  3. “out” আর q ≤ stock: stock আর sent দুটোই q-এর হিসাবে বদলায়, সমীকরণ টেকে; stock − q ≥ 0, কারণ q ≤ stock. — শাখার শর্ত
  4. “out” আর q > stock: কিছুই বদলায় না, I টেকে। তাই প্রতিটি ঘটনার পরে I সত্য। — proof by cases; ঘটনার সংখ্যার ওপর induction
  5. লুপ থামে, কারণ variant “বাকি ঘটনার সংখ্যা” অঋণাত্মক পূর্ণসংখ্যা আর প্রতি ঘোরায় ১ কমে। থামলে I থেকেই দাবি। ∎ — well-ordering

লক্ষ করুন ধাপ ৩: “q ≤ stock” শর্তটিই stock ≥ 0 রক্ষা করে। ব্লকটি দাবিটি ২,০০০টি এলোমেলো দিনে assertion দিয়ে পাহারা দেয়, তারপর একটি ইচ্ছাকৃত bug (৫ ইউনিট “ধার”) ঢোকায়।

import random
random.seed(78)

def run(S0, events, slack=0):              # slack > 0 মানে ইচ্ছাকৃত bug
    stock, got, sent, rej = S0, 0, 0, 0
    for i, (kind, q) in enumerate(events):
        assert stock == S0 + got - sent and stock >= 0, (i, stock)   # I
        if kind == "in":                   # মাল এল
            stock, got = stock + q, got + q
        elif q <= stock + slack:           # অর্ডার: মজুত থাকলে পাঠানো
            stock, sent = stock - q, sent + q
        else:                              # নইলে ফেরত
            rej += 1
    assert stock == S0 + got - sent and stock >= 0, ("end", stock)   # Q
    return stock, got, sent, rej

days = [[(random.choice(["in", "out", "out"]), random.randint(1, 12))
         for _ in range(60)] for _ in range(2000)]
res = [run(20, d) for d in days]
print("২০০০ দিন x ৬০ ঘটনা: invariant সবখানে টিকেছে")
print("দিন ০: (শেষ মজুত, এল, গেল, ফেরত) =", res[0])
print("সবচেয়ে কম শেষ মজুত:", min(r[0] for r in res))

for k, d in enumerate(days):               # bug: ৫ ইউনিট ধার দেওয়া
    try:
        run(20, d, slack=5)
    except AssertionError as err:
        print("slack=5: দিন", k, "-তে ভাঙল, (ঘটনা, মজুত) =", err)
        break

আসল আউটপুট (ব্রাউজারের Python):

২০০০ দিন x ৬০ ঘটনা: invariant সবখানে টিকেছে
দিন ০: (শেষ মজুত, এল, গেল, ফেরত) = (4, 172, 188, 5)
সবচেয়ে কম শেষ মজুত: 0
slack=5: দিন 0 -তে ভাঙল, (ঘটনা, মজুত) = (4, -1)

১,২০,০০০টি ঘটনায় একবারও invariant ভাঙেনি; শেষ মজুত কখনো কখনো ঠিক ০, কিন্তু তার নিচে নয়। দিন ০-এ ২০ থেকে শুরু, ১৭২ এল, ১৮৮ গেল, শেষে ৪ = ২০ + ১৭২ − ১৮৮. Bug-এর সংস্করণ প্রথম দিনেই ধরা পড়ল: ঘটনা ৪-এর (০ থেকে গোনা) শুরুতে assertion দেখল মজুত −১, অর্থাৎ ঘটনা ৩ মজুতের চেয়ে বেশি পাঠিয়েছে। তবে ২,০০০ দিনে টেকা প্রমাণ নয়; প্রমাণ ওপরের পাঁচ ধাপ, আর অংশ ৯-এ Z3 তা সব পূর্ণসংখ্যায় যাচাই করবে।

৪ · অর্ডার-ID-র check digit আর দিনের সারাংশে খেলনা সই

বাজারঘরের অর্ডার-ID: “BG”, তারপর ছয় অঙ্ক d1…d6, শেষে একটি check digit c. নিয়ম: 7d1 + 6d2 + 5d3 + 4d4 + 3d5 + 2d6 + c ≡ 0 (mod 11); c = 10 হলে লেখা হয় “X”. যেমন payload 047213-এ যোগফল 76 ≡ 10, তাই c = 1, ID “BG0472131”.

দাবি ৪.১

এই পদ্ধতি প্রতিটি একক অঙ্কের ভুল আর যেকোনো দুটি অবস্থানের দুটি ভিন্ন অঙ্কের অদলবদল (transposition error) ধরে।

  1. পর্ব ২৬-এর lemma: একক ভুল ফসকায় ⇔ 11 ∣ wi(d′ − d); অদলবদল ফসকায় ⇔ 11 ∣ (wi − wj)(a − b). — যোগফলের পার্থক্য
  2. এখানে 1 ≤ wi ≤ 7, 1 ≤ |wi − wj| ≤ 6, আর 1 ≤ |d′ − d|, |a − b| ≤ 10. — আকারের সীমা
  3. 11 মৌলিক, তাই গুণফলকে ভাগ করলে কোনো একটি গুণককে ভাগ করত; কিন্তু 1 থেকে 10-এর কোনো সংখ্যা 11-এর গুণিতক নয়। ∎ — Euclid-এর lemma
import random
random.seed(26)
W11 = [7, 6, 5, 4, 3, 2, 1]                # ৬টি অঙ্ক + check, mod 11
W10 = [1, 3, 1, 3, 1, 3, 1]               # তুলনার জন্য: EAN-ধাঁচ, mod 10

def val(ch):
    return 10 if ch == "X" else int(ch)
def valid(code, W, m):
    return sum(w * val(ch) for w, ch in zip(W, code)) % m == 0
def make(p, W, m):                         # p = ৬ অঙ্কের payload
    c = -sum(w * int(x) for w, x in zip(W, p)) % m
    return p + ("X" if c == 10 else str(c))

pay = [f"{random.randrange(10**6):06d}" for _ in range(1000)]
print("নমুনা id:", ["BG" + make(p, W11, 11) for p in pay[:3]])
for W, m in ((W11, 11), (W10, 10)):
    ids = [make(p, W, m) for p in pay]
    alpha = "0123456789X" if m == 11 else "0123456789"
    s1 = m1 = s2 = m2 = 0
    for code in ids:
        for i in range(7):
            for ch in alpha:
                if ch != code[i]:
                    s1 += 1; m1 += valid(code[:i] + ch + code[i+1:], W, m)
            if i < 6 and code[i] != code[i + 1]:    # পাশাপাশি অদলবদল
                t = code[:i] + code[i + 1] + code[i] + code[i + 2:]
                s2 += 1; m2 += valid(t, W, m)
    print(f"mod {m}: একক ভুল {s1}, ফসকেছে {m1} | অদলবদল {s2}, ফসকেছে {m2}")

আসল আউটপুট (ব্রাউজারের Python):

নমুনা id: ['BG7835914', 'BG9507779', 'BG212601X']
mod 11: একক ভুল 70000, ফসকেছে 0 | অদলবদল 5431, ফসকেছে 0
mod 10: একক ভুল 63000, ফসকেছে 0 | অদলবদল 5410, ফসকেছে 641

হাজার ID-তে ৭০,০০০টি একক ভুল আর ৫,৪৩১টি পাশাপাশি অদলবদল, একটিও ফসকায়নি, প্রমাণ যেমন বলে। তুলনার mod 10 পদ্ধতিও (ওজন 1, 3, …) সব একক ভুল ধরে, কারণ 1 আর 3 দুটোই 10-এর সহমৌলিক; কিন্তু ৫,৪১০টি অদলবদলের ৬৪১টি (১১.৮%) ফসকায়। এগুলো ঠিক সেই জোড়া, যেখানে |a − b| = 5: তখন (3 − 1)(a − b) = ±10. দাম হলো “X” অক্ষর; বাজারঘর তা মেনে নিলে ID-এর বর্ণমালায় X যোগ হয় (অংশ ৮-এর DFA এটি মনে রাখবে)।

দিনের সারাংশে সই — শুধু খেলনা

সতর্কতা: নিচের RSA শুধু গণিত বোঝার খেলনা। ছোট n, “SHA-256 mod n”-এর মতো ছোট hash আর padding-ছাড়া textbook RSA কোনো আসল তথ্যে কখনো ব্যবহার করবেন না। বাস্তবে বড় key, PSS-এর মতো padding আর পরীক্ষিত লাইব্রেরি লাগে: Cybersecurity কোর্সের পর্ব ১০, পর্ব ১১ আর পর্ব ১৪.

প্রতিদিন রাতে গুদাম হিসাব-বিভাগে একটি সারাংশ পাঠায়। হিসাব-বিভাগ জানতে চায় সারাংশটি গুদামেরই আর পথে বদলায়নি। পর্ব ২৮-এর hash-then-sign রীতি: h = H(M), সই s = hd mod n, যাচাই se ≡ h (mod n). সঠিকতা পর্ব ২৮-এর উপপাদ্য থেকে: se = hed ≡ h. Key সেই পর্বের (n, e, d) = (3233, 17, 2753).

import hashlib
n, e, d = 3233, 17, 2753                   # খেলনা key: n = 61 · 53 (শুধু শেখার জন্য)

def H(msg):                                # খেলনা hash: SHA-256 mod n
    return int.from_bytes(hashlib.sha256(msg.encode()).digest(), "big") % n

summary = "2026-11-12|orders=137|cod=88|total=245610"
h = H(summary)
s = pow(h, d, n)                           # সই: h^d mod n
print("h =", h, "| s =", s, "| যাচাই s^e ≡ h:", pow(s, e, n) == h)

fake = summary.replace("245610", "254610")
print("এক অঙ্ক বদলানো সারাংশ:", pow(s, e, n) == H(fake))

# দুর্বলতা ১: h মাত্র 3233টি মান নিতে পারে, তাই মিল খোঁজা সহজ
for k in range(1, 10**6):
    forged = f"2026-11-12|orders=137|cod=88|total={k}"
    if H(forged) == h:
        print(k, "নম্বর চেষ্টায় একই h-এর জাল সারাংশ:", forged)
        print("  পুরোনো সই দিয়েই যাচাই:", pow(s, e, n) == H(forged))
        break

# দুর্বলতা ২: ছোট n ভাগ করে d বের করা
p = next(q for q in range(2, n) if n % q == 0)
print("n =", p, "x", n // p, "| d =", pow(e, -1, (p - 1) * (n // p - 1)))

আসল আউটপুট (ব্রাউজারের Python):

h = 796 | s = 637 | যাচাই s^e ≡ h: True
এক অঙ্ক বদলানো সারাংশ: False
558 নম্বর চেষ্টায় একই h-এর জাল সারাংশ: 2026-11-12|orders=137|cod=88|total=558
  পুরোনো সই দিয়েই যাচাই: True
n = 53 x 61 | d = 2753

সই কাজ করে, আর একটি অঙ্ক বদলালে যাচাই ব্যর্থ হয়। কিন্তু খেলনাটি দুই জায়গায় ভাঙে। প্রথমত, h মাত্র 3233টি মানের একটি, তাই একই h-এর অন্য একটি সারাংশ (“total=558”) মাত্র ৫৫৮টি চেষ্টায় মিলল, আর পুরোনো সই তাতেও বৈধ: জালিয়াতির জন্য d জানারই দরকার হয়নি। দ্বিতীয়ত, 3233 ভাগ করতে এক মুহূর্ত লাগে, আর তারপর d বের হয়ে যায়। বাস্তবের ২০৪৮-bit বা তার বড় n আর পূর্ণ দৈর্ঘ্যের hash এই দুই পথই কার্যত বন্ধ করে, যদিও কেবল তাতেই নিরাপত্তা আসে না (পর্ব ২৮)।

৫ · কুপন-কোড গোনা আর ছোট কোডে মিলে যাওয়ার ঝুঁকি

কুপন-কোডের বর্ণমালা: ২৪টি বড় হাতের অক্ষর (I আর O বাদ, কারণ 1 আর 0-এর সঙ্গে গুলিয়ে যায়) আর ৮টি অঙ্ক (2–9), মোট ৩২টি চিহ্ন; দৈর্ঘ্য ৬. পাঁচটি প্রশ্ন আর তাদের নিয়ম:

  • মোট: গুণের নিয়ম, 326.
  • অন্তত একটি অঙ্ক: complement-এর নিয়মে 326 − 246.
  • ঠিক ২টি অঙ্ক: কোন দুই ঘরে অঙ্ক তা C(6, 2), তারপর 82 · 244.
  • কোনো চিহ্ন দুবার নয়: P(32, 6) = 32 · 31 ⋯ 27.
  • ক্রমহীন, অর্থাৎ শুধু কোন চিহ্ন কয়বার (multiset): stars and bars, C(32 + 6 − 1, 6).

সূত্রে ভুল হলে বড় সংখ্যায় তা ধরা অসম্ভব, তাই আগে ছোট বর্ণমালায় সব কোড বানিয়ে গুনে মেলাই।

from itertools import product
from math import comb, perm

def formulas(L, D, k):                     # L অক্ষর, D অঙ্ক, দৈর্ঘ্য k
    A = L + D
    return {"মোট": A**k,
            "অন্তত একটি অঙ্ক": A**k - L**k,
            "ঠিক ২টি অঙ্ক": comb(k, 2) * D**2 * L**(k - 2),
            "কোনো চিহ্ন দুবার নয়": perm(A, k),
            "ক্রমহীন (multiset)": comb(A + k - 1, k)}

def brute(letters, digits, k):             # সব কোড বানিয়ে গোনা
    cs = list(product(letters + digits, repeat=k))
    nd = lambda c: sum(ch in digits for ch in c)
    return {"মোট": len(cs),
            "অন্তত একটি অঙ্ক": sum(nd(c) >= 1 for c in cs),
            "ঠিক ২টি অঙ্ক": sum(nd(c) == 2 for c in cs),
            "কোনো চিহ্ন দুবার নয়": sum(len(set(c)) == k for c in cs),
            "ক্রমহীন (multiset)": len({tuple(sorted(c)) for c in cs})}

for letters, digits, k in [("ABC", "23", 4), ("ABCD", "234", 5)]:
    print(letters, digits, k, "→ সূত্র = brute force:",
          brute(letters, digits, k) == formulas(len(letters), len(digits), k))

print("২৪ অক্ষর + ৮ অঙ্ক, দৈর্ঘ্য ৬:")
for name, v in formulas(24, 8, 6).items():
    print(f"  {name}: {v:,}")

আসল আউটপুট (ব্রাউজারের Python):

ABC 23 4 → সূত্র = brute force: True
ABCD 234 5 → সূত্র = brute force: True
২৪ অক্ষর + ৮ অঙ্ক, দৈর্ঘ্য ৬:
  মোট: 1,073,741,824
  অন্তত একটি অঙ্ক: 882,638,848
  ঠিক ২টি অঙ্ক: 318,504,960
  কোনো চিহ্ন দুবার নয়: 652,458,240
  ক্রমহীন (multiset): 2,324,784

দুটি ছোট বর্ণমালায় (৬২৫ আর ১৬,৮০৭টি কোড) পাঁচটি সূত্রই brute force-এর সঙ্গে হুবহু মেলে। তারপর আসল উত্তর: প্রায় ১০৭ কোটি কোড, যার প্রায় ৮২% অন্তত একটি অঙ্কওয়ালা। Multiset মাত্র ২৩ লাখের মতো, কারণ “AB2345” আর “5432BA” একই multiset; কুপনে ক্রম গুরুত্বপূর্ণ, তাই এই সংখ্যা শুধু তুলনার জন্য।

এবার ঝুঁকি। ধরুন দ্রুত টাইপের জন্য ৪ চিহ্নের ছোট কোড, N = 324 = 1,048,576টি সম্ভাব্য মান, আর প্রতিটি নতুন কোড এলোমেলোভাবে বাছা। k-টি কোডে সবগুলো আলাদা হওয়ার সম্ভাবনা ∏i=0k−1(1 − i/N), পর্ব ৪২-এর জন্মদিনের সূত্র। দুটি সীমাও কাজে লাগবে: 1 − x ≤ e−x থেকে নিচের সীমা 1 − e−k(k−1)/2N, আর union bound থেকে ওপরের সীমা C(k, 2)/N: প্রতিটি জোড়া মেলার সম্ভাবনা 1/N, আর জোড়া C(k, 2)টি।

from math import exp, sqrt, comb

def p_clash(k, N):                         # 1 − ∏ (1 − i/N), i = 0..k−1
    q = 1.0
    for i in range(k):
        q *= 1 - i / N
    return 1 - q

N = 32 ** 4                                # ৪ চিহ্নের ছোট কোড
print("N =", N)
for k in (100, 500, 1000, 1206, 2000):
    approx = 1 - exp(-k * (k - 1) / (2 * N))
    print(f"k = {k:>4}: সূত্রে {p_clash(k, N):.4f} | আসন্ন {approx:.4f}"
          f" | ওপরের সীমা C(k,2)/N {min(1, comb(k, 2) / N):.4f}")
k50 = next(k for k in range(1, 5000) if p_clash(k, N) >= 0.5)
print("প্রথম k যেখানে ≥ 0.5:", k50, "| 1.1774·√N ≈", round(1.1774 * sqrt(N), 1))

K = 100_000                                # এক বছরের কোড
for L in range(4, 12):
    if comb(K, 2) / 32 ** L <= 0.001:      # union bound: নিশ্চিত সীমা
        print(f"{K:,} কোডে P(মিল) ≤ 0.001 নিশ্চিত: দৈর্ঘ্য {L},",
              f"সীমা {comb(K, 2) / 32 ** L:.6f}, সূত্রে {p_clash(K, 32 ** L):.6f}")
        break

আসল আউটপুট (ব্রাউজারের Python):

N = 1048576
k =  100: সূত্রে 0.0047 | আসন্ন 0.0047 | ওপরের সীমা C(k,2)/N 0.0047
k =  500: সূত্রে 0.1122 | আসন্ন 0.1122 | ওপরের সীমা C(k,2)/N 0.1190
k = 1000: সূত্রে 0.3791 | আসন্ন 0.3790 | ওপরের সীমা C(k,2)/N 0.4764
k = 1206: সূত্রে 0.5000 | আসন্ন 0.4999 | ওপরের সীমা C(k,2)/N 0.6930
k = 2000: সূত্রে 0.8516 | আসন্ন 0.8514 | ওপরের সীমা C(k,2)/N 1.0000
প্রথম k যেখানে ≥ 0.5: 1206 | 1.1774·√N ≈ 1205.7
100,000 কোডে P(মিল) ≤ 0.001 নিশ্চিত: দৈর্ঘ্য 9, সীমা 0.000142, সূত্রে 0.000142

দশ লাখের বেশি কোড থাকলেও ১,২০৬টিতেই মিলের সম্ভাবনা অর্ধেক, ঠিক পর্ব ৪২-এর 1.1774√N নিয়ম, অর্থাৎ birthday bound-এর হিসাব। আসন্নমান প্রায় নির্ভুল, আর ওপরের সীমা ছোট k-তে কাছাকাছি, কিন্তু বড় k-তে ১-এ আটকে অর্থহীন। এক বছরে ১,০০,০০০ কোডে মিলের সম্ভাবনা ০.১%-এর নিচে নিশ্চিত করতে দৈর্ঘ্য ৯ লাগে; union bound সীমাটি নিশ্চয়তা দেয়, কারণ তা কোনো আসন্নমান নয়। বাস্তবে আরেকটি সহজ পথও আছে: নতুন কোড বানিয়ে ডেটাবেসে আগে থেকে আছে কি না দেখা।

৬ · হাবের নেটওয়ার্ক — পথ, কেবল, শিফট আর রাইডার

ছয়টি হাব, নয়টি রাস্তা; সংখ্যাগুলো কিলোমিটার, বানানো। একই weighted graph-এ তিনটি আলাদা প্রশ্ন: গুদাম A থেকে প্রতিটি হাবে সবচেয়ে ছোট পথ; সব হাবকে নেটওয়ার্ক কেবলে জুড়তে সবচেয়ে কম কেবল; আর পাশাপাশি হাবের (যারা কর্মী ভাগ করে) মজুত-অডিট একই শিফটে না রেখে সবচেয়ে কম শিফট।

বাজারঘরের ছয় হাব আর রাস্তা (কিমি, কাল্পনিক)

সবুজ মোটা রেখা: minimum spanning tree (মোট ১৪ কিমি)। ধূসর রেখা MST-তে নেই। গুদাম A থেকে দূরত্ব: A 0, C 3, B 6, D 8, E 11, F 13.

Six hubs with road lengths in km; the minimum spanning tree is drawn in thick green 3 7 3 2 6 8 4 7 2 ACB DEF A আমবাগান (গুদাম) · B বটতলা C চরপাড়া · D দীঘিরপাড় E ঈদগাহ মোড় · F ফুলতলী

A থেকে Dijkstra হাতে চালাই। প্রতি ধাপে সবচেয়ে ছোট d-এর অমীমাংসিত হাব settle হয়, আর তার প্রতিবেশীদের d কমানো (relaxation) হয়:

d(v) আর settled সেট
settle
A B C
D E F
  1. A settle (d = 0). প্রতিবেশী B আর C: d(B) = 7, d(C) = 3. বাকিরা এখনো ∞.
  2. C settle (d = 3). সবচেয়ে ছোট অমীমাংসিত d. C দিয়ে B-তে 3 + 3 = 6 < 7, তাই d(B) = 6; D-তে 3 + 6 = 9; E-তে 3 + 8 = 11.
  3. B settle (d = 6). B দিয়ে D-তে 6 + 2 = 8 < 9, তাই d(D) = 8. সরাসরি A–B রাস্তা (7) আর কোনো কাজে লাগবে না।
  4. D settle (d = 8). D দিয়ে E-তে 8 + 4 = 12, যা 11-এর চেয়ে বড়, বদল নেই; F-এ 8 + 7 = 15.
  5. E settle (d = 11). E দিয়ে F-এ 11 + 2 = 13 < 15, তাই d(F) = 13.
  6. F settle (d = 13). সব হাব settled. পথের tree: A–C, C–B, B–D, C–E, E–F.
import heapq
E = [("A", "B", 7), ("A", "C", 3), ("B", "C", 3), ("B", "D", 2), ("C", "D", 6),
     ("C", "E", 8), ("D", "E", 4), ("D", "F", 7), ("E", "F", 2)]   # কিমি, কাল্পনিক
W = {frozenset((u, v)): w for u, v, w in E}
adj = {}
for u, v, w in E:
    adj.setdefault(u, []).append((v, w)); adj.setdefault(v, []).append((u, w))
dist, par, pq, done = {"A": 0}, {"A": None}, [(0, "A")], []   # Dijkstra, গুদাম A থেকে
while pq:
    du, u = heapq.heappop(pq)
    if u in done: continue
    done.append(u)
    for v, w in adj[u]:
        if du + w < dist.get(v, float("inf")):
            dist[v], par[v] = du + w, u
            heapq.heappush(pq, (du + w, v))
print("settle-এর ক্রম:", done)
print("দূরত্ব:", dict(sorted(dist.items())))
spt = [(par[v], v) for v in sorted(par) if par[v]]
print("পথের tree:", spt, "| মোট:", sum(W[frozenset(e)] for e in spt), "কিমি")

root = {v: v for v in adj}                                  # Kruskal + union-find
def find(x):
    while root[x] != x: x = root[x]
    return x
mst = []
for u, v, w in sorted(E, key=lambda t: t[2]):
    if find(u) != find(v):
        root[find(u)] = find(v); mst.append((u, v, w))
print("MST:", mst, "| মোট:", sum(w for *_, w in mst), "কিমি")

আসল আউটপুট (ব্রাউজারের Python):

settle-এর ক্রম: ['A', 'C', 'B', 'D', 'E', 'F']
দূরত্ব: {'A': 0, 'B': 6, 'C': 3, 'D': 8, 'E': 11, 'F': 13}
পথের tree: [('C', 'B'), ('A', 'C'), ('B', 'D'), ('C', 'E'), ('E', 'F')] | মোট: 18 কিমি
MST: [('B', 'D', 2), ('E', 'F', 2), ('A', 'C', 3), ('B', 'C', 3), ('D', 'E', 4)] | মোট: 14 কিমি

দূরত্ব stepper-এর সঙ্গে মেলে। সবচেয়ে ছোট পথের tree (shortest path tree) মোট ১৮ কিমি, কিন্তু MST ১৪ কিমি, আর তাতে C–E (৮) নেই। উল্টো দিকে MST ধরে A থেকে E যেতে A–C–B–D–E = ১২ কিমি, অথচ সবচেয়ে ছোট পথ ১১. দুই প্রশ্ন আলাদা: পথের tree প্রতিটি হাবের দূরত্ব কমায়, MST সব রাস্তার যোগফল।

কেন ঠিক: Dijkstra-র সঠিকতা পর্ব ৬১-এর settled সেটের ওপর induction, যার মূল ধাপে লাগে সব weight অঋণাত্মক; এখানে সব রাস্তার দৈর্ঘ্য ধনাত্মক, তাই শর্ত মেলে। Kruskal-এর প্রতিটি পছন্দ পর্ব ৬৭-এর cut property: নেওয়া edge তার দুই প্রান্তের দলের মধ্যকার cut-এর সবচেয়ে হালকা edge. সমান weight আছে (দুটি 2, দুটি 3), তাই অনন্যতার নিশ্চয়তা নেই, কিন্তু মোট ১৪ যেকোনো MST-এর।

এবার শিফট আর রাইডার। অডিটের প্রশ্নটি chromatic number: হাব = vertex, রাস্তা = “কর্মী ভাগ করে”, শিফট = রং। রাইডারের প্রশ্নটি bipartite graph-এ maximum matching: পাঁচটি অর্ডার O1–O5, পাঁচজন রাইডার R1–R5, আর কে কোন অর্ডার নিতে পারেন তার তালিকা (দূরত্ব আর বাহন দেখে)।

from itertools import product, combinations
E = ["AB", "AC", "BC", "BD", "CD", "CE", "DE", "DF", "EF"]   # হাবের রাস্তা
V = "ABCDEF"
ok = lambda col: all(col[u] != col[v] for u, v in E)
greedy = {}
for v in V:                                # বর্ণক্রমে greedy
    used = {greedy[u] for u in V if u in greedy and (u + v in E or v + u in E)}
    greedy[v] = min(c for c in range(6) if c not in used)
print("greedy শিফট:", greedy, "| বৈধ:", ok(greedy))
for k in (1, 2, 3):                        # brute force: কত রঙে সম্ভব
    n = sum(ok(dict(zip(V, c))) for c in product(range(k), repeat=6))
    print(f"{k} শিফটে বৈধ বণ্টন: {n}")

can = {"O1": ["R1", "R2"], "O2": ["R1", "R3"], "O3": ["R4", "R5"],
       "O4": ["R4", "R5"], "O5": ["R4"]}   # কোন অর্ডার কোন রাইডার নিতে পারেন
match = {}                                 # রাইডার -> অর্ডার
def augment(o, seen):                      # augmenting path খোঁজা
    for r in can[o]:
        if r not in seen:
            seen.add(r)
            if r not in match or augment(match[r], seen):
                match[r] = o
                return True
    return False
size = sum(augment(o, set()) for o in can)
print("সবচেয়ে বড় matching:", size, dict(sorted(match.items())))
bad = [S for k in range(1, 6) for S in combinations(can, k)
       if len({r for o in S for r in can[o]}) < len(S)]
print("Hall ভাঙে এমন ক্ষুদ্রতম দল:", min(bad, key=len))

আসল আউটপুট (ব্রাউজারের Python):

greedy শিফট: {'A': 0, 'B': 1, 'C': 2, 'D': 0, 'E': 1, 'F': 2} | বৈধ: True
1 শিফটে বৈধ বণ্টন: 0
2 শিফটে বৈধ বণ্টন: 0
3 শিফটে বৈধ বণ্টন: 6
সবচেয়ে বড় matching: 4 {'R1': 'O2', 'R2': 'O1', 'R4': 'O4', 'R5': 'O3'}
Hall ভাঙে এমন ক্ষুদ্রতম দল: ('O3', 'O4', 'O5')

Greedy ৩টি শিফট দেয়, আর brute force বলে ২টিতে অসম্ভব: তাই χ = 3. বৈধ ৩-শিফট বণ্টন ঠিক ৬টি = 3!, অর্থাৎ রঙের নাম অদলবদল ছাড়া বণ্টন একটিই। রাইডারে সবচেয়ে বড় matching ৪, পাঁচ নয়; কারণ O3, O4, O5 তিনটি অর্ডার মিলে মাত্র দুজন রাইডার (R4, R5) চেনে।

কেন ঠিক (দুই দিকের সীমা): A, B, C পরস্পর সংলগ্ন, একটি ত্রিভুজ (clique), তাই χ ≥ 3; greedy একটি বৈধ ৩-রং দিয়েছে, তাই χ ≤ 3. Matching-এ Hall-এর উপপাদ্য: সব অর্ডারকে রাইডার দেওয়া যায় ⇔ প্রতিটি অর্ডার-দল X-এ |N(X)| ≥ |X|; X = {O3, O4, O5}-এ |N(X)| = 2 < 3, তাই perfect matching নেই। আর ৪-ই সবচেয়ে বড়, তার প্রমাণপত্র König: {O1, O2, R4, R5} চারটি vertex প্রতিটি edge ছোঁয় (vertex cover), তাই কোনো matching ৪-এর বেশি হতে পারে না। ব্যবস্থাপকের জন্য বার্তা: আরেকজন রাইডারকে O3–O5-এর এলাকায় পাঠান, নইলে একটি অর্ডার দেরিতে যাবে।

৭ · অর্ডার-লগ — Huffman-এ ছোট, Hamming-এ নিরাপদ

প্রতিটি অর্ডারের অবস্থা পাঁচটির একটি: D (ডেলিভারি হয়েছে), T (পথে), P (প্যাক হচ্ছে), R (ফেরত), C (বাতিল)। একটি দিনের ১০০টি অবস্থায় D ৪৫, T ২৫, P ১৫, R ১০, C ৫. Fixed-length-এ ৫টি চিহ্নের জন্য ৩ bit লাগে। Huffman কত কমায়, আর সেই ছোট লগ ভুলপ্রবণ পথে পাঠালে কী হয়?

import heapq, random
from collections import Counter
from math import log2
random.seed(68)
# D ডেলিভারি হয়েছে, T পথে, P প্যাক হচ্ছে, R ফেরত, C বাতিল (কাল্পনিক লগ)
log = list("D" * 45 + "T" * 25 + "P" * 15 + "R" * 10 + "C" * 5)
random.shuffle(log)
freq = Counter(log)
heap = [(f, i, {s: ""}) for i, (s, f) in enumerate(sorted(freq.items()))]
heapq.heapify(heap)
nxt = len(heap)
while len(heap) > 1:                       # সবচেয়ে হালকা দুটি জোড়া
    f1, _, a = heapq.heappop(heap)
    f2, _, b = heapq.heappop(heap)
    merged = {s: "0" + c for s, c in a.items()} | {s: "1" + c for s, c in b.items()}
    heapq.heappush(heap, (f1 + f2, nxt, merged)); nxt += 1
code = heap[0][2]
print("code:", dict(sorted(code.items(), key=lambda t: len(t[1]))))
bits = "".join(code[s] for s in log)
N = len(log)
H = -sum(f / N * log2(f / N) for f in freq.values())
print(f"bit: Huffman {len(bits)}, fixed 3-bit {3 * N}; গড় {len(bits) / N:.2f}, H = {H:.3f}")
back = {c: s for s, c in code.items()}
def decode(b):                             # prefix code: বাঁ থেকে পড়া
    out, cur = [], ""
    for x in b:
        cur += x
        if cur in back: out.append(back[cur]); cur = ""
    return out
print("ফেরত পড়া নিখুঁত:", decode(bits) == log, "| প্রথম ১২টি:", "".join(log[:12]))

আসল আউটপুট (ব্রাউজারের Python):

code: {'D': '0', 'T': '10', 'P': '110', 'C': '1110', 'R': '1111'}
bit: Huffman 200, fixed 3-bit 300; গড় 2.00, H = 1.977
ফেরত পড়া নিখুঁত: True | প্রথম ১২টি: DDRTCTTDTDDD

দৈর্ঘ্য ১, ২, ৩, ৪, ৪; গড় ঠিক ২.০০ bit, অর্থাৎ ৩০০ থেকে ২০০ bit. Entropy H = 1.977, আর পর্ব ৬৮-এর সীমা H ≤ L < H + 1 মেলে: 1.977 ≤ 2.00 < 2.977. সমতা আসেনি, কারণ সম্ভাবনাগুলো 2-এর ঋণাত্মক ঘাত নয়। কোনো codeword অন্যটির শুরু নয় (prefix code), তাই বিভাজক ছাড়াই লগ ফেরত পড়া গেল।

কেন সর্বোত্তম: পর্ব ৬৮-এর দুই lemma: সবচেয়ে বিরল দুটি চিহ্ন (C, R) কোনো সর্বোত্তম tree-তে সবচেয়ে গভীরে ভাই হতে পারে, আর তাদের জোড়া দিলে চিহ্ন এক কমে একই সমস্যা হয়; চিহ্নের সংখ্যার ওপর induction. এবার এই ২০০ bit-কে ৪-bit টুকরোয় ভেঙে প্রতিটিকে Hamming(7,4)-এ পাঠাই।

G = ["1110000", "1001100", "0101010", "1101001"]   # Hamming(7,4)-এর সারি
def enc(d4):
    c = 0
    for bit, row in zip(d4, G):
        if bit == "1": c ^= int(row, 2)
    return format(c, "07b")
def dec(r):                                # syndrome = 1 থাকা অবস্থানগুলোর XOR
    s = 0
    for pos, bit in enumerate(r, start=1):
        if bit == "1": s ^= pos
    if s: r = r[:s - 1] + "10"[int(r[s - 1])] + r[s:]
    return r[2] + r[4] + r[5] + r[6]       # data: অবস্থান 3, 5, 6, 7
flip = lambda w, i: w[:i] + "10"[int(w[i])] + w[i + 1:]

blocks = [bits[i:i + 4] for i in range(0, len(bits), 4)]
sent = [enc(b) for b in blocks]
print("data", len(bits), "bit →", len(blocks), "block →", 7 * len(sent), "bit")
rng = random.Random(72)
hit = rng.sample(range(len(sent)), 12)     # ১২টি block-এ একটি করে বিট উল্টানো
got = [flip(w, rng.randrange(7)) if j in hit else w for j, w in enumerate(sent)]
print("১২টি একক ভুল, Hamming শোধরানোর পর লগ নিখুঁত:",
      decode("".join(dec(w) for w in got)) == log)

raw = flip(bits, 40)                       # Hamming ছাড়া একটিমাত্র বিট
out = decode(raw)
print("সুরক্ষা ছাড়া ১টি বিট উল্টালে: চিহ্ন", len(out), "টি, ভুল জায়গায়",
      sum(a != b for a, b in zip(out, log)), "টি")
two = [flip(flip(w, 0), 1) if j == 0 else w for j, w in enumerate(sent)]
print("এক block-এ ২টি ভুল:", decode("".join(dec(w) for w in two)) == log)

আসল আউটপুট (ব্রাউজারের Python):

data 200 bit → 50 block → 350 bit
১২টি একক ভুল, Hamming শোধরানোর পর লগ নিখুঁত: True
সুরক্ষা ছাড়া ১টি বিট উল্টালে: চিহ্ন 101 টি, ভুল জায়গায় 55 টি
এক block-এ ২টি ভুল: False

৫০টি block, ৩৫০ bit. ১২টি block-এ একটি করে বিট উল্টানো হলো, আর syndrome প্রতিটি ভুলের অবস্থান বলে দিল; লগ হুবহু ফিরল। সুরক্ষা ছাড়া একটিমাত্র বিট উল্টালে ফল ভয়াবহ: Huffman decoder ছন্দ হারায়, ১০০-এর জায়গায় ১০১টি চিহ্ন পড়ে, আর ৫৫টি অবস্থানে অবস্থা ভুল। এক block-এ দুটি ভুল হলে Hamming(7,4) ভুলভাবে শোধরায় (False), কারণ minimum distance 3 শুধু ⌊(3 − 1)/2⌋ = 1টি ভুলের নিশ্চয়তা দেয়।

ক্রমটি লক্ষ করুন: আগে সংকোচন, পরে সুরক্ষা। সংকোচন বাড়তি অংশ ছেঁটে ফেলে, সুরক্ষা হিসাব করে বাড়তি অংশ যোগ করে; উল্টো ক্রমে সংকোচন সুরক্ষার বিটগুলোই ছেঁটে ফেলতে চাইত। সংকোচিত আর সুরক্ষিত লগ ৩৫০ bit, সুরক্ষাহীন fixed-length লগের (৩০০) চেয়ে সামান্য বেশি, অথচ একক ভুলে টেকে।

৮ · অর্ডার-কোডের DFA আর রাইডারের শিফটে 2-SAT

অংশ ৪-এর ID-কে একটি DFA দিয়ে যাচাই করা যায়, শুধু ফরম্যাট নয়, check digit-ও। চাবি: যোগফল পুরোটা মনে রাখার দরকার নেই, শুধু তার mod 11 মান। তাই state = (কয়টি চিহ্ন পড়া হলো, এ পর্যন্ত ওজনযুক্ত যোগফল mod 11), সঙ্গে একটি “dead” state; গ্রহণ শুধু (9, 0)-তে। নিচের stepper “BG0472131” পড়ে:

পড়া অংশ আর state
পড়া হয়েছে
যোগ
state
  1. শুরু। State (0, 0): কিছু পড়া হয়নি, যোগফল 0.
  2. উপসর্গ। B আর G ঠিক জায়গায়, state (2, 0). অন্য কোনো অক্ষর এলে সোজা dead.
  3. প্রথম অঙ্ক। 0, ওজন 7: যোগ 0, state (3, 0).
  4. দ্বিতীয়। 4, ওজন 6: 0 + 24 = 24 ≡ 2 (mod 11), state (4, 2).
  5. তৃতীয়। 7, ওজন 5: 2 + 35 = 37 ≡ 4, state (5, 4).
  6. চতুর্থ। 2, ওজন 4: 4 + 8 = 12 ≡ 1, state (6, 1).
  7. পঞ্চম। 1, ওজন 3: 1 + 3 = 4, state (7, 4).
  8. ষষ্ঠ। 3, ওজন 2: 4 + 6 = 10, state (8, 10). Check digit-কে এখন 1 হতে হবে।
  9. শেষ অঙ্ক। 1, ওজন 1: 10 + 1 = 11 ≡ 0, state (9, 0): গৃহীত।
import random
W, DIG = [7, 6, 5, 4, 3, 2, 1], "0123456789"
def step(q, ch):                           # q = (অবস্থান, যোগফল mod 11) বা "dead"
    if q == "dead": return "dead"
    i, r = q
    if i == 0: return (1, 0) if ch == "B" else "dead"
    if i == 1: return (2, 0) if ch == "G" else "dead"
    if 2 <= i <= 7 and ch in DIG: return (i + 1, (r + W[i - 2] * int(ch)) % 11)
    if i == 8 and (ch in DIG or ch == "X"):
        return (9, (r + (10 if ch == "X" else int(ch))) % 11)
    return "dead"
def accepts(s):
    q = (0, 0)
    for ch in s: q = step(q, ch)
    return q == (9, 0)                     # শেষ অবস্থান, যোগফল ≡ 0
seen, todo = {(0, 0)}, [(0, 0)]            # পৌঁছানো যায় এমন state গোনা
while todo:
    q = todo.pop()
    for t in {step(q, ch) for ch in "BGXZ" + DIG} - seen:
        seen.add(t); todo.append(t)
print("পৌঁছানো যায় এমন state:", len(seen))
def by_math(s):                            # অঙ্কের হিসাবে একই নিয়ম
    ok = len(s) == 9 and s[:2] == "BG" and s[2:8].isdigit() and s[8] in DIG + "X"
    return ok and sum(w * (10 if c == "X" else int(c)) for w, c in zip(W, s[2:])) % 11 == 0
random.seed(74)
tests = ["BG" + "".join(random.choice(DIG + "X") for _ in range(7)) for _ in range(20000)]
tests += ["BG0472131", "BG04721X", "bg0472131", "BG0472113"]
print("DFA = হিসাব, সব", len(tests), "স্ট্রিংয়ে:", all(accepts(t) == by_math(t) for t in tests))
print("গৃহীত:", sum(map(accepts, tests)), "| শেষ চারটির গৃহীত:", [t for t in tests[-4:] if accepts(t)])

আসল আউটপুট (ব্রাউজারের Python):

পৌঁছানো যায় এমন state: 80
DFA = হিসাব, সব 20004 স্ট্রিংয়ে: True
গৃহীত: 1037 | শেষ চারটির গৃহীত: ['BG0472131']

পৌঁছানো যায় এমন state ৮০টি: শুরুর তিনটি ((0, 0), (1, 0), (2, 0)), অবস্থান ৩-এ ১০টি (7d mod 11 কখনো 4 হয় না, কারণ d ≤ 9), অবস্থান ৪ থেকে ৯-এ ১১টি করে ৬৬টি, আর dead. ২০,০০৪টি স্ট্রিংয়ে DFA আর সরাসরি হিসাব একমত। শেষ চারটির মধ্যে শুধু আসল ID গৃহীত; ছোট হাতের “bg”, ছোট দৈর্ঘ্য আর শেষ দুই অঙ্কের অদলবদল বাতিল।

কেন ঠিক: পর্ব ৭৪-এর মতো invariant: “BG”-এর পরে i-টি অঙ্ক পড়ার পরে state = (i + 2, প্রথম i অঙ্কের ওজনযুক্ত যোগফল mod 11) হয়। পড়া দৈর্ঘ্যের ওপর induction-এ প্রমাণ হয়, কারণ (r + w·d) mod 11 শুধু r mod 11-এর ওপর নির্ভর করে। ফলে check digit-এর শর্তওয়ালা ID-এর ভাষাটি regular; সসীম স্মৃতিই যথেষ্ট।

রাইডারের শিফট — 2-SAT

চার রাইডার: a (আরিফ), b (বিনা), c (চয়ন), d (দীপা); variable সত্য মানে সকালের শিফট। ব্যবস্থাপকের শর্ত: চাবি থাকে a বা b-এর কাছে, তাই (a ∨ b); a আর c একটি বাইক ভাগ করেন, তাই (¬a ∨ ¬c); সকালে c বা d অন্তত একজন, (c ∨ d); b আর d একসঙ্গে সকালে নন, (¬b ∨ ¬d). পরে দুটি নতুন অনুরোধ আসে: “a সকালে হলে c-ও সকালে” (¬a ∨ c) আর “a বা d অন্তত একজন সকালে” (a ∨ d). প্রতিটি clause-এ দুটি literal, তাই এটি 2-SAT: পর্ব ৭৭-এর implication graph আর SCC দিয়ে রৈখিক সময়ে মেলে।

from itertools import product
V = "abcd"                                 # x সত্য = রাইডার x সকালের শিফটে
neg = lambda l: l[1:] if l[0] == "~" else "~" + l
def solve(clauses):
    g = {}
    for a, b in clauses:                   # (a ∨ b): ¬a → b, ¬b → a
        g.setdefault(neg(a), []).append(b); g.setdefault(neg(b), []).append(a)
    def reach(s, t):
        seen, st = {s}, [s]
        while st:
            u = st.pop()
            for w in g.get(u, []):
                if w not in seen: seen.add(w); st.append(w)
        return t in seen
    bad = [x for x in V if reach(x, "~" + x) and reach("~" + x, x)]
    val = lambda l, m: not m[l[1:]] if l[0] == "~" else m[l]
    models = [m for m in (dict(zip(V, t)) for t in product([1, 0], repeat=4))
              if all(val(a, m) or val(b, m) for a, b in clauses)]
    return bad, models

C = [("a", "b"), ("~a", "~c"), ("c", "d"), ("~b", "~d")]
bad, models = solve(C)
print("x আর ¬x একই SCC-তে:", bad, "| model:", len(models), models)
C2 = C + [("~a", "c"), ("a", "d")]         # দুটি নতুন অনুরোধ
bad, models = solve(C2)
print("নতুন অনুরোধসহ: একই SCC-তে", bad, "| model:", len(models))

আসল আউটপুট (ব্রাউজারের Python):

x আর ¬x একই SCC-তে: [] | model: 2 [{'a': 1, 'b': 0, 'c': 0, 'd': 1}, {'a': 0, 'b': 1, 'c': 1, 'd': 0}]
নতুন অনুরোধসহ: একই SCC-তে ['a', 'b', 'c', 'd'] | model: 0

প্রথম চার শর্তে দুটি সমাধান: (a, d) সকালে বা (b, c) সকালে। নতুন দুই অনুরোধসহ কোনো সমাধান নেই, আর graph তা দেখায়: a → c (¬a ∨ c), c → ¬a (¬a ∨ ¬c), তাই a ⇝ ¬a; আবার ¬a → d (a ∨ d), d → ¬b (¬b ∨ ¬d), ¬b → a (a ∨ b), তাই ¬a ⇝ a. a আর ¬a একই SCC-তে, তাই পর্ব ৭৭-এর উপপাদ্য অনুযায়ী UNSAT. কোড SCC-র বদলে প্রতিটি x-এর জন্য দুই দিকের পথ খোঁজে, যা ছোট graph-এ যথেষ্ট, আর brute force (১৬টি বণ্টন) একই উত্তর দেয়।

৯ · পাতা থেকে প্রকল্পে — Z3-এ প্রমাণ, নিজের মূল্যায়ন আর পরের ধাপ

ডেস্কটপে সত্যিকারের run: Z3 দিয়ে দুটি দাবির প্রমাণ

ব্লক ২ ২,০০০টি দিন পরীক্ষা করেছিল; SMT solver Z3 একই preservation ধাপ সব পূর্ণসংখ্যায় প্রমাণ করতে পারে (পর্ব ১৩ আর ৭৭-এর মতো)। নিচে uv দিয়ে বানানো একটি ছোট প্রকল্প (Python 3.12.13, z3-solver 5.4.1.0, MIT লাইসেন্স), path নিরপেক্ষ করা:

from z3 import Ints, Bools, Implies, And, Or, Not, Solver, prove, If, get_version_string

print("z3", get_version_string())
S0, got, sent, stock, q = Ints("S0 got sent stock q")
I = And(stock == S0 + got - sent, stock >= 0)

def after(kind_in, slack):
    st2 = If(kind_in, stock + q, If(q <= stock + slack, stock - q, stock))
    got2 = If(kind_in, got + q, got)
    sent2 = If(kind_in, sent, If(q <= stock + slack, sent + q, sent))
    return And(st2 == S0 + got2 - sent2, st2 >= 0)

for slack in (0, 5):
    for kind_in in (True, False):
        print(f"slack={slack} kind_in={kind_in}:", end=" ")
        prove(Implies(And(I, q >= 1), after(kind_in, slack)))

f, e, b, v, n = Bools("f e b v n")
R = [Implies(f, v), Implies(e, b), Or(b, v), Not(And(b, v)), Implies(n, Not(v))]
print("R entails f -> not n:", end=" ")
prove(Implies(And(R), Implies(f, Not(n))))
s = Solver()
for i, r in enumerate(R + [f, e], 1):
    s.assert_and_track(r, f"c{i}")
print(s.check(), "core:", sorted(str(c) for c in s.unsat_core()))
~/bazarghar-dm$ uv add z3-solver
 + z3-solver==5.4.1.0
~/bazarghar-dm$ uv run python verify_bazar.py
z3 5.4.1
slack=0 kind_in=True: proved
slack=0 kind_in=False: proved
slack=5 kind_in=True: proved
slack=5 kind_in=False: counterexample
[stock = 0, q = 1, sent = -1, S0 = -1, got = 0]
R entails f -> not n: proved
unsat core: ['c1', 'c2', 'c4', 'c6', 'c7']

সঠিক কোডের (slack = 0) দুই শাখাই “proved”: এটি আর টেস্ট নয়, সব পূর্ণসংখ্যার জন্য প্রমাণ। Bug-এর সংস্করণে Z3 একটি counterexample দিল: stock = 0, q = 1-এ ধার নিয়ে মজুত −১ হয়ে যায়। বাকি মান (S0 = −1 ইত্যাদি) শুধু সমীকরণ মেলাতে বাছা, কারণ I-এ S0 ≥ 0 লেখা নেই; preservation-এর প্রমাণে S0-এর চিহ্ন লাগেও না। শেষ দুই লাইন অংশ ২-এর ফল আবার দেয়: entailment প্রমাণিত, আর unsat core c1, c2, c4, c6, c7 = R1, R2, R4, F1, F2.

নিজেকে মূল্যায়ন — rubric

দিকশুরুদক্ষচমৎকার
মডেলউত্তর আছে, মডেল অস্পষ্টvariable, graph বা code স্পষ্ট লেখাকোন ভুল বা ঘটনা মডেলের বাইরে, তাও লেখা
প্রমাণ“কোড চলেছে, তাই ঠিক”উপপাদ্যের নাম আর মূল ধাপপ্রতিটি শর্ত কোথায় লাগল, দেখানো
যাচাইএকটি উদাহরণছোট ক্ষেত্রে brute forcebrute force + ইচ্ছাকৃত bug ধরা + solver
সীমাউল্লেখ নেইখেলনা উদাহরণ চিহ্নিতকোথায় ভাঙে তার counterexample (যেমন ২টি ভুল)
যোগাযোগশুধু সংখ্যাসংখ্যা + এক বাক্যের অর্থব্যবস্থাপকের জন্য সিদ্ধান্ত আর বিকল্প

আরও এগোতে চাইলে

  • দুটি ভুলের ক্ষেত্রে: প্রতিটি block-এ একটি parity বিট যোগ করে SECDED বানান (পর্ব ৭২), আর ব্লক ১০-এর শেষ লাইনে কী বদলায় দেখুন।
  • বড় নেটওয়ার্ক: ২০টি হাবের এলোমেলো graph-এ Kirchhoff দিয়ে spanning tree গুনুন (পর্ব ৬৭), আর metric TSP-এর 2-approximation চালান।
  • Stable matching: রাইডার আর অর্ডারের পছন্দের তালিকা দিয়ে Gale–Shapley চালিয়ে দেখুন কে কাকে পান (পর্ব ৬৪)।
  • কুপনে probability: লটারির কুপনে “অন্তত একজন দুটি পান”-এর সম্ভাবনা inclusion–exclusion দিয়ে (পর্ব ৪৯)।
  • বাস্তব সই: খেলনা RSA-র জায়গায় একটি পরীক্ষিত লাইব্রেরির PSS সই কীভাবে ব্যবহার হয়, Cybersecurity কোর্সের পর্ব ১১ থেকে দেখে নিন।

১০ · AI-এর যুগে — AI সহকারীর সঙ্গে গণিতের প্রকল্প

AI সহকারী এমন প্রকল্পের কোড আর “প্রমাণ” কয়েক মিনিটে লিখে দেয়, প্রায়ই এই নীরব ভুলসহ: মজুতের invariant থেকে “stock ≥ 0” অংশ বাদ, যাতে exit ধাপ খাটে না; mod 10-এর check digit-কে “সব অদলবদল ধরে” বলা; birthday সম্ভাবনায় k/N-কে C(k, 2)/N-এর জায়গায় বসানো; MST-কে “সবচেয়ে ছোট পথ” বলা; Hall-এর উপপাদ্য bipartite না হওয়া graph-এ খাটানো; আর বড় সংখ্যার modular হিসাবে ভুল অঙ্ক। এমন উত্তরে প্রথম কাজ: ছোট ক্ষেত্রে brute force, তারপর প্রতিটি ধাপের যুক্তি আর উপপাদ্যের শর্ত।

কাজ ছোট ভাগে চান, আর প্রতিটির সঙ্গে যাচাইয়ের পথও চান:

আমার একটি অর্ডার-ID: ৬টি অঙ্ক, শেষে check digit, ওজন 7, 6, 5, 4, 3, 2, 1, mod 11, আর 10 হলে "X" লেখা হয়। ১. শুধু Python standard library দিয়ে make(payload) আর valid(code) ফাংশন লেখো। ২. প্রমাণ লেখো যে এটি সব একক ভুল আর সব অদলবদল ধরে; প্রতিটি ধাপের পাশে কোন উপপাদ্য, আর তার শর্ত কেন মেলে, তা লেখো। ৩. একটি brute-force test লেখো, যা ১০০০টি এলোমেলো ID-তে সব একক ভুল আর সব পাশাপাশি অদলবদল চেষ্টা করে ফসকানোর সংখ্যা ছাপে। ৪. কোন ধরনের ভুল এই পদ্ধতি ধরে না, তার একটি নির্দিষ্ট উদাহরণ দাও। ৫. কোনো সংখ্যা নিজে বানিয়ে লিখবে না; আমি কোড চালিয়ে মেলাব।

আরও গভীরে: এই পাতার প্রায় প্রতিটি প্রশ্নের বড় সংস্করণ গবেষণার বিষয়: সংকোচন আর error-correction একসঙ্গে নকশা (joint source-channel coding), বিশাল নিয়মের তালিকায় সবচেয়ে ছোট unsat core খোঁজা (যা নিজেই কঠিন), আর বাস্তব শিফটের সমস্যা, যেখানে তিন বা বেশি জনের শর্ত 2-SAT-এর সহজ জগৎ ছেড়ে সাধারণ SAT-এ নিয়ে যায়। শিল্পে এগুলো সমাধান হয় SAT/SMT solver, integer programming আর বিশেষায়িত code দিয়ে।

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

  • ৩২ চিহ্নের ৬ ঘরের কোডে অন্তত একটি অঙ্ক (৮টি অঙ্ক) কয়টি? 326 − 246 = 882,638,848.
  • MST আর shortest path tree কি একই? না; MST যোগফল কমায়, পথের tree প্রতিটি দূরত্ব। এখানে ১৪ বনাম ১৮ কিমি।
  • Check digit-এ mod 11 কেন? 11 মৌলিক, তাই Euclid-এর lemma-য় কোনো একক ভুল বা অদলবদল ফসকায় না।
  • Hamming(7,4) কয়টি ভুল শোধরায়? d = 3, তাই ১টি; ২টি ভুলে ভুলভাবে শোধরাতে পারে।

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

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

প্র ০১mod 11-এর check digit সব একক ভুল আর অদলবদল ধরে, অথচ অনেক বাস্তব নম্বর mod 10 ব্যবহার করে। বাজারঘর কোনটি বাছবে?

প্রথমে প্রমাণের পার্থক্যটি মনে করুন। mod 11-এ অদলবদল ফসকাতে হলে 11-কে (wi − wj)(a − b) ভাগ করতে হবে, আর 11 মৌলিক বলে Euclid-এর lemma তা অসম্ভব করে, যতক্ষণ দুই গুণকই 1 থেকে 10-এর মধ্যে। mod 10-এ 10 = 2 · 5, তাই (3 − 1)(a − b) = 2 · 5 হতেই পারে: পার্থক্য 5-এর জোড়া ফসকায়। ব্লক ৩-এ এটি ৫,৪১০টির ৬৪১টি, প্রায় ৮টিতে একটি।

পর্ব ২৬-এ দেখেছেন, হাতে নম্বর লেখার সবচেয়ে সাধারণ ভুল একক ভুল আর পাশাপাশি অদলবদল, এই পর্যবেক্ষণ সাধারণত ডাচ গণিতবিদ J. Verhoeff-এর ১৯৬৯ সালের কাজের নামে উল্লেখ করা হয়। পুরোনো ১০ অঙ্কের ISBN mod 11 নেয় আর check digit 10-এর জন্য “X” লেখে; EAN-13-এর মতো mod 10 পদ্ধতি “X”-এর ঝামেলা এড়ায়, কিন্তু কিছু অদলবদল ফসকানোর দাম দেয়। Luhn পদ্ধতিও mod 10-এর আপস, যা শুধু 09 ↔ 90 ফসকায়।

বাজারঘরের সিদ্ধান্ত নির্ভর করে ID কে আর কীভাবে লেখেন তার ওপর। গুদামের কর্মী ফোনে শুনে টাইপ করলে অদলবদলের ঝুঁকি বেশি, আর ID-তে আগে থেকেই “BG” অক্ষর আছে, তাই একটি “X” বাড়তি বোঝা নয়: mod 11 ভালো বাছাই। কিন্তু যদি ID শুধু সংখ্যার কিপ্যাডে লেখা হয়, বা পুরোনো কোনো ব্যবস্থা শুধু অঙ্ক নেয়, তবে mod 10 বা দশমিক অঙ্কে কাজ করা অন্য পদ্ধতি বিবেচনা করা যায়। যেকোনো পথে, সিদ্ধান্তের যুক্তি আর কোন ভুল ফসকায় তার তালিকা নথিতে লেখা থাকা উচিত।

মূল উপলব্ধি: modulus মৌলিক হলে প্রমাণ সহজ আর নিশ্চয়তা পূর্ণ; mod 10-এর সুবিধা ব্যবহারিক, দাম কিছু অদলবদল।

প্র ০২ব্লক ২ ১,২০,০০০টি ঘটনায় invariant পরীক্ষা করেছে। তবু হাতে লেখা প্রমাণ বা Z3 কেন লাগে?

কারণ টেস্ট শুধু দেখা ইনপুটের কথা বলে। পর্ব ১৩-এ Edsger Dijkstra-র যে উক্তি দেখেছেন, “program testing can be used to show the presence of bugs, but never to show their absence”, তা সাধারণত ১৯৬৯–৭০ সালের দিকের তাঁর লেখা ও বক্তৃতার বলে উল্লেখ করা হয়। আমাদের এলোমেলো ঘটনায় q সবসময় 1 থেকে 12, শুরুর মজুত সবসময় 20; কোনো bug যদি শুধু বড় q বা বিশেষ ক্রমে দেখা দিত, টেস্ট তা ফসকাতে পারত।

প্রমাণের ধারণাটি পুরোনো। পর্ব ১৩ অনুযায়ী Robert Floyd ১৯৬৭ সালে “Assigning Meanings to Programs”-এ প্রোগ্রামের প্রতিটি অংশে শর্ত বসিয়ে প্রমাণের পদ্ধতি দেন, আর Tony Hoare ১৯৬৯ সালে “An Axiomatic Basis for Computer Programming”-এ তাকে triple আর নিয়মের রূপ দেন। Loop invariant এই ঐতিহ্যের কেন্দ্র: লুপের প্রতিটি ঘোরায় সত্য একটি বাক্য, যা আসলে ঘোরার সংখ্যার ওপর induction.

Z3 তৃতীয় একটি স্তর যোগ করে। আমাদের হাতে লেখা প্রমাণেও ভুল থাকতে পারে, যেমন কোনো শাখা বাদ পড়া। Z3 preservation-এর প্রতিটি শাখাকে সব পূর্ণসংখ্যার ওপর একটি সূত্র হিসেবে দেখে: “proved” মানে কোনো counterexample নেই, আর bug থাকলে একটি নির্দিষ্ট counterexample দেয়, যেমন stock = 0, q = 1. তবে Z3-ও শুধু সেই মডেল যাচাই করে যা আমরা লিখেছি; Python-এর আসল কোড আর Z3-এর সূত্র এক কি না, তা আবার মানুষের দায়িত্ব। তাই তিন স্তর একে অপরের পরিপূরক: টেস্ট দ্রুত bug ধরে, প্রমাণ বোঝায় কেন ঠিক, solver প্রমাণের ফাঁক ধরে।

মূল উপলব্ধি: টেস্ট নমুনা দেখে, প্রমাণ সবগুলো; solver মডেলকে প্রমাণ করে, কোডের সঙ্গে মডেলের মিল যাচাই আপনার কাজ।

প্র ০৩একই হাবের graph-এ MST আর shortest path tree আলাদা। বাজারঘর কেবল টানবে কোনটি ধরে, আর ভ্যান চালাবে কোনটি ধরে?

দুটি প্রশ্নের লক্ষ্য আলাদা। কেবল একবার টানা হয়, আর খরচ মোট দৈর্ঘ্যের সমানুপাতিক; প্রতিটি হাব যুক্ত থাকলেই চলে, তথ্য কোন পথে ঘুরে যায় তাতে প্রায় কিছু যায় আসে না। এটি MST-এর প্রশ্ন: ১৪ কিমি। ভ্যান প্রতিদিন গুদাম A থেকে প্রতিটি হাবে যায়, আর প্রতিটি যাত্রার দৈর্ঘ্য আলাদা করে গুরুত্বপূর্ণ; এটি shortest path tree-এর প্রশ্ন। সেখানে মোট রাস্তা ১৮ কিমি, কিন্তু প্রতিটি হাবে দূরত্ব সবচেয়ে কম, যেমন E-তে ১১, যেখানে MST ধরে গেলে ১২.

দুই সমস্যার ইতিহাসও আলাদা পথে এসেছে। পর্ব ৬৭ অনুযায়ী MST-এর প্রথম পরিচিত সমাধান চেক গণিতবিদ Otakar Borůvka ১৯২৬ সালে প্রকাশ করেন, প্রসঙ্গ ছিল মোরাভিয়া অঞ্চলে বিদ্যুৎ নেটওয়ার্কের কম খরচের নকশা: ঠিক আমাদের কেবলের প্রশ্ন। Vojtěch Jarník ১৯৩০ সালে আরেকটি পদ্ধতি দেন, যা পরে Robert Prim ১৯৫৭ সালে আবার প্রকাশ করেন; Joseph Kruskal-এর পদ্ধতি ১৯৫৬ সালের। Edsger Dijkstra-র shortest path-এর প্রবন্ধ ১৯৫৯ সালের, আর পর্ব ৬১ অনুযায়ী তিনি algorithm-টি ১৯৫৬ সালে ভাবেন।

গণিতের দিক থেকে সম্পর্কটি একটি অসমতা: shortest path tree নিজেও একটি spanning tree, তাই তার মোট weight MST-এর চেয়ে কম হতে পারে না (১৮ ≥ ১৪)। আর MST-এর পথ যেকোনো হাবে সবচেয়ে ছোট হবে, এমন কোনো নিশ্চয়তা নেই। বাস্তবে ব্যবস্থাপক মাঝামাঝি নকশাও চাইতে পারেন, যেমন মোট দৈর্ঘ্যের সীমার মধ্যে দূরত্ব কমানো; তখন সমস্যাটি দুই লক্ষ্যের আপস, আর প্রায়ই কঠিনতর।

মূল উপলব্ধি: “সবচেয়ে কম” বলার আগে জিজ্ঞেস করুন, কিসের যোগফল বা কোন দূরত্ব; একই graph-এ উত্তর বদলে যায়।

প্র ০৪Huffman বাড়তি অংশ ছাঁটে, Hamming বাড়তি অংশ যোগ করে। দুটি একসঙ্গে ব্যবহার কি স্ববিরোধী নয়?

না, কারণ দুই বাড়তি অংশের চরিত্র আলাদা। লগের স্বাভাবিক বাড়তি অংশ অগোছালো: D খুব বেশি আসে, তবু fixed-length code তাকে ৩ bit দেয়। এই অপচয় কোনো ভুল ধরতে সাহায্য করে না। Huffman সেটি ছেঁটে গড় ২.০০ bit-এ নামায়। Hamming(7,4)-এর বাড়তি তিনটি parity বিট পরিকল্পিত: তারা এমনভাবে বসানো যে একটি বিট উল্টালে syndrome তার অবস্থান বলে দেয়। প্রথমটি এলোমেলো চর্বি, দ্বিতীয়টি মাপা বর্ম।

এই চিন্তার উৎস Claude Shannon-এর ১৯৪৮ সালের প্রবন্ধ “A Mathematical Theory of Communication”, যেখানে entropy দিয়ে সংকোচনের সীমা আর channel capacity দিয়ে নির্ভরযোগ্য যোগাযোগের সীমা, দুটিই আসে। পর্ব ৬৮ আর ৭২ অনুযায়ী David Huffman-এর সর্বোত্তম prefix code ১৯৫২ সালের, আর Richard Hamming-এর “Error Detecting and Error Correcting Codes” ১৯৫০ সালে প্রকাশিত। আধুনিক যোগাযোগ ব্যবস্থাও মোটামুটি এই ভাগ মানে: আগে source coding, পরে channel coding, যদিও নির্দিষ্ট পরিস্থিতিতে দুটিকে একসঙ্গে নকশা করার গবেষণাও আছে।

ব্লক ১০ দেখায় ক্রম কেন গুরুত্বপূর্ণ। সংকোচিত লগ ভঙ্গুর: একটি বিট উল্টালেই Huffman decoder ছন্দ হারায়, আর ১০০টির ৫৫টি অবস্থা ভুল পড়ে। কারণ পরিবর্তনশীল দৈর্ঘ্যের code-এ একটি ভুল পরের সব codeword-এর সীমানা সরিয়ে দেয়। তাই সংকোচনের পরে সুরক্ষা অপরিহার্য। খরচের হিসাবও ভালো: ৩৫০ bit, সুরক্ষাহীন ৩০০-এর চেয়ে মাত্র ১৭% বেশি। তবে দুটি ভুল এক block-এ পড়লে Hamming(7,4) ভুলভাবে শোধরায়; ঝুঁকি বেশি হলে SECDED বা শক্তিশালী code লাগে।

মূল উপলব্ধি: সংকোচন অকেজো বাড়তি অংশ ফেলে, error-correction কাজের বাড়তি অংশ যোগ করে; আগে ছাঁটা, পরে বর্ম।

প্র ০৫চার রাইডারের শিফট 2-SAT-এ মুহূর্তে মিলল। “তিনজনের অন্তত একজন সকালে” ধরনের শর্ত এলে কী বদলায়?

দুই literal-এর clause একটি implication-এর জোড়া, আর implication-এর শিকল একটি directed graph-এ পথ। তাই 2-SAT-এর পুরো প্রশ্ন graph-এর reachability-তে নামে: কোনো x থেকে ¬x-এ আর ¬x থেকে x-এ পথ আছে কি না। পর্ব ৭৭ অনুযায়ী এই রৈখিক-সময়ের পদ্ধতি সাধারণত Aspvall, Plass আর Tarjan-এর ১৯৭৯ সালের প্রবন্ধের নামে পরিচিত। আমাদের ব্লকে a → c → ¬a আর ¬a → d → ¬b → a: দুই দিকের শিকলই বিরোধের প্রমাণপত্র, যা ব্যবস্থাপককে দেখানো যায়।

তিন literal-এর একটি clause (a ∨ b ∨ c) একটি implication-এ নামে না: ¬a সত্য হলে b বা c-এর কোনো একটি সত্য হতে হবে, কোনটি তা নির্দিষ্ট নয়। Graph-এর শিকল ভেঙে যায়, আর সমস্যাটি সাধারণ SAT হয়ে ওঠে। Stephen Cook ১৯৭১ সালে দেখান SAT NP-complete (Leonid Levin স্বাধীনভাবে প্রায় একই ফল পান, প্রকাশ ১৯৭৩), আর 3-SAT-ও NP-complete; তাই সব ক্ষেত্রে দ্রুত চলবে এমন algorithm জানা নেই, আর P ≠ NP হলে তা নেই।

তবু বাস্তবে আশা হারানোর কারণ নেই। পর্ব ০৭ আর ৭৭-এ দেখেছেন, আধুনিক CDCL solver হাজার হাজার variable-এর বাস্তব সূত্র প্রায়ই দ্রুত মেলায়, যদিও কঠিন উদাহরণও আছে (যেমন pigeonhole সূত্র)। ব্যবস্থাপকের জন্য ব্যবহারিক পরামর্শ: শর্তগুলো যতটা সম্ভব দুই জনের ভাষায় রাখুন, কারণ তখন উত্তর নিশ্চিত দ্রুত আর বিরোধের ব্যাখ্যা সহজ; বড় দলের শর্ত লাগলে solver ব্যবহার করুন, আর তার উত্তর (model) সরাসরি সব শর্তে বসিয়ে যাচাই করুন।

মূল উপলব্ধি: দুই জনের শর্ত graph-এর পথ, তাই সহজ; তিন জনের শর্ত সাধারণ SAT, তাই কঠিন, কিন্তু solver-এ প্রায়ই চলে।

অনুশীলন

প্রমাণ, গণনা, ভুল ধরা আর আউটপুট অনুমান; প্রতিটির উত্তর নিচে লুকানো আছে।

  1. প্রমাণ: একটি ID-তে ছয় অঙ্ক d1…d6 আর check digit c, শর্ত 7d1 + 6d2 + 5d3 + 4d4 + 3d5 + 2d6 + c ≡ 0 (mod 11). প্রমাণ করুন: যেকোনো দুটি অবস্থানের (পাশাপাশি না হলেও) দুটি ভিন্ন অঙ্ক অদলবদল হলে বৈধ ID অবৈধ হয়ে যায়।
    1. অবস্থান i আর j-তে অঙ্ক a ≠ b, ওজন wi ≠ wj. অদলবদলে যোগফল বদলায় wi(b − a) + wj(a − b) = −(wi − wj)(a − b). — বীজগণিত
    2. নতুন ID বৈধ ⇔ 11 এই পার্থক্যকে ভাগ করে। — congruence-এর সংজ্ঞা
    3. ওজন 1 থেকে 7, আলাদা, তাই 1 ≤ |wi − wj| ≤ 6; অঙ্ক (X = 10 ধরে) 0 থেকে 10, তাই 1 ≤ |a − b| ≤ 10. — আকারের সীমা
    4. 11 মৌলিক; গুণফলকে ভাগ করলে কোনো একটি গুণককে ভাগ করত, কিন্তু 1 থেকে 10-এর কেউ 11-এর গুণিতক নয়। তাই নতুন ID অবৈধ। ∎ — Euclid-এর lemma
  2. গণনা (numerical): ২৪টি অক্ষর আর ৮টি অঙ্কের বর্ণমালায় ৬ ঘরের কুপন-কোড। (ক) কয়টি কোডে ঠিক ২টি অঙ্ক আর কোনো চিহ্ন দুবার নেই? (খ) কয়টি কোডে অন্তত একটি অঙ্ক আর অন্তত একটি অক্ষর?
    • (ক) অঙ্কের দুই ঘর বাছা C(6, 2) = 15; সেখানে দুটি ভিন্ন অঙ্ক ক্রমে P(8, 2) = 56; বাকি চার ঘরে চারটি ভিন্ন অক্ষর P(24, 4) = 255,024. গুণের নিয়মে 15 · 56 · 255,024 = 214,220,160.
    • (খ) মোট থেকে “শুধু অক্ষর” আর “শুধু অঙ্ক” বাদ; দুই দল বিচ্ছিন্ন, তাই 326 − 246 − 86 = 1,073,741,824 − 191,102,976 − 262,144 = 882,376,704.
    • যাচাই: ৪টি অক্ষর আর ৩টি অঙ্কের ৫ ঘরের ছোট রূপে brute force দুটি সূত্রের সঙ্গেই মেলে (১,৪৪০ আর ১৫,৫৪০)।
  3. ভুল ধরুন: একজনের দাবি: “একটি connected weighted graph-এর MST সব edge-এর মোট weight সবচেয়ে কম রাখে। তাই MST-এর ভেতরের প্রতিটি পথও সবচেয়ে ছোট; ফলে গুদাম থেকে ভ্যানের পথ MST ধরে দিলেই চলবে।” প্রমাণের ভুল কোথায়? একটি counterexample দিন।

    ভুল লাফটি “মোট সবচেয়ে কম, তাই প্রতিটি অংশ সবচেয়ে ছোট”। MST-এর সর্বোত্তমতা একটি যোগফলের, আর যোগফল কমাতে গিয়ে কোনো একটি পথ লম্বা হতেই পারে; shortest path-এর optimal substructure একটি পথের ধর্ম, tree-এর মোট weight-এর নয়। Counterexample এই পাতার হাবের graph: MST ধরে A থেকে E = A–C–B–D–E = 3 + 3 + 2 + 4 = 12, কিন্তু A–C–E = 3 + 8 = 11. আরও সরল: ত্রিভুজ x–y = 2, y–z = 2, x–z = 3-এ MST = {x–y, y–z}, যেখানে x থেকে z পথ 4 > 3.

  4. আউটপুট অনুমান: নিচের কোড কী ছাপবে?
    W = [7, 6, 5, 4, 3, 2, 1]
    for p in ["000000", "100000", "000006"]:
        c = -sum(w * int(x) for w, x in zip(W, p)) % 11
        print(p, "X" if c == 10 else c)

    সত্যিকারের run (Python 3.14):

    000000 0
    100000 4
    000006 X

    প্রথমটিতে যোগফল 0. দ্বিতীয়টিতে 7 · 1 = 7, আর Python-এ −7 % 11 = 4 (ভাগশেষ সবসময় অঋণাত্মক, যা অনেক ভাষায় সত্য নয়)। তৃতীয়টিতে 2 · 6 = 12 ≡ 1, তাই c = −1 ≡ 10, লেখা হয় “X”. ফাঁদটি “%”-এর চিহ্ন: C বা Java-তে −7 % 11 = −7.

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

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

ক্রমএকটি বাস্তব প্রশ্ন সমাধানের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. প্রশ্নটি নির্ভুলভাবে লেখা: কী জানতে চাই, কোন ভুল বা ঘটনা গোনা হবে
  2. গাণিতিক মডেল: সূত্র, সেট, graph, code বা automaton
  3. উপযুক্ত উপপাদ্য বাছা আর তার প্রতিটি শর্ত মেলানো
  4. প্রমাণ বা প্রমাণের খসড়া লেখা
  5. ছোট ক্ষেত্রে brute force আর আসল আকারে হিসাব দিয়ে যাচাই
  6. সীমা আর সিদ্ধান্ত লেখা: কোথায় খাটে না, ব্যবস্থাপক কী করবেন

একটি ID-তে check digit-এর শর্ত 7d1 + 6d2 + … + 2d6 + c ≡ 0 (mod 11). কোনো পাশাপাশি অদলবদল (দুটি ভিন্ন অঙ্ক) কেন ফসকায় না?

  • কারণ পাশাপাশি দুই অবস্থানের ওজন আলাদা
  • কারণ 11 মৌলিক, আর পরিবর্তনের দুই গুণকের কেউ 11-এর গুণিতক নয়
  • কারণ check digit ১১টি মান নিতে পারে
  • কারণ এই পদ্ধতি যেকোনো সংখ্যক ভুল ধরে

২৪টি অক্ষর আর ৮টি অঙ্ক মিলে ৩২টি চিহ্ন। ৬ ঘরের কোডে (পুনরাবৃত্তি চলে, ক্রম গুরুত্বপূর্ণ) অন্তত একটি অঙ্ক আছে এমন কোড কয়টি?

  • 6 · 8 · 325
  • 86
  • 326 − 246
  • C(37, 6)

৪ ঘরের কোডে N = 324 = 1,048,576টি সম্ভাব্য মান। এলোমেলোভাবে কোড বানালে মোটামুটি কয়টিতে কোনো দুটি মিলে যাওয়ার সম্ভাবনা অর্ধেক ছাড়ায়?

  • প্রায় ১,২০০
  • প্রায় ৫ লাখ
  • ঠিক ১,০২৪
  • প্রায় ৩২

একটি connected graph-এ সব edge-এর weight ধনাত্মক। vertex A থেকে shortest path tree T আর একটি minimum spanning tree M সম্পর্কে কোনটি সবসময় সত্য?

  • T আর M সবসময় একই tree
  • M-এর পথ ধরে A থেকে প্রতিটি vertex-এ দূরত্ব সবচেয়ে ছোট
  • M সবসময় অনন্য
  • T-এর মোট weight M-এর মোট weight-এর চেয়ে কম নয়

Hamming(7,4)-এ (parity অবস্থান 1, 2, 4; data অবস্থান 3, 5, 6, 7) একটি block এল 1100100. Syndrome হলো যেসব অবস্থানে 1, তাদের নম্বরের XOR. শোধরানোর পরে data কী?

  • 0100
  • 0110
  • 1100
  • 0000

একটি 2-CNF সূত্রের implication graph-এ প্রতিটি clause (α ∨ β) থেকে ¬α → β আর ¬β → α. সূত্রটি কখন unsatisfiable?

  • Graph-এ যেকোনো একটি directed cycle থাকলে
  • কোনো x থেকে ¬x-এ পথ থাকলে
  • কোনো x-এর জন্য x আর ¬x একই strongly connected component-এ থাকলে
  • Clause-এর সংখ্যা variable-এর দ্বিগুণের বেশি হলে

একটি লুপের প্রস্তাবিত invariant I = (stock = S0 + got − sent). লক্ষ্য: লুপ শেষে stock ≥ 0. প্রমাণে কী সমস্যা?

  • I লক্ষ্যের জন্য যথেষ্ট শক্ত নয়; stock ≥ 0 অংশ I-তে যোগ করতে হবে
  • I কোনো ঘোরায় টেকে না
  • লুপ কখনো থামে না
  • Invariant-এ অসমতা রাখা যায় না

শব্দকোষ

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

মনে রাখুন

  1. ছাঁচ: প্রশ্ন → মডেল → উপপাদ্যের শর্ত মেলানো → প্রমাণ → brute force আর হিসাবে যাচাই → সীমা লেখা।
  2. যুক্তি আর লুপ: consistency = satisfiability, entailment = নিয়ম ∧ ¬দাবি-এর model নেই; invariant-এ লক্ষ্যের সব অংশ (stock ≥ 0) রাখুন, termination আলাদা দেখান।
  3. সংখ্যা আর গণনা: মৌলিক modulus সব একক ভুল আর অদলবদল ধরে; complement, গুণ, P আর stars and bars আলাদা প্রশ্নের উত্তর; √N-এর কাছেই মিলের ঝুঁকি।
  4. Graph: MST যোগফল কমায়, পথের tree দূরত্ব; χ আর matching-এ দুই দিকের প্রমাণ: clique আর রং, Hall আর König দিয়ে।
  5. Code আর যন্ত্র: আগে Huffman, পরে Hamming; mod 11-এর check-ও DFA পারে; 2-SAT graph-এর পথ, তিন জনের শর্তে সাধারণ SAT; খেলনা RSA কখনো আসল কাজে নয়।

পরের পর্বে: পর্ব ৭৯ “Numerical সমস্যার অনুশীলন — discrete math-এর হিসাব, পূর্ণ সমাধানসহ” থেকে পরীক্ষা ও ইন্টারভিউ প্রস্তুতির মডিউল শুরু: পুরো কোর্সের হিসাব, ধাপে ধাপে সমাধানসহ।

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

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