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

Heuristic আর A* — অনুমান দিয়ে দ্রুত পথ

Heuristic Search and A* — Greedy Best-First, Admissible and Consistent Heuristics, the 8-Puzzle, Weighted A* and IDA*

Taught in Bangla, this lesson compares greedy best-first, A* and uniform cost search on a small delivery map, explains admissible and consistent heuristics with a proof sketch, measures the 8-puzzle heuristics and effective branching factor in real runs, and covers weighted A*, IDA* and how to check AI-written A* code.

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

  • Heuristic h(n) কী, আর ম্যাপে straight-line ও grid-এ Manhattan distance কেন ভালো অনুমান
  • Greedy best-first, A* আর UCS-কে একই ম্যাপে চালিয়ে খরচ আর expand হওয়া node-এর সংখ্যা তুলনা
  • Admissible heuristic কেন A*-কে সেরা পথ দেয় (সহজ যুক্তি আর প্রমাণের খসড়া), consistency কেন graph search-এ দরকার, আর বাড়িয়ে বলা heuristic কী ক্ষতি করে
  • 8-puzzle-এর দুটি heuristic, dominance আর effective branching factor b* দিয়ে তুলনা; সমস্যা শিথিল করে নতুন heuristic বানানো
  • Weighted A*, memory-র সমস্যা আর IDA*, আর বাস্তবে গেম ও রুট খোঁজায় A*-এর জায়গা

আগের পর্বে (পর্ব ৪০ · অন্ধ search — BFS, DFS আর uniform cost) দেখেছেন, BFS, DFS আর UCS লক্ষ্য কোন দিকে তা জানে না। তারা চারদিকে সমানভাবে ছড়ায়, ঠিক যেন অন্ধকার ঘরে হাতড়ে চাবি খোঁজা। কিন্তু বাস্তবের বেশিরভাগ সমস্যায় আমরা লক্ষ্যের দিক মোটামুটি জানি। মিরপুর থেকে মতিঝিলের দিকে যাওয়া রিকশাচালক উত্তরা হয়ে ঘোরার কথা ভাবেন না, কারণ উত্তরা উল্টো দিকে। এই “মোটামুটি জানা” কে একটি সংখ্যায় লিখলে তাকে বলে heuristic. এই পর্বের প্রশ্ন: এই অনুমান search-কে কতটা দ্রুত করে, আর কখন অনুমান ভুল হলেও উত্তর ঠিক থাকে? Search সমস্যার গঠন (state, action, goal, path cost) পর্ব ৩৯-এ; graph, BFS আর Dijkstra-র ডেটা-স্ট্রাকচারের দিক DSA কোর্সের graph, BFS, Dijkstra আর heap পর্বে আছে। এখানে সেগুলো আবার শেখানো হবে না।

এক লাইনে ঝালাই: search-এর frontier হলো যেসব node খুঁজে পাওয়া গেছে কিন্তু এখনো খোলা হয়নি। একটি node expand করা মানে তাকে frontier থেকে তুলে তার প্রতিবেশীদের frontier-এ যোগ করা। UCS frontier-কে একটি priority queue-তে রাখে আর সবসময় সবচেয়ে কম path cost g(n)-এর node আগে তোলে। এই পর্বের সব অ্যালগরিদম একই কাঠামো, শুধু queue সাজানোর নিয়ম আলাদা।

১ · Heuristic কী — বাকি পথের একটি সস্তা অনুমান

বাজারঘরের (এই কোর্সের কাল্পনিক অনলাইন দোকান) একজন ডেলিভারি রাইডার মিরপুরের গুদাম থেকে মতিঝিলের এক ক্রেতার কাছে যাবেন। নিচের ম্যাপে ঢাকার ১২টি এলাকা আর ১৯টি রাস্তা। সব দূরত্ব আর অবস্থান বানানো, শুধু শেখার জন্য; এলাকার নাম আসল হলেও কিলোমিটারগুলো আসল নয়। প্রতিটি এলাকার একটি কাল্পনিক (x, y) স্থানাঙ্ক আছে কিলোমিটারে, যাতে দুই এলাকার মধ্যে সরলরেখার দূরত্ব হিসাব করা যায়।

প্রতিটি node n-এর জন্য তিনটি সংখ্যা মনে রাখুন:

  • g(n): শুরু থেকে n পর্যন্ত এখন পর্যন্ত পাওয়া পথের আসল খরচ (যা ইতিমধ্যে চলা হয়েছে)।
  • h(n): n থেকে লক্ষ্য পর্যন্ত বাকি খরচের অনুমান, যা heuristic function দেয়।
  • h*(n): n থেকে লক্ষ্যের আসল সেরা খরচ, যা আমরা জানি না (জানলে search লাগত না)।

ম্যাপে সবচেয়ে স্বাভাবিক heuristic হলো সরলরেখার দূরত্ব, মানে Euclidean distance: h(n) = √((xn − xলক্ষ্য)² + (yn − yলক্ষ্য)²). মিরপুর (2, 9) আর মতিঝিল (6, 0)-এর মধ্যে এটি √(16 + 81) = √97 ≈ 9.85 km. কাকের উড়ে যাওয়ার দূরত্ব কখনো রাস্তার দূরত্বের চেয়ে বেশি হতে পারে না, কারণ দুই বিন্দুর মধ্যে সবচেয়ে ছোট পথ সরলরেখা। এই ম্যাপে প্রতিটি রাস্তার দৈর্ঘ্য তার দুই প্রান্তের সরলরেখার দূরত্বের সমান বা বেশি রাখা হয়েছে; নিচের কোডের একটি assert প্রতিটি রাস্তায় এটি যাচাই করে।

বাজারঘরের ডেলিভারি ম্যাপ, রাস্তার km আর সরলরেখার h 9 5 5 7 8 4 6 3 4 3 4 6 4 4 5 6 3 5 3 মিরপুর (শুরু) উত্তরা মোহাম্মদপুর ফার্মগেট গুলশান তেজগাঁও ধানমন্ডি বাড্ডা রামপুরা লালবাগ মতিঝিল (লক্ষ্য) খিলগাঁও h=9.8 h=15.0 h=7.1 h=5.4 h=8.2 h=5.0 h=3.6 h=7.2 h=4.2 h=4.0 h=0 h=3.0
কাল্পনিক ম্যাপ। ধূসর সংখ্যা রাস্তার দৈর্ঘ্য (km), বেগুনি h হলো মতিঝিল পর্যন্ত সরলরেখার দূরত্ব। সবুজ: সেরা পথ মিরপুর → ফার্মগেট → তেজগাঁও → মতিঝিল, 5 + 3 + 6 = 14 km. কমলা ভাঙা রেখা: greedy যে শেষ অংশটি বাছে, ফার্মগেট → ধানমন্ডি → মতিঝিল, মোট 5 + 4 + 6 = 15 km.

নিচের টেবিলে প্রতিটি এলাকার h আর আসল সেরা বাকি খরচ h* পাশাপাশি (h* বের করা হয়েছে মতিঝিল থেকে উল্টো দিকে Dijkstra চালিয়ে, লেখকের যন্ত্রে)। লক্ষ করুন, প্রতিটি সারিতে h ≤ h*: অনুমান কখনো বাড়িয়ে বলে না।

এলাকা(x, y)h (সরলরেখা)h* (আসল)
মিরপুর(2, 9)9.8514
মোহাম্মদপুর(1, 5)7.0710
ফার্মগেট(4, 5)5.399
তেজগাঁও(6, 5)5.006
ধানমন্ডি(3, 2)3.616
গুলশান(8, 8)8.2510
রামপুরা(9, 3)4.245
খিলগাঁও(9, 0)3.003

Grid-এর জগতে (একটি গোলকধাঁধা, যেখানে শুধু উপরে-নিচে-ডানে-বাঁয়ে এক ঘর করে যাওয়া যায়) সরলরেখার চেয়ে ভালো অনুমান Manhattan distance: |Δসারি| + |Δকলাম|. দেয়াল না থাকলে এটিই হুবহু আসল খরচ, আর দেয়াল থাকলে পথ শুধু লম্বা হয়, তাই এটিও কখনো বাড়িয়ে বলে না। নামটি এসেছে নিউইয়র্কের Manhattan-এর ছকের মতো রাস্তা থেকে, যেখানে কোনাকুনি যাওয়া যায় না।

পরীক্ষার খাতা দেখার কথা ভাবুন। একজন অভিজ্ঞ শিক্ষক খাতার প্রথম পাতা দেখেই আন্দাজ করেন কোন খাতায় বেশি সময় লাগবে। আন্দাজটি নিখুঁত নয়, কিন্তু সস্তা: পুরো খাতা না পড়েই পাওয়া যায়। Heuristic ঠিক এমন: পুরো search না করে, শুধু state দেখে, বাকি কাজের একটি দ্রুত অনুমান।

মূল ধারণা

একটি ভালো heuristic-এর তিনটি গুণ: সস্তা (একটি node-এ দ্রুত হিসাব হয়), তথ্যপূর্ণ (আসল h*-এর যত কাছে, তত ভালো), আর অনেক সময় আশাবাদী (কখনো আসল খরচের চেয়ে বেশি বলে না)। লক্ষ্যে h = 0 হতে হবে। h(n) = 0 সবসময় দিলে সেটিও বৈধ, কিন্তু তখন কোনো তথ্য নেই, আর A* হয়ে যায় UCS.

২ · Greedy best-first search — f = h, দ্রুত কিন্তু সেরা নয়

সবচেয়ে সরল ধারণা: frontier থেকে সবসময় সেই node তুলুন, যা লক্ষ্যের সবচেয়ে কাছে মনে হয়, মানে যার h সবচেয়ে ছোট। এর নাম greedy best-first search. এখানে priority হলো f(n) = h(n); এ পর্যন্ত কত পথ চলা হলো (g), তা হিসাবে নেই। লোভী (greedy) কারণ প্রতিটি ধাপে শুধু সামনের সবচেয়ে লোভনীয় দিকটি দেখে।

ম্যাপে হাতে চালিয়ে দেখুন। মিরপুর expand করলে চার প্রতিবেশী: মোহাম্মদপুর (h 7.07), ফার্মগেট (5.39), গুলশান (8.25), উত্তরা (15.0). সবচেয়ে ছোট h ফার্মগেটের, তাই সেটি আগে। ফার্মগেট থেকে তেজগাঁও (h 5.00) আর ধানমন্ডি (h 3.61). ধানমন্ডি মতিঝিলের সরলরেখায় বেশি কাছে, তাই greedy ধানমন্ডি বাছে। ধানমন্ডি থেকে মতিঝিল (h 0), আর লক্ষ্য পাওয়া গেল। মোট পথ 5 + 4 + 6 = 15 km, মাত্র ৪টি node expand করে।

কিন্তু তেজগাঁও হয়ে পথ 5 + 3 + 6 = 14 km. ধানমন্ডি সরলরেখায় কাছে হলেও সেখান থেকে মতিঝিলের রাস্তা ঘুরপথে 6 km, আর ফার্মগেট থেকে ধানমন্ডি যেতেই 4 km লেগেছে, যা greedy দেখেনি। এটিই greedy-র মূল দুর্বলতা: যে পথ চলতে ইতিমধ্যে কত খরচ হলো, তা উপেক্ষা করা। ফলে greedy-র optimality-র কোনো নিশ্চয়তা নেই: পাওয়া পথটি সবচেয়ে কম খরচের হবে, এমন কথা বলা যায় না।

Greedy আরও খারাপও হতে পারে। Tree search হিসেবে (আগে দেখা state মনে না রেখে) চালালে greedy দুটি node-এর মধ্যে আটকে ঘুরতে পারে, যদি একটি গলি-পথে লক্ষ্য কাছে মনে হয় অথচ সেখান থেকে বের হওয়ার রাস্তা পেছনে। তাই greedy সাধারণভাবে complete-ও নয়; সসীম state space-এ graph search হিসেবে (দেখা state মনে রেখে) চালালে complete হয়, কিন্তু optimal তখনও নয়। এর শক্তি গতি: ভালো heuristic থাকলে খুব অল্প node দেখে একটি “ভালো-চলনসই” পথ পায়।

৩ · A* — চলা পথ আর বাকি অনুমান একসঙ্গে, f = g + h

সমাধানটি সরল: দুটি তথ্যই নিন। f(n) = g(n) + h(n), মানে “n হয়ে গেলে পুরো পথের আনুমানিক খরচ”। g হলো শুরু থেকে n পর্যন্ত আসল খরচ, h হলো n থেকে লক্ষ্যের অনুমান। frontier থেকে সবসময় সবচেয়ে ছোট f-এর node তোলা হয়, আর লক্ষ্য node যখন frontier থেকে তোলা হয় (শুধু খুঁজে পাওয়া নয়), তখন থামা হয়। এটিই A* (উচ্চারণ “এ-স্টার”)। দুটি চরম রূপ মনে রাখুন: h = 0 হলে f = g, মানে UCS; আর g-কে বাদ দিলে f = h, মানে greedy. A* দুইয়ের মাঝখানে।

A* প্রকাশিত হয় ১৯৬৮ সালে, Peter Hart, Nils Nilsson আর Bertram Raphael-এর প্রবন্ধ “A Formal Basis for the Heuristic Determination of Minimum Cost Paths”-এ, IEEE Transactions on Systems Science and Cybernetics জার্নালে। তাঁরা তখন Stanford Research Institute (SRI)-তে Shakey নামের একটি চলমান রোবটের প্রকল্পে কাজ করছিলেন, যার ঘরের মধ্যে পথ খুঁজতে হতো। প্রবন্ধের মূল অবদান ছিল প্রমাণ: কোন শর্তে heuristic-চালিত search সবচেয়ে কম খরচের পথ দেবেই। Edsger Dijkstra-র ১৯৫৯ সালের সবচেয়ে ছোট পথের অ্যালগরিদম A*-এর একটি বিশেষ রূপ হিসেবে দেখা যায় (h = 0).

নিচের stepper-এ একই ম্যাপে A*-এর প্রতিটি ধাপ। frontier-এর প্রতিটি entry লেখা আছে “এলাকা g + h = f” আকারে, ছোট f আগে।

1n = frontier.pop_min_f()2if n == goal: return path3for m in neighbours(n):4    g2 = g[n] + km(n, m)5    if g2 < g[m]: push(m, g2 + h(m))
মান (Watch)
expand
নতুন বা উন্নত
frontier (ছোট f আগে)
expand হলো
  1. শুরু। মিরপুরে g = 0, h = 9.85. এর চার প্রতিবেশী frontier-এ ঢোকে। উত্তরার f = 9 + 15 = 24: রাস্তা কাছে হলেও উল্টো দিকে, তাই f বড়।
  2. ফার্মগেট (f 10.39). সবচেয়ে ছোট f. এর থেকে তেজগাঁও (g 8, f 13.0) আর ধানমন্ডি (g 9, f 12.61) frontier-এ। মিরপুরে ফিরলে g 10, যা 0-এর চেয়ে খারাপ, তাই বাদ।
  3. মোহাম্মদপুর (f 12.07). Greedy এটিকে কখনো তুলত না, কিন্তু A* g-ও দেখে: মাত্র 5 km চলা হয়েছে। এখান থেকে লালবাগ (g 11, f 15.0) যোগ হয়; ধানমন্ডিতে g 9 আসে, যা আগের 9-এর চেয়ে ভালো নয়।
  4. ধানমন্ডি (f 12.61). এখান থেকে মতিঝিল g 15-এ frontier-এ ঢোকে। Greedy এখানেই 15 km নিয়ে থেমে যেত। কিন্তু A* লক্ষ্যকে frontier থেকে তোলার আগে থামে না, আর তেজগাঁওয়ের f 13.0 এখনো 15-এর চেয়ে ছোট।
  5. তেজগাঁও (f 13.0). এখান থেকে মতিঝিল g = 8 + 6 = 14, আগের 15-এর চেয়ে ভালো, তাই নতুন entry f 14.0. পুরোনো 15-এর entry পরে উঠলে বাতিল হবে।
  6. মতিঝিল (f 14.0) তোলা হলো। লক্ষ্য, তাই থামা। পথ 14 km, মোট ৬টি expand. frontier-এ বাকি সবার f ≥ 15, তাই 14-এর চেয়ে ভালো কোনো পথ ওদিকে থাকতে পারে না (কারণ h কখনো বাড়িয়ে বলে না)।

এবার তিনটি অ্যালগরিদম একই কোডে, শুধু priority function বদলে। best_first function একটি f(g, n) নেয়: UCS-এ g, greedy-তে h(n), A*-এ g + h(n). frontier হলো Python-এর heapq, যা একটি min-heap (সবচেয়ে ছোট মান আগে ওঠে)। প্রথমবার চালালে ব্রাউজারে Python নামতে কয়েক সেকেন্ড লাগতে পারে।

import heapq, math
XY = {"Mirpur": (2, 9), "Uttara": (6, 15), "Mohammadpur": (1, 5), "Farmgate": (4, 5),
      "Gulshan": (8, 8), "Tejgaon": (6, 5), "Dhanmondi": (3, 2), "Badda": (10, 6),
      "Rampura": (9, 3), "Lalbagh": (2, 0), "Motijheel": (6, 0), "Khilgaon": (9, 0)}
ROADS = """Mirpur Uttara 9, Mirpur Mohammadpur 5, Mirpur Farmgate 5, Mirpur Gulshan 7,
Uttara Gulshan 8, Mohammadpur Dhanmondi 4, Mohammadpur Lalbagh 6, Farmgate Tejgaon 3,
Farmgate Dhanmondi 4, Gulshan Badda 3, Gulshan Tejgaon 4, Tejgaon Motijheel 6,
Badda Rampura 4, Rampura Khilgaon 4, Rampura Motijheel 5, Dhanmondi Motijheel 6,
Dhanmondi Lalbagh 3, Lalbagh Motijheel 5, Khilgaon Motijheel 3"""
G = {n: [] for n in XY}
for r in ROADS.split(","):
    a, b, km = r.split(); km = int(km)
    assert km >= math.dist(XY[a], XY[b])      # রাস্তা কখনো সরলরেখার চেয়ে ছোট নয়
    G[a].append((b, km)); G[b].append((a, km))
h = lambda n: math.dist(XY[n], XY["Motijheel"])   # straight-line distance

def best_first(start, goal, f):          # f(g, n) দিয়ে frontier সাজানো
    frontier, best, expanded = [(f(0, start), 0, start, [start])], {start: 0}, []
    while frontier:
        _, g, n, path = heapq.heappop(frontier)
        if g > best[n]: continue          # পুরোনো, বেশি খরচের entry
        expanded.append(n)
        if n == goal: return path, g, expanded
        for m, km in G[n]:
            if g + km < best.get(m, math.inf):
                best[m] = g + km
                heapq.heappush(frontier, (f(g + km, m), g + km, m, path + [m]))

for name, f in [("UCS    f=g", lambda g, n: g), ("Greedy f=h", lambda g, n: h(n)),
                ("A*   f=g+h", lambda g, n: g + h(n))]:
    path, cost, ex = best_first("Mirpur", "Motijheel", f)
    print(f"{name}: {cost} km, expanded {len(ex):2} | {' > '.join(path)}")
print("h(Mirpur) =", round(h("Mirpur"), 2), "km; true best = 14 km")

আসল আউটপুট:

UCS    f=g: 14 km, expanded 10 | Mirpur > Farmgate > Tejgaon > Motijheel
Greedy f=h: 15 km, expanded  4 | Mirpur > Farmgate > Dhanmondi > Motijheel
A*   f=g+h: 14 km, expanded  6 | Mirpur > Farmgate > Tejgaon > Motijheel
h(Mirpur) = 9.85 km; true best = 14 km

তিনটি লাইন তিনটি গল্প। UCS সেরা পথ (14 km) পায়, কিন্তু ১২টির মধ্যে ১০টি এলাকা খোলে, উত্তরা আর বাড্ডার মতো উল্টো দিকের এলাকাসহ, কারণ সে শুধু g দেখে আর সব দিকে সমানভাবে ছড়ায়। Greedy মাত্র ৪টি খোলে, কিন্তু পথ 15 km, সেরা নয়। A* দুটির সুবিধা নেয়: সেরা 14 km, মাত্র ৬টি expand. কোনো assert ভাঙেনি, মানে প্রতিটি রাস্তা তার সরলরেখার চেয়ে লম্বা বা সমান, তাই এই h বাড়িয়ে বলে না। লক্ষ করুন, best dictionary প্রতিটি এলাকার এখন পর্যন্ত সেরা g রাখে; পুরোনো বেশি খরচের entry heap-এ থেকে গেলেও তোলার সময় g > best[n] দেখে বাদ পড়ে। এটি heap-এ “decrease key”-এর বদলে চলা সাধারণ কৌশল।

৪ · Admissible heuristic — কেন A* সেরা পথ দেয়, আর বাড়িয়ে বললে কী হয়

একটি heuristic admissible যদি প্রতিটি node-এ h(n) ≤ h*(n), মানে কখনো আসল বাকি খরচের চেয়ে বেশি না বলে। এটি একটি আশাবাদী অনুমান: “অন্তত এতটুকু তো লাগবেই।” সরলরেখার দূরত্ব আর grid-এর Manhattan distance দুটিই admissible, যা অংশ ১-এর টেবিলে দেখেছেন।

সহজ যুক্তি: আশাবাদী অনুমান কোনো ভালো পথ লুকাতে পারে না

ধরুন A* লক্ষ্যে একটি পথ পেয়েছে, খরচ C. অন্য কোনো, এখনো না-খোলা, পথ যদি আরও সস্তা হতো, তাহলে সেই পথের কোনো একটি node n এই মুহূর্তে frontier-এ বসে আছে। সেই n-এর f(n) = g(n) + h(n). h কখনো বাড়িয়ে বলে না, তাই f(n) ≤ ওই সস্তা পথের আসল মোট খরচ < C. অর্থাৎ f(n) লক্ষ্যের f = C-এর চেয়ে ছোট, তাই A* লক্ষ্য তোলার আগেই n-কে তুলত। ফলে A* কখনো একটি খরচ-বেশি লক্ষ্য আগে তুলে থামতে পারে না। ক্রিকেটের ভাষায়: সম্ভাব্য সর্বনিম্ন রান যার এখনো আমাদের সেরা স্কোরের চেয়ে কম, তাকে বাদ দেওয়া যায় না।

উল্টোটা দেখুন। রাইডারের নিজের অভিজ্ঞতা থেকে বানানো একটি অনুমান-টেবিল ভাবুন, যেখানে সব এলাকায় সরলরেখার দূরত্বই আছে, শুধু তেজগাঁওয়ের পাশে লেখা “জ্যাম, 12 km”. অথচ তেজগাঁও থেকে মতিঝিলের আসল খরচ মাত্র 6 km. এক জায়গায় এই বাড়িয়ে বলাই যথেষ্ট:

1h_bad = straight_line.copy()2h_bad["Tejgaon"] = 12   # আসল h* = 63A*(f = g + h_bad)
মান (Watch)
expand
তেজগাঁও-এর f
frontier
  1. প্রথম ধাপ একই। মিরপুর expand, চার প্রতিবেশী frontier-এ। তেজগাঁও এখনো দেখা হয়নি, তাই পার্থক্য নেই।
  2. ফার্মগেট থেকে তেজগাঁও। g = 8, কিন্তু h_bad = 12, তাই f = 20. সঠিক heuristic-এ এটি 13.0 হতো আর পরের তিন ধাপের মধ্যে খোলা হতো। এখন এটি queue-র পেছনে চলে গেল।
  3. মোহাম্মদপুর। আগের মতোই; লালবাগ f 15.0-এ যোগ হয়।
  4. ধানমন্ডি। মতিঝিল g 15-এ frontier-এ ঢোকে, f = 15.0. তেজগাঁওয়ের মিথ্যা f 20 এর চেয়ে বড় দেখাচ্ছে।
  5. লালবাগ (f 15.0). সমান f-এ ছোট g আগে ওঠে (কোডের tie-breaking). লালবাগ থেকে মতিঝিল g 16, 15-এর চেয়ে খারাপ, তাই কিছু বদলায় না।
  6. মতিঝিল তোলা হলো, 15 km. A* থামল। তেজগাঁও (আসলে f 13) কখনো খোলা হয়নি, তাই 14 km-এর পথটি চোখের আড়ালে রয়ে গেল। এক জায়গায় বাড়িয়ে বলাই optimality নষ্ট করেছে।

এই run-টি লেখকের যন্ত্রে অংশ ৩-এর একই best_first দিয়ে চালানো হয়েছে, শুধু h বদলে; ফল ছিল (['Mirpur', 'Farmgate', 'Dhanmondi', 'Motijheel'], 15). মজার ব্যাপার, inadmissible heuristic সবসময় ভুল পথ দেয় না; শুধু নিশ্চয়তা হারায়। কোথায় বাড়িয়ে বলা হলো, তার ওপর নির্ভর করে ফল ঠিক থাকতেও পারে। কিন্তু “সাধারণত ঠিক আসে” আর “প্রমাণসহ সবসময় ঠিক” এক কথা নয়।

আরও গভীরে: optimality-র প্রমাণের খসড়া। ধরুন সেরা খরচ C*, আর A* প্রথমে একটি লক্ষ্য G2 তুলেছে যার g(G2) > C*. লক্ষ্যে h = 0, তাই f(G2) = g(G2) > C*. সেরা পথের ওপর একটি node n সবসময় frontier-এ থাকে (শুরুতে শুরুর node, আর প্রতিবার তোলা হলে তার পরের node ঢোকে), আর n-এ g(n) সেরা পথের আসল খরচ। Admissibility থেকে f(n) = g(n) + h(n) ≤ g(n) + h*(n) = C* < f(G2). তাই n-কে G2-এর আগে তোলা হতো; বিরোধ। ফলে প্রথম যে লক্ষ্য তোলা হয়, তার খরচ C*. Graph search-এ (closed set সহ) একই যুক্তি খাটে যদি h consistent হয় (অংশ ৫)। এর সঙ্গে একটি জোরালো ফল আছে: consistent heuristic-এ A* f < C*-এর প্রতিটি node অবশ্যই expand করে, আর একই heuristic ব্যবহার করা অন্য কোনো optimal অ্যালগরিদমও সেগুলো এড়াতে পারে না (tie-breaking বাদে); Russell ও Norvig-এর “Artificial Intelligence: A Modern Approach” (৪র্থ সংস্করণ) এই ধারণাকে optimally efficient বলে, আর Dechter ও Pearl ১৯৮৫ সালে এর শর্তগুলো খুঁটিয়ে বিশ্লেষণ করেন।

৫ · Consistency — ত্রিভুজ-অসমতা আর graph search-এ আবার খোলা

একটি heuristic consistent (কখনো monotone-ও বলা হয়) যদি প্রতিটি রাস্তা n → n′-এর জন্য:

h(n) ≤ c(n, n′) + h(n′)

এখানে c(n, n′) হলো n থেকে n′-এর এক ধাপের খরচ। এটি একটি triangle inequality (ত্রিভুজ-অসমতা): n থেকে লক্ষ্যের অনুমান, এক ধাপ চলে n′ থেকে লক্ষ্যের অনুমানের সঙ্গে সেই ধাপের খরচ যোগের চেয়ে বেশি হতে পারবে না। সহজ কথায়, এক ধাপ এগোলে অনুমান ধাপের খরচের চেয়ে বেশি কমতে পারে না। সরলরেখার দূরত্ব এটি মানে, কারণ ত্রিভুজের এক বাহু অন্য দুই বাহুর যোগের চেয়ে বড় নয়; Manhattan distance grid-এ এক ধাপে ঠিক 1 বা তার কম বদলায়। লেখকের যন্ত্রে ম্যাপের ১৯টি রাস্তার দুই দিকেই পরীক্ষা করে একটিও অসঙ্গত রাস্তা পাওয়া যায়নি।

কেন দরকার? Consistent হলে একটি পথ ধরে f কখনো কমে না: f(n′) = g(n) + c + h(n′) ≥ g(n) + h(n) = f(n). ফলে কোনো node প্রথমবার যখন frontier থেকে ওঠে, তখনই তার g সেরা। তাই graph search একবার খোলা node-কে closed set-এ রেখে নিশ্চিন্তে ভুলে যেতে পারে। Consistent হলে admissible-ও হয় (লক্ষ্য থেকে উল্টো দিকে এই অসমতা যোগ করলেই আসে), কিন্তু উল্টোটা সবসময় নয়।

একটি ছোট উদাহরণ, যা হাতে মিলিয়ে নেওয়া যায়। রাস্তা: S → A (1), S → B (3), A → B (1), B → G (3). সেরা পথ S → A → B → G, খরচ 5. Heuristic: h(S) = 0, h(A) = 4, h(B) = 1, h(G) = 0. এটি admissible (h*(A) = 4, h*(B) = 3), কিন্তু A → B রাস্তায় h(A) = 4 > 1 + h(B) = 2, তাই inconsistent.

ধাপতোলা হলোclosed node আবার খোলা হয় নাআবার খোলা যায় (reopen)
১S (f 0)A f = 1 + 4 = 5, B f = 3 + 1 = 4একই
২B (g 3, f 4)B closed; G f = 6একই
৩A (g 1, f 5)B-তে g 2 পাওয়া গেল, কিন্তু B closed, তাই বাদB আবার frontier-এ, g 2, f 3
৪—G তোলা হলো, খরচ 6B (g 2) আবার expand, G f 5
৫——G তোলা হলো, খরচ 5

লেখকের যন্ত্রে দুই রূপই চালিয়ে মেলানো হয়েছে: closed node না খুললে পথ SBG, খরচ 6; আবার খুললে SABG, খরচ 5. অর্থাৎ শুধু admissible heuristic নিয়ে graph search করলে হয় closed node আবার খোলার ব্যবস্থা রাখতে হবে (আরও কাজ), নয়তো heuristic consistent হতে হবে। বাস্তবে বেশিরভাগ স্বাভাবিক admissible heuristic (সরলরেখা, Manhattan, পরের অংশের h1 আর h2) consistent-ও, তাই সমস্যাটি সাধারণত হাতে বানানো বা কয়েকটি heuristic মেশানোর সময় দেখা দেয়।

অংশ ৩-এর কোডে কী হয়? সেই best_first আলাদা closed set রাখে না; যে node-এ ভালো g আসে, তাকে আবার heap-এ ঢোকায়। তাই এটি inconsistent heuristic-এও admissible থাকলে ঠিক উত্তর দেয়, কিন্তু কিছু node একাধিকবার expand হতে পারে। অংশ ৬-এর grid কোড closed set রাখে, যা Manhattan-এর মতো consistent heuristic-এ নিরাপদ।

৬ · Grid গোলকধাঁধায় A* — Manhattan distance আর কতগুলো ঘর খুলতে হলো

গুদামের ভেতরে একটি পণ্য-তোলার রোবট বা একটি ভিডিও গেমের চরিত্র ভাবুন, যে ঘরে ঘরে চলে: উপরে, নিচে, ডানে বা বাঁয়ে, প্রতি ধাপে খরচ 1. কিছু ঘরে দেয়াল (তাক)। লক্ষ্য (rT, cT) হলে Manhattan heuristic h(r, c) = |r − rT| + |c − cT|. নিচের কোড একটি ২০ × ২০ grid বানায়, যার প্রায় ২৮% ঘরে এলোমেলো দেয়াল (seed 7, তাই সবার একই ছবি)। একই astar function একটি ওজন w নেয়: w = 0 মানে h-কে শূন্য ধরা, অর্থাৎ UCS; w = 1 মানে সাধারণ A*. সমান f-এ ছোট h-এর ঘর আগে ওঠে (tie-breaking). এই block-এর function পরের অংশের weighted A* block-ও ব্যবহার করবে।

import heapq
import numpy as np
import matplotlib.pyplot as plt

def astar(wall, S, T, w=1.0):            # w = 0 হলে UCS, w = 1 হলে A*
    H, W = wall.shape
    h = lambda p: abs(p[0] - T[0]) + abs(p[1] - T[1])    # Manhattan
    g, par, closed = {S: 0}, {S: None}, set()
    fr = [(w * h(S), 0, 0, S)]           # (f, w*h, g, ঘর)
    while fr:
        f, _, gs, p = heapq.heappop(fr)
        if p in closed: continue
        closed.add(p)
        if p == T: break
        for q in ((p[0]+1, p[1]), (p[0]-1, p[1]), (p[0], p[1]+1), (p[0], p[1]-1)):
            if 0 <= q[0] < H and 0 <= q[1] < W and not wall[q] and gs + 1 < g.get(q, 1e9):
                g[q], par[q] = gs + 1, p
                heapq.heappush(fr, (gs + 1 + w * h(q), w * h(q), gs + 1, q))
    path, p = [], T
    while p is not None: path.append(p); p = par.get(p)
    return g.get(T), closed, path[::-1]

rng = np.random.default_rng(7)
wall = rng.random((20, 20)) < 0.28
S, T = (0, 0), (19, 19)
wall[S] = wall[T] = False
fig, axes = plt.subplots(1, 2, figsize=(8, 4))
for ax, w, name in zip(axes, (0, 1), ("UCS (h = 0)", "A* (Manhattan h)")):
    cost, closed, path = astar(wall, S, T, w)
    img = np.where(wall, 0.0, 1.0)
    for c in closed: img[c] = 0.6       # expand হওয়া ঘর ধূসর
    ax.imshow(img, cmap="gray", vmin=0, vmax=1)
    ax.plot([c[1] for c in path], [c[0] for c in path], "r-", lw=2)
    ax.set_title(f"{name}: cost {cost}, expanded {len(closed)}"); ax.axis("off")
    print(name, "| cost:", cost, "| expanded:", len(closed), "| free cells:", (~wall).sum())

আসল আউটপুট (ছবির নিচে এই দুই লাইন):

UCS (h = 0) | cost: 42 | expanded: 275 | free cells: 283
A* (Manhattan h) | cost: 42 | expanded: 166 | free cells: 283

দুই ছবিতে কালো ঘর দেয়াল, ধূসর ঘর expand হয়েছে, লাল রেখা পাওয়া পথ। খোলা ঘর আছে ২৮৩টি। UCS প্রায় সবগুলো, ২৭৫টি, খুলে তবে লক্ষ্যে পৌঁছায়: সে শুরু থেকে “পানির ঢেউয়ের মতো” সব দিকে সমানভাবে ছড়ায়। A* খোলে ১৬৬টি, আর ধূসর অংশটি শুরু থেকে লক্ষ্যের দিকে কোনাকুনি একটি ফিতার মতো। দুটির খরচই 42, কারণ Manhattan admissible. দেয়াল না থাকলে খরচ হতো 19 + 19 = 38, যা শুরুতে h-এর মান; দেয়ালের জন্য ঘুরপথে আরও 4 ধাপ লেগেছে, তাই h(শুরু) = 38 আসল 42-এর চেয়ে কম, ঠিক যেমন admissible heuristic-এর হওয়ার কথা। লাভ এখানে প্রায় ৪০% কম ঘর; খোলা মাঠে লাভ অনেক বেশি, আর জটিল গোলকধাঁধায় (যেখানে Manhattan বিভ্রান্ত করে) কম।

৭ · 8-puzzle — দুটি heuristic, dominance আর effective branching factor

8-puzzle হলো ৩ × ৩ বোর্ডে ১ থেকে ৮ নম্বরের আটটি টাইল আর একটি ফাঁকা ঘর। ফাঁকার পাশের একটি টাইলকে ফাঁকায় সরানো একটি action, খরচ 1. লক্ষ্য: উপরের সারি 1 2 3, মাঝে 4 5 6, নিচে 7 8 আর ফাঁকা। বোর্ডের সব সাজানো 9! = 362,880 রকম, কিন্তু শুধু অর্ধেক, 181,440টি, লক্ষ্য থেকে পৌঁছানো যায়; লেখকের যন্ত্রে লক্ষ্য থেকে BFS চালিয়ে ঠিক এই সংখ্যা আর সবচেয়ে কঠিন অবস্থার জন্য 31 ধাপ পাওয়া গেছে। দুটি পরিচিত heuristic:

  • h1 = ভুল ঘরে থাকা টাইলের সংখ্যা (ফাঁকা গোনা হয় না)। প্রতিটি ভুল টাইলকে অন্তত একবার সরাতেই হবে, তাই h1 ≤ h*.
  • h2 = প্রতিটি টাইলের নিজের লক্ষ্য-ঘর পর্যন্ত Manhattan distance-এর যোগ। প্রতিটি সরানো একটি টাইলকে ঠিক এক ঘর এগোয়, তাই h2 ≤ h*.
1অবস্থা    লক্ষ্য25 1 3     1 2 334 _ 2     4 5 647 8 6     7 8 _
মান (Watch)
টাইল
এখন (সারি, কলাম)
লক্ষ্য-ঘর
|Δসারি| + |Δকলাম|
h2 (চলতি যোগ)
h1 (চলতি গণনা)
  1. অবস্থা। সারি আর কলাম ০ থেকে গোনা। ফাঁকা (_) মাঝখানে। প্রতিটি টাইলের এখনকার ঘর আর লক্ষ্য-ঘরের দূরত্ব যোগ করা হবে।
  2. টাইল 5. আছে (0, 0)-তে, থাকার কথা (1, 1)-এ: এক সারি নিচে, এক কলাম ডানে, দূরত্ব 2. ভুল ঘরে, তাই h1-এ 1.
  3. টাইল 1. (0, 1) থেকে (0, 0): এক কলাম, দূরত্ব 1. h2 এখন 3, h1 2.
  4. টাইল 3, 4, 7, 8. চারটিই নিজের ঘরে, দূরত্ব 0, h1-এ কিছু যোগ হয় না।
  5. টাইল 2. (1, 2) থেকে (0, 1): এক সারি, এক কলাম, দূরত্ব 2. h2 = 5, h1 = 3.
  6. টাইল 6. (2, 2) থেকে (1, 2): দূরত্ব 1. মোট h2 = 6, h1 = 4. লেখকের যন্ত্রে A* দিয়ে এই অবস্থার সেরা সমাধান 12 ধাপ পাওয়া গেছে, তাই দুটিই কম বলে, আর h2 আসলের বেশি কাছে।

Dominance — বড় admissible heuristic ভালো

প্রতিটি অবস্থায় h2 ≥ h1, কারণ যে টাইল ভুল ঘরে, তার Manhattan distance অন্তত 1. দুটিই admissible হলে আর সব node-এ h2 ≥ h1 হলে বলা হয় h2, h1-কে dominate করে। তখন A*-h2 কখনো A*-h1-এর চেয়ে বেশি node expand করে না (tie-breaking বাদে): A* f(n) < C*-এর প্রায় সব node খোলে, মানে h(n) < C* − g(n), আর বড় h-এ এই শর্ত কম node পূরণ করে। লেখকের যন্ত্রে ১,৮১,৪৪০টি অবস্থার প্রতিটিতে পরীক্ষা করে h1 ≤ h*, h2 ≤ h* আর h1 ≤ h2 তিনটিই সত্য পাওয়া গেছে।

Effective branching factor b*

বিভিন্ন গভীরতার সমস্যায় অ্যালগরিদম তুলনা করতে একটি সংখ্যা চাই। Branching factor b মানে প্রতিটি node থেকে গড়ে কতটি সন্তান। Effective branching factor b* হলো সেই b, যার একটি সুষম গাছ d গভীরতায় ঠিক N + 1টি node রাখে:

N + 1 = 1 + b* + (b*)2 + … + (b*)d

এখানে N হলো search-এর দেখা node-এর সংখ্যা, d সমাধানের গভীরতা। b* যত 1-এর কাছে, heuristic তত সরাসরি লক্ষ্যের দিকে নিয়ে যায়। একটি হাতে হিসাব: d = 2-এ N = 6 হলে 1 + b + b² = 7, তাই b² + b − 6 = 0, b* = 2. Russell ও Norvig তাঁদের বইয়ে N হিসেবে তৈরি হওয়া (generated) node গোনেন; এই পর্বের কোড expand হওয়া node গোনে, তাই এখানকার b* তাঁদের সংখ্যার চেয়ে ছোট আসে। ধারা জোড়া মেলানো যায় না বলে b* বের করা হয় bisection দিয়ে: একটি সীমার মাঝখান নিয়ে বারবার অর্ধেক করা।

নিচের কোড লক্ষ্য থেকে এলোমেলো চাল দিয়ে (আগের অবস্থায় না ফিরে) তিনটি সমাধানযোগ্য puzzle বানায়, seed 1. লক্ষ্য থেকে উল্টো চাল দিয়ে বানানো বলে প্রতিটি অবশ্যই সমাধানযোগ্য। তারপর UCS (h = 0), A*-h1 আর A*-h2 চালিয়ে expand হওয়া node আর b* দেখায়। পাতার ভেতরে গভীরতা ছোট রাখা হয়েছে, কারণ UCS গভীরতা ২০-এ চল্লিশ হাজারের বেশি node খোলে; বড় গভীরতা নিচে ডেস্কটপের run-এ।

import heapq, random
GOAL = (1, 2, 3, 4, 5, 6, 7, 8, 0)               # 0 = ফাঁকা ঘর
POS = {t: divmod(i, 3) for i, t in enumerate(GOAL)}
def h1(s): return sum(1 for i, t in enumerate(s) if t and t != GOAL[i])
def h2(s): return sum(abs(i // 3 - POS[t][0]) + abs(i % 3 - POS[t][1])
                      for i, t in enumerate(s) if t)
def h0(s): return 0                               # h = 0 মানে UCS
def moves(s):
    z = s.index(0); r, c = divmod(z, 3)
    for dr, dc in ((1, 0), (-1, 0), (0, 1), (0, -1)):
        if 0 <= r + dr < 3 and 0 <= c + dc < 3:
            j = (r + dr) * 3 + c + dc; t = list(s)
            t[z], t[j] = t[j], t[z]; yield tuple(t)
def astar(s, h):
    best, fr, closed = {s: 0}, [(h(s), 0, s)], set()
    while fr:
        f, g, x = heapq.heappop(fr)
        if x in closed: continue
        closed.add(x)
        if x == GOAL: return g, len(closed)       # খরচ, expand হওয়া node
        for y in moves(x):
            if g + 1 < best.get(y, 99):
                best[y] = g + 1; heapq.heappush(fr, (g + 1 + h(y), g + 1, y))
def bstar(N, d):                     # N + 1 = 1 + b + ... + b^d, bisection
    lo, hi = 1.0, 3.0
    for _ in range(40):
        b = (lo + hi) / 2
        lo, hi = (b, hi) if sum(b**i for i in range(d + 1)) < N + 1 else (lo, b)
    return b
rng = random.Random(1); print(" d   UCS  A*h1  A*h2 | b*h1  b*h2")
for walk in (8, 12, 16):             # পাতায় ছোট গভীরতা; বড়গুলো ডেস্কটপে
    s, prev = GOAL, None
    for _ in range(walk): s, prev = rng.choice([m for m in moves(s) if m != prev]), s
    (d, n0), (_, n1), (_, n2) = astar(s, h0), astar(s, h1), astar(s, h2)
    print(f"{d:2} {n0:5} {n1:5} {n2:5} | {bstar(n1, d):.2f}  {bstar(n2, d):.2f}")

আসল আউটপুট:

 d   UCS  A*h1  A*h2 | b*h1  b*h2
 8   180    20    16 | 1.20  1.15
12  1249    79    20 | 1.27  1.08
16  8211   563    88 | 1.37  1.18

প্রথম কলাম আসল সেরা গভীরতা d, যা তিনটি অ্যালগরিদমই একই পেয়েছে (তিনটিই optimal). এলোমেলো চাল ৮, ১২, ১৬ হলেও সমাধান আরও ছোট হতে পারত; এখানে ঘটনাক্রমে সমান এসেছে। গভীরতা ৮ থেকে ১৬-তে UCS-এর কাজ প্রায় ৪৬ গুণ বেড়েছে (180 থেকে 8211), A*-h1-এর প্রায় ২৮ গুণ, আর A*-h2-এর মাত্র সাড়ে পাঁচ গুণ। গভীরতা ১৬-তে h2 UCS-এর চেয়ে প্রায় ৯৩ গুণ কম node খোলে। b* কলামে h2 সবসময় h1-এর চেয়ে ছোট, dominance-এর প্রত্যাশামতো। মাত্র একটি করে puzzle বলে সংখ্যাগুলো এলোমেলো ওঠানামা করে (যেমন d = 12-এ h2-এর b* 1.08, যা d = 8-এর চেয়েও কম); গড় নিতে অনেক puzzle লাগে, যা নিচে।

ডেস্কটপে বড় পরীক্ষা — প্রতিটি গভীরতায় ২০টি puzzle

লেখকের যন্ত্রে সত্যিকারের run (Python 3.14.4, শুধু standard library). একই astar, h1, h2 আর bstar; পার্থক্য হলো, প্রথমে লক্ষ্য থেকে পুরো BFS চালিয়ে প্রতিটি অবস্থার আসল দূরত্ব বের করা হয়েছে, তারপর প্রতিটি জোড় গভীরতা d থেকে ঠিক d দূরত্বের ২০টি অবস্থা এলোমেলোভাবে (seed 2026) নেওয়া হয়েছে। টেবিলে expand হওয়া node-এর গড়, আর সেই গড় থেকে b*.

~/bazarghar-ai$ python puzzle_bench.py
reachable states: 181440 | max depth: 31
h1 <= h* everywhere: True
h2 <= h* everywhere: True
h1 <= h2 everywhere: True

mean nodes expanded over 20 random puzzles per depth (seed 2026)
 d      UCS    A*-h1  A*-h2 | b*UCS b*h1  b*h2
 4        22       5      5 | 1.81  1.10  1.09
 6        72      10      8 | 1.79  1.13  1.08
 8       192      16     12 | 1.74  1.16  1.08
10       506      40     20 | 1.71  1.24  1.13
12      1310      93     32 | 1.69  1.29  1.15
14      3340     233     65 | 1.67  1.34  1.19
16      8679     576    123 | 1.66  1.37  1.22
18     19938    1357    218 | 1.65  1.39  1.23
20     44102    3488    447 | 1.63  1.41  1.25
22     79249    8334    994 | 1.60  1.43  1.28
24    122546   18479   1830 | 1.56  1.43  1.28
total time 17 s, Python 3.14.4

তিনটি পর্যবেক্ষণ। এক, গভীরতা ২৪-এ UCS গড়ে ১,২২,৫৪৬টি node খোলে, পুরো ১,৮১,৪৪০-এর দুই-তৃতীয়াংশ; A*-h2 মাত্র ১,৮৩০টি, প্রায় ৬৭ গুণ কম। দুই, b*UCS গভীরতার সঙ্গে কমছে (1.81 থেকে 1.56), কারণ state space সসীম আর graph search আগে দেখা অবস্থা বাদ দেয়, তাই গাছটি আসলে সুষম নয়। তিন, A*-h1 আর A*-h2-এর পার্থক্য গভীরতার সঙ্গে বাড়ে: d = 8-এ মাত্র 16 বনাম 12, d = 24-এ ১০ গুণ। এসব একটি যন্ত্রের একটি run, expand-গণনা দিয়ে; generated node গুনলে বা অন্য tie-breaking নিলে সংখ্যা বদলাবে, কিন্তু ক্রম (UCS > h1 > h2) বদলানোর কথা নয়।

৮ · Heuristic বানানো — নিয়ম শিথিল করা আর pattern database

h1 আর h2 আকাশ থেকে পড়েনি। দুটিই একই কৌশলে পাওয়া যায়: সমস্যার কিছু নিয়ম তুলে দিয়ে একটি সহজ সমস্যা বানানো, যাকে বলে relaxed problem, আর সেই সহজ সমস্যার আসল সেরা খরচকে মূল সমস্যার heuristic হিসেবে নেওয়া। 8-puzzle-এর নিয়ম: “একটি টাইল A থেকে B-তে যেতে পারে, যদি A আর B পাশাপাশি হয় এবং B ফাঁকা হয়।”

  • “B ফাঁকা হতে হবে” শর্তটি তুলে দিলে প্রতিটি টাইল অন্যদের ওপর দিয়ে পাশের ঘরে যেতে পারে; সেরা খরচ হয় প্রতিটি টাইলের Manhattan distance-এর যোগ, মানে h2.
  • দুটি শর্তই তুলে দিলে প্রতিটি টাইল এক চালে যেকোনো ঘরে লাফ দিতে পারে; সেরা খরচ হয় ভুল ঘরের টাইলের সংখ্যা, মানে h1.

কেন এভাবে পাওয়া heuristic admissible? মূল সমস্যার প্রতিটি সমাধান শিথিল সমস্যারও একটি সমাধান (নিয়ম কমলে আগের সব বৈধ চাল বৈধই থাকে), তাই শিথিল সমস্যার সেরা খরচ মূলের সেরা খরচের চেয়ে বেশি হতে পারে না। আর এটি শিথিল সমস্যার আসল খরচ বলে consistent-ও হয়। ম্যাপের সরলরেখার দূরত্বও একই কৌশল: “শুধু রাস্তা ধরে চলতে হবে” নিয়মটি তুলে দিলে রাইডার কাকের মতো সোজা উড়ে যেতে পারেন।

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

কয়েকটি admissible heuristic থাকলে, যার কোনোটি অন্যটিকে সব জায়গায় dominate করে না, প্রতিটি node-এ তাদের সর্বোচ্চটি নেওয়া যায়: h(n) = max(ha(n), hb(n)). সর্বোচ্চটিও admissible (প্রতিটি ≤ h*), আর প্রত্যেকটিকে dominate করে। খরচ: প্রতিটি node-এ সবগুলো হিসাব করতে হয়।

Pattern database (একটি নোট): শুধু কয়েকটি টাইলের (ধরুন 1, 2, 3, 4) অবস্থান ধরে বাকিগুলোকে “যেকোনো” ধরে একটি উপ-সমস্যা বানানো যায়। সেই উপ-সমস্যার প্রতিটি সম্ভাব্য বিন্যাসের সেরা খরচ আগেই, লক্ষ্য থেকে উল্টো দিকে BFS চালিয়ে, একটি টেবিলে রাখা হয়। Search-এর সময় h হলো শুধু টেবিল দেখা। ১৯৯০-এর দশকের শেষে Joseph Culberson আর Jonathan Schaeffer এই ধারণা প্রকাশ করেন বলে পরিচিত, আর পরে Richard Korf ও সহকর্মীরা ১৫-puzzle আর Rubik's cube-এর মতো সমস্যায় একে কাজে লাগান। কিছু শর্তে আলাদা টাইল-দলের টেবিলের মান যোগও করা যায় (disjoint pattern database). দাম memory: বড় টেবিল।

৯ · Weighted A*, memory-র সমস্যা আর বাস্তবে A*

অনেক সময় একেবারে সেরা পথের চেয়ে দ্রুত উত্তর বেশি জরুরি: গেমের চরিত্রকে এক ফ্রেমের মধ্যে নড়তে হবে, রাইডারকে এখনই একটি রুট দিতে হবে। Weighted A* heuristic-কে একটি ওজন w ≥ 1 দিয়ে গুণ করে:

f(n) = g(n) + w · h(n)

w = 1 হলে সাধারণ A*; w বড় হলে আচরণ greedy-র দিকে যায়। Ira Pohl ১৯৭০ সালের আশেপাশে এই ধরনের ওজন নিয়ে কাজ করেন বলে পরিচিত। এর একটি সুন্দর নিশ্চয়তা আছে: h admissible হলে weighted A*-এর পাওয়া পথের খরচ সেরা খরচের সর্বোচ্চ w গুণ। আগের grid block-এর astar function দিয়ে এবার ৬০ × ৬০ grid (প্রায় ৩০% দেয়াল, seed 6):

# আগের grid block-এর astar() এখানে আবার ব্যবহার হচ্ছে (একই namespace)
rng = np.random.default_rng(6)
big = rng.random((60, 60)) < 0.30
S2, T2 = (0, 0), (59, 59)
big[S2] = big[T2] = False
print("free cells:", (~big).sum(), "| Manhattan h(start) =", 59 + 59)
opt = None
for w in (0, 1, 1.5, 3):
    cost, closed, path = astar(big, S2, T2, w)
    opt = opt or cost                # w = 0 (UCS) দেয় আসল সেরা খরচ
    print(f"w = {w:<3}  cost = {cost:3}  ({cost / opt:.2f} x optimal)"
          f"  expanded = {len(closed):4}")

আসল আউটপুট:

free cells: 2516 | Manhattan h(start) = 118
w = 0    cost = 126  (1.00 x optimal)  expanded = 2467
w = 1    cost = 126  (1.00 x optimal)  expanded = 1455
w = 1.5  cost = 152  (1.21 x optimal)  expanded =  831
w = 3    cost = 174  (1.38 x optimal)  expanded =  754

এই block আগে grid block-টি না চালিয়ে চালালেও চলবে; পাতা নিজেই আগেরটি আগে চালিয়ে নেয়, তাই তার ছবিও দেখাবে। UCS প্রায় সব খোলা ঘর (২,৪৬৭টি) দেখে, A* (w = 1) দেখে ১,৪৫৫টি, দুটিই 126 ধাপের সেরা পথ পায়। w = 1.5-এ node আরও প্রায় অর্ধেক কমে ৮৩১, কিন্তু পথ 152, সেরার চেয়ে ২১% লম্বা; w = 3-এ ৭৫৪টি node আর পথ ৩৮% লম্বা। দুটিই প্রতিশ্রুত সীমার মধ্যে (1.5 × 126 = 189, 3 × 126 = 378), কিন্তু শেষ ধাপে node খুব একটা কমেনি অথচ পথ অনেক খারাপ হয়েছে। এটি একটি grid-এর একটি run; অন্য গোলকধাঁধায় লেনদেনটি অন্যরকম হতে পারে, তাই নিজের সমস্যায় কয়েকটি w মেপে দেখাই নিয়ম।

Memory: A*-এর আসল দুর্বলতা

A* প্রতিটি দেখা node মনে রাখে: frontier আর explored দুটিই। গভীরতা ২৪-এর 8-puzzle-এ এটি সমস্যা নয়, কিন্তু ১৫-puzzle-এ (৪ × ৪ বোর্ড) পৌঁছানো যায় এমন অবস্থা 16!/2, প্রায় ১০ লক্ষ কোটি (১.০৫ × ১০১৩); কঠিন উদাহরণে A* সময় শেষ হওয়ার অনেক আগেই memory শেষ করে ফেলে। IDA* (iterative deepening A*, Richard Korf ১৯৮৫) এর একটি সমাধান: DFS-এর মতো এক পথ ধরে গভীরে যায়, কিন্তু যে node-এর f একটি সীমা ছাড়ায়, সেখানে থামে। এক দফা শেষ হলে সীমাটিকে সবচেয়ে ছোট ছাড়িয়ে-যাওয়া f-এ বাড়িয়ে আবার শুরু। memory লাগে শুধু বর্তমান পথের সমান, আর admissible h-এ উত্তর optimal থাকে; দাম হলো একই node বারবার দেখা। Korf এই পদ্ধতিতে ১৫-puzzle-এর এলোমেলো উদাহরণগুলোর সেরা সমাধান বের করেছিলেন বলে পরিচিত। একই উদ্দেশ্যে RBFS আর SMA*-এর মতো আরও কিছু পদ্ধতি আছে, যা এখানে শুধু নামে রইল।

বাস্তবে A* কোথায়

  • গেমের pathfinding: অনেক ভিডিও গেমে চরিত্রের পথ খুঁজতে grid বা navigation mesh (হাঁটার উপযোগী জায়গাকে বহুভুজে ভাগ করা ম্যাপ)-এর ওপর A* বা তার রূপভেদ ব্যবহার হয়, এটি গেম-ডেভেলপমেন্টের বই আর লেখায় ব্যাপকভাবে বর্ণিত। কোনাকুনি চলা গেলে Manhattan আর admissible থাকে না; তখন octile distance (অনুশীলন ৪) লাগে। গতির জন্য প্রায়ই weighted A* বা আগে থেকে হিসাব করা তথ্য ব্যবহার হয়।
  • রুট খোঁজা: মহাদেশ-আকারের সড়ক-জালে (কোটি কোটি সংযোগ) সরল A* একা যথেষ্ট দ্রুত নয়। গবেষণায় আগে থেকে হিসাব রেখে search দ্রুত করার নানা পদ্ধতি আছে, যেমন landmark আর ত্রিভুজ-অসমতা দিয়ে আরও ভালো admissible heuristic বানানো (ALT, Goldberg ও Harrelson ২০০৫) বা contraction hierarchies. কোন বাণিজ্যিক ম্যাপ-অ্যাপ ঠিক কী ব্যবহার করে, তা সাধারণত প্রকাশিত নয়, তাই এখানে কোনো দাবি করা হচ্ছে না। ট্রাফিকের মতো বদলানো খরচ সমস্যাকে আরও জটিল করে।

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

  • A*-এর evaluation function কী? f(n) = g(n) + h(n): শুরু থেকে n-এর আসল খরচ আর n থেকে লক্ষ্যের অনুমান।
  • Admissible আর consistent-এর পার্থক্য? Admissible: h(n) ≤ h*(n). Consistent: প্রতিটি ধাপে h(n) ≤ c(n, n′) + h(n′); consistent হলে admissible, উল্টোটা সবসময় নয়।
  • h = 0 হলে A* কী? UCS (ধনাত্মক খরচে Dijkstra-র মতো)। আর শুধু f = h হলে greedy best-first.
  • h1 আর h2-এর মধ্যে কোনটি ভালো, কেন? h2 (Manhattan), কারণ দুটিই admissible আর সব জায়গায় h2 ≥ h1, তাই কম node expand হয়।
  • A*-এর প্রধান অসুবিধা? Memory: সব দেখা node রাখে; IDA* বা অন্য memory-সীমিত রূপ এর সমাধান।

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

“Python-এ A* লিখে দাও” বললে AI সহকারী কয়েক সেকেন্ডে চলনসই কোড দেয়, আর সেই কোড প্রায়ই একটি পথ ফেরত দেয়ও। সমস্যা হলো, পথটি সেরা কি না তা চোখে দেখে বোঝা যায় না। এই বিষয়ে AI সহকারীরা প্রায়ই যা করে:

  • লক্ষ্য খুঁজে পাওয়ামাত্র থামা: লক্ষ্যকে frontier-এ ঢোকানোর সময় থামলে A*-এর optimality-র যুক্তি ভেঙে যায় (অংশ ৩-এর ধাপ ৪-এ 15 km-এর মতো)। থামতে হয় লক্ষ্য frontier থেকে তোলার সময়।
  • ভুল heuristic: কোনাকুনি চলা যায় এমন grid-এ Manhattan, বা km-এর ম্যাপে মিনিটে মাপা h; একক না মেলালে admissibility হারায়।
  • closed set-এর ভুল: inconsistent heuristic-এ closed node আর না খোলা, বা উল্টো দিকে, closed set একেবারে না রেখে বারবার একই node খোলা।
  • heap-এর খুঁটিনাটি: ভালো g পেলে পুরোনো entry বাদ না দেওয়া, অথবা heapq-এ তুলনা করা যায় না এমন বস্তু (যেমন dict) রাখা, ফলে সমান f-এ TypeError.
  • বানানো তুলনা: “A* সবসময় BFS-এর চেয়ে ১০ গুণ দ্রুত” ধরনের সাধারণ দাবি, বা না চালিয়েই node-সংখ্যার টেবিল। লাভ heuristic আর সমস্যার ওপর নির্ভর করে, মাপতে হয়।

প্রম্পটে সমস্যা, খরচ, heuristic-এর শর্ত আর যাচাইয়ের উপায় নিজেই বলে দিন। নিচের প্রম্পটে আসল ক্রেতার ঠিকানা নেই, শুধু বানানো ছোট ম্যাপ।

আমি Python 3.14 ব্যবহার করছি, শুধু standard library (heapq, math). একটি ছোট কাল্পনিক রাস্তার graph আছে: প্রতিটি node-এর (x, y) km-এ, আর প্রতিটি রাস্তার দৈর্ঘ্য km-এ। ১. A* লেখো, h = লক্ষ্য পর্যন্ত সরলরেখার দূরত্ব। লক্ষ্য frontier থেকে তোলার সময় থামবে, ঢোকানোর সময় নয়। ২. প্রতিটি রাস্তায় assert দিয়ে যাচাই করো যে দৈর্ঘ্য ≥ সরলরেখার দূরত্ব, আর প্রতিটি রাস্তায় h(a) ≤ c(a, b) + h(b). ৩. একই কোডে h = 0 (UCS) চালিয়ে খরচ মেলাও; দুটি খরচ আলাদা হলে কোড ভুল। ৪. expand হওয়া node-এর সংখ্যা গুনে প্রিন্ট করো; কোনো সংখ্যা আন্দাজে লিখবে না। ৫. সমান f-এ heap কীভাবে tie ভাঙছে, এক লাইনে বলো। কোনো function বা আচরণ নিয়ে নিশ্চিত না হলে বানিয়ে লিখবে না, সরাসরি বলবে।

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

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

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

প্র ০১A* কেন একটি রোবট প্রকল্প থেকে এল, আর ১৯৬৮ সালের প্রবন্ধের নামে “Formal Basis” কথাটি কেন গুরুত্বপূর্ণ?

১৯৬০-এর দশকের মাঝামাঝি SRI-তে Shakey নামের একটি রোবট বানানো হচ্ছিল, যাকে প্রায়ই প্রথম দিকের এমন রোবট বলা হয় যে নিজের কাজ নিয়ে যুক্তি করতে পারত: ক্যামেরা দিয়ে ঘর দেখা, বাধা চেনা, আর এক ঘর থেকে আরেক ঘরে যাওয়ার পথ ঠিক করা। পথ খোঁজার জন্য Nils Nilsson প্রথমে একটি heuristic-ভিত্তিক পদ্ধতি নিয়ে কাজ করেন, পরে Bertram Raphael আর Peter Hart যুক্ত হন। প্রচলিত বর্ণনা অনুযায়ী আগের কয়েকটি রূপের পর তাঁরা যে রূপে পৌঁছান, সেটিই A*, যেখানে তারকা চিহ্নটি “সেরা” রূপ বোঝাতে।

সে সময় heuristic search নতুন ছিল না। Newell আর Simon-এর মতো গবেষকেরা সমস্যা-সমাধানে heuristic ব্যবহার করছিলেন, আর ১৯৬৬ সালে Doran আর Michie-র Graph Traverser অনুমান দিয়ে node বাছত বলে পরিচিত। কিন্তু বেশিরভাগ কাজ ছিল পরীক্ষানির্ভর: “এটি ভালো চলে”। Hart, Nilsson আর Raphael-এর প্রবন্ধের নামের “Formal Basis” কথাটি এখানেই গুরুত্বপূর্ণ। তাঁরা প্রমাণ করলেন কোন শর্তে (অনুমান কখনো আসল খরচের বেশি নয়) heuristic search সবচেয়ে কম খরচের পথ দেবেই, আর একই তথ্য ব্যবহার করা অন্য পদ্ধতির তুলনায় অপ্রয়োজনীয় কাজ কম করে।

এই প্রমাণ একটি দর্শনগত বদলও আনে: heuristic মানেই অনিশ্চিত কৌশল নয়। ঠিক শর্ত মানলে অনুমান শুধু গতি বাড়ায়, ফলের নির্ভুলতা কমায় না। প্রবন্ধের একটি প্রমাণে পরে একটি ফাঁক ধরা পড়ে, আর লেখকেরা ১৯৭২ সালে একটি সংশোধনী প্রকাশ করেন বলে উল্লেখ পাওয়া যায়; এর সঙ্গে consistency-র (তখন consistency assumption নামে) ভূমিকা জড়িত ছিল। পরে Dechter আর Pearl-এর মতো গবেষকেরা optimality-র শর্তগুলো আরও খুঁটিয়ে বিশ্লেষণ করেন। পঞ্চাশ বছরেরও বেশি পরে A* এখনো পাঠ্যবই, গেম আর রোবটিক্সে মৌলিক অ্যালগরিদম।

মূল উপলব্ধি: A*-এর শক্তি শুধু গতিতে নয়, প্রমাণে: একটি স্পষ্ট শর্ত (admissibility) মানলে অনুমান দিয়েও নিশ্চিতভাবে সেরা পথ পাওয়া যায়, আর এই গাণিতিক নিশ্চয়তাই একে টিকিয়ে রেখেছে।

প্র ০২বড় heuristic মানেই কি সবসময় ভালো? h = h* হলে কী হয়, আর কোথায় এর দাম দিতে হয়?

Dominance-এর ফল বলে, দুটি admissible heuristic-এর মধ্যে বড়টি কম node expand করায়। চরম রূপে h = h* হলে, মানে অনুমান হুবহু আসল খরচ, A* প্রায় সরাসরি সেরা পথ ধরে চলে: শুধু সেরা পথের node আর সমান f-এর কিছু node খোলে। তাহলে সবসময় যত বড় সম্ভব heuristic নিলেই তো হয়?

না, কারণ দুটি খরচ আছে। প্রথমটি হিসাবের খরচ। h* বের করা মানে মূল সমস্যাটিই সমাধান করা; তাহলে search-এর দরকার থাকত না। একটি heuristic যত আসলের কাছে যায়, সাধারণত তত ব্যয়বহুল হয়। মোট সময় = (expand হওয়া node) × (প্রতি node-এ খরচ, heuristic হিসাবসহ)। কম node দেখেও যদি প্রতিটিতে অনেক সময় লাগে, মোট সময় বাড়তে পারে। পর্ব ৪৩-এ (Constraint satisfaction) একই লেনদেন দেখবেন: ভালো অনুমান কতটা হিসাব-খরচের যোগ্য।

দ্বিতীয়টি memory-র খরচ। Pattern database খুব শক্তিশালী heuristic দেয়, কিন্তু বড় টেবিল লাগে, যা আগে হিসাব করে memory-তে রাখতে হয়। ১৫-puzzle-এর জন্য টাইল-দলভিত্তিক টেবিল কয়েক কোটি entry পর্যন্ত হতে পারে বলে বর্ণনা আছে। ছোট যন্ত্রে, যেমন একটি মোবাইল গেমে, এই memory না-ও থাকতে পারে।

একটি তৃতীয় সূক্ষ্মতাও আছে: “বড়” হতে হবে admissible থেকেই। Admissibility ভেঙে বড় করলে (inadmissible) node আরও কমতে পারে, কিন্তু optimality-র নিশ্চয়তা চলে যায়, যা অংশ ৪-এ দেখেছেন। আধুনিক গবেষণায় neural network দিয়ে heuristic শেখানোর চেষ্টাও হয়েছে (যেমন Rubik's cube-এর জন্য ২০১৯ সালে প্রকাশিত DeepCubeA নামের একটি কাজ); সেখানে শেখা heuristic সাধারণত admissible হওয়ার নিশ্চয়তা দেয় না, তাই পাওয়া সমাধান খুব ভালো হলেও সবসময় প্রমাণিতভাবে সেরা নয়।

মূল উপলব্ধি: ভালো heuristic মানে admissibility রেখে আসলের যতটা কাছে যাওয়া যায়, কিন্তু প্রতি node-এর হিসাবের সময় আর memory-র দাম মিলিয়ে; মোট সময়ই আসল মাপকাঠি, শুধু node-এর সংখ্যা নয়।

প্র ০৩বাস্তবে কি সবসময় সেরা পথ দরকার? গেম আর ডেলিভারি অ্যাপে optimality বনাম গতির লেনদেন কীভাবে দেখা হয়?

একটি গেমের কথা ভাবুন, যেখানে একসঙ্গে শত শত চরিত্র চলছে, আর পর্দা প্রতি সেকেন্ডে ৬০ বার আঁকতে হয়। প্রতিটি ফ্রেমে কয়েক মিলিসেকেন্ডের বেশি সময় পথ খুঁজতে দেওয়া যায় না। খেলোয়াড় খেয়াল করবেন না চরিত্রটি সেরা পথের চেয়ে দুই ঘর বেশি হাঁটল কি না, কিন্তু খেয়াল করবেন যদি গেম আটকে যায়। তাই গেমে weighted A*, পথ ছোট টুকরোয় খোঁজা, আর আগে থেকে হিসাব রাখা তথ্য প্রায়ই সাধারণ পছন্দ বলে গেম-ডেভেলপমেন্টের লেখায় দেখা যায়।

ডেলিভারি রুটের ক্ষেত্রে হিসাব ভিন্ন। বাজারঘর দিনে হাজারটি ডেলিভারি করলে প্রতি রুটে ৫% বাড়তি দূরত্ব মানে জ্বালানি আর সময়ের উল্লেখযোগ্য খরচ। আবার রাইডারকে রুট দিতে এক সেকেন্ড দেরি করা যায়, এক ঘণ্টা নয়। এখানে weighted A*-এর নিশ্চয়তাটি কাজে লাগে: w = 1.1 নিলে পথ সেরার চেয়ে সর্বোচ্চ ১০% খারাপ, এটি আগেই জানা। অংশ ৯-এর run দেখায়, প্রকৃত খারাপ হওয়া প্রায়ই সীমার চেয়ে অনেক কম (w = 3-এ সীমা ৩ গুণ, আসলে ১.৩৮ গুণ)।

এই চিন্তা থেকে “anytime” অ্যালগরিদম এসেছে: প্রথমে বড় w দিয়ে দ্রুত একটি পথ, তারপর সময় থাকলে w কমিয়ে কমিয়ে পথটি উন্নত করা। Maxim Likhachev ও সহকর্মীদের ২০০৩ সালের আশেপাশের ARA* এর একটি পরিচিত উদাহরণ, যা রোবটিক্সে ব্যবহার হয়েছে বলে বর্ণনা আছে। আরেকটি দিক হলো, বাস্তবের খরচ নিজেই অনিশ্চিত: ট্রাফিক বদলায়, রাস্তা বন্ধ হয়। গাণিতিকভাবে সেরা পথটি আসলে পাঁচ মিনিট পরে আর সেরা না-ও থাকতে পারে। তখন দ্রুত আবার পরিকল্পনা করার ক্ষমতা (incremental search, যেমন D* Lite) নিখুঁত optimality-র চেয়ে বেশি মূল্যবান হতে পারে।

মূল উপলব্ধি: Optimality একটি মূল্যবান নিশ্চয়তা, কিন্তু বিনামূল্যে নয়; সময়ের সীমা, খরচের অনিশ্চয়তা আর ভুলের দাম মিলিয়ে ঠিক করতে হয় কতটুকু সেরা যথেষ্ট সেরা, আর weighted A* এই লেনদেনকে একটি স্পষ্ট সংখ্যায় বাঁধে।

প্র ০৪Heuristic কি মানুষের অন্তর্দৃষ্টি, নাকি যন্ত্রও নিজে বানাতে পারে? Relaxation-এর ধারণা এখানে কী বলে?

“Heuristic” শব্দটি গ্রিক ভাষার “খুঁজে পাওয়া” অর্থের শব্দ থেকে এসেছে বলে ধরা হয়, আর গণিতবিদ George Pólya-র ১৯৪৫ সালের বই “How to Solve It” সমস্যা সমাধানের মানবিক কৌশল হিসেবে heuristic-এর ধারণাকে জনপ্রিয় করে। AI-এর শুরুর দিকে heuristic মানে ছিল মানুষ বিশেষজ্ঞের অভিজ্ঞতা থেকে আসা কৌশল, যা প্রোগ্রামে হাতে লেখা হতো। দাবা প্রোগ্রামে “মন্ত্রী মূল্যবান, তাকে রক্ষা করুন” ধরনের নিয়ম এর উদাহরণ (খেলার search পর্ব ৪২-এ)।

Relaxation এই ধারণাকে আরও যান্ত্রিক করে। Judea Pearl-এর ১৯৮৪ সালের বই “Heuristics” এর একটি প্রভাবশালী ব্যাখ্যা দেয়: সমস্যার নিয়মগুলোকে একটি আনুষ্ঠানিক ভাষায় লিখলে, কোন শর্ত তুলে দিলে একটি সহজ সমস্যা পাওয়া যায় তা যন্ত্র নিজেই খুঁজতে পারে। অংশ ৮-এ দেখেছেন, 8-puzzle-এর একটি নিয়ম থেকে দুটি শর্ত তুলে দিয়ে h1 আর h2 পাওয়া যায়। ১৯৯০-এর দশকের শুরুতে Armand Prieditis-এর ABSOLVER নামের একটি প্রোগ্রাম এভাবে স্বয়ংক্রিয়ভাবে heuristic বানিয়েছিল, আর তাতে 8-puzzle-এর জন্য একটি নতুন ভালো heuristic পাওয়া গিয়েছিল বলে Russell ও Norvig-এর বইয়ে উল্লেখ আছে।

AI planning-এ (পর্ব ৪৯) এটি এখন সাধারণ চর্চা। আধুনিক planner-গুলো কাজের বর্ণনা থেকে নিজেই শিথিল সমস্যা বানিয়ে heuristic হিসাব করে, যেমন “কাজের নেতিবাচক প্রভাব উপেক্ষা করা” (delete relaxation). আর তৃতীয় পথ হলো ডেটা থেকে শেখা: অনেক সমাধান করা উদাহরণ থেকে একটি মডেলকে h* আন্দাজ করতে শেখানো। এটি শক্তিশালী হতে পারে, কিন্তু শেখা মডেল কখন বাড়িয়ে বলবে, তা আগে থেকে বলা কঠিন।

তাই প্রশ্নটির উত্তর দুই দিকেই। মানুষের অন্তর্দৃষ্টি এখনো ঠিক করে কোন কাঠামো (টাইলের অবস্থান, রাস্তার জ্যামিতি) গুরুত্বপূর্ণ, আর যন্ত্র সেই কাঠামো থেকে নিয়মমাফিক, প্রমাণসহ heuristic বানায় বা শেখে।

মূল উপলব্ধি: Relaxation heuristic-কে “অভিজ্ঞের আন্দাজ” থেকে একটি নিয়মমাফিক কৌশলে বদলেছে: নিয়ম তুলে সহজ সমস্যা বানালে admissible আর consistent অনুমান আপনা থেকেই আসে, যা যন্ত্রও খুঁজে নিতে পারে।

প্র ০৫A* যদি সবচেয়ে কম node expand করে, তবু IDA*-এর মতো “একই node বারবার দেখা” পদ্ধতি কেন দরকার হলো?

A*-এর optimal efficiency মাপা হয় expand হওয়া node দিয়ে, memory দিয়ে নয়। আর A* যত node দেখে, প্রায় সবগুলো মনে রাখে: frontier-এ যারা অপেক্ষায়, আর closed set-এ যারা খোলা হয়েছে। 8-puzzle-এ সব মিলিয়ে ১,৮১,৪৪০টি অবস্থা, আধুনিক যন্ত্রে সহজেই ধরে। কিন্তু ১৫-puzzle-এ প্রায় ১.০৫ × ১০১৩টি অবস্থা। প্রতিটি অবস্থা কয়েক ডজন byte হলেও এর একটি ছোট অংশ রাখতেই বহু গিগাবাইট লাগে। ১৯৮০-এর দশকের যন্ত্রে, যেখানে memory ছিল কয়েক মেগাবাইট, কঠিন ১৫-puzzle-এ A* সময়ের অনেক আগেই memory ফুরিয়ে ফেলত।

Richard Korf ১৯৮৫ সালে IDA* প্রকাশ করেন, যা পর্ব ৪০-এর iterative deepening-এর ধারণাকে f-এর সীমার সঙ্গে মেলায়। প্রতিটি দফা একটি DFS, তাই memory লাগে শুধু বর্তমান পথের দৈর্ঘ্যের সমান। দাম হলো পুনরাবৃত্তি: প্রতিটি দফায় আগের দফার সব node আবার দেখা হয়। কিন্তু search tree দ্রুত বড় হয় বলে শেষ দফাটিই মোট কাজের বড় অংশ, তাই পুনরাবৃত্তির বাড়তি খরচ প্রায়ই সহনীয়। Korf IDA* আর Manhattan heuristic দিয়ে ১৫-puzzle-এর ১০০টি এলোমেলো উদাহরণের সেরা সমাধান বের করেছিলেন বলে পরিচিত, যা সে সময় A* দিয়ে সম্ভব ছিল না।

IDA*-এর নিজের দুর্বলতাও আছে। যে সমস্যায় খরচ বাস্তব সংখ্যা (যেমন 3.7 km, 4.1 km), সেখানে প্রতি দফায় f-এর সীমা হয়তো খুব সামান্য বাড়ে, আর দফার সংখ্যা বিশাল হয়। আর DFS আগে দেখা অবস্থা মনে রাখে না বলে অনেক পথ একই অবস্থায় গেলে (যেমন grid-এ) একই কাজ বহুবার হয়। এজন্য মাঝামাঝি পদ্ধতি এসেছে: RBFS, যা অল্প তথ্য রেখে ফেরে, আর SMA*, যা যতটুকু memory আছে পুরোটা ব্যবহার করে আর ভরে গেলে সবচেয়ে খারাপ node ফেলে দেয়।

মূল উপলব্ধি: Search-এ সময় আর memory দুটি আলাদা সীমা; A* সময়ের দিক থেকে সেরা হলেও memory-তে আটকায়, আর IDA* সামান্য বাড়তি সময়ের বিনিময়ে memory-কে পথের দৈর্ঘ্যে নামিয়ে আনে।

অনুশীলন

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

  1. হাতে trace: একটি দুই-দিকের graph-এ রাস্তা: S–A 2, S–B 5, A–C 4, A–B 2, B–G 4, C–G 3. Heuristic: h(S) = 6, h(A) = 5, h(B) = 3, h(C) = 3, h(G) = 0. (ক) h কি admissible আর consistent? (খ) A* চালিয়ে প্রতিটি ধাপে কোন node expand হয় আর frontier কী থাকে, লিখুন; পাওয়া পথ আর খরচ? (গ) Greedy best-first কোন পথ দেয়?

    (ক) আসল বাকি খরচ: h*(G) = 0, h*(C) = 3, h*(B) = 4, h*(A) = 6 (A–B–G), h*(S) = 8 (S–A–B–G). প্রতিটি h ≤ h*, তাই admissible. প্রতিটি রাস্তায় দুই দিকেই h(n) ≤ c + h(n′) মেলে (যেমন A–B: 5 ≤ 2 + 3, C–G: 3 ≤ 3 + 0), তাই consistent-ও।

    (খ)

    • S expand (f 6): A g 2, f 7; B g 5, f 8.
    • A expand (f 7): C g 6, f 9; B-তে g 4, যা 5-এর চেয়ে ভালো, তাই B f 7.
    • B expand (g 4, f 7): G g 8, f 8. পুরোনো B (f 8) heap-এ থাকলেও পরে বাতিল হয়।
    • G তোলা হলো (f 8): পথ S → A → B → G, খরচ 8.

    (গ) Greedy: S থেকে B (h 3) আর A (h 5)-এর মধ্যে B; B থেকে G (h 0). পথ S → B → G, খরচ 9, সেরা নয়। লেখকের যন্ত্রে দুটি trace-ই কোড দিয়ে মেলানো হয়েছে: A* ('SABG', 8), greedy ('SBG', 9).

  2. আউটপুট অনুমান করুন: অংশ ৭-এর 8-puzzle কোডের h1 আর h2 একই রেখে নিচের লাইনগুলো চালালে কী প্রিন্ট হবে? চালানোর আগে লিখুন, তারপর সেই block-এর শেষে বসিয়ে মিলিয়ে দেখুন।
    s = (2, 0, 3, 1, 5, 6, 4, 7, 8)
    print(h1(s), h2(s))
    import heapq
    q = []
    for f, name in [(7, "A"), (5, "B"), (7, "C"), (6, "D")]:
        heapq.heappush(q, (f, name))
    print([heapq.heappop(q)[1] for _ in range(4)])
    5 5
    ['B', 'D', 'A', 'C']

    বোর্ডটি হলো 2 _ 3 / 1 5 6 / 4 7 8. ভুল ঘরে: 2, 1, 4, 7, 8, মোট ৫টি (3, 5, 6 ঠিক জায়গায়), তাই h1 = 5. প্রতিটি ভুল টাইল তার লক্ষ্য থেকে ঠিক এক ঘর দূরে (2 এক কলাম, 1 এক সারি, 4 এক সারি, 7 আর 8 এক কলাম করে), তাই h2-ও 5. এখানে h1 = h2, যা dominance ভাঙে না: h2 ≥ h1 মানে বড় বা সমান। দ্বিতীয় লাইনে heap সবচেয়ে ছোট f আগে দেয়: 5 (B), 6 (D), তারপর দুটি 7. সমান f-এ tuple-এর পরের অংশ, মানে নাম, তুলনা হয়, তাই "A" আগে, "C" পরে। এটিই tie-breaking; নামের বদলে তুলনা করা যায় না এমন বস্তু থাকলে এখানে TypeError আসত (লেখকের যন্ত্রে Python 3.14.4 দিয়ে মেলানো)।

  3. নকশা ও সমালোচনা: একটি AI সহকারী বাজারঘরের রাইডার-অ্যাপের জন্য নিচের A* লিখেছে, যেখানে রাস্তার খরচ মিনিটে আর coords km-এ। অন্তত পাঁচটি সমস্যা চিহ্নিত করে একটি ভালো নকশা বলুন।
    def astar(start, goal):
        frontier = [(0, start)]; seen = set()
        while frontier:
            f, n = heapq.heappop(frontier)
            seen.add(n)
            for m, minutes in roads[n]:
                if m == goal: return "found"
                if m not in seen:
                    h = 2 * math.dist(coords[m], coords[goal])   # "নিরাপদ" অনুমান
                    heapq.heappush(frontier, (minutes + h, m))
    • লক্ষ্য দেখামাত্র থামা: প্রতিবেশী লক্ষ্য হলেই ফেরত দেয়, frontier থেকে তোলার আগে; সস্তা পথ বাকি থাকতে পারে।
    • g নেই: priority-তে শুধু শেষ রাস্তার মিনিট + h, শুরু থেকে মোট খরচ জমা হয় না; এটি আসলে A* নয়।
    • একক মেলেনি আর বাড়িয়ে বলা: km-কে মিনিটের সঙ্গে যোগ, তার ওপর 2 দিয়ে গুণ; “নিরাপদ” নয়, বরং inadmissible হওয়ার সম্ভাবনা বেশি। km থেকে মিনিটে যেতে সর্বোচ্চ সম্ভব গতি দিয়ে ভাগ করতে হবে, যাতে অনুমান কখনো বেশি না হয়।
    • closed set-এর ভুল: একই node বারবার তোলা হলে প্রতিবার expand হয় (তোলার সময় seen পরীক্ষা নেই); আবার ভালো g এলে আপডেটের ব্যবস্থাও নেই।
    • পথ আর খরচ ফেরত দেয় না: শুধু "found"; parent রাখা আর খরচ ফেরত দেওয়া দরকার, পথ না থাকলে স্পষ্ট None.
    • যাচাই নেই: UCS-এর সঙ্গে খরচ মেলানো বা node গোনার কোনো ব্যবস্থা নেই।

    ভালো নকশা: অংশ ৩-এর best_first-এর মতো (f, g, n, path) heap, best dictionary-তে সেরা g, লক্ষ্য-পরীক্ষা তোলার সময়, h = সরলরেখার km ÷ সর্বোচ্চ গতি (মিনিটে), ফেরত (path, cost), আর একটি পরীক্ষা যা h = 0 দিয়ে একই খরচ মেলায়।

  4. কোড লিখে heuristic বদলান: অংশ ৬-এর grid-এ এবার কোনাকুনিও চলা যায়: আট দিকে, সোজা ধাপে খরচ 1, কোনাকুনি √2. (ক) Manhattan কেন আর admissible নয়, একটি উদাহরণ দিন। (খ) একটি admissible heuristic লিখুন। (গ) ৩০ × ৩০ grid-এ (২৫% দেয়াল, seed 3) UCS, আপনার heuristic আর Manhattan চালিয়ে খরচ আর expand-সংখ্যা তুলনা করুন।

    (ক) ফাঁকা grid-এ (0, 0) থেকে (3, 3): তিনটি কোনাকুনি ধাপে খরচ 3√2 ≈ 4.24, কিন্তু Manhattan বলে 6, বেশি। (খ) Octile distance: dx, dy পার্থক্য হলে h = max(dx, dy) + (√2 − 1) · min(dx, dy), মানে যতটা পারা যায় কোনাকুনি, বাকিটা সোজা; দেয়াল না থাকলে এটিই আসল খরচ, তাই admissible.

    import heapq, math
    import numpy as np
    
    def astar8(wall, S, T, h):
        H, W = wall.shape
        g, closed, fr = {S: 0.0}, set(), [(h(S, T), 0.0, S)]
        while fr:
            f, gs, p = heapq.heappop(fr)
            if p in closed: continue
            closed.add(p)
            if p == T: return round(gs, 3), len(closed)
            for dr in (-1, 0, 1):
                for dc in (-1, 0, 1):
                    q = (p[0] + dr, p[1] + dc)
                    if (dr or dc) and 0 <= q[0] < H and 0 <= q[1] < W and not wall[q]:
                        c = math.sqrt(2) if dr and dc else 1.0
                        if gs + c < g.get(q, math.inf):
                            g[q] = gs + c; heapq.heappush(fr, (gs + c + h(q, T), gs + c, q))
    
    manhattan = lambda p, T: abs(p[0] - T[0]) + abs(p[1] - T[1])
    def octile(p, T):
        dx, dy = abs(p[0] - T[0]), abs(p[1] - T[1])
        return max(dx, dy) + (math.sqrt(2) - 1) * min(dx, dy)
    zero = lambda p, T: 0
    
    rng = np.random.default_rng(3)
    wall = rng.random((30, 30)) < 0.25
    S, T = (0, 0), (29, 29); wall[S] = wall[T] = False
    for name, h in [("UCS", zero), ("octile", octile), ("manhattan", manhattan)]:
        print(name, astar8(wall, S, T, h))

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

    UCS (43.941, 669)
    octile (43.941, 155)
    manhattan (44.77, 47)

    Octile UCS-এর একই সেরা খরচ 43.941 পায়, মাত্র ১৫৫টি ঘর খুলে (UCS ৬৬৯)। Manhattan আরও কম, ৪৭টি ঘর খোলে, কিন্তু পথ 44.77, সেরার চেয়ে লম্বা: বাড়িয়ে বলা heuristic দ্রুত, কিন্তু optimality হারায়, অংশ ৪-এর মতোই। মনে রাখুন, closed set রাখা হয়েছে; octile consistent বলে এটি নিরাপদ।

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

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

ক্রমA* graph search-এর একটি পূর্ণ চক্রের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 🚩 শুরুর node-কে frontier-এ রাখা, g = 0, f = h(শুরু)
  2. 📤 frontier থেকে সবচেয়ে ছোট f-এর node তোলা
  3. 🎯 তোলা node লক্ষ্য হলে পথ আর খরচ ফেরত দিয়ে থামা
  4. 🔍 না হলে প্রতিটি প্রতিবেশীর জন্য g + ধাপের খরচ হিসাব
  5. 📥 নতুন g আগের চেয়ে ভালো হলে f = g + h দিয়ে frontier-এ ঢোকানো, তারপর আবার তোলার ধাপে ফেরা

একটি ম্যাপে node n-এর g(n) = 7 km আর সরলরেখার heuristic h(n) = 4.5 km. A*, greedy best-first আর UCS এই node-কে কোন priority দেবে?

  • A* 11.5, greedy 7, UCS 4.5
  • A* 11.5, greedy 4.5, UCS 7
  • A* 31.5, greedy 4.5, UCS 7
  • তিনটিই 11.5

একটি heuristic h-কে admissible বলা হয় কখন? (h*(n) মানে n থেকে লক্ষ্যের আসল সেরা খরচ)

  • সব node-এ h(n) ≥ h*(n)
  • শুধু যখন সব node-এ h(n) = h*(n)
  • প্রতিটি ধাপে h(n) ≤ c(n, n′) + h(n′)
  • সব node-এ h(n) ≤ h*(n)

8-puzzle-এর অবস্থা: উপরের সারি 1 2 3, মাঝে 4 ফাঁকা 6, নিচে 7 5 8. লক্ষ্য 1 2 3 / 4 5 6 / 7 8 ফাঁকা। ভুল ঘরের টাইলের সংখ্যা h1 আর Manhattan যোগ h2 কত?

  • h1 = 2, h2 = 2
  • h1 = 3, h2 = 3
  • h1 = 2, h2 = 3
  • h1 = 1, h2 = 1

একটি search 4 গভীরতায় সমাধান পেয়েছে, আর effective branching factor-এর সংজ্ঞা N + 1 = 1 + b* + (b*)2 + (b*)3 + (b*)4. b* = 2 হলে N কত?

  • 16
  • 31
  • 30
  • 8

একটি admissible heuristic h দিয়ে weighted A* চালানো হলো, f = g + 3·h. সেরা পথের খরচ 126 হলে পাওয়া পথের খরচ সম্পর্কে কী নিশ্চিতভাবে বলা যায়?

  • এটি অবশ্যই 126
  • 126 থেকে 378-এর মধ্যে
  • 126-এর চেয়ে কম হতে পারে
  • কোনো সীমা নেই, যেকোনো খরচ হতে পারে

A* graph search (closed node আবার খোলা হয় না) একটি admissible কিন্তু inconsistent heuristic দিয়ে চালানো হলো। কোন ঝুঁকিটি আছে?

  • ভালো পথে পরে পৌঁছানো একটি closed node বাদ পড়ায় পাওয়া পথ সেরা না-ও হতে পারে
  • অ্যালগরিদম কখনো থামবে না
  • কোনো ঝুঁকি নেই, admissible হলেই যথেষ্ট
  • heap উল্টো ক্রমে node দেবে

8-puzzle-এ “টাইল শুধু ফাঁকা ঘরে যেতে পারে” শর্তটি তুলে দিলে (পাশের যেকোনো ঘরে যেতে পারে) শিথিল সমস্যার সেরা খরচ কোন heuristic দেয়, আর তা admissible কেন?

  • ভুল ঘরের টাইলের সংখ্যা, কারণ এটি সবসময় শূন্য
  • Manhattan যোগ, কারণ এটি সবসময় আসল খরচের চেয়ে বেশি
  • Manhattan যোগ (h2), কারণ শিথিল সমস্যার সেরা খরচ মূলের সেরা খরচের বেশি হতে পারে না
  • একটি pattern database, কারণ এটি টেবিল দেখে

শব্দকোষ

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

মনে রাখুন

  1. h(n) বাকি খরচের সস্তা অনুমান: ম্যাপে সরলরেখা, grid-এ Manhattan, লক্ষ্যে 0. g চলা পথের আসল খরচ, h* অজানা আসল বাকি খরচ।
  2. Greedy f = h, UCS f = g, A* f = g + h: একই ম্যাপে greedy ৪টি node-এ 15 km (ভুল), UCS ১০টিতে 14, A* ৬টিতে 14. লক্ষ্য-পরীক্ষা হয় frontier থেকে তোলার সময়।
  3. Admissible (h ≤ h*) হলে A* সেরা পথ দেয়; consistent (h(n) ≤ c + h(n′)) হলে graph search-এ কোনো node আবার খুলতে হয় না। এক জায়গায় বাড়িয়ে বললেই সেরা পথ হারাতে পারে।
  4. বড় admissible heuristic কম node খোলায়: 8-puzzle-এ h2 ≥ h1; গভীরতা ২৪-এ গড়ে UCS ১,২২,৫৪৬, h1 ১৮,৪৭৯, h2 ১,৮৩০ (একটি run). b* যত 1-এর কাছে, তত ভালো। নিয়ম শিথিল করে heuristic বানানো যায়।
  5. Weighted A* (g + w·h) দ্রুত, খরচ সর্বোচ্চ w গুণ; A*-এর বড় দুর্বলতা memory, যার একটি সমাধান IDA*. গেম ও রুট খোঁজায় A*-এর নানা রূপ চলে, তবে কোন পণ্যে কী, তা না জেনে দাবি নয়।

পরের পর্বে: এবার প্রতিপক্ষ আছে। টিক-ট্যাক-টো আর ছোট খেলায় আপনার প্রতিটি চালের জবাবে প্রতিপক্ষও সেরা চাল খোঁজে: minimax, alpha-beta pruning আর গভীরতা-সীমিত search-এ মূল্যায়নের অনুমান।

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

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