এই পর্বে যা শিখবেন
- যুক্তির নিয়ম দিয়ে কোডের জটিল শর্ত সরল করা আর সরল রূপটি সব সারিতে যাচাই করা
- Short-circuit evaluation আর guard clause-এর পেছনের যুক্তি, আর কোথায় এটি গণিতের ∧ থেকে আলাদা
- নিয়মের তালিকাকে SAT প্রশ্নে রূপান্তর করে বিরোধ আর তার ক্ষুদ্রতম কারণ খোঁজা
- Sudoku আর সময়সূচির মতো সমস্যাকে CNF-এ লেখা, variable আর clause গোনা, “ঠিক একটি” encoding-এর প্রমাণ
- DPLL হাতে চালানো, তার সঠিকতার প্রমাণ, আর আসল SAT ও SMT solver (PySAT, Z3) দিয়ে বাস্তব যাচাই
১ · কোডের শর্তে যুক্তি — সরল করা, short-circuit আর guard clause
এই মডিউলের আগের ছয়টি পর্বে আমরা যুক্তির ভাষা শিখেছি: proposition আর truth table (পর্ব ০২), logical equivalence-এর নিয়ম (পর্ব ০৩), quantifier (পর্ব ০৪ আর পর্ব ০৫), অনুমানের নিয়ম (পর্ব ০৬)। এই শেষ পর্বে দেখব, সেই ভাষা প্রতিদিনের কাজে কোথায় লাগে: প্রোগ্রামের শর্তে, নিয়মের তালিকায়, ধাঁধায়, আর এমন যন্ত্রে যা লাখো variable-এর সূত্র সমাধান করে।
শুরু করি বাজারঘরের চেকআউট কোড থেকে। একজন প্রোগ্রামার ফ্রি ডেলিভারির শর্ত লিখেছেন, কয়েকবার জোড়াতালি দেওয়ার পর যা দাঁড়িয়েছে এরকম:
if not (not is_member and total < 1000) and not (is_remote and not is_member):
fee = 0
লাইনটি পড়ে কেউ এক নজরে বলতে পারবেন না, কে ফ্রি ডেলিভারি পান। যুক্তির ভাষায় আনি: m = “ক্রেতা সদস্য”, b = “অর্ডার ১০০০ টাকা বা বেশি” (তাই total < 1000 মানে ¬b), r = “ঠিকানা দূরের জেলায়”। শর্তটি তখন
¬(¬m ∧ ¬b) ∧ ¬(r ∧ ¬m)
পর্ব ০৩-এর নিয়মগুলো দিয়ে এটিকে ধাপে ধাপে সরল করি। প্রতিটি ধাপে ঠিক একটি নিয়ম খাটবে, আর পাশে লেখা থাকবে কোন নিয়ম। “পরে” চাপুন:
1কোড → সূত্র2De Morgan3Double negation4Commutative5Distributive6সূত্র → কোড
| সূত্র | |
| চিহ্ন |
- কোড থেকে সূত্র।
not= ¬,and= ∧.total < 1000হলো ¬b. মোট ৮টি চিহ্ন (৫টি ¬, ৩টি ∧): পড়তে কঠিন হওয়ার কারণ এখানেই। - De Morgan, প্রথম অংশে। ¬(x ∧ y) ≡ ¬x ∨ ¬y. তাই ¬(¬m ∧ ¬b) ≡ ¬¬m ∨ ¬¬b. বন্ধনীর বাইরের ¬ ভেতরে ঢুকলে ∧ বদলে ∨ হয়।
- Double negation. ¬¬m ≡ m, ¬¬b ≡ b. প্রথম অংশ এখন m ∨ b: “সদস্য অথবা ১০০০ টাকার বেশি”।
- দ্বিতীয় অংশে আবার De Morgan আর double negation. ¬(r ∧ ¬m) ≡ ¬r ∨ ¬¬m ≡ ¬r ∨ m: “দূরে নয় অথবা সদস্য”।
- Commutative. ¬r ∨ m ≡ m ∨ ¬r. দুই বন্ধনীতেই এখন m প্রথমে, যাতে পরের নিয়মটি চোখে পড়ে।
- Distributive (উল্টো দিকে). (m ∨ x) ∧ (m ∨ y) ≡ m ∨ (x ∧ y). তাই (m ∨ b) ∧ (m ∨ ¬r) ≡ m ∨ (b ∧ ¬r). চিহ্ন ৮ থেকে ৩-এ নামল।
- আবার কোডে।
if is_member or (total >= 1000 and not is_remote):— বাংলায়: “সদস্য হলে সবসময় ফ্রি; নইলে ১০০০ টাকার অর্ডার আর কাছের ঠিকানা হলে ফ্রি।” এবার নিয়মটি মার্কেটিং দলকে পড়ে শোনানো যায়।
প্রতিটি ধাপ একটি প্রমাণিত সমতা, তাই শেষ রূপটি প্রথমটির সমান: এটিই প্রমাণ। তবু পরীক্ষা করে নেওয়া সস্তা। নিচের ব্লক ৮টি সারিতে দুটি রূপ মেলায়, একটি সাধারণ ভুল “সরলীকরণ” ধরে, আর short-circuit কীভাবে কাজ করে তা দেখায়। প্রথমবার চালালে ব্রাউজারে Python (Pyodide) নামতে কয়েক সেকেন্ড লাগে, পরে দ্রুত চলে।
from itertools import product
imp = lambda a, b: (not a) or b
# m = সদস্য, b = অর্ডার ১০০০ টাকা বা বেশি, r = দূরের জেলা
messy = lambda m, b, r: not (not m and not b) and not (r and not m)
clean = lambda m, b, r: m or (b and not r)
wrong = lambda m, b, r: (m and b) and not (r and not m) # De Morgan ভুল
for name, f in [("clean", clean), ("wrong", wrong)]:
bad = [row for row in product([True, False], repeat=3)
if messy(*row) != f(*row)]
print(f"messy vs {name}: {8 - len(bad)} of 8 rows agree", bad)
# nested if ≡ একসঙ্গে শর্ত: p → (q → r) ≡ (p ∧ q) → r
print("exportation:", all(imp(p, imp(q, r)) == imp(p and q, r)
for p, q, r in product([True, False], repeat=3)))
def is_member(cid): # ধীর কাজের ভান: ডাটাবেস খোঁজা
print(" looked up member", cid)
return cid == 7
for total in (1200, 300):
print("total", total)
print(" free:", total >= 1000 or is_member(7))
order = None # None হলে .get() চালানোই হয় না
print("guarded:", order is not None and order.get("total", 0) > 500)আসল আউটপুট (ব্রাউজারের Python):
messy vs clean: 8 of 8 rows agree []
messy vs wrong: 5 of 8 rows agree [(True, False, True), (True, False, False), (False, True, False)]
exportation: True
total 1200
free: True
total 300
looked up member 7
free: True
guarded: False
সরল রূপ ৮টি সারিতেই মেলে। “wrong” রূপে কেউ ¬(¬m ∧ ¬b)-কে m ∧ b লিখেছেন, যা De Morgan নয়; তিনটি সারিতে ফল আলাদা, যেমন (True, False, …): সদস্য অথচ ১০০০ টাকার কম অর্ডার, যিনি আসলে ফ্রি ডেলিভারি পাওয়ার কথা। দ্বিতীয় অংশে exportation নিয়ম যাচাই হলো। তৃতীয় অংশে লক্ষ করুন: ১২০০ টাকায় is_member একবারও ডাকা হয়নি, কারণ or-এর বাঁ দিক সত্য হলেই উত্তর জানা হয়ে যায়। শেষ লাইনে order None, তাই and-এর ডান দিক চালানোই হয়নি; না হলে AttributeError হতো।
Short-circuit: কোডের ∧ গণিতের ∧-এর চেয়ে একটু বেশি
Python, C, Java, JavaScript-এর মতো বেশিরভাগ ভাষায় and/&& আর or/|| short-circuit করে: বাঁ দিক দেখেই উত্তর জানা গেলে ডান দিক হিসাবই হয় না। Truth table-এর ভাষায় এটি নিরাপদ, কারণ x = F হলে x ∧ y = F, y যা-ই হোক। কিন্তু একটি সূক্ষ্ম পার্থক্য আছে। গণিতে x ∧ y ≡ y ∧ x (commutative). কোডে order is not None and order.total > 500 নিরাপদ, অথচ ক্রম উল্টালে order.total > 500 and order is not None None-এর ক্ষেত্রে error দেয়। কারণ কোডের একটি অংশ “অসংজ্ঞায়িত” হতে পারে (error, অসীম লুপ, ধীর ডাটাবেস কল), যা দুই-মানের truth table-এ নেই। নিয়ম: যে শর্ত পরের অংশকে নিরাপদ করে, সেটি বাঁয়ে লিখুন। Python-এর শর্ত লেখার বিস্তারিত Python প্রোগ্রামিং কোর্সের পর্ব ১১-এ।
Guard clause আর exportation নিয়ম
অনেক স্তরের nested if পড়া কঠিন। Guard clause মানে ফাংশনের শুরুতেই বাদ পড়ার ক্ষেত্রগুলো সরিয়ে দেওয়া:
# আগে: তিন স্তরের nesting
if order is not None:
if order.paid:
if not order.cancelled:
ship(order)
# পরে: guard clause
if order is None or not order.paid or order.cancelled:
return
ship(order)
দুটি রূপ যে একই কাজ করে, সেটি যুক্তির দুটি নিয়মের ফল। প্রথমত, nested if মানে p → (q → (s → ship)), আর exportation নিয়মে p → (q → r) ≡ (p ∧ q) → r, তাই পুরোটা (p ∧ q ∧ s) → ship. দ্বিতীয়ত, guard-এর শর্ত ¬p ∨ ¬q ∨ ¬s, De Morgan-এ যা ¬(p ∧ q ∧ s): ঠিক যে ক্ষেত্রে ship হবে না। কোড পরিষ্কার রাখার আরও কৌশল সফটওয়্যার ইঞ্জিনিয়ারিং ও Git কোর্সের পর্ব ৪২-এ।
কোডে নিয়ম খাটানোর সময় একটি ফাঁদ: not (a < 5)-কে a > 5 লেখা। সঠিক উল্টো হলো a >= 5; সমান হওয়ার ক্ষেত্রটি হারিয়ে যায়। আর float-এ NaN থাকলে not (x < 5) আর x >= 5-ও আলাদা, কারণ NaN-এর সঙ্গে সব তুলনাই False. সরল করার পর সীমানার মানগুলো (৯৯৯, ১০০০, ১০০১) আলাদা করে পরীক্ষা করুন।
২ · Specification — পরস্পরবিরোধী নিয়ম আর তার ক্ষুদ্রতম কারণ
পর্ব ০২-এ দেখেছেন, একগুচ্ছ নিয়ম consistent যদি অন্তত একটি মান-বসানো সবকটিকে একসঙ্গে সত্য করে। অর্থাৎ specification-এর consistency প্রশ্নটি আসলে নিয়মগুলোর ∧ satisfiable কি না, সেই প্রশ্ন। এবার দুটি নতুন প্রশ্ন তুলি: কখন বিরোধ হতেই পারে না, আর বিরোধ হলে দোষী কোন নিয়মগুলো।
একগুচ্ছ clause-এর প্রতিটিতে যদি অন্তত একটি negative literal (¬x রূপ) থাকে, তবে সব variable-কে F বসালে প্রতিটি clause সত্য হয়। তাই এমন নিয়মের তালিকা সবসময় consistent.
প্রমাণ:
- সব variable = F ধরি। তাহলে প্রতিটি negative literal ¬x-এর মান ¬F = T.
- একটি clause হলো literal-গুলোর ∨; তার অন্তত একটি literal T হলেই clause T.
- ধরে নেওয়া হয়েছে প্রতিটি clause-এ অন্তত একটি negative literal আছে, যা ধাপ ১ অনুযায়ী T. তাই প্রতিটি clause T, আর তাদের ∧-ও T. ∎
এর অর্থ কাজের: “h হলে ¬u” (¬h ∨ ¬u) বা “p হলে u” (¬p ∨ u) ধরনের নিষেধ আর শর্তসাপেক্ষ নিয়ম যত খুশি থাকুক, “কিছুই না করা” সবসময় একটি সমাধান। বিরোধ জন্মাতে হলে অন্তত একটি বাধ্যতামূলক নিয়ম লাগে, যার সব literal positive: “ক্রেতা প্রিমিয়াম” (p), বা “প্রতিটি অর্ডার অন্তত একটি স্লটে যাবে” (x₁ ∨ x₂ ∨ x₃)। তাই নিয়ম পর্যালোচনার সময় বাধ্যতামূলক নিয়মগুলোকে আলাদা চোখে দেখুন।
বাজারঘরের ডেলিভারি দলের নিয়ম, যেখানে u = “আজই ডেলিভারি”, c = “ক্যাশ অন ডেলিভারি”, h = “ঠিকানা হাওর এলাকায়”, p = “ক্রেতা প্রিমিয়াম সদস্য”:
- R1: হাওর এলাকায় আজই ডেলিভারি নয়: h → ¬u
- R2: প্রিমিয়াম সদস্য সবসময় আজই পান: p → u
- R3: প্রিমিয়াম সদস্যরা আগে টাকা দেন, ক্যাশ অন ডেলিভারি নয়: p → ¬c
- R4: হাওর এলাকায় শুধু ক্যাশ অন ডেলিভারি: h → c
- F1 আর F2 (একটি অর্ডারের ঘটনা): ঠিকানা হাওরে (h), ক্রেতা প্রিমিয়াম (p)
R1–R4-এর প্রতিটিতে negative literal আছে, তাই উপপাদ্য ২.১ অনুযায়ী এরা নিজেরা consistent. বিরোধ এলে আসবে F1, F2-এর মতো বাধ্যতামূলক তথ্য থেকে। কোন নিয়মগুলো একসঙ্গে বিরোধ ঘটায়, তা খুঁজতে সব উপসেট ছোট থেকে বড় ক্রমে পরীক্ষা করি। যে উপসেট নিজে unsatisfiable কিন্তু যার যেকোনো একটি নিয়ম বাদ দিলে satisfiable, তাকে বলে minimal unsat core.
from itertools import product, combinations
# u = আজই ডেলিভারি, c = ক্যাশ অন ডেলিভারি,
# h = ঠিকানা হাওর এলাকায়, p = ক্রেতা প্রিমিয়াম সদস্য
imp = lambda a, b: (not a) or b
rules = {
"R1 h -> not u": lambda u, c, h, p: imp(h, not u),
"R2 p -> u": lambda u, c, h, p: imp(p, u),
"R3 p -> not c": lambda u, c, h, p: imp(p, not c),
"R4 h -> c": lambda u, c, h, p: imp(h, c),
"F1 h": lambda u, c, h, p: h,
"F2 p": lambda u, c, h, p: p,
}
def models(names):
return [row for row in product([True, False], repeat=4)
if all(rules[n](*row) for n in names)]
print("all 6:", len(models(rules)), "assignments")
for k in range(1, 7): # ছোট থেকে বড় উপসেট
cores = [g for g in combinations(rules, k) if not models(g)]
if cores:
print(f"smallest conflicting sets, size {k}:")
for g in cores:
print(" ", [n[:2] for n in g])
break
for n in rules: # একটি করে বাদ দিয়ে দেখা
rest = [m for m in rules if m != n]
print(f"without {n[:2]}: {len(models(rest))} assignment(s)")আসল আউটপুট (ব্রাউজারের Python):
all 6: 0 assignments
smallest conflicting sets, size 4:
['R1', 'R2', 'F1', 'F2']
['R3', 'R4', 'F1', 'F2']
without R1: 0 assignment(s)
without R2: 0 assignment(s)
without R3: 0 assignment(s)
without R4: 0 assignment(s)
without F1: 1 assignment(s)
without F2: 1 assignment(s)
ছয়টি নিয়ম একসঙ্গে ১৬টি সারির কোনোটিতেই সত্য নয়। ক্ষুদ্রতম বিরোধ দুটি, আলাদা আলাদা: হাওরের প্রিমিয়াম ক্রেতা একসঙ্গে “আজই” আর “আজ নয়” (R1, R2), আবার একসঙ্গে “ক্যাশ” আর “ক্যাশ নয়” (R3, R4). তাই R1 বাদ দিলেও কাজ হয় না: অন্য বিরোধটি থেকে যায়। এটিই বাস্তবের শিক্ষা: একটি বিরোধ সারানোর পর আবার পরীক্ষা করুন। সমাধান নিয়মের মালিকের হাতে, যেমন R2-কে লেখা “প্রিমিয়াম সদস্য আজই পান, হাওর এলাকা বাদে”: (p ∧ ¬h) → u. এখানে ২⁶ = ৬৪টি উপসেট আর ১৬টি সারি; ৩০০টি নিয়মে এই brute force অচল, তখন solver লাগে (অংশ ৫)।
সফটওয়্যারের requirement সংগ্রহ আর পর্যালোচনার পুরো প্রক্রিয়া সফটওয়্যার ইঞ্জিনিয়ারিং ও Git কোর্সের পর্ব ২০-এ আছে। এখানে আমরা দেখলাম তার গাণিতিক কেন্দ্রটুকু: নিয়মকে সূত্রে লিখলে “পরস্পরবিরোধী কি না” প্রশ্নটি যন্ত্রকে দেওয়া যায়।
৩ · ধাঁধাকে clause-এ লেখা — Sudoku আর “ঠিক একটি”
Sudoku, n-queens, ক্লাসের রুটিন, ডেলিভারির স্লট: সবগুলোই constraint satisfaction সমস্যা। কিছু ঘরে মান বসাতে হবে, এমনভাবে যাতে সব শর্ত মানা হয়। এদের SAT-এ আনার কৌশলকে বলে SAT encoding: প্রতিটি “এই ঘরে এই মান কি না” প্রশ্নের জন্য একটি সত্য-মিথ্যা variable, আর প্রতিটি শর্তের জন্য কয়েকটি clause. ফলে পাওয়া যায় CNF সূত্র, যা যেকোনো SAT solver নেয়।
4 × 4 Sudoku নিই: প্রতিটি সারি, কলাম আর 2 × 2 বাক্সে 1–4 ঠিক একবার। Variable xr,c,v মানে “সারি r, কলাম c-এর ঘরে v আছে”। r, c, v প্রতিটির ৪টি মান, তাই 4 × 4 × 4 = ৬৪টি variable. প্রতিটি নিয়মই “ঠিক একটি” ধরনের: প্রতিটি ঘরে ঠিক একটি সংখ্যা, প্রতিটি সারিতে প্রতিটি সংখ্যা ঠিক একবার, ইত্যাদি। একটি নিয়ম clause-এ লিখি:
1x1c = x(1,c,3)2অন্তত ১টি: x11 ∨ … ∨ x143বড়জোর ১টি: ¬x11 ∨ ¬x124… বাকি ৫টি জোড়া5মোট: ১ + ৬ = ৭
| এই নিয়মে clause | |
| পুরো 4 × 4-এ |
- Variable বাছাই। নিয়মটি সারি ১-এর চারটি ঘর নিয়ে: x(1,1,3), x(1,2,3), x(1,3,3), x(1,4,3). সংক্ষেপে x11, x12, x13, x14 লিখি; x1j = “সারি ১, কলাম j-তে 3”.
- “ঠিক একটি” ভাঙা। ঠিক একটি সত্য = (অন্তত একটি সত্য) ∧ (বড়জোর একটি সত্য)। CNF-এ দুটোকে আলাদা clause-এ লিখব।
- অন্তত একটি। একটিমাত্র clause: x11 ∨ x12 ∨ x13 ∨ x14. চারটিই মিথ্যা হলে এটি মিথ্যা; অর্থাৎ সারিতে 3 কোথাও না থাকা নিষেধ।
- বড়জোর একটি, একটি জোড়ায়। “x11 আর x12 দুটো একসঙ্গে নয়” = ¬(x11 ∧ x12), De Morgan-এ ¬x11 ∨ ¬x12. এটি একটি clause, দুটি negative literal নিয়ে।
- সব জোড়া। চারটি ঘর থেকে জোড়া C(4, 2) = ৬টি: (1,2), (1,3), (1,4), (2,3), (2,4), (3,4). প্রতিটির জন্য একটি clause. এই নিয়মে মোট ১ + ৬ = ৭টি clause.
- পুরো ধাঁধা। “ঠিক একটি” গ্রুপ চার ধরনের: ঘর, সারি-সংখ্যা, কলাম-সংখ্যা, বাক্স-সংখ্যা; প্রতিটি ১৬টি, মোট ৬৪টি গ্রুপ, প্রতিটিতে ৭টি clause: ৪৪৮. 9 × 9-এ গ্রুপ 4 × 81 = 324, প্রতিটিতে 1 + C(9, 2) = 37: মোট ১১,৯৮৮। দেওয়া সংখ্যাগুলো যোগ হয় এক-literal-এর clause হিসেবে।
x1, …, xn variable-এর জন্য C(n, 2)টি clause ¬xi ∨ ¬xj (1 ≤ i < j ≤ n) সবকটি একসঙ্গে সত্য হয় ঠিক তখন, যখন x1, …, xn-এর বড়জোর একটি সত্য। এর সঙ্গে x1 ∨ … ∨ xn যোগ করলে পাওয়া যায় “ঠিক একটি”, মোট 1 + C(n, 2)টি clause-এ।
প্রমাণ (দুই দিক আলাদা করে):
- (⇐) বড়জোর একটি সত্য হলে সব clause সত্য। যেকোনো জোড়া i < j নিন। বড়জোর একটি সত্য বলে xi আর xj দুটোই সত্য নয়; তাই অন্তত একটি মিথ্যা, ধরুন xi. তখন ¬xi = T, আর clause ¬xi ∨ ¬xj সত্য।
- (⇒) সব clause সত্য হলে বড়জোর একটি সত্য। Contrapositive দিয়ে দেখাই: ধরুন দুটি বা বেশি সত্য, যেমন xi = xj = T, i < j. তখন clause ¬xi ∨ ¬xj = F ∨ F = F, অর্থাৎ অন্তত একটি clause মিথ্যা।
- “ঠিক একটি” = “অন্তত একটি” ∧ “বড়জোর একটি”; প্রথমটি একটি clause, দ্বিতীয়টি ধাপ ১–২ অনুযায়ী C(n, 2)টি clause. মোট 1 + C(n, 2). ∎
C(n, 2) = n(n − 1)/2 দ্রুত বাড়ে: n = 100-এ ৪,৯৫০টি clause. তাই বড় গ্রুপে solver-রা অতিরিক্ত সাহায্যকারী variable দিয়ে প্রায় রৈখিক সংখ্যক clause-এর encoding ব্যবহার করে (যেমন sequential counter); অংশ ৫-এ PySAT-এ সেটি দেখবেন। Combination-এর গণনা পর্ব ৩৩-এ বিস্তারিত আসবে। নিচের ব্লক যেকোনো k-এর জন্য k² × k² Sudoku encode করে clause গোনে, আর একটি পূরণ করা গ্রিড সব clause মানে কি না যাচাই করে।
from itertools import combinations
def encode(k): # k = 2 → 4×4, k = 3 → 9×9
n = k * k
var = lambda r, c, v: n * n * r + n * c + v + 1
groups = []
for r in range(n):
for c in range(n):
groups.append([var(r, c, v) for v in range(n)]) # ঘর
for v in range(n):
for i in range(n):
groups.append([var(i, c, v) for c in range(n)]) # সারি
groups.append([var(r, i, v) for r in range(n)]) # কলাম
br, bc = k * (i // k), k * (i % k)
groups.append([var(br + a, bc + b, v)
for a in range(k) for b in range(k)]) # বাক্স
cnf = []
for g in groups:
cnf.append(g) # অন্তত একটি
cnf += [[-a, -b] for a, b in combinations(g, 2)] # বড়জোর একটি
return n, var, cnf
for k in (2, 3, 4):
n, var, cnf = encode(k)
print(f"{n}x{n}: variables {n**3}, clauses {len(cnf)}")
n, var, cnf = encode(2)
grid = ["1234", "3412", "2143", "4321"] # একটি পূরণ করা 4×4
true = {var(r, c, int(grid[r][c]) - 1) for r in range(4) for c in range(4)}
holds = lambda cl: any((l > 0) == (abs(l) in true) for l in cl)
print("valid grid satisfies all:", all(holds(cl) for cl in cnf))
grid[0] = "1243" # এক সারিতে দুটি ঘর অদলবদল
true = {var(r, c, int(grid[r][c]) - 1) for r in range(4) for c in range(4)}
print("after a swap, violated clauses:", sum(not holds(cl) for cl in cnf))আসল আউটপুট (ব্রাউজারের Python):
4x4: variables 64, clauses 448
9x9: variables 729, clauses 11988
16x16: variables 4096, clauses 123904
valid grid satisfies all: True
after a swap, violated clauses: 4
সংখ্যাগুলো সূত্র 4N²(1 + C(N, 2)) মেলায়, যেখানে N = গ্রিডের পাশ: N = 16-এ 4 × 256 × 121 = 123,904. দ্বিতীয় অংশটি একটি মৌলিক সত্য দেখায়: একটি প্রস্তাবিত উত্তর যাচাই করা সহজ, প্রতিটি clause একবার দেখলেই হয়। প্রথম সারির শেষ দুটি ঘর অদলবদল করলে (1243) কলাম ৩-এ 4 দুবার আর 3 নেই, কলাম ৪-এ 3 দুবার আর 4 নেই: ঠিক ৪টি clause ভাঙে। উত্তর খোঁজা কঠিন হতে পারে, যাচাই সহজ: এই অসমতাই অংশ ৭-এর NP শ্রেণির মূল ধারণা।
পুরো প্রক্রিয়াটি এক ছবিতে: বাস্তব সমস্যা থেকে CNF, solver, তারপর দুই রকম উত্তর।
SAT দিয়ে সমস্যা সমাধানের পথ
Encode → solve → decode, and check the answer independently
৪ · DPLL — বাধ্য মান ছড়ানো, অনুমান আর পিছিয়ে আসা
৭২৯টি variable-এর সব সারি দেখা অসম্ভব: 2729 একটি ২২০ অঙ্কের সংখ্যা। Solver তাহলে কী করে? ভিত্তিটি DPLL পদ্ধতি, যার নাম Davis, Putnam, Logemann আর Loveland-এর নামের আদ্যক্ষর থেকে। তিনটি ধারণা:
- সরল করা: একটি literal ℓ সত্য ধরলে যে clause-এ ℓ আছে সেটি সত্য হয়ে বাদ যায়, আর যে clause-এ ¬ℓ আছে সেখান থেকে ¬ℓ মুছে যায় (সেটি আর সাহায্য করতে পারবে না)। ফলের সূত্রকে লেখা হয় F|ℓ.
- Unit propagation: কোনো clause-এ একটিই literal বাকি থাকলে (unit clause) তার মান বাধ্য, অনুমানের দরকার নেই। একটি বাধ্য মান অন্য clause-কে unit বানায়, সেখান থেকে আরেকটি: শিকলের মতো ছড়ায়।
- শাখা আর backtracking: বাধ্য মান না থাকলে একটি variable বেছে T ধরে এগোন; কোনো clause-এর সব literal মুছে গেলে (empty clause) বিরোধ, তখন পিছিয়ে এসে F চেষ্টা করুন।
নিচের পাঁচটি clause-এর সূত্রে DPLL হাতে চালাই (a, b, c, d চারটি variable):
(a ∨ b) ∧ (¬a ∨ c) ∧ (¬c ∨ d) ∧ (¬b ∨ ¬d) ∧ (¬a ∨ ¬d)
C1a ∨ bC2¬a ∨ cC3¬c ∨ dC4¬b ∨ ¬dC5¬a ∨ ¬d
| a b c d | |
| C1 a ∨ b | |
| C2 ¬a ∨ c | |
| C3 ¬c ∨ d | |
| C4 ¬b ∨ ¬d | |
| C5 ¬a ∨ ¬d |
- শুরু। প্রতিটি clause-এ দুটি literal, কোনো unit clause নেই, কোনো empty clause-ও নেই। তাই একটি variable-এ অনুমান করতে হবে। প্রথম variable a বাছি।
- অনুমান a = T. C1-এ a আছে, তাই C1 সত্য। C2 থেকে ¬a মুছে যায়, থাকে শুধু c: unit. C5 থেকেও ¬a মুছে থাকে ¬d: unit.
- Unit propagation: c = T. C2 unit (c), তাই c = T বাধ্য। C2 সত্য হলো; C3 থেকে ¬c মুছে থাকে d: আরেকটি unit.
- d = T, আর বিরোধ। C3 থেকে d = T বাধ্য। কিন্তু C5-এ বাকি ছিল শুধু ¬d; এখন সেটিও মিথ্যা, C5 খালি হয়ে গেল। এই শাখায় কোনো সমাধান নেই। লক্ষ করুন, b-তে কখনো হাত দিতে হয়নি: a = T-এর ৮টি সারি একসঙ্গে বাদ গেল।
- Backtrack: a = F. a = T থেকে আসা সব মান (c, d) মুছে ফেলি। a = F ধরলে C2 আর C5 সত্য (¬a আছে), আর C1 থেকে a মুছে থাকে b: unit.
- Unit propagation: b = T. C1 সত্য। C4 থেকে ¬b মুছে থাকে ¬d: unit.
- d = F. C4 সত্য। C3 থেকে d মুছে থাকে ¬c: unit.
- c = F. C3 সত্য। আর কোনো খোলা clause নেই: সূত্রটি satisfiable, model a = F, b = T, c = F, d = F.
- যাচাই। C1: F ∨ T, C2: T ∨ F, C3: T ∨ F, C4: F ∨ T, C5: T ∨ T — সবই সত্য। Brute force-এ ১৬টি সারির ঠিক এই একটিই model; DPLL মাত্র একটি অনুমান আর একটি backtrack-এ সেটি পেয়েছে।
এবার পদ্ধতিটি কেন সঠিক, তার প্রমাণ। এখানে F একটি CNF সূত্র, আর “F satisfiable” মানে কোনো মান-বসানোয় F-এর সব clause সত্য।
(ক) F satisfiable ⇔ F|x অথবা F|¬x satisfiable (যেকোনো variable x-এর জন্য)। (খ) F-এ unit clause (ℓ) থাকলে F satisfiable ⇔ F|ℓ satisfiable. (গ) F-এ empty clause থাকলে F unsatisfiable.
প্রমাণ:
- (ক) F-এর যেকোনো model-এ x হয় T নয় F. x = T হলে: যে clause-এ x আছে তা সত্য, আর যে clause-এ ¬x আছে তার সত্যতা আসে অন্য কোনো literal থেকে (¬x মিথ্যা); তাই একই মান-বসানো F|x-এর প্রতিটি clause সত্য করে। x = F হলে একইভাবে F|¬x. উল্টো দিকে, F|x-এর একটি model নিয়ে তাতে x = T যোগ করলে মুছে-ফেলা clause-গুলো (x আছে বলে) আর ছোট-করা clause-গুলো (বাকি অংশ সত্য বলে) সবই সত্য, তাই F সত্য; F|¬x-এর ক্ষেত্রেও একই যুক্তি।
- (খ) F-এর যেকোনো model-এ unit clause (ℓ) সত্য, অর্থাৎ ℓ = T. তাই (ক)-এর দুই শাখার মধ্যে ¬ℓ শাখায় কোনো model নেই, আর F satisfiable ⇔ F|ℓ satisfiable.
- (গ) Empty clause-এ কোনো literal নেই, তাই কোনো মান-বসানোয় তা সত্য হতে পারে না; ∧-এ একটি মিথ্যা থাকলে পুরো F মিথ্যা। ∎
n-টি variable-এর যেকোনো CNF সূত্রে DPLL থামে, আর “SAT” বলে ঠিক তখন, যখন সূত্রটি satisfiable; SAT বললে যে মান-বসানো দেয়, তা একটি model.
প্রমাণ (খোলা variable-এর সংখ্যা k-এর ওপর strong induction; পূর্ণ নিয়ম পর্ব ১১-এ):
- ভিত্তি, k = 0: কোনো variable বাকি নেই, তাই প্রতিটি clause হয় সব literal মুছে empty, নয় আগেই সত্য হয়ে বাদ। Empty clause থাকলে DPLL UNSAT বলে, Lemma (গ) অনুযায়ী ঠিক; না থাকলে সূত্র খালি, সব clause সত্য, DPLL SAT বলে: ঠিক।
- ধরে নিই: k-এর চেয়ে কম খোলা variable-এর সব সূত্রে দাবিটি সত্য।
- ধাপ: k খোলা variable-এ DPLL হয় একটি unit ℓ বসায়, নয় x-এ শাখা করে। প্রতিটি ক্ষেত্রে নতুন সূত্রে খোলা variable অন্তত একটি কমে, তাই induction-এর অনুমান খাটে: প্রতিটি পুনরাবৃত্ত ডাক থামে আর সঠিক উত্তর দেয়।
- Unit-এর ক্ষেত্রে Lemma (খ) বলে F আর F|ℓ একসঙ্গে satisfiable বা unsatisfiable, তাই উত্তর ঠিক। শাখার ক্ষেত্রে DPLL প্রথমে F|x, ব্যর্থ হলে F|¬x চেষ্টা করে; Lemma (ক) বলে F satisfiable ঠিক তখন, যখন দুটির অন্তত একটি satisfiable. তাই দুটোই ব্যর্থ হলে UNSAT ঠিক, আর কোনো একটি SAT হলে তার model-এ শাখার literal যোগ করে F-এর model পাওয়া যায়। ∎
প্রমাণটি খরচ নিয়ে কিছু বলে না: খারাপ ক্ষেত্রে শাখার গাছে প্রায় 2ⁿটি পাতা থাকতে পারে। নিচের ব্লকে উপরের stepper-এর হুবহু DPLL, প্রতিটি সিদ্ধান্ত ছাপাসহ। দ্বিতীয় ডাকে একটি ষষ্ঠ clause (a ∨ ¬b) যোগ করা হয়েছে; দেখুন কী হয়। Literal লেখা হয়েছে সংখ্যায়, যেমন SAT solver-দের প্রচলিত DIMACS রীতিতে: 1 = a, −1 = ¬a.
def simplify(cnf, lit): # lit সত্য ধরে সূত্র ছোট করা
out = []
for cl in cnf:
if lit in cl:
continue # clause সত্য হয়ে গেছে, বাদ
out.append([l for l in cl if l != -lit]) # মিথ্যা literal মুছে ফেলা
return out
def dpll(cnf, assign, depth=0):
pad = " " * depth
while True: # unit propagation
if [] in cnf:
print(pad + "conflict: empty clause, back")
return None
units = [cl[0] for cl in cnf if len(cl) == 1]
if not units:
break
u = units[0]
print(pad + f"unit {u:+d}")
assign, cnf = assign + [u], simplify(cnf, u)
if not cnf:
return assign # সব clause সত্য
x = abs(cnf[0][0]) # শাখা: প্রথম খোলা variable
for lit in (x, -x):
print(pad + f"decide {lit:+d}")
res = dpll(simplify(cnf, lit), assign + [lit], depth + 1)
if res is not None:
return res
return None
# 1=a, 2=b, 3=c, 4=d; -1 মানে ¬a
F = [[1, 2], [-1, 3], [-3, 4], [-2, -4], [-1, -4]]
for formula in (F, F + [[1, -2]]):
res = dpll(formula, [])
print("result:", res if res is not None else "UNSAT", "\n")আসল আউটপুট (ব্রাউজারের Python):
decide +1
unit +3
unit +4
conflict: empty clause, back
decide -1
unit +2
unit -4
unit -3
result: [-1, 2, -4, -3]
decide +1
unit +3
unit +4
conflict: empty clause, back
decide -1
unit +2
conflict: empty clause, back
result: UNSAT
প্রথম ডাক stepper-এর প্রতিটি ধাপ মেলায়: [−1, 2, −4, −3] মানে a = F, b = T, d = F, c = F. দ্বিতীয় সূত্রে a = F দিলে C1 থেকে b আর নতুন clause (a ∨ ¬b) থেকে ¬b দুটোই বাধ্য হয়; একটি বসাতেই অন্যটি খালি হয়ে যায়। দুই শাখাই ব্যর্থ, তাই উপপাদ্য ৪.২ অনুযায়ী সূত্রটি নিশ্চিতভাবে unsatisfiable. এই ছোট কোডে মূল DPLL-এর pure literal নিয়মটি (যে variable সব জায়গায় একই চিহ্নে আসে, তাকে সেই মান দেওয়া) রাখা হয়নি; উত্তর তাতে ভুল হয় না, শুধু কখনো কখনো ধীর হয়।
৫ · আসল SAT solver — CDCL, PySAT দিয়ে Sudoku আর ডেলিভারি স্লট
আধুনিক SAT solver DPLL-এর কাঠামো রাখে, কিন্তু একটি বড় কৌশল যোগ করে: CDCL (conflict-driven clause learning). প্রতিবার বিরোধ হলে solver খুঁজে দেখে কোন অনুমানগুলো মিলে বিরোধটি ঘটাল, তারপর “এই অনুমানগুলো একসঙ্গে চলবে না” কথাটি একটি নতুন clause হিসেবে সূত্রে যোগ করে, আর দরকার হলে অনেক ধাপ পেছনে এক লাফে ফেরে। উপরের stepper-এ বিরোধের কারণ ছিল শুধু a = T; CDCL সেখান থেকে শিখত clause (¬a), যা আর কখনো ভুলবে না। নতুন clause মূল সূত্রের যৌক্তিক ফল (পর্ব ০৬-এর resolution দিয়ে পাওয়া), তাই যোগ করলে সমাধানের সেট বদলায় না।
ইতিহাস সংক্ষেপে, যতটা নিশ্চিতভাবে বলা যায়: Martin Davis আর Hilary Putnam ১৯৬০ সালে resolution-ভিত্তিক একটি পদ্ধতি দেন, যা বড় সূত্রে প্রচুর memory খেত। ১৯৬২ সালে Davis, George Logemann আর Donald Loveland তাকে আজকের শাখা-আর-পিছিয়ে-আসা রূপ দেন; দুটি মিলিয়ে নাম DPLL. ১৯৯০-এর দশকের মাঝামাঝি থেকে শেষে João Marques-Silva আর Karem Sakallah-র GRASP solver বিরোধ থেকে clause শেখা প্রতিষ্ঠা করে, আর ২০০১ সালের Chaff solver দ্রুত ডেটা স্ট্রাকচার ও variable বাছাইয়ের নিয়ম এনে গতি বহুগুণ বাড়ায়। এরপর MiniSat, Glucose, CaDiCaL, Kissat-এর মতো solver আর নিয়মিত SAT প্রতিযোগিতা এই ধারা এগিয়ে নিয়েছে। বিস্তারিত অ্যালগরিদমিক দিক DSA কোর্সের পর্ব ৬৫-এ।
সত্যিকারের run ১: 9 × 9 Sudoku
নিচের পরীক্ষাগুলো এই কোর্সের লেখার সময় একটি সাধারণ Windows ডেস্কটপে সত্যিকারভাবে চালানো হয়েছে: Python 3.12, python-sat 1.9.dev21 (PySAT, MIT লাইসেন্স; ভেতরে Armin Biere-এর CaDiCaL 1.9.5 solver, এটিও MIT) আর z3-solver 5.3.3 (MIT). নিজের কম্পিউটারে চালাতে চাইলে:
cd ~/bazarghar-dm
uv add python-sat z3-solver
uv run python sudoku_sat.py hardফাইলের মূল অংশ, অংশ ৩-এর encoding-ই, শুধু 9 × 9-এ (পুরো ফাইলে আরও আছে ধাঁধার স্ট্রিং আর গ্রিড ছাপার কয়েক লাইন):
from pysat.solvers import Solver
def var(r, c, v): # r, c, v in 0..8 -> 1..729
return 81 * r + 9 * c + v + 1
def exactly_one(lits):
yield list(lits) # at least one
for a, b in combinations(lits, 2): # at most one
yield [-a, -b]
# … cell, row, column, box groups → cnf (11988 clauses) …
givens = [[var(k // 9, k % 9, int(ch) - 1)] for k, ch in enumerate(P) if ch != "."]
cnf += givens
with Solver(name="cadical195", bootstrap_with=cnf) as s:
t0 = time.perf_counter(); ok = s.solve(); t1 = time.perf_counter()
model = set(l for l in s.get_model() if l > 0)
# … print the grid …
s.add_clause([-l for l in model if l <= 729]) # block this solution
again = s.solve() # a second solution?সত্যিকারের run-এর আউটপুট, মাত্র ২১টি দেওয়া ঘরের একটি ধাঁধায় (হাতে করতে গেলে অনেক অনুমান লাগে, এমন ধরনের):
$ uv run python sudoku_sat.py hard
variables: 729, rule clauses: 11988, givens: 21, total: 12009
SAT in 0.7 ms
8 1 2 7 5 3 6 4 9
9 4 3 6 8 2 1 7 5
6 7 5 4 9 1 2 8 3
1 5 4 2 3 7 8 9 6
3 6 9 8 4 5 7 2 1
2 8 7 1 6 9 5 3 4
5 2 1 9 7 4 3 6 8
4 3 8 5 2 6 9 1 7
7 9 6 3 1 8 4 5 2
another solution? False (0.1 ms) -> unique
solver stats: {'decisions': 84, 'conflicts': 79, 'propagations': 5994}
তিনটি জিনিস লক্ষ করুন। প্রথমত, সময় এক মিলিসেকেন্ডেরও কম; মানুষের কাছে যা কঠিন, solver-এর কাছে তা ছোট। দ্বিতীয়ত, blocking clause: পাওয়া উত্তরের ঠিক বিপরীত একটি clause যোগ করে আবার চালালে UNSAT এলো, অর্থাৎ উত্তরটি অনন্য; এভাবে solver-কে “আর কোনো সমাধান আছে কি?” প্রশ্নও করা যায়। তৃতীয়ত, পরিসংখ্যান: ৮৪টি অনুমান আর ৭৯টি বিরোধ, অথচ প্রায় ছয় হাজার বাধ্য মান-ছড়ানো; কাজের বেশিরভাগ করেছে unit propagation. (একই ফাইলে সহজ, ৩০ ঘরের একটি ধাঁধায় solver একটিও অনুমান না করে শুধু propagation-এ মিলিয়েছে।) সময়ের সংখ্যা যন্ত্রভেদে বদলাবে।
সত্যিকারের run ২: বাজারঘরের ডেলিভারি স্লট
একটি ভ্যান, তিনটি স্লট (সকাল, দুপুর, বিকেল), ছয়টি ডেলিভারি D1–D6. নিয়ম: প্রতিটি ডেলিভারি ঠিক একটি স্লটে; প্রতি স্লটে বড়জোর ২টি; D1 আর D4 শহরের দুই প্রান্তে, D2 আর D3-ও, তাই এই জোড়াগুলো একই স্লটে নয়; D3 শুধু সকাল বা দুপুর; D5 সকালে নয়; D6 শুধু বিকেলে। “বড়জোর ২টি” একটি cardinality constraint; PySAT তাকে sequential counter নামের encoding-এ কিছু সাহায্যকারী variable দিয়ে clause বানায়।
for s in S: # one van: at most 2 per slot
enc = CardEnc.atmost([x(d, s) for d in D], bound=2,
vpool=pool, encoding=EncType.seqcounter)
hard += enc.clauses
# … exactly-one slot per delivery, far-apart pairs, customer wishes …
# new requests, each switched on by an "assumption" literal
req = {"R1 D2 evening only": [x("D2", "evening")],
"R2 D4 evening only": [x("D4", "evening")],
"R3 D1 not noon": [-x("D1", "noon")],
"R4 D5 noon only": [x("D5", "noon")]}
# … add (not sel or request) for each, then:
s.solve(assumptions=list(sel.values()))
core = s.get_core()$ uv run python slots_sat.py
variables: 42 (18 slot vars + 24 counter vars), clauses: 87
a schedule: {'D1': 'morning', 'D2': 'morning', 'D3': 'noon', 'D4': 'noon', 'D5': 'evening', 'D6': 'evening'}
all schedules: 10
with all 4 requests: UNSAT
conflicting requests: ['R1 D2 evening only', 'R2 D4 evening only']
Solver একটি সময়সূচি দিল, আর blocking clause দিয়ে সব গুনে বলল মোট ১০টি বৈধ সময়সূচি (আলাদা একটি brute-force স্ক্রিপ্ট 36 = ৭২৯টি বিন্যাস ঘুরে একই ১০ পেয়েছে)। এরপর ক্রেতাদের চারটি নতুন অনুরোধ এলো। প্রতিটি অনুরোধের সঙ্গে একটি “সুইচ” variable জুড়ে দিয়ে solver-কে বলা হলো সব সুইচ চালু ধরে চেষ্টা করতে (assumption). উত্তর UNSAT, আর core বলছে দোষী শুধু R1 আর R2: D6 আগেই বিকেলে বাঁধা, R1 আর R2 আরও দুটি ডেলিভারি বিকেলে চায়, অথচ ভ্যানে জায়গা দুটি। R3 আর R4 নির্দোষ। অংশ ২-এর brute-force core খোঁজা এখানে solver-এর ভেতরেই হলো, আর এই পদ্ধতি হাজারো নিয়মেও চলে।
৬ · SMT আর Z3 — টাকার অঙ্কওয়ালা নিয়ম যাচাই
বাস্তবের নিয়মে শুধু সত্য-মিথ্যা থাকে না; থাকে “অর্ডার ৩০০ টাকার বেশি”, “ছাড় মোটের ২০%-এর বেশি নয়”। এগুলোকে bit-এ ভেঙে SAT-এ লেখা যায়, কিন্তু ঝামেলার। SMT solver (satisfiability modulo theories) সরাসরি এমন সূত্র নেয়, যেখানে Boolean variable-এর পাশাপাশি পূর্ণসংখ্যা, বাস্তব সংখ্যা, array আর bit-vector-এর ওপর শর্ত থাকে। ভেতরে একটি SAT solver যুক্তির কাঠামো সামলায়, আর প্রতিটি “তত্ত্বের” আলাদা অংশ (যেমন রৈখিক অসমতার সমাধানকারী) বলে সংখ্যার শর্তগুলো একসঙ্গে সম্ভব কি না। সবচেয়ে পরিচিত বিনামূল্যের SMT solver-গুলোর একটি Z3; ২০০৮ সালের একটি গবেষণাপত্রে Leonardo de Moura আর Nikolaj Bjørner এটি পরিচয় করিয়ে দেন, আর পরে এটি MIT লাইসেন্সে উন্মুক্ত হয়।
সত্যিকারের run ৩: সরল করা শর্ত সব পূর্ণসংখ্যায় প্রমাণ
অংশ ১-এ Python ৮টি সারি দেখেছিল, যেখানে b একটি সত্য-মিথ্যা variable. আসল কোডে total একটি পূর্ণসংখ্যা। Z3-কে সরাসরি জিজ্ঞেস করি: সব সম্ভাব্য total-এ দুটি শর্ত কি সমান?
from z3 import Bools, Int, Not, And, Or, prove
member, remote = Bools("member remote")
total = Int("total")
messy = And(Not(And(Not(member), total < 1000)), Not(And(remote, Not(member))))
clean = Or(member, And(total >= 1000, Not(remote)))
wrong = And(member, total >= 1000, Not(And(remote, Not(member))))
print("messy == clean :", end=" "); prove(messy == clean)
print("messy == wrong :", end=" "); prove(messy == wrong)$ uv run python cond_z3.py
messy == clean : proved
messy == wrong : counterexample
[remote = True, member = True, total = 0]
prove(G) ভেতরে কী করে? ¬G satisfiable কি না দেখে। ¬G-এর কোনো model না থাকলে G সব ক্ষেত্রে সত্য, অর্থাৎ tautology (অনুশীলন ১-এ এর প্রমাণ)। থাকলে সেই model-ই একটি counterexample: দূরের জেলার একজন সদস্য, ০ টাকার অর্ডার; মূল শর্তে তিনি ফ্রি ডেলিভারি পান, ভুল রূপে পান না। ৮ সারির পরীক্ষার চেয়ে এটি শক্তিশালী: total-এর অসীম সংখ্যক মানের জন্য একবারে প্রমাণ।
সত্যিকারের run ৪: কুপন-নিয়মে ফাঁক খোঁজা
বাজারঘরের কুপন-নিয়ম, যেখানে total = অর্ডারের মোট টাকা, disc = ছাড়ের টাকা:
- S1: ছাড় কখনো ঋণাত্মক নয়: disc ≥ 0
- S2: ছাড় মোটের ২০%-এর বেশি নয়: 5 · disc ≤ total
- S3: নতুন ক্রেতা আর মোট ≥ ৩০০ হলে ছাড় অন্তত ১০০ টাকা
- S4: মোট ৩০০-এর কম হলে কোনো ছাড় নেই: disc = 0
from z3 import Int, Bool, Solver, Implies, And, Not, ForAll
total, disc = Int("total"), Int("disc") # order total and discount, in taka
new = Bool("new_customer")
rules = {
"S1 discount is never negative": disc >= 0,
"S2 discount at most 20% of total": 5 * disc <= total,
"S3 new customer, total >= 300 -> >=100": Implies(And(new, total >= 300), disc >= 100),
"S4 total < 300 -> no discount": Implies(total < 300, disc == 0),
}
s = Solver(); s.add(*rules.values())
print("1) all rules together:", s.check(), "->", s.model())
s = Solver() # an order with NO legal discount?
s.add(total >= 0, ForAll([disc], Not(And(*rules.values()))))
print("2) order with no legal discount:", s.check(), "->", s.model())
s = Solver(); s.set(unsat_core=True) # which rules clash at 400 taka?
for name, r in rules.items():
s.assert_and_track(r, name)
s.add(total == 400, new)
print("3) new customer, 400 taka:", s.check(), "-> core", s.unsat_core())$ uv run python coupon_z3.py
1) all rules together: sat -> [new_customer = True, disc = 100, total = 500]
2) order with no legal discount: sat -> [new_customer = True, total = 304]
3) new customer, 400 taka: unsat -> core [S2 discount at most 20% of total,
S3 new customer, total >= 300 -> >=100]
প্রথম প্রশ্নে নিয়মগুলো consistent: ৫০০ টাকার অর্ডারে নতুন ক্রেতা ১০০ টাকা ছাড় পেলে সব মানা হয়। কিন্তু consistent মানেই নির্ভুল নয়। দ্বিতীয় প্রশ্নটি nested quantifier (পর্ব ০৫): “এমন কোনো total আছে কি, যার জন্য প্রতিটি disc-এ কোনো না কোনো নিয়ম ভাঙে?” Z3 একটি পেল: ৩০৪ টাকার অর্ডারে নতুন ক্রেতা। S3 বলে ছাড় ≥ ১০০, S2 বলে ছাড় ≤ ৩০৪/৫, অর্থাৎ ≤ ৬০. হাতে হিসাব করলে ফাঁকটি পুরো পরিসর: ৩০০ ≤ total < ৫০০, কারণ 5 × 100 = 500. তৃতীয় প্রশ্নে ৪০০ টাকার অর্ডারে Z3-এর unsat core দোষী হিসেবে দেখাল ঠিক S2 আর S3. চেকআউট কোড লেখার আগেই নীতিনির্ধারকদের জানানো যায়: “৩০০ থেকে ৪৯৯ টাকার অর্ডারে নতুন ক্রেতার জন্য আপনার দুটি নিয়ম একসঙ্গে মানা অসম্ভব।”
SMT-এর সীমাও আছে। পূর্ণসংখ্যার রৈখিক শর্তে Z3-এর ক্ষমতা ভালো, কিন্তু quantifier আর অরৈখিক গুণ (x · y) মেশালে সাধারণভাবে প্রশ্নটি অনির্ণেয়ও হতে পারে; তখন solver “unknown” বলতে পারে। Solver-এর “unknown”-কে কখনো “ঠিক আছে” পড়বেন না। SAT আর SMT দিয়ে সফটওয়্যার যাচাইয়ের পূর্ণ আলোচনা আসবে পর্ব ৭৭-এ।
৭ · SAT আর NP-completeness — কেন কঠিন, তবু কেন চলে
অংশ ৩-এ দেখেছেন: একটি প্রস্তাবিত মান-বসানো দিলে সব clause একবার করে দেখে polynomial সময়ে যাচাই করা যায়। এমন “দ্রুত যাচাইযোগ্য” সমস্যার শ্রেণির নাম NP. SAT এই শ্রেণির একটি বিশেষ সদস্য:
SAT NP-complete: NP-এর প্রতিটি সমস্যাকে polynomial সময়ে SAT-এর একটি উদাহরণে রূপান্তর করা যায়, এমনভাবে যে মূল প্রশ্নের উত্তর “হ্যাঁ” ঠিক তখন, যখন রূপান্তরিত সূত্রটি satisfiable.
এটি সাধারণত Stephen Cook-এর ১৯৭১ সালের প্রবন্ধের নামে পরিচিত; Leonid Levin প্রায় একই সময়ে সোভিয়েত ইউনিয়নে স্বাধীনভাবে একই ধরনের ফল পান (Cook–Levin theorem). প্রমাণের মূল ভাবনা: যেকোনো যাচাই-প্রোগ্রামের ধাপে ধাপে চলাকে বিশাল একটি clause-এর তালিকায় লেখা যায়, ঠিক যেমন আমরা Sudoku-র নিয়ম লিখেছি। এই পর্বের Sudoku, সময়সূচি আর পর্ব ৬৩-এর graph coloring একই কৌশলে SAT-এ যায়। পুরো প্রমাণ আর reduction-এর ধারণা DSA কোর্সের পর্ব ৬৪-এ।
তাই দুটি কথা একসঙ্গে সত্য। তাত্ত্বিকভাবে: সব ক্ষেত্রে polynomial সময়ে SAT সমাধানের কোনো পদ্ধতি জানা নেই, আর থাকলে P = NP হতো, যা কম্পিউটার বিজ্ঞানের সবচেয়ে বিখ্যাত খোলা প্রশ্ন। ছোট আকারের কিছু ধরন অবশ্য সহজ: প্রতিটি clause-এ বড়জোর দুটি literal থাকলে (2-SAT) polynomial সময়ের পদ্ধতি আছে, কিন্তু তিনটি হলেই (3-SAT) সমস্যা আবার NP-complete. ব্যবহারিকভাবে: chip নকশা যাচাই, সফটওয়্যার পরীক্ষা, সময়সূচি আর package-এর নির্ভরতা মেলানোর মতো কাজে লাখো variable-এর সূত্র প্রায়ই দ্রুত মেলে, কারণ বাস্তবের সূত্রে গঠন থাকে আর CDCL সেই গঠন কাজে লাগায়। খারাপ ক্ষেত্রও বানানো যায়, তাই সময়সীমা রাখতে হয়।
আরও গভীরে: এলোমেলোভাবে বানানো 3-SAT সূত্রে clause আর variable-এর অনুপাত প্রায় ৪.২৬-এর কাছে পৌঁছালে সূত্রগুলো হঠাৎ “প্রায় সবই satisfiable” থেকে “প্রায় সবই unsatisfiable”-এ সরে যায়, আর ঠিক সেই এলাকায় solver-দের সবচেয়ে বেশি সময় লাগে; এটি ১৯৯০-এর দশকের পরীক্ষা থেকে পাওয়া পর্যবেক্ষণ, সাধারণ k-এর জন্য সঠিক সীমার প্রমাণ এখনো গবেষণার বিষয়। আরেক দিকে, SAT solver এখন গণিতের প্রমাণেও কাজে লাগে, আর UNSAT-এর দাবির সঙ্গে একটি যাচাইযোগ্য প্রমাণ-ফাইল (যেমন DRAT ফরম্যাটে) দেওয়া এখন প্রচলিত রীতি, যাতে solver-এর নিজের bug ধরা যায়।
পরীক্ষা ও ইন্টারভিউতে: বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর পরীক্ষায় এই ধরনের প্রশ্ন প্রায়ই আসে:
not (a and not b)-কে সরল করুন। De Morgan আর double negation-এnot a or b, যা a → b.- n-টি variable-এর “ঠিক একটি” শর্তে জোড়াভিত্তিক encoding-এ কয়টি clause? 1 + n(n − 1)/2; n = 9-এ ৩৭।
- (x) ∧ (¬x ∨ y) ∧ (¬y ∨ z)-এ unit propagation কী দেয়? x = T, তারপর y = T, তারপর z = T; কোনো অনুমান লাগে না।
- F tautology কি না, SAT solver দিয়ে কীভাবে জানবেন? ¬F-কে solver-এ দিন; UNSAT হলে F tautology.
x != 0 and 10 / x > 2-এ x = 0 হলে কী হয়? False; short-circuit-এর জন্য ভাগ হয়ই না, তাই error নেই।
৮ · AI-এর যুগে — শর্ত, নিয়ম আর encoding-এ AI সহকারী
AI সহকারী একটি জটিল if-শর্ত “সরল” করে দিতে, নিয়মের তালিকা থেকে Z3 কোড লিখে দিতে বা Sudoku-র encoding বানাতে খুব দ্রুত। কিন্তু এখানে তার ভুল বিশেষভাবে বিপজ্জনক: ভুল encoding-এ solver নির্ভুলভাবে ভুল প্রশ্নের উত্তর দেয়, আর “SAT” বা “proved” লেখা দেখে সবাই নিশ্চিন্ত হয়ে যান। এই বিষয়ে যে ভুলগুলো বেশি দেখা যায়:
| AI-এর ভুল | কীভাবে ধরবেন |
|---|---|
| De Morgan উল্টো খাটানো: ¬(a ∧ b)-কে ¬a ∧ ¬b লেখা | সব সারিতে মূল আর নতুন রূপ মিলিয়ে দেখুন (অংশ ১-এর ব্লক) |
not (x < 5)-কে x > 5 বানানো, সীমানা হারানো | সীমানার মান (৪, ৫, ৬) আলাদা করে পরীক্ষা করুন, বা Z3-এ prove |
| Short-circuit-এর ক্রম উল্টে দেওয়া, None বা শূন্যে ভাগের error | যে শর্ত পরের অংশকে নিরাপদ করে, সেটি বাঁয়ে আছে কি না দেখুন |
| “ঠিক একটি”-র বদলে শুধু “অন্তত একটি” encode করা | একটি ভুল উত্তর (দুটি মান একই ঘরে) বানিয়ে দেখুন clause ভাঙে কি না |
| Clause গুনতে off-by-one, বা C(n, 2)-এর বদলে n² | ছোট n-এ কোড দিয়ে গুনে সূত্রের সঙ্গে মেলান |
| “unknown” বা timeout-কে সমাধান ধরে নেওয়া; অস্তিত্বহীন solver option বা প্রবন্ধ উদ্ধৃত করা | আসল আউটপুট নিজে পড়ুন, documentation আর উৎস নিজে মিলিয়ে দেখুন |
AI-কে encoding বা সরলীকরণের কাজ দিলে উত্তরের সঙ্গে যাচাইয়ের উপায়ও চেয়ে নিন:
AI-এর উত্তর হাতে পেলে — যাচাইয়ের চেকলিস্ট
AI-তে যুক্তি আর constraint কীভাবে ব্যবহার হয়, তা AI-এর ভিত্তি আর Python for AI কোর্সের পর্ব ৪৩ (constraint satisfaction) আর পর্ব ৪৫-এ আছে; এখানে আমরা তার নিচের গণিত আর যাচাইয়ের দিকটি দেখলাম।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।
প্র ০১গণিতে p ∧ q আর q ∧ p একই, অথচ কোডে a and b আর b and a আলাদা ফল দিতে পারে। তাহলে কি কোড যুক্তির নিয়ম ভাঙে?
না, কোড যুক্তির একটি বড় ভাষায় কথা বলে। Truth table-এ প্রতিটি অংশের মান হয় T নয় F, আর মান বের করতে কোনো খরচ বা বিপদ নেই। কোডে একটি অংশের হিসাব তিন রকম হতে পারে: T, F, অথবা “কোনো মানই এল না” (error, অসীম লুপ, নেটওয়ার্ক বন্ধ)। এই তৃতীয় সম্ভাবনা যোগ হলে ∧-এর একাধিক যুক্তিসংগত সংজ্ঞা দাঁড়ায়। Short-circuit সংজ্ঞায় বাঁ দিক F হলে ডান দিক যা-ই হোক ফল F, এমনকি ডান দিক error হলেও; কিন্তু বাঁ দিক error হলে ফলও error. তাই এই ∧ commutative নয়।
এই ধরনের বাঁ-থেকে-ডান যুক্তি প্রোগ্রামিং ভাষায় এসেছে অনেক আগে। John McCarthy ১৯৬০-এর দশকের শুরুতে Lisp-এর কাজে শর্তসাপেক্ষ রাশি দিয়ে and/or সংজ্ঞায়িত করেন, আর তিন-মানের এই যুক্তিকে অনেক বইয়ে “McCarthy-র” যুক্তি বলা হয়। C ভাষার && আর || একই রীতি নেয়, আর পরে Java, Python, JavaScript-এর মতো বেশিরভাগ জনপ্রিয় ভাষা।
কেন এটি ভালো নকশা? কারণ বাস্তবের শর্তে প্রায়ই একটি অংশ অন্যটির “পাহারাদার”: i < len(a) and a[i] == 0, user is not None and user.active. গণিতের সমমিত ∧ থাকলে প্রতিবার আলাদা if লাগত। আবার এটি বিপদও আনে: কেউ “পরিষ্কার” করতে গিয়ে অংশ দুটির ক্রম বদলালে বা De Morgan খাটিয়ে নতুন ক্রম বানালে পাহারাদার পেছনে চলে যায়। Tony Hoare ২০০৯ সালের একটি বক্তৃতায় null reference-কে নিজের “billion-dollar mistake” বলেছিলেন; null-এর পাহারায় short-circuit সবচেয়ে প্রচলিত হাতিয়ার, আর তার ক্রমভুল সবচেয়ে প্রচলিত দুর্ঘটনাগুলোর একটি।
কীভাবে নিরাপদে সরল করবেন? যুক্তির নিয়ম খাটানোর আগে নিশ্চিত হোন যে প্রতিটি অংশ সব ক্ষেত্রে সংজ্ঞায়িত (error নেই, side effect নেই)। না হলে পাহারাদারকে আলাদা guard clause-এ সরিয়ে দিন, তারপর বাকি অংশে নিশ্চিন্তে বীজগণিত চালান।
মূল উপলব্ধি: দুই-মানের যুক্তি কোডের একটি আদর্শ মডেল। মডেলটি যেখানে বাস্তবের সঙ্গে মেলে না (error, সময়, side effect), সেখানে commutative-এর মতো নিয়মও সাবধানে খাটাতে হয়।
প্র ০২DPLL ১৯৬২ সালের পদ্ধতি। মাঝের প্রায় চার দশকে কী বদলাল যে আজকের solver লাখো variable সামলায়?
শুরুটা ছিল প্রমাণ খোঁজার যন্ত্র বানানোর চেষ্টা। Martin Davis আর Hilary Putnam-এর ১৯৬০ সালের পদ্ধতি একটি করে variable সরিয়ে দিত resolution দিয়ে (পর্ব ০৬): x আছে এমন প্রতিটি clause-কে ¬x আছে এমন প্রতিটির সঙ্গে জুড়ে। এতে clause-এর সংখ্যা বিস্ফোরিত হতো, আর সেই সময়ের কম্পিউটারের memory দ্রুত ফুরিয়ে যেত। ১৯৬২ সালে Davis, Logemann আর Loveland resolution-এর বদলে শাখা নেওয়া আর পিছিয়ে আসার কৌশল নেন, যার memory খরচ সামান্য। এটিই এই পর্বের DPLL.
প্রায় তিন দশক ধরে উন্নতি এসেছে মূলত ভালো variable বাছাইয়ের নিয়মে। বড় মোড় আসে ১৯৯০-এর দশকের মাঝামাঝি থেকে শেষে: Marques-Silva আর Sakallah-র GRASP দেখায়, বিরোধ হলে তার কারণ বিশ্লেষণ করে একটি নতুন clause শেখা যায়, আর বিরোধের আসল উৎসের ধাপে সরাসরি ফেরা যায় (non-chronological backtracking). একই সময়ে আরও কয়েকটি গবেষক দল কাছাকাছি ধারণা নিয়ে কাজ করছিলেন বলে ইতিহাসে পাওয়া যায়।
২০০১ সালের Chaff solver দুটি প্রকৌশল-কৌশল আনে, যা এখনো প্রায় সব solver-এ আছে: প্রতিটি clause-এর মাত্র দুটি literal “পাহারায়” রাখা (watched literals), যাতে unit propagation-এ প্রতিবার সব clause দেখতে না হয়; আর সাম্প্রতিক বিরোধে বেশি জড়িত variable-কে আগে বাছা। পরে MiniSat ছোট আর পরিষ্কার কোডে এই সব কৌশল একত্র করে, যা বহু গবেষণার ভিত্তি হয়। নিয়মিত “restart”, শেখা clause-এর মধ্যে কোনগুলো রাখতে হবে তার নিয়ম, আর preprocessing আরও গতি এনেছে।
এই উন্নতির একটি বড় চালিকাশক্তি ছিল শিল্পের চাহিদা: chip নকশায় দুটি সার্কিট একই কাজ করে কি না, বা কোনো অবস্থায় ভুল আচরণ সম্ভব কি না, এমন প্রশ্ন SAT-এ সরাসরি লেখা যায়। ১৯৯৯ সালের দিকে Armin Biere ও সহকর্মীদের bounded model checking এই ধারাকে বড় গতি দেয় বলে সাধারণত উল্লেখ করা হয়।
মূল উপলব্ধি: তত্ত্ব বলে SAT কঠিন; প্রকৌশল দেখায় বাস্তবের SAT প্রায়ই সহজ। দুটির মাঝখানে আছে ভুল থেকে শেখা (clause learning) আর কাজ না করার কৌশল (watched literals).
প্র ০৩SAT যদি NP-complete হয়, তাহলে Sudoku বা ডেলিভারি স্লটের মতো সমস্যা solver কয়েক মিলিসেকেন্ডে কীভাবে মেলায়? এটি কি P = NP-এর ইঙ্গিত?
না। NP-completeness হলো খারাপতম ক্ষেত্রের কথা: সব সম্ভাব্য সূত্রের মধ্যে এমন কিছু থাকবে (যদি P ≠ NP হয়) যেগুলোতে যেকোনো পদ্ধতি polynomial-এর চেয়ে বেশি সময় নেবে। কোন নির্দিষ্ট সূত্র সহজ, সে বিষয়ে এটি কিছু বলে না। 9 × 9 Sudoku-র আকার নির্দিষ্ট: ৭২৯টি variable. নির্দিষ্ট আকারের সমস্যা যত কঠিনই হোক, তা “অসীম পরিবারের” প্রশ্ন নয়, তাই NP-completeness-এর সঙ্গে এর সরাসরি সম্পর্ক নেই। Sudoku-র সাধারণ রূপ (n² × n² গ্রিড, n যত খুশি বড়) অবশ্য NP-complete বলে প্রমাণিত, ২০০৩ সালের দিকে জাপানের গবেষক Takayuki Yato আর Takahiro Seta-র কাজে।
বাস্তবের সূত্র কেন সহজ? এদের গঠন থাকে। Sudoku-র ক্ষেত্রে একটি ঘর ঠিক হলেই তার সারি, কলাম আর বাক্সের ২০টি ঘর থেকে একটি করে সম্ভাবনা মুছে যায়: unit propagation-এর বিশাল শিকল। আমাদের সত্যিকারের run-এ সহজ ধাঁধাটি শূন্য অনুমানে মিলেছে। শিল্পের সূত্রগুলোতেও variable-গুলো ছোট ছোট গোষ্ঠীতে জড়িত থাকে, আর CDCL সেই গোষ্ঠীর ভেতরের কারণ শেখে।
কঠিন সূত্র কেমন? এলোমেলো 3-SAT-এ clause আর variable-এর অনুপাত প্রায় ৪.২৬-এর কাছে থাকলে সূত্রগুলো সবচেয়ে কঠিন হয়; ১৯৯০-এর দশকের শুরুতে Mitchell, Selman আর Levesque-এর মতো গবেষকদের পরীক্ষায় এই “phase transition” স্পষ্ট হয়। আবার কিছু সূত্রের জন্য প্রমাণিত আছে যে resolution-ভিত্তিক যেকোনো পদ্ধতির (CDCL-সহ) প্রমাণ exponential বড় হবেই; যেমন pigeonhole নীতির সূত্র (n + 1টি পায়রা, nটি খোপ), যার সূচকীয় নিম্নসীমা Armin Haken ১৯৮৫ সালে প্রমাণ করেন। পর্ব ৩২-এর pigeonhole principle মানুষ এক লাইনে প্রমাণ করে, অথচ CDCL solver-এর জন্য তা কঠিন: একটি সুন্দর বৈপরীত্য।
P = NP-এর প্রশ্ন: solver-দের সাফল্য এর পক্ষে বা বিপক্ষে কোনো প্রমাণ নয়। বেশিরভাগ গবেষক মনে করেন P ≠ NP, কিন্তু প্রমাণ নেই; এর অ্যালগরিদমিক দিক DSA কোর্সের পর্ব ৬৪-এ।
মূল উপলব্ধি: জটিলতা-তত্ত্ব সবচেয়ে খারাপ ক্ষেত্রের সীমা টানে; প্রকৌশল কাজ করে সাধারণ ক্ষেত্রে। দুটিকে গুলিয়ে ফেললে হয় অকারণ ভয়, নয় অকারণ ভরসা।
প্র ০৪২০১৬ সালে একটি SAT solver প্রায় ২০০ টেরাবাইটের একটি “প্রমাণ” বানিয়েছিল। এমন প্রমাণ কি সত্যিকারের প্রমাণ?
প্রশ্নটি ছিল Boolean Pythagorean triples problem: ১, ২, ৩, … সংখ্যাগুলোকে লাল আর নীল দুই রঙে এমনভাবে রাঙানো যায় কি, যাতে a² + b² = c² মানে এমন কোনো তিনটি সংখ্যা (যেমন ৩, ৪, ৫) একই রঙের না হয়? Marijn Heule, Oliver Kullmann আর Victor Marek ২০১৬ সালে দেখান, ১ থেকে ৭৮২৪ পর্যন্ত এমন রাঙানো সম্ভব, কিন্তু ৭৮২৫ পর্যন্ত নয়। প্রশ্নটি সরাসরি SAT: প্রতিটি সংখ্যার জন্য একটি variable (লাল কি না), আর প্রতিটি পিথাগোরীয় ত্রয়ীর জন্য দুটি clause (“তিনটিই লাল নয়”, “তিনটিই নীল নয়”)। ৭৮২৫-এর সূত্রটি UNSAT, আর সেই দাবির প্রমাণ-ফাইল প্রায় ২০০ টেরাবাইট হয়েছিল বলে প্রকাশিত তথ্যে জানা যায়।
“প্রমাণ” কেন বলা হয়? কারণ solver শুধু “UNSAT” বলেনি; প্রতিটি শেখা clause কীভাবে মূল clause থেকে আসে, তার পূর্ণ লগ দিয়েছে (DRAT ফরম্যাট)। একটি আলাদা, অনেক ছোট আর সরল যাচাই-প্রোগ্রাম সেই লগের প্রতিটি ধাপ পরীক্ষা করে। এমনকি এমন যাচাই-প্রোগ্রামও তৈরি হয়েছে যাদের নিজেদের সঠিকতা proof assistant-এ আনুষ্ঠানিকভাবে প্রমাণ করা। ফলে আস্থা রাখতে হয় শুধু ছোট যাচাইকারীর ওপর, বিশাল solver-এর ওপর নয়।
ইতিহাসের প্রতিধ্বনি: ১৯৭৬ সালে Kenneth Appel আর Wolfgang Haken চার রঙের উপপাদ্য প্রমাণ করেন কম্পিউটারে বহু ক্ষেত্র পরীক্ষা করে, আর তখন অনেক গণিতবিদ আপত্তি তোলেন: যে প্রমাণ মানুষ পড়তে পারে না, তা কি প্রমাণ? (পর্ব ৬৩-এ চার রঙের উপপাদ্য।) আজ বিতর্কটি অনেকটা বদলে গেছে: প্রশ্ন এখন “মানুষ পড়তে পারে কি না” নয়, “প্রতিটি ধাপ যন্ত্রে স্বাধীনভাবে যাচাইযোগ্য কি না”। এরপর Schur number five-এর মতো আরও কয়েকটি প্রশ্ন একই পদ্ধতিতে মেটানো হয়েছে।
সীমা: এমন প্রমাণ বলে “কী সত্য”, কিন্তু প্রায়ই বলে না “কেন সত্য”। ৭৮২৫ কেন বিশেষ, তার কোনো মানুষ-বোধ্য ব্যাখ্যা পাওয়া যায়নি বলে জানা যায়। গণিতবিদরা প্রমাণ থেকে বোঝাপড়াও চান, তাই দুই ধরনের কাজই চলবে।
মূল উপলব্ধি: প্রমাণের মূল শর্ত হলো প্রতিটি ধাপ যাচাইযোগ্য হওয়া। বিশাল যন্ত্র-প্রমাণ গ্রহণযোগ্য হয় তখনই, যখন একটি ছোট, বিশ্বস্ত যাচাইকারী প্রতিটি ধাপ মেলাতে পারে।
প্র ০৫আপনার কম্পিউটারে package install করার সময় কি গোপনে একটি SAT সমস্যা সমাধান হয়?
প্রায়ই হয়। একটি package manager-কে প্রতিবার একটি প্রশ্নের উত্তর দিতে হয়: হাজারো package-এর কোন কোন version বাছলে সব নির্ভরতা (dependency) মিটবে আর কোনো দুটি পরস্পরবিরোধী version একসঙ্গে থাকবে না? ধরুন pv = “package p-এর version v install হবে”। তখন নিয়মগুলো এই পর্বের ভাষায় সরাসরি লেখা যায়: প্রতিটি package-এর বড়জোর একটি version (অংশ ৩-এর জোড়াভিত্তিক clause); “A 2.0 লাগলে B-এর 1.4 অথবা 1.5 লাগবে” মানে ¬A2.0 ∨ B1.4 ∨ B1.5; “C 3.0 আর D 1.0 একসঙ্গে চলে না” মানে ¬C3.0 ∨ ¬D1.0; আর ব্যবহারকারী যা চেয়েছেন তা একটি unit clause.
২০০০-এর দশকের মাঝামাঝি ইউরোপের একটি গবেষণা প্রকল্পে (Roberto Di Cosmo ও সহকর্মীদের কাজ বলে সাধারণত উল্লেখ করা হয়) দেখানো হয় যে Linux distribution-এর এই “installability” প্রশ্ন সাধারণভাবে NP-complete; প্রমাণটি 3-SAT থেকে reduction. তাই কিছু package manager সরাসরি SAT solver ব্যবহার করে; যেমন Python-এর conda ecosystem বহু বছর pycosat (Armin Biere-এর PicoSAT solver-এর Python রূপ) দিয়ে নির্ভরতা মেলাত বলে জানা যায়, আর কয়েকটি Linux distribution-এর package manager libsolv নামের SAT-ভিত্তিক লাইব্রেরি ব্যবহার করে। অন্যরা নিজস্ব backtracking খোঁজ চালায়, যা আসলে DPLL-এরই আত্মীয়।
কেন বিরোধ দেখায় এত অস্পষ্টভাবে? “Cannot resolve dependencies” বার্তাটি আসলে UNSAT. একটি ভালো package manager অংশ ২ আর ৫-এর মতো core খুঁজে বলতে পারে, “আপনার চাওয়া X আর Y-এর কারণে Z-এর দুটি version লাগছে”। এমন ব্যাখ্যা দেওয়া নিজেই একটি গবেষণার বিষয়, কারণ ক্ষুদ্রতম core খোঁজাও কঠিন সমস্যা।
বাজারঘরের শিক্ষা: কুপন, ডেলিভারি, স্টাফের অনুমতি, সব জায়গায় একই ছবি: নিয়মের তালিকা বড় হলে মানুষের চোখে বিরোধ ধরা পড়ে না, কিন্তু solver-এর কাছে তা কয়েক মিলিসেকেন্ডের প্রশ্ন। নিয়মগুলো সূত্রে লেখার অভ্যাসটিই আসল বিনিয়োগ।
মূল উপলব্ধি: যেখানেই “অনেকগুলো পছন্দ, প্রতিটির কিছু শর্ত” আছে, সেখানে প্রায়ই একটি SAT সমস্যা লুকিয়ে থাকে। সেটি চিনতে পারলে পুরোনো, পরীক্ষিত solver-কে কাজে লাগানো যায়।
অনুশীলন
আগে খাতায় করুন, তারপর উত্তর খুলে মেলান।
-
প্রমাণ: প্রমাণ করুন যে একটি সূত্র F tautology ঠিক তখন, যখন ¬F unsatisfiable. (Z3-এর
proveএই সত্যের ওপরই দাঁড়িয়ে।)- (⇒) ধরুন F tautology, অর্থাৎ প্রতিটি মান-বসানোয় F = T. তাহলে প্রতিটি মান-বসানোয় ¬F = F. কোনো মান-বসানোয় ¬F সত্য নয়, তাই ¬F unsatisfiable.
- (⇐) ধরুন ¬F unsatisfiable, অর্থাৎ কোনো মান-বসানোয় ¬F = T নয়. তাই প্রতিটি মান-বসানোয় ¬F = F, আর ¬-এর সংজ্ঞায় F = T. অর্থাৎ F tautology.
- দুই দিক মিলিয়ে: F tautology ⇔ ¬F unsatisfiable. ∎
ফল: tautology যাচাইয়ের প্রশ্ন SAT-এর “উল্টো” প্রশ্ন। ¬F satisfiable হলে solver যে model দেয়, সেটিই F-এর counterexample.
-
হিসাব: 8 × 8 বোর্ডে 8-queens: variable qr,c = “সারি r, কলাম c-তে রানি”। নিয়ম: প্রতিটি সারিতে ঠিক একটি রানি, প্রতিটি কলামে ঠিক একটি, আর প্রতিটি কর্ণরেখায় (দুই দিকেরই) বড়জোর একটি। জোড়াভিত্তিক encoding-এ (ক) variable কয়টি, (খ) সারি আর কলামের clause কয়টি, (গ) কর্ণরেখার clause কয়টি, (ঘ) মোট কয়টি?
(ক) 8 × 8 = ৬৪টি।
(খ) প্রতিটি সারি একটি “ঠিক একটি” গ্রুপ, ৮টি variable নিয়ে: 1 + C(8, 2) = 1 + 28 = ২৯টি clause. ৮টি সারি আর ৮টি কলাম: 16 × 29 = ৪৬৪টি।
(গ) এক দিকের কর্ণরেখাগুলোর দৈর্ঘ্য 1, 2, …, 7, 8, 7, …, 2, 1. দৈর্ঘ্য L-এর রেখায় C(L, 2)টি clause (দৈর্ঘ্য ১-এ শূন্য)। এক দিকে 2 × (1 + 3 + 6 + 10 + 15 + 21) + 28 = 2 × 56 + 28 = ১৪০; দুই দিকে ২৮০টি।
(ঘ) মোট 464 + 280 = ৭৪৪টি clause. একটি ছোট স্ক্রিপ্টে গুনে একই সংখ্যা পাওয়া গেছে। (কলামের “অন্তত একটি” বাদ দিলেও সমাধানের সেট বদলায় না, কারণ ৮টি সারিতে ৮টি রানি আর প্রতিটি কলামে বড়জোর একটি থাকলে প্রতিটি কলামে ঠিক একটি হতেই হয়; কিন্তু প্রশ্ন যেভাবে দেওয়া, সেভাবে ৭৪৪।)
-
আউটপুট অনুমান: কোড না চালিয়ে বলুন কী ছাপা হবে, তারপর যেকোনো Python-এ চালিয়ে মেলান।
def f(x): print("f", x) return x > 0 print(f(1) or f(2)) print(f(-1) and f(3)) print(not f(0) or f(5))f 1 True f -1 False f 0 Trueপ্রথম লাইনে f(1) = True, তাই
orথেমে যায়; f(2) কখনো ডাকা হয় না। দ্বিতীয়টিতে f(−1) = False, তাইandথেমে যায়; f(3) ডাকা হয় না। তৃতীয়টিতেnotসবচেয়ে আগে খাটে, তাই এটি(not f(0)) or f(5); f(0) = False, not False = True, আরorআবার থেমে যায়। তিন জায়গাতেই ডান দিকের ফাংশন একবারও চলেনি, যা “f 2”, “f 3”, “f 5” না ছাপা থেকে বোঝা যায়। -
প্রমাণের ভুল ধরুন: একজন লিখলেন, “দাবি: যে CNF সূত্রের প্রতিটি clause-এ অন্তত দুটি literal আছে, তা satisfiable. প্রমাণ: যেকোনো clause নিন। তাতে অন্তত দুটি literal আছে, তাই অন্তত একটিকে সত্য করার মতো মান বসানো যায় (একটি variable-এর মান অন্যটির মান আটকায় না)। প্রতিটি clause-কে এভাবে সত্য করা যায়, তাই সব clause একসঙ্গে সত্য। ∎” ভুলটি কোথায়? একটি counterexample দিন।
- Quantifier-এর ক্রম গুলিয়ে ফেলা: প্রমাণটি দেখিয়েছে “∀ clause ∃ মান-বসানো, যা সেটিকে সত্য করে”। দাবি করেছে “∃ মান-বসানো ∀ clause, যা সবগুলোকে একসঙ্গে সত্য করে”। পর্ব ০৫-এ দেখেছেন, ∀∃ থেকে ∃∀ আসে না।
- পছন্দগুলোর পারস্পরিক নির্ভরতা উপেক্ষা: একটি clause-কে সত্য করতে বেছে নেওয়া মান অন্য clause-এর literal মিথ্যা করে দিতে পারে, কারণ একই variable বহু clause-এ থাকে।
Counterexample: (a ∨ b) ∧ (a ∨ ¬b) ∧ (¬a ∨ b) ∧ (¬a ∨ ¬b). প্রতিটি clause-এ দুটি literal, কিন্তু চারটি clause a, b-এর চারটি মান-বসানোর প্রতিটিকে একটি করে বাদ দেয়: (a, b) = (F, F) প্রথমটিকে, (F, T) দ্বিতীয়টিকে, (T, F) তৃতীয়টিকে, (T, T) চতুর্থটিকে ভাঙে। তাই সূত্রটি unsatisfiable, যদিও কোনো unit বা empty clause নেই।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমSAT solver দিয়ে একটি বাস্তব সমস্যা সমাধানের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- প্রতিটি সত্য-মিথ্যা প্রশ্নের জন্য variable ঠিক করা আর তার অর্থ লেখা
- প্রতিটি নিয়মকে clause-এ লেখা (“ঠিক একটি” = অন্তত একটি + বড়জোর একটি)
- ছোট আকারে clause গুনে আর একটি জানা উত্তর দিয়ে encoding পরীক্ষা করা
- Solver চালানো আর SAT বা UNSAT উত্তর পাওয়া
- Model থেকে উত্তর পড়া (decode), বা UNSAT হলে core দেখা
- পাওয়া উত্তরটি মূল নিয়মে আলাদা কোড দিয়ে যাচাই করা
Python-এ not (a and not b)-এর সমান শর্ত কোনটি?
- not a and b
- not a or b
- a or not b
- a and b
৯টি variable-এর মধ্যে “ঠিক একটি সত্য” শর্তকে জোড়াভিত্তিক encoding-এ লিখলে মোট কয়টি clause লাগে?
- ৯
- ৩৬
- ৩৭
- ৮১
DPLL চলার মাঝে একটি clause (¬x ∨ y ∨ z)-এ y আর z দুটোই মিথ্যা বসে গেছে, x এখনো খোলা। পরের ধাপে কী হয়?
- x = F বাধ্যতামূলকভাবে বসে
- x = T বাধ্যতামূলকভাবে বসে
- বিরোধ, তাই backtrack
- x-এ অনুমান করে দুই শাখা চেষ্টা করা হয়
Python-এ x = 0 হলে x != 0 and 10 / x > 2 কী দেয়?
- ZeroDivisionError
- True
- None
- False
একটি দোকানের ২০টি নিয়মকে CNF-এ লিখে SAT solver-এ দেওয়া হলো, উত্তর এলো UNSAT. এর সঠিক অর্থ কী?
- ২০টি নিয়মের সবগুলোই ভুল
- কোনো পরিস্থিতিতেই সবকটি নিয়ম একসঙ্গে মানা সম্ভব নয়
- Solver সময়ের মধ্যে উত্তর খুঁজে পায়নি
- নিয়মগুলো মানার অন্তত একটি উপায় আছে
SAT সমস্যা সম্পর্কে কোন কথাটি সঠিক?
- প্রমাণিত হয়েছে যে SAT-এর কোনো polynomial সময়ের পদ্ধতি নেই
- 2-SAT NP-complete
- SAT NP-complete, আর সব ক্ষেত্রে polynomial সময়ের কোনো পদ্ধতি জানা নেই
- আধুনিক CDCL solver যেকোনো সূত্র কয়েক সেকেন্ডে মেলায়
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- if-শর্ত = সূত্র: De Morgan, double negation আর distributive দিয়ে সরল করুন, তারপর সব সারিতে (বা Z3-এ) মিলিয়ে নিন; কোডে পাহারাদার শর্ত বাঁয়ে রাখুন।
- Specification-এর consistency = satisfiability; সব clause-এ negative literal থাকলে বিরোধ অসম্ভব, আর UNSAT হলে ক্ষুদ্রতম core দেখায় কোন নিয়ম বদলাতে হবে।
- “ঠিক একটি” = ১টি “অন্তত একটি” clause + C(n, 2)টি জোড়া-clause; 9 × 9 Sudoku-তে ৭২৯ variable, ১১,৯৮৮ নিয়ম-clause.
- DPLL = unit propagation + শাখা + backtracking, induction-এ সঠিক বলে প্রমাণিত; CDCL বিরোধ থেকে clause শিখে বাস্তবে বহুগুণ দ্রুত।
- SAT NP-complete (Cook–Levin), তবু বাস্তবের সূত্র প্রায়ই সহজ; SMT (Z3) সংখ্যাওয়ালা নিয়মে প্রমাণ আর counterexample দেয়।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ০৮ · প্রমাণের পদ্ধতি — direct, contrapositive আর contradictionপরবর্তী পর্বমডিউল ২-এর শুরু: যুক্তি থেকে প্রমাণ লেখা।
- ডিসক্রিট ম্যাথমেটিক্স · কোর্স হোম৮৩টি পর্বপুরো কোর্সের মানচিত্র আর ১৪টি মডিউল।
- DSA · পর্ব ৬৫ · NP-hard সমস্যা বাস্তবেSolver-এর অ্যালগরিদমSAT solver, ILP আর heuristic কীভাবে কঠিন সমস্যা সামলায়।
- AI-এর ভিত্তি · পর্ব ৪৩ · Constraint satisfactionAI-তে constraintরুটিন, Sudoku আর backtracking, AI-এর দৃষ্টিতে।
- সফটওয়্যার ইঞ্জিনিয়ারিং · পর্ব ৪৭ · Test case নকশাশর্তের পরীক্ষাDecision table আর boundary value: শর্তের প্রতিটি সারি পরীক্ষায় আনা।