রেগুলার এক্সপ্রেশন ও রেগুলার ল্যাঙ্গুয়েজ
এই পাঠে যা শিখবেন
- রেগুলার এক্সপ্রেশন কী কী মৌলিক উপাদান দিয়ে গঠিত (concatenation, union, Kleene star)
- Kleene's theorem-এর বক্তব্য — DFA, NFA ও regex-এর সমতুল্যতা
- L34-এর DFA-র ভাষাকে regex দিয়ে বর্ণনা করে দুটোর উত্তর মিলিয়ে দেখা
- কেন সব ভাষা regular নয়, এবং Pumping Lemma-র ভূমিকা কী (সংক্ষেপে)
১ · রেগুলার এক্সপ্রেশনের মৌলিক উপাদান
একটি Regular ExpressionRegular Expressionএকটি প্যাটার্ন যা কয়েকটি মৌলিক নিয়ম (খালি স্ট্রিং, একক সিম্বল, concatenation, union, Kleene star) ব্যবহার করে স্ট্রিং-এর একটি সেট (ভাষা) বর্ণনা করে। নিচের মৌলিক উপাদান থেকে তৈরি হয় —
শুধু খালি স্ট্রিং-এর ভাষা $\{\varepsilon\}$ বর্ণনা করে
যেমন
0 বা 1 — শুধু সেই একটি সিম্বলের ভাষাA-এর একটি স্ট্রিং তারপর B-এর একটি স্ট্রিং — পরপর জোড়া লাগানো
A-এর কোনো স্ট্রিং অথবা B-এর কোনো স্ট্রিং
A-এর শূন্য বা তার বেশিবার পুনরাবৃত্তি (repetition)
২ · Kleene's theorem — তিনটি সমতুল্য দৃষ্টিভঙ্গি
একটি গভীর ফলাফল (এখানে শুধু বক্তব্য বলা হলো, প্রমাণ এই কোর্সের সুযোগের বাইরে) —
একটি ভাষা $L$ regular হয় যদি এবং কেবল যদি —
(ক) $L$ কোনো DFA দিয়ে চেনা যায় (L34), ⟺ (খ) $L$ কোনো NFA দিয়ে চেনা যায় (L34), ⟺ (গ) $L$ কোনো regular expression দিয়ে বর্ণনা করা যায়।
অর্থাৎ — DFA, NFA ও regex এই তিনটি সম্পূর্ণ ভিন্ন দেখতে টুল আসলে ঠিক একই শ্রেণীর ভাষা বর্ণনা করতে পারে, একটির চেয়ে অন্যটি বেশি শক্তিশালী নয়।
৩ · Worked উদাহরণ — L34-এর DFA-কে regex দিয়ে বর্ণনা
L34-এ আমরা দেখেছি "জোড় সংখ্যক 1" চেনার একটি দুই-স্টেট DFA। এই একই ভাষা নিচের regex দিয়ে হুবহু বর্ণনা করা যায় —
$$0^*(10^*10^*)^*$$
অনানুষ্ঠানিকভাবে পড়লে: "শুরুতে যেকোনো সংখ্যক 0, তারপর শূন্য বা তার বেশিবার — প্রতিটিতে ঠিক দুটি 1 (মাঝে ও
পাশে যেকোনো সংখ্যক 0 সহ)"। প্রতিটি (10*10*) ব্লক ঠিক দুটি নতুন 1 যোগ করে — তাই মোট 1-এর সংখ্যা
সবসময় জোড় থাকে, ঠিক DFA-র মতোই।
import re
pattern = r'0*(10*10*)*'
for s in ["1010", "111", "0", "11011"]:
matched = bool(re.fullmatch(pattern, s))
print(f"'{s}' -> regex match: {matched}")
# L34-এর DFA-র সাথে তুলনা (একই ফলাফল আসা উচিত)
delta = {
('q_even', '0'): 'q_even', ('q_even', '1'): 'q_odd',
('q_odd', '0'): 'q_odd', ('q_odd', '1'): 'q_even',
}
def run_dfa(s):
state = 'q_even'
for ch in s:
state = delta[(state, ch)]
return state == 'q_even'
print()
for s in ["1010", "111", "0", "11011"]:
dfa_result = run_dfa(s)
regex_result = bool(re.fullmatch(pattern, s))
print(f"'{s}': DFA={dfa_result}, regex={regex_result}, একমত={dfa_result == regex_result}")
re মডিউল, grep, এবং প্রতিটি টেক্সট এডিটরের "find & replace" আসলে এই একই
তত্ত্বের একটি সমৃদ্ধতর, বাস্তবিক (extended) বাস্তবায়ন — যদিও বাস্তব regex ইঞ্জিনে backreference-এর মতো কিছু
ফিচার যোগ করা হয়েছে যা কঠোরভাবে regular নয়, মূল ইঞ্জিনের ভিত্তি এই একই DFA/NFA তত্ত্ব।
৪ · সব ভাষা regular নয়
একটি গুরুত্বপূর্ণ সীমাবদ্ধতা: সব ভাষা DFA/regex দিয়ে বর্ণনা করা যায় না। ক্লাসিক উদাহরণ — balanced
parentheses (যেমন "(()())" বৈধ, কিন্তু "(()" নয়) — এই ভাষা চেনার জন্য
"এখন পর্যন্ত কতগুলো খোলা bracket আছে" তা গণনা করতে হয়, এবং এই গণনা যেকোনো সীমা ছাড়িয়ে বাড়তে পারে।
কিন্তু একটি DFA-র স্টেট সংখ্যা সসীম ও ফিক্সড — তাই এটি অসীম সংখ্যক ভিন্ন "কাউন্ট" আলাদা করতে পারে না।
একটি ভাষা non-regular তা প্রমাণ করার একটি প্রমিত টুল আছে — Pumping LemmaPumping Lemmaএকটি ভাষা non-regular তা প্রমাণ করার জন্য ব্যবহৃত একটি টেকনিক্যাল টুল — এই কোর্সে শুধু এর অস্তিত্বের কথা উল্লেখ করা হলো, বিস্তারিত ডেরিভেশন কভার করা হয়নি। — এই কোর্সে আমরা এর বিস্তারিত ডেরিভেশনে যাচ্ছি না, শুধু এতটুকু জেনে রাখুন যে এই সীমাবদ্ধতা প্রমাণ করার একটি আনুষ্ঠানিক পদ্ধতি বিদ্যমান।
DFA, NFA ও regex — তিনটি সম্পূর্ণ ভিন্ন দেখতে বর্ণনা পদ্ধতি — আসলে একই সীমারেখার (regular languages) তিনটি মুখ। L34-L35 একসাথে দেখায় কীভাবে একটি বিমূর্ত গাণিতিক মডেল (DFA) সরাসরি একটি ব্যবহারিক টুলে (regex) রূপান্তরিত হয়, যা প্রতিদিন কোটি কোটি প্রোগ্রামার ব্যবহার করেন — প্রায়ই না জেনেই যে তারা ১৯৫০-এর দশকের অটোমাটা তত্ত্ব ব্যবহার করছেন।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১
regex 0*(10*10*)*-এ শুরুর 0*-টি না থাকলে কী সমস্যা হতো?
শুরুর 0* ছাড়া প্যাটার্নটি হতো (10*10*)*, যা এখনো খালি স্ট্রিং এবং "1" দিয়ে শুরু
হওয়া বৈধ স্ট্রিং মেলে, কিন্তু "0" দিয়ে শুরু হওয়া কোনো স্ট্রিং মেলাতে পারবে না যদি না তার
আগে অন্তত একটি সম্পূর্ণ (10*10*) ব্লক থাকে। যেমন ইনপুট "0" (একাই) মিলবে না, যদিও
এতে শূন্যটি 1 আছে (জোড়) — DFA অনুযায়ী এটি accept হওয়া উচিত।
তাই শুরুর 0* জরুরি — এটি নিশ্চিত করে যে স্ট্রিং যেকোনো সংখ্যক 0 দিয়ে শুরু হতে পারে, প্রথম 1
আসার আগে। কোড সেলে "0"-এর জন্য regex ও DFA উভয়ে True দেয়, যা এই সঠিকতা যাচাই করে।
প্র ০২ Kleene's theorem বলে DFA, NFA ও regex সমতুল্য — তাহলে কেন প্রোগ্রামাররা DFA না লিখে regex লেখেন?
এক্সপ্রেসিভনেস তত্ত্বে সমান হলেও, ergonomics-এ সমান নয়। একটি regex একটি কমপ্যাক্ট,
মানুষের পড়ার-উপযোগী নোটেশন — 0*(10*10*)* একলাইনে লেখা যায়, কিন্তু এর সমতুল্য DFA বর্ণনা করতে
স্টেট, ট্রানজিশন টেবিল ইত্যাদি অনেক বেশি জায়গা নেয়।
বাস্তবে, regex ইঞ্জিন (যেমন Python-এর re) পর্দার পেছনে regex-কে NFA বা DFA-তে কম্পাইল করে
চালায় — প্রোগ্রামার শুধু উচ্চ-স্তরের নোটেশন (regex) লেখেন, নিম্ন-স্তরের স্টেট মেশিন কম্পাইলার/ইন্টারপ্রেটার
স্বয়ংক্রিয়ভাবে তৈরি করে। এটি ঠিক তেমন যেমন প্রোগ্রামাররা অ্যাসেম্বলি না লিখে উচ্চ-স্তরের ভাষা লেখেন।
প্র ০৩ "balanced parentheses" কেন non-regular কিন্তু "জোড় সংখ্যক 1" regular — দুটোই তো একধরনের গণনা মনে হয়?
পার্থক্যটি সূক্ষ্ম কিন্তু গুরুত্বপূর্ণ — "জোড় সংখ্যক 1"-এ শুধু প্যারিটি (জোড়/বিজোড়, মাত্র ২টি সম্ভাব্য অবস্থা) মনে রাখলেই যথেষ্ট, যা একটি সসীম DFA-তে ২টি স্টেট দিয়ে ধারণ করা যায়।
কিন্তু "balanced parentheses"-এ শুধু জোড়/বিজোড় জানলেই যথেষ্ট নয় — ঠিক কতগুলো খোলা bracket এখনো
বন্ধ হয়নি তা জানতে হবে, এবং এই সংখ্যা ইনপুট যত লম্বা হয় তত আনবাউন্ডেডভাবে বাড়তে পারে
(যেমন "((((("-এ পাঁচটি খোলা bracket আছে, আরও লম্বা ইনপুটে আরও বেশি হতে পারে)। একটি সসীম DFA-র
স্টেট সংখ্যা ফিক্সড, তাই এটি অসীম সংখ্যক ভিন্ন "কাউন্ট" ধারণ করতে পারে না — এখানেই "সসীম" (finite) এবং
"আনবাউন্ডেড" (unbounded) গণনার মধ্যে মৌলিক পার্থক্য।
অনুশীলন
-
হাতে যাচাই করুন: regex
0*(10*10*)*স্ট্রিং"1100"-এর সাথে মেলে কি না হাতে-কলমে বিশ্লেষণ করুন, তারপর কোড চালিয়ে মিলিয়ে দেখুন।"1100"-এ দুটি 1 আছে (জোড়) — DFA অনুযায়ী accept হওয়া উচিত। বিশ্লেষণ: শুরুর0*এখানে শূন্যবার প্রয়োগ হয় (কোনো leading 0 নেই), তারপর(10*10*)ব্লকটি "1", "1", তারপর "00" মেলায় (দ্বিতীয়0*"00" ধরে নেয়) — পুরো স্ট্রিং মিলে যায়। কোড চালিয়েre.fullmatchসত্যিইTrueদেয় কি না যাচাই করুন। -
ডিজাইন করুন: "শুধু 0 দিয়ে শুরু হওয়া বাইনারি স্ট্রিং" (অর্থাৎ প্রথম ক্যারেক্টার অবশ্যই 0, বাকিটা যেকোনো কিছু) চেনার একটি regex লিখুন।
0(0|1)*— প্রথমে ঠিক একটি0, তারপর 0 বা 1-এর যেকোনো সংখ্যক পুনরাবৃত্তি। লক্ষ্য করুন খালি স্ট্রিং এখানে মেলে না (কারণ প্রথম0বাধ্যতামূলক), এবং শুধু"1..."দিয়ে শুরু হওয়া কোনো স্ট্রিংও মেলে না।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৪৪টি পাঠ পরবর্তী পাঠ থেকে একটি নতুন মডিউল শুরু — অ্যালগরিদম বিশ্লেষণ ও জটিলতা (Big-O)।
- Data Structures & Algorithms কোর্স সঙ্গী কোর্স regex ও state machine বাস্তব প্রোগ্রামে (parser, validator) কীভাবে ব্যবহৃত হয় তা দেখতে DSA কোর্সটিও দেখুন।
- সব Courses দেখুন ABCL TECH C, C++, Python, Java, JavaScript, DSA, DBMS, HTML & CSS ও Discrete Mathematics — সব এক জায়গায়।