চমস্কি নর্মাল ফর্ম
এই পাঠে যা শিখবেন
- CNF-এর ফরমাল সংজ্ঞা এবং কেন এই rigid কাঠামো algorithmically দরকারি (CYK পার্সিং)
- রূপান্তরের পাঁচটি ধাপ — নতুন স্টার্ট, ε/unit-প্রোডাকশন অপসারণ, L19-এর ইউজলেস-সিম্বল অপসারণ, CNF-শেপে রূপান্তর
- Python দিয়ে টার্মিনাল-আইসোলেশন ও লম্বা-রুল-ভাঙার অ্যালগরিদম বাস্তবায়ন
- একটি
is_cnfchecker দিয়ে ফলাফল যাচাই, এবং মূল ও রূপান্তরিত গ্রামারের ভাষা অভিন্ন কি না তা কোড দিয়ে নিশ্চিত করা
১ · CNF-এর ফরমাল সংজ্ঞা
একটি গ্রামার চমস্কি নর্মাল ফর্মেChomsky Normal Form (CNF)একটি স্ট্যান্ডার্ডাইজড গ্রামার ফর্ম যেখানে প্রতিটি রুল হয় A→BC (দুই ভেরিয়েবল) অথবা A→a (এক টার্মিনাল)। আছে যদি প্রতিটি রুল ঠিক দুটো শেপের একটির মধ্যে পড়ে —
$$A \to BC \quad (B, C \in V) \qquad \text{অথবা} \qquad A \to a \quad (a \in \Sigma)$$
প্লাস, ঐচ্ছিকভাবে, $S \to \varepsilon$ — যদি ε সত্যিই ভাষার সদস্য হয় (একটি স্পেশাল-কেসড ব্যতিক্রম, যেহেতু বাকি সব রুল কোনো ε অনুমতি দেয় না)।
থিওরেম: প্রতিটি কনটেক্সট-ফ্রি ভাষার (ε-ব্যতিক্রম বাদে) একটি সমতুল্য CNF গ্রামার আছে। এটি genuinely দরকারি কারণ CNF-এর rigid, ইউনিফর্ম গঠন algorithmic প্রসেসিং সম্ভব করে — সবচেয়ে বিখ্যাত উদাহরণ হলো CYK পার্সিং অ্যালগরিদম (নাম-উল্লেখ, একটি স্ট্যান্ডার্ড dynamic-programming পার্সার যা ঠিক CNF-শেপের রুল দাবি করে — এখানে সম্পূর্ণ বাস্তবায়ন আবশ্যক নয়)।
২ · রূপান্তরের পাঁচ ধাপ
- নতুন স্টার্ট সিম্বল যোগ করুন ($S_0 \to S$) যাতে স্টার্ট সিম্বল কখনো কোনো রুলের ডানপাশে না আসে।
- ε-প্রোডাকশন বাদ দিন — nullable ভেরিয়েবলের প্রতিটি occurrence অপশনালি সরিয়ে সমতুল্য নতুন রুল তৈরি করুন (L19-সংশ্লিষ্ট, শুধু উল্লেখ)।
- Unit প্রোডাকশন বাদ দিন — $A \to B$ আকারের রুল সরিয়ে $B$-এর রুলগুলো সরাসরি $A$-এ কপি করুন (শুধু উল্লেখ)।
- ইউজলেস সিম্বল বাদ দিন — L19-এর ঠিক এই
remove_useless_symbolsঅ্যালগরিদম প্রয়োগ করুন। - বাকি রুল CNF-শেপে রূপান্তর করুন — টার্মিনাল আইসোলেট করুন (একটি রুলে অন্য সিম্বলের পাশে থাকা টার্মিনালকে নতুন ভেরিয়েবলে বদলান) এবং লম্বা রুল ($k>2$ সিম্বলের) ভেঙে বাইনারি চেইনে রূপান্তর করুন।
এই পাঠের কোড সেল ধাপ ৫-এ ফোকাস করে (যেহেতু ধাপ ২-৩ এই নির্দিষ্ট গ্রামারের জন্য concrete ভাবে হাতে-করা যায়, আর ধাপ ৪ সরাসরি L19-এর ফাংশন পুনর্ব্যবহার করে) — কিন্তু সম্পূর্ণ পাইপলাইন একসাথে চালিয়ে দেখানো হবে, যেন চূড়ান্ত ফলাফল একটি genuinely valid, ভাষা-সংরক্ষণকারী CNF গ্রামার হয়।
# ধাপ ৫: isolate_terminals ও break_long_rules -- এবং সম্পূর্ণ পাইপলাইন
from collections import deque
import itertools
class Grammar:
def __init__(self, variables, terminals, rules, start):
self.V = set(variables)
self.T = set(terminals)
self.rules = rules
self.S = start
def isolate_terminals(grammar, counter=None):
"""যেকোনো length>=2 রুলে টার্মিনালকে একটি ফ্রেশ ভেরিয়েবলে বদলায় (A->aB হয় A->TaB, Ta->a)"""
if counter is None:
counter = itertools.count(1)
new_rules = {v: list(rhss) for v, rhss in grammar.rules.items()}
new_vars = set(grammar.V)
term_vars = {}
for var in list(new_rules.keys()):
updated = []
for rhs in new_rules[var]:
if len(rhs) < 2:
updated.append(rhs)
continue
new_rhs = []
for sym in rhs:
if sym in grammar.T:
if sym not in term_vars:
tv = f"T{next(counter)}"
term_vars[sym] = tv
new_vars.add(tv)
new_rhs.append(term_vars[sym])
else:
new_rhs.append(sym)
updated.append(tuple(new_rhs))
new_rules[var] = updated
for sym, tv in term_vars.items():
new_rules[tv] = [(sym,)]
return Grammar(new_vars, grammar.T, new_rules, grammar.S)
def break_long_rules(grammar, counter=None):
"""A -> B1 B2 ... Bk (k>2) কে A->B1 Y1, Y1->B2 Y2, ..., Y(k-2)->B(k-1)Bk চেইনে ভাঙে"""
if counter is None:
counter = itertools.count(1)
new_rules = {}
new_vars = set(grammar.V)
for var, rhss in grammar.rules.items():
updated = []
for rhs in rhss:
if len(rhs) <= 2:
updated.append(rhs)
continue
symbols = list(rhs)
chain_start = f"Y{next(counter)}"
new_vars.add(chain_start)
updated.append((symbols[0], chain_start))
cur, i = chain_start, 1
while i < len(symbols) - 2:
nxt = f"Y{next(counter)}"
new_vars.add(nxt)
new_rules.setdefault(cur, []).append((symbols[i], nxt))
cur, i = nxt, i + 1
new_rules.setdefault(cur, []).append((symbols[i], symbols[i + 1]))
new_rules[var] = updated
return Grammar(new_vars, grammar.T, new_rules, grammar.S)
def is_cnf(grammar):
for var, rhss in grammar.rules.items():
for rhs in rhss:
if var == grammar.S and rhs == ():
continue
if len(rhs) == 1 and rhs[0] in grammar.T:
continue
if len(rhs) == 2 and rhs[0] in grammar.V and rhs[1] in grammar.V:
continue
return False, (var, rhs)
return True, None
def generate_language_up_to_length(grammar, start, max_length):
result = set()
queue = deque([(start,)])
seen = {(start,)}
while queue:
form = queue.popleft()
if sum(1 for s in form if s not in grammar.V) > max_length:
continue
idx = next((i for i, sym in enumerate(form) if sym in grammar.V), None)
if idx is None:
if len(form) <= max_length:
result.add(''.join(form))
continue
var = form[idx]
for rhs in grammar.rules.get(var, []):
new_form = form[:idx] + rhs + form[idx + 1:]
if sum(1 for s in new_form if s not in grammar.V) > max_length:
continue
if new_form in seen:
continue
seen.add(new_form)
queue.append(new_form)
return result
# ---- মূল: ব্যালেন্সড বন্ধনী গ্রামার, S -> (S)S | epsilon ----
G0 = Grammar({'S'}, {'(', ')'}, {'S': [('(', 'S', ')', 'S'), ()]}, 'S')
# ধাপ ১+২ (concrete, এই নির্দিষ্ট গ্রামারের জন্য): নতুন স্টার্ট S0, S-এর নিজস্ব
# ε-প্রোডাকশন বাদ দিয়ে (S)S-এর প্রতিটি S-occurrence অপশনালি সরানোর সব কম্বিনেশন যোগ
rules1 = {
'S0': [('S',), ()],
'S': [('(', 'S', ')', 'S'), ('(', ')', 'S'), ('(', 'S', ')'), ('(', ')')],
}
G1 = Grammar({'S0', 'S'}, {'(', ')'}, rules1, 'S0')
# ধাপ ৩: unit প্রোডাকশন S0->S বাদ -- S-এর রুল সরাসরি S0-তে কপি
rules2 = {
'S0': [('(', 'S', ')', 'S'), ('(', ')', 'S'), ('(', 'S', ')'), ('(', ')'), ()],
'S': [('(', 'S', ')', 'S'), ('(', ')', 'S'), ('(', 'S', ')'), ('(', ')')],
}
G2 = Grammar({'S0', 'S'}, {'(', ')'}, rules2, 'S0')
# ধাপ ৪ (L19): S0, S উভয়ই জেনারেটিং ও রিচেবল -- কিছু বাদ দেওয়ার প্রয়োজন নেই
# ধাপ ৫: টার্মিনাল আইসোলেট, তারপর লম্বা রুল ভাঙা
G3 = isolate_terminals(G2)
G4 = break_long_rules(G3)
ok, bad = is_cnf(G4)
print("is_cnf(চূড়ান্ত গ্রামার):", ok, bad)
lang_orig = generate_language_up_to_length(G0, 'S', 6)
lang_cnf = generate_language_up_to_length(G4, 'S0', 6)
print("L(মূল) length<=6:", sorted(lang_orig, key=lambda w: (len(w), w)))
print("L(CNF) length<=6:", sorted(lang_cnf, key=lambda w: (len(w), w)))
print("ভাষা অভিন্ন:", lang_orig == lang_cnf)
# ---- উদাহরণ ২: A -> aBcD (টার্মিনাল ও একাধিক ভেরিয়েবলের মিশ্রণ) ----
G5 = Grammar({'A', 'B', 'D'}, {'a', 'c'}, {'A': [('a', 'B', 'c', 'D')], 'B': [('a',)], 'D': [('c',)]}, 'A')
G5 = isolate_terminals(G5)
G5 = break_long_rules(G5)
ok5, bad5 = is_cnf(G5)
print("\nA->aBcD উদাহরণ, is_cnf:", ok5, bad5)
for v, rhss in G5.rules.items():
print(" ", v, "->", ' | '.join(''.join(r) if r else 'ε' for r in rhss))
isolate_terminals শুধু length ≥ ২ রুলের টার্মিনাল বদলায় ($A \to a$-এর মতো ইতিমধ্যে-বৈধ রুল
স্পর্শ করে না) — আর break_long_rules শুধু length > ২ রুল ভাঙে। এই দুই ধাপ একসাথে
যেকোনো কনটেক্সট-ফ্রি রুলকে (ε/unit-প্রোডাকশন-মুক্ত হওয়ার পরে) নিশ্চিতভাবে $A \to BC$ অথবা $A \to a$-তে
রূপান্তর করে — is_cnf checker এই দাবিটি প্রতিটি রুলে সরাসরি যাচাই করে।
CNF একটি গ্রামারকে দুটো ফিক্সড, predictable শেপে সংকুচিত করে — এই rigid গঠনই CYK-এর মতো dynamic-programming পার্সিং অ্যালগরিদমকে সম্ভব করে। কোড সেল প্রমাণ করেছে রূপান্তরটি কেবল "সঠিক আকৃতি"-ই তৈরি করে না — ভাষাও অক্ষত রাখে, যা L17-এর ব্যালেন্সড-বন্ধনী গ্রামারের CNF সংস্করণকে পরবর্তী পাঠগুলোতে (M5-এর PDA-CFG ইকুইভ্যালেন্স, M6-এর CYK) নিরাপদে পুনর্ব্যবহারযোগ্য করে তোলে।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১ ধাপ ৪ (ইউজলেস সিম্বল অপসারণ) কেন ধাপ ৫ (CNF-শেপে রূপান্তর)-এর আগে করা জরুরি?
কারণ ε ও unit-প্রোডাকশন বাদ দেওয়ার প্রক্রিয়া (ধাপ ২-৩) প্রায়ই নতুন রুল তৈরি করে, এবং কিছু পুরনো ভেরিয়েবল হয়তো আর কোনো কাজে লাগে না (L19-এর ঠিক যে সমস্যাটি দেখানো হয়েছে)। যদি ইউজলেস সিম্বল বাদ না দিয়ে সরাসরি CNF-রূপান্তর করা হয়, তাহলে চূড়ান্ত CNF গ্রামারে এমন ভেরিয়েবল/রুল থেকে যাবে যেগুলো কখনো কোনো টার্মিনাল স্ট্রিং-এ অবদান রাখে না — গ্রামার "টেকনিক্যালি CNF" কিন্তু অপ্রয়োজনীয়ভাবে বড়, এবং CYK-এর মতো অ্যালগরিদম অহেতুক অতিরিক্ত রুল প্রসেস করবে।
প্র ০২
isolate_terminals কেন length ১-এর রুল ($A \to a$) স্পর্শ করে না?
কারণ $A \to a$ ইতিমধ্যেই CNF-এর দ্বিতীয় বৈধ শেপ — একটি ভেরিয়েবল থেকে ঠিক একটি টার্মিনাল। যদি এই রুলেও টার্মিনাল আইসোলেট করা হতো, তাহলে অহেতুক একটি অতিরিক্ত পরোক্ষ ধাপ ($A \to T_a$, $T_a \to a$) তৈরি হতো — গ্রামারকে বড় করত কিন্তু কোনো উপকার আনত না, যেহেতু মূল রুলটি ইতিমধ্যেই বৈধ ছিল। শুধুমাত্র length ≥ ২ রুলে (যেখানে টার্মিনাল অন্য সিম্বলের সাথে "মিশে" আছে) এই আইসোলেশন সত্যিই প্রয়োজনীয়।
প্র ০৩
break_long_rules-এ ফ্রেশ ভেরিয়েবলের নাম ($Y_1, Y_2, \ldots$) কেন গুরুত্বপূর্ণ — এলোমেলো/পুনরাবৃত্ত নাম ব্যবহার করলে কী সমস্যা হতো?
যদি দুটো ভিন্ন লম্বা রুল ভাঙার সময় একই ফ্রেশ ভেরিয়েবলের নাম পুনরায় ব্যবহার করা হতো, তাহলে সেই
ভেরিয়েবলটি দুটো সম্পূর্ণ ভিন্ন "চেইন"-এর জন্য দুই ভিন্ন অর্থ বহন করত — একটি চেইনের রুল অন্য চেইনের সাথে
ভুলবশত মিশে যেত, এবং রূপান্তরিত গ্রামারটি মূল গ্রামারের চেয়ে বেশি স্ট্রিং জেনারেট করতে শুরু করত
(ভুল combinations সম্ভব হয়ে যেত) — ভাষা-সংরক্ষণ ভেঙে পড়ত। itertools.count দিয়ে প্রতিটি
ফ্রেশ ভেরিয়েবলকে globally ইউনিক রাখা এই সমস্যা এড়ায়।
অনুশীলন
-
হাতে-কলমে করুন: রুল $A \to aBcD$-কে হাতে-কলমে CNF-এ রূপান্তর করুন (টার্মিনাল আইসোলেট করে, তারপর লম্বা রুল ভেঙে), তারপর কোড সেলের আউটপুটের সাথে মিলিয়ে দেখুন।
প্রথমে টার্মিনাল আইসোলেট: $A \to T_aBT_cD$, $T_a \to a$, $T_c \to c$ (৪-সিম্বল রুল)। তারপর ভাঙা: $A \to T_aY_1$, $Y_1 \to BY_2$, $Y_2 \to T_cD$ — এখন প্রতিটি রুল $A \to BC$ অথবা $A \to a$ শেপে, কোড সেলের আউটপুটের কাঠামোগতভাবে অভিন্ন (ভেরিয়েবলের নাম হয়তো ভিন্ন, কিন্তু শেপ ও চেইনের দৈর্ঘ্য একই)।
-
পরীক্ষা করুন: কোড সেলে
generate_language_up_to_length(G0, 'S', 6)-এর6-কে8-এ বদলে Run চেপে দেখুন মূল ও CNF গ্রামারের ভাষা এখনো অভিন্ন থাকে কি না।হ্যাঁ, অভিন্ন থাকবে —
lang_orig == lang_cnfএখনোTrueপ্রিন্ট হবে, শুধু length ৮ পর্যন্ত আরও বেশি স্ট্রিং (কাটালান সংখ্যা অনুযায়ী বাড়তে থাকা ব্যালেন্সড-বন্ধনী গণনা) যোগ হবে। এটি নিশ্চিত করে রূপান্তরটি কোনো নির্দিষ্ট length-এ সীমাবদ্ধ কাকতালীয় মিল নয় — সত্যিকারের ভাষা-সংরক্ষণকারী রূপান্তর।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৬টি পাঠ পরবর্তী পাঠ — গ্রেইবাখ নর্মাল ফর্ম — একটি ভিন্ন স্ট্যান্ডার্ডাইজড রূপ শেখাবে, যেখানে প্রতিটি ডেরিভেশন ধাপ ঠিক একটি টার্মিনাল consume করে।
- পাঠ ১৯ · CFG সিমপ্লিফিকেশন পূর্ববর্তী পাঠ এই পাঠের ধাপ ৪ (ইউজলেস সিম্বল অপসারণ) সরাসরি সেই পাঠের অ্যালগরিদমের ওপর নির্ভর করে।
- Programming Languages & Compiler Design কোর্স সঙ্গী কোর্স সেই কোর্স ব্যবহারিক পার্সিং অ্যালগরিদম (recursive-descent, LL/LR) কভার করে — CYK এখানে যেভাবে CNF ব্যবহার করে, সেই একই ধরনের একটি ভিন্ন পার্সিং কৌশল।
- সব Courses দেখুন ABCL TECH C, C++, Python, Java, JavaScript, DSA, DBMS, Discrete Mathematics, System Design, Cybersecurity, Cloud Computing & DevOps, Computer Networks, Operating Systems, Computer Architecture, Programming Languages & Compiler Design, Software Engineering & Git ও Theory of Computation — সব এক জায়গায়।