DFA মিনিমাইজেশন
এই পাঠে যা শিখবেন
- স্টেট সমতুল্যতার সংজ্ঞা এবং কেন কম স্টেট ব্যবহারিকভাবে গুরুত্বপূর্ণ
- Partition refinement অ্যালগরিদমের ধাপে-ধাপে যুক্তি
- একটি রিডানড্যান্ট DFA-তে real কোড দিয়ে সমতুল্য স্টেট খুঁজে মার্জ করা
- মিনিমাইজেশনের আগে ও পরে ভাষা অপরিবর্তিত আছে কিনা তা systematically যাচাই করা
১ · সমস্যা — অপ্রয়োজনীয় স্টেট
L18-এর subset construction থেকে পাওয়া DFA সঠিক, কিন্তু সবসময় সর্বনিম্ন সংখ্যক স্টেট ব্যবহার করে না। কিছু স্টেট হতে পারে সমতুল্য (equivalent)দুটি স্টেট সমতুল্য যদি — সেই দুটি স্টেট থেকে শুরু করে যেকোনো একই বাকি ইনপুট স্ট্রিং দিলে, দুটোই একই ফলাফলে (accept/reject) পৌঁছায়। অর্থাৎ ভবিষ্যতের কোনো ইনপুটেই তাদের মধ্যে পার্থক্য বোঝা যায় না। — অর্থাৎ, ভবিষ্যতে যেকোনো বাকি ইনপুট স্ট্রিং দিলে দুটি স্টেট থেকেই একই ফলাফল (accept বা reject) আসবে। এমন স্টেট দুটোকে একটিতে মার্জ করলেও DFA-র চেনা ভাষা এতটুকু বদলায় না — শুধু স্টেট সংখ্যা কমে।
বাস্তবে এটি গুরুত্বপূর্ণ কেন (সরাসরি L20-এর লেক্সারের সাথে সম্পর্কিত): কম স্টেট মানে ছোট ট্রানজিশন টেবিল, কম মেমরি, এবং একটি বাস্তব কম্পাইলারের লেক্সারে যেখানে হাজারো টোকেন-প্যাটার্নের DFA একত্রে থাকে, সেখানে এই সাশ্রয় প্রকৃত পার্থক্য তৈরি করে।
২ · Partition refinement অ্যালগরিদম
কৌশলটি "গ্রুপ ভাঙা"-র একটি fixed-point প্রক্রিয়া —
- শুরুর বিভাজন: সব accepting স্টেট এক গ্রুপে, সব non-accepting স্টেট আরেক গ্রুপে (এই দুটো গ্রুপ স্বতঃসিদ্ধভাবেই আলাদা — একটি স্ট্রিং শেষে accept করে, আরেকটি করে না)।
- ভাঙা (split): যেকোনো গ্রুপের মধ্যে যদি দুটি স্টেট কোনো একটি ইনপুট সিম্বলে ভিন্ন গ্রুপে ট্রানজিশন করে, তাহলে সেই গ্রুপকে ভেঙে ফেলো — একই সিম্বলে একই গ্রুপে যাওয়া স্টেটগুলো একসাথে থাকবে।
- পুনরাবৃত্তি: আর কোনো গ্রুপ ভাঙা সম্ভব না হওয়া পর্যন্ত ধাপ ২ চালিয়ে যাও (fixed point)।
- চূড়ান্ত প্রতিটি গ্রুপ মিনিমাইজড DFA-র একটি স্টেট।
৩ · Worked উদাহরণ — একটি রিডানড্যান্ট DFA
ভাষা: "{0,1}-এর উপর এমন বাইনারি স্ট্রিং যাতে অন্তত একটি 1 আছে"। ইচ্ছাকৃতভাবে ৪টি স্টেট দিয়ে (২টি মিনিমাল হলেও
যথেষ্ট) DFA বানানো হয়েছে যাতে qB ও qC সমতুল্য কিন্তু আলাদা স্টেট হিসেবে থেকে যায় —
যেমনটি subset construction থেকে বাস্তবে ঘটতে পারে।
$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-এর কাছে অপ্রাসঙ্গিক।
# রিডানড্যান্ট 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}")
{qB, qC}-কেই একসাথে মার্জ করে (q0 ও
qF নিজেদের আলাদা গ্রুপে থেকে যায়) — ফলে ৪টি স্টেট থেকে ৩টি স্টেটে নেমে আসে। এবং ১২টি টেস্ট
স্ট্রিং-এর প্রতিটিতে মূল ও মিনিমাইজড DFA হুবহু একই accept/reject ফলাফল দেয় — এই মিলটাই মিনিমাইজেশনের সঠিকতার
প্রকৃত প্রমাণ।
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 হয়ে "গরমিল!" প্রিন্ট করত। এই ধরনের
স্বাধীন ক্রস-চেক ছাড়া শুধু "স্টেট সংখ্যা কমেছে" দেখে ভুল বাগও সঠিক মনে হতে পারত।
অনুশীলন
-
হাতে করুন: শুরুর বিভাজন
[{qF}, {q0, qB, qC}]থেকে শুরু করে, '0' ও '1' উভয় সিম্বলের জন্য প্রতিটি স্টেটের "গ্রুপ-স্বাক্ষর" হাতে-কলমে লিখে দেখান কীভাবেq0আলাদা হয়ে যায়।রাউন্ড ১-এ গ্রুপ ইনডেক্স:
{qF}=গ্রুপ০,{q0,qB,qC}=গ্রুপ১। স্বাক্ষর (0-এ গন্তব্য-গ্রুপ, 1-এ গন্তব্য-গ্রুপ):q0→ (গ্রুপ১, গ্রুপ১) [কারণ qB,qC দুটোই গ্রুপ১-এ],qB→ (গ্রুপ১, গ্রুপ০),qC→ (গ্রুপ১, গ্রুপ০)।q0-এর স্বাক্ষর (গ্রুপ১,গ্রুপ১) আলাদাqB/qC-এর (গ্রুপ১,গ্রুপ০) থেকে — তাইq0আলাদা হয়ে যায়, আরqB-qCঅভিন্ন স্বাক্ষরের কারণে একসাথে থাকে। -
পরীক্ষা করুন: কোড সেলে
delta[('qC', '1')]-এর মান'qF'-এর বদলে'qB'করে (ইচ্ছাকৃতভাবে qB-qC-কে অ-সমতুল্য বানিয়ে) Run চাপুন — মিনিমাইজড স্টেট সংখ্যা কী হয়?এখন
qB-এর '1' স্বাক্ষর যায়qF-এ (accepting গ্রুপ), কিন্তুqC-এর '1' স্বাক্ষর যায়qB-তে (non-accepting গ্রুপ) — দুটো ভিন্ন গ্রুপ! তাইqBওqCআর সমতুল্য থাকে না, এবং অ্যালগরিদম তাদের আলাদা রাখে — মিনিমাইজড স্টেট সংখ্যা এখন ৪-ই থেকে যায় (কোনো মার্জ হয় না)। এটি দেখায় সমতুল্যতা কতটা সঠিকভাবে নির্ভর করে প্রতিটি ট্রানজিশনের উপর — সামান্য পরিবর্তনও সমতুল্যতা ভেঙে দিতে পারে।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৮টি পাঠ পরবর্তী পাঠ — এই মিনিমাইজড DFA-গুলো ব্যবহার করে একটি সম্পূর্ণ লেক্সার বানানো দেখাবে।
- Data Structures & Algorithms কোর্স সঙ্গী কোর্স partition refinement-এর সেট ও গ্রুপ নিয়ে কাজ করা, এবং "কম মেমরি/কম স্টেট" অপ্টিমাইজেশনের এই চিন্তাভঙ্গি সরাসরি DSA কোর্সের efficiency-centric দক্ষতার সাথে সম্পর্কিত।
- সব 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 — সব এক জায়গায়।