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

CFG সিমপ্লিফিকেশন — অপ্রয়োজনীয় সিম্বল ও প্রোডাকশন

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

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

  • জেনারেটিং ও রিচেবল ভেরিয়েবলের ফরমাল সংজ্ঞা, এবং কেন উভয় শর্তই দরকার
  • Fixed-point অ্যালগরিদম দিয়ে জেনারেটিং সিম্বল খুঁজে বের করা
  • গ্রাফ রিচেবিলিটি দিয়ে রিচেবল সিম্বল খুঁজে বের করা
  • সরানোর সঠিক ক্রম কেন গুরুত্বপূর্ণ — একটি concrete, code-verified পাল্টা-উদাহরণ দিয়ে

১ · ইউজলেস সিম্বল — দুটো আলাদা শর্ত

L17-এর গ্রামারে অনেক সময় এমন ভেরিয়েবল থাকতে পারে যেগুলো ভাষার $L(G)$-এ আসলে কোনো ভূমিকাই রাখে না — এদের সরিয়ে ফেললে গ্রামার ছোট, পরিষ্কার হয়, অথচ ভাষা অপরিবর্তিত থাকে। এই সিমপ্লিফিকেশন L20-এর চমস্কি নর্মাল ফর্মের একটি প্রি-কন্ডিশন — CNF রূপান্তর একটি "পরিষ্কার" গ্রামার দাবি করে।

একটি ভেরিয়েবল $A$ কে ইউজফুল বলা হয় শুধু যদি সে দুটো আলাদা শর্তই পূরণ করে —

জেনারেটিং
$A \Rightarrow^* w$ কোনো টার্মিনাল স্ট্রিং $w \in \Sigma^*$-এর জন্য সত্য — অর্থাৎ $A$ থেকে শুরু করে কখনো না কখনো একটি সম্পূর্ণ টার্মিনাল স্ট্রিং-এ পৌঁছানো সম্ভব।
রিচেবল
$S \Rightarrow^* \alpha A \beta$ কোনো $\alpha, \beta \in (V \cup \Sigma)^*$-এর জন্য সত্য — অর্থাৎ স্টার্ট সিম্বল থেকে ডেরিভেশন চালিয়ে কখনো না কখনো $A$-এ পৌঁছানো সম্ভব।

$A$ ইউজফুলUseful Symbolজেনারেটিং এবং রিচেবল — উভয় শর্তই পূরণ করে এমন ভেরিয়েবল, যা L(G)-তে সত্যিই কোনো ভূমিকা রাখে। শুধু তখনই, যখন সে জেনারেটিং এবং রিচেবল, দুটোই — যেকোনো একটি শর্ত মিস হলেই সেই ভেরিয়েবল (এবং তাকে ব্যবহারকারী প্রতিটি রুল) $L(G)$-তে বাস্তবে অংশগ্রহণ করতে পারে না।

২ · জেনারেটিং সিম্বল — fixed-point অ্যালগরিদম

জেনারেটিং সিম্বল খোঁজার অ্যালগরিদম L09-এর ε-closure-এর মতোই একটি fixed-point কম্পিউটেশন — শুরু করা হয় যেসব ভেরিয়েবলের কোনো রুলের ডানপাশ সম্পূর্ণ টার্মিনাল (বা ε) দিয়ে, তারপর বারবার নতুন ভেরিয়েবল যোগ করা হয় যাদের কোনো রুল ইতিমধ্যে-জানা জেনারেটিং সিম্বল (এবং টার্মিনাল) দিয়েই সম্পূর্ণভাবে তৈরি করা যায় — যতক্ষণ না নতুন কিছু আর যোগ হয়।

৩ · রিচেবল সিম্বল — গ্রাফ রিচেবিলিটি

রিচেবল সিম্বল খোঁজা DSA কোর্সের গ্রাফ ট্র্যাভার্সাল অ্যালগরিদমের একটি সরাসরি প্রয়োগ — গ্রামারের রুলগুলোকে একটি "রেফারেন্স গ্রাফ" হিসেবে দেখা যায় ($A$ থেকে $B$-তে একটি এজ, যদি $A$-এর কোনো রুলের RHS-এ $B$ থাকে), এবং স্টার্ট সিম্বল $S$ থেকে BFS/DFS চালিয়ে রিচেবল সব ভেরিয়েবল খুঁজে বের করা হয়।

সরানোর সঠিক ক্রম — একটি সূক্ষ্ম কিন্তু গুরুত্বপূর্ণ পয়েন্ট

প্রথমে non-generating সিম্বল সরান, তারপর অবশিষ্ট গ্রামারে reachability চেক করুন। উল্টো ক্রমে করলে (আগে reachability, পরে generating-check) কিছু genuinely ইউজলেস সিম্বল অবশিষ্ট থেকে যেতে পারে — কারণ একটি সিম্বল মূল গ্রামারে "রিচেবল" ছিল এমন একটি রুলের মাধ্যমে, যে রুলটি পরে non-generating সিম্বল সরানোর ফলে নিজেই বাদ পড়ে যায় — তখন সেই রিচেবিলিটি তথ্যটি আর বৈধ থাকে না, কিন্তু ভুল ক্রমে সেটি পুনরায় চেক করা হয় না।

৪ · কোডে সঠিক ক্রম যাচাই করা

নিচের গ্রামারটি ইচ্ছাকৃতভাবে এমনভাবে বানানো — যেখানে ভুল ক্রমে সিমপ্লিফাই করলে একটি ইউজলেস সিম্বল ($A$) অবশিষ্ট থেকে যায়, সঠিক ক্রমে করলে সঠিকভাবে বাদ পড়ে —

$$S \to AB \mid a \qquad A \to b \qquad B \to bB \quad (\text{কোনো base case নেই, তাই B কখনো টার্মিনেট করে না})$$

Python
# find_generating_symbols, find_reachable_symbols, remove_useless_symbols -- সঠিক ক্রমে
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 find_generating_symbols(grammar):
    """fixed-point: A জেনারেটিং যদি তার কোনো রুল সম্পূর্ণভাবে টার্মিনাল ও ইতিমধ্যে-জানা জেনারেটিং সিম্বল দিয়ে তৈরি"""
    generating = set()
    changed = True
    while changed:
        changed = False
        for var, rhss in grammar.rules.items():
            if var in generating:
                continue
            for rhs in rhss:
                if all((sym in grammar.T) or (sym in generating) for sym in rhs):
                    generating.add(var)
                    changed = True
                    break
    return generating

def find_reachable_symbols(grammar, start):
    """S থেকে BFS -- গ্রামারের রুলকে একটি রেফারেন্স গ্রাফ হিসেবে ট্র্যাভার্স করে"""
    reachable = {start}
    queue = deque([start])
    while queue:
        var = queue.popleft()
        for rhs in grammar.rules.get(var, []):
            for sym in rhs:
                if sym in grammar.V and sym not in reachable:
                    reachable.add(sym)
                    queue.append(sym)
    return reachable

def _restrict_rules(grammar, keep_vars):
    new_rules = {}
    for var, rhss in grammar.rules.items():
        if var not in keep_vars:
            continue
        kept = [rhs for rhs in rhss if all((s in grammar.T) or (s in keep_vars) for s in rhs)]
        if kept:
            new_rules[var] = kept
    return new_rules

def remove_useless_symbols(grammar):
    """সঠিক ক্রম: generating আগে, তারপর অবশিষ্ট গ্রামারে reachability"""
    generating = find_generating_symbols(grammar)
    gen_rules = _restrict_rules(grammar, generating | grammar.T)
    gen_grammar = Grammar(generating, grammar.T, gen_rules, grammar.S)
    reachable = find_reachable_symbols(gen_grammar, grammar.S)
    final_vars = generating & reachable
    final_rules = _restrict_rules(gen_grammar, final_vars | grammar.T)
    return Grammar(final_vars, grammar.T, final_rules, grammar.S)

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

# ডেলিবারেটলি: B রিচেবল কিন্তু কখনো জেনারেট করে না (কোনো base case নেই);
# A জেনারেটিং কিন্তু সঠিক-ক্রম সিমপ্লিফিকেশনের পর আর রিচেবল থাকবে না
V = {'S', 'A', 'B'}
T = {'a', 'b'}
rules = {
    'S': [('A', 'B'), ('a',)],
    'A': [('b',)],
    'B': [('b', 'B')],
}
G = Grammar(V, T, rules, 'S')

print("generating:", find_generating_symbols(G))
print("reachable (মূল গ্রামারে):", find_reachable_symbols(G, 'S'))

simplified = remove_useless_symbols(G)
print("\nসঠিক ক্রমে সিমপ্লিফাই করার পর:")
print("  ভেরিয়েবল:", simplified.V)
print("  রুল:", simplified.rules)

lang_before = generate_language_up_to_length(G, 'S', 5)
lang_after = generate_language_up_to_length(simplified, 'S', 5)
print("\nL(মূল গ্রামার) length<=5:", sorted(lang_before))
print("L(সিমপ্লিফায়েড) length<=5:", sorted(lang_after))
print("ভাষা অপরিবর্তিত:", lang_before == lang_after)

    
চালিয়ে দেখুন — generating = {'S', 'A'} (B বাদ, কখনো টার্মিনেট করে না) কিন্তু reachable (মূল গ্রামারে) = {'S', 'A', 'B'} (B রিচেবল, যেহেতু $S \to AB$ রুলে তাকে রেফার করা হয়েছে)। সঠিক ক্রমে সিমপ্লিফাই করার পর — প্রথমে B বাদ পড়ে (non-generating), যার ফলে $S \to AB$ রুলটিও বাদ পড়ে (যেহেতু এটি এখন-অনুপস্থিত B-কে রেফার করে) — আর তখন $A$-কে রেফারেন্স করার মতো আর কোনো রুলই অবশিষ্ট থাকে না, তাই reachability-এর দ্বিতীয় ধাপে $A$-ও বাদ পড়ে। চূড়ান্ত গ্রামার শুধু S -> a — অথচ ভাষা অপরিবর্তিত ({"a"}), কারণ মূল গ্রামারের $S \to AB$ শাখা আদতে কখনোই কোনো টার্মিনাল স্ট্রিং-এ পৌঁছাতে পারত না (B কখনো টার্মিনেট করত না)।
মূল কথা · Key takeaway

একটি ভেরিয়েবল সত্যিই ইউজফুল হতে জেনারেটিং ও রিচেবল উভয়ই হতে হবে — আর এই দুই শর্ত যাচাইয়ের ক্রম সঠিক রাখা জরুরি, কারণ non-generating সিম্বল সরানো নতুন করে কিছু রুল বাদ দিতে পারে, যা reachability-কে বদলে দিতে পারে। L20-এর CNF রূপান্তর এই ঠিক এই remove_useless_symbols ফাংশনটিকেই একটি প্রি-কন্ডিশন হিসেবে ব্যবহার করবে।

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

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

প্র ০১ একটি ভেরিয়েবল জেনারেটিং হওয়া সত্ত্বেও কেন সে ইউজলেস হতে পারে?

কারণ জেনারেটিং শুধু বলে "এই ভেরিয়েবল থেকে কোনো না কোনো টার্মিনাল স্ট্রিং তৈরি করা সম্ভব" — কিন্তু যদি স্টার্ট সিম্বল $S$ থেকে কোনো ডেরিভেশনই আদৌ এই ভেরিয়েবলে পৌঁছাতে না পারে (অর্থাৎ সে রিচেবল না হয়), তাহলে তার "জেনারেট করার ক্ষমতা" $L(G)$-তে কখনোই বাস্তবায়িত হয় না — এই পাঠের কোড উদাহরণে এভাবেই $A$ (জেনারেটিং হওয়া সত্ত্বেও) চূড়ান্তভাবে ইউজলেস প্রমাণিত হয়েছে, একবার সঠিক ক্রমে $B$ সরানোর পর তার reachability হারিয়ে যাওয়ায়।

প্র ০২ যদি প্রথমে reachability চেক করা হতো, তাহলে চূড়ান্ত ফলাফলে ঠিক কী ভুল থেকে যেত?

ভুল ক্রমে (আগে reachability, পরে generating), মূল গ্রামারে $A$, $B$, $S$ সবাই রিচেবল হিসেবে ধরা পড়ত (যেহেতু $S \to AB$ রুলটি তখনও বিদ্যমান)। এরপর generating-check প্রয়োগ করলে $B$ বাদ পড়ে (non-generating), কিন্তু $A$ থেকে যায় — কারণ ইতিমধ্যে-গণনা-করা reachable সেটে $A$ ছিল, এবং সেই তথ্য $B$-সরানোর পরে আর পুনরায় যাচাই করা হয় না। ফলাফল: চূড়ান্ত "সিমপ্লিফায়েড" গ্রামারে $A \to b$ রুলটি অহেতুক থেকে যায় — এটি ভাষা পরিবর্তন করে না, কিন্তু গ্রামারকে অপ্রয়োজনীয়ভাবে বড় রাখে, যা L20-এর CNF রূপান্তরের জন্য একটি অপরিষ্কার প্রি-কন্ডিশন।

প্র ০৩ find_generating_symbols-এর fixed-point লুপ কখন থামে, এবং কেন সেটি নিশ্চিতভাবে থামে (অসীম লুপ হয় না)?

লুপটি থামে যখন একটি সম্পূর্ণ পাসে কোনো নতুন ভেরিয়েবল generating সেটে যোগ হয় না (changed = False)। এটি নিশ্চিতভাবে থামে কারণ generating সেট প্রতিটি পাসে হয় বাড়ে, নয়তো অপরিবর্তিত থাকে (কখনো ছোট হয় না) — এবং এটি $V$-এর একটি উপসেট, যা ফাইনাইট। তাই সর্বাধিক $|V|$ বার পাস চালালেই সেট আর বাড়তে পারবে না — এটি L09-এর ε-closure fixed-point-এর একই যুক্তি (মনোটোনিক বৃদ্ধি + ফাইনাইট আপার বাউন্ড = নিশ্চিত টার্মিনেশন)।

অনুশীলন

  1. হাতে-কলমে করুন: গ্রামার $S \to AB \mid a$, $A \to b$, $B \to bB$-এ, find_generating_symbols ও find_reachable_symbols-এর ফলাফল আগে থেকে হাতে-কলমে অনুমান করুন, তারপর কোড সেল চালিয়ে মিলিয়ে দেখুন।

    জেনারেটিং: $\{S, A\}$ ($S \to a$ সরাসরি টার্মিনাল দিয়ে টার্মিনেট করে, $A \to b$ও তাই; $B$ কখনো টার্মিনেট করে না)। রিচেবল (মূল গ্রামারে): $\{S, A, B\}$ (সবাই $S$ থেকে কোনো না কোনো রুলের মাধ্যমে পৌঁছানো যায়)। কোড সেলের আউটপুট এই দুটোর সাথেই মিলবে।

  2. পরীক্ষা করুন: কোড সেলে গ্রামারে একটি নতুন রুল 'B': [('b', 'B'), ('b',)] যোগ করে (অর্থাৎ B-কে একটি base case দিয়ে) Run চেপে দেখুন সিমপ্লিফিকেশনের ফলাফল কীভাবে বদলায়।

    এখন $B$ জেনারেটিং হয়ে যাবে ($B \to b$ ভায়া বেস কেস), তাই generating = $\{S, A, B\}$ — কোনো সিম্বল non-generating হিসেবে বাদ পড়বে না। যেহেতু $B$ এখন জেনারেটিং, $S \to AB$ রুলটি টিকে থাকে, এবং তাই reachability-এর দ্বিতীয় ধাপেও $A$ ও $B$ উভয়ই রিচেবল থেকে যায় — চূড়ান্ত সিমপ্লিফায়েড গ্রামারে সব তিনটি ভেরিয়েবলই ইউজফুল হিসেবে টিকে থাকবে (কোনো সিম্বলই বাদ পড়বে না)।

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

আগের পাঠ
L18 · পার্স ট্রি ও অ্যাম্বিগুইটি