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

কনটেক্সট-ফ্রি গ্রামার — ফরমাল ডেফিনিশন ও ডেরিভেশন

CFG — formal definition & derivations
৯ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

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

  • CFG-এর ফরমাল ৪-টাপল সংজ্ঞা — V, Σ, R, S এবং প্রতিটির নির্দিষ্ট অর্থ
  • ডেরিভেশন রিলেশন $\Rightarrow$ ও তার reflexive-transitive closure $\Rightarrow^*$-এর নির্ভুল সংজ্ঞা
  • গ্রামারের ভাষা $L(G)$ এবং "কনটেক্সট-ফ্রি ভাষা"-র সংজ্ঞা — DFA-এর সমান্তরাল কাঠামো
  • Python দিয়ে একটি সত্যিকারের leftmost-derivation-search ও exhaustive language-generation অ্যালগরিদম

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

Programming Languages & Compiler Design কোর্সের M3/L13 ইতিমধ্যে CFG ও ডেরিভেশন ব্যবহারিক পার্সিং-গ্রাউন্ডওয়ার্কের কোণ থেকে কভার করেছে। এই পাঠ সেই একই ধারণাগুলো ফরমালি পুনরায় বলছে — M4-M6-এর তাত্ত্বিক ফলাফল (নর্মাল ফর্ম, পাম্পিং লেমা, PDA ইকুইভ্যালেন্স)-এর ভিত্তি হিসেবে।

একটি কনটেক্সট-ফ্রি গ্রামারContext-Free Grammar (CFG)ফরমালি একটি ৪-টাপল $G=(V,\Sigma,R,S)$ যা টার্মিনাল স্ট্রিং তৈরির নিয়ম বর্ণনা করে। ফরমালি একটি ৪-টাপল —

$$G = (V, \Sigma, R, S)$$

V
একটি ফাইনাইট সেট — ভেরিয়েবলনন-টার্মিনাল সিম্বল, প্রতিটি একটি "বিমূর্ত ক্যাটেগরি" প্রতিনিধিত্ব করে (নন-টার্মিনাল)।
Σ
টার্মিনাল অ্যালফাবেট — চূড়ান্ত স্ট্রিং-এ যে প্রকৃত সিম্বল থাকবে, V থেকে ডিসজয়েন্ট।
R
একটি ফাইনাইট সেট প্রোডাকশন রুলের, প্রতিটি $A \to \alpha$ আকারে, যেখানে $A \in V$ এবং $\alpha \in (V \cup \Sigma)^*$।
S
স্টার্ট ভেরিয়েবল, $S \in V$ — প্রতিটি ডেরিভেশন এখান থেকেই শুরু হয়।

লক্ষ্য করুন $\alpha \in (V \cup \Sigma)^*$ — একটি রুলের ডানপাশে ভেরিয়েবল ও টার্মিনাল যেকোনো মিশ্রণে, যেকোনো ক্রমে, যেকোনো সংখ্যক বার (এমনকি শূন্যবার — ε-প্রোডাকশন) থাকতে পারে। এই নমনীয়তাই CFG-কে রেগুলার ভাষার (L06-L11) চেয়ে বেশি শক্তিশালী করে তোলে — একটি DFA-এর ট্রানজিশন ফাংশন কখনো "মনে রাখতে" পারে না কতগুলো ওপেনিং প্যারেন এখনো বন্ধ হয়নি, কিন্তু একটি রিকার্সিভ CFG রুল সেটা স্বাভাবিকভাবেই করতে পারে (নিচের উদাহরণে দেখুন)।

২ · ডেরিভেশন — এক ধাপ ও বহু ধাপ

L03-এর স্ট্রাকচারাল ইনডাকশন ফ্রেমিং সরাসরি পুনরায় ব্যবহার করে, ডেরিভেশন সংজ্ঞায়িত হয় একটি এক-ধাপ ডেরিভেশন রিলেশন$u \Rightarrow v$ — u থেকে v-তে ঠিক একটি রুল প্রয়োগ করে পৌঁছানো যায় দিয়ে —

$$u \Rightarrow v \quad \text{যদি} \quad u = xAy,\ v = x\alpha y,\ \text{এবং}\ A \to \alpha \in R$$

অর্থাৎ $u$-এর কোথাও একটি ভেরিয়েবল $A$ আছে, এবং $R$-এ $A$-এর কোনো একটি রুল প্রয়োগ করে তাকে $\alpha$ দিয়ে প্রতিস্থাপন করলে $v$ পাওয়া যায় — বাকি সব ($x$ ও $y$) অপরিবর্তিত থাকে। এরপর $\Rightarrow^*$ ("u derives v") হলো এই রিলেশনের reflexive-transitive closure — শূন্য বা তার বেশি একক ধাপ পরপর প্রয়োগ করা (রিফ্লেক্সিভ অংশ মানে $u \Rightarrow^* u$ সবসময় সত্য, শূন্য ধাপেই)।

৩ · গ্রামারের ভাষা $L(G)$

একটি গ্রামার $G$-এর ভাষা হলো স্টার্ট সিম্বল থেকে ডেরাইভ-করা-যায় এমন সব টার্মিনাল স্ট্রিং —

$$L(G) = \{w \in \Sigma^* : S \Rightarrow^* w\}$$

একটি ভাষা কনটেক্সট-ফ্রি যদি কোনো CFG সেই ভাষা জেনারেট করে — L06-এর "একটি ভাষা রেগুলার যদি কোনো DFA সেটি অ্যাকসেপ্ট করে"-এর সাথে সম্পূর্ণ সমান্তরাল একটি সংজ্ঞা, শুধু চমস্কি হায়ারার্কির (L04) এক ধাপ ওপরে — DFA-এর বদলে গ্রামার, "অ্যাকসেপ্ট" করার বদলে "জেনারেট" করা।

G = (V, Σ, R, S) ফরমাল ৪-টাপল S ⇒ ⋯ ⇒ w বারবার এক-ধাপ ডেরিভেশন L(G) = {w : S ⇒* w} গ্রামারের সম্পূর্ণ ভাষা
প্রতিটি সফল derivation একটি স্ট্রিং $w$-কে $L(G)$-এর সদস্য বলে প্রমাণ করে — নিচের কোড সেল ঠিক এই প্রক্রিয়াটিই কম্পিউট করবে।

৪ · Worked example — ব্যালেন্সড বন্ধনী গ্রামার

একটি ছোট্ট, ক্লাসিক CFG দেখা যাক — ব্যালেন্সড বন্ধনীর ভাষা, $V=\{S\}$, $\Sigma=\{(,\ )\}$, এবং একটিমাত্র ভেরিয়েবলের দুটি রুল —

$$S \to (S)S \mid \varepsilon$$

এই গ্রামারটি পরের কয়েকটি পাঠে (M4-এর CNF রূপান্তর, M5-এর PDA-CFG ইকুইভ্যালেন্স, M6-এর CYK পার্সিং) বারবার পুনরায় ব্যবহার হবে — তাই এখানেই এটির derivation ও ভাষা কোড দিয়ে যাচাই করে নেওয়া দরকার।

Python
# CFG-এর ফরমাল সংজ্ঞা কোডে -- একটি real Grammar ক্লাস ও leftmost-derivation-search
from collections import deque

class Grammar:
    def __init__(self, variables, terminals, rules, start):
        self.V = set(variables)   # V -- ভেরিয়েবল (নন-টার্মিনাল)
        self.T = set(terminals)   # Sigma -- টার্মিনাল অ্যালফাবেট
        self.rules = rules        # R -- dict: variable -> RHS-সিকোয়েন্সের লিস্ট
        self.S = start            # S -- স্টার্ট ভেরিয়েবল

def derive_leftmost(grammar, start, target, max_steps=200):
    """সবচেয়ে বামের ভেরিয়েবল বারবার এক্সপ্যান্ড করে (BFS সহ) S =>* target -- সত্যিকারের derivation খোঁজে"""
    init = (start,)
    queue = deque([(init, [init])])
    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):        # ফাইনালাইজড 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) + 5:   # গ্রোথ বাউন্ড -- অতিরিক্ত বড় ব্রাঞ্চ prune
                continue
            queue.append((new_form, history + [new_form]))
    return None

def generate_language_up_to_length(grammar, start, max_length):
    """S থেকে সব সম্ভাব্য derivation BFS করে L(G)-কে length bound পর্যন্ত exhaustively জেনারেট করে"""
    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

# ব্যালেন্সড বন্ধনী গ্রামার -- G = (V, Sigma, R, S)
V, T = {'S'}, {'(', ')'}
rules = {'S': [('(', 'S', ')', 'S'), ()]}   # S -> (S)S | epsilon
G = Grammar(V, T, rules, 'S')

target = "(())()"
history = derive_leftmost(G, 'S', target)
print(f"টার্গেট স্ট্রিং: {target!r}\n")
print("খুঁজে পাওয়া leftmost derivation:")
for i, form in enumerate(history):
    shown = ''.join(form) if form else 'ε'
    print(f"  ধাপ {i}: {shown}")

print()
lang = generate_language_up_to_length(G, 'S', 6)
print(f"L(G), length <= 6 পর্যন্ত ({len(lang)}টি স্ট্রিং):")
print(sorted(lang, key=lambda w: (len(w), w)))

    
derive_leftmost-এর দুটি prune শর্ত লক্ষ্য করুন — (১) ফাইনালাইজড prefix টার্গেটের সাথে মিলছে কি না (leftmost derivation-এ ভেরিয়েবলের বাম পাশের সব সিম্বল ইতিমধ্যে টার্মিনাল, তাই সেগুলো পরিবর্তন হবে না), এবং (২) ফর্মের length টার্গেটের length-এর বেশি বেড়ে গেছে কি না। এই দুটো prune ছাড়া $S \to (S)S$-এর রিকার্শন তাত্ত্বিকভাবে অসীম সার্চ স্পেস তৈরি করতে পারত।
মূল কথা · Key takeaway

$G=(V,\Sigma,R,S)$ চারটি উপাদান দিয়ে একটি গ্রামার সম্পূর্ণভাবে বর্ণনা করে; $\Rightarrow$ ও $\Rightarrow^*$ সেই গ্রামার থেকে স্ট্রিং তৈরির প্রক্রিয়া নির্ভুলভাবে সংজ্ঞায়িত করে; আর $L(G)$ সেই প্রক্রিয়ায় তৈরিযোগ্য সব স্ট্রিং-এর সম্পূর্ণ সেট। M18-M21 এই একই ভিত্তির ওপর দাঁড়িয়ে অ্যাম্বিগুইটি, সিমপ্লিফিকেশন ও নর্মাল ফর্ম নিয়ে আলোচনা করবে।

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

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

প্র ০১ একটি রুলের ডানপাশ $\alpha \in (V \cup \Sigma)^*$ — এখানে ভেরিয়েবল ও টার্মিনালের মিশ্রণ অনুমোদিত কেন এটি এত গুরুত্বপূর্ণ?

কারণ এই নমনীয়তাই CFG-কে DFA/NFA (M2)-এর চেয়ে বেশি শক্তিশালী করে। একটি DFA-এর ট্রানজিশন ফাংশন $\delta(q,a) \to q$ শুধু একটি ফিক্সড, ফাইনাইট সংখ্যক অবস্থার মধ্যে থাকতে পারে — তাই সে "কতগুলো ওপেনিং বন্ধনী বাকি আছে" গণনা করতে পারে না (সংখ্যাটি অসীম হতে পারে)। কিন্তু $S \to (S)S$-এর মতো একটি রিকার্সিভ রুল, যেখানে RHS-এ ভেরিয়েবল নিজেই ফিরে আসে, স্বাভাবিকভাবেই "নেস্টিং" ট্র্যাক করতে পারে — এটাই M4-M6 জুড়ে CFL-কে রেগুলার ভাষার চেয়ে বেশি শক্তিশালী করে তোলে (M22-এর PDA-এর স্ট্যাক দিয়ে এই একই ক্ষমতা মেশিন-স্তরে বাস্তবায়িত হবে)।

প্র ০২ $\Rightarrow^*$-এর সংজ্ঞায় "reflexive" অংশ ($u \Rightarrow^* u$ শূন্য ধাপেই সত্য) কেন দরকার?

কারণ এটি $L(G)$-এর সংজ্ঞাকে সঠিক রাখে। যদি $S$ নিজেই টার্মিনাল হতো (একটি কৃত্রিম উদাহরণে), অথবা কোনো গ্রামারে সরাসরি $S \to \varepsilon$ থাকে (যেমন এই পাঠের গ্রামারে), তাহলে "শূন্য ধাপে" $S \Rightarrow^* w$ সত্য হওয়ার সুযোগ থাকা দরকার — নাহলে ε-এর মতো স্ট্রিং কখনো ভাষার সদস্য হিসেবে গণ্য হতো না, এমনকি একটি সরাসরি $S \to \varepsilon$ রুল থাকা সত্ত্বেও (যেহেতু সেটি একটি বৈধ এক-ধাপ ডেরিভেশন, শূন্য-ধাপ নয়, কিন্তু reflexive closure-এর সাধারণ সংজ্ঞা যেকোনো সংখ্যক ধাপ — শূন্য সহ — কভার করে বলেই এই পুরো প্রক্রিয়াটি গাণিতিকভাবে সুসংগত থাকে)।

প্র ০৩ কোড সেলের generate_language_up_to_length কেন length bound ছাড়া চালানো যাবে না?

কারণ $L(G)$ এখানে অসীম — ব্যালেন্সড বন্ধনীর যেকোনো length-এর জন্য (২, ৪, ৬, ...) নতুন নতুন বৈধ স্ট্রিং আছে, এবং $S \to (S)S$ রুল যতবার খুশি রিকার্সিভলি প্রয়োগ করা যায়। একটি অসীম সেট সম্পূর্ণভাবে "প্রিন্ট" করা অসম্ভব — তাই max_length প্যারামিটার BFS-কে একটি নির্দিষ্ট বিন্দুতে থামতে বাধ্য করে, একটি ফাইনাইট, পরীক্ষাযোগ্য উপসেট তৈরি করে। এটি L02-এর মূল পর্যবেক্ষণেরই প্রতিফলন — $\Sigma^*$ (এবং তাই অনেক ভাষাই) কাউন্টেবলি ইনফাইনাইট, তাই কম্পিউটেশনালি শুধু একটি বাউন্ডেড উপসেট নিয়েই কাজ করা সম্ভব।

অনুশীলন

  1. হাতে-কলমে করুন: $S \to (S)S \mid \varepsilon$ গ্রামার ব্যবহার করে "()()"-এর একটি সম্পূর্ণ leftmost derivation নিজে হাতে লিখুন, প্রতিটি ধাপে প্রয়োগ করা রুল উল্লেখ করে।

    $S \Rightarrow (S)S$  $\Rightarrow ()S$  (প্রথম $S \to \varepsilon$)  $\Rightarrow ()(S)S$  (দ্বিতীয় $S \to (S)S$)  $\Rightarrow ()()S$  ($S \to \varepsilon$)  $\Rightarrow ()()$ ($S \to \varepsilon$) — মোট ৪টি derivation ধাপ, চূড়ান্ত ফলাফল "()()"।

  2. পরীক্ষা করুন: কোড সেলে target = "(())()"-কে target = "((()))" করে Run চেপে দেখুন — কতটি derivation ধাপ লাগে, এবং সেটি কি আপনার প্রত্যাশার সাথে মেলে (একটি সম্পূর্ণ নেস্টেড গঠন, তিনটি বন্ধনী-জোড়া)?

    সার্চ সফল হবে এবং একটি ৭-ধাপের derivation পাওয়া যাবে (একই সংখ্যক ধাপ যত এই পাঠের মূল উদাহরণে, যেহেতু দুটি স্ট্রিং-এই তিনটি বন্ধনী-জোড়া আছে, শুধু নেস্টিং প্যাটার্ন ভিন্ন) — প্রতিটি $S \to (S)S$ প্রয়োগ একটি নতুন নেস্টিং লেয়ার তৈরি করে, ভেতরের-প্রথম-এর বদলে সবসময় সবচেয়ে বামের $S$ এক্সপ্যান্ড হয় বলে।

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

আগের পাঠ
L16 · DFA মিনিমাইজেশন ও ডিসিশন প্রপার্টি