পাঠ ২১ · ৫৬-এর মধ্যে · মডিউল ৪
Home / Courses / Formal Language & Automata Theory / Theory of Computation / কনটেক্সট-ফ্রি গ্রামার

গ্রেইবাখ নর্মাল ফর্ম

Greibach Normal Form
৯ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

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

  • GNF-এর ফরমাল সংজ্ঞা এবং CNF থেকে এর কাঠামোগত পার্থক্য
  • কেন GNF-এ derivation length স্ট্রিং length-এর সমান — এবং এর ব্যবহারিক গুরুত্ব
  • CNF থেকে GNF রূপান্তরের মূল কৌশল — left recursion elimination-এর রূপরেখা
  • একটি concrete গ্রামারে হাতে-করা রূপান্তর, Python দিয়ে is_gnf ও derivation-length যাচাই

১ · GNF-এর ফরমাল সংজ্ঞা

L20-এর CNF-এর বিপরীতে, গ্রেইবাখ নর্মাল ফর্মGreibach Normal Form (GNF)একটি স্ট্যান্ডার্ডাইজড গ্রামার ফর্ম যেখানে প্রতিটি রুল ঠিক একটি টার্মিনাল দিয়ে শুরু হয়, তারপর শূন্য বা তার বেশি ভেরিয়েবল। একটি ভিন্ন স্ট্যান্ডার্ডাইজড রূপ — প্রতিটি রুল এই শেপে —

$$A \to a\alpha \qquad (a \in \Sigma,\ \alpha \in V^*)$$

অর্থাৎ প্রতিটি রুল ঠিক একটি টার্মিনাল দিয়ে শুরু হয়, তারপর শুধুমাত্র ভেরিয়েবল (শূন্যটিও হতে পারে) — CNF-এর মতোই, GNF-এও একটি ঐচ্ছিক $S \to \varepsilon$ ব্যতিক্রম আছে যদি ε ভাষার সদস্য হয়।

থিওরেম (L20-এর সমান্তরাল): প্রতিটি কনটেক্সট-ফ্রি ভাষার একটি সমতুল্য GNF গ্রামার আছে।

২ · GNF-এর genuine payoff — derivation length = string length

যেহেতু প্রতিটি রুল ঠিক একটি টার্মিনাল দিয়ে শুরু হয়, প্রতিটি ডেরিভেশন ধাপ (leftmost ভেরিয়েবল এক্সপ্যান্ড করা) ঠিক একটি নতুন টার্মিনাল ইনপুটে যোগ করে — কখনো শূন্য (unit/ε-প্রোডাকশনের মতো), কখনো একাধিক নয়। ফলাফল: length $n$-এর একটি স্ট্রিং derive করতে ঠিক $n$টি derivation ধাপ লাগে, প্রতিবার, নিশ্চিতভাবে — CNF-এ এই নিশ্চয়তা নেই ($A \to BC$ রুলে কোনো টার্মিনাল consume হয় না)।

এই বৈশিষ্ট্যটি টপ-ডাউন পার্সিং-কে left-recursion সমস্যা ছাড়াই সোজাসুজি করে তোলে — সরাসরি সম্পর্কিত Programming Languages & Compiler Design কোর্সের M5-এর left-recursion-elimination আলোচনার সাথে (একটি ভিন্ন, কিন্তু সম্পর্কিত কৌশল একই অন্তর্নিহিত সমস্যা সমাধান করছে)।

৩ · CNF থেকে GNF — রূপান্তরের রূপরেখা

সাধারণ GNF রূপান্তর অ্যালগরিদম genuinely জটিল — এটি সাধারণত CNF (L20) থেকে শুরু করে, ভেরিয়েবলদের একটি ক্রম নির্ধারণ করে, এবং সেই ক্রম অনুযায়ী left recursion দূর করে ও পরোক্ষ রেফারেন্স ধারাবাহিকভাবে "আনফোল্ড" করে (আগের ভেরিয়েবলের রুল দিয়ে প্রতিস্থাপন করে) যতক্ষণ না প্রতিটি রুল টার্মিনাল দিয়ে শুরু হয়। এখানে সম্পূর্ণ সাধারণ অ্যালগরিদম বাস্তবায়ন আবশ্যক নয় — বরং একটি নির্দিষ্ট, ছোট গ্রামারে এই কৌশলটি হাতে-কলমে প্রয়োগ করে দেখানো হবে, এবং কোড দিয়ে ফলাফল যাচাই করা হবে।

CNF গ্রামার A → BC | a leading ভেরিয়েবল প্রতিস্থাপন left recursion দূর, আনফোল্ড GNF গ্রামার A → aα
CNF-এর প্রতিটি রুলের leading ভেরিয়েবলকে তার নিজস্ব (ইতিমধ্যে-GNF) রুল দিয়ে প্রতিস্থাপন করে, ক্রমান্বয়ে প্রতিটি রুল টার্মিনাল-দিয়ে-শুরু করানো হয়।

৪ · Worked example — $a^nb^n$ ($n \geq 1$)-এর CNF থেকে GNF

একটি ছোট CNF গ্রামার দিয়ে শুরু করা যাক, $L = \{a^nb^n : n \geq 1\}$-এর জন্য —

$$S \to AB \mid AC \qquad C \to SB \qquad A \to a \qquad B \to b$$

এখানে $A \to a$ ও $B \to b$ ইতিমধ্যেই GNF-শেপে (টার্মিনাল, তারপর শূন্য ভেরিয়েবল)। $S$-এর রুলে leading সিম্বল $A$ — সেটিকে তার নিজের রুল ($A \to a$) দিয়ে প্রতিস্থাপন করলে $S \to aB \mid aC$ পাওয়া যায়, যা এখন GNF-শেপে। $C \to SB$-তে leading সিম্বল $S$ — সেটিকে (ইতিমধ্যে-GNF) $S$-এর রুল দিয়ে প্রতিস্থাপন করলে $C \to aBB \mid aCB$ পাওয়া যায়, যা-ও GNF-শেপে। $A$ এখন আর কোথাও রেফারেন্স হয় না, তাই L19-এর reachability অনুযায়ী বাদ দেওয়া যায় —

$$S \to aB \mid aC \qquad B \to b \qquad C \to aBB \mid aCB$$

Python
# হাতে-করা GNF রূপান্তর -- is_gnf checker ও derivation-length যাচাই
from collections import deque

class Grammar:
    def __init__(self, variables, terminals, rules, start):
        self.V = set(variables)
        self.T = set(terminals)
        self.rules = rules
        self.S = start

def is_gnf(grammar):
    """প্রতিটি রুল A -> a-alpha শেপে কি না -- ঠিক একটি টার্মিনাল দিয়ে শুরু, তারপর শুধু ভেরিয়েবল"""
    for var, rhss in grammar.rules.items():
        for rhs in rhss:
            if var == grammar.S and rhs == ():
                continue
            if len(rhs) == 0 or rhs[0] not in grammar.T:
                return False, (var, rhs)
            if any(sym not in grammar.V for sym in rhs[1:]):
                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

def derive_leftmost(grammar, start, target, max_steps=60):
    queue = deque([((start,), [(start,)])])
    while queue:
        form, history = queue.popleft()
        if len(history) - 1 > max_steps:
            continue
        idx = next((i for i, sym in enumerate(form) if sym in grammar.V), None)
        if idx is None:
            if ''.join(form) == target:
                return history
            continue
        prefix = ''.join(form[:idx])
        if not target.startswith(prefix):
            continue
        var = form[idx]
        for rhs in grammar.rules.get(var, []):
            new_form = form[:idx] + rhs + form[idx + 1:]
            terms = []
            for s in new_form:
                if s in grammar.V:
                    break
                terms.append(s)
            if not target.startswith(''.join(terms)):
                continue
            if len(new_form) > len(target) + 6:
                continue
            queue.append((new_form, history + [new_form]))
    return None

def derivation_length_matches_string_length(grammar, start, string):
    """একটি স্ট্রিং derive করতে ঠিক len(string) ধাপ লাগে কি না -- GNF-এর দাবিকৃত বৈশিষ্ট্য"""
    hist = derive_leftmost(grammar, start, string, max_steps=len(string) + 2)
    if hist is None:
        return False, None
    steps = len(hist) - 1
    return steps == len(string), steps

# ---- সোর্স CNF গ্রামার, a^n b^n (n>=1) ----
rules_cnf = {
    'S': [('A', 'B'), ('A', 'C')],
    'A': [('a',)],
    'B': [('b',)],
    'C': [('S', 'B')],
}
G_cnf = Grammar({'S', 'A', 'B', 'C'}, {'a', 'b'}, rules_cnf, 'S')

# ---- হাতে-করা GNF (A প্রতিস্থাপিত, তাই আর রেফারেন্স হয় না -- L19 অনুযায়ী বাদ) ----
rules_gnf = {
    'S': [('a', 'B'), ('a', 'C')],
    'B': [('b',)],
    'C': [('a', 'B', 'B'), ('a', 'C', 'B')],
}
G_gnf = Grammar({'S', 'B', 'C'}, {'a', 'b'}, rules_gnf, 'S')

ok, bad = is_gnf(G_gnf)
print("is_gnf(G_gnf):", ok, bad)

lang_cnf = generate_language_up_to_length(G_cnf, 'S', 8)
lang_gnf = generate_language_up_to_length(G_gnf, 'S', 8)
print("L(CNF) length<=8:", sorted(lang_cnf, key=lambda w: (len(w), w)))
print("L(GNF) length<=8:", sorted(lang_gnf, key=lambda w: (len(w), w)))
print("ভাষা অভিন্ন:", lang_cnf == lang_gnf)

print()
for s in ["ab", "aabb", "aaabbb"]:
    matches, steps = derivation_length_matches_string_length(G_gnf, 'S', s)
    print(f"স্ট্রিং={s!r}  length={len(s)}  derivation ধাপ={steps}  মেলে={matches}")

    
লক্ষ্য করুন "aabb" (length ৪)-এর derivation ঠিক ৪ ধাপে হয় — $S \Rightarrow aC \Rightarrow aaBB \Rightarrow aabB \Rightarrow aabb$ — প্রতিটি ধাপ ঠিক একটি নতুন টার্মিনাল যোগ করছে। L20-এর CNF গ্রামারে একই স্ট্রিং-এর derivation-এ এই নিশ্চয়তা নেই ($A \to BC$-এর মতো রুল কোনো টার্মিনাল consume না করেই একটি ধাপ ব্যবহার করে)।
মূল কথা · Key takeaway

GNF-এ প্রতিটি রুল একটি টার্মিনাল দিয়ে শুরু হওয়ার গ্যারান্টি দেয় বলেই derivation length ও string length সবসময় সমান থাকে — এটি শুধু একটি কৌতূহলোদ্দীপক তথ্য নয়, বরং টপ-ডাউন পার্সিং-এ left-recursion সমস্যা এড়ানোর একটি গাণিতিক ভিত্তি। L20-এর CNF ও L21-এর GNF — একই কনটেক্সট-ফ্রি ভাষার দুটো ভিন্ন, প্রতিটি নিজ নিজ অ্যালগরিদমিক payoff-এর জন্য অপ্টিমাইজড, স্ট্যান্ডার্ডাইজড রূপ।

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

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

প্র ০১ GNF-এর সংজ্ঞায় $\alpha \in V^*$ (শুধু ভেরিয়েবল) কেন — কেন RHS-এর মাঝখানে বা শেষে আরেকটি টার্মিনাল অনুমোদিত নয়?

কারণ GNF-এর পুরো পয়েন্টই হলো "প্রতিটি রুল প্রয়োগ = ঠিক একটি টার্মিনাল consume" এই নিশ্চয়তা বজায় রাখা। যদি RHS-এর মাঝখানে আরেকটি টার্মিনাল অনুমোদিত হতো (যেমন $A \to aBcD$), তাহলে সেই টার্মিনাল $c$ কোন derivation ধাপে "consume" হয়েছে তা অস্পষ্ট হয়ে যেত — leftmost derivation-এ $c$ ততক্ষণ পর্যন্ত অপেক্ষা করে যতক্ষণ না $B$ সম্পূর্ণভাবে টার্মিনাল স্ট্রিং-এ বিস্তৃত হয়, কিন্তু $c$ নিজে কোনো ভেরিয়েবল এক্সপ্যানশনের ফলাফল নয় — এই অস্পষ্টতা derivation-length = string-length নিশ্চয়তা ভেঙে দিত।

প্র ০২ এই পাঠের worked example-এ $A$-কে কেন সরিয়ে ফেলা হলো — এটি কি L19-এর কোন নির্দিষ্ট ধারণার প্রয়োগ?

হ্যাঁ, ঠিক L19-এর reachability ধারণার প্রয়োগ। রূপান্তরের সময় $S$-এর রুলে $A$-কে তার নিজের রুল ($A \to a$) দিয়ে সরাসরি প্রতিস্থাপন করা হয়েছে ($S \to AB$ থেকে $S \to aB$) — এই প্রতিস্থাপনের পর চূড়ান্ত GNF গ্রামারে $A$-কে রেফারেন্স করার মতো আর কোনো রুল অবশিষ্ট থাকে না, তাই $A$ আর রিচেবল নয় (L19-এর সংজ্ঞা অনুযায়ী), এবং তাকে বাদ দেওয়া নিরাপদ — ভাষা অপরিবর্তিত থাকে, কোড সেলের lang_cnf == lang_gnf চেক এটিই নিশ্চিত করে।

প্র ০৩ যদি কোনো স্ট্রিং-এর জন্য একাধিক ভিন্ন leftmost derivation থাকত (L18-এর অ্যাম্বিগুইটি), তাহলে কি derivation length তবুও string length-এর সমান থাকত?

হ্যাঁ — GNF-এর derivation-length গ্যারান্টি প্রতিটি বৈধ leftmost derivation-এর জন্য পৃথকভাবে সত্য, এটি কোনো নির্দিষ্ট derivation-এর ওপর নির্ভর করে না। যেকোনো derivation, যেভাবেই সে ভেরিয়েবলগুলো এক্সপ্যান্ড করুক না কেন, GNF গ্রামারে প্রতিটি ধাপে ঠিক একটি টার্মিনাল consume করবে — তাই অ্যাম্বিগুয়াস স্ট্রিং-এর একাধিক ভিন্ন derivation থাকলেও, প্রতিটি derivation-এর length পৃথকভাবে ঠিক স্ট্রিং-এর length-এর সমান হবে। অ্যাম্বিগুইটি (L18) ও derivation-length (এই পাঠ) সম্পূর্ণ আলাদা, স্বাধীন বৈশিষ্ট্য।

অনুশীলন

  1. হাতে-কলমে করুন: $C \to aCB$ রুল ব্যবহার করে "aaabbb" ($n=3$)-এর একটি সম্পূর্ণ leftmost derivation হাতে লিখুন, প্রতি ধাপে কতগুলো টার্মিনাল এখন পর্যন্ত consume হয়েছে গণনা করুন।

    $S \Rightarrow aC \Rightarrow a(aCB) = aaCB \Rightarrow aa(aBB)B = aaaBBB \Rightarrow aaabBB \Rightarrow aaabbB \Rightarrow aaabbb$ — মোট ৬ ধাপ, স্ট্রিং-এর length ৬-এর সমান। প্রতি ধাপে ঠিক একটি নতুন টার্মিনাল (a বা b) যোগ হয়েছে, ঠিক যেমন কোড সেলের derivation_length_matches_string_length দাবি করে।

  2. পরীক্ষা করুন: কোড সেলের টেস্ট-লিস্টে "aaaabbbb" ($n=4$) যোগ করে Run চেপে দেখুন derivation length এখনো string length-এর সমান থাকে কি না।

    হ্যাঁ — matches=True, steps=8 (স্ট্রিং-এর length ৮-এর সমান)। এই প্যাটার্ন যেকোনো $n$-এর জন্য চলতে থাকে, যেহেতু GNF গ্রামারে প্রতিটি ডেরিভেশন ধাপ কাঠামোগতভাবেই ঠিক একটি টার্মিনাল consume করতে বাধ্য — এটি কোনো নির্দিষ্ট $n$-এর ওপর নির্ভরশীল কাকতালীয় ফলাফল নয়, বরং GNF-এর সংজ্ঞা থেকে সরাসরি অনুসৃত একটি গ্যারান্টি।

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

আগের পাঠ
L20 · চমস্কি নর্মাল ফর্ম