সরাসরি পর্বে যান
পর্ব ৩৯ · ৭৯-এর মধ্যে · মডিউল ৭

সমস্যাকে search হিসেবে দেখা — state, action আর goal

Problems as Search — States, Actions, Goals, State Spaces, Tree vs Graph Search and the Generic Search Loop

Taught in Bangla, this lesson shows how to formulate a problem for search with states, actions, a transition model, a goal test and path cost, using a delivery map, the 8-puzzle and the water jug puzzle. A real run counts the 181,440 reachable 8-puzzle states, and in-page code compares tree and graph search inside one generic loop.

এই পর্বে যা শিখবেন

  • কোন ধরনের সমস্যায় search ঠিক পদ্ধতি, আর কোথায় নয়
  • যেকোনো সমস্যাকে state, initial state, action, transition model, goal test আর path cost দিয়ে লেখা; তিনটি সম্পূর্ণ উদাহরণসহ
  • State-এ কী রাখবেন আর কী বাদ দেবেন, আর ভুল নকশা কীভাবে state space ফুলিয়ে দেয়
  • 8-puzzle-এর state গোনা, parity-র যুক্তি, আর search tree বনাম state graph-এর পার্থক্য
  • Frontier-কে parameter রেখে একটি generic search loop Python-এ লেখা, আর b, d, m দিয়ে algorithm-এর খরচ মাপা

এই পর্ব দিয়ে মডিউল ৭ শুরু। মডিউল ৩ থেকে ৬ পর্যন্ত আপনি ডেটা নিয়ে কাজ করেছেন: array, টেবিল, chart আর সম্ভাবনা। এবার AI-এর আরেকটি পুরোনো আর এখনো খুব কাজের দিক: শেখার ডেটা ছাড়াই, শুধু সমস্যার নিয়ম জেনে সমাধান খুঁজে বের করা। চলমান উদাহরণ বাজারঘর (এই কোর্সের কাল্পনিক অনলাইন দোকান)। তার একজন ডেলিভারি রাইডার মিরপুরের গুদাম থেকে মতিঝিলে একটি পার্সেল নেবেন। কোন পথে যাবেন? প্রশ্নটি সহজ, কিন্তু কম্পিউটারকে দিয়ে উত্তর বের করাতে হলে আগে প্রশ্নটিকে নিখুঁতভাবে লিখতে হবে। এই পর্বের পুরো বিষয় সেই লেখার কৌশল। কোন algorithm দিয়ে খুঁজবেন, তা পরের পাঁচটি পর্বে।

১ · Agent থেকে search — কখন এটি ঠিক টুল

পর্ব ১-এ আপনি agent দেখেছেন: যে environment থেকে percept পায় আর action নেয়। সেখানে পরিবেশের ছয়টি বৈশিষ্ট্য আর পাঁচ ধরনের agent ছিল, তাই এখানে আবার বলা হবে না। শুধু একটি ধরন মনে করিয়ে দেওয়া দরকার: goal-based agent, যাকে একটি লক্ষ্য দেওয়া হয় আর সে ভেবে দেখে কোন কাজের ধারাবাহিকতা লক্ষ্যে নিয়ে যাবে। এই “ভেবে দেখা”-র সবচেয়ে সরল রূপের নাম search. পর্ব ৫-এর সমস্যার মানচিত্রে search আর planning আলাদা একটি পরিবার ছিল, যার আউটপুট একটি লেবেল বা সংখ্যা নয়, একটি সমাধান: কাজের একটি ক্রম। আর পর্ব ৩-এর তিন পথের মধ্যে এটি নিয়মভিত্তিক (symbolic) পথের অংশ: এখানে কোনো ডেটা থেকে শেখা নেই, সমস্যার নিয়ম আমরা নিজেরাই লিখে দিই।

নতুন শহরে একজন রিকশাওয়ালাকে ভাবুন, যাঁর হাতে একটি নির্ভুল মানচিত্র আছে আর রাস্তায় কোনো চমক নেই। তিনি রিকশা ছাড়ার আগেই মানচিত্রে আঙুল রেখে পুরো পথ ঠিক করে ফেলতে পারেন: এই মোড়ে ডানে, তারপর সোজা, তারপর বাঁয়ে। পথ খোঁজা হয় মাথার ভেতরে, চালানো পরে। Search-ভিত্তিক agent ঠিক এভাবে কাজ করে: আগে পুরো সমাধান খোঁজে, তারপর চোখ বন্ধ করে একটির পর একটি কাজ করে যায়।

এই “চোখ বন্ধ করে চালানো” তখনই নিরাপদ, যখন পরিবেশ চারটি শর্ত মোটামুটি মানে:

শর্তমানেনা মানলে কী হয়
Fully observableAgent জানে সে এখন ঠিক কোন অবস্থায়শুরুর অবস্থাই অনিশ্চিত; একাধিক সম্ভাব্য অবস্থা একসঙ্গে মাথায় রাখতে হয়
Deterministicএকটি কাজের ফল নিশ্চিতআগে ঠিক করা পথ মাঝপথে ভেঙে যায়; পর্ব ৫৪-এর MDP-র মতো পদ্ধতি লাগে
Known (জানা নিয়ম)কোন অবস্থায় কোন কাজে কী হয়, তা আগে থেকে লেখা যায়নিয়ম শিখতে হয়, যা reinforcement learning-এর এলাকা
Discrete ও staticঅবস্থা আর কাজ গোনা যায়, ভাবার সময় পরিবেশ স্থিরঅবিচ্ছিন্ন জগতে আগে ভাগ করে নিতে হয়; দ্রুত বদলালে বারবার নতুন করে খুঁজতে হয়

ধাঁধা (8-puzzle, Sudoku), রুট খোঁজা আর কারখানার কাজের ক্রম এই শর্তগুলোর কাছাকাছি। আবার পরের মাসের বিক্রি অনুমান বা একটি বাংলা রিভিউয়ের মনোভাব বোঝা search-এর সমস্যা নয়: সেখানে “কাজের ক্রম” বলে কিছু নেই, আছে ডেটা থেকে একটি অনুমান, যা মডিউল ১০-এর বিষয়।

বাস্তব কখনো পুরো শর্ত মানে না। আসল রাস্তায় জ্যাম আছে, রাস্তা বন্ধ হয়। তবু রুট খোঁজার অ্যাপ search ব্যবহার করে, কারণ তারা একটি সরল মডেলে খোঁজে আর নতুন খবর এলে আবার খোঁজে। Search-এর ফল সব সময় মডেলের মতোই ভালো, তার চেয়ে বেশি নয়। মডেল কীভাবে বানাবেন, তা অংশ ৪-এ।

২ · সমস্যার ছয়টি অংশ — রাইডারের রোড ম্যাপ

কম্পিউটারের কাছে “মতিঝিলে যাওয়ার ভালো পথ কোনটি?” কোনো স্পষ্ট প্রশ্নই নয়। তাকে ছয়টি জিনিস নির্দিষ্ট করে দিতে হয়। এই ছয়টি মিলিয়ে লেখাকে বলে problem formulation. Russell ও Norvig-এর “Artificial Intelligence: A Modern Approach” (চতুর্থ সংস্করণ, ২০২০/২০২১) বইয়ে এই কাঠামো যেভাবে সাজানো, বিশ্ববিদ্যালয়ের AI কোর্সে সাধারণত সেভাবেই পড়ানো হয়; নিচের উদাহরণগুলো এই কোর্সের নিজের।

১. State

জগতের একটি অবস্থার বর্ণনা, যতটুকু সিদ্ধান্তের জন্য দরকার। সব সম্ভাব্য state-এর সেটকে বলে state space.

২. Initial state

Agent যেখান থেকে শুরু করে। যেমন: রাইডার মিরপুরের গুদামে।

৩. Actions(s)

State s-এ কোন কোন কাজ করা যায়, তার তালিকা। মিরপুর থেকে চারটি রাস্তা, তাই চারটি action.

৪. Transition model

Result(s, a): state s-এ action a নিলে কোন নতুন state হয়। একটি state থেকে এক ধাপে পৌঁছানো state-গুলোকে বলে তার successor.

৫. Goal test

একটি state লক্ষ্য কি না, তা বলার নিয়ম। একটিমাত্র লক্ষ্য হতে পারে (মতিঝিল), আবার একটি শর্তও হতে পারে (“যেকোনো খোলা ফার্মেসি”)।

৬. Path cost

একটি পথের মোট খরচ, সাধারণত প্রতিটি ধাপের খরচ c(s, a, s′)-এর যোগ। এখানে কিলোমিটার; অন্য সমস্যায় সময়, টাকা বা শুধু ধাপের সংখ্যা।

এই ছয়টি দিলে একটি সমাধান মানে initial state থেকে লক্ষ্যে নিয়ে যাওয়া action-এর একটি ক্রম। সব সমাধানের মধ্যে যার path cost সবচেয়ে কম, সেটি optimal solution. লক্ষ করুন, ছয়টির মধ্যে প্রথম চারটি মিলে একটি graph তৈরি হয়: state হলো node, action হলো তীর, আর ধাপের খরচ তীরের ওজন। এই graph-কে বলে state-space graph. এখন রাইডারের জগৎটি দেখুন। মানচিত্রটি বানানো; এলাকার নাম সত্যি, কিলোমিটারগুলো নয়।

রাইডারের কাল্পনিক রোড ম্যাপ (দূরত্ব বানানো) 5 7 6 12 3 4 4 6 6 2 9 3 7 4 উত্তরা মিরপুর বনানী গুলশান বাড্ডা ফার্মগেট মোহাম্মদপুর ধানমন্ডি মতিঝিল কম রাস্তা: ১৩ কিমিকম কিমি: ১২ কিমি সবুজ = শুরু (গুদাম), লাল = লক্ষ্য; সংখ্যাগুলো বানানো কিমি
নয়টি এলাকা, চৌদ্দটি দুই-মুখী রাস্তা। কম রাস্তার পথ (নীল, দুটি রাস্তা) আর কম কিলোমিটারের পথ (সবুজ, তিনটি রাস্তা) আলাদা; কোন algorithm কোনটি খুঁজে পায়, তা অংশ ৭-এ দেখবেন।

এবার ছয়টি অংশ এক এক করে ভরুন। Stepper-এর কোডের প্রতিটি লাইন একটি অংশের Python রূপ।

1states = set(ROADS)          # ৯টি এলাকা2initial = "Mirpur"3def actions(s): return list(ROADS[s])4def result(s, a): return a5def is_goal(s): return s == "Motijheel"6def step_cost(s, a): return ROADS[s][a]7plan = search(initial, ...)
সমস্যার ছক
state
initial
actions
result
goal test
path cost
  1. State. সিদ্ধান্তের জন্য শুধু একটি তথ্য লাগে: রাইডার এখন কোন এলাকায়। তাই state space-এ ৯টি state. বাইকের রং বা আবহাওয়া বাদ, কারণ পথ বাছাইয়ে তারা এই মডেলে ভূমিকা রাখে না।
  2. Initial state. মিরপুর, যেখানে বাজারঘরের গুদাম। একই মানচিত্রে অন্য গুদাম থেকে শুরু করলে শুধু এই লাইনটি বদলাবে।
  3. Actions. মিরপুর থেকে চারটি রাস্তা বেরিয়েছে, তাই Actions(মিরপুর)-এ চারটি কাজ: Go(মোহাম্মদপুর), Go(ফার্মগেট), Go(বনানী), Go(উত্তরা)। অন্য এলাকায় তালিকাটি আলাদা।
  4. Transition model. এখানে নিয়মটি সরল: Go(X) নিলে রাইডার X-এ পৌঁছান, নিশ্চিতভাবে। এই “নিশ্চিতভাবে”-ই deterministic ধরে নেওয়া; বাস্তবে রাস্তা বন্ধ থাকলে মডেল ভুল।
  5. Goal test. state মতিঝিল হলেই লক্ষ্য পূরণ। এটি একটি সরল তুলনা, কিন্তু অনেক সমস্যায় লক্ষ্য একটি শর্ত, যেমন 8-puzzle-এ “সব টাইল ঠিক ঘরে”।
  6. Path cost. প্রতিটি রাস্তার খরচ তার কিমি, আর পথের খরচ সেগুলোর যোগ। কী মাপবেন, তা সমস্যার মালিকের সিদ্ধান্ত।
  7. সমাধান। ছয়টি অংশ মিলে সমস্যাটি সম্পূর্ণ। এখন যেকোনো search algorithm একে নিতে পারে। হাতে মিলিয়ে দেখুন: মোহাম্মদপুর আর ধানমন্ডি হয়ে 5 + 3 + 4 = 12 কিমি, যা ফার্মগেট হয়ে 7 + 6 = 13 কিমির চেয়ে কম।
মূল ধারণা

Search algorithm সমস্যার অর্থ জানে না; সে শুধু এই ছয়টি function ডাকে। তাই একই algorithm রুট, ধাঁধা আর সময়সূচি সব সমাধান করতে পারে, যদি আপনি সমস্যাটি এই ছাঁচে লিখে দেন। আর ফল ভুল হলে প্রায়ই দোষ algorithm-এর নয়, formulation-এর।

৩ · আরও দুটি formulation — 8-puzzle আর পানির জগ

8-puzzle

8-puzzle একটি ৩ × ৩ বোর্ড, যেখানে ১ থেকে ৮ নম্বরের আটটি টাইল আর একটি ফাঁকা ঘর। ফাঁকা ঘরের পাশের একটি টাইল ঠেলে ফাঁকায় আনা যায়। লক্ষ্য একটি নির্দিষ্ট সাজানো বিন্যাস। ছয়টি অংশ:

  • State: ৯টি ঘরের কোনটিতে কোন টাইল, ফাঁকা ঘরকে 0 ধরে ৯টি সংখ্যার একটি tuple, যেমন (1, 2, 3, 4, 0, 6, 7, 5, 8).
  • Initial state: যেকোনো বিন্যাস, যা ধাঁধাটি দেয়।
  • Actions: টাইলের বদলে ফাঁকা ঘরকে সরানো ভাবলে সহজ: Up, Down, Left, Right, বোর্ডের কিনারা যতটা দেয়। মাঝখানে ফাঁকা থাকলে ৪টি, কিনারার মাঝে ৩টি, কোণে ২টি।
  • Transition model: ফাঁকা ঘর আর সেই দিকের পাশের টাইল জায়গা বদল করে।
  • Goal test: state == (1, 2, 3, 4, 5, 6, 7, 8, 0).
  • Path cost: প্রতিটি চাল ১, তাই পথের খরচ মানে চালের সংখ্যা।

নিচের কোড এই ছয়টি অংশকে একটি ছোট class-এ রাখে (class কী, তা Python কোর্সে)। এটি একটি state থেকে সব successor ছাপে, আর একটি ধাপ বেশি চালিয়ে লক্ষ্য খোঁজে। শেষে inversions নামের একটি গণনা আছে, যার মানে অংশ ৫-এ পরিষ্কার হবে। প্রথমবার চালালে ব্রাউজারে Python নামতে কয়েক সেকেন্ড লাগতে পারে।

GOAL = (1, 2, 3, 4, 5, 6, 7, 8, 0)               # 0 = blank
STEP = {"Up": -3, "Down": 3, "Left": -1, "Right": 1}  # where the blank moves

class Puzzle8:
    def __init__(self, tiles):
        self.tiles = tuple(tiles)
    def actions(self):
        r, c = divmod(self.tiles.index(0), 3)
        ok = {"Up": r > 0, "Down": r < 2, "Left": c > 0, "Right": c < 2}
        return [a for a in STEP if ok[a]]
    def result(self, action):                    # transition model
        t = list(self.tiles)
        i = t.index(0); j = i + STEP[action]
        t[i], t[j] = t[j], t[i]                  # slide one tile into the gap
        return Puzzle8(t)
    def is_goal(self):
        return self.tiles == GOAL
    def inversions(self):                        # pairs out of order, blank ignored
        t = [x for x in self.tiles if x]
        return sum(t[i] > t[j] for i in range(8) for j in range(i + 1, 8))
    def __str__(self):
        s = "".join(str(x) if x else "_" for x in self.tiles)
        return " ".join(s[k:k + 3] for k in (0, 3, 6))

start = Puzzle8([1, 2, 3, 4, 0, 6, 7, 5, 8])
print("start:", start, "| goal?", start.is_goal(), "| inversions:", start.inversions())
for a in start.actions():
    child = start.result(a)
    print(f"  {a:<5} -> {child}  goal? {child.is_goal()}")
two = start.result("Down").result("Right")
print("Down, Right:", two, "| goal?", two.is_goal())
swapped = Puzzle8([1, 2, 3, 4, 5, 6, 8, 7, 0])
print("8 and 7 swapped:", swapped, "| inversions:", swapped.inversions())
print("corner blank actions:", Puzzle8(GOAL).actions())

আসল আউটপুট:

start: 123 4_6 758 | goal? False | inversions: 2
  Up    -> 1_3 426 758  goal? False
  Down  -> 123 456 7_8  goal? False
  Left  -> 123 _46 758  goal? False
  Right -> 123 46_ 758  goal? False
Down, Right: 123 456 78_ | goal? True
8 and 7 swapped: 123 456 87_ | inversions: 1
corner blank actions: ['Up', 'Left']

বোর্ডটি এক লাইনে তিনটি সারি হিসেবে ছাপা, “_” মানে ফাঁকা। মাঝখানে ফাঁকা বলে চারটি action, আর প্রতিটি থেকে একটি আলাদা successor; কোনোটিই লক্ষ্য নয়। দুই চাল (Down, তারপর Right) দিলে লক্ষ্য, তাই এই শুরুর state-এর optimal সমাধানের খরচ ২। লক্ষ্যে ফাঁকা ঘর ডান-নিচের কোণে, তাই সেখান থেকে মাত্র দুটি action: Up আর Left. State-টি tuple, list নয়; এর কারণ অংশ ৬-এ, যখন state-কে একটি set-এ রাখতে হবে। শেষের দুই লাইনের inversion সংখ্যা ২ আর ১: একটি জোড়, একটি বিজোড়। এই পার্থক্যটি অংশ ৫-এ পুরো গল্প বলবে।

একটি state থেকে successor বানানোর কাজটিকে বলে expansion. নিচে একই শুরুর বোর্ড ধাপে ধাপে expand করে দেখুন।

8-puzzle-এর একটি state থেকে successor 12346758 শুরু (depth 0) Up 13426758 Down 12345678 Left 12346758 Right 12346758 Upআগের state! 12346758 Leftdepth 2 12345678 Rightলক্ষ্য 12345678
  1. শুরুর state. 123 / 4_6 / 758. ফাঁকা ঘর মাঝখানে, তাই Actions-এ চারটি কাজ। হলুদ টাইল প্রতিটি successor-এ কোন টাইলটি সরেছে, তা দেখাবে।
  2. Up. ফাঁকা ঘর ওপরে যায়, তাই ওপরের টাইল 2 নিচে নামে: 1_3 / 426 / 758. লক্ষ্য নয়।
  3. Down. ফাঁকা ঘর নিচে যায়, টাইল 5 ওপরে ওঠে: 123 / 456 / 7_8. ওপরের দুই সারি এখন লক্ষ্যের মতো।
  4. Left. টাইল 4 ডানে সরে: 123 / _46 / 758. লক্ষ্য নয়।
  5. Right. টাইল 6 বাঁয়ে সরে: 123 / 46_ / 758. চারটি successor তৈরি, কোনোটিই লক্ষ্য নয়; এরা এখন frontier-এ অপেক্ষা করবে।
  6. Down-কে expand. 7_8-এর নিচের সারিতে ফাঁকা, তাই তিনটি কাজ। Up আবার শুরুর state ফেরত দেয়, মানে একটি repeated state; Left দেয় _78; আর Right দেয় 123 / 456 / 78_, যা লক্ষ্য। Depth ২-এ সমাধান।

পানির জগের ধাঁধা

পাড়ার চায়ের দোকানে একটি অর্ডারের জন্য ঠিক ৪ লিটার দুধ মাপা দরকার। মাপার দাগ নেই, আছে শুধু একটি ৫ লিটারের আর একটি ৩ লিটারের জগ, আর একটি কল। ধাঁধাটি পুরোনো আর নানা রূপে পরিচিত; এর formulation এমন:

  • State: (a, b), যেখানে a = বড় জগে কত লিটার (0–5), b = ছোট জগে কত (0–3). মোট 6 × 4 = 24টি সম্ভাব্য জোড়া।
  • Initial state: (0, 0), দুটিই খালি।
  • Actions: ছয়টি: বড়টি ভরা, ছোটটি ভরা, বড়টি খালি করা, ছোটটি খালি করা, বড় থেকে ছোটতে ঢালা, ছোট থেকে বড়তে ঢালা।
  • Transition model: ঢালার নিয়মটিই আসল কাজ: বড় থেকে ছোটতে ঢালা যায় min(a, 3 − b) লিটার, মানে যতক্ষণ না বড়টি খালি বা ছোটটি ভরা হয়।
  • Goal test: a == 4. ছোট জগে কত আছে, তাতে কিছু যায় আসে না, তাই লক্ষ্য একটি state নয়, একটি শর্ত।
  • Path cost: প্রতিটি কাজ ১।

এই formulation অংশ ৭-এর generic loop-এ বসালে (অনুশীলন ৪-এ নিজে লিখে দেখবেন) সমাধান আসে ৬ ধাপে: বড়টি ভরা (5, 0), ছোটতে ঢালা (2, 3), ছোটটি খালি (2, 0), আবার ঢালা (0, 2), বড়টি ভরা (5, 2), ছোটতে ঢালা (4, 3). হাতে মিলিয়ে নিন: শেষ ধাপে ছোট জগে মাত্র ১ লিটারের জায়গা ছিল, তাই বড়টিতে ঠিক ৪ লিটার থাকে। ২৪টি জোড়ার মধ্যে কিন্তু শুরু থেকে মাত্র ১৬টিতে পৌঁছানো যায়; যেমন (2, 2) কখনো হয় না, কারণ প্রতিটি কাজের পর অন্তত একটি জগ হয় খালি নয়তো ভরা থাকে।

৪ · Abstraction — state-এ কী রাখবেন, কী বাদ দেবেন

রাইডারের আসল যাত্রায় হাজারটা জিনিস আছে: বাইকের তেলের মিলিলিটার, রেডিওতে কী গান বাজছে, আকাশে মেঘ, প্রতিটি সেকেন্ডের GPS অবস্থান। আমরা এসব ফেলে দিয়ে শুধু “কোন এলাকায়” রেখেছি। এই বাদ দেওয়ার নাম abstraction. এটি অলসতা নয়, এটিই formulation-এর সবচেয়ে বড় সিদ্ধান্ত।

ঢাকার মেট্রো রেলের ম্যাপ মনে করুন। তাতে স্টেশনগুলো সমান দূরত্বে আর সোজা রেখায় আঁকা; আসল দূরত্ব, বাঁক, পাশের বাড়িঘর কিছুই নেই। তবু “উত্তরা থেকে মতিঝিল কোন স্টেশন হয়ে” প্রশ্নে এটিই সবচেয়ে কাজের ম্যাপ, কারণ প্রশ্নের জন্য যা অপ্রাসঙ্গিক, তা বাদ দেওয়া হয়েছে। কিন্তু হেঁটে স্টেশন থেকে অফিসে যেতে এই ম্যাপ কোনো কাজে আসে না। ভালো abstraction সব সময় একটি নির্দিষ্ট প্রশ্নের জন্য ভালো।

একটি abstraction ঠিক আছে কি না, তা যাচাইয়ের একটি সহজ পরীক্ষা: সরল মডেলের প্রতিটি সমাধান কি আসল জগতে চালানো যায়? “মিরপুর থেকে ফার্মগেট” একটি action হিসেবে কাজ করে, কারণ আসল রাইডার যেকোনো সময় এটি করতে পারেন, ভেতরের খুঁটিনাটি তিনি নিজেই সামলান। দ্বিতীয় পরীক্ষা: বাদ দেওয়া কোনো তথ্য কি সিদ্ধান্ত বদলে দিতে পারে? যদি তেল এত কম থাকে যে উত্তরা ঘুরে আসা যায় না, তাহলে তেল বাদ দেওয়া abstraction আর ঠিক নেই।

একই সমস্যার খারাপ আর ভালো state নকশা

বাজারঘরের একটি নতুন সমস্যা ধরুন। একজন রাইডার গুদাম থেকে ৩টি পার্সেল তিনটি ঠিকানায় দেবেন, যেকোনো ক্রমে, তারপর গুদামে ফিরবেন। State-এ কী থাকবে?

নকশাState-এ যা আছেসমস্যা
খারাপ ১ (খুব বেশি)রাইডারের GPS (মিটার পর্যন্ত), ঘড়ির সময় (সেকেন্ড পর্যন্ত), তেলের মিলিলিটার, আবহাওয়া, কোন পার্সেল বাকিপ্রায় প্রতিটি state আলাদা, একই এলাকায় দুবার পৌঁছানোও “নতুন” state; state space বিশাল, অথচ বেশিরভাগ তথ্য পথের সিদ্ধান্তে লাগে না
খারাপ ২ (খুব কম)শুধু রাইডার এখন কোন এলাকায়কোন পার্সেল দেওয়া হয়েছে, তা জানা নেই; তাই goal test (“সব দেওয়া হয়েছে আর গুদামে”) লেখাই যায় না। গুদামে ফিরে আসা শুরুর state-এর সঙ্গে এক হয়ে যায়
ভালো(এখনকার এলাকা, কোন পার্সেল বাকি) — যেমন (ফার্মগেট, {ধানমন্ডি, বাড্ডা})সিদ্ধান্তের জন্য যা লাগে, ঠিক তা আছে। ৯টি এলাকা × 23 = 8 রকম বাকি-তালিকা, মানে 9 × 8 = 72টি state পর্যন্ত

লক্ষ করুন, “খুব কম” নকশাটি দেখতে সবচেয়ে সরল, কিন্তু এটি ভুল উত্তর দেবে, কারণ এতে দুটি সত্যিকারের ভিন্ন অবস্থা এক হয়ে গেছে। তাই নিয়মটি দুই দিকেই খাটে: সিদ্ধান্ত বদলাতে পারে এমন সব কিছু রাখুন, বাকি সব ফেলে দিন। পার্সেলের সংখ্যা বাড়ালে এই state space দ্রুত বাড়ে: ২০টি পার্সেলে বাকি-তালিকা 220 = 1,048,576 রকম। এটি travelling salesman ধরনের সমস্যা, যার কঠিনতার কথা মডিউল ৭-এর শেষ পর্বগুলোতে আবার আসবে।

Action-এর abstraction-ও আছে। “Go(ফার্মগেট)” একটি বড় কাজ, যার ভেতরে আছে শত শত ছোট কাজ: গিয়ার বদল, মোড় ঘোরা, সিগন্যালে থামা। কোন স্তরে action লিখবেন, তাও abstraction-এর সিদ্ধান্ত। খুব ছোট স্তরে (প্রতিটি স্টিয়ারিং ঘোরানো) লিখলে পথ হাজার ধাপ লম্বা আর search অসম্ভব; খুব বড় স্তরে লিখলে (“মতিঝিলে পৌঁছানো”) search-এর কিছুই বাকি থাকে না।

৫ · State space কত বড় — 8-puzzle-এর 181,440

৯টি ঘরে ৮টি টাইল আর একটি ফাঁকা ঘর যত রকমে সাজানো যায়, তা 9! = 9 × 8 × … × 1 = 362,880 (! চিহ্নের মানে factorial, ১ পর্যন্ত সব সংখ্যার গুণফল)। কিন্তু একটি চমক আছে: এর ঠিক অর্ধেক বিন্যাস লক্ষ্য থেকে কখনো পৌঁছানো যায় না, যত চালই দিন। কেন?

Parity-র যুক্তি, সহজ ভাষায়

ফাঁকা ঘর বাদ দিয়ে টাইলগুলো বাঁ থেকে ডানে, ওপর থেকে নিচে একটি সারিতে পড়ুন। তারপর গুনুন কতগুলো জোড়া “উল্টো ক্রমে” আছে, মানে বড় সংখ্যা ছোট সংখ্যার আগে। একে বলে inversion-এর সংখ্যা। লক্ষ্যে (12345678) একটিও নেই, তাই 0. ওপরের কোডের শুরুর বোর্ড 123 4_6 758 পড়লে 1 2 3 4 6 7 5 8: উল্টো জোড়া (6, 5) আর (7, 5), মোট ২।

এবার দেখুন একটি চালে এই সংখ্যার কী হয়। পাশাপাশি চাল (Left বা Right) টাইলের পড়ার ক্রম বদলায় না, কারণ টাইলটি একই সারিতে ফাঁকার এক ঘর এদিক-ওদিক যায়; inversion একই থাকে। ওপর-নিচের চাল (Up বা Down) একটি টাইলকে পড়ার ক্রমে ঠিক দুটি টাইল ডিঙিয়ে নিয়ে যায় (একই সারির বাকি দুটি ঘর)। সেই দুটি টাইলের প্রতিটির সঙ্গে জোড়াটি হয় উল্টো হয়, নয়তো সোজা হয়; তাই inversion বদলায় +2, 0 বা −2. দুটি ক্ষেত্রেই inversion-এর জোড়-বিজোড় (parity) বদলায় না। লক্ষ্যের inversion 0, জোড়; তাই লক্ষ্য থেকে পৌঁছানো প্রতিটি বিন্যাসের inversion জোড়। যে বিন্যাসে inversion বিজোড়, যেমন ওপরের “8 আর 7 অদলবদল” (১টি), তা কখনো সমাধান করা যায় না।

এই যুক্তি দেখায় “অর্ধেকের বেশি পৌঁছানো যায় না”। ঠিক অর্ধেকই যে পৌঁছানো যায়, অর্থাৎ জোড় parity-র প্রতিটি বিন্যাস, তার প্রমাণ একটু লম্বা। তার বদলে আমরা গুনে দেখি: লক্ষ্য থেকে BFS চালিয়ে (যার বিস্তারিত পর্ব ৪০-এ) প্রতিটি পৌঁছানো state একবার করে গোনা। পুরো গণনা ব্রাউজারে চালানো যায়, কিন্তু কয়েক সেকেন্ড পাতা আটকে থাকতে পারে, তাই এটি লেখকের যন্ত্রে একটি সাধারণ Python script হিসেবে চালানো হয়েছে।

# count_states.py (মূল অংশ; পুরো script-এ জগের ধাঁধাও আছে)
GOAL = (1, 2, 3, 4, 5, 6, 7, 8, 0)
NEIGH = {i: [j for j in (i - 3, i + 3, i - 1, i + 1)
             if 0 <= j < 9 and (j // 3 == i // 3 or j % 3 == i % 3)]
         for i in range(9)}
depth = {GOAL: 0}
q = deque([GOAL])
while q:
    s = q.popleft()
    i = s.index(0)
    for j in NEIGH[i]:
        t = list(s); t[i], t[j] = t[j], t[i]; t = tuple(t)
        if t not in depth:
            depth[t] = depth[s] + 1
            q.append(t)
…

সত্যিকারের run, Python 3.14.4 (শুধু standard library):

~/bazarghar-ai$ python count_states.py
Python 3.14.4
all arrangements 9!     : 362,880
reachable from goal     : 181,440
9!/2                    : 181,440
odd-inversion reachable : 0
even-inversion arrangements: 181,440
deepest optimal solution: 31 moves, states: 2
mean optimal solution   : 21.97 moves
states by depth 0..10   : [1, 2, 4, 8, 16, 20, 39, 62, 116, 152, 286]
BFS time                : 0.23 s
15-puzzle 16!/2         : 10,461,394,944,000
jug states reachable    : 16 of 24
jug plan                : fill5 pour5to3 empty3 pour5to3 fill5 pour5to3 | steps: 6

গণনা যুক্তির সঙ্গে হুবহু মিলেছে: লক্ষ্য থেকে 181,440টি state-এ পৌঁছানো যায়, যা 9!/2. তাদের একটিরও inversion বিজোড় নয়, আর জোড় inversion-এর মোট বিন্যাসও ঠিক 181,440; মানে জোড় parity-র প্রতিটি বিন্যাসই পৌঁছানো যায়। BFS প্রতিটি state-এর লক্ষ্য থেকে সবচেয়ে কম দূরত্বও দিয়েছে: সবচেয়ে কঠিন state-এর জন্য ৩১ চাল লাগে, আর এমন state মাত্র ২টি; গড়ে প্রায় ২২ চাল। শুরুর দিকে প্রতি depth-এ state প্রায় দ্বিগুণ হচ্ছে (1, 2, 4, 8, 16), তারপর বৃদ্ধি একটু ধীর, কারণ অনেক পথ আগে দেখা state-এ ফিরে আসে। সময়টি একটি সাধারণ ল্যাপটপে এক বারের run, সাধারণ মাপ নয়। শেষ দুই লাইন জগের ধাঁধার: ২৪টির মধ্যে ১৬টি জোড়ায় পৌঁছানো যায়, আর সমাধান ৬ ধাপে।

15-puzzle কেন অনেক বড়

৪ × ৪ বোর্ডের 15-puzzle-এ ১৬টি ঘর। একই ধরনের parity যুক্তি (তবে বোর্ডের চওড়া জোড় বলে নিয়মে ফাঁকা ঘরের সারিও গুনতে হয়) দেখায় যে এখানেও ঠিক অর্ধেক পৌঁছানো যায়: 16!/2 = 10,461,394,944,000, প্রায় 1.05 × 1013. বোর্ডে মাত্র সাতটি ঘর বাড়তেই state বেড়েছে 16!/9! = 57,657,600 গুণ, প্রায় পৌনে ছয় কোটি গুণ। প্রতিটি state-এর জন্য মাত্র ১ byte রাখলেও প্রায় ১০ terabyte লাগবে; তাই 15-puzzle-এ “সব state গুনে রাখা” আর সাধারণ যন্ত্রে চলে না, আর 24-puzzle (৫ × ৫)-এ সংখ্যাটি প্রায় 7.8 × 1024. এখানেই heuristic-এর দরকার, যা পর্ব ৪১-এর বিষয়। এই হঠাৎ বিস্ফোরণকে বলে combinatorial explosion.

৬ · Search tree বনাম state graph — repeated state আর explored set

State-space graph হলো জগতের মানচিত্র: প্রতিটি state একবার। Search algorithm কিন্তু মানচিত্রটি আগে থেকে হাতে পায় না; সে শুরু থেকে এক এক করে পথ বাড়ায়। সে যে পথগুলো ধরে, সেগুলো একটি গাছ তৈরি করে, যার নাম search tree. গাছের প্রতিটি node একটি পথের শেষ বিন্দু: তাতে থাকে state, কোন parent থেকে এসেছে, কোন action-এ, আর এ পর্যন্ত খরচ। একই state অনেক পথে আসতে পারে, তাই গাছে একই state অনেক node-এ থাকতে পারে।

State graph বনাম search tree S A B C D G state graph: ৬টি state search tree: পথের গাছ S A B S B C S A D লাল = আগেই দেখা state আবার depth 2-তেই ৬টি node
বাঁয়ের graph-এ ৬টি state, কিন্তু S, A আর B মিলে একটি চক্র আছে। ডানের গাছে depth ২-তেই S আবার এসেছে (মূল থেকে A হয়ে ফেরত), আর A ও B একে অপরের নিচে আবার এসেছে।

এই পুনরাবৃত্তিকে বলে repeated state. চক্র থাকলে search tree অসীম: S → A → S → A → … চিরকাল চলতে পারে। চক্র না থাকলেও ক্ষতি আছে: একই state-এ পৌঁছানোর অনেক পথ থাকলে গাছ সেই state-এর নিচের পুরো অংশ বারবার খোঁজে। 8-puzzle-এ প্রতিটি চালের উল্টো চাল আছে (Down-এর পর Up), তাই এখানে চক্র সব জায়গায়; ওপরের stepper-এর শেষ ধাপে আপনি একটি দেখেছেন।

প্রতিকার সহজ: যে state একবার expand হয়েছে, তাদের একটি set-এ লিখে রাখা, আর আবার এলে বাদ দেওয়া। এই set-ই explored set (কোনো বইয়ে reached set বা closed list). এটি রাখলে algorithm-টির নাম graph search, না রাখলে tree search. নিচের কোড ওপরের ছবির graph-টিতে দুটিকে পাশাপাশি চালায় আর expansion গোনে। Tree search-কে ২,০০০ expansion-এ থামানো হয়েছে, যাতে পাতা আটকে না যায়।

from collections import deque

G = {"S": "AB", "A": "SBC", "B": "SAD", "C": "ADG", "D": "BC", "G": "C"}

def run(start, goal, use_explored, cap=2000):
    frontier = deque([(start, start)])           # (state, path so far)
    explored, expanded = set(), 0
    while frontier and expanded < cap:
        state, path = frontier.popleft()         # FIFO frontier
        if state == goal:
            return path, expanded, len(frontier)
        if use_explored:
            if state in explored: continue
            explored.add(state)
        expanded += 1
        for nxt in G[state]:
            if not (use_explored and nxt in explored):
                frontier.append((nxt, path + nxt))
    return None, expanded, len(frontier)

for goal in ["G", "Z"]:
    for name, flag in [("graph search", True), ("tree search ", False)]:
        path, n, left = run("S", goal, flag)
        print(f"goal {goal} | {name} | path {path} | expanded {n:>4} | frontier {left}")

level, sizes = ["S"], []                         # search tree, level by level
for d in range(7):
    sizes.append(len(level))
    level = [n for s in level for n in G[s]]
print("tree nodes per depth:", sizes, "| distinct states:", len(G))

আসল আউটপুট:

goal G | graph search | path SACG | expanded    5 | frontier 0
goal G | tree search  | path SACG | expanded   16 | frontier 25
goal Z | graph search | path None | expanded    6 | frontier 0
goal Z | tree search  | path None | expanded 2000 | frontier 3184
tree nodes per depth: [1, 2, 6, 15, 39, 99, 252] | distinct states: 6

Graph-এর প্রতিটি অক্ষর একটি state, আর "AB" মানে S-এর পাশের state A আর B. লক্ষ্য G হলে দুটিই একই পথ SACG পায়, কিন্তু tree search ১৬টি expansion করেছে আর শেষে frontier-এ ২৫টি node পড়ে আছে, যেখানে graph search ৫টিতে শেষ। দ্বিতীয় পরীক্ষাটি আরও গুরুত্বপূর্ণ: “Z” নামে কোনো state নেই, মানে সমাধান নেই। Graph search ৬টি state দেখে নিশ্চিতভাবে None ফেরত দেয়। Tree search থামতেই পারে না; ২,০০০-এর সীমা না দিলে চিরকাল চলত, আর frontier ততক্ষণে ৩,১৮৪টি node-এ ফুলে গেছে। শেষ লাইনটি কারণ দেখায়: মাত্র ৬টি state থেকেও search tree-র প্রতিটি স্তর প্রায় আড়াই গুণ হচ্ছে, depth ৬-এ ২৫২টি node.

Explored set বিনা মূল্যে আসে না। এটি প্রতিটি দেখা state মনে রাখে, তাই memory লাগে state space-এর আকারের সমান পর্যন্ত। 8-puzzle-এ 181,440টি tuple সহজেই ধরে, 15-puzzle-এর 1013 ধরে না। তাই বড় সমস্যায় অনেক algorithm ইচ্ছা করে tree search-এর দিকে ফেরে, আর শুধু চলতি পথে চক্র আটকায়। State-কে set-এ রাখতে হলে তাকে hash করা যেতে হয় (DSA কোর্সের hash table): তাই Python-এ state tuple বা frozenset, কখনো list নয়।

৭ · Generic search loop — frontier নীতি একটি parameter

এবার মূল কাঠামো। প্রায় প্রতিটি classical search algorithm একই loop: frontier থেকে একটি node তোলা, লক্ষ্য কি না দেখা, না হলে expand করা, আর সন্তানদের frontier-এ রাখা। Frontier হলো সেই node-গুলোর তালিকা, যা তৈরি হয়েছে কিন্তু এখনো expand হয়নি, মানে খোঁজা এলাকার সীমানা। Algorithm-গুলোর পার্থক্য শুধু একটি প্রশ্নে: frontier থেকে পরের কোনটি তোলা হবে?

ডাক্তারের চেম্বারের সামনে রোগীদের সারি ভাবুন। কেউ আগে এসে আগে দেখান (সারি, queue); জরুরি বিভাগে কে সবচেয়ে গুরুতর, তাকে আগে (priority queue); আবার টেবিলে স্তূপ করা ফাইলে সবচেয়ে ওপরেরটি, মানে সবশেষে রাখা ফাইলটি আগে খোলা হয় (stack). চেম্বারের কাজ একই, বদলায় শুধু “পরের জন কে”। Search-এও তাই।

Frontierপরের যে node তোলা হয়ফলে যে algorithmDSA-তে ঝালাই
Queue (FIFO)সবচেয়ে পুরোনোBreadth-first searchQueue ও deque
Stack (LIFO)সবচেয়ে নতুনDepth-first searchStack
Priority queue, g(n) দিয়েসবচেয়ে কম path costUniform-cost search (Dijkstra-র মতো)Heap ও priority queue
Priority queue, g(n) + h(n) দিয়েসবচেয়ে কম “খরচ + অনুমান”A*Dijkstra

Queue, stack আর heap কীভাবে বানানো হয়, তা DSA কোর্সে আছে; এখানে শুধু ব্যবহার। নিচের কোডে search function সমস্যার অর্থ কিছুই জানে না: সে নেয় একটি শুরুর state, একটি goal test, একটি successor function আর একটি frontier object. Frontier-এর শুধু দুটি method লাগে, push আর pop. এখানে একটি FIFO দেওয়া হয়েছে, Python-এর deque থেকে বানিয়ে; পরের পর্বগুলো একই loop-এ অন্য frontier বসাবে।

from collections import deque, defaultdict

EDGES = [("Mirpur", "Mohammadpur", 5), ("Mirpur", "Farmgate", 7),
         ("Mirpur", "Banani", 6), ("Mirpur", "Uttara", 12),
         ("Mohammadpur", "Dhanmondi", 3), ("Mohammadpur", "Farmgate", 4),
         ("Farmgate", "Dhanmondi", 4), ("Farmgate", "Gulshan", 6),
         ("Farmgate", "Motijheel", 6), ("Banani", "Gulshan", 2),
         ("Banani", "Uttara", 9), ("Gulshan", "Badda", 3),
         ("Badda", "Motijheel", 7), ("Dhanmondi", "Motijheel", 4)]
ROADS = defaultdict(dict)                 # invented km, two-way roads
for a, b, km in EDGES:
    ROADS[a][b] = km; ROADS[b][a] = km
class FIFO(deque):                        # frontier policy: first in, first out
    push = deque.append
    pop = deque.popleft

def search(start, is_goal, successors, frontier):
    frontier.push((start, [start], 0))    # (state, path, path cost)
    explored, expanded = set(), 0
    while frontier:
        state, path, cost = frontier.pop()
        if is_goal(state): return path, cost, expanded
        if state in explored: continue
        explored.add(state); expanded += 1
        for nxt, km in successors(state):
            if nxt not in explored:
                frontier.push((nxt, path + [nxt], cost + km))
    return None, None, expanded

path, cost, n = search("Mirpur", lambda s: s == "Motijheel",
                       lambda s: ROADS[s].items(), FIFO())
print("path    :", " -> ".join(path), "|", len(path) - 1, "roads")
print("cost    :", cost, "km | expanded:", n, "of", len(ROADS), "areas")
alt = ["Mirpur", "Mohammadpur", "Dhanmondi", "Motijheel"]
print("another :", sum(ROADS[a][b] for a, b in zip(alt, alt[1:])), "km via Dhanmondi")

আসল আউটপুট:

path    : Mirpur -> Farmgate -> Motijheel | 2 roads
cost    : 13 km | expanded: 7 of 9 areas
another : 12 km via Dhanmondi

FIFO frontier সবচেয়ে কম রাস্তার সংখ্যার পথ পেয়েছে: ফার্মগেট হয়ে দুটি রাস্তা, 13 কিমি। কিন্তু মোহাম্মদপুর আর ধানমন্ডি হয়ে পথটি 12 কিমি, তিনটি রাস্তা হলেও সস্তা। মানে FIFO frontier ধাপ গোনে, কিমি নয়; path cost হিসাব করলেও সেই হিসাব দিয়ে বাছাই করে না। কিমি ধরে সবচেয়ে সস্তা পথ পেতে frontier-কে path cost অনুযায়ী সাজাতে হবে, যা পর্ব ৪০-এর uniform-cost search. আরেকটি বিষয়: Motijheel তৃতীয় expansion-এই (ফার্মগেট থেকে) তৈরি হয়েছিল, কিন্তু loop-টি goal test করে node তোলার সময়, তাই আরও চারটি এলাকা expand করার পর থেমেছে; মোট ৯টির ৭টি। BFS-এ তৈরির সময়েই পরীক্ষা করলে আগে থামা যায়, সেই কৌশলও পর্ব ৪০-এ।

এবার একই loop ছোট ৬ node-এর graph-এ (অংশ ৬-এর graph) এক ধাপ এক ধাপ করে চলতে দেখুন। প্রতিটি ধাপে frontier থেকে বাঁ দিকের node-টি তোলা হয়।

Generic loop-এর ছয় node-এর graph S A B C D G সবুজ = explored, লাল = লক্ষ্য আর পাওয়া পথ
1frontier.push(S)2while frontier:3    s = frontier.pop()4    if is_goal(s): return path5    if s in explored: continue6    explored.add(s)7    push children not explored
মান (Watch)
তোলা node
frontier
explored
expanded
  1. শুরু। Frontier-এ শুধু S, explored খালি, expansion 0.
  2. S তোলা। লক্ষ্য নয়, explored-এ নেই। S-কে explored-এ লিখে expand: সন্তান A আর B frontier-এ যায়। Frontier [A, B].
  3. A তোলা। A-র প্রতিবেশী S, B, C. S explored, তাই বাদ; B আর C যায়। B এখন frontier-এ দুবার: [B, B, C]. এই সরল loop তৈরির সময় শুধু explored দেখে, frontier নয়।
  4. B তোলা। প্রতিবেশী S আর A explored, তাই শুধু D যায়। Frontier [B, C, D], explored {S, A, B}, expansion ৩।
  5. আবার B. B ইতিমধ্যে explored, তাই continue: কোনো expansion নয়। এই একটি লাইনই চক্রে ঘুরপাক আটকায়।
  6. C তোলা। প্রতিবেশী A, B explored; D আর G যায়। G তৈরি হলো, কিন্তু goal test হবে তোলার সময়। Frontier [D, D, G].
  7. D তোলা। প্রতিবেশী B, C দুটিই explored, তাই কোনো সন্তান নেই। Explored-এ এখন ৫টি state, expansion ৫।
  8. আবার D. ইতিমধ্যে explored, বাদ। Frontier-এ শুধু G বাকি।
  9. G তোলা। Goal test সত্য, তাই loop থামে আর পথ ফেরত দেয়: S → A → C → G, ৩টি ধাপ, ৫টি expansion. ওপরের কোডের graph search-এর সংখ্যার সঙ্গে হুবহু মিলেছে।

আরও গভীরে: Russell ও Norvig-এর চতুর্থ সংস্করণ এই loop-টিকে “best-first search” নামে একটি সাধারণ রূপে লেখে, যেখানে frontier একটি evaluation function f(n) দিয়ে সাজানো priority queue, আর explored set-এর বদলে একটি reached dictionary থাকে, যা প্রতিটি state-এর এ পর্যন্ত পাওয়া সবচেয়ে ভালো node মনে রাখে। তখন কোনো state-এ সস্তা নতুন পথ এলে পুরোনোটি বদলানো যায়, যা cost-optimal algorithm-এর জন্য জরুরি। ওপরের সরল রূপটি (“তোলার সময় বাদ দেওয়া”, যাকে অনেকে lazy deletion বলেন) priority queue-তেও ঠিক কাজ করে, কারণ সস্তা পথটি আগে তোলা হবে আর পরের দামি কপিগুলো বাদ পড়বে; খরচ হলো frontier-এ কিছু বাড়তি কপি।

৮ · Search algorithm মাপার চার মাপকাঠি — b, d আর m

পরের পর্বগুলোতে অনেক algorithm আসবে। তাদের তুলনা হবে চারটি প্রশ্নে:

Completeness

সমাধান থাকলে algorithm কি নিশ্চিতভাবে একটি খুঁজে পাবে? আর না থাকলে কি তা বলে থামবে? অংশ ৬-এর tree search “Z”-এর বেলায় থামেনি।

Optimality

যে সমাধান পায়, তা কি সবচেয়ে কম path cost-এর (cost-optimal)? অংশ ৭-এর FIFO frontier ১৩ কিমির পথ দিয়েছে, ১২ কিমির নয়।

Time complexity

সমাধান পেতে কতগুলো node তৈরি বা expand করতে হয়; সেকেন্ডে নয়, node-এর সংখ্যায় মাপা।

Space complexity

একসঙ্গে সর্বোচ্চ কতগুলো node memory-তে রাখতে হয়: frontier আর explored set মিলিয়ে।

DSA কোর্সে graph algorithm-এর খরচ লেখা হয় O(V + E) দিয়ে, যেখানে V আর E হলো node আর edge-এর সংখ্যা (Big-O-র ঝালাই)। AI-এর search-এ সেটি প্রায়ই কাজে আসে না, কারণ graph-টি হাতে নেই, আর তার আকার 1013 বা অসীম হতে পারে। তাই খরচ লেখা হয় তিনটি সংখ্যায়:

  • b, branching factor: একটি node-এর সর্বোচ্চ (বা গড়) কতগুলো successor.
  • d, depth: সবচেয়ে কাছের (সবচেয়ে কম ধাপের) সমাধানের গভীরতা।
  • m, maximum depth: যেকোনো পথের সর্বোচ্চ দৈর্ঘ্য; চক্র থাকলে tree search-এ এটি অসীম।

Depth d পর্যন্ত একটি পূর্ণ গাছে node থাকে 1 + b + b2 + … + bd, যা b-এর বেশ বড় মানে প্রায় bd. তাই ধাপে ধাপে স্তর ধরে খোঁজা algorithm-এর খরচ সাধারণত O(bd) লেখা হয়। হাতে একটি হিসাব: b = 3, d = 5 হলে শেষ স্তরে 35 = 243টি node, আর মোট 1 + 3 + 9 + 27 + 81 + 243 = 364টি। শেষ স্তরেই মোটের দুই-তৃতীয়াংশ, এটিই exponential বৃদ্ধির চেহারা।

সমস্যাb (প্রায়)dState space
রাইডারের রোড ম্যাপগড় 3.1, সর্বোচ্চ 5 (ফার্মগেট)2 রাস্তা (সবচেয়ে সস্তা পথে 3)9
পানির জগ (৫ ও ৩ লিটার)সর্বোচ্চ 6616 (পৌঁছানো যায়)
8-puzzle2–4, গড়ে প্রায় 2.7; উল্টো চাল বাদ দিলে কমগড়ে প্রায় 22, সর্বোচ্চ 31181,440
15-puzzle2–4, গড়ে ঠিক 3প্রকাশিত কাজে সর্বোচ্চ 80 বলা হয়প্রায় 1.05 × 1013

8-puzzle-এর গড় b আসে ফাঁকা ঘরের ৯টি অবস্থানের গড় থেকে: ৪টি কোণে ২টি করে, ৪টি কিনারায় ৩টি করে, মাঝে ৪টি, মোট (8 + 12 + 4) ÷ 9 ≈ 2.67. নিচের কোড bd-এর বৃদ্ধি একটি টেবিলে আর log-scale chart-এ দেখায়, আর একটি অনুমানে time ও memory-র হিসাব করে: প্রতি সেকেন্ডে দশ লক্ষ node আর প্রতি node ১,০০০ byte. এই দুটি সংখ্যা উদাহরণের জন্য ধরা, কোনো যন্ত্রের মাপা মান নয়।

import numpy as np
import matplotlib.pyplot as plt

d = np.arange(0, 13)
bs = [2, 3, 4, 10]
print(" d" + "".join(f"{'b=' + str(b):>15}" for b in bs))
for k in [2, 4, 6, 8, 10]:
    print(f"{k:>2}" + "".join(f"{b**k:>15,}" for b in bs))

# all nodes of a full tree down to depth d: 1 + b + b^2 + ... + b^d
total = sum(10**k for k in range(13))
print("b=10, d=12 total nodes:", f"{total:,}")
rate, per_node = 1_000_000, 1_000                # assumed: 1e6 nodes/s, 1 KB/node
print(f"at {rate:,} nodes/s: {total / rate / 86400:,.1f} days")
print(f"memory at {per_node} bytes/node: {total * per_node / 1e12:,.0f} TB")

fig, ax = plt.subplots(figsize=(6, 4))
for b in bs:
    ax.plot(d, [float(b) ** k for k in d], marker="o", ms=3, label=f"b = {b}")
ax.axhline(181_440, color="gray", ls="--", lw=1)
ax.text(0.2, 2.5e5, "8-puzzle reachable states (181,440)", fontsize=8, color="gray")
ax.set_yscale("log")
ax.set_xlabel("depth d")
ax.set_ylabel("nodes at depth d (b^d)")
ax.set_title("Search tree growth")
ax.legend(fontsize=8)
plt.show()

আসল আউটপুট:

 d            b=2            b=3            b=4           b=10
 2              4              9             16            100
 4             16             81            256         10,000
 6             64            729          4,096      1,000,000
 8            256          6,561         65,536    100,000,000
10          1,024         59,049      1,048,576 10,000,000,000
b=10, d=12 total nodes: 1,111,111,111,111
at 1,000,000 nodes/s: 12.9 days
memory at 1000 bytes/node: 1,111 TB

প্রতিটি সারিতে bd: b = 2-এ depth ১০-এ মাত্র 1,024, কিন্তু b = 10-এ একই depth-এ 1010, এক হাজার কোটি। b = 3-এর গাছ depth ১০-এ 59,049, যা তখনো 8-puzzle-এর 181,440-এর নিচে; chart-এ ধূসর রেখাটি দেখায় কোন depth-এ প্রতিটি রেখা সেই সংখ্যা পার হয়। এখানেই graph search-এর সুবিধা: গাছ যত বড়ই হোক, explored set থাকলে কোনো state একবারের বেশি expand হয় না, তাই 8-puzzle-এ expansion কখনো 181,440 ছাড়ায় না। নিচের তিন লাইন ধরা অনুমানের ফল: b = 10 আর d = 12-এর পূর্ণ গাছে প্রায় 1.1 × 1012টি node, প্রতি সেকেন্ডে দশ লক্ষ node দেখলেও প্রায় 12.9 দিন, আর প্রতিটি node memory-তে রাখলে প্রায় 1,111 TB. সময় হয়তো অপেক্ষা করা যায়, memory সাধারণত আগে ফুরায়। Chart-এর y-অক্ষ log-scale, তাই সোজা রেখা মানে exponential বৃদ্ধি, আর রেখার ঢাল b-এর log.

মূল ধারণা

Exponential বৃদ্ধিতে দ্রুত কম্পিউটার সামান্যই সাহায্য করে। b = 10 হলে কম্পিউটার ১০ গুণ দ্রুত করলে আপনি মাত্র এক ধাপ বেশি গভীরে যেতে পারেন। তাই search-এর আসল উন্নতি আসে কম node খোলা থেকে: repeated state বাদ দেওয়া (অংশ ৬), ভালো abstraction (অংশ ৪), আর লক্ষ্যের দিকে ঝোঁকানো heuristic (পর্ব ৪১)। আর অনেক সময় memory, সময়ের আগেই ফুরিয়ে যায়।

৯ · বাস্তবে search, আর মডিউলের বাকি পথ

Search শুধু খেলনা ধাঁধার জন্য নয়, যদিও আসল সিস্টেমগুলো এই পর্বের সরল loop-এর চেয়ে অনেক জটিল, আর কোম্পানিগুলো নিজেদের ভেতরের নকশা সব সময় প্রকাশ করে না:

  • রুট খোঁজা: মানচিত্রের অ্যাপ আর ডেলিভারি সিস্টেম রাস্তার graph-এ সবচেয়ে কম সময়ের পথ খোঁজে। প্রকাশিত গবেষণা অনুযায়ী বড় রাস্তার জালে সাধারণ Dijkstra বা A*-এর সঙ্গে আগে থেকে হিসাব রাখার (preprocessing) নানা কৌশল যোগ হয়, আর যানজটের খবর এলে খরচ বদলায়।
  • ধাঁধা আর খেলা: 8-puzzle, Rubik's cube, Sudoku; প্রতিপক্ষ থাকলে search-এর রূপ বদলায় (পর্ব ৪২)।
  • Planning: একটি গুদামের রোবট বা একটি কারখানার কাজের ক্রম। ১৯৭০-এর দশকের শুরুতে Stanford Research Institute-এর Shakey রোবটের জন্য STRIPS নামের planning ভাষা তৈরি হয়েছিল, যার ধারা পর্ব ৪৯-এ দেখবেন।
  • রোবটের নড়াচড়া (motion planning): অবিচ্ছিন্ন জায়গাকে আগে grid বা নমুনা বিন্দুতে ভাগ করে graph বানানো হয়, তারপর খোঁজা; grid মোটা হলে সরু পথ হারায়।

এই মডিউলের বাকি পাঁচটি পর্ব এই পর্বের কাঠামোর ওপরই দাঁড়াবে। পর্ব ৪০ (অন্ধ search — BFS, DFS আর uniform cost)-এ একই loop-এ queue, stack আর priority queue বসিয়ে তাদের completeness, optimality, time ও space মাপা হবে। পর্ব ৪১ (Heuristic আর A*)-এ লক্ষ্যের দূরত্বের অনুমান যোগ হবে। পর্ব ৪২-এ খেলার search, পর্ব ৪৩-এ constraint satisfaction, যেখানে state-এর ভেতরের গঠন কাজে লাগে, আর পর্ব ৪৪-এ local search, যেখানে পথ নয়, শুধু একটি ভালো state খোঁজা হয়।

পরীক্ষা ও ইন্টারভিউতে (বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর পরীক্ষায় এ ধরনের প্রশ্ন সাধারণ):

  • একটি search problem-এর অংশগুলো কী কী? State space, initial state, actions, transition model, goal test, path cost.
  • 8-puzzle-এর কতটি state লক্ষ্য থেকে পৌঁছানো যায়? 9!/2 = 181,440; বিজোড় inversion-এর বিন্যাস অসাধ্য।
  • Tree search আর graph search-এর পার্থক্য? Graph search explored set রেখে repeated state বাদ দেয়; চক্রে আটকায় না, কিন্তু বেশি memory লাগে।
  • b = 2, d = 10 হলে পূর্ণ গাছে মোট node কত? 211 − 1 = 2,047.
  • Completeness আর optimality আলাদা কেন? সমাধান পাওয়ার নিশ্চয়তা আর সবচেয়ে সস্তা সমাধান পাওয়ার নিশ্চয়তা এক নয়; FIFO frontier ওজনসহ graph-এ complete, কিন্তু cost-optimal নয়।

১০ · AI-এর যুগে — AI সহকারীর লেখা search কোড যাচাই

AI সহকারীর কাছে একটি ধাঁধার জন্য BFS চাইলে সে কয়েক সেকেন্ডে কোড দেবে, আর ছোট উদাহরণে তা প্রায়ই ঠিকঠাক চলবে। সমস্যা ধরা পড়ে বড় বা কিনারার উদাহরণে। Search কোডে AI সহকারীরা প্রায়ই যা করে:

  • Explored set ভুলে যাওয়া, বা state list হিসেবে রেখে set-এ ঢোকাতে গিয়ে TypeError: unhashable type, তারপর সেটি এড়াতে str(state)-এর মতো ভঙ্গুর কৌশল।
  • list.pop(0) দিয়ে queue, যা প্রতিবার পুরো list সরায়; বড় frontier-এ খুব ধীর। collections.deque ঠিক পছন্দ।
  • ভুল দাবি: ওজনসহ graph-এ BFS-কে “সবচেয়ে ছোট পথ” বলা, বা 8-puzzle-এর 9! = 362,880টি state সবই সমাধানযোগ্য বলা। এ ধরনের বিশ্বাসযোগ্য কিন্তু ভুল সংখ্যা সহজে চোখ এড়ায়।
  • বানানো বা পুরোনো API: কোনো library-তে নেই এমন function, বা ভুল parameter-এর নাম। যেমন NetworkX-এ আসল function nx.astar_path, কিন্তু অনুমানে লেখা নাম আলাদা হতে পারে।
  • Formulation-এর ভুল চাপা পড়া: state-এ দরকারি তথ্য না রাখা (অংশ ৪-এর “খুব কম” নকশা), বা অসাধ্য শুরুর state-এ অসীম loop, যা কোড ঠিক থাকলেও উত্তর ভুল করে।

প্রম্পটে formulation, সীমা আর যাচাইয়ের উপায় নিজেই বলে দিন। নিচের প্রম্পটে কোনো আসল গ্রাহক বা ঠিকানা নেই, শুধু বানানো উদাহরণ।

আমি Python 3.14 ব্যবহার করছি, শুধু standard library. 8-puzzle-এর জন্য একটি graph search লিখে দাও। ১. State হবে ৯টি সংখ্যার tuple, 0 মানে ফাঁকা; actions, result আর is_goal আলাদা function-এ। ২. Frontier হবে collections.deque, আর একটি explored set থাকবে; list.pop(0) ব্যবহার করবে না। ৩. Search শুরুর আগে inversion গুনে বলবে state সমাধানযোগ্য কি না; অসাধ্য হলে search চালাবে না। ৪. ফল হিসেবে পথ, চালের সংখ্যা আর কতগুলো node expand হলো, তা ফেরত দেবে। ৫. দুটি test দাও: (1, 2, 3, 4, 0, 6, 7, 5, 8) থেকে ঠিক ২ চাল, আর (1, 2, 3, 4, 5, 6, 8, 7, 0) অসাধ্য। ৬. কোনো দাবি (যেমন কতগুলো state পৌঁছানো যায়) নিশ্চিত না হলে বানিয়ে লিখবে না, বলবে কীভাবে গুনে যাচাই করা যায়।

AI-এর কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট

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

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।

প্র ০১Newell আর Simon কেন বলেছিলেন বুদ্ধিমান আচরণের কেন্দ্রে আছে search? দাবিটি আজ কতটা টেকে?

১৯৫০-এর দশকের মাঝামাঝি Allen Newell, Herbert Simon আর Cliff Shaw মিলে Logic Theorist নামের একটি প্রোগ্রাম বানান, যা গণিতের যুক্তির কিছু উপপাদ্য প্রমাণ করতে পারত; ১৯৫৬ সালের Dartmouth সভার সময়ে এটি দেখানো হয়েছিল বলে প্রচলিত। এর পরে তাঁরা General Problem Solver (GPS) তৈরি করেন, যার প্রথম বিবরণ ১৯৫০-এর দশকের শেষ দিকে প্রকাশিত। GPS-এর মূল ধারণা ছিল এই পর্বের কাঠামো: সমস্যাকে state আর operator দিয়ে লেখা, তারপর বর্তমান অবস্থা আর লক্ষ্যের পার্থক্য কমায় এমন operator বেছে নেওয়া, যাকে তাঁরা means-ends analysis বলতেন।

১৯৭২ সালের বই “Human Problem Solving”-এ তাঁরা মানুষের ধাঁধা সমাধানকেও “problem space”-এ পথ খোঁজা হিসেবে বর্ণনা করেন। ১৯৭৫ সালের Turing Award বক্তৃতায় (প্রকাশ ১৯৭৬) তাঁরা দুটি বড় দাবি করেন: physical symbol system hypothesis, অর্থাৎ প্রতীক নিয়ে কাজ করা একটি ব্যবস্থাই সাধারণ বুদ্ধির জন্য প্রয়োজনীয় ও যথেষ্ট; আর heuristic search hypothesis, অর্থাৎ এমন ব্যবস্থা সমস্যা সমাধান করে মূলত search দিয়ে।

দাবিগুলোর একটি অংশ ভালোভাবে টিকেছে: planning, scheduling, প্রমাণ খোঁজা আর খেলার AI-তে search আজও কেন্দ্রে। ২০১৬ সালের AlphaGo-তেও Monte Carlo tree search ছিল, শেখা neural network-এর সঙ্গে মিলিয়ে। কিন্তু দ্বিতীয় অংশ, “বুদ্ধি মানেই প্রতীক আর search”, নিয়ে গুরুতর আপত্তি আছে। ছবি চেনা বা ভাষা বোঝার মতো কাজে হাতে লেখা প্রতীক আর নিয়ম ভালো কাজ করেনি, আর ডেটা থেকে শেখা মডেল এগিয়ে গেছে (পর্ব ৩-এর বিতর্ক)। আরেকটি সীমা এই পর্বেই দেখলেন: search-এর আগে কাউকে সমস্যাটি formulate করতে হয়, আর সেই কাজটি, মানে কী state, কী action, কী বাদ, মূলত মানুষ করে।

মূল উপলব্ধি: Search বুদ্ধিমান আচরণের একটি শক্তিশালী আর এখনো অপরিহার্য উপাদান, কিন্তু “সবই search” দাবিটি বিতর্কিত; আধুনিক সিস্টেমে search প্রায়ই শেখা মডেলের সঙ্গে জোড়া লাগিয়ে কাজ করে।

প্র ০২১৮৮০ সালের 15-puzzle-এর উন্মাদনা আর তার অসাধ্য পুরস্কার থেকে আজকের search-এর জন্য কী শেখার আছে?

১৫টি টাইলের এই ধাঁধা ১৮৭০-এর দশকের শেষে যুক্তরাষ্ট্রে তৈরি হয়; আবিষ্কারক হিসেবে সাধারণত ডাকঘরের কর্মী Noyes Chapman-এর নাম বলা হয়। ১৮৮০ সালের দিকে এটি যুক্তরাষ্ট্র আর ইউরোপে প্রবল জনপ্রিয় হয়, অনেকটা এক সময়ের ভিডিও গেমের উন্মাদনার মতো। একটি বিখ্যাত চ্যালেঞ্জ ছিল: ১৪ আর ১৫ অদলবদল করা অবস্থা থেকে শুরু করে সাজানো অবস্থায় আনা। ধাঁধার লেখক Sam Loyd পরে দাবি করেছিলেন যে তিনিই এটি আবিষ্কার করেন আর এই চ্যালেঞ্জে বড় পুরস্কার ঘোষণা করেছিলেন; ইতিহাসবিদদের মতে আবিষ্কারের দাবিটি ভুল, আর পুরস্কারের গল্পও সম্ভবত অতিরঞ্জিত।

পুরস্কার কেউ পায়নি, আর পাওয়া সম্ভবও ছিল না। ১৮৭৯ সালে William Johnson আর William Story “American Journal of Mathematics”-এ দেখান যে বিন্যাসগুলো দুই দলে ভাগ হয়, আর এক দল থেকে অন্য দলে কোনো চালে যাওয়া যায় না; এটি অংশ ৫-এর parity যুক্তিরই ৪ × ৪ রূপ। ১৪-১৫ অদলবদলে inversion একটি, বিজোড়, তাই সাজানো অবস্থা থেকে বিচ্ছিন্ন।

আজকের search-এর জন্য এখান থেকে তিনটি শিক্ষা। এক, অসাধ্য শুরু আগে ধরুন। সাধারণ BFS অসাধ্য 8-puzzle-এ পুরো 181,440টি state ঘুরে তারপর “সমাধান নেই” বলবে, আর 15-puzzle-এ কার্যত কখনো বলবে না। একটি ছোট inversion গণনা সেই কাজ এক মুহূর্তে করে। দুই, state space-এর গঠন বুঝুন। “পৌঁছানো যায়” আর “সাজানো যায়” এক নয়; জগের ধাঁধায় ২৪টির মধ্যে ১৬টি পৌঁছানো যায়। Graph-টি একাধিক বিচ্ছিন্ন অংশে ভাগ হতে পারে। তিন, সংখ্যা যাচাই করুন। অনেক লেখায়, এমনকি AI সহকারীর উত্তরে, 8-puzzle-এর “9!টি state” বলা হয়, যা ঠিক দ্বিগুণ ভুল; একটি ছোট গণনা ভুলটি ধরে।

মূল উপলব্ধি: Search শুরুর আগে state space-এর গঠন নিয়ে একটু গণিত, যেমন একটি invariant (যা কোনো চালে বদলায় না), অনেক সময় বিশাল অপ্রয়োজনীয় খোঁজা বাঁচায়।

প্র ০৩রাইডারের মানচিত্র থেকে যানজট, বৃষ্টি আর রাস্তা বন্ধ বাদ দেওয়া হয়েছে। এই abstraction কখন ক্ষতিকর হয়, আর তখন কী করা হয়?

এই পর্বের মডেল ধরে নিয়েছে যে প্রতিটি রাস্তার খরচ স্থির আর জানা, আর Go(X) নিলে রাইডার নিশ্চিতভাবে X-এ পৌঁছান। ঢাকার বাস্তবে দুটিই সত্য নয়। বিকেলের ফার্মগেট আর সকালের ফার্মগেট এক নয়; ভারী বৃষ্টিতে কিছু রাস্তায় পানি জমে; হঠাৎ মিছিল বা সংস্কারের কাজে রাস্তা বন্ধ হয়। মডেল যখন বলে “ফার্মগেট হয়ে ১৩ কিমি, সবচেয়ে কম রাস্তা”, তখন সেই উত্তর মডেলের ভেতরে সত্য, বাস্তবে নাও হতে পারে।

ক্ষতি তিন রকম হতে পারে। প্রথমত, খরচের ভুল: কিমি দিয়ে মাপলে কম দূরত্বের পথ বেশি সময় নিতে পারে; তখন খরচ হিসেবে কিমির বদলে সময়ের অনুমান লাগে, যা দিনের সময় অনুযায়ী বদলায়। এটি এখনো search, শুধু path cost বদলেছে। দ্বিতীয়ত, ফলের অনিশ্চয়তা: একটি রাস্তা ৩০% সময় বন্ধ থাকলে “আগে পুরো পথ ঠিক করে চোখ বন্ধ করে চলা” আর যথেষ্ট নয়; তখন দরকার এমন একটি নীতি, যা প্রতিটি মোড়ে নতুন তথ্য দেখে সিদ্ধান্ত নেয়। এ ধরনের সমস্যাকে Markov decision process হিসেবে লেখা হয়, যা পর্ব ৫৪-এ। তৃতীয়ত, পরিবেশ বদলে যাওয়া: চলার পথে নতুন খবর এলে বাকি পথ আবার খুঁজতে হয়। রুট-খোঁজার অনেক সিস্টেম ঠিক এটি করে: সরল মডেলে দ্রুত খোঁজে আর ঘন ঘন আবার খোঁজে (replanning).

একটি পুরোনো কথা এখানে খাটে: মানচিত্র আর ভূখণ্ড এক নয় (map is not the territory). প্রতিটি AI মডেল একটি abstraction, আর তার উত্তর abstraction-এর মতোই ভালো।

মূল উপলব্ধি: Abstraction ক্ষতিকর হয় যখন বাদ দেওয়া তথ্য সিদ্ধান্ত বদলাতে পারে; তখন হয় path cost আরও বাস্তব করতে হয়, নয়তো অনিশ্চয়তা মডেলে আনতে হয় (MDP), নয়তো চলার পথে বারবার নতুন করে খুঁজতে হয়।

প্র ০৪দাবা বা Go-তে পুরো search tree খোঁজা অসম্ভব হলে কম্পিউটার কীভাবে মানুষের সেরা খেলোয়াড়দের হারাল?

১৯৫০ সালে Claude Shannon “Programming a Computer for Playing Chess” প্রবন্ধে একটি মোটা হিসাব দেন: প্রতিটি চালে গড়ে প্রায় ৩০টি বিকল্প, আর একটি খেলায় প্রায় ৪০ জোড়া চাল ধরলে সম্ভাব্য খেলার সংখ্যা মোটামুটি 10120. এটি পরে “Shannon number” নামে পরিচিত হয়; সংখ্যাটি একটি মোটা অনুমান, নিখুঁত গণনা নয়। এই পর্বের ভাষায়, b প্রায় ৩০ আর d প্রায় ৮০, তাই bd যেকোনো কম্পিউটারের নাগালের বাইরে। অংশ ৮-এর টেবিল দেখিয়েছে, b = 10-এই d = 12 পর্যন্ত পূর্ণ গাছ খুঁজতে দিনের পর দিন লাগে।

Shannon নিজেই পথ দেখিয়েছিলেন: পুরো খেলা শেষ পর্যন্ত না খুঁজে কয়েক চাল পর্যন্ত খোঁজা, তারপর অবস্থানটিকে একটি evaluation function দিয়ে মূল্যায়ন করা (গুটির মান, অবস্থানের সুবিধা)। আর তুলনামূলক কম সম্ভাবনার চালগুলো বেছে বাদ দেওয়া। এর ওপর alpha-beta pruning (পর্ব ৪২) আর নানা কৌশল যোগ হয়ে IBM-এর Deep Blue ১৯৯৭ সালে Garry Kasparov-কে একটি ছয় খেলার ম্যাচে হারায়।

Go-তে প্রতি চালে বিকল্প শতাধিক, আর ভালো evaluation function হাতে লেখা খুব কঠিন ছিল। ২০১৬ সালে DeepMind-এর AlphaGo Lee Sedol-কে ৪–১-এ হারায়, যেখানে Monte Carlo tree search-কে neural network-এর সঙ্গে জোড়া হয়েছিল: একটি network বলে কোন চাল খোঁজার যোগ্য (কার্যকর b কমায়), আরেকটি বলে অবস্থানটি কতটা ভালো (গভীর পর্যন্ত যাওয়ার দরকার কমায়)। তাই মূল শিক্ষা হলো: search নিজে বিস্ফোরণ থামাতে পারে না; থামায় ভালো জ্ঞান, হাতে লেখা হোক বা ডেটা থেকে শেখা। এটি পর্ব ৩-এর “bitter lesson” বিতর্কের সঙ্গেও যুক্ত: সেখানে দাবি ছিল দীর্ঘমেয়াদে সবচেয়ে ভালো কাজ করে computation কাজে লাগাতে পারা search আর learning.

মূল উপলব্ধি: বিশাল খেলায় সাফল্য এসেছে সম্পূর্ণ search থেকে নয়, বরং সীমিত গভীরতার search আর ভালো মূল্যায়নের জোড় থেকে, যা কার্যকর b আর d দুটিই কমায়।

প্র ০৫Explored set না রাখলে চক্রে ঘোরার ঝুঁকি, রাখলে memory ফুরানোর ঝুঁকি। গবেষকেরা এই টানাপোড়েন কীভাবে সামলান?

অংশ ৬-এ দেখলেন, ৬ state-এর graph-এ tree search-এর ১৬টি expansion লেগেছে, graph search-এর ৫টি; আর সমাধান না থাকলে tree search থামেইনি। এ থেকে মনে হতে পারে explored set সব সময় রাখা উচিত। কিন্তু 15-puzzle-এর প্রায় 1013টি state-এর প্রতিটির জন্য কয়েক byte রাখলেও terabyte-এর হিসাব আসে, আর বাস্তবের planning সমস্যায় state space আরও বড়। Graph search-এর memory খরচ তাই অনেক সময় সময়ের খরচের আগেই সীমায় পৌঁছায়।

এর একটি বিখ্যাত উত্তর দেন Richard Korf, ১৯৮৫ সালের একটি প্রবন্ধে: iterative deepening, আর তার heuristic রূপ IDA*. ধারণাটি হলো tree search-এর দিকে ফেরা, যা শুধু চলতি পথটুকু মনে রাখে, আর গভীরতার একটি সীমা ধাপে ধাপে বাড়ানো। একই node বারবার তৈরি হয়, কিন্তু exponential গাছে শেষ স্তরটিই মোটের বেশিরভাগ (অংশ ৮-এর ৩৬৪-এর মধ্যে ২৪৩), তাই বাড়তি সময় তুলনামূলক কম, অথচ memory লাগে শুধু d-এর অনুপাতে। Korf এভাবে 15-puzzle-এর এলোমেলো উদাহরণ সর্বোত্তমভাবে সমাধান করে দেখান বলে পরিচিত। পর্ব ৪০-এ iterative deepening আর পর্ব ৪১-এ heuristic-এর সঙ্গে এর সম্পর্ক দেখবেন।

মাঝামাঝি পথও আছে। শুধু চলতি পথের state মনে রেখে ছোট চক্র আটকানো যায় (path checking), যাতে memory প্রায় লাগে না, কিন্তু ভিন্ন পথে একই state-এ আসা আটকায় না। আবার state-কে ছোট করে encode করা যায় (8-puzzle-এর ৯টি ছোট সংখ্যা একটি পূর্ণসংখ্যায় ধরে), বা খুব বড় গণনায় state ডিস্কে রাখা যায়।

মূল উপলব্ধি: Explored set সময় বাঁচায় memory-র দামে; সমস্যার আকার অনুযায়ী পূর্ণ graph search, শুধু চলতি পথ পরীক্ষা, আর iterative deepening-এর মধ্যে বেছে নিতে হয়।

অনুশীলন

প্রথমটি হাতে হিসাবের, দ্বিতীয়টি আউটপুট অনুমানের, তৃতীয়টি নকশা ও সমালোচনার, শেষটি কোড লিখে একটি ধাঁধা সমাধানের।

  1. হাতে হিসাব: (ক) একটি search tree-তে b = 3. Depth 5 পর্যন্ত পূর্ণ গাছে শেষ স্তরে কতটি node, আর মোট কতটি? (খ) 8-puzzle-এর বোর্ড 813 / 4_2 / 765. Inversion গুনে বলুন এটি লক্ষ্য 123 / 456 / 78_ থেকে পৌঁছানো যায় কি না। (গ) রাইডারের মানচিত্রে state-এ “কোন এলাকা” আর “৩টি পার্সেলের কোনগুলো বাকি” থাকলে সর্বোচ্চ কতটি state?

    (ক) শেষ স্তরে 35 = 243; মোট 1 + 3 + 9 + 27 + 81 + 243 = 364. সূত্রে (36 − 1) ÷ (3 − 1) = 728 ÷ 2 = 364.

    (খ) ফাঁকা বাদ দিয়ে পড়ুন: 8 1 3 4 2 7 6 5. উল্টো জোড়া: 8-এর পরের সাতটিই ছোট (৭টি); 1-এর কোনোটি নয় (০); 3-এর পরে 2 (১টি); 4-এর পরে 2 (১টি); 2-এর কোনোটি নয় (০); 7-এর পরে 6 আর 5 (২টি); 6-এর পরে 5 (১টি)। মোট 7 + 1 + 1 + 2 + 1 = 12, জোড়, তাই পৌঁছানো যায়। লেখকের যন্ত্রে অংশ ৩-এর inversions দিয়ে মেলানো: 12.

    (গ) ৯টি এলাকা × 23 = 8 রকম বাকি-তালিকা, মোট 9 × 8 = 72. সবগুলো হয়তো পৌঁছানো যায় না (যেমন একটি ঠিকানায় না গিয়েই সেই পার্সেল দেওয়া), তাই এটি সর্বোচ্চ সীমা।

  2. আউটপুট অনুমান করুন: নিচের কোড একটি ছোট graph-এ FIFO frontier আর explored set নিয়ে চলে। চালানোর আগে দুটি লাইনের আউটপুট লিখুন, তারপর যেকোনো Python ব্লকে বসিয়ে মিলিয়ে দেখুন।
    from collections import deque
    G = {"S": "AB", "A": "C", "B": "CD", "C": "G", "D": "G", "G": ""}
    frontier, explored, order = deque("S"), set(), []
    while frontier:
        s = frontier.popleft()
        if s in explored: continue
        explored.add(s); order.append(s)
        if s == "G": break
        frontier.extend(c for c in G[s] if c not in explored)
    print(order)
    print(len(explored), list(frontier))
    ['S', 'A', 'B', 'C', 'D', 'G']
    6 ['G']

    ধাপগুলো: S তোলা, frontier [A, B]; A, তারপর [B, C]; B, তারপর [C, C, D]; C, তারপর [C, D, G]; দ্বিতীয় C বাদ; D, তারপর [G, G]; প্রথম G তোলা, explored-এ লেখা আর break. তাই order-এ ছয়টি state, আর frontier-এ G-এর দ্বিতীয় কপিটি পড়ে আছে। এখানে goal test explored-এ লেখার পরে, তাই G-ও explored-এ গোনা হয়েছে। লেখকের যন্ত্রে Python 3.14.4-এ মেলানো।

  3. নকশা ও সমালোচনা: একজন সহকর্মী (বা একটি AI সহকারী) রাইডারের সমস্যার জন্য এই formulation দিয়েছেন। অন্তত পাঁচটি সমস্যা চিহ্নিত করে একটি ভালো formulation লিখুন।
    state      = (latitude, longitude to 6 decimals, clock in seconds,
                  fuel in ml, weather, radio station)
    actions    = turn left / turn right / go straight 1 metre
    goal test  = distance to Motijheel office < 1 metre
    path cost  = number of turns
    search     = tree search with a stack, no explored set
    • State-এ অপ্রাসঙ্গিক তথ্য: রেডিও আর আবহাওয়া এই মডেলে পথের সিদ্ধান্ত বদলায় না; ঘড়ির সেকেন্ড প্রতিটি state-কে আলাদা করে দেয়, তাই repeated state কখনো ধরা পড়বে না।
    • Action খুব সূক্ষ্ম স্তরে: ১ মিটারের ধাপে কয়েক কিমির পথ হাজার হাজার action; d বিশাল, আর bd অসম্ভব।
    • Goal test অস্বাভাবিক কড়া: ১ মিটারের মধ্যে পৌঁছানো এই abstraction-এ অর্থহীন; “মতিঝিল এলাকায় পৌঁছানো” যথেষ্ট, শেষ মিটারগুলো রাইডার নিজে সামলান।
    • ভুল path cost: মোড়ের সংখ্যা কমানো আর কিমি বা সময় কমানো এক নয়; কম মোড়ের পথ অনেক লম্বা হতে পারে। ব্যবসার লক্ষ্য অনুযায়ী কিমি বা সময়।
    • Explored set ছাড়া stack: চক্রে (বাঁয়ে, বাঁয়ে, বাঁয়ে, বাঁয়ে) চিরকাল ঘুরতে পারে, সমাধান না থাকলে থামবে না, আর পাওয়া পথ সস্তা হওয়ারও নিশ্চয়তা নেই।
    • যা আসলে লাগত, তা নেই: তেল যদি সত্যিই সমস্যা হয়, তাহলে মিলিলিটার নয়, একটি মোটা মাপ (“পরের পাম্প পর্যন্ত চলবে কি না”) রাখলেই চলে।

    ভালো formulation: state = এখনকার এলাকা (৯টির একটি); actions = পাশের এলাকায় যাওয়া; transition = সেই এলাকায় পৌঁছানো; goal test = state == মতিঝিল; path cost = রাস্তার কিমি (বা দিনের সময় অনুযায়ী অনুমিত মিনিট); search = explored set সহ graph search, আর সস্তা পথ চাইলে path cost দিয়ে সাজানো frontier (পর্ব ৪০)।

  4. কোড লিখে ধাঁধা সমাধান: অংশ ৭-এর FIFO আর search function হুবহু রেখে পানির জগের জন্য একটি jug_successors(s) লিখুন, যা state (a, b)-এর সব successor আর প্রতিটির খরচ ১ ফেরত দেয়। তারপর (0, 0) থেকে “বড় জগে ৪ লিটার” খুঁজুন। পথ, ধাপের সংখ্যা আর expansion কত?
    def jug_successors(s):
        a, b = s                                  # a: 5 L jug, b: 3 L jug
        pour_ab = min(a, 3 - b); pour_ba = min(b, 5 - a)
        moves = [(5, b), (a, 3), (0, b), (a, 0),
                 (a - pour_ab, b + pour_ab), (a + pour_ba, b - pour_ba)]
        return [(t, 1) for t in moves if t != s]  # every action costs 1
    
    path, cost, n = search((0, 0), lambda s: s[0] == 4, jug_successors, FIFO())
    print(path)
    print("steps:", cost, "| expanded:", n)

    লেখকের যন্ত্রে সত্যিকারের run (Python 3.14.4):

    [(0, 0), (5, 0), (2, 3), (2, 0), (0, 2), (5, 2), (4, 3)]
    steps: 6 | expanded: 12

    অংশ ৩-এর হাতে লেখা পথটিই এসেছে। if t != s সেই কাজগুলো বাদ দেয়, যা state বদলায় না (যেমন ভরা জগ আবার ভরা)। লক্ষ্যটি একটি শর্ত (s[0] == 4), নির্দিষ্ট state নয়; generic loop-এর কিছুই বদলাতে হয়নি, এটিই formulation আর algorithm আলাদা রাখার সুবিধা।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমGeneric graph search loop-এর এক চক্রের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 📥 শুরুর state-কে frontier-এ রাখা
  2. 📤 frontier-এর নীতি মেনে একটি node তোলা
  3. 🎯 node-টির state লক্ষ্য কি না পরীক্ষা
  4. 🔁 state আগেই explored হলে বাদ দেওয়া
  5. 📝 state-কে explored set-এ লেখা
  6. 🌱 expand করে নতুন সন্তানদের frontier-এ রাখা

একটি search problem-এ Result(s, a) function কোন অংশকে প্রকাশ করে?

  • Goal test
  • Transition model
  • Path cost
  • Actions(s)

8-puzzle-এর ৯টি ঘরে টাইল আর ফাঁকা ঘর 9! = 362,880 রকমে সাজানো যায়। এর মধ্যে কতগুলো একটি নির্দিষ্ট লক্ষ্য বিন্যাস থেকে চাল দিয়ে পৌঁছানো যায়?

  • 362,880, সবগুলো
  • 40,320
  • 181,440
  • 31

S, A, B, C, G নামের state-এর একটি graph-এ S আর A-র মধ্যে দুই-মুখী রাস্তা আছে, আর কোনো পথে G-তে পৌঁছানো যায় না। Explored set ছাড়া একটি tree search এখানে কী করবে?

  • চিরকাল চলতে থাকবে, কারণ S → A → S → … গাছটি অসীম
  • ৫টি state দেখে “সমাধান নেই” বলে থামবে
  • একটি ভুল পথকে সমাধান হিসেবে ফেরত দেবে
  • Python একটি cycle error দিয়ে থামাবে

একই generic search loop-এ frontier হিসেবে FIFO queue দিলে কোন algorithm পাওয়া যায়, আর ওজনসহ রাস্তার graph-এ তা কী নিশ্চিত করে?

  • Depth-first search; সবচেয়ে কম কিমির পথ
  • Uniform-cost search; সবচেয়ে কম কিমির পথ
  • Breadth-first search; সবচেয়ে কম কিমির পথ
  • Breadth-first search; সবচেয়ে কম ধাপের (রাস্তার সংখ্যার) পথ

একটি search tree-তে branching factor b = 10. Depth 6 পর্যন্ত পূর্ণ গাছে শেষ স্তরে কতগুলো node থাকে?

  • 60
  • 1,000,000
  • 610 ≈ 6 কোটি
  • 1,111,111

একজন রাইডার তিনটি পার্সেল তিন ঠিকানায় দিয়ে গুদামে ফিরবেন। কোন state নকশাটি goal test ঠিকভাবে লেখার জন্য যথেষ্ট আর অপ্রয়োজনীয় তথ্যমুক্ত?

  • শুধু রাইডার এখন কোন এলাকায়
  • GPS অবস্থান, সেকেন্ডের ঘড়ি, তেলের মিলিলিটার আর আবহাওয়া
  • (এখনকার এলাকা, কোন পার্সেলগুলো এখনো বাকি)
  • শুধু তিনটি ঠিকানার নাম

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. Search ঠিক টুল যখন পরিবেশ fully observable, deterministic, discrete আর নিয়ম জানা; তখন agent আগে পুরো পথ খুঁজে পরে চালায়।
  2. ছয়টি অংশ: state, initial state, actions, transition model, goal test, path cost. Algorithm শুধু এগুলো ডাকে; ভুল উত্তরের প্রায়ই দোষ formulation-এর।
  3. Abstraction: সিদ্ধান্ত বদলাতে পারে এমন সব রাখুন, বাকি ফেলে দিন। খুব বেশি রাখলে state space ফোলে, খুব কম রাখলে ভিন্ন অবস্থা এক হয়ে ভুল উত্তর আসে।
  4. 8-puzzle-এ 181,440 = 9!/2 state, কারণ চালে inversion-এর parity বদলায় না; 15-puzzle-এ প্রায় 1.05 × 1013. Search tree আর state graph আলাদা; graph search explored set রেখে repeated state আর চক্র আটকায়, দামে memory.
  5. একটি loop, বদলায় শুধু frontier: FIFO = BFS (কম ধাপ, কম কিমি নয়), stack = DFS, priority queue = UCS/A*. মাপকাঠি completeness, optimality, time, space; খরচ b, d, m দিয়ে, যেমন O(bd).

পরের পর্বে: অন্ধ search. এই পর্বের loop-এ queue, stack আর priority queue বসিয়ে BFS, DFS, iterative deepening আর uniform-cost search চালানো, আর রাইডারের ১২ কিমির পথটি কোনটি খুঁজে পায়, তা মাপা।

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

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান