পাঠ ১৩ · ৫৬-এর মধ্যে · মডিউল ৩

রেগুলার ল্যাঙ্গুয়েজের জন্য পাম্পিং লেমা

Pumping lemma for regular languages
৯ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • পাম্পিং লেমার নিখুঁত ফরমাল স্টেটমেন্ট — তিনটি শর্তসহ
  • কেন এটি সত্য — পিজনহোল প্রিন্সিপলের সাথে DFA-এর স্টেট-পুনরাবৃত্তির সংযোগ
  • এই লেমা কী প্রমাণ করে না — "necessary but not sufficient" ফাঁদ বোঝা
  • Python-এ real find_pumping_split — DFA সিমুলেট করে পিজনহোল আর্গুমেন্ট কোড হিসেবে বাস্তবায়ন
  • $xy^0z$, $xy^1z$, $xy^2z$, $xy^3z$ — চারটিই DFA-তে সরাসরি টেস্ট করে পাম্পিং প্রপার্টি নিশ্চিতভাবে যাচাই

১ · পাম্পিং লেমা কী, এবং কী নয়

পাম্পিং লেমাPumping Lemmaপ্রতিটি রেগুলার ল্যাঙ্গুয়েজের একটি নিশ্চিত কাঠামোগত ধর্ম, যা DFA-র ফাইনাইট স্টেট-সংখ্যা থেকে সরাসরি অনুসৃত হয়। হলো এমন একটি ধর্ম যা প্রতিটি রেগুলার ল্যাঙ্গুয়েজ মেনে চলতেই হয় — কিন্তু এই ধর্ম মেনে চলা কোনো ল্যাঙ্গুয়েজকে রেগুলার হওয়ার গ্যারান্টি দেয় না (necessary but not sufficient)। এর বাস্তব ব্যবহারিক মূল্য উল্টো দিক থেকে — L14-এ দেখা যাবে, যদি কোনো ল্যাঙ্গুয়েজ এই ধর্ম মেনে না চলে, তাহলে কনট্রাপজিটিভ (contrapositive) যুক্তিতে সেই ল্যাঙ্গুয়েজ নিশ্চিতভাবে রেগুলার নয় — এটিই এই লেমার সবচেয়ে গুরুত্বপূর্ণ ব্যবহার।

২ · ফরমাল স্টেটমেন্ট

পাম্পিং লেমা (Regular Languages)

যদি $L$ একটি রেগুলার ল্যাঙ্গুয়েজ হয়, তাহলে একটি পূর্ণসংখ্যা $p \geq 1$ (পাম্পিং লেংথ) থাকে যাতে — দৈর্ঘ্য $|s| \geq p$ এমন প্রতিটি স্ট্রিং $s \in L$-কে $s = xyz$ আকারে ভাগ করা যায়, যেখানে:

  • (১) $|y| > 0$ — "পাম্প" করা অংশটি খালি নয়
  • (২) $|xy| \leq p$ — বিভাজনটি স্ট্রিং-এর প্রথম $p$ ক্যারেক্টারের মধ্যেই ঘটে
  • (৩) সব $i \geq 0$-এর জন্য, $xy^iz \in L$ — মাঝের অংশটি যতবার ইচ্ছা পুনরাবৃত্তি (i=0 সহ, অর্থাৎ পুরোপুরি বাদ দিয়েও) করলেও ফলাফল $L$-এর মধ্যেই থাকে

৩ · কেন এটি সত্য — পিজনহোল প্রিন্সিপল

ধরুন $L$ রেগুলার, তাই L06-এর সংজ্ঞা অনুযায়ী একটি DFA $M$ আছে যার ঠিক $p$টি স্টেট এবং $L(M) = L$। এখন দৈর্ঘ্য $\geq p$ এমন কোনো স্ট্রিং $s$ প্রসেস করার সময় DFA প্রথম $p$টি ট্রানজিশনে $p+1$টি স্টেট ভিজিট করে (শুরুর স্টেটসহ) — কিন্তু DFA-তে মোট স্টেটই আছে মাত্র $p$টি। পিজনহোল প্রিন্সিপলPigeonhole Principle$n+1$টি বস্তু $n$টি বাক্সে রাখলে অন্তত একটি বাক্সে কমপক্ষে দুটো বস্তু পড়তেই হবে — Discrete Mathematics কোর্সে ফরমালি প্রমাণিত একটি মৌলিক কম্বিনেটোরিক্স ফ্যাক্ট। অনুযায়ী — $p+1$টি ভিজিট, $p$টি সম্ভাব্য স্টেট — কোনো একটি স্টেট অন্তত দুইবার ভিজিট হতেই হবে।

সেই পুনরাবৃত্ত স্টেটের দুইবার ভিজিটের মাঝে যে সাবস্ট্রিং প্রসেস হয়েছে, সেটিই $y$। যেহেতু $y$ প্রসেস করার আগে ও পরে DFA একই স্টেটে থাকে, $y$-কে যতবার পুনরাবৃত্তি (বা সম্পূর্ণ বাদ) করা হোক না কেন, বাকি স্ট্রিং $z$ ঠিক একইভাবে প্রসেস হবে — চূড়ান্ত স্টেট অপরিবর্তিত থাকবে, ফলে accept/reject-এর সিদ্ধান্তও অপরিবর্তিত থাকবে। এই স্টেট-পুনরাবৃত্তিই পাম্পিং লেমার তিনটি শর্তের সরাসরি উৎস।

DFA-এর $p$টি স্টেট ($|s| \geq p$ প্রসেস করলে $p{+}1$ ভিজিট) পিজনহোল প্রিন্সিপল কোনো স্টেট ২বার ভিজিট হবেই $s = xyz$ split $y$ = দুই ভিজিটের মাঝের অংশ $xy^iz \in L$, সব $i \geq 0$-এর জন্য
DFA-এর ফাইনাইট স্টেট-সংখ্যাই পাম্পিং লেমার একমাত্র উৎস — একটি স্টেট পুনরাবৃত্তি মানেই সেই লুপ "পাম্প" করা যায়।
সাধারণ ভুল — পাম্পিং লেমা প্রমাণ করে না যে কোনো ল্যাঙ্গুয়েজ রেগুলার। এটি শুধু বলে — রেগুলার হলে এই ধর্ম থাকতেই হবে (necessary)। বহু নন-রেগুলার ল্যাঙ্গুয়েজও কাকতালীয়ভাবে এই ধর্ম মেনে চলতে পারে (sufficient নয়) — তাই পাম্পিং লেমা দিয়ে সরাসরি "এই ল্যাঙ্গুয়েজ রেগুলার" প্রমাণ করার চেষ্টা করা একটি সাধারণ যুক্তি-ভুল। এটি শুধু কনট্রাপজিটিভ দিকে (ধর্ম না মানলে নন-রেগুলার, L14) নির্ভরযোগ্য।

৪ · কোডে পিজনহোল আর্গুমেন্ট — real DFA সিমুলেশন

নিচের কোড সেলে find_pumping_split(dfa, string) ফাংশনটি সত্যিকারের DFA সিমুলেশন চালিয়ে প্রথম $p$টি ট্রানজিশনের মধ্যে ভিজিট করা স্টেট-সিকোয়েন্স ট্র্যাক করে, এবং প্রথম পুনরাবৃত্ত স্টেট খুঁজে বের করে ($x$, $y$, $z$ split)। এটি L06-এর "even number of 1s" DFA-তে চালানো হয়েছে, এবং তারপর $xy^0z, xy^1z, xy^2z, xy^3z$ — চারটি স্ট্রিংই সরাসরি DFA-তে টেস্ট করে দেখানো হয়েছে সবগুলোর accept/reject ফলাফল মূল স্ট্রিং-এর সাথে অভিন্ন — পাম্পিং প্রপার্টির একটি বাস্তব, রান-টাইম-এ যাচাইকৃত নিশ্চিতকরণ।

Python
class DFA:
    def __init__(self, states, alphabet, transitions, start, accept_states):
        self.states = list(states)
        self.alphabet = set(alphabet)
        self.transitions = transitions
        self.start = start
        self.accept_states = set(accept_states)
        for q in self.states:
            for a in self.alphabet:
                assert (q, a) in self.transitions

    def run(self, s):
        state = self.start
        for ch in s:
            state = self.transitions[(state, ch)]
        return state

    def accepts(self, s):
        return self.run(s) in self.accept_states


# L06-এর "even number of 1s" DFA পুনর্ব্যবহার
even_ones = DFA(
    states=["q0", "q1"], alphabet={"0", "1"},
    transitions={("q0","0"):"q0", ("q0","1"):"q1", ("q1","0"):"q1", ("q1","1"):"q0"},
    start="q0", accept_states={"q0"},
)


def find_pumping_split(dfa, string):
    p = len(dfa.states)
    assert len(string) >= p, "পাম্পিং লেমার জন্য |s| >= p দরকার"
    # state_seq[i] = string[:i] প্রসেস করার পর DFA-র স্টেট
    state_seq = [dfa.start]
    state = dfa.start
    for ch in string[:p]:          # প্রথম p ট্রানজিশন -- p+1টি ভিজিট -- পিজনহোল নিশ্চিত করে একটি পুনরাবৃত্তি
        state = dfa.transitions[(state, ch)]
        state_seq.append(state)

    seen = {}
    first_repeat = None
    for i, st in enumerate(state_seq):
        if st in seen:
            first_repeat = (seen[st], i)   # (প্রথমবার ভিজিট, দ্বিতীয়বার ভিজিট)
            break
        seen[st] = i
    assert first_repeat is not None, "পিজনহোল প্রিন্সিপল অনুযায়ী একটি পুনরাবৃত্তি থাকতেই হবে"

    j, k = first_repeat
    x, y, z = string[:j], string[j:k], string[k:]
    assert len(y) > 0 and len(x) + len(y) <= p and x + y + z == string
    return x, y, z, state_seq


def verify_pumping(dfa, x, y, z, original_string):
    original_result = dfa.accepts(original_string)
    results = {}
    for i in range(4):
        pumped = x + y * i + z
        results[i] = (pumped, dfa.accepts(pumped))
    all_consistent = all(res == original_result for (_, res) in results.values())
    return results, all_consistent


test_string = "1101011"   # |s| = 7 >= p = 2
p = len(even_ones.states)
x, y, z, seq = find_pumping_split(even_ones, test_string)

print(f"DFA states = {even_ones.states}, পাম্পিং লেংথ p = {p}")
print(f"s = {test_string!r} (|s| = {len(test_string)})")
print(f"প্রথম p ট্রানজিশনে ভিজিট করা স্টেট-সিকোয়েন্স: {seq}")
print(f"পিজনহোল-এ পাওয়া split: x={x!r}, y={y!r}, z={z!r}")
print(f"শর্ত (১) |y|>0: {len(y)>0}   শর্ত (২) |xy|<=p: {len(x)+len(y)} <= {p} -> {len(x)+len(y)<=p}")
print(f"মূল স্ট্রিং-এর ফলাফল: accepted = {even_ones.accepts(test_string)}")
print()

results, consistent = verify_pumping(even_ones, x, y, z, test_string)
for i, (pumped, acc) in results.items():
    print(f"  i={i}: xy^{i}z = {pumped!r:15s} -> accepted = {acc}")
print()
print("শর্ত (৩) সব xy^iz-ই মূল ফলাফলের সাথে সঙ্গতিপূর্ণ (পাম্পিং প্রপার্টি সত্য):", consistent)

    
লক্ষ্য করুন find_pumping_split কোনো ভাষা-নির্দিষ্ট জ্ঞান ব্যবহার করছে না — এটি শুধু DFA-র স্টেট-সিকোয়েন্স ট্র্যাক করছে এবং পিজনহোল প্রিন্সিপল প্রয়োগ করছে। তাই এই একই ফাংশন যেকোনো DFA আর দৈর্ঘ্য $\geq p$ যেকোনো স্ট্রিং-এর জন্য কাজ করবে — ঠিক যেমন পাম্পিং লেমার প্রমাণ কোনো নির্দিষ্ট ল্যাঙ্গুয়েজের উপর নির্ভর করে না, শুধু "DFA-র ফাইনাইট স্টেট আছে" এই ফ্যাক্টের উপর নির্ভর করে।
মূল কথা · Key takeaway

পাম্পিং লেমা DFA-র ফাইনাইট স্টেট-সংখ্যা থেকে পিজনহোল প্রিন্সিপল দিয়ে সরাসরি অনুসৃত একটি প্রয়োজনীয় (কিন্তু পর্যাপ্ত নয়) ধর্ম। এর আসল শক্তি কনট্রাপজিটিভ দিকে — L14-এ দেখা যাবে, এই ধর্ম ভাঙলে ল্যাঙ্গুয়েজ নিশ্চিতভাবে নন-রেগুলার, এবং এটিই এই থিওরির সবচেয়ে ব্যবহৃত প্রমাণ-কৌশলগুলোর একটি।

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

প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।

প্র ০১ পাম্পিং লেংথ $p$ ঠিক কী প্রতিনিধিত্ব করে — এটি কি সবসময় নির্দিষ্ট, একক কোনো সংখ্যা?

$p$ যেকোনো DFA-র স্টেট-সংখ্যা (বা তার চেয়ে বড়) হতে পারে যা $L$ recognize করে — এটি অনন্য (unique) নয়, শুধু অস্তিত্বশীল (∃)। সাধারণত সবচেয়ে ছোট বৈধ পাম্পিং লেংথ হলো $L$-এর মিনিমাল DFA-র (L16) স্টেট-সংখ্যা — কিন্তু লেমার স্টেটমেন্ট নিজেই বলে না যে $p$ সবচেয়ে ছোট হতে হবে, শুধু বলে কোনো একটি $p$ থাকে যাতে শর্তগুলো সব $|s|\geq p$ স্ট্রিং-এর জন্য সত্য হয়। বড় কোনো $p'$ ($p' > p$) নিলেও শর্তগুলো এখনও সত্যই থাকবে, কারণ $|s| \geq p'$ মানে স্বয়ংক্রিয়ভাবে $|s|\geq p$-ও সত্য।

প্র ০২ শর্ত (২) $|xy| \leq p$ কেন লেমাতে আলাদা করে বলা দরকার — এটি কি স্বয়ংক্রিয়ভাবেই সত্য হয় না?

না, এটি প্রমাণের নির্মাণ-পদ্ধতি থেকেই আসে এবং আলাদাভাবে গুরুত্বপূর্ণ — কারণ এটিই নিশ্চিত করে যে পুনরাবৃত্ত স্টেটটি স্ট্রিং-এর প্রথম অংশেই পাওয়া গেছে (পুরো স্ট্রিং জুড়ে খোঁজার দরকার নেই, শুধু প্রথম $p$ ক্যারেক্টারের মধ্যেই পিজনহোল দিয়ে গ্যারান্টিড)। এই শর্তটি ছাড়া $y$ স্ট্রিং-এর যেকোনো জায়গায় হতে পারত, এবং L14-এর মতো প্রমাণে এই "$y$ শুধু প্রথম $p$ ক্যারেক্টারের মধ্যে থাকবে" শর্তটিই প্রায়ই সবচেয়ে গুরুত্বপূর্ণ যুক্তির ভিত্তি হয় (উদাহরণ: $0^p1^p$-এ প্রথম $p$ ক্যারেক্টার সব 0, তাই $y$-ও নিশ্চিতভাবে শুধু 0 দিয়ে গঠিত)।

প্র ০৩ উপরের কোডে $i=0$ (অর্থাৎ $y$ সম্পূর্ণ বাদ দেওয়া) কেন বিশেষভাবে গুরুত্বপূর্ণ?

কারণ $i=0$ ("পাম্প ডাউন") হলো সেই কেসটি যেখানে স্ট্রিং-এর একটি অংশ সম্পূর্ণ সরিয়ে ফেলা হয় — এটি প্রায়ই সবচেয়ে সহজে একটি "স্ট্রাকচারাল কনস্ট্রেইন্ট" ভাঙে (যেমন $0^n1^n$-এ 0-এর সংখ্যা কমিয়ে দিলে সমতা ভেঙে যায়, L14 দেখুন)। $i=2,3$ ("পাম্প আপ") সাধারণত ভিন্ন ধরনের সমস্যা তৈরি করে। যেহেতু লেমার শর্ত (৩) বলে সব $i\geq 0$-এর জন্য সত্য হতে হবে, একটি মাত্র $i$-এ ব্যর্থ হলেই পুরো শর্তটি ভেঙে যায় — তাই নন-রেগুলারিটি প্রমাণে প্রায়ই শুধু $i=0$ পরীক্ষা করলেই যথেষ্ট হয়, যদিও কোডে আমরা $i=0,1,2,3$ — চারটিই টেস্ট করে অতিরিক্ত নিশ্চয়তা নিয়েছি।

অনুশীলন

  1. চিন্তা করুন: যদি একটি DFA-র ৫টি স্টেট থাকে এবং একটি স্ট্রিং-এর দৈর্ঘ্য ঠিক ৫ হয়, তাহলে পিজনহোল প্রিন্সিপল কি নিশ্চিতভাবে একটি পুনরাবৃত্ত স্টেট গ্যারান্টি দেয়? দৈর্ঘ্য ৪ হলে কী হবে?

    দৈর্ঘ্য ৫ (অর্থাৎ $|s|=p=5$): হ্যাঁ, গ্যারান্টিড। ৫টি ট্রানজিশন মানে ৬টি স্টেট ভিজিট (শুরুরটাসহ), কিন্তু মাত্র ৫টি সম্ভাব্য স্টেট — পিজনহোল অনুযায়ী একটি পুনরাবৃত্তি অবশ্যই ঘটবে। দৈর্ঘ্য ৪ ($|s|=4 < p=5$): না, গ্যারান্টিড নয় — মাত্র ৫টি স্টেট ভিজিট (৪ ট্রানজিশন + শুরুরটা), যা ঠিক স্টেট-সংখ্যার সমান, তাই তত্ত্বগতভাবে সবগুলোই ভিন্ন স্টেট হতে পারে, কোনো পুনরাবৃত্তি ছাড়াই। এই কারণেই লেমার শর্তে স্পষ্টভাবে $|s| \geq p$ (শুধু $>$ নয়, $\geq$-ও) বলা আছে।

  2. পরীক্ষা করুন: উপরের কোড সেলে test_string-এর বদলে একটি ভিন্ন, লম্বা বাইনারি স্ট্রিং (যেমন "0010110") ব্যবহার করে Run চাপুন — নতুন split এবং নতুন $xy^iz$ ফলাফলগুলো কি এখনও সব মূল স্ট্রিং-এর accept/reject অবস্থার সাথে সঙ্গতিপূর্ণ থাকে?

    হ্যাঁ, থাকা উচিত — এবং সবসময় থাকবে, যেকোনো দৈর্ঘ্য $\geq p$ স্ট্রিং-এর জন্যই, কারণ প্রমাণের যুক্তি (স্টেট-পুনরাবৃত্তি) কোনো নির্দিষ্ট স্ট্রিং-এর বৈশিষ্ট্যের উপর নির্ভর করে না। "0010110"-এ (দৈর্ঘ্য ৭, ১-এর সংখ্যা ৩টি, বিজোড়) DFA q0→q0→q0→q1→q1→q1→q0→q1 পথে চলে — প্রথম ২ ট্রানজিশনেই q0 দুইবার ভিজিট হয় (x='', y='00', z='10110'), এবং $xy^iz$-এর প্রতিটিই ১-এর সংখ্যা অপরিবর্তিত রেখে শুধু 0-এর সংখ্যা বদলায় — তাই সব $i$-এর জন্যই reject-ই থেকে যায়, মূল স্ট্রিং-এর মতোই।

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

আগের পাঠ
L12 · রেগুলার ল্যাঙ্গুয়েজের ক্লোজার প্রপার্টি