পাঠ ১৯ · ৫৮-এর মধ্যে · মডিউল ৪
Home / Courses / Concepts of Programming Languages & Compiler Design / DFA মিনিমাইজেশন

DFA মিনিমাইজেশন

DFA minimization
৮ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

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

  • স্টেট সমতুল্যতার সংজ্ঞা এবং কেন কম স্টেট ব্যবহারিকভাবে গুরুত্বপূর্ণ
  • Partition refinement অ্যালগরিদমের ধাপে-ধাপে যুক্তি
  • একটি রিডানড্যান্ট DFA-তে real কোড দিয়ে সমতুল্য স্টেট খুঁজে মার্জ করা
  • মিনিমাইজেশনের আগে ও পরে ভাষা অপরিবর্তিত আছে কিনা তা systematically যাচাই করা

১ · সমস্যা — অপ্রয়োজনীয় স্টেট

L18-এর subset construction থেকে পাওয়া DFA সঠিক, কিন্তু সবসময় সর্বনিম্ন সংখ্যক স্টেট ব্যবহার করে না। কিছু স্টেট হতে পারে সমতুল্য (equivalent)দুটি স্টেট সমতুল্য যদি — সেই দুটি স্টেট থেকে শুরু করে যেকোনো একই বাকি ইনপুট স্ট্রিং দিলে, দুটোই একই ফলাফলে (accept/reject) পৌঁছায়। অর্থাৎ ভবিষ্যতের কোনো ইনপুটেই তাদের মধ্যে পার্থক্য বোঝা যায় না। — অর্থাৎ, ভবিষ্যতে যেকোনো বাকি ইনপুট স্ট্রিং দিলে দুটি স্টেট থেকেই একই ফলাফল (accept বা reject) আসবে। এমন স্টেট দুটোকে একটিতে মার্জ করলেও DFA-র চেনা ভাষা এতটুকু বদলায় না — শুধু স্টেট সংখ্যা কমে।

বাস্তবে এটি গুরুত্বপূর্ণ কেন (সরাসরি L20-এর লেক্সারের সাথে সম্পর্কিত): কম স্টেট মানে ছোট ট্রানজিশন টেবিল, কম মেমরি, এবং একটি বাস্তব কম্পাইলারের লেক্সারে যেখানে হাজারো টোকেন-প্যাটার্নের DFA একত্রে থাকে, সেখানে এই সাশ্রয় প্রকৃত পার্থক্য তৈরি করে।

২ · Partition refinement অ্যালগরিদম

কৌশলটি "গ্রুপ ভাঙা"-র একটি fixed-point প্রক্রিয়া —

  1. শুরুর বিভাজন: সব accepting স্টেট এক গ্রুপে, সব non-accepting স্টেট আরেক গ্রুপে (এই দুটো গ্রুপ স্বতঃসিদ্ধভাবেই আলাদা — একটি স্ট্রিং শেষে accept করে, আরেকটি করে না)।
  2. ভাঙা (split): যেকোনো গ্রুপের মধ্যে যদি দুটি স্টেট কোনো একটি ইনপুট সিম্বলে ভিন্ন গ্রুপে ট্রানজিশন করে, তাহলে সেই গ্রুপকে ভেঙে ফেলো — একই সিম্বলে একই গ্রুপে যাওয়া স্টেটগুলো একসাথে থাকবে।
  3. পুনরাবৃত্তি: আর কোনো গ্রুপ ভাঙা সম্ভব না হওয়া পর্যন্ত ধাপ ২ চালিয়ে যাও (fixed point)।
  4. চূড়ান্ত প্রতিটি গ্রুপ মিনিমাইজড DFA-র একটি স্টেট।

৩ · Worked উদাহরণ — একটি রিডানড্যান্ট DFA

ভাষা: "{0,1}-এর উপর এমন বাইনারি স্ট্রিং যাতে অন্তত একটি 1 আছে"। ইচ্ছাকৃতভাবে ৪টি স্টেট দিয়ে (২টি মিনিমাল হলেও যথেষ্ট) DFA বানানো হয়েছে যাতে qB ও qC সমতুল্য কিন্তু আলাদা স্টেট হিসেবে থেকে যায় — যেমনটি subset construction থেকে বাস্তবে ঘটতে পারে।

রিডানড্যান্ট DFA-র সংজ্ঞা

$Q=\{q_0,q_B,q_C,q_F\}$,   $q_0$ শুরু,   $F=\{q_F\}$

$\delta(q_0,0)=q_B,\ \delta(q_0,1)=q_C,\ \delta(q_B,0)=q_B,\ \delta(q_B,1)=q_F,\ \delta(q_C,0)=q_C,\ \delta(q_C,1)=q_F,\ \delta(q_F,0)=q_F,\ \delta(q_F,1)=q_F$

লক্ষ্য করুন — $q_B$ ও $q_C$ প্রতিটি সিম্বলে একই ধরনের স্থানে যায়: 0-তে নিজের গ্রুপেই থাকে, 1-তে দুটোই সরাসরি $q_F$-তে যায়। এই দুটো স্টেট তাই আচরণগতভাবে সমতুল্য — শুধু $q_0$ থেকে কীভাবে পৌঁছানো হয়েছে তাতে পার্থক্য (0 পড়ে vs 1 পড়ে), যা partition refinement-এর কাছে অপ্রাসঙ্গিক।

qB ও qC সমতুল্য — মার্জ হবে start q0 qB qC qF 0 1 0 0 1 1 0,1
qB ও qC প্রতিটি সিম্বলে একই ধরনের গ্রুপে যায় ('0'-এ নিজেদের গ্রুপে, '1'-এ qF-এ) — তাই আচরণগতভাবে সমতুল্য, যদিও লিটারেলি ভিন্ন স্টেট।
Python
# রিডানড্যান্ট DFA: "অন্তত একটি 1 আছে" -- qB ও qC ইচ্ছাকৃতভাবে সমতুল্য কিন্তু আলাদা
states = {'q0', 'qB', 'qC', 'qF'}
alphabet = ['0', '1']
delta = {
    ('q0', '0'): 'qB', ('q0', '1'): 'qC',
    ('qB', '0'): 'qB', ('qB', '1'): 'qF',
    ('qC', '0'): 'qC', ('qC', '1'): 'qF',
    ('qF', '0'): 'qF', ('qF', '1'): 'qF',
}
start = 'q0'
accept = {'qF'}

def run_dfa(states, delta, start, accept, s):
    state = start
    for ch in s:
        state = delta[(state, ch)]
    return state in accept

def group_index(partition, state):
    for i, g in enumerate(partition):
        if state in g:
            return i
    raise ValueError("স্টেট কোনো গ্রুপে নেই")

def minimize_dfa(states, alphabet, delta, start, accept):
    non_accept = states - accept
    partition = [g for g in [set(accept), set(non_accept)] if g]
    changed = True
    while changed:
        changed = False
        new_partition = []
        for group in partition:
            buckets = {}
            for state in group:
                # প্রতিটি সিম্বলে এই স্টেট কোন গ্রুপে যায় তার "স্বাক্ষর"
                sig = tuple(group_index(partition, delta[(state, sym)]) for sym in alphabet)
                buckets.setdefault(sig, set()).add(state)
            if len(buckets) > 1:
                changed = True
            new_partition.extend(buckets.values())
        partition = new_partition

    # প্রতিটি গ্রুপকে একটি নতুন মিনিমাইজড স্টেট নাম দাও
    group_name = {i: "G" + str(i) + "_" + "_".join(sorted(g)) for i, g in enumerate(partition)}
    rep = {s: group_name[i] for i, g in enumerate(partition) for s in g}
    new_states = set(group_name.values())
    new_delta = {}
    for i, g in enumerate(partition):
        any_state = next(iter(g))
        for sym in alphabet:
            new_delta[(group_name[i], sym)] = rep[delta[(any_state, sym)]]
    new_start = rep[start]
    new_accept = {group_name[i] for i, g in enumerate(partition) if g & accept}
    return new_states, new_delta, new_start, new_accept, partition

print(f"মূল DFA-র স্টেট সংখ্যা: {len(states)}")
new_states, new_delta, new_start, new_accept, partition = minimize_dfa(states, alphabet, delta, start, accept)
print(f"মিনিমাইজড DFA-র স্টেট সংখ্যা: {len(new_states)}")
print("চূড়ান্ত গ্রুপ:")
for g in partition:
    print(f"  {sorted(g)}")

test_strings = ["", "0", "1", "00", "01", "10", "11", "000", "010", "101", "0101", "0000"]
all_match = True
print("\nমূল বনাম মিনিমাইজড DFA -- সব টেস্ট স্ট্রিং:")
for s in test_strings:
    orig = run_dfa(states, delta, start, accept, s)
    minim = run_dfa(new_states, new_delta, new_start, new_accept, s)
    ok = orig == minim
    all_match = all_match and ok
    print(f"  {s!r:8s} মূল={orig!s:5s} মিনিমাইজড={minim!s:5s} {'ঠিক আছে' if ok else 'গরমিল!'}")
print(f"\nসব ফলাফল হুবহু মিলেছে: {all_match}")

    
কোড চালালে দেখা যায় partition refinement ঠিক {qB, qC}-কেই একসাথে মার্জ করে (q0 ও qF নিজেদের আলাদা গ্রুপে থেকে যায়) — ফলে ৪টি স্টেট থেকে ৩টি স্টেটে নেমে আসে। এবং ১২টি টেস্ট স্ট্রিং-এর প্রতিটিতে মূল ও মিনিমাইজড DFA হুবহু একই accept/reject ফলাফল দেয় — এই মিলটাই মিনিমাইজেশনের সঠিকতার প্রকৃত প্রমাণ।
মূল কথা · Key takeaway

DFA মিনিমাইজেশন কখনো ভাষা বদলায় না — এটি শুধু একই ভাষা চেনার জন্য প্রয়োজনীয় সর্বনিম্ন সংখ্যক স্টেট খুঁজে বের করে। L18-এর subset construction যতগুলো স্টেটই তৈরি করুক না কেন, partition refinement সবসময় একটি নির্দিষ্ট, প্রমাণযোগ্যভাবে সর্বনিম্ন DFA-তে নামিয়ে আনে — এবং এই মিনিমাইজড DFA-ই L20-এর লেক্সারে ব্যবহারের জন্য সবচেয়ে উপযুক্ত।

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

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

প্র ০১ শুরুর বিভাজনে q0, qB, qC সবাই একই (non-accepting) গ্রুপে ছিল। কেন প্রথম রাউন্ডেই q0 আলাদা হয়ে গেল, কিন্তু qB-qC একসাথে থেকে গেল?

কারণ '1' সিম্বলে তিনটি স্টেটের গন্তব্য ভিন্ন গ্রুপে পড়ে: q0-এর '1' ট্রানজিশন যায় qC-তে, যা (প্রথম রাউন্ডে) এখনও non-accepting গ্রুপেই আছে — কিন্তু qB ও qC-এর '1' ট্রানজিশন দুটোই সরাসরি qF-এ যায়, যা accepting গ্রুপে। তাই q0-এর "স্বাক্ষর" (কোন গ্রুপে যায়) qB/qC-এর থেকে আলাদা হয়ে যায় — এই পার্থক্যই q0-কে আলাদা করে ফেলে, অথচ qB ও qC-এর স্বাক্ষর অভিন্ন থেকে যায় বলে তারা একসাথে থেকে যায়।

প্র ০২ মিনিমাইজেশনের পর নতুন গ্রুপ-স্টেটের নাম যেমন G1_qB_qC — বাস্তব কম্পাইলারে কি এই ধরনের নাম ব্যবহার করা হয়, নাকি অন্য কিছু?

বাস্তব কম্পাইলার সাধারণত এই বর্ণনামূলক নাম রাখে না — প্রতিটি গ্রুপকে শুধু একটি সাংখ্যিক ID (0, 1, 2, ...) দেওয়া হয়, কারণ ট্রানজিশন টেবিল সাধারণত একটি ২-ডাইমেনশনাল অ্যারে (স্টেট × সিম্বল) হিসেবে সংরক্ষিত হয়, যেখানে নাম নয়, ইনডেক্স গুরুত্বপূর্ণ। এই পাঠে বর্ণনামূলক নাম শুধু শিক্ষামূলক স্বচ্ছতার জন্য ব্যবহার করা হয়েছে — যাতে বোঝা যায় কোন মূল স্টেটগুলো কোন নতুন গ্রুপে মিশে গেছে।

প্র ০৩ যদি partition refinement অ্যালগরিদমে কোনো বাগ থাকত যা ভুলভাবে দুটি অ-সমতুল্য স্টেট মার্জ করে ফেলত, তাহলে এই পাঠের কোড কীভাবে সেই বাগ ধরতে পারত?

ঠিক সেই কারণেই কোডে all_match চেক আছে — মূল DFA ও মিনিমাইজড DFA-কে ১২টি ভিন্ন টেস্ট স্ট্রিং-এ স্বাধীনভাবে চালিয়ে ফলাফল তুলনা করা হয়েছে। যদি দুটি সত্যিকারের ভিন্ন-আচরণের স্টেট ভুলভাবে মার্জ হয়ে যেত, তাহলে অন্তত একটি টেস্ট স্ট্রিং-এ মূল ও মিনিমাইজড DFA ভিন্ন ফলাফল দিত (একটি accept, আরেকটি reject) — এবং ok = orig == minim লাইনটি সেখানে False হয়ে "গরমিল!" প্রিন্ট করত। এই ধরনের স্বাধীন ক্রস-চেক ছাড়া শুধু "স্টেট সংখ্যা কমেছে" দেখে ভুল বাগও সঠিক মনে হতে পারত।

অনুশীলন

  1. হাতে করুন: শুরুর বিভাজন [{qF}, {q0, qB, qC}] থেকে শুরু করে, '0' ও '1' উভয় সিম্বলের জন্য প্রতিটি স্টেটের "গ্রুপ-স্বাক্ষর" হাতে-কলমে লিখে দেখান কীভাবে q0 আলাদা হয়ে যায়।

    রাউন্ড ১-এ গ্রুপ ইনডেক্স: {qF}=গ্রুপ০, {q0,qB,qC}=গ্রুপ১। স্বাক্ষর (0-এ গন্তব্য-গ্রুপ, 1-এ গন্তব্য-গ্রুপ): q0 → (গ্রুপ১, গ্রুপ১) [কারণ qB,qC দুটোই গ্রুপ১-এ], qB → (গ্রুপ১, গ্রুপ০), qC → (গ্রুপ১, গ্রুপ০)। q0-এর স্বাক্ষর (গ্রুপ১,গ্রুপ১) আলাদা qB/qC-এর (গ্রুপ১,গ্রুপ০) থেকে — তাই q0 আলাদা হয়ে যায়, আর qB-qC অভিন্ন স্বাক্ষরের কারণে একসাথে থাকে।

  2. পরীক্ষা করুন: কোড সেলে delta[('qC', '1')]-এর মান 'qF'-এর বদলে 'qB' করে (ইচ্ছাকৃতভাবে qB-qC-কে অ-সমতুল্য বানিয়ে) Run চাপুন — মিনিমাইজড স্টেট সংখ্যা কী হয়?

    এখন qB-এর '1' স্বাক্ষর যায় qF-এ (accepting গ্রুপ), কিন্তু qC-এর '1' স্বাক্ষর যায় qB-তে (non-accepting গ্রুপ) — দুটো ভিন্ন গ্রুপ! তাই qB ও qC আর সমতুল্য থাকে না, এবং অ্যালগরিদম তাদের আলাদা রাখে — মিনিমাইজড স্টেট সংখ্যা এখন ৪-ই থেকে যায় (কোনো মার্জ হয় না)। এটি দেখায় সমতুল্যতা কতটা সঠিকভাবে নির্ভর করে প্রতিটি ট্রানজিশনের উপর — সামান্য পরিবর্তনও সমতুল্যতা ভেঙে দিতে পারে।

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

আগের পাঠ
ফাইনাইট অটোমাটা — NFA থেকে DFA