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

Permutation আর combination তৈরি — lexicographic order আর backtracking

Generating Combinatorial Objects — Subsets and Gray Codes, Next Permutation, Combinations in Lexicographic Order, Ranking and Unranking, the Fisher–Yates Shuffle and Backtracking

Taught in Bangla, this lesson moves from counting objects to listing each one exactly once. It generates all subsets by binary counting and proves the one-bit property of the reflected Gray code by induction, explains the next permutation and next combination algorithms with their correctness, and ranks and unranks with the Lehmer code and combinatorial number system. It proves Fisher–Yates is uniform while the naive swap is biased, and in-page Python runs Gray codes, next permutation, ranking and a shuffle check.

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

  • Binary counting আর reflected Gray code দিয়ে সব subset তৈরি, আর Gray code-এর এক-বিট ধর্মের প্রমাণ
  • Lexicographic ক্রমে next permutation আর next combination algorithm, তাদের শুদ্ধতার যুক্তি আর গড় খরচ
  • Ranking আর unranking: Lehmer code আর combinatorial number system
  • Fisher–Yates shuffle কেন প্রতিটি permutation সমান সম্ভাবনায় দেয়, আর সাধারণ ভুল সংস্করণ কেন দেয় না
  • Backtracking-এ শর্তযুক্ত বস্তু তৈরি, আর “সব তৈরি করা” কখন অসম্ভব হয়ে পড়ে

১ · গোনা থেকে তৈরি করা — কেন সব বস্তু একে একে বানাতে হয়

আগের কয়েকটি পর্বে প্রশ্ন ছিল “কয়টি?”. পর্ব ৩৩ “Permutation আর combination”-এ জেনেছেন ৪টি পণ্য একটি তাকে 4! = 24 ভাবে সাজানো যায়, আর পর্ব ৩৫-এ জেনেছেন ৪ ফ্লেভার থেকে ৬ প্যাকেটের ঝুড়ি ৮৪টি। কিন্তু কাল্পনিক অনলাইন দোকান বাজারঘরের প্রকৌশলীদের প্রায়ই সংখ্যা যথেষ্ট নয়, তাঁদের দরকার বস্তুগুলো নিজেই:

  • একজন রাইডার ৬টি ঠিকানায় যাবেন; সবচেয়ে ছোট পথ খুঁজতে সব 6! = 720টি ক্রম একে একে পরীক্ষা করতে হবে।
  • ১০টি পণ্যের মধ্যে কোন ৩টি একসঙ্গে “কম্বো অফার” হবে, তা ঠিক করতে C(10, 3) = 120টি দল একে একে দেখতে হবে।
  • নতুন sort ফাংশন পরীক্ষার জন্য ৭টি সংখ্যার সব 5040টি ক্রম ইনপুট হিসেবে চালাতে হবে।
  • লটারির মতো এলোমেলোভাবে ক্রেতাদের সারি সাজাতে হবে, যাতে প্রতিটি ক্রম সমান সম্ভাব্য হয়।

পরীক্ষার হলে রোল নম্বর ধরে উপস্থিতি নেওয়ার কথা ভাবুন। “এই কক্ষে ৪০ জন” জানা এক কাজ; প্রত্যেকের নাম একবার করে ডাকা, কাউকে বাদ না দিয়ে আর কাউকে দুবার না ডেকে, আরেক কাজ। দ্বিতীয় কাজের জন্য একটি ক্রম লাগে (রোল নম্বর), আর “পরের জন কে” বলার একটি নিয়ম। এই পর্বের প্রতিটি algorithm ঠিক এমন একটি “পরের জন” নিয়ম।

একটি ভালো generator-এর কাছে আমরা তিনটি জিনিস চাই। সম্পূর্ণতা: প্রতিটি বস্তু অন্তত একবার আসে। পুনরাবৃত্তিহীনতা: কোনো বস্তু দুবার আসে না। এই দুটি মিলে বলে, generator-এর তালিকা আর বস্তুর সেটের মধ্যে একটি bijection আছে (পর্ব ১৬), তাই তালিকার দৈর্ঘ্য গোনার সূত্রের সঙ্গে মিলতে বাধ্য: এটিই আমাদের প্রথম পরীক্ষা। দক্ষতা: এক বস্তু থেকে পরেরটিতে যেতে অল্প কাজ, আদর্শভাবে গড়ে ধ্রুবক।

ক্রম হিসেবে সবচেয়ে স্বাভাবিক হলো অভিধানের ক্রম, যাকে বলে lexicographic order: দুটি তালিকার প্রথম যে ঘরে পার্থক্য, সেখানে যার মান ছোট, সে আগে। যেমন 1 3 2 আসে 2 1 3-এর আগে, কারণ প্রথম ঘরেই 1 < 2. অভিধানে “কলম” আগে, “কলা” পরে নয়, একই নিয়মে। এই পর্বের মূল কাজ: lexicographic ক্রমে একটি বস্তু দেওয়া থাকলে ঠিক পরেরটি বের করা, আর প্রমাণ করা যে সেটিই পরেরটি।

২ · সব subset — binary counting

সবচেয়ে সহজ উদাহরণ দিয়ে শুরু। S = {চা, ডাল, তেল}-এর সব subset তৈরি করতে চাই, যাদের সংখ্যা 23 = 8 (পর্ব ১৪-এর power set). প্রতিটি subset-কে তিন বিটের একটি bit string দিয়ে লিখুন: প্রথম বিট 1 মানে চা আছে, দ্বিতীয় বিট ডাল, তৃতীয় বিট তেল. যেমন 101 = {চা, তেল}. এখন 0 থেকে 7 পর্যন্ত সাধারণ গোনা, binary-তে 000, 001, 010, 011, 100, 101, 110, 111, ঠিক একবার করে প্রতিটি subset দেয়।

উপপাদ্য ২.১ (binary counting)

S = {s1, …, sn} হলে k ↦ {sᵢ : k-এর binary রূপের i-তম বিট (বাঁ থেকে) 1} ফাংশনটি {0, 1, …, 2n − 1} থেকে P(S)-এ একটি bijection. তাই k = 0, 1, …, 2n − 1 ক্রমে চললে প্রতিটি subset ঠিক একবার আসে।

প্রমাণ:

  1. 0 ≤ k ≤ 2n − 1-এর প্রতিটি সংখ্যার ঠিক একটি n-বিট binary রূপ আছে (শুরুতে দরকারমতো 0 বসিয়ে)। — স্থানীয় মানের সংখ্যা পদ্ধতি
  2. n-বিট string আর S-এর subset-এর মধ্যে “বিট 1 মানে উপাদানটি আছে” একটি bijection, যার উল্টো ফাংশন subset থেকে প্রতিটি বিট পড়ে নেয়। — পর্ব ১৪
  3. দুটি bijection-এর composition bijection. ∎ — পর্ব ১৬

এই কৌশলের একটি দুর্বলতা আছে। 011 থেকে 100-এ যেতে তিনটি বিটই বদলায়: ঝুড়ি থেকে ডাল আর তেল সরিয়ে চা ঢোকাতে হয়। যদি প্রতিটি subset-এর জন্য কোনো খরচসাপেক্ষ হিসাব থাকে (যেমন ঝুড়ির মোট ওজন), তাহলে পরপর দুটি subset যত কাছাকাছি, হিসাব হালনাগাদ তত সস্তা। একটি করে বিট বদলিয়ে সব subset ঘুরে আসা যায় কি? উত্তর হ্যাঁ, আর সেটিই পরের অংশের বিষয়।

৩ · Gray code — প্রতি ধাপে মাত্র একটি বিট

সব n-বিট string-এর এমন একটি তালিকা, যেখানে পরপর দুটির মধ্যে ঠিক একটি বিট আলাদা, তাকে বলে Gray code. দুটি string কয়টি ঘরে আলাদা, সেই সংখ্যাকে বলে Hamming distance; তাই Gray code-এ পরপর দুটি string-এর Hamming distance 1. সবচেয়ে পরিচিত নির্মাণটি “প্রতিফলন” দিয়ে, তাই নাম reflected Gray code:

সংজ্ঞা ৩.১ (reflected Gray code)

G(1) = 0, 1. আর n ≥ 1 হলে G(n + 1) পাওয়া যায় এভাবে: প্রথমে G(n)-এর প্রতিটি শব্দের আগে 0 বসান; তারপর G(n)-কে উল্টো ক্রমে লিখে (প্রতিফলন) প্রতিটির আগে 1 বসান। প্রতীকে G(n + 1) = 0·G(n), 1·G(n)R.

এভাবে G(2) = 00, 01, 11, 10 আর G(3) = 000, 001, 011, 010, 110, 111, 101, 100. নিচের stepper নির্মাণটি ধাপে ধাপে দেখায়।

1G(1) = 0, 12G(2) = 0·G(1), 1·G(1)ᴿ3G(3) প্রথম অর্ধেক: 0·G(2)4G(3) দ্বিতীয় অর্ধেক: 1·G(2)ᴿ5যাচাই: সংযোগ, আবর্তন, সূত্র
চলক
তালিকা
প্রতিফলন
সংযোগস্থল
  1. ভিত্তি। এক বিটের দুটি শব্দ 0 আর 1; তারা এক বিটে আলাদা, আর 1 থেকে 0-তে ফিরে আসাও এক বিটের বদল।
  2. G(2). 0 বসিয়ে 00, 01; তারপর উল্টো ক্রম 1, 0-এর আগে 1 বসিয়ে 11, 10. সংযোগস্থলে 01 → 11, শুধু প্রথম বিট বদলেছে, কারণ উল্টো তালিকা ঠিক সেই শব্দ দিয়ে শুরু হয় যেখানে আগেরটি শেষ।
  3. G(3), প্রথম অর্ধেক। G(2)-এর আগে 0: 000, 001, 011, 010. ভেতরের বদলগুলো G(2)-এর বদলই, প্রথম বিট স্থির।
  4. G(3), দ্বিতীয় অর্ধেক। G(2)ᴿ = 10, 11, 01, 00-এর আগে 1: 110, 111, 101, 100. সংযোগে 010 → 110, শুধু প্রথম বিট বদল।
  5. যাচাই। শেষ 100 থেকে প্রথম 000-এও এক বিট, তাই তালিকাটি চক্রাকার। আর k-তম শব্দ (০ থেকে গুনে) k ⊕ (k ≫ 1): k = 5-এ 101 ⊕ 010 = 111, তালিকার ৬ নম্বর শব্দ ✓.
উপপাদ্য ৩.২ (Gray code-এর ধর্ম)

প্রতিটি n ≥ 1-এর জন্য G(n)-এ সব 2nটি n-বিট string ঠিক একবার আছে, পরপর যেকোনো দুটি শব্দ ঠিক এক বিটে আলাদা, আর শেষ শব্দ ও প্রথম শব্দও এক বিটে আলাদা।

প্রমাণ (n-এর ওপর mathematical induction, পর্ব ১০):

  1. ভিত্তি n = 1: G(1) = 0, 1. দুটি শব্দই আছে, একবার করে, আর 0 ↔ 1 এক বিটের বদল। — সরাসরি পরীক্ষা
  2. অনুমান: G(n)-এর জন্য তিনটি দাবিই সত্য। ধরুন G(n)-এর প্রথম শব্দ f আর শেষ শব্দ ℓ. — inductive hypothesis
  3. সম্পূর্ণতা: 0·G(n)-এ শুরুতে 0 আছে এমন সব 2nটি শব্দ একবার করে, 1·G(n)R-এ শুরুতে 1 আছে এমন সব; দুই অর্ধেকের শব্দ প্রথম বিটেই আলাদা। মোট 2n+1, পুনরাবৃত্তিহীন। — অনুমান + যোগের নিয়ম
  4. অর্ধেকের ভেতরে: 0·G(n)-এ পরপর দুটি শব্দ 0w আর 0w′, যেখানে w, w′ G(n)-এ পরপর, তাই এক বিটে আলাদা। উল্টো ক্রমেও পাশাপাশি থাকা জোড়া একই থাকে, তাই 1·G(n)R-এও একই কথা। — অনুমান
  5. সংযোগস্থল: প্রথম অর্ধেক শেষ হয় 0ℓ দিয়ে, দ্বিতীয় অর্ধেক শুরু হয় 1ℓ দিয়ে (প্রতিফলনে ℓ প্রথমে আসে)। পার্থক্য শুধু প্রথম বিটে। — নির্মাণ
  6. আবর্তন: G(n + 1) শুরু হয় 0f দিয়ে আর শেষ হয় 1f দিয়ে (প্রতিফলনে f শেষে যায়); পার্থক্য শুধু প্রথম বিটে। ∎ — নির্মাণ

ঘনকের ওপর Gray code: একটি Hamiltonian cycle

৩-বিট string-গুলো একটি ঘনকের ৮টি কোণ; এক বিটে আলাদা দুটি কোণ একটি ধার দিয়ে যুক্ত। রঙিন পথ G(3)-এর ক্রম, প্রতিটি কোণ একবার ছুঁয়ে শুরুতে ফেরে।

Reflected Gray code G(3) as a Hamiltonian cycle on the 3-cube 000100 010110 001101 011111 0123 4567 লাল সংখ্যা = G(3)-এ অবস্থান · কমলা ধার = আবর্তন 7 → 0

ছবিটি একটি সুন্দর সমতুল্যতা দেখায়: n-বিট string-গুলোকে কোণ ধরে, এক বিটে আলাদা কোণ যুক্ত করলে যে গ্রাফ পাওয়া যায় তার নাম n-মাত্রার hypercube Qn, আর একটি চক্রাকার Gray code মানে Qn-এর একটি Hamiltonian cycle, অর্থাৎ প্রতিটি কোণ ঠিক একবার ছুঁয়ে ফেরার পথ। উপপাদ্য ৩.২ তাই প্রমাণ করে, প্রতিটি n ≥ 2-এর জন্য Qn-এ Hamiltonian cycle আছে। গ্রাফের এই ভাষা মডিউল ১০-এর গ্রাফ তত্ত্বে আরও আসবে।

Reflected Gray code-এর একটি সরাসরি সূত্রও আছে: G(n)-এর k-তম শব্দ (০ থেকে গুনে) হলো k ⊕ (k ≫ 1), অর্থাৎ k-এর binary রূপের সঙ্গে এক ঘর ডানে সরানো k-এর বিটওয়াইজ XOR. Hardware-এ এই রূপান্তর এবং তার উল্টোটি Computer Architecture কোর্সের পর্ব ৪-এ আছে; এখানে আমরা সূত্রটি প্রমাণের বদলে Python-এ বড় n পর্যন্ত মিলিয়ে দেখব (অনুশীলন ১-এ এর একটি ফল প্রমাণ করবেন)। প্রথমবার চালালে ব্রাউজারে Python (Pyodide) নামতে কয়েক সেকেন্ড লাগে।

def gray(n):
    """reflected Gray code: G(n+1) = 0 G(n), then 1 G(n) reversed"""
    codes = [""]
    for _ in range(n):
        codes = ["0" + c for c in codes] + ["1" + c for c in reversed(codes)]
    return codes

def diff(a, b):                       # Hamming distance
    return sum(x != y for x, y in zip(a, b))

print("G(3):", " ".join(gray(3)))
for n in range(1, 13):
    g = gray(n)
    one_bit = all(diff(g[k], g[(k + 1) % len(g)]) == 1 for k in range(len(g)))
    distinct = len(set(g)) == 2 ** n
    formula = all(int(g[k], 2) == k ^ (k >> 1) for k in range(len(g)))
    if n in (1, 2, 3, 4, 8, 12):
        print(f"n = {n:2}: {len(g):4} words, cyclic one-bit: {one_bit},"
              f" all distinct: {distinct}, k xor (k>>1): {formula}")

items = ["cha", "dal", "tel"]         # a 3-item shop shelf
prev = "000"
for code in gray(3)[1:]:
    k = next(i for i in range(3) if code[i] != prev[i])
    move = "add" if code[k] == "1" else "remove"
    basket = [items[i] for i in range(3) if code[i] == "1"]
    print(code, f"{move:6} {items[k]:3} ->", basket)
    prev = code

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

G(3): 000 001 011 010 110 111 101 100
n =  1:    2 words, cyclic one-bit: True, all distinct: True, k xor (k>>1): True
n =  2:    4 words, cyclic one-bit: True, all distinct: True, k xor (k>>1): True
n =  3:    8 words, cyclic one-bit: True, all distinct: True, k xor (k>>1): True
n =  4:   16 words, cyclic one-bit: True, all distinct: True, k xor (k>>1): True
n =  8:  256 words, cyclic one-bit: True, all distinct: True, k xor (k>>1): True
n = 12: 4096 words, cyclic one-bit: True, all distinct: True, k xor (k>>1): True
001 add    tel -> ['tel']
011 add    dal -> ['dal', 'tel']
010 remove tel -> ['dal']
110 add    cha -> ['cha', 'dal']
111 add    tel -> ['cha', 'dal', 'tel']
101 remove dal -> ['cha', 'tel']
100 remove tel -> ['cha']

প্রতিটি n-এ তিনটি দাবিই True: আবর্তনসহ এক-বিট ধর্ম ((k + 1) % len(g) শেষ শব্দকে প্রথমটির সঙ্গে মেলায়), সব শব্দ আলাদা, আর k ⊕ (k ≫ 1) সূত্র। n = 12-এ ৪০৯৬টি শব্দ, তবু এটি প্রমাণ নয়, শুধু উপপাদ্য ৩.২-এর একটি পরীক্ষা। শেষ সাত লাইন বাজারঘরের তাকের ভাষায় একই তালিকা: প্রতিটি ধাপে ঝুড়িতে ঠিক একটি পণ্য ঢোকে বা বের হয়, আর আট ধাপে সব ৮টি ঝুড়ি একবার করে আসে (শুরুর খালি ঝুড়িসহ)।

৪ · Lexicographic ক্রমে permutation — next permutation algorithm

{1, 2, 3}-এর ৬টি permutation lexicographic ক্রমে: 123, 132, 213, 231, 312, 321. প্রথমটি ছোট থেকে বড় সাজানো, শেষটি বড় থেকে ছোট। প্রশ্ন: যেকোনো একটি permutation দেওয়া থাকলে ঠিক পরেরটি কীভাবে বের করবেন, সব তালিকা না বানিয়ে? 1 4 3 2-এর পরে কী? ভাবুন: 1 দিয়ে শুরু হওয়া permutation-গুলোর মধ্যে 1 4 3 2 সবচেয়ে বড়, কারণ বাকি অংশ 4 3 2 বড় থেকে ছোট সাজানো। তাই পরেরটিকে প্রথম ঘর বদলাতেই হবে, আর সবচেয়ে ছোট বদল হলো 1-এর জায়গায় 2, তারপর বাকিগুলো ছোট থেকে বড়: 2 1 3 4. এই যুক্তিকেই algorithm বানানো যায়।

Algorithm ৪.১ (next permutation)

ইনপুট a1 a2 … an.

  1. ডান থেকে বাঁয়ে খুঁজে সবচেয়ে বড় i বের করুন, যেখানে ai < ai+1. এমন i না থাকলে পুরো তালিকা অবর্ধমান, এটিই শেষ permutation; থামুন। ai-কে বলি pivot, আর ai+1 … an অংশকে suffix.
  2. Suffix-এ ai-এর চেয়ে বড় সবচেয়ে ডানের উপাদান aj খুঁজুন।
  3. ai আর aj অদলবদল করুন।
  4. Suffix-টি উল্টে দিন।
1a = 1 4 3 22ডান থেকে অবর্ধমান suffix খোঁজা3pivot = suffix-এর বাঁয়ের উপাদান4suffix-এ pivot-এর চেয়ে বড়, সবচেয়ে ডানে5অদলবদল6suffix উল্টানো
চলক
a
suffix
pivot / j
  1. প্রশ্ন। 1 4 3 2-এর ঠিক পরের permutation, lexicographic ক্রমে। ঘরগুলো বাঁ থেকে 1, 2, 3, 4 নম্বর।
  2. Suffix. ডান থেকে পড়লে 2, 3, 4 বাড়ছে, অর্থাৎ বাঁ থেকে 4 3 2 অবর্ধমান। তারপর 1, যা 4-এর চেয়ে ছোট, তাই suffix এখানে থামে।
  3. Pivot. i = 1, মান 1. Suffix 4 3 2 তার সর্বোচ্চ বিন্যাসে আছে, তাই 1 দিয়ে শুরু কোনো বড় permutation আর নেই।
  4. বদলি উপাদান। Suffix-এ 1-এর চেয়ে বড় সবচেয়ে ডানের উপাদান 2 (ঘর 4). suffix অবর্ধমান বলে এটিই 1-এর চেয়ে বড় উপাদানগুলোর মধ্যে সবচেয়ে ছোট।
  5. অদলবদল। 2 4 3 1. প্রথম ঘর যতটা সম্ভব কম বেড়েছে। নতুন suffix 4 3 1 এখনো অবর্ধমান।
  6. উল্টানো। অবর্ধমান suffix উল্টালে সবচেয়ে ছোট বিন্যাস 1 3 4 হয়। উত্তর 2 1 3 4, যা 2 দিয়ে শুরু প্রথম permutation ✓.
উপপাদ্য ৪.২ (শুদ্ধতা)

a আলাদা উপাদানের একটি permutation আর a শেষ permutation না হলে Algorithm ৪.১-এর ফল b হলো lexicographic ক্রমে a-এর ঠিক পরের permutation: b > a, আর a < c < b এমন কোনো permutation c নেই।

প্রমাণ:

  1. Suffix ai+1 … an অবর্ধমান, তাই এই উপাদানগুলোর সব বিন্যাসের মধ্যে এটিই সবচেয়ে বড়। ফলে a1 … ai দিয়ে শুরু কোনো permutation a-এর চেয়ে বড় নয়। — lexicographic ক্রমের সংজ্ঞা
  2. অতএব a-এর চেয়ে বড় যেকোনো c, a থেকে প্রথম আলাদা হয় কোনো ঘর p ≤ i-তে, সেখানে বড় মান নিয়ে। যাদের p = i (প্রথম i − 1টি ঘর a-এর মতো), তারা p < i এমন সবার আগে আসে, কারণ ঘর p-তে তারা a-এর ছোট মানটিই রাখে। p = i দলটি ফাঁকা নয়, কারণ ai+1 > ai ঘর i-তে বসতে পারে। তাই পরেরটি এই দলে। — ধাপ ১
  3. p = i দলের সবচেয়ে ছোট সদস্য পেতে i নম্বর ঘরে বসাতে হবে suffix-এর উপাদানগুলোর মধ্যে ai-এর চেয়ে বড় সবচেয়ে ছোটটি, আর বাকি ঘরে বাকি উপাদান ছোট থেকে বড়। — ক্রমের সংজ্ঞা
  4. Suffix অবর্ধমান বলে ai-এর চেয়ে বড় সবচেয়ে ডানের উপাদান aj-ই তাদের মধ্যে সবচেয়ে ছোট। অদলবদলের পর suffix-এ j-এর বাঁয়ের মানগুলো ≥ aj > ai, আর ডানের মানগুলো ≤ ai (j সবচেয়ে ডানের বড় উপাদান বলে), তাই নতুন suffix-ও অবর্ধমান। — suffix-এর গঠন
  5. অবর্ধমান তালিকা উল্টালে তা ছোট থেকে বড় হয়। তাই ফল b ঠিক ধাপ ৩-এর বর্ণনার permutation, অর্থাৎ a-এর ঠিক পরেরটি। ∎ — ধাপ ৩, ৪

খরচ: একটি কলের কাজ মোটামুটি suffix-এর দৈর্ঘ্য L-এর সমানুপাতিক। কয়েকটি কল দীর্ঘ (শেষের কাছে L = n), কিন্তু গড়ে খরচ ছোট। L ≥ j হয় ঠিক তখন, যখন শেষ j-টি উপাদান অবর্ধমান; সব n! permutation-এর মধ্যে এমনগুলোর ভগ্নাংশ 1/j!, কারণ শেষ j-টি ঘরে যে j-টি মানই থাকুক, তাদের j!টি বিন্যাসের ঠিক একটি অবর্ধমান। গড় L = ∑j≥1 (L ≥ j এমনগুলোর ভগ্নাংশ), তাই গড় L = ∑j=1n 1/j!, যা n বাড়লে e − 1 ≈ 1.718-এর দিকে যায়। অর্থাৎ পুরো তালিকা তৈরিতে প্রতি permutation-এ গড়ে ধ্রুবক কাজ (DSA কোর্সের amortized analysis-এর ভাষায়)। Scratch-এ n = 8-এর সব 40320টি permutation-এ গড় L মেপে 1.71828 পাওয়া গেছে, সূত্রের সঙ্গে হুবহু।

নিচের ব্লক algorithm-টি লিখে তার তৈরি পুরো তালিকা itertools.permutations-এর তালিকার সঙ্গে মেলায়। লক্ষ করুন, ধাপ ১ আর ২-এ >= আর <= ব্যবহার করায় একই উপাদান একাধিকবার থাকলেও algorithm চলে, আর তখন শুধু আলাদা বিন্যাসগুলো দেয়।

from itertools import permutations
from math import factorial

def next_perm(a):
    """next arrangement of a in lexicographic order, or None if a is last"""
    a = list(a)
    i = len(a) - 2
    while i >= 0 and a[i] >= a[i + 1]:   # step 1: find the pivot
        i -= 1
    if i < 0:
        return None                       # fully non-increasing: the last one
    j = len(a) - 1
    while a[j] <= a[i]:                   # step 2: rightmost element > pivot
        j -= 1
    a[i], a[j] = a[j], a[i]               # step 3: swap
    a[i + 1:] = reversed(a[i + 1:])       # step 4: suffix becomes increasing
    return a

def all_by_next(start):
    out, p = [], sorted(start)
    while p is not None:
        out.append(tuple(p))
        p = next_perm(p)
    return out

print("after 1 4 3 2 :", next_perm([1, 4, 3, 2]))
print("after 3 5 4 2 1:", next_perm([3, 5, 4, 2, 1]))
for n in range(1, 8):
    ours = all_by_next(range(1, n + 1))
    same = ours == list(permutations(range(1, n + 1)))
    print(f"n = {n}: {len(ours):5} arrangements, n! = {factorial(n):5}, same order as itertools: {same}")
words = ["".join(w) for w in all_by_next("BAZAR")]   # A repeats
print(len(words), "arrangements of BAZAR; first five:", words[:5])

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

after 1 4 3 2 : [2, 1, 3, 4]
after 3 5 4 2 1: [4, 1, 2, 3, 5]
n = 1:     1 arrangements, n! =     1, same order as itertools: True
n = 2:     2 arrangements, n! =     2, same order as itertools: True
n = 3:     6 arrangements, n! =     6, same order as itertools: True
n = 4:    24 arrangements, n! =    24, same order as itertools: True
n = 5:   120 arrangements, n! =   120, same order as itertools: True
n = 6:   720 arrangements, n! =   720, same order as itertools: True
n = 7:  5040 arrangements, n! =  5040, same order as itertools: True
60 arrangements of BAZAR; first five: ['AABRZ', 'AABZR', 'AARBZ', 'AARZB', 'AAZBR']

প্রথম লাইন stepper-এর উত্তর মেলায়। দ্বিতীয় লাইনে 3 5 4 2 1-এর suffix 5 4 2 1, pivot 3, বদলি 4, ফল 4 1 2 3 5. n = 1 থেকে 7 পর্যন্ত তালিকার দৈর্ঘ্য n!, আর ক্রমও itertools-এর সঙ্গে হুবহু এক, যা সম্পূর্ণতা, পুনরাবৃত্তিহীনতা আর ক্রম তিনটিই দেখায়। শেষ লাইনে BAZAR শব্দে A দুবার, তাই আলাদা বিন্যাস 5!/2! = 60, ঠিক পর্ব ৩৫-এর multiset permutation-এর সূত্র; itertools.permutations এখানে 120টি দিত, প্রতিটি শব্দ দুবার।

৫ · r-combination তৈরি, আর ranking ও unranking

{1, …, n}-এর একটি r-উপাদানের combination-কে সবসময় ছোট থেকে বড় লিখুন: c1 < c2 < … < cr. তখন lexicographic ক্রমে n = 5, r = 2-এর ১০টি: 12, 13, 14, 15, 23, 24, 25, 34, 35, 45. খেয়াল করুন, i নম্বর ঘরের সর্বোচ্চ সম্ভাব্য মান n − r + i, কারণ তার ডানে আরও r − i-টি বড় সংখ্যার জায়গা রাখতে হয় (n = 5, r = 2-এ প্রথম ঘর বড়জোর 4, দ্বিতীয়টি বড়জোর 5).

Algorithm ৫.১ (পরের r-combination)

ডান থেকে সবচেয়ে বড় i খুঁজুন, যেখানে ci < n − r + i. না থাকলে এটিই শেষ ({n − r + 1, …, n})। থাকলে ci-কে 1 বাড়ান, আর তার ডানের ঘরগুলোতে বসান ci + 1, ci + 2, … (পরপর সংখ্যা)।

শুদ্ধতার যুক্তি next permutation-এর মতোই। (১) i-এর ডানের সব ঘর সর্বোচ্চে, তাই c1 … ci দিয়ে শুরু কোনো বড় combination নেই। (২) তাই পরেরটি i নম্বর ঘর বাড়াবে, আর সবচেয়ে কম বাড়ানো হলো 1; মান ci + 1 ব্যবহারযোগ্য, কারণ তা ci+1-এর সমান হলে ci+1 সর্বোচ্চে থাকায় ci-ও সর্বোচ্চে থাকত। (৩) বাকি ঘরে সম্ভাব্য সবচেয়ে ছোট মান, অর্থাৎ পরপর সংখ্যা। যেমন n = 6-এ {1, 5, 6}: ঘর ৩ আর ২ সর্বোচ্চে (6 আর 5), ঘর ১-এ 1 < 4, তাই পরেরটি {2, 3, 4}.

Ranking আর unranking

তালিকায় একটি বস্তুর অবস্থান (০ থেকে গুনে) তার rank. বস্তু থেকে rank বের করা ranking, আর rank থেকে বস্তু unranking. কাজে লাগে অনেক জায়গায়: একটি combination-কে একটি পূর্ণসংখ্যা হিসেবে সংরক্ষণ, তালিকার মাঝখান থেকে কাজ শুরু (যেমন দুটি কম্পিউটারে অর্ধেক অর্ধেক ভাগ), আর একটি এলোমেলো পূর্ণসংখ্যা বেছে unrank করে সমান সম্ভাবনায় একটি এলোমেলো বস্তু পাওয়া।

Permutation-এর rank (Lehmer code): a1 … an-এর প্রতিটি ঘরে di = ডানে থাকা ai-এর চেয়ে ছোট উপাদানের সংখ্যা। তাহলে rank = ∑ di · (n − i)!. কারণ a1-এর চেয়ে ছোট d1টি মানের যেকোনোটি দিয়ে শুরু প্রতিটি permutation আগে আসে, আর প্রতিটি শুরুর জন্য (n − 1)!টি; তারপর একই যুক্তি দ্বিতীয় ঘরে। যেমন 3 1 4 2-এ d = (2, 0, 1, 0), rank = 2 · 3! + 0 + 1 · 1! + 0 = 13, অর্থাৎ ২৪টির মধ্যে ১৪ নম্বর।

Combination-এর rank: এখানে একটি অদ্ভুত সুন্দর সূত্র আছে, তবে lexicographic-এর বদলে একটু ভিন্ন ক্রমে, যার নাম colex (co-lexicographic): দুটি subset তুলনায় আগে বড় উপাদান দেখা হয়। উপাদান ০ থেকে গুনলে {0, 1, …}-এর 3-subset-গুলো colex ক্রমে {0,1,2}, {0,1,3}, {0,2,3}, {1,2,3}, {0,1,4}, …

উপপাদ্য ৫.২ (combinatorial number system)

S = {c1 < c2 < … < ck} ⊆ {0, 1, 2, …} হলে colex ক্রমে S-এর আগে ঠিক rank(S) = C(c1, 1) + C(c2, 2) + … + C(ck, k)টি k-subset আসে। ফলে {0, …, n − 1}-এর k-subset থেকে {0, 1, …, C(n, k) − 1}-এ rank একটি bijection, আর প্রতিটি অঋণাত্মক পূর্ণসংখ্যা N-এর এই আকারে ঠিক একটি প্রকাশ আছে।

প্রমাণ (k-এর ওপর induction):

  1. Colex ক্রমে T, S-এর আগে মানে: দুটির যে বৃহত্তম উপাদানে পার্থক্য, সেটি S-এ আছে। — সংজ্ঞা
  2. S-এর আগের T দুই ধরনের। (ক) max T < ck: এরা {0, …, ck − 1}-এর সব k-subset, সংখ্যায় C(ck, k). (খ) max T = ck: তখন T − {ck}-কে colex-এ S − {ck}-এর আগে আসতে হয়, দুটিই {0, …, ck − 1}-এর (k − 1)-subset. (max T > ck হলে T পরে আসে।) — ধাপ ১
  3. Inductive hypothesis অনুযায়ী (খ)-এর সংখ্যা C(c1, 1) + … + C(ck−1, k − 1); ভিত্তি k = 1-এ {c1}-এর আগে আছে c1টি একক সেট = C(c1, 1). — induction
  4. যোগের নিয়মে rank(S) = ∑ C(ci, i). Colex একটি সম্পূর্ণ ক্রম, আর C(n, k)টি subset-এর প্রতিটির rank তার আগে কয়টি আছে, তাই rank-গুলো ঠিক 0, 1, …, C(n, k) − 1, প্রতিটি একবার। ∎ — যোগের নিয়ম

Unranking লোভী (greedy) পদ্ধতিতে চলে: ck হলো সবচেয়ে বড় c যেখানে C(c, k) ≤ N; N থেকে তা বাদ দিয়ে একই কাজ k − 1-এ। যেমন N = 100, k = 3: C(9, 3) = 84 ≤ 100 < 120 = C(10, 3), তাই c3 = 9, বাকি 16; C(6, 2) = 15 ≤ 16 < 21, তাই c2 = 6, বাকি 1; C(1, 1) = 1, তাই c1 = 1. উত্তর {1, 6, 9}. পরের ব্লক দুটি দাবিই পরীক্ষা করে।

from itertools import combinations
from math import comb

def next_comb(c, n):            # next r-subset of {1..n}, lexicographic
    c, r = list(c), len(c)
    i = r - 1
    while i >= 0 and c[i] == n - r + i + 1:     # c[i] is at its maximum
        i -= 1
    if i < 0:
        return None
    c[i] += 1
    c[i + 1:] = range(c[i] + 1, c[i] + r - i)   # smallest possible tail
    return c
for n, r in [(5, 2), (6, 3), (10, 4)]:
    ours, c = [], list(range(1, r + 1))
    while c is not None:
        ours.append(tuple(c))
        c = next_comb(c, n)
    print(f"n={n}, r={r}: {len(ours)} subsets, C(n,r)={comb(n, r)},",
          "same as itertools:", ours == list(combinations(range(1, n + 1), r)))
print("after {1, 5, 6} in n = 6:", next_comb([1, 5, 6], 6))
def rank(s):                    # combinatorial number system (0-based items)
    return sum(comb(c, i + 1) for i, c in enumerate(sorted(s)))
def unrank(N, k):               # greedy: largest c with C(c, i) <= N
    out = []
    for i in range(k, 0, -1):
        c = i - 1
        while comb(c + 1, i) <= N:
            c += 1
        out.append(c)
        N -= comb(c, i)
    return sorted(out)
ranks = sorted(rank(s) for s in combinations(range(10), 3))
print("ranks of all 3-subsets of 0..9 are 0..119:", ranks == list(range(120)))
print("unrank(100, 3) =", unrank(100, 3), " rank back:", rank(unrank(100, 3)))

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

n=5, r=2: 10 subsets, C(n,r)=10, same as itertools: True
n=6, r=3: 20 subsets, C(n,r)=20, same as itertools: True
n=10, r=4: 210 subsets, C(n,r)=210, same as itertools: True
after {1, 5, 6} in n = 6: [2, 3, 4]
ranks of all 3-subsets of 0..9 are 0..119: True
unrank(100, 3) = [1, 6, 9]  rank back: 100

তিনটি (n, r)-এ তালিকার দৈর্ঘ্য C(n, r), আর ক্রম itertools.combinations-এর সঙ্গে এক (itertools-ও lexicographic ক্রমে দেয়)। {1, 5, 6}-এর পরে {2, 3, 4}, উপরের যুক্তির মতোই। {0, …, 9}-এর ১২০টি 3-subset-এর rank সাজালে ঠিক 0 থেকে 119, প্রতিটি একবার: উপপাদ্য ৫.২-এর bijection একটি নির্দিষ্ট n, k-তে। শেষ লাইনে হাতের হিসাব {1, 6, 9} মিলেছে, আর rank ফেরত দিয়েছে 100.

৬ · এলোমেলো permutation — Fisher–Yates আর তার সমতার প্রমাণ

সব permutation তালিকা করার বদলে কখনো দরকার শুধু একটি এলোমেলো permutation, যেখানে n!টির প্রতিটি সমান সম্ভাবনায় আসবে: ঈদের অফারে ক্রেতাদের সারি, পরীক্ষায় প্রশ্নের ক্রম, বা একটি algorithm-এর জন্য এলোমেলো ইনপুট। কম্পিউটারে এলোমেলো সংখ্যা আসে একটি pseudorandom number generator থেকে; এখানে ধরে নিই, সেটি 0 থেকে i-এর মধ্যে প্রতিটি পূর্ণসংখ্যা সমান সম্ভাবনায় আর আগের পছন্দ থেকে স্বাধীনভাবে দেয়। সমাধানটির নাম Fisher–Yates shuffle:

Algorithm ৬.১ (Fisher–Yates)

ঘর 0 থেকে n − 1. i = n − 1, n − 2, …, 1-এর প্রতিটির জন্য: 0 ≤ j ≤ i থেকে একটি j সমান সম্ভাবনায় বাছুন, আর a[i] ও a[j] অদলবদল করুন (j = i হলে কিছুই বদলায় না)।

বাজারঘরের লাকি ড্রয়ে বাক্স থেকে একটি একটি করে টিকিট তোলার কথা ভাবুন: প্রথমে বাক্সের যেকোনো টিকিট সমান সম্ভাবনায় শেষ ঘরে যায়, তারপর বাকিদের মধ্য থেকে একটি তার আগের ঘরে, এভাবে। Algorithm-টি ঠিক এই কাজ করে, শুধু তোলা টিকিট আলাদা বাক্সে না রেখে একই তালিকার ডান অংশে রাখে।

1a = A B C D2i = 3: j ∈ {0..3}3i = 2: j ∈ {0..2}4i = 1: j ∈ {0, 1}5ফল আর সম্ভাবনা
চলক
a
j
সম্ভাবনা
  1. শুরু। ৪টি পণ্য A, B, C, D, ঘর 0 থেকে 3. লক্ষ্য: ২৪টি বিন্যাসের প্রতিটি সমান সম্ভাবনায়।
  2. i = 3. ৪টি সমান সম্ভাব্য পছন্দের মধ্যে ধরুন j = 1 এল। a[3] আর a[1] অদলবদল: A D C B. শেষ ঘরে B স্থির হলো, প্রতিটি পণ্যের এখানে আসার সম্ভাবনা 1/4.
  3. i = 2. এবার ৩টির মধ্যে j = 2 এল, অর্থাৎ নিজের সঙ্গেই অদলবদল: কিছু বদলায় না, C ঘর 2-এ স্থির। এই সম্ভাবনা বাদ দিলে algorithm ভুল হয়ে যেত।
  4. i = 1. ২টির মধ্যে j = 0: a[1] আর a[0] অদলবদল, D A C B. ঘর 0-এ যা থাকে, তা আপনা থেকেই স্থির।
  5. ফল। পছন্দের ক্রম (1, 2, 0) এই ফল দিয়েছে, সম্ভাবনা 1/4 · 1/3 · 1/2 = 1/24 = 1/4!. পছন্দের ক্রম মোট 4 · 3 · 2 = 24টি, permutation-ও 24টি।
উপপাদ্য ৬.২ (Fisher–Yates-এর সমতা)

উপরের অনুমানে, n আলাদা উপাদানের যেকোনো নির্দিষ্ট permutation π-এর জন্য P(ফল = π) = 1/n!. অর্থাৎ ফলটির বণ্টন n!টি permutation-এর ওপর uniform distribution.

প্রমাণ (n-এর ওপর induction):

  1. ভিত্তি n = 1: কোনো ধাপ নেই, একমাত্র permutation-টি সম্ভাবনা 1 = 1/1!-এ আসে। — সরাসরি
  2. n ≥ 2-এ প্রথম ধাপ (i = n − 1) শেষ ঘরে বসায় a[j], যেখানে j সমান সম্ভাবনায় 0..n − 1 থেকে। উপাদানগুলো আলাদা, তাই π-এর শেষ উপাদান শেষ ঘরে আসে ঠিক একটি j-তে: সম্ভাবনা 1/n. — uniform পছন্দ
  3. এরপর বাকি ধাপগুলো (i = n − 2, …, 1) শুধু প্রথম n − 1টি ঘরে কাজ করে, আর সেগুলো হুবহু সেই n − 1টি উপাদানের ওপর Fisher–Yates. তাদের পছন্দ প্রথম পছন্দ থেকে স্বাধীন। — algorithm-এর গঠন
  4. Inductive hypothesis অনুযায়ী প্রথম n − 1টি ঘর π-এর প্রথম n − 1টির মতো হওয়ার সম্ভাবনা 1/(n − 1)!. — induction
  5. স্বাধীনতার জন্য গুণ: 1/n · 1/(n − 1)! = 1/n!. ∎ — গুণের নিয়ম

গণনার ভাষায় একই কথা: n · (n − 1) ⋯ 2 = n!টি সমান সম্ভাব্য পছন্দের ক্রম আছে, আর প্রমাণটি দেখায় প্রতিটি permutation ঠিক একটি ক্রম থেকে আসে, অর্থাৎ পছন্দের ক্রম থেকে permutation-এ একটি bijection. সম্ভাবনার পূর্ণ কাঠামো পর্ব ৩৮ “বিচ্ছিন্ন সম্ভাবনা — sample space, event আর Laplace-এর সংজ্ঞা”-এ আসবে; algorithm হিসেবে Fisher–Yates আর reservoir sampling-এর আলোচনা DSA কোর্সের পর্ব ৬১-এ আছে।

উপপাদ্য ৬.৩ (সাধারণ ভুল সংস্করণ পক্ষপাতদুষ্ট)

“i = 0, 1, …, n − 1-এর প্রতিটির জন্য 0..n − 1 থেকে যেকোনো j নিয়ে a[i] ↔ a[j]” সংস্করণটি n ≥ 3 হলে প্রতিটি permutation সমান সম্ভাবনায় দেয় না।

প্রমাণ (বিরোধ দিয়ে):

  1. এখানে প্রতিটি ধাপে n-টি পছন্দ, তাই nnটি সমান সম্ভাব্য পছন্দের ক্রম; প্রতিটি permutation-এর সম্ভাবনা (তাকে দেওয়া ক্রমের সংখ্যা)/nn. — গুণের নিয়ম
  2. ধরুন বণ্টন uniform. তাহলে প্রতিটি permutation পায় nn/n!টি ক্রম, যা পূর্ণসংখ্যা হতে হবে, অর্থাৎ n! ∣ nn. — বিরোধের অনুমান
  3. n − 1 ∣ n!, তাই n − 1 ∣ nn হতো। কিন্তু gcd(n − 1, n) = 1, ফলে gcd(n − 1, nn) = 1 (পর্ব ২১). n ≥ 3-এ n − 1 ≥ 2, তাই n − 1 ∤ nn. বিরোধ। ∎ — gcd-এর ধর্ম

শেষ ব্লকটি দুটি shuffle-কে n = 6-এ ৭২,০০০ বার চালায় (প্রতিটি permutation গড়ে ১০০ বার আসা উচিত), আর প্রতিটির জন্য chi-square statistic হিসাব করে: ∑ (দেখা − প্রত্যাশিত)² / প্রত্যাশিত. সত্যিই uniform হলে এর গড় মান মোটামুটি ঘরের সংখ্যা − 1 = 719, আর সাধারণ ওঠানামা প্রায় ±38; অনেক বড় মান মানে পক্ষপাত। তারপর n = 3-এ ভুল সংস্করণের সব 27টি পছন্দের ক্রম একবার করে চালিয়ে নির্ভুল গণনা দেখায়। প্রতিটি generator-এ seed দেওয়া, তাই প্রতিবার একই ফল।

import random
from collections import Counter
from itertools import permutations, product

def fisher_yates(a, rng):
    a = list(a)
    for i in range(len(a) - 1, 0, -1):
        j = rng.randint(0, i)            # 0 <= j <= i
        a[i], a[j] = a[j], a[i]
    return tuple(a)

def naive(a, rng):                       # the common wrong version
    a = list(a)
    for i in range(len(a)):
        j = rng.randint(0, len(a) - 1)   # any position, every time
        a[i], a[j] = a[j], a[i]
    return tuple(a)
def chi_square(shuffle, seed, n=6, trials=72_000):
    rng = random.Random(seed)
    counts = Counter(shuffle(range(n), rng) for _ in range(trials))
    expected = trials / 720              # 6! = 720 outcomes, 100 each
    return sum((counts[p] - expected) ** 2 / expected
               for p in permutations(range(n)))

for seed in (1, 2, 3):
    print(f"seed {seed}: chi2 Fisher-Yates {chi_square(fisher_yates, seed):6.1f},"
          f"  naive {chi_square(naive, seed):6.1f}  (df = 719)")
# exact count for n = 3: every sequence of random choices, once
exact = Counter()
for js in product(range(3), repeat=3):   # 3^3 = 27 equally likely runs
    a = list("ABC")
    for i, j in enumerate(js):
        a[i], a[j] = a[j], a[i]
    exact["".join(a)] += 1
print("naive, n = 3, out of 27:", dict(sorted(exact.items())))

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

seed 1: chi2 Fisher-Yates  716.7,  naive 6394.6  (df = 719)
seed 2: chi2 Fisher-Yates  700.5,  naive 5770.1  (df = 719)
seed 3: chi2 Fisher-Yates  802.0,  naive 5491.3  (df = 719)
naive, n = 3, out of 27: {'ABC': 4, 'ACB': 5, 'BAC': 5, 'BCA': 5, 'CAB': 4, 'CBA': 4}

Fisher–Yates-এর তিনটি মান 719-এর আশপাশে (802-ও প্রায় দুই ওঠানামার মধ্যে, এলোমেলো নমুনায় এমন হয়), আর ভুল সংস্করণের মান প্রায় আট গুণ: পক্ষপাত স্পষ্ট। তবে simulation শুধু ইঙ্গিত দেয়; প্রমাণ উপরের দুটি উপপাদ্য। শেষ লাইনই উপপাদ্য ৬.৩-এর নির্ভুল ছবি: 27টি সমান সম্ভাব্য ক্রম ৬টি permutation-এ ভাগ হয়েছে 4, 5, 5, 5, 4, 4 করে, কারণ 27 সংখ্যাটি 6 দিয়ে বিভাজ্যই নয়। ACB, BAC আর BCA আসে 5/27 সম্ভাবনায়, বাকিরা 4/27-এ।

৭ · শর্তযুক্ত বস্তু — backtracking, আর সব কিছু তৈরির খরচ

এখন পর্যন্ত যে বস্তুগুলো তৈরি করেছি, তাদের ওপর কোনো শর্ত ছিল না। কিন্তু প্রায়ই দরকার শুধু শর্ত মানা বস্তুগুলো: যেমন n × n দাবার বোর্ডে n-টি রানি, কোনো দুটি একই সারি, কলাম বা কোণাকুণিতে নয় (N-queens). প্রতিটি সারিতে একটি রানি, তাই একটি সমাধান হলো “কোন সারিতে কোন কলাম” এর একটি permutation, যেখানে কোণাকুণির শর্তও মানা হয়েছে। সরল পথ: সব n! permutation তৈরি করে প্রতিটি পরীক্ষা। ভালো পথ: backtracking, অর্থাৎ সারি ধরে ধরে রানি বসানো, আর কোনো আংশিক বসানো শর্ত ভাঙলেই সেই শাখার বাকিটা না দেখে পিছিয়ে আসা।

গাণিতিকভাবে backtracking একটি গাছ ঘুরে দেখে, যার নাম state-space tree: root খালি বোর্ড, প্রতিটি সন্তান একটি নতুন সিদ্ধান্ত যোগ করে, আর গভীরতা n-এর পাতাগুলো পূর্ণ বস্তু। শর্তহীন অবস্থায় এই গাছের পাতাগুলো ঠিক সব permutation, আর গাছের depth-first ভ্রমণ এগুলো lexicographic ক্রমেই দেয়; অর্থাৎ next permutation আর backtracking একই তালিকার দুই রূপ। শর্ত এলে গাছ ছাঁটা (pruning) হয়। Scratch-এ চালানো একটি সরল N-queens backtracking-এর গণনা:

nসমাধানঘুরে দেখা আংশিক বসানো (root সহ)n!nn
421724256
6415372046,656
8922,05740,32016,777,216

n = 1 থেকে 10 পর্যন্ত সমাধানের সংখ্যা 1, 0, 0, 2, 10, 4, 40, 92, 352, 724 (scratch-এ গোনা)। লক্ষ করুন, ক্রম নিয়মিত নয়: n = 6-এ মাত্র ৪টি, n = 5-এ ১০টি। n = 8-এ backtracking ২,০৫৭টি অবস্থা দেখে, যেখানে সব permutation পরীক্ষায় ৪০,৩২০টি আর “প্রতি সারিতে যেকোনো কলাম” পরীক্ষায় প্রায় ১.৬৮ কোটি। কোড আর pruning-এর কৌশল DSA কোর্সের পর্ব ৫৬-এ আছে; এখানে মূল কথা গাণিতিক: backtracking একই bijection-ভিত্তিক তালিকা, শুধু অচল শাখা বাদ দিয়ে।

সব তৈরি করা কখন অসম্ভব

ভালো generator প্রতি বস্তুতে গড়ে ধ্রুবক কাজ করে, কিন্তু বস্তুর সংখ্যাই যদি বিশাল হয়, কোনো কৌশল বাঁচাতে পারে না। একে বলে combinatorial explosion. ধরুন একটি কম্পিউটার সেকেন্ডে ১০ কোটি (108) permutation পরীক্ষা করতে পারে (একটি আনুমানিক ধারণা মাত্র):

nn!সেকেন্ডে 108 হারে সময়
103,628,800প্রায় 0.04 সেকেন্ড
136,227,020,800প্রায় ১ মিনিট
20≈ 2.43 × 1018প্রায় ৭৭০ বছর
52≈ 8.07 × 1067মহাবিশ্বের বয়সের চেয়ে অকল্পনীয় গুণ বেশি

তাই ২০টি ঠিকানার ডেলিভারি রুটের জন্য সব ক্রম তৈরি করে সেরাটি বাছা বাস্তবে সম্ভব নয়; সেখানে লাগে DP, branch and bound বা আনুমানিক algorithm, যা DSA কোর্সে আছে (বৃদ্ধির হারের তুলনা পর্ব ৪-এ)। “সব তৈরি করুন” কৌশলের সঠিক জায়গা হলো ছোট n: সূত্র যাচাই, প্রতিটি ছোট ইনপুটে একটি প্রোগ্রাম পরীক্ষা, আর counterexample খোঁজা, যা এই কোর্সে আমরা বারবার করছি।

৮ · পরীক্ষার অঙ্ক আর গণনার একটি ভুল প্রমাণ

বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর লিখিত পরীক্ষায় এই ধাঁচের অঙ্ক আসে। প্রতিটি উত্তর scratch-এ Python দিয়ে মেলানো।

অঙ্ক ১: 1 2 5 4 3-এর পরের permutation, আর 3 5 4 2 1-এর rank

  1. 1 2 5 4 3: suffix 5 4 3, pivot 2; suffix-এ 2-এর চেয়ে বড় সবচেয়ে ডানের 3. অদলবদলে 1 3 5 4 2, suffix উল্টে 1 3 2 4 5.
  2. 3 5 4 2 1-এর Lehmer code: 3-এর ডানে ছোট 2টি (2, 1), 5-এর ডানে 3টি, 4-এর ডানে 2টি, 2-এর ডানে 1টি: d = (2, 3, 2, 1, 0). rank = 2 · 24 + 3 · 6 + 2 · 2 + 1 · 1 = 71 (০ থেকে গুনে), অর্থাৎ ১২০টির মধ্যে ৭২ নম্বর। যুক্তিতেও মেলে: 3 দিয়ে শুরু ২৪টি permutation-এর এটি শেষটি, আর 1 ও 2 দিয়ে শুরু ৪৮টি আগে আছে: 48 + 23 = 71.

অঙ্ক ২: 4-বিট reflected Gray code-এর ১০ নম্বর শব্দ (০ থেকে গুনে)

  1. k = 10 = 1010, k ≫ 1 = 0101. XOR: 1111.
  2. যাচাই: G(4) = 0·G(3), 1·G(3)R; ১০ নম্বর হলো দ্বিতীয় অর্ধেকের ২ নম্বর (০ থেকে)। G(3)R = 100, 101, 111, …-এর ২ নম্বর 111, আগে 1 বসিয়ে 1111 ✓.

অঙ্ক ৩: combination

  1. n = 7-এ {2, 5, 6, 7}-এর পরের 4-combination: ঘর ৪, ৩, ২ সর্বোচ্চে (7, 6, 5), ঘর ১-এ 2 < 4. 2 → 3, তারপর 4, 5, 6: {3, 4, 5, 6}.
  2. Combinatorial number system-এ 50, k = 4: C(7, 4) = 35 ≤ 50 < 70, বাকি 15; C(5, 3) = 10 ≤ 15 < 20, বাকি 5; C(3, 2) = 3 ≤ 5 < 6, বাকি 2; C(2, 1) = 2. উত্তর {2, 3, 5, 7}.

প্রমাণের ভুল ধরুন: “৫টি পণ্য থেকে ৩টির দল ৬০টি”

একজন লিখলেন: “দাবি: ৫টি পণ্য থেকে ৩টির কম্বো দল 60টি। প্রমাণ: আমার generator permutations(items, 3) চালিয়ে প্রতিটি ফলাফলকে একটি দল হিসেবে নিয়েছে; সেটি 5 · 4 · 3 = 60টি ফল দিয়েছে, আর প্রতিটি ফলাফলে ৩টি আলাদা পণ্য আছে, তাই প্রতিটিই একটি বৈধ দল। অতএব দল 60টি। ∎” পড়ে ভুলটি নিজে খুঁজুন।

ভুল কোথায়: “প্রতিটি ফলাফল একটি বৈধ দল” দেখায় শুধু যে generator-এর ফল থেকে দলে একটি ফাংশন আছে, যা onto. কিন্তু গণনার সমতার জন্য দরকার bijection, আর ফাংশনটি one-to-one নয়: (চা, ডাল, তেল), (ডাল, চা, তেল) সহ ৩! = 6টি ক্রম একই দল দেয়। এটি পুরোনো overcounting-এর ভুল, generator-এর আড়ালে। প্রতিটি দল ঠিক ৬ বার আসে বলে ভাগের নিয়মে উত্তর 60/6 = 10 = C(5, 3). Counterexample দিয়েও ধরা যায়: ৩টি পণ্য থেকে ৩টির দল স্পষ্টতই ১টি, অথচ যুক্তিটি 3! = 6 বলত। শিক্ষা: generator-এর তালিকার দৈর্ঘ্যকে সংখ্যা ধরার আগে প্রমাণ করুন যে কোনো বস্তু দুবার আসে না, অথবা len(set(...)) দিয়ে মিলিয়ে নিন।

আরও গভীরে: পরপর বস্তুর মধ্যে “সবচেয়ে ছোট বদল” রাখার অনুসন্ধান একটি পুরো গবেষণাক্ষেত্র, যার নাম combinatorial Gray code: যেমন Steinhaus–Johnson–Trotter পদ্ধতিতে প্রতিটি permutation আগেরটি থেকে শুধু পাশাপাশি দুটি উপাদানের অদলবদলে আসে, আর Heap-এর পদ্ধতিতে প্রতিবার একটি অদলবদল। কিছু ক্ষেত্রে প্রতি বস্তুতে শুধু গড়ে নয়, সবচেয়ে খারাপ ক্ষেত্রেও ধ্রুবক কাজ সম্ভব (loopless algorithm). Donald Knuth-এর The Art of Computer Programming-এর চতুর্থ খণ্ড (Volume 4A, ২০১১) এই বিষয়ের মানদণ্ড গ্রন্থ।

পরীক্ষা ও ইন্টারভিউতে: বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর পরীক্ষায় এই ধরনের প্রশ্ন প্রায়ই আসে:

  • 2 3 1-এর পরের permutation? suffix 3 1, pivot 2, বদলি 3: 3 1 2.
  • 3-বিট Gray code লিখুন। 000 001 011 010 110 111 101 100.
  • n উপাদানের সব subset তৈরির সময়? 2nটি subset, প্রতিটি লিখতে O(n): Θ(n · 2n).
  • Fisher–Yates-এ i-এর জন্য j কোন পরিসর থেকে? 0..i, পুরো 0..n − 1 নয়; নইলে nnটি ক্রম, যা n! দিয়ে বিভাজ্য নয়।
  • 8-queens-এর সমাধান কয়টি? 92.

৯ · AI-এর যুগে — তৈরি করা কোড আর গণনার দাবি যাচাই

“সব permutation তৈরি করো” বা “একটি shuffle লেখো” এমন অনুরোধে AI সহকারী দ্রুত কোড দেয়, আর কোডটি প্রায়ই দেখতে ঠিক। কিন্তু এই বিষয়ে কয়েকটি ভুল নিয়মিত দেখা যায়, আর সবগুলোই ছোট n-এ সব তালিকা করে মেলালেই ধরা পড়ে:

AI-এর ভুলকীভাবে ধরবেন
Shuffle-এ j-এর পরিসর 0..n − 1 বা 0..i − 1 (প্রথমটি পক্ষপাতদুষ্ট, দ্বিতীয়টি শুধু “চক্রাকার” permutation দেয়)n = 3-এ সব পছন্দের ক্রম চালিয়ে প্রতিটি ফল গুনুন; ৬টি ফলের প্রতিটি সমান কি না দেখুন
Next permutation-এ “<” আর “<=” গুলিয়ে ফেলা (পুনরাবৃত্ত উপাদানে ভুল ফল বা error), বা অদলবদলের পর suffix উল্টাতে ভুলে যাওয়াপুরো তালিকা তৈরি করে itertools-এর সঙ্গে মেলান, আর পুনরাবৃত্ত উপাদানসহ একটি ইনপুটও দিন
Generator-এর তালিকার দৈর্ঘ্যকেই সংখ্যা ধরা, অথচ বস্তু দুবার আসছেlen(list) আর len(set) দুটিই ছাপান, আর গোনার সূত্রের সঙ্গে মেলান
Gray code-এ আবর্তনের বদল বা সূত্র k ⊕ (k ≫ 1)-এর বদলে k ⊕ (k ≪ 1)পরপর প্রতিটি জোড়ার Hamming distance হিসাব করুন, শেষ-প্রথম জোড়াসহ
প্রমাণে হাইপোথিসিস বাদ, যেমন “random সংখ্যা স্বাধীন আর uniform” অনুমান না বলা, বা আত্মবিশ্বাসী বৃত্তাকার যুক্তিপ্রতিটি ধাপের যুক্তি আলাদা করে চান, আর কোন অনুমান কোথায় লাগছে তা লিখতে বলুন
বানানো ইতিহাস বা উৎস, যেমন ভুল সাল বা অস্তিত্বহীন বইবিশ্বস্ত পাঠ্যবই বা মূল উৎসে মিলিয়ে নিন

AI-এর কাছে একটি generator চাইলে এমনভাবে চাইতে পারেন, যাতে প্রমাণ আর পরীক্ষা দুটিই সঙ্গে আসে:

আমার জন্য একটি Python generator লেখো, যা [যেমন {1..n}-এর সব r-combination, lexicographic ক্রমে] তৈরি করে। ১. algorithm-টি ধাপে ধাপে বাংলায় লেখো, আর কোন ক্রমে বস্তুগুলো আসবে তা স্পষ্ট বলো। ২. শুদ্ধতার যুক্তি দাও: কেন প্রতিটি বস্তু ঠিক একবার আসে, আর কেন ক্রমটি ঠিক। কোনো অনুমান (যেমন উপাদানগুলো আলাদা, বা random সংখ্যা uniform আর স্বাধীন) থাকলে তা আলাদা করে লেখো। ৩. প্রতিটি বস্তুর জন্য গড় আর সবচেয়ে খারাপ ক্ষেত্রের কাজ কত, বলো। ৪. একটি পরীক্ষা কোড দাও, যা n ≤ 7-এর সব ক্ষেত্রে তালিকাটি itertools-এর সঙ্গে মেলায়, len আর len(set) দুটিই ছাপায়, আর গোনার সূত্রের সঙ্গে তুলনা করে। ৫. random কিছু থাকলে seed ব্যবহার করো, আর n = 3-এ সব সম্ভাব্য পছন্দ চালিয়ে নির্ভুল গণনা দেখাও।

AI-এর উত্তর হাতে পেলে — যাচাইয়ের চেকলিস্ট

Generator যাচাইয়ের সবচেয়ে শক্তিশালী হাতিয়ার আপনি নিজেই এই পর্বে বানিয়েছেন: ছোট n-এ সব বস্তু তৈরি করে গোনা, আর একটি বিশ্বস্ত তালিকার সঙ্গে মেলানো। AI যত সুন্দর ব্যাখ্যাই দিক, পাঁচ লাইনের এই পরীক্ষা চালানোর আগে কোডটি বিশ্বাস করবেন না।

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

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

প্র ০১সব বিন্যাস একে একে তালিকা করা আর তালিকায় একটি বিন্যাসের অবস্থান বের করা, এই প্রশ্নগুলো ভারতীয় উপমহাদেশের ছন্দশাস্ত্র আর গণিতে কীভাবে এসেছিল?

সংস্কৃত ছন্দে প্রতিটি অক্ষর হয় লঘু (ছোট) নয়তো গুরু (দীর্ঘ), তাই n অক্ষরের একটি ছন্দ আসলে দৈর্ঘ্য n-এর একটি দুই-অক্ষরের string, যাকে আজ আমরা bit string বলি। পিঙ্গলের ছন্দঃশাস্ত্র (রচনাকাল নিয়ে মতভেদ আছে, সাধারণত খ্রিস্টপূর্ব কয়েক শতাব্দী বলা হয়) আর তার পরবর্তী ভাষ্যগুলোতে কয়েকটি নিয়মবদ্ধ প্রক্রিয়া পাওয়া যায় বলে ইতিহাসবিদেরা লেখেন। “প্রস্তার” হলো সব ছন্দ একটি নির্দিষ্ট ক্রমে তালিকা করার পদ্ধতি; “নষ্ট” হলো একটি ক্রমিক নম্বর থেকে ছন্দটি বের করা; আর “উদ্দিষ্ট” হলো একটি ছন্দ থেকে তার ক্রমিক নম্বর বের করা। আধুনিক ভাষায় এগুলো ঠিক generation, unranking আর ranking, আর বিবরণে প্রক্রিয়াটি binary সংখ্যা গোনার সঙ্গে ঘনিষ্ঠভাবে মেলে।

পরে চতুর্দশ শতাব্দীতে নারায়ণ পণ্ডিতের “গণিত কৌমুদী” (প্রায়ই ১৩৫৬ সাল বলা হয়) permutation নিয়ে বিস্তারিত আলোচনা করে। Donald Knuth তাঁর The Art of Computer Programming-এর চতুর্থ খণ্ডে lexicographic ক্রমে পরের permutation বের করার পদ্ধতির একটি প্রাচীন রূপ নারায়ণ পণ্ডিতের বলে উল্লেখ করেছেন; ঠিক কোন রূপে তিনি পদ্ধতিটি লিখেছিলেন, তার খুঁটিনাটি অনুবাদ আর ব্যাখ্যার ওপর নির্ভর করে, তাই এখানে সতর্কভাবে বলা ভালো যে মূল ধারণাটি তাঁর লেখায় পাওয়া যায় বলে মনে করা হয়।

শিক্ষাটি ইতিহাসের চেয়েও বড়। গোনা, তালিকা করা, আর অবস্থান থেকে বস্তু ও বস্তু থেকে অবস্থান, এই চারটি প্রশ্ন একই কাঠামোর চার দিক, আর প্রতিটির পেছনে আছে একটি bijection: বস্তুর সেট আর {0, 1, …, N − 1}-এর মধ্যে। ছন্দশাস্ত্রের কবিরা কম্পিউটার ছাড়াই এই কাঠামো ব্যবহার করেছিলেন, কারণ সব সম্ভাব্য ছন্দ একবার করে, কোনোটি বাদ না দিয়ে, বিবেচনা করা দরকার ছিল।

মূল উপলব্ধি: Generation, ranking আর unranking আধুনিক কম্পিউটার বিজ্ঞানের শব্দ হলেও প্রশ্নগুলো বহু পুরোনো; তিনটিই একটি সুনির্দিষ্ট ক্রম আর একটি bijection-এর ওপর দাঁড়িয়ে।

প্র ০২Gray code-এর নাম কোথা থেকে এল, আর বাস্তব যন্ত্রে “একবারে একটি বিট বদল” কেন এত গুরুত্বপূর্ণ? Tower of Hanoi-এর সঙ্গে এর সম্পর্ক কী?

নামটি এসেছে Bell Labs-এর গবেষক Frank Gray-এর নাম থেকে, যিনি “Pulse code communication” শিরোনামে একটি পেটেন্টে এই কোড ব্যবহার করেন; আবেদন ১৯৪৭ সালে, অনুমোদন ১৯৫৩ সালে বলে সাধারণত উল্লেখ করা হয়। তবে ধারণাটি তাঁর আগেও ছিল: উনিশ শতকে টেলিগ্রাফে Émile Baudot একই ধাঁচের কোড ব্যবহার করেছিলেন বলে লেখা হয়, আর চীনা রিং ধাঁধা (baguenaudier)-র সমাধানও এই ক্রম মেনে চলে। তাই নামটি আবিষ্কারের নয়, পরিচিতির সূত্রে।

গুরুত্ব বোঝার জন্য একটি ঘূর্ণায়মান চাকার অবস্থান-সেন্সর (rotary encoder) ভাবুন, যেখানে চাকার প্রতিটি খণ্ডে একটি সংখ্যা ছাপা, আর কয়েকটি সেন্সর বিটগুলো পড়ে। সাধারণ binary-তে 011 থেকে 100-এ যেতে তিনটি বিট একসঙ্গে বদলায়; কিন্তু বাস্তবের সেন্সর ঠিক একই মুহূর্তে বদলায় না। সীমানায় এক মুহূর্তের জন্য 111 বা 000-এর মতো সম্পূর্ণ ভুল মান পড়া যেতে পারে। Gray code-এ সীমানায় শুধু একটি বিট বদলায়, তাই সবচেয়ে খারাপ ক্ষেত্রেও পড়া মান দুই প্রতিবেশীর একটি। Digital logic-এর K-map-এর সারি আর কলামও এই ক্রমে সাজানো হয়, যাতে পাশের ঘরগুলো এক চলকে আলাদা থাকে।

Tower of Hanoi-এর সংযোগটি বিস্ময়কর। n-টি চাকতির সর্বোত্তম সমাধানে 2n − 1টি চাল, আর k-তম চালে কোন চাকতি সরে, তা হলো k-এর binary রূপের সবচেয়ে ডানের 1-বিটের অবস্থান (ছোটতম চাকতি = বিট 0). ঠিক এই অবস্থানের বিটটিই Gray code-এ k − 1 থেকে k-তে যাওয়ার সময় বদলায় (অনুশীলন ১)। অর্থাৎ Hanoi-এর চালের ক্রম আর Gray code একই “ruler sequence” 0, 1, 0, 2, 0, 1, 0, … মেনে চলে। Recursion-এ Hanoi সমাধান আর প্রতিফলনে Gray code নির্মাণ, দুটির কাঠামোও এক: অর্ধেক কাজ, একটি বড় পদক্ষেপ, তারপর অর্ধেক কাজ উল্টো দিকে।

মূল উপলব্ধি: “পরপর বস্তুর মধ্যে সবচেয়ে ছোট বদল” একটি গাণিতিক ধর্ম, যা বাস্তবে ভুল পড়া কমায়, হিসাব হালনাগাদ সস্তা করে, আর আশ্চর্য জায়গায় (Hanoi, hypercube) একই কাঠামো হিসেবে ফিরে আসে।

প্র ০৩Fisher–Yates পদ্ধতি কীভাবে কাগজ-কলম থেকে কম্পিউটারে এল, আর একটি ভালো shuffle algorithm থাকলেও এলোমেলো সংখ্যার উৎস কেন আলাদা প্রশ্ন?

Ronald Fisher আর Frank Yates ১৯৩৮ সালে তাঁদের পরিসংখ্যানের সারণির বইয়ে (Statistical Tables for Biological, Agricultural and Medical Research) একটি পদ্ধতি দেন: সংখ্যাগুলো কাগজে লিখে, এলোমেলো সংখ্যার সারণি থেকে একটি একটি করে বাকি থাকা সংখ্যা বেছে কেটে দেওয়া। এতে প্রতিবার বাকিগুলো খুঁজতে সময় লাগে। কম্পিউটারের জন্য একই তালিকার ভেতরে অদলবদলের রূপটি Richard Durstenfeld ১৯৬৪ সালে প্রকাশ করেন বলে উল্লেখ করা হয়, আর Donald Knuth-এর The Art of Computer Programming (দ্বিতীয় খণ্ড) রূপটিকে ব্যাপক পরিচিতি দেয়। তাই একে কখনো Knuth shuffle-ও বলা হয়। এই রূপে মোট কাজ Θ(n).

উপপাদ্য ৬.২-এর প্রমাণে একটি অনুমান ছিল: প্রতিটি j সত্যিকারের uniform আর স্বাধীন। বাস্তবে সংখ্যা আসে একটি pseudorandom generator থেকে, যার আচরণ পুরোপুরি ঠিক হয় তার শুরুর অবস্থা (seed) দিয়ে। এখানে একটি সরল গণনার যুক্তি প্রচণ্ড শক্তিশালী: generator-এর সম্ভাব্য অবস্থা যদি S-টি হয়, তাহলে সে বড়জোর S-টি আলাদা shuffle দিতে পারে। ৫২টি তাসের বিন্যাস 52! ≈ 8.07 × 1067, যা প্রায় 2225.6. ৩২-বিট seed-এর একটি generator তাই বড়জোর 232 ≈ ৪৩০ কোটি বিন্যাস দিতে পারে, যা সব বিন্যাসের অকল্পনীয় ছোট একটি অংশ; আর seed অনুমান করা গেলে পুরো ক্রমই অনুমান করা যায়। এটি pigeonhole principle-এর (পর্ব ৩২) সরাসরি প্রয়োগ।

Python-এর random module Mersenne Twister ব্যবহার করে, যার অবস্থা অনেক বড়, তাই পরীক্ষা আর simulation-এর জন্য ভালো; কিন্তু এটি নিরাপত্তার জন্য তৈরি নয়, কারণ যথেষ্ট আউটপুট দেখলে অবস্থা পুনর্গঠন করা যায়। লটারি, টোকেন বা পাসওয়ার্ডের মতো নিরাপত্তার কাজে Python-এর secrets module বা অপারেটিং সিস্টেমের নিরাপদ উৎস ব্যবহার করতে হয়। এছাড়া অনেক ভুল shuffle সহজ ভুল থেকে আসে: j-এর পরিসর ভুল, বা “random তুলনা দিয়ে sort”, যার ফলাফলের বণ্টন sort algorithm-এর ওপর নির্ভর করে এবং সাধারণত uniform হয় না।

মূল উপলব্ধি: একটি shuffle-এর ন্যায্যতা দুই স্তরের: algorithm-টি প্রমাণসহ uniform কি না (গণনার প্রশ্ন), আর এলোমেলো সংখ্যার উৎসের অবস্থা যথেষ্ট বড় ও অননুমেয় কি না (pigeonhole আর নিরাপত্তার প্রশ্ন)।

প্র ০৪সফটওয়্যার পরীক্ষায় “ছোট সব ইনপুট তৈরি করে চালানো” কতটা কাজের? এ নিয়ে কোনো উপপাদ্য আছে কি, যা বলে ছোট পরীক্ষাই যথেষ্ট?

সাধারণভাবে কোনো সসীম পরীক্ষা অসীম ইনপুটের প্রোগ্রামের শুদ্ধতা প্রমাণ করে না। তবু অভিজ্ঞতায় দেখা যায়, বেশির ভাগ ভুল ছোট ইনপুটেই ধরা পড়ে: খালি তালিকা, এক উপাদান, দুটি সমান উপাদান, উল্টো সাজানো তালিকা। Software design-এর গবেষক Daniel Jackson এই পর্যবেক্ষণকে “small scope hypothesis” নামে প্রচলিত করেন বলে উল্লেখ করা হয়, আর তাঁর Alloy টুল একটি সীমা পর্যন্ত সব সম্ভাব্য অবস্থা খুঁজে দেখে। এটি একটি অনুমান, উপপাদ্য নয়, কিন্তু খরচের তুলনায় লাভ বিশাল।

কিছু ক্ষেত্রে সত্যিকারের উপপাদ্যও আছে। Sorting network হলো তুলনা-অদলবদলের একটি নির্দিষ্ট তালিকা, ইনপুটের ওপর নির্ভর না করে। “০-১ নীতি” (0-1 principle, Knuth-এর বইয়ে বিস্তারিত) বলে: একটি comparator network সব ইনপুট সঠিকভাবে sort করে, যদি এবং কেবল যদি সব 0/1 ইনপুট সঠিকভাবে sort করে। তাই n-টি ইনপুটের network যাচাইয়ে n!টি permutation নয়, 2nটি bit string-ই যথেষ্ট; n = 16-এ 216 = 65,536, যেখানে 16! প্রায় 2 × 1013. এখানে এই পর্বের দুটি generator, subset আর permutation, সরাসরি তুলনায় আসে।

আজকের property-based testing পদ্ধতিও (যেমন Python-এর Hypothesis লাইব্রেরি) একই ধারণার এলোমেলো রূপ: একটি সাধারণ ধর্ম লিখুন (“sort-এর ফল সাজানো আর ইনপুটের একটি permutation”), আর টুলটি অনেক ইনপুট তৈরি করে ধর্মটি ভাঙে কি না দেখে, ভাঙলে সবচেয়ে ছোট counterexample খোঁজে। পরীক্ষার কৌশলের বিস্তারিত Software Engineering কোর্সে আছে। এই কোর্সে আমরা প্রতিটি পর্বে একই কাজ করি: সূত্রটি ছোট n-এ brute force দিয়ে মেলাই, আর তারপর প্রমাণ লিখি।

মূল উপলব্ধি: ছোট সব ইনপুট তৈরি করে চালানো প্রমাণের বিকল্প নয়, কিন্তু ভুল ধরার সবচেয়ে সস্তা জাল; আর কিছু বিশেষ ক্ষেত্রে (যেমন ০-১ নীতি) একটি উপপাদ্য সসীম পরীক্ষাকেই পূর্ণ প্রমাণে পরিণত করে।

প্র ০৫Ranking আর unranking কেন শুধু তাত্ত্বিক খেলা নয়? একটি বিন্যাসকে একটি পূর্ণসংখ্যায় পরিণত করলে কম্পিউটার বিজ্ঞানে কী কী সুবিধা হয়?

প্রথম সুবিধা জায়গা আর দ্রুত খোঁজ। ধাঁধা-সমাধানকারী প্রোগ্রাম (যেমন ৮টি টাইলের স্লাইডিং ধাঁধা) প্রতিটি অবস্থা আগে দেখা হয়েছে কি না মনে রাখে। অবস্থাগুলো হলো ৯টি ঘরের permutation, মোট 9! = 362,880টি। প্রতিটি অবস্থাকে Lehmer code দিয়ে 0..362,879-এর একটি সংখ্যায় পরিণত করলে একটি সাধারণ array-ই “দেখা হয়েছে” তালিকা হয়ে যায়, hash table ছাড়া, আর কোনো জায়গা নষ্ট হয় না। বড় ধাঁধার জন্য আংশিক অবস্থার এমন সূচক দিয়ে বানানো সারণি (pattern database) heuristic search-এ ব্যবহার হয় বলে গবেষণায় জানা যায়।

দ্বিতীয় সুবিধা কাজ ভাগ করা। C(40, 6) = 3,838,380টি combination পরীক্ষা করতে চারটি কম্পিউটার থাকলে প্রতিটিকে rank-এর একটি পরিসর দিন, আর প্রতিটি নিজের পরিসরের শুরুর বস্তুটি unrank করে সেখান থেকে next combination চালাক। কোনো যোগাযোগ ছাড়াই কাজ সমান চার ভাগ, আর কোনো বস্তু বাদ বা দুবার নয়, কারণ rank একটি bijection. তৃতীয় সুবিধা এলোমেলো নমুনা: 0..C(n, k) − 1 থেকে uniform একটি পূর্ণসংখ্যা নিয়ে unrank করলে প্রতিটি k-subset সমান সম্ভাবনায় আসে, কারণ bijection সমান সম্ভাবনা রক্ষা করে।

ইতিহাস নিয়ে একটু সাবধানতা: combinatorial number system-এর ধারণা উনিশ শতকের শেষে Ernesto Pascal-এর কাজে পাওয়া যায় বলে উল্লেখ করা হয়, আর factorial number system বা Lehmer code-এর নাম বিংশ শতাব্দীর গণিতবিদ D. H. Lehmer-এর সঙ্গে যুক্ত; সঠিক অগ্রাধিকার নিয়ে উৎসভেদ আছে। দুটি পদ্ধতিই আসলে “মিশ্র ভিত্তির সংখ্যা পদ্ধতি”: Lehmer code-এ ঘরের মান (n − 1)!, (n − 2)!, …; combinatorial number system-এ C(c, i). ঘড়িতে ঘণ্টা-মিনিট-সেকেন্ড যেমন ২৪-৬০-৬০ ভিত্তি মিলিয়ে সময়কে একটি সংখ্যায় রূপ দেয়, এরাও তেমনি বিন্যাসকে একটি সংখ্যায় রূপ দেয়।

মূল উপলব্ধি: Ranking একটি সসীম সেট আর {0, …, N − 1}-এর মধ্যে একটি গণনাযোগ্য bijection; তা পেলে সংরক্ষণ, কাজ ভাগ আর uniform নমুনা, তিনটিই প্রায় বিনা খরচে আসে।

অনুশীলন

আগে খাতায় করুন, তারপর উত্তর খুলে মেলান।

  1. প্রমাণ: g(k) = k ⊕ (k ≫ 1) ধরে নিয়ে প্রমাণ করুন, g(k) আর g(k + 1) ঠিক এক বিটে আলাদা, আর সেই বিটের অবস্থান t হলো k + 1-এর binary রূপের সবচেয়ে ডানের 1-বিটের অবস্থান (বিট 0 সবচেয়ে ডানে)।
    1. ধরুন k-এর ডান প্রান্তে ঠিক t-টি 1-বিট, তারপর একটি 0. k + 1 গুনলে ওই t-টি 1 হয়ে যায় 0, আর বিট t-এর 0 হয় 1; বাকি বিট অপরিবর্তিত। তাই k + 1-এর সবচেয়ে ডানের 1-বিট অবস্থান t-তে, আর k ⊕ (k + 1) = 2t+1 − 1 (নিচের t + 1টি বিট 1). — binary যোগের carry
    2. XOR-এর বিনিময় ও সংযোগ ধর্মে g(k) ⊕ g(k + 1) = (k ⊕ (k + 1)) ⊕ ((k ≫ 1) ⊕ ((k + 1) ≫ 1)), আর ডানে সরানো XOR-এর সঙ্গে বণ্টিত হয়, তাই দ্বিতীয় বন্ধনী (k ⊕ (k + 1)) ≫ 1 = 2t − 1. — বিটওয়াইজ ধর্ম
    3. (2t+1 − 1) ⊕ (2t − 1): নিচের t-টি বিট দুটিতেই 1, তাই কেটে যায়; বিট t শুধু প্রথমটিতে 1. ফল 2t, অর্থাৎ ঠিক একটি বিট, অবস্থান t. ∎

    যাচাই: k = 3 → 4: g(3) = 010, g(4) = 110, বদলেছে বিট 2, আর 4 = 100-এর সবচেয়ে ডানের 1 বিট 2-তে ✓. Scratch-এ 0 ≤ k ≤ 14-এর সব ক্ষেত্রে মেলানো। এই অবস্থানের ক্রম 0, 1, 0, 2, 0, 1, 0, 3, … হলো Hanoi-এর চাকতির ক্রম (প্র ০২)।

  2. হিসাব: (ক) {1, 2, 3, 4}-এর permutation-গুলোর lexicographic তালিকায় 2 4 1 3 কত নম্বরে (১ থেকে গুনে)? (খ) n = 6-এ {1, 3, 5, 6}-এর পরের 4-combination কী? (গ) Combinatorial number system-এ k = 3 আর N = 20 হলে subset কোনটি?

    (ক) d = (1, 2, 0, 0): 2-এর ডানে ছোট শুধু 1; 4-এর ডানে 1 আর 3; বাকিগুলোতে 0. rank = 1 · 3! + 2 · 2! = 6 + 4 = 10 (০ থেকে), তাই ১১ নম্বর। যুক্তিতে: 1 দিয়ে শুরু ৬টি, তারপর 2 1 …, 2 3 … দিয়ে ৪টি, এরপর 2 4 1 3.

    (খ) সর্বোচ্চ মান: ঘর ৪-এ 6, ঘর ৩-এ 5, ঘর ২-এ 4. ঘর ৪ আর ৩ সর্বোচ্চে, ঘর ২-এ 3 < 4. 3 → 4, তারপর 5, 6: {1, 4, 5, 6}.

    (গ) C(6, 3) = 20 ≤ 20 < 35, তাই c3 = 6, বাকি 0. তারপর C(c, 2) ≤ 0-এর সবচেয়ে বড় c = 1, আর C(c, 1) ≤ 0-এর c = 0. উত্তর {0, 1, 6}; যাচাই: C(0, 1) + C(1, 2) + C(6, 3) = 0 + 0 + 20 ✓. তিনটি উত্তরই scratch-এ itertools দিয়ে মেলানো।

  3. আউটপুট অনুমান: কোড না চালিয়ে বলুন কী ছাপা হবে। তারপর যেকোনো Python-এ চালিয়ে মেলান।
    from itertools import permutations, combinations
    p = list(permutations("abc"))
    print(len(p), "".join(p[3]))
    print([k ^ (k >> 1) for k in range(8)])
    print(list(combinations(range(4), 2))[4])
    6 bca
    [0, 1, 3, 2, 6, 7, 5, 4]
    (1, 3)

    itertools lexicographic ক্রমে দেয় (ইনপুটের ক্রম অনুযায়ী): abc, acb, bac, bca, …, তাই ০ থেকে গুনে ৩ নম্বর bca. দ্বিতীয় লাইন G(3)-এর শব্দগুলো দশমিকে: 000, 001, 011, 010, 110, 111, 101, 100. শেষ লাইনে {0..3}-এর 2-combination: (0,1), (0,2), (0,3), (1,2), (1,3), …, ৪ নম্বর (1, 3).

  4. প্রমাণের ভুল ধরুন: একজন next permutation-কে “সরল” করলেন: “pivot খুঁজে suffix-এর সবচেয়ে বড় উপাদানের সঙ্গে অদলবদল করি, তারপর suffix উল্টাই। প্রমাণ: বড় উপাদান সামনে এলে permutation বড় হয়, তাই ফল a-এর চেয়ে বড়; আর suffix উল্টে ছোট করেছি, তাই এটিই পরেরটি। ∎” কোথায় ভুল?
    • ভুল ১: “ফল a-এর চেয়ে বড়” দেখানো যথেষ্ট নয়; দেখাতে হবে মাঝে আর কিছু নেই। i নম্বর ঘরে বসাতে হবে ai-এর চেয়ে বড় সবচেয়ে ছোট উপাদান, সবচেয়ে বড়টি নয়।
    • Counterexample: 1 3 2: pivot 1, suffix 3 2. “সরল” পদ্ধতি 1-কে 3-এর সঙ্গে বদলে 3 1 2, তারপর suffix উল্টে 3 2 1. কিন্তু আসল পরেরটি 2 1 3; মাঝে 2 1 3, 2 3 1, 3 1 2 বাদ পড়েছে।
    • ভুল ২: অদলবদলের পর suffix আর অবর্ধমান না-ও থাকতে পারে (1 4 3 2-এ 4-এর সঙ্গে বদলালে suffix 1 3 2), তাই “উল্টালেই সবচেয়ে ছোট” দাবিও ভাঙে; scratch-এ পদ্ধতিটি 1 4 3 2 থেকে 4 2 3 1 দেয়, যেখানে সঠিক উত্তর 2 1 3 4.

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

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

ক্রমLexicographic ক্রমে পরের permutation বের করার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. ডান থেকে সবচেয়ে লম্বা অবর্ধমান suffix খোঁজা
  2. Suffix-এর ঠিক বাঁয়ের উপাদানকে pivot ধরা
  3. Suffix-এ pivot-এর চেয়ে বড় সবচেয়ে ডানের উপাদান খোঁজা
  4. Pivot আর সেই উপাদান অদলবদল করা
  5. Suffix-টি উল্টে ছোট থেকে বড় করা

Lexicographic ক্রমে 1 3 5 4 2-এর ঠিক পরের permutation কোনটি?

  • 1 5 2 3 4
  • 1 4 2 3 5
  • 1 4 5 3 2
  • 2 1 3 4 5

3-বিট reflected Gray code G(3) = 000, 001, 011, 010, 110, 111, 101, 100. এর ৬ নম্বর শব্দ (০ থেকে গুনে) কোনটি, আর সূত্র k ⊕ (k ≫ 1) কী বলে?

  • 110
  • 111
  • 101
  • 100

৫টি উপাদানে Fisher–Yates চালালে (i = 4, 3, 2, 1-এর প্রতিটিতে 0..i থেকে একটি j), সমান সম্ভাব্য পছন্দের ক্রম মোট কয়টি?

  • 3125
  • 120
  • 24
  • 25

৩টি উপাদানে “প্রতিটি i-এর জন্য 0..2 থেকে যেকোনো j নিয়ে অদলবদল” shuffle-টি কেন ৬টি permutation সমান সম্ভাবনায় দিতে পারে না?

  • কারণ কিছু permutation কখনো আসেই না
  • কারণ ধাপের সংখ্যা কম
  • কারণ কম্পিউটারের random সংখ্যা নিখুঁত নয়
  • কারণ 27 সংখ্যাটি 6 দিয়ে বিভাজ্য নয়

{1, …, 6}-এর 3-combination-গুলো lexicographic ক্রমে সাজালে {2, 4, 6}-এর ঠিক পরে কোনটি?

  • {2, 5, 6}
  • {3, 4, 5}
  • {2, 4, 7}
  • {2, 5, 4}

Combinatorial number system-এ rank(S) = C(c1, 1) + C(c2, 2) + C(c3, 3), যেখানে c1 < c2 < c3. S = {0, 2, 5} হলে rank কত?

  • 11
  • 10
  • 6
  • 8

শব্দকোষ

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

মনে রাখুন

  1. Generator মানে bijection: তালিকা আর বস্তুর সেটের মধ্যে এক-এক মিল; দৈর্ঘ্য গোনার সূত্রের সমান, আর len(set)-ও।
  2. Subset: 0..2n − 1 গুনলেই সব; reflected Gray code G(n + 1) = 0·G(n), 1·G(n)R-এ পরপর এক বিট বদল (আবর্তনসহ), k-তম শব্দ k ⊕ (k ≫ 1).
  3. Next permutation: অবর্ধমান suffix, pivot, তার চেয়ে বড় সবচেয়ে ডানের উপাদানের সঙ্গে অদলবদল, suffix উল্টানো; গড় খরচ ধ্রুবক (∑ 1/j! → e − 1).
  4. Combination আর rank: সর্বোচ্চে না থাকা সবচেয়ে ডানের ঘর বাড়ান; Lehmer code ∑ dᵢ(n − i)!, আর colex rank ∑ C(cᵢ, i).
  5. Shuffle আর backtracking: Fisher–Yates-এ j ∈ 0..i, n!টি ক্রম ↔ n!টি permutation; ভুল সংস্করণে nⁿ, যা n! দিয়ে বিভাজ্য নয় (n ≥ 3). শর্ত থাকলে backtracking অচল শাখা ছাঁটে, কিন্তু n! বৃদ্ধি বড় n-এ সব তৈরি অসম্ভব করে।

পরের পর্বে: পর্ব ৩৭ “গণনার কৌশল — lattice path, bijection আর double counting”: grid-এ পথ গোনা, reflection principle আর Catalan সংখ্যা, আর এক সমস্যাকে আরেক সমস্যায় রূপান্তর করে গোনার শিল্প।

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

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