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

অন্ধ search — BFS, DFS আর uniform cost

Uninformed Search — Breadth-First, Depth-First, Depth-Limited, Iterative Deepening, Uniform-Cost and Bidirectional Search Compared

Taught in Bangla, this lesson compares BFS, DFS, iterative deepening and uniform-cost search on one made-up delivery map and a grid maze, with step-by-step frontiers and in-page Python runs. It explains when to test for the goal, why BFS hits a memory wall, why iterative deepening costs little extra, and how UCS relates to Dijkstra.

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

  • uninformed search কী, আর চারটি মাপকাঠি: completeness, optimality, time আর space; b, d আর m-এর মানে
  • BFS-এর frontier ধাপে ধাপে, node তৈরির সময় বনাম expand-এর সময় goal test, আর BFS-এর memory-র দেয়ালের হিসাব
  • DFS কেন কম memory নেয় কিন্তু হারিয়ে যেতে পারে; depth-limited search আর iterative deepening, বাড়তি কাজের হিসাবসহ
  • Uniform-cost search, heapq, Dijkstra-র সঙ্গে সম্পর্ক, আর কম hop বনাম কম km-এর উদাহরণ
  • চারটি পদ্ধতি একই মানচিত্র আর একই maze-এ চালিয়ে তুলনা, bidirectional search-এর ধারণা, আর কোন পরিস্থিতিতে কোনটি

পর্ব ৩৯-এ (সমস্যাকে search হিসেবে দেখা) একটি সমস্যাকে state, action, transition, goal test আর path cost দিয়ে লেখা হয়েছে, আর একটি সাধারণ search loop দেখা হয়েছে: frontier থেকে একটি node বের করুন, goal কি না দেখুন (সেখানে goal test হয় বের করার সময়), না হলে তার সন্তানদের frontier-এ রাখুন। এই পর্বের প্রশ্ন: frontier থেকে কোন node আগে বের করবেন? উত্তর বদলালেই পদ্ধতি বদলায়, আর তার সঙ্গে বদলায় পাওয়া পথের মান, সময় আর memory. Graph, queue, stack আর heap কীভাবে বানানো হয়, তা DSA কোর্সের বিষয় (BFS, DFS, Dijkstra); এখানে AI-এর দৃষ্টিতে কৌশলগুলোর তুলনা।

দুটি চলমান উদাহরণ থাকবে। প্রথমটি বাজারঘরের (এই কোর্সের কাল্পনিক অনলাইন দোকান) এক ডেলিভারি রাইডারের বানানো রাস্তার মানচিত্র: ঢাকার ১০টি এলাকা, মিরপুরের গুদাম থেকে মতিঝিলে পার্সেল। এলাকার নাম আসল, কিন্তু রাস্তা আর km সব বানানো, বাস্তবের দূরত্ব নয়। এটি পর্ব ৩৯-এর ৯ এলাকার মানচিত্র থেকে আলাদা, একটু বড় একটি বানানো মানচিত্র, তাই সংখ্যাগুলোও আলাদা। দ্বিতীয়টি ৭ সারি × ১০ কলামের একটি grid maze, যেখানে প্রতিটি ধাপের খরচ সমান।

১ · “অন্ধ” search মানে কী — goal কত দূরে, জানা নেই

ধরুন একজন রিকশাচালক প্রথমবার একটি অচেনা মফস্বল শহরে এসেছেন, হাতে মানচিত্র বা ফোন নেই। তিনি জানেন প্রতিটি মোড়ে কোন কোন রাস্তা বের হয়েছে, আর গন্তব্যে পৌঁছালে সাইনবোর্ড দেখে চিনবেন; কিন্তু কোন রাস্তাটি গন্তব্যের দিকে, তা বোঝার উপায় নেই। তাঁর হাতে শুধু কৌশল বাছার স্বাধীনতা: কাছের সব গলি আগে, নাকি একটি রাস্তা ধরে যতদূর যায়, নাকি সবসময় এখন পর্যন্ত সবচেয়ে কম ভাড়ার পথটি আগে।

এটিই uninformed search, বাংলায় “অন্ধ” search: পদ্ধতিটি সমস্যার সংজ্ঞার বাইরে কিছু জানে না। সবচেয়ে বড় কথা, কোনো state থেকে goal কত দূরে, তার কোনো অনুমান (estimate) এটি ব্যবহার করে না। goal test শুধু “পৌঁছেছি কি না” বলে, “কতটা কাছে” বলে না। সেই অনুমান যোগ করলে পদ্ধতিকে বলে informed বা heuristic search, যা পর্ব ৪১-এর (Heuristic আর A*) বিষয়।

মূল ধারণা

সব অন্ধ search একই loop চালায়; পার্থক্য শুধু frontier-এর নিয়মে। BFS সবচেয়ে পুরোনো node বের করে (FIFO queue), DFS সবচেয়ে নতুনটি (LIFO stack), আর uniform-cost search সবচেয়ে কম মোট খরচেরটি (priority queue). একটি node বের করে তার সন্তান তৈরি করাকে বলে expand করা।

চারটি মাপকাঠি আর তিনটি অক্ষর

পদ্ধতিগুলো তুলনা করা হয় চারটি প্রশ্নে। এক, completeness: সমাধান থাকলে পদ্ধতিটি কি নিশ্চিতভাবে তা পাবে? দুই, optimality: পাওয়া সমাধানটি কি সবচেয়ে কম path cost-এর? তিন, time: কতগুলো node তৈরি বা expand হয়? চার, space: একসঙ্গে সবচেয়ে বেশি কতগুলো node memory-তে রাখতে হয়? সময় আর memory লেখা হয় Big-O দিয়ে (বিস্তারিত DSA-র Big-O পর্বে), তিনটি অক্ষরে:

  • b = branching factor: একটি state থেকে সর্বোচ্চ কতগুলো সন্তান। মানচিত্রে মিরপুরের ৪টি রাস্তা, maze-এ একটি ঘরের সর্বোচ্চ ৪টি প্রতিবেশী।
  • d = সবচেয়ে অগভীর goal-এর গভীরতা, মানে শুরু থেকে সবচেয়ে কম কয়টি ধাপে goal-এ পৌঁছানো যায়।
  • m = state space-এ যেকোনো পথের সর্বোচ্চ দৈর্ঘ্য; চক্র থাকলে আর কোনো বাধা না থাকলে এটি অসীম হতে পারে।

গভীরতা d পর্যন্ত একটি পূর্ণ গাছে node-এর সংখ্যা 1 + b + b2 + … + bd, যা প্রায় bd. তাই bd এই পর্বে বারবার আসবে। নিচের ছবিতে একই ছোট গাছে BFS আর DFS কোন ক্রমে node খোলে, দেখুন।

BFS স্তর ধরে, DFS শাখা ধরে 123 4567 125 3467 BFS: স্তর ধরে DFS: শাখা ধরে গভীরতা ১ শেষ, তারপর ২ এক শাখার তলা পর্যন্ত, তারপর ফেরা
সংখ্যাগুলো expand-এর ক্রম। BFS আগে পুরো একটি স্তর (হলুদ, তারপর নীল) শেষ করে; DFS বাঁ শাখার তলা পর্যন্ত (১, ২, ৩, ৪) শেষ করে ফিরে আসে, তারপর ডান শাখা (৫, ৬, ৭)।

AI-তে search-কে কেন্দ্রীয় ধারণা বানানোর কৃতিত্ব সাধারণত Allen Newell আর Herbert Simon-কে দেওয়া হয়। ১৯৫০-এর দশকের শেষে তাঁদের General Problem Solver (GPS) সমস্যাকে অবস্থা থেকে অবস্থায় যাওয়ার খোঁজ হিসেবে দেখত, আর ১৯৭৫ সালের Turing পুরস্কারের বক্তৃতায় (প্রকাশ ১৯৭৬) তাঁরা “physical symbol system” ধারণার পাশাপাশি বুদ্ধিমান আচরণকে বহুলাংশে heuristic search হিসেবে ব্যাখ্যা করেন। এই পর্বের অন্ধ কৌশলগুলো সেই কাঠামোর সবচেয়ে মৌলিক স্তর। আজকের পাঠ্যবইয়ের প্রচলিত উপস্থাপন, যেমন Russell ও Norvig-এর “Artificial Intelligence: A Modern Approach” (৪র্থ সংস্করণ, ২০২০), এগুলোকে ঠিক এই চার মাপকাঠিতে তুলনা করে; এখানে সেই কাঠামো, কিন্তু নিজের উদাহরণে।

২ · Breadth-first search — FIFO frontier, ঢেউয়ের মতো ছড়ানো

পুকুরে ঢিল ছুড়লে ঢেউ চারদিকে সমানভাবে ছড়ায়: আগে কাছের বৃত্ত, তারপর দূরের। Breadth-first search (BFS) ঠিক তাই করে। শুরুর node থেকে ১ ধাপ দূরের সবগুলো আগে, তারপর ২ ধাপ দূরের সবগুলো, এভাবে। frontier একটি FIFO queue: নতুন node লাইনের পেছনে দাঁড়ায়, আর সামনে থেকে বের হয়, যেমন ব্যাংকের কাউন্টারে টোকেন ধরে ডাকা হয়। Python-এ এর জন্য collections.deque, যার popleft() সামনে থেকে O(1) সময়ে বের করে (queue কীভাবে বানানো হয়, দেখুন DSA-র queue আর deque).

পাশাপাশি একটি explored set বা reached set রাখা হয়, যাতে একই এলাকা বারবার frontier-এ না ঢোকে। পর্ব ৩৯-এর ভাষায় এটি graph search; তা না রাখলে মিরপুর → গাবতলী → মিরপুর → … করে একই রাস্তায় ঘুরপাক খেতে হতো। নিচে মিরপুর থেকে মতিঝিল পর্যন্ত BFS ধাপে ধাপে দেখুন। প্রতিবেশীদের ক্রম dhaka_map.txt ফাইলে যেভাবে লেখা, সেভাবেই।

বানানো ঢাকার ডেলিভারি মানচিত্রে BFS: মিরপুর থেকে মতিঝিল। frontier ঢেউয়ের মতো ছড়ায়; মহাখালী expand করার সময় মতিঝিল তৈরি হয়, পথ মিরপুর-মহাখালী-মতিঝিল, ২২ km 4 7 8 11 7 4 3 3 3 14 13 9 6 4 10 উত্তরা মিরপুর গাবতলী মহাখালী গুলশান ফার্মগেট তেজগাঁও বাড্ডা ধানমন্ডি মতিঝিল frontier explored এখন expand
BFS-এর অবস্থা (queue = FIFO)
expand
নতুন তৈরি
frontier (সামনে → পেছনে)
explored
পথ
  1. শুরু। frontier-এ শুধু মিরপুর, আর reached set-এও মিরপুর। এখনো কিছু expand হয়নি। goal মতিঝিল।
  2. মিরপুর expand. queue-এর সামনে থেকে মিরপুর বের হলো। চার প্রতিবেশী গাবতলী, ফার্মগেট, মহাখালী আর উত্তরা তৈরি হয়ে queue-এর পেছনে দাঁড়াল। এরা সবাই গভীরতা ১।
  3. গাবতলী expand. queue-এর সামনে গাবতলী। তার প্রতিবেশী মিরপুর আগেই দেখা, তাই বাদ; ধানমন্ডি নতুন, গভীরতা ২, লাইনের শেষে।
  4. ফার্মগেট expand. ধানমন্ডি আগেই reached, তাই আবার ঢোকে না; তেজগাঁও নতুন। লক্ষ করুন, গভীরতা ১-এর সবাই শেষ না হওয়া পর্যন্ত গভীরতা ২-এর কেউ expand হয় না।
  5. মহাখালী expand, goal তৈরি। তেজগাঁও আগেই reached; গুলশান নতুন; তারপর মতিঝিল তৈরি হতেই goal test সফল (বেগুনি দাগ)। BFS এখানেই থামে, মতিঝিলকে queue-তে রাখার দরকারই হয় না। মোট ৪টি node expand হলো।
  6. পথ। parent ধরে উল্টো দিকে: মতিঝিল ← মহাখালী ← মিরপুর। ২টি hop, সবচেয়ে কম ধাপ। কিন্তু km যোগ করলে ৮ + ১৪ = ২২। ফার্মগেট-তেজগাঁও হয়ে ১৬ km-এর একটি পথ আছে, BFS তা দেখেও না, কারণ সে শুধু ধাপ গোনে।

Goal test: তৈরির সময় না expand-এর সময়?

উপরের ধাপ ৫-এ BFS মতিঝিল তৈরি হওয়ার মুহূর্তেই goal test করেছে, queue থেকে বের হওয়া পর্যন্ত অপেক্ষা করেনি। একে বলে early goal test. BFS-এ এটি নিরাপদ: BFS গভীরতা d-এর সব node, গভীরতা d + 1-এর কোনো node-এর আগে তৈরি করে, তাই প্রথম তৈরি হওয়া goal-ই সবচেয়ে অগভীর goal.

কিন্তু পার্থক্যটি খরচে বিশাল। ধরুন b = 10, goal গভীরতা ৪-এ আর দুর্ভাগ্যবশত সেই স্তরের সবচেয়ে শেষ node. তৈরির সময় test করলে সর্বোচ্চ 1 + 10 + 100 + 1,000 + 10,000 = 11,111টি node তৈরি হয়। expand-এর সময় test করলে goal-এর আগের গভীরতা ৪-এর বাকি 9,999টি node-ও expand হবে, প্রতিটি ১০টি করে সন্তান বানাবে: আরও 99,990টি, মোট 1,11,101. প্রায় দশ গুণ, মানে পুরো একটি বাড়তি স্তর। এই পার্থক্য পরের অংশের memory-র হিসাবেও আসবে। তবে uniform-cost search-এ early goal test ভুল ফল দেয়, অংশ ৬-এ দেখবেন।

BFS-এর চার মাপকাঠি

  • Complete: হ্যাঁ, যদি b সসীম হয়। goal কোনো সসীম গভীরতা d-তে থাকলে BFS সেই স্তরে একদিন পৌঁছাবেই, এমনকি state space অসীম হলেও।
  • Optimal: শুধু তখনই, যখন প্রতিটি ধাপের খরচ সমান (বা গভীরতার সঙ্গে খরচ কখনো কমে না)। BFS সবচেয়ে কম ধাপের পথ দেয়; ধাপের খরচ আলাদা হলে তা সবচেয়ে সস্তা নাও হতে পারে, ঠিক যেমন ২২ km.
  • Time: O(bd), কারণ সবচেয়ে খারাপ অবস্থায় গভীরতা d পর্যন্ত প্রায় সব node তৈরি হয়।
  • Space: O(bd). পুরো শেষ স্তরটি একসঙ্গে frontier-এ থাকে, আর reached set-এ আগের সবাই। এটিই BFS-এর আসল দুর্বলতা।

৩ · BFS-এর memory-র দেয়াল

“O(bd)” শুনতে বিমূর্ত। সংখ্যায় দেখলে বোঝা যায় কেন বাস্তবে BFS প্রায়ই সময়ের আগে memory-তে আটকে যায়। নিচের টেবিলে b = 10, আর গভীরতা d পর্যন্ত মোট node 1 + 10 + … + 10d (early goal test-এ সবচেয়ে খারাপ অবস্থা)।

ধরে নেওয়া মান (assumption), মাপা নয়: প্রতিটি node memory-তে 1,000 byte নেয় (state, parent, খরচ আর dict-এর বাড়তি খরচ মিলিয়ে একটি গোল সংখ্যা), আর কম্পিউটার সেকেন্ডে ১০ লাখ node তৈরি করে। আসল মান ভাষা, ডেটা-কাঠামো আর যন্ত্রের ওপর অনেক বদলায়; টেবিলের উদ্দেশ্য বৃদ্ধির হার দেখানো। একক দশমিক: 1 KB = 1,000 byte, 1 GB = 109 byte.

গভীরতা dnode (b = 10)memory (1,000 byte/node)সময় (১০ লাখ node/সেকেন্ড)
2111111 KB0.1 মিলিসেকেন্ড
411,11111.1 MB0.01 সেকেন্ড
6প্রায় 1.11 × 1061.11 GB1.1 সেকেন্ড
8প্রায় 1.11 × 108111 GBপ্রায় ১.৯ মিনিট
10প্রায় 1.11 × 101011.1 TBপ্রায় ৩.১ ঘণ্টা
12প্রায় 1.11 × 10121.11 PBপ্রায় ১২.৯ দিন

টেবিলটি দুটি কথা বলে। প্রথমত, প্রতি দুই স্তরে খরচ একশো গুণ বাড়ে; যন্ত্র দশ গুণ দ্রুত হলে আপনি মাত্র এক স্তর বেশি যেতে পারবেন। দ্বিতীয়ত, গভীরতা ৮-এ সময় মাত্র দুই মিনিট, কিন্তু memory 111 GB, যা একটি সাধারণ ল্যাপটপের RAM-এর অনেক গুণ। গভীরতা ১০-এ তিন ঘণ্টা অপেক্ষা করা যায়, কিন্তু 11 TB RAM কোথায় পাবেন? BFS-এ memory সময়ের অনেক আগে ফুরায়। সংখ্যার এমন বিস্ফোরণকে বলে combinatorial explosion. এই দেয়াল পার হওয়ার দুটি পথ এই পর্বেই: DFS-ঘরানার কম memory-র কৌশল (অংশ ৪–৫), আর পরের পর্বে goal-এর দিকে অনুমান দিয়ে কম node খোলা।

৪ · Depth-first search — LIFO frontier, গভীরে ডুব

গোলকধাঁধায় হারিয়ে গেলে অনেকে একটি পুরোনো কৌশল নেন: একটি পথ ধরে যতদূর যায় যান, আটকে গেলে শেষ মোড়ে ফিরে অন্য পথ ধরুন। Depth-first search (DFS) এটিই। frontier এখানে একটি LIFO stack, থালার স্তূপের মতো: সবচেয়ে শেষে রাখা থালাটি আগে তোলা হয়। তাই সবচেয়ে নতুন তৈরি node, মানে সবচেয়ে গভীরটি, আগে expand হয়। Python-এ list-এর append আর pop() দিয়েই stack (DSA-র stack), অথবা recursion দিয়ে।

নিচের stepper-এ একই মানচিত্রে DFS. কোডে প্রতিবেশীদের উল্টো ক্রমে push করা হয়, যাতে ফাইলের প্রথম প্রতিবেশীটি stack-এর ওপরে থাকে আর আগে expand হয়। একটি node stack-এ একাধিকবার থাকতে পারে; বের হওয়ার সময় আগেই explored হলে সেটি বাদ যায়। goal test হয় expand-এর সময়।

একই মানচিত্রে DFS: stack-এর ওপরের node আগে, তাই গাবতলী, ধানমন্ডি, ফার্মগেট ধরে গভীরে যায়; ১০টি node expand করে ৮ hop আর ৩৮ km-এর পথ পায় 4 7 8 11 7 4 3 3 3 14 13 9 6 4 10 উত্তরা মিরপুর গাবতলী মহাখালী গুলশান ফার্মগেট তেজগাঁও বাড্ডা ধানমন্ডি মতিঝিল frontier explored এখন expand
DFS-এর অবস্থা (stack = LIFO)
expand
stack (ওপর → নিচ)
explored
পথের গভীরতা
  1. শুরু। stack-এ শুধু মিরপুর। explored set খালি।
  2. মিরপুর expand. চার প্রতিবেশী উল্টো ক্রমে push হলো, তাই stack-এর ওপরে গাবতলী, নিচে উত্তরা। পথের গভীরতা ০।
  3. গাবতলী expand. ওপর থেকে গাবতলী বের হলো, তার নতুন প্রতিবেশী ধানমন্ডি ওপরে বসল। BFS হলে এখন ফার্মগেট আসত; DFS আরও গভীরে যাচ্ছে (গভীরতা ১)।
  4. ধানমন্ডি expand. প্রতিবেশী ফার্মগেট আর মতিঝিল push হলো। মতিঝিল এখন stack-এ, কিন্তু ওপরে নয়; DFS goal test করে বের হওয়ার সময়, তাই আগে ওপরের ফার্মগেট। লক্ষ করুন, ফার্মগেট stack-এ দুবার।
  5. ফার্মগেট expand. এটি ধানমন্ডির push করা ফার্মগেট (গভীরতা ৩)। মিরপুর আর ধানমন্ডি explored, তাই শুধু তেজগাঁও push হলো।
  6. তেজগাঁও expand. মহাখালী আর মতিঝিল push; ওপরে মহাখালী। মতিঝিল এখন stack-এ দুবার, তবু DFS তার দিকে যাচ্ছে না।
  7. মহাখালী expand. গুলশান আর মতিঝিল push; ওপরে গুলশান। পথের গভীরতা ৫, অথচ মতিঝিল মিরপুর থেকে মাত্র ২ ধাপ দূরে।
  8. গুলশান expand. উত্তরা আর বাড্ডা push হলো; ওপরে উত্তরা। stack-এ এখন ৮টি entry, যার তিনটি মতিঝিল।
  9. উত্তরা expand. উত্তরার দুই প্রতিবেশী মিরপুর আর গুলশান দুটিই explored: কানাগলি। DFS ফিরে আসে (backtrack), stack-এর পরের entry বাড্ডা।
  10. বাড্ডা expand. শুধু মতিঝিল নতুন, push হলো ওপরে। stack-এর ওপরের চারটিই এখন মতিঝিল।
  11. মতিঝিল expand, goal. ১০টি node expand হলো, মানে মানচিত্রের সবগুলোই। পথ: মিরপুর → গাবতলী → ধানমন্ডি → ফার্মগেট → তেজগাঁও → মহাখালী → গুলশান → বাড্ডা → মতিঝিল, ৮ hop, ৩৮ km. একটি পথ পাওয়া গেছে, কিন্তু খুব খারাপ পথ।

DFS কোথায় হারিয়ে যায়

উপরের DFS explored set রেখেছিল বলে শেষ পর্যন্ত পৌঁছেছে। এবার দুটি বিপদ দেখুন।

  • চক্র: explored set বা অন্তত নিজের পথের ওপর কোনো যাচাই ছাড়া (tree search) DFS মিরপুর → গাবতলী → মিরপুর → গাবতলী … করে চিরকাল ঘুরবে, কারণ সবসময় সবচেয়ে নতুন node-টি আগে, আর প্রথম প্রতিবেশী আবার মিরপুর। প্রতিটি রাস্তা দুই দিকে চলে বলে এই মানচিত্রে চক্র সর্বত্র।
  • অসীম state space: ধরুন state হলো “একটি সংখ্যা”, action “১ যোগ” বা “২ দিয়ে গুণ”। DFS “১ যোগ” শাখা ধরে অসীম গভীরে নামতে থাকবে, পাশের শাখায় goal থাকলেও। BFS এখানে পায়, কারণ সে স্তর ধরে চলে।

তাই DFS complete নয়, অসীম বা চক্রযুক্ত space-এ কোনো যাচাই ছাড়া। সসীম space-এ graph search হিসেবে (explored set রেখে) সে complete, কিন্তু তখন explored set-এ সব state রাখতে হয়, ফলে memory-র সুবিধা হারায়। Optimal-ও নয়, উপরে ৩৮ km দেখলেন। সময় O(bm), যেখানে m সর্বোচ্চ গভীরতা, যা d-এর চেয়ে অনেক বড় হতে পারে।

তাহলে DFS কেন? memory. শুধু নিজের পথের ওপর চক্র-যাচাই করে (পুরো explored set না রেখে) চালালে DFS-কে রাখতে হয় শুধু বর্তমান পথটি আর পথের প্রতিটি node-এর এখনো না-দেখা ভাইবোন: সর্বোচ্চ প্রায় b × m টি node, মানে space O(bm). b = 10 আর m = 12 হলে মাত্র প্রায় ১২০টি node, BFS-এর 1.11 PB-এর পাশে প্রায় শূন্য। Recursion দিয়ে একটি একটি করে সন্তান তৈরি করলে (backtracking, DSA-র backtracking) এটি O(m)-এ নামে।

এবার grid maze-এ দুটি পদ্ধতি একসঙ্গে চালান। state হলো একটি ঘর (সারি, কলাম); প্রতিবেশীর ক্রম ডানে, নিচে, বাঁয়ে, ওপরে; প্রতিটি ধাপের খরচ 1. শুরু ওপরের বাঁ কোণে (S), goal নিচের ডান কোণে (G). ফলে * দিয়ে পাওয়া পথ আঁকা হবে। এই ব্লক আর পরের তিনটি ব্লক একই namespace ভাগ করে (data-shared), তাই এখানে লেখা bfs আর dfs পরে আবার ব্যবহার হবে; পরের কোনো ব্লক আগে চালালে এটি নিজেই আগে চলে যায়। প্রথমবার ব্রাউজারে Python নামতে কয়েক সেকেন্ড লাগতে পারে।

from collections import deque
MAZE = open("maze.txt").read().split()                 # '#' = দেয়াল
R, C = len(MAZE), len(MAZE[0]); START, GOAL = (0, 0), (R - 1, C - 1)
def grid_nbrs(s):                    # ক্রম: ডানে, নিচে, বাঁয়ে, ওপরে; প্রতি ধাপ 1
    r, c = s
    return [((a, b), 1) for a, b in [(r, c+1), (r+1, c), (r, c-1), (r-1, c)]
            if 0 <= a < R and 0 <= b < C and MAZE[a][b] != "#"]
def walk(parent, s):                 # parent ধরে উল্টো দিকে হেঁটে পথ
    return [] if s is None else walk(parent, parent[s]) + [s]
def bfs(start, goal, nbrs):
    frontier, parent, order, maxf = deque([start]), {start: None}, [], 1
    while frontier:
        s = frontier.popleft(); order.append(s)            # FIFO: সবচেয়ে পুরোনো
        for t, _ in nbrs(s):
            if t not in parent:                            # আগে দেখা হয়নি
                parent[t] = s
                if t == goal: return walk(parent, t), order, maxf  # তৈরির সময় test
                frontier.append(t)
        maxf = max(maxf, len(frontier))
def dfs(start, goal, nbrs):
    frontier, parent, order, maxf = [(start, None)], {}, [], 1
    while frontier:
        s, par = frontier.pop()                            # LIFO: সবচেয়ে নতুন
        if s in parent: continue                           # আগেই expand হয়েছে
        parent[s] = par; order.append(s)
        if s == goal: return walk(parent, s), order, maxf  # expand-এর সময় test
        frontier += [(t, s) for t, _ in reversed(nbrs(s)) if t not in parent]
        maxf = max(maxf, len(frontier))

for name, f in [("BFS", bfs), ("DFS", dfs)]:
    path, order, maxf = f(START, GOAL, grid_nbrs)
    print(f"{name}: steps {len(path)-1}, expanded {len(order)}, max frontier {maxf}")
    print("  first 6 expanded:", order[:6])
    for r in range(R): print("  " + "".join("*" if (r, c) in path else MAZE[r][c] for c in range(C)))

আসল আউটপুট:

BFS: steps 19, expanded 44, max frontier 4
  first 6 expanded: [(0, 0), (0, 1), (1, 0), (0, 2), (2, 0), (1, 2)]
  ***#******
  .#*#*####*
  .#***#...*
  .###.#.##*
  ...#.#..#*
  .#.#.##.#*
  .#......#*
DFS: steps 25, expanded 26, max frontier 5
  first 6 expanded: [(0, 0), (0, 1), (0, 2), (1, 2), (2, 2), (2, 3)]
  ***#......
  .#*#.####.
  .#***#****
  .###*#*##*
  ...#*#**#*
  .#.#*##*#*
  .#..****#*

দুটি পদ্ধতিই পথ পেয়েছে, কিন্তু ভিন্ন। BFS-এর পথ ১৯ ধাপের; প্রতিটি ধাপের খরচ সমান বলে এটিই সবচেয়ে ছোট পথ। DFS-এর পথ ২৫ ধাপ: প্রথম মোড় থেকে সে নিচে নেমে মাঝখানের সরু পথ ধরে এঁকেবেঁকে গেছে। এখানে DFS কম ঘর expand করেছে (২৬, BFS ৪৪), কারণ তার প্রথম শাখাটি ভাগ্যক্রমে goal-এ পৌঁছেছে; এটি সাধারণ নিয়ম নয়, প্রতিবেশীর ক্রম আর maze-এর আকৃতির ওপর নির্ভর করে। first 6 expanded লাইনে ক্রমের পার্থক্য স্পষ্ট: BFS (0, 1) আর (1, 0), দুটি ১-ধাপ দূরের ঘর পাশাপাশি খোলে; DFS (0, 1), (0, 2), (1, 2) করে একটানা এগোয়। পথ সরু বলে দুটির max frontier ছোট (৪ আর ৫); খোলা জায়গায় BFS-এর frontier অনেক চওড়া হয়।

৫ · Depth-limited search আর iterative deepening

DFS-এর অসীম গভীরতার সমস্যার সরল সমাধান: একটি সীমা দিন। Depth-limited search (DLS) গভীরতা l-এর node-গুলোকে এমনভাবে দেখে যেন তাদের কোনো সন্তান নেই। ফলে দুই রকম ব্যর্থতা হয়: “সমাধান নেই” (failure), আর “এই সীমার মধ্যে নেই” (cutoff). সীমা কোথা থেকে আসবে? কখনো সমস্যা থেকেই: ১০টি এলাকার মানচিত্রে চক্রহীন কোনো পথ ৯ ধাপের বেশি হতে পারে না, তাই l = 9 যথেষ্ট। কিন্তু l < d হলে DLS goal পায় না (complete নয়), আর l অনেক বড় হলে আবার DFS-এর মতো খারাপ পথ দিতে পারে।

যদি d জানা না থাকে? তখন iterative deepening DFS (IDDFS): l = 0 দিয়ে DLS চালান, না পেলে l = 1, তারপর 2, … যতক্ষণ না goal পাওয়া যায়। প্রতিটি দফা একটি DFS, তাই memory মাত্র বর্তমান পথ। আবার প্রথম যে সীমায় goal পাওয়া যায়, তা সবচেয়ে অগভীর goal-এর গভীরতা d, তাই BFS-এর মতো সবচেয়ে কম ধাপের পথ।

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

বারবার দেখার দাম কত? b = 10-এর হিসাব

গভীরতা d-এর goal-এর জন্য IDDFS-এ শেষ স্তরের node (গভীরতা d) একবার তৈরি হয়, গভীরতা d − 1-এর node দুবার, …, গভীরতা ১-এর node d বার। শুরুর node বাদ দিয়ে মোট:

N(IDDFS) = d·b + (d − 1)·b2 + … + 2·bd−1 + 1·bd

b = 10, d = 5 হলে: 5 × 10 + 4 × 100 + 3 × 1,000 + 2 × 10,000 + 1 × 1,00,000 = 50 + 400 + 3,000 + 20,000 + 1,00,000 = 1,23,450. একই goal-এ BFS (শুরু বাদ দিয়ে) 10 + 100 + 1,000 + 10,000 + 1,00,000 = 1,11,110. অনুপাত 1,23,450 ÷ 1,11,110 ≈ 1.11, মানে মাত্র প্রায় ১১% বাড়তি কাজ। কারণ, বেশিরভাগ node সবচেয়ে নিচের স্তরে, আর সেটি একবারই তৈরি হয়। d বড় হলে অনুপাত b ÷ (b − 1)-এর দিকে যায়: b = 10-এ 1.11, b = 2-এ 2. b ছোট হলেই কেবল পুনরাবৃত্তির দাম বেশি।

নিচের কোড দুটি জিনিস চালায়: মানচিত্রে IDDFS, প্রতিটি সীমায় কতগুলো node দেখা হলো তা গুনে; আর b = 10-এর একটি কৃত্রিম গাছে, যেখানে goal গভীরতা ৪-এর একেবারে শেষ node, যাতে ওপরের সূত্র সত্যিকারের গণনার সঙ্গে মেলানো যায়। এখানে DLS শুধু নিজের পথের ওপর চক্র ঠেকায়, কোনো explored set রাখে না; তাই memory শুধু পথটি। গণনায় শুরুর node-ও ধরা হয়েছে।

ROADS = {}                                     # বানানো রাস্তার মানচিত্র, km
for line in open("dhaka_map.txt").read().splitlines():
    a, rest = line.split(":")
    ROADS[a] = [(b, int(km)) for b, km in (x.split() for x in rest.split(","))]
map_nbrs = lambda s: ROADS[s]

def dls(s, goal, limit, nbrs, path, st):
    st["nodes"] += 1; st["deep"] = max(st["deep"], len(path))
    if s == goal: return list(path)
    if limit == 0: return None                  # cutoff: এর নিচে যাওয়া নিষেধ
    for t, _ in nbrs(s):
        if t not in path:                       # নিজের পথে চক্র নয়
            path.append(t)
            found = dls(t, goal, limit - 1, nbrs, path, st)
            path.pop()
            if found: return found
    return None
def iddfs(start, goal, nbrs, max_limit=60, show=False):
    st, total = {"nodes": 0, "deep": 0}, 0
    for limit in range(max_limit + 1):          # 0, 1, 2, ... করে গভীরতা বাড়ানো
        st["nodes"] = 0
        found = dls(start, goal, limit, nbrs, [start], st)
        total += st["nodes"]
        if show: print(f"  limit {limit}: nodes {st['nodes']:>6}, total {total:>6}")
        if found: return found, total, st["deep"]

print("delivery map:")
p, total, deep = iddfs("Mirpur", "Motijheel", map_nbrs, show=True)
print("  path:", " -> ".join(p), "| hops", len(p) - 1)

print("uniform tree, b = 10, goal = last node at depth 4:")
tree = lambda n: [(10 * n + k, 1) for k in range(1, 11)]
p, total, deep = iddfs(0, 11110, tree, show=True)
bfs_nodes = 1 + 10 * len(bfs(0, 11110, tree)[1])     # root + expanded × b
print(f"  BFS nodes {bfs_nodes} | IDDFS/BFS = {total / bfs_nodes:.3f} | memory: {deep} nodes")

আসল আউটপুট:

delivery map:
  limit 0: nodes      1, total      1
  limit 1: nodes      5, total      6
  limit 2: nodes     10, total     16
  path: Mirpur -> Mohakhali -> Motijheel | hops 2
uniform tree, b = 10, goal = last node at depth 4:
  limit 0: nodes      1, total      1
  limit 1: nodes     11, total     12
  limit 2: nodes    111, total    123
  limit 3: nodes   1111, total   1234
  limit 4: nodes  11111, total  12345
  BFS nodes 11111 | IDDFS/BFS = 1.111 | memory: 5 nodes

মানচিত্রে সীমা ০-এ শুধু মিরপুর (১টি node), সীমা ১-এ মিরপুর আর চার প্রতিবেশী (৫টি), সীমা ২-এ মহাখালীর শাখায় মতিঝিল পাওয়া গেল ১০টি node দেখার পর। তিন দফায় মোট ১৬টি node, আর পথ BFS-এর মতোই ২ hop (অবশ্যই ২২ km, কারণ IDDFS-ও শুধু ধাপ গোনে)। কৃত্রিম গাছে প্রতিটি দফার সংখ্যা 1, 11, 111, 1111, 11111, মানে 1 + 10 + … + 10l; মোট 12345. BFS এই goal-এ 11111টি node তৈরি করে, অনুপাত 1.111, ঠিক উপরের হাতের হিসাবের মতো। আর memory: IDDFS-কে একসঙ্গে রাখতে হয়েছে মাত্র ৫টি node-এর একটি পথ, যেখানে BFS-এর frontier-এ শেষে হাজার হাজার node. BFS-এর গণনাটি 1 + 10 × expanded, কারণ goal এখানে শেষ expand হওয়া node-এর শেষ সন্তান।

সংক্ষেপে IDDFS: complete (b সসীম হলে), optimal যখন সব ধাপের খরচ সমান, time O(bd), space O(bd). state space বড় আর সমাধানের গভীরতা অজানা হলে এটি সাধারণত ভালো অন্ধ পদ্ধতি। দুর্বলতা: explored set না রাখায় একই state ভিন্ন পথে বারবার আসে; grid-এর মতো চক্রে ভরা graph-এ তা অনেক (অংশ ৮)।

৬ · Uniform-cost search — খরচ ধরে priority queue

রাইডারের আসল চিন্তা ধাপ নয়, km (বা সময়, বা তেলের খরচ)। Uniform-cost search (UCS) frontier-কে মোট পথ-খরচ g(n) ধরে সাজায়: শুরু থেকে node n পর্যন্ত পাওয়া পথের সব ধাপের খরচের যোগ। সবসময় সবচেয়ে কম g-এর node আগে expand হয়। frontier এখানে একটি priority queue, Python-এ heapq (binary heap, ভেতরের গঠন DSA-র heap পর্বে)। heapq.heappush(h, (g, node)) দিলে tuple-এর প্রথম মান g ধরে সাজায়; g সমান হলে দ্বিতীয় মান, এখানে এলাকার ইংরেজি নাম, বর্ণানুক্রমে।

দুটি নিয়ম মনে রাখুন। এক, কোনো node-এ নতুন করে সস্তা পথ পেলে তার g হালনাগাদ করে নতুন entry push করা হয়; পুরোনো দামি entry heap-এ থেকে যায় আর বের হলে বাদ পড়ে (heapq-তে “দাম কমানো”-র সরাসরি উপায় নেই, তাই এই কৌশল)। দুই, goal test হয় expand-এর সময়, তৈরির সময় নয়।

একই মানচিত্রে uniform-cost search: সবসময় সবচেয়ে কম মোট km-এর node আগে expand; মতিঝিল প্রথমে ২২ km-এ দেখা গেলেও ১৬ km-এর পথ মিরপুর-ফার্মগেট-তেজগাঁও-মতিঝিল পাওয়া যায় 4 7 8 11 7 4 3 3 3 14 13 9 6 4 10 উত্তরা মিরপুর গাবতলী মহাখালী গুলশান ফার্মগেট তেজগাঁও বাড্ডা ধানমন্ডি মতিঝিল frontier explored এখন expand
UCS-এর অবস্থা (priority queue, g = মোট km)
expand (g)
কী বদলাল
heap (কম g আগে)
explored
  1. শুরু। heap-এ (০, মিরপুর)। g মানে শুরু থেকে এখন পর্যন্ত মোট km.
  2. মিরপুর (০) expand. চার প্রতিবেশী নিজেদের রাস্তার km নিয়ে ঢুকল: গাবতলী ৪, ফার্মগেট ৭, মহাখালী ৮, উত্তরা ১১. heap সবসময় সবচেয়ে ছোট g ওপরে রাখে।
  3. গাবতলী (৪) expand. ধানমন্ডি ঢুকল ৪ + ৭ = ১১ km-এ।
  4. ফার্মগেট (৭) expand. তেজগাঁও ৭ + ৩ = ১০. ধানমন্ডিতে ফার্মগেট হয়ে ৭ + ৪ = ১১, আগের ১১-এর চেয়ে কম নয়, তাই কিছু বদলাল না।
  5. মহাখালী (৮) expand. মতিঝিল প্রথমবার দেখা গেল, ৮ + ১৪ = ২২ km-এ। এখানে থামলে ভুল হতো: তৈরির সময় goal test করলে UCS ২২ km-এর পথ ফেরত দিত। গুলশান ১১ ঢুকল; তেজগাঁও মহাখালী হয়ে ১১, আগের ১০-এর চেয়ে দামি।
  6. তেজগাঁও (১০) expand. মতিঝিলে তেজগাঁও হয়ে ১০ + ৬ = ১৬, আগের ২২-এর চেয়ে সস্তা। নতুন entry (১৬, মতিঝিল) ঢুকল; পুরোনো (২২) heap-এ থেকে গেল, কিন্তু আর কাজে লাগবে না।
  7. ধানমন্ডি (১১) expand. তিনটি node-এর g ১১; নামের বর্ণানুক্রমে (Dhanmondi, Gulshan, Uttara) ধানমন্ডি আগে। মতিঝিলে ধানমন্ডি হয়ে ১১ + ৯ = ২০, ১৬-এর চেয়ে দামি।
  8. গুলশান (১১) expand. বাড্ডা ঢুকল ১১ + ৪ = ১৫-এ।
  9. উত্তরা (১১) expand. দুই প্রতিবেশীই explored, নতুন কিছু নেই।
  10. বাড্ডা (১৫) expand. মতিঝিলে বাড্ডা হয়ে ১৫ + ১০ = ২৫, দামি। heap-এর ওপরে এখন (১৬, মতিঝিল)।
  11. মতিঝিল (১৬) expand, goal. এখন goal test সফল। ১৬-এর চেয়ে কম g-এর সব node আগেই expand হয়ে গেছে, তাই এর চেয়ে সস্তা পথ থাকতে পারে না। পথ: মিরপুর → ফার্মগেট → তেজগাঁও → মতিঝিল, ৩ hop, ১৬ km. মোট ১০টি node expand হলো।

কেন UCS optimal, আর কখন নয়

যুক্তিটি এক লাইনের: সব ধাপের খরচ ঋণাত্মক না হলে কোনো পথ এগোলে তার খরচ কমে না, আর UCS g-এর ছোট থেকে বড় ক্রমে node expand করে। তাই কোনো node যখন প্রথমবার expand হয়, তখন তার g-ই সবচেয়ে কম সম্ভব খরচ; goal-এর ক্ষেত্রেও তাই। এ কারণেই goal test expand-এর সময়: ধাপ ৫-এ মতিঝিল তৈরি হয়েছিল ২২-এ, কিন্তু তখনো ১০ আর ১১ খরচের node বাকি ছিল, যাদের পথ ধরে সস্তা রাস্তা আসতে পারত, আর এসেছেও।

  • ঋণাত্মক খরচ (ধরুন কোনো রাস্তায় গেলে ক্যাশব্যাক পাওয়া যায়) থাকলে এই যুক্তি ভাঙে, UCS আর Dijkstra ভুল ফল দিতে পারে; তখন Bellman-Ford-এর মতো অ্যালগরিদম লাগে (DSA-তে)।
  • শূন্য খরচের চক্র থাকলে UCS ঘুরপাক খেতে পারে; completeness-এর জন্য প্রতিটি ধাপের খরচ অন্তত একটি ধনাত্মক ε (epsilon, ছোট একটি সংখ্যা) হওয়া দরকার।
  • খরচ: সবচেয়ে ভালো সমাধানের খরচ C* হলে time আর space O(b1 + ⌊C*/ε⌋); এখানে ⌊ ⌋ মানে নিচের পূর্ণসংখ্যা। ধাপের খরচ খুব ছোট আর সমাধান দামি হলে এটি BFS-এর bd-এর চেয়ে অনেক খারাপ হতে পারে, কারণ UCS অনেক সস্তা-কিন্তু-অকাজের ছোট ধাপ আগে ঘোরে। সব ধাপের খরচ সমান হলে UCS কার্যত BFS-ই, শুধু goal test দেরিতে।

UCS আর Dijkstra: একই ধারণা, ভিন্ন ব্যবহার

Dijkstra-র অ্যালগরিদম (Edsger Dijkstra, ১৯৫৯) একটি উৎস থেকে জানা graph-এর সব node-এর সবচেয়ে ছোট দূরত্ব বের করে (DSA-র Dijkstra). UCS একই নিয়মে চলে, কিন্তু node তৈরি করে যখন দরকার আর goal expand হলেই থামে; ৮-puzzle-এর মতো বিশাল state space-এ এই পার্থক্য বড় (ভাবনার প্রশ্ন ৩)।

এবার BFS আর UCS একই মানচিত্রে, একই শুরু আর goal-এ। km(p) একটি পথের সব রাস্তার km যোগ করে।

import heapq

def ucs(start, goal, nbrs):
    frontier, best, parent = [(0, start)], {start: 0}, {start: None}
    order, maxf = [], 1
    while frontier:
        g, s = heapq.heappop(frontier)                    # সবচেয়ে কম g আগে
        if g > best[s]: continue                          # পুরোনো, বেশি দামের entry
        order.append(s)
        if s == goal: return walk(parent, s), order, maxf # expand-এর সময় test
        for t, w in nbrs(s):
            if g + w < best.get(t, float("inf")):         # নতুন বা সস্তা পথ
                best[t], parent[t] = g + w, s
                heapq.heappush(frontier, (g + w, t))
        maxf = max(maxf, len(frontier))

km = lambda p: sum(dict(ROADS[a])[b] for a, b in zip(p, p[1:]))
for name, f in [("BFS", bfs), ("UCS", ucs)]:
    p, order, maxf = f("Mirpur", "Motijheel", map_nbrs)
    print(f"{name}: {' -> '.join(p)}")
    print(f"     hops {len(p) - 1}, km {km(p)}, expanded {len(order)}")
print("UCS expand order:", ", ".join(ucs("Mirpur", "Motijheel", map_nbrs)[1]))

আসল আউটপুট:

BFS: Mirpur -> Mohakhali -> Motijheel
     hops 2, km 22, expanded 4
UCS: Mirpur -> Farmgate -> Tejgaon -> Motijheel
     hops 3, km 16, expanded 10
UCS expand order: Mirpur, Gabtoli, Farmgate, Mohakhali, Tejgaon, Dhanmondi, Gulshan, Uttara, Badda, Motijheel

BFS-এর পথে hop কম (২), কিন্তু km বেশি (২২); UCS-এর পথে hop বেশি (৩), কিন্তু km কম (১৬)। কোনটি “ঠিক”, তা নির্ভর করে খরচ কী দিয়ে মাপছেন তার ওপর: প্রতিটি মোড়ে সিগন্যালে দাঁড়াতে হলে hop গুরুত্বপূর্ণ, তেলের খরচ হলে km. খরচের সংজ্ঞাই সমস্যার অংশ (পর্ব ৩৯)। BFS মাত্র ৪টি node expand করে থেমেছে, UCS ১০টি, মানে এই ছোট মানচিত্রের সবগুলো; সস্তা পথের নিশ্চয়তার দাম বেশি কাজ। শেষ লাইনের expand-এর ক্রম stepper-এর সঙ্গে হুবহু মেলে।

৭ · Bidirectional search — দুই দিক থেকে খোঁড়া সুড়ঙ্গ

পাহাড়ের নিচে সুড়ঙ্গ খুঁড়তে দুই দিক থেকে দুটি দল কাজ শুরু করে, মাঝখানে মিলিত হয়। Bidirectional search একই কাজ করে: একটি search শুরু থেকে সামনে, আরেকটি goal থেকে পেছনে, আর দুটি frontier কোনো state-এ মিলে গেলে থামে। লাভটি ঘাতের হিসাবে: প্রতিটি দিক মাত্র d/2 গভীরতা পর্যন্ত যায়, তাই মোট প্রায় bd/2 + bd/2, যা bd-এর চেয়ে অনেক কম। b = 10, d = 6 হলে 2 × 103 = 2,000 বনাম 106 = দশ লাখ।

শর্ত আর সাবধানতা। goal একটি নির্দিষ্ট state হতে হবে (“যেকোনো চেকমেট অবস্থান” হলে পেছন থেকে শুরু করা কঠিন), আর action উল্টো দিকে হিসাব করা যেতে হবে: “কোন কোন state থেকে এখানে আসা যায়?” রাস্তার মানচিত্রে সহজ, অনেক ধাঁধায় কঠিন। প্রতিটি নতুন node অন্য দিকের reached set-এ আছে কি না, তা দেখতে হয়, তাই অন্তত একটি দিকের সব node memory-তে রাখতে হয়। দুই দিকে UCS চালালে প্রথম মিলনেই থামলে সবসময় সবচেয়ে সস্তা পথ মেলে না; সঠিক থামার নিয়ম সূক্ষ্ম। ধারণাটি ১৯৬০-এর দশকের শেষ থেকে Ira Pohl-এর কাজের সঙ্গে যুক্ত। এই পর্বে এটুকুই; কোড নেই।

৮ · পাশাপাশি তুলনা — তত্ত্ব আর সত্যিকারের run

প্রথমে তত্ত্বের টেবিল, যা বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর পরীক্ষায় প্রায়ই চাওয়া হয়। b = branching factor, d = সবচেয়ে অগভীর goal-এর গভীরতা, m = সর্বোচ্চ গভীরতা, l = গভীরতার সীমা, C* = সবচেয়ে সস্তা সমাধানের খরচ, ε = সবচেয়ে ছোট ধাপের খরচ।

পদ্ধতিcomplete?optimal?timespace
BFSহ্যাঁ¹হ্যাঁ³O(bd)O(bd)
Uniform-costহ্যাঁ¹˒²হ্যাঁO(b1+⌊C*/ε⌋)O(b1+⌊C*/ε⌋)
DFSনা⁴নাO(bm)O(bm)
Depth-limitedনা (l < d হলে)নাO(bl)O(bl)
Iterative deepeningহ্যাঁ¹হ্যাঁ³O(bd)O(bd)
Bidirectionalহ্যাঁ¹˒⁵হ্যাঁ³˒⁵O(bd/2)O(bd/2)

¹ b সসীম হলে। ² প্রতিটি ধাপের খরচ ≥ ε > 0 হলে। ³ সব ধাপের খরচ সমান হলে। ⁴ সসীম state space-এ explored set রাখলে complete, কিন্তু তখন memory বেশি। ⁵ দুই দিকেই BFS (বা ঠিক থামার নিয়মসহ UCS) হলে।

এবার চারটি পদ্ধতি দুটি সমস্যায় সত্যিকারে চালিয়ে মিলিয়ে দেখুন। expanded কলামে BFS, DFS আর UCS-এর জন্য expand হওয়া node, আর IDDFS-এর জন্য সব দফা মিলিয়ে দেখা node (প্রতিটি দফার শুরুর node-সহ)। max mem কলামে প্রথম তিনটির জন্য frontier-এর সর্বোচ্চ আকার, আর IDDFS-এর জন্য memory-তে থাকা পথের সর্বোচ্চ দৈর্ঘ্য। মানচিত্রে cost হলো km, maze-এ ধাপের সংখ্যা। নিচে maze-এ BFS আর DFS কোন ঘর কোন ক্রমে খুলেছে, তার একটি ছবিও আসবে।

import numpy as np
import matplotlib.pyplot as plt

def compare(title, start, goal, nbrs, cost):
    print(f"{title}\n  {'algo':6}{'expanded':>9}{'max mem':>8}{'steps':>6}{'cost':>6}")
    for name, f in [("BFS", bfs), ("DFS", dfs), ("IDDFS", iddfs), ("UCS", ucs)]:
        p, work, mem = f(start, goal, nbrs)
        if name != "IDDFS": work = len(work)          # IDDFS আগেই মোট সংখ্যা দেয়
        print(f"  {name:6}{work:>9}{mem:>8}{len(p) - 1:>6}{cost(p):>6}")

compare("delivery map (km)", "Mirpur", "Motijheel", map_nbrs, km)
compare("grid maze (steps)", START, GOAL, grid_nbrs, lambda p: len(p) - 1)

walls = np.where(np.array([[ch == "#" for ch in row] for row in MAZE]), 1.0, np.nan)
fig, axes = plt.subplots(1, 2, figsize=(8, 3.8))
for ax, (name, f) in zip(axes, [("BFS", bfs), ("DFS", dfs)]):
    p, order, _ = f(START, GOAL, grid_nbrs)
    seen = np.full((R, C), np.nan)
    for i, s in enumerate(order): seen[s] = i       # কততম expand
    ax.imshow(seen, cmap="viridis")
    ax.imshow(walls, cmap="Greys", vmin=0, vmax=1)
    ys, xs = zip(*p)
    ax.plot(xs, ys, color="red", lw=2.5, label="path")
    ax.set_title(f"{name}: {len(order)} expanded, path {len(p) - 1} steps")
    ax.set_xticks([]); ax.set_yticks([])
axes[0].legend(loc="lower left", fontsize=8)
fig.suptitle("Expanded cells (dark = early, yellow = late), walls in black")
plt.tight_layout()
plt.show()

আসল আউটপুট:

delivery map (km)
  algo   expanded max mem steps  cost
  BFS           4       4     2    22
  DFS          10       8     8    38
  IDDFS        16       3     2    22
  UCS          10       5     3    16
grid maze (steps)
  algo   expanded max mem steps  cost
  BFS          44       4    19    19
  DFS          26       5    25    25
  IDDFS       601      20    19    19
  UCS          45       4    19    19

মানচিত্রে চারটি গল্প। BFS আর IDDFS একই ২-hop, ২২ km-এর পথ দিল; IDDFS ১৬টি node দেখেছে (তিন দফার পুনরাবৃত্তি) কিন্তু memory-তে কখনো ৩টির বেশি রাখেনি। DFS ৮ ধাপ আর ৩৮ km, সবচেয়ে খারাপ। শুধু UCS সবচেয়ে সস্তা ১৬ km দিল, সবচেয়ে বেশি expand করে। maze-এ সব ধাপের খরচ সমান, তাই BFS, IDDFS আর UCS তিনটিই একই দৈর্ঘ্যের (১৯) সবচেয়ে ছোট পথ পেয়েছে; UCS-এর ৪৫ বনাম BFS-এর ৪৪, কারণ UCS goal-কে নিজেও expand করে (দেরিতে goal test). এখানে IDDFS-এর দাম বড়: ৬০১টি node, কারণ grid-এ একই ঘরে অনেক পথে পৌঁছানো যায় আর IDDFS explored set রাখে না; ১১%-এর হিসাবটি গাছের জন্য। ছবিতে রং দেখায় ঘরটি কততম expand হয়েছে (গাঢ় বেগুনি আগে, হলুদ পরে): BFS প্রায় পুরো maze ঢেউয়ের মতো ভরেছে, DFS একটি শাখা ধরে গেছে আর বাঁ দিকের বড় অংশ ছোঁয়নি। এগুলো দুটি ছোট সমস্যার একটি run, সাধারণ নিয়ম নয়।

৯ · কোনটি কখন — পরিস্থিতি দেখে বাছাই

কোনো একটি পদ্ধতি সবসময় সেরা নয়; প্রশ্নগুলো এভাবে করুন:

খরচ সমান, memory যথেষ্ট

BFS. সবচেয়ে কম ধাপের পথ, সহজ কোড। ছোট maze, সামাজিক নেটওয়ার্কে “কত ধাপ দূরে”, ছোট ধাঁধা।

খরচ সমান, space বিশাল, গভীরতা অজানা

Iterative deepening. BFS-এর মতো পথ, DFS-এর মতো memory; গাছের মতো state space-এ বাড়তি কাজ সামান্য।

ধাপের খরচ আলাদা

Uniform-cost. km, সময় বা টাকা; খরচ ঋণাত্মক নয়। goal-এর দিকে ভালো অনুমান থাকলে A*, পর্ব ৪১-এ।

যেকোনো একটি সমাধান, অনেক সমাধান আছে

DFS, গভীরতার সীমাসহ। Sudoku বা সময়সূচির মতো constraint সমস্যায় backtracking (পর্ব ৪৩)।

goal নির্দিষ্ট আর পেছনে হাঁটা সহজ হলে bidirectional ভাবুন। আর bd যন্ত্রের সাধ্যের বাইরে হলে কোনো অন্ধ পদ্ধতিই চলবে না: তখন goal-এর দিকে অনুমান (পর্ব ৪১) বা local search (পর্ব ৪৪)।

আরও গভীরে। Richard Korf-এর ১৯৮৫ সালের প্রবন্ধ “Depth-first iterative-deepening: an optimal admissible tree search” (Artificial Intelligence জার্নাল) দেখায় যে গাছের মতো সমস্যায় IDDFS সময়, memory আর সমাধানের মানে একসঙ্গে প্রায় সবচেয়ে ভালো অন্ধ পদ্ধতি; একই ধারণা পরে IDA*-তে heuristic-এর সঙ্গে মেলে। Ariel Felner-এর ২০১১ সালের একটি position paper যুক্তি দেয় যে AI-তে Dijkstra-র প্রথাগত “সব node আগে queue-তে” রূপের চেয়ে UCS-এর মতো দরকার-মতো node তৈরি করা রূপটিই শেখানো উচিত। বাস্তব কোডে heap-এর পুরোনো entry রেখে দেওয়া (lazy deletion) সাধারণত decrease-key-এর চেয়ে সহজ আর প্রায়ই দ্রুত, কিন্তু heap বড় হয়।

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

  • BFS কি সবসময় সবচেয়ে ছোট পথ দেয়? সবচেয়ে কম ধাপের পথ দেয়; ধাপের খরচ আলাদা হলে সবচেয়ে সস্তা পথের জন্য UCS লাগে।
  • b = 10, d = 5-এ IDDFS বনাম BFS-এর node? 1,23,450 বনাম 1,11,110, প্রায় ১১% বেশি।
  • DFS-এর space complexity? O(bm), BFS-এর O(bd)-এর তুলনায় রৈখিক।
  • UCS-এ goal test কখন? expand-এর সময়; তৈরির সময় করলে দামি পথ ফেরত আসতে পারে।
  • Bidirectional search-এর লাভ? bd-এর বদলে প্রায় 2·bd/2; শর্ত: goal জানা আর action উল্টানো যায়।

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

AI সহকারী কয়েক সেকেন্ডে BFS বা UCS-এর কোড দেবে, আর ছোট উদাহরণে তা প্রায়ই ঠিকও চলবে; ভুল লুকিয়ে থাকে কিনারায়। AI সহকারীরা প্রায়ই যা করে:

  • ভুল কাঠামো: queue হিসেবে list.pop(0) (প্রতিবার O(n), বড় search-এ ধীর), অথবা BFS-এ “visited” চিহ্ন বের হওয়ার সময় দেওয়া, ফলে একই node queue-তে বহুবার।
  • UCS-এ তৈরির সময় goal test, যা অংশ ৬-এর মতো ২২ km ফেরত দেয়; অথবা ওজনওয়ালা graph-এ BFS দিয়ে “সবচেয়ে ছোট পথ” দাবি।
  • heapq-এ তুলনা করা যায় না এমন জিনিস: (g, state_dict); g সমান হলেই TypeError. সমাধান: মাঝে একটি ক্রমিক সংখ্যা, (g, counter, state).
  • বড় grid-এ recursive DFS, যা Python-এর recursion সীমা (সাধারণত ১,০০০) পার হয়ে RecursionError দেয়; বা গভীরতার সীমা ছাড়া অসীম space-এ DFS.
  • জটিলতা আর তুলনার ভুল দাবি: “DFS সবসময় দ্রুত”, “IDDFS দ্বিগুণ খরচ”, বা node না গুনেই কোনটি ভালো বলে দেওয়া।

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

আমি Python 3.14 ব্যবহার করছি, শুধু standard library (collections, heapq). একটি বানানো রাস্তার মানচিত্র আছে, dict হিসেবে: প্রতিটি এলাকা থেকে (প্রতিবেশী, km)-এর list. উদাহরণ: S: A 1, B 4; A: S 1, B 2, C 5; B: S 4, A 2, C 1, G 7; C: A 5, B 1, G 3; G: B 7, C 3 ১. uniform-cost search লেখো heapq দিয়ে; goal test expand-এর সময় করবে, তৈরির সময় নয়, আর কেন, এক লাইনে বলো। ২. heap-এ (g, counter, state) রাখো, যাতে g সমান হলে state তুলনা করতে না হয়। ৩. পুরোনো, দামি entry বের হলে বাদ দাও (lazy deletion). ৪. পথ, মোট km আর expand হওয়া node-এর ক্রম print করো; উপরের উদাহরণে আমি হাতে S-A-B-C-G, 7 km পেয়েছি, মেলাও। ৫. একই graph-এ BFS চালিয়ে hop আর km পাশাপাশি দেখাও। কোনো function নিয়ে নিশ্চিত না হলে বানিয়ে লিখবে না, সরাসরি বলবে।

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

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

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

প্র ০১BFS কে আবিষ্কার করেছিলেন? একই ধারণা কেন বারবার, আলাদা আলাদা জায়গায় এসেছে?

সাধারণত দুটি নাম আসে। Edward F. Moore ১৯৫৯ সালে একটি সম্মেলনে “The shortest path through a maze” নামে লেখায় গোলকধাঁধার সবচেয়ে ছোট পথ খোঁজার জন্য স্তর ধরে খোঁজার পদ্ধতি বর্ণনা করেন। প্রায় একই সময়ে, ১৯৬১ সালে, C. Y. Lee বৈদ্যুতিক circuit board-এ তার টানার পথ (wire routing) খুঁজতে একই ধরনের ধারণা প্রকাশ করেন, যা এখনো chip ডিজাইনে “Lee algorithm” নামে পরিচিত। অনেক সূত্রে আরও আগের একটি নামও আসে: Konrad Zuse ১৯৪০-এর দশকে তাঁর Plankalkül ভাষার কাজে graph-এর সংযুক্ত অংশ খোঁজার এমন একটি পদ্ধতি লিখেছিলেন, যা দীর্ঘদিন প্রকাশিত হয়নি। এই আগের দাবিটি নিয়ে বিস্তারিত সূত্রের বিবরণ ভিন্ন হতে পারে, তাই সাবধানে নেওয়াই ভালো।

DFS-এর শিকড় আরও পুরোনো। উনিশ শতকের ফরাসি গণিতবিদ Édouard Lucas তাঁর বিনোদনমূলক গণিতের বইয়ে Charles Pierre Trémaux-এর গোলকধাঁধা সমাধানের নিয়ম বর্ণনা করেন: একটি পথে চিহ্ন দিতে দিতে এগোনো, কানাগলিতে ফেরা, আর একই পথ দুবারের বেশি না যাওয়া। এটি আসলে হাতে-কলমে DFS. ১৯৭০-এর দশকে Robert Tarjan আর John Hopcroft DFS-কে graph অ্যালগরিদমের একটি শক্তিশালী হাতিয়ার হিসেবে প্রতিষ্ঠিত করেন।

একই ধারণা বারবার আসার কারণ হলো, সমস্যাটি স্বাভাবিক। যেকোনো ক্ষেত্রে “ধাপে ধাপে বিকল্পগুলো দেখা” লাগলে দুটি সরল নিয়মই হাতে থাকে: পুরোনোটি আগে (queue) বা নতুনটি আগে (stack). গোলকধাঁধা, টেলিফোন নেটওয়ার্ক, circuit board আর পরে AI-এর ধাঁধা, প্রত্যেক ক্ষেত্রের মানুষ নিজের ভাষায় একই কাঠামো খুঁজে পেয়েছেন। AI-এর বিশেষ অবদান হলো এগুলোকে “অবস্থার জগতে খোঁজা”-র সাধারণ কাঠামোয় বসানো, যাতে একই কোড মানচিত্র, puzzle বা পরিকল্পনা সবকিছুতে চলে, আর completeness ও optimality-র মতো প্রশ্ন দিয়ে তুলনা করা যায়।

মূল উপলব্ধি: BFS আর DFS কোনো একজনের একক আবিষ্কার নয়; গোলকধাঁধা থেকে circuit পর্যন্ত বহু ক্ষেত্রে স্বাধীনভাবে এসেছে, আর AI এগুলোকে একটি সাধারণ search কাঠামোয় একত্র করেছে।

প্র ০২Iterative deepening শুনতে অপচয়ী, তবু দাবার প্রোগ্রাম থেকে পাঠ্যবই পর্যন্ত কেন এত জনপ্রিয়?

প্রথম কারণ গাণিতিক, যা অংশ ৫-এ দেখলেন। ঘাতের হারে বাড়া গাছে বেশিরভাগ node শেষ স্তরে থাকে। b = 10-এ শেষ স্তরে মোট node-এর প্রায় ৯০%. তাই আগের স্তরগুলো যতবারই আবার দেখা হোক, মোট খরচ শেষ স্তরের খরচের একটি ছোট গুণিতক: d বড় হলে b/(b − 1), মানে b = 10-এ প্রায় ১.১১ গুণ। বিনিময়ে memory O(bd) থেকে O(bd)-তে নামে, যা প্রায়ই “চলে না” আর “চলে”-র পার্থক্য।

দ্বিতীয় কারণ ব্যবহারিক, আর এটি দাবার প্রোগ্রামে স্পষ্ট। দাবার খেলায় প্রতিটি চালের জন্য সময় সীমিত। গভীরতা ১, ২, ৩ করে খুঁজলে যেকোনো মুহূর্তে সময় ফুরালে শেষ সম্পূর্ণ দফার সেরা চালটি হাতে থাকে, যাকে বলে anytime আচরণ। শুরুতেই গভীরতা ১০ ঠিক করলে সময় ফুরালে হাতে কিছুই থাকে না। ১৯৭০-এর দশকে Northwestern University-র Chess 4.x প্রোগ্রাম (David Slate আর Larry Atkin) iterative deepening জনপ্রিয় করায় বড় ভূমিকা রেখেছিল বলে প্রায়ই উল্লেখ করা হয়। তৃতীয় সুবিধা: আগের দফায় যে চালগুলো ভালো দেখাচ্ছিল, পরের দফায় সেগুলো আগে পরীক্ষা করলে alpha-beta pruning (পর্ব ৪২) বেশি শাখা কাটে, ফলে পুনরাবৃত্তি কখনো মোট কাজ কমায়ও।

তত্ত্বের দিকে Richard Korf ১৯৮৫ সালে দেখান যে গাছের মতো সমস্যায় IDDFS সময়, memory আর সমাধানের মানে একসঙ্গে অন্ধ পদ্ধতিগুলোর মধ্যে অ্যাসিম্পটোটিকভাবে প্রায় সেরা। তবে সীমাও আছে: অংশ ৮-এর grid-এ চক্র অনেক, IDDFS ৬০১টি node দেখেছে, BFS মাত্র ৪৪. যেখানে একই state অনেক পথে আসে আর memory যথেষ্ট, সেখানে BFS বা explored set-ওয়ালা পদ্ধতি ভালো।

মূল উপলব্ধি: ঘাতের গাছে পুনরাবৃত্তির দাম ছোট আর memory-র সাশ্রয় বিশাল; সময়-সীমিত খেলায় anytime আচরণ আর চাল সাজানোর সুবিধা iterative deepening-কে প্রায় অপরিহার্য করেছে, তবে চক্রে ভরা graph-এ হিসাব বদলায়।

প্র ০৩Uniform-cost search আর Dijkstra-র অ্যালগরিদম কি আসলে একই জিনিস? পার্থক্যটি কেন গুরুত্বপূর্ণ?

মূল নিয়মে একই: ঋণাত্মক নয় এমন খরচে, এখন পর্যন্ত সবচেয়ে কম দূরত্বের node-কে চূড়ান্ত ধরা আর তার প্রতিবেশীদের দূরত্ব হালনাগাদ করা। Edsger Dijkstra ১৯৫৯ সালে Numerische Mathematik জার্নালে মাত্র তিন পাতার একটি লেখায় এটি প্রকাশ করেন; পরে তিনি বলেছিলেন অ্যালগরিদমটি তিনি আমস্টারডামে একটি ক্যাফেতে বসে কাগজ-কলম ছাড়াই প্রায় বিশ মিনিটে ভেবেছিলেন। গল্পটি তাঁর নিজের সাক্ষাৎকার থেকে, তাই প্রচলিত, তবে খুঁটিনাটি স্মৃতিনির্ভর।

পার্থক্য প্রেক্ষাপটে। প্রথাগত পাঠ্যবইয়ের Dijkstra একটি পুরো জানা graph নেয়, শুরুতেই সব node-কে ∞ দূরত্ব দিয়ে priority queue-তে রাখে, আর সব node-এর দূরত্ব বের করে শেষ হয়। AI-এর UCS-এর graph প্রায়ই অন্তর্নিহিত: শুধু শুরুর state আর একটি function আছে যা বলে “এখান থেকে কোথায় যাওয়া যায়”। ৮-puzzle-এ ১,৮১,৪৪০টি পৌঁছানো যায় এমন state, রুবিকস কিউবে প্রায় 4.3 × 1019. সেগুলো আগে queue-তে রাখার প্রশ্নই ওঠে না। UCS তাই শুধু শুরুর node দিয়ে শুরু করে, দরকার হলে নতুন node তৈরি করে, আর goal expand হলেই থামে; graph অসীম হলেও চলে, যদি সমাধান সসীম খরচে থাকে।

Ariel Felner ২০১১ সালে একটি position paper-এ এই বিষয়ে যুক্তি দেন যে শিক্ষা আর বাস্তব কাজ, দুই ক্ষেত্রেই UCS-এর রূপটি বেশি স্বাভাবিক, আর তাঁর বর্ণনায় পরীক্ষাগুলোতে তা প্রায়ই দ্রুতও ছিল, কারণ queue ছোট থাকে। অন্যদিকে রাস্তার নেভিগেশনে, যেখানে graph আগেই memory-তে আর একই graph-এ লাখো প্রশ্ন আসে, আগে থেকে হিসাব রেখে দেওয়ার (preprocessing) নানা কৌশল চলে। কোনটি “ঠিক”, তা প্রশ্নের ধরনের ওপর নির্ভর করে।

মূল উপলব্ধি: নিয়ম এক, কিন্তু Dijkstra সাধারণত জানা graph-এ সব দূরত্ব বের করে, আর UCS অজানা বা বিশাল state space-এ দরকার-মতো node বানিয়ে একটি goal পর্যন্ত থামে; AI-এর search-এ দ্বিতীয় রূপটিই কাজের।

প্র ০৪DFS optimal নয়, complete-ও নয়; তবু বাস্তবে এত ব্যবহার হয় কেন? কখন DFS BFS-এর চেয়ে ভালো?

প্রথম কারণ memory, যা অংশ ৩-এর টেবিলে দেখলেন। অনেক সমস্যায় BFS-এর frontier কয়েক স্তর পরেই RAM ছাড়িয়ে যায়, অথচ DFS-কে রাখতে হয় শুধু একটি পথ। একটি পুরোনো যন্ত্রে বা ছোট embedded ডিভাইসে এটিই সিদ্ধান্ত নেয়।

দ্বিতীয় কারণ সমস্যার ধরন। যখন অনেক সমাধান আছে আর যেকোনো একটি হলেই চলে, তখন DFS প্রায়ই দ্রুত একটিতে পৌঁছে যায়, কারণ সে শুরুতেই গভীরে যায়, যেখানে সমাধানগুলো থাকে। Sudoku, n-queens বা পরীক্ষার রুটিন বানানোর মতো constraint সমস্যায় সব সমাধান একই গভীরতায় (সব ঘর পূরণ হলে), আর অগভীর স্তরে কোনো সমাধান নেই; সেখানে BFS প্রতিটি স্তরের সব আংশিক বিন্যাস memory-তে রাখবে অকারণে। তাই এসব সমস্যার মূল হাতিয়ার DFS-ভিত্তিক backtracking (পর্ব ৪৩)। অংশ ৪-এর maze-এও DFS কম ঘর খুলেছিল (২৬ বনাম ৪৪), যদিও পথ লম্বা।

তৃতীয় কারণ, DFS-এর ক্রম নিজেই তথ্য দেয়। Graph-এর তত্ত্বে চক্র খোঁজা, topological sort, শক্তভাবে সংযুক্ত অংশ (strongly connected components) বের করা, সবই DFS-এর “কখন ঢুকলাম, কখন বের হলাম” সময় থেকে আসে, যা ১৯৭০-এর দশকে Tarjan আর Hopcroft-এর কাজে প্রতিষ্ঠিত। Compiler-এর গাছ পড়া বা ফাইল সিস্টেমের ফোল্ডার ঘোরাও স্বাভাবিকভাবে DFS.

তবে DFS খারাপ বাছাই যখন সবচেয়ে ছোট পথ দরকার (৩৮ km বনাম ১৬), যখন একটি অগভীর সমাধানের পাশে অসীম বা খুব গভীর শাখা আছে, বা যখন প্রতিবেশীর ক্রমের দুর্ভাগ্যে সে বিশাল একটি ফাঁকা অংশে ঢুকে পড়ে। সমাধান প্রায়ই গভীরতার সীমা, বা পুরো iterative deepening.

মূল উপলব্ধি: DFS memory-তে সস্তা, অনেক-সমাধানের সমস্যায় দ্রুত, আর graph-এর গঠন বোঝার হাতিয়ার; সবচেয়ে ছোট পথ বা নিশ্চয়তা দরকার হলে গভীরতার সীমা বা iterative deepening যোগ করতে হয়।

প্র ০৫আজকের AI-তে, যেখানে বড় মডেলের কথা বেশি শোনা যায়, এই অন্ধ search-এর জায়গা কোথায়?

প্রথমত, অন্ধ search এখনো অনেক কাজে সরাসরি চলে: web crawler স্তর ধরে লিংক ঘোরে, package manager নির্ভরতার গাছ ঘুরে install-এর ক্রম ঠিক করে, game-এর চরিত্র ছোট grid-এ BFS দিয়ে পথ খোঁজে। এসব জায়গায় state space ছোট বা মাঝারি, তাই সরল পদ্ধতিই যথেষ্ট আর নির্ভরযোগ্য।

দ্বিতীয়ত, বড় সমস্যায় অন্ধ search কাঠামো হয়ে থাকে, আর তার ওপর অন্য কিছু বসে। A* হলো UCS-এর সঙ্গে একটি অনুমান যোগ (পর্ব ৪১)। দাবা আর Go খেলার প্রোগ্রাম গভীরতা-সীমিত search আর iterative deepening-এর ওপর মূল্যায়ন যোগ করে, আর AlphaGo (২০১৬) neural network-এর মূল্যায়ন আর Monte Carlo tree search মিলিয়েছিল (পর্ব ৪২)। অর্থাৎ শিখে নেওয়া মডেল search-কে প্রতিস্থাপন করেনি, অনেক ক্ষেত্রে search-কে পথ দেখায়।

তৃতীয়ত, language model-এর যুগেও ধারণাগুলো ফিরে এসেছে। ২০২৩ সালের “Tree of Thoughts” প্রবন্ধে (Yao ও সহকর্মীরা) একটি language model-এর তৈরি মধ্যবর্তী চিন্তাগুলোকে গাছের node ধরে BFS আর DFS চালানোর পরীক্ষা হয়েছিল; কিছু ধাঁধায় তা সরাসরি উত্তরের চেয়ে ভালো ফল দিয়েছিল বলে লেখকেরা জানান, তবে খরচ অনেক বেশি আর ফল কাজের ধরনের ওপর নির্ভরশীল। এখানে b আর d-এর হিসাব আরও কঠিন, কারণ প্রতিটি node তৈরি মানে মডেলকে একবার ডাকা, যা সময় আর টাকা খরচ করে।

সবচেয়ে বড় শিক্ষাটি অবশ্য অংশ ৩-এর টেবিল: combinatorial explosion. সব বিকল্প দেখা যে অসম্ভব, এই উপলব্ধি থেকেই heuristic, pruning, শেখা মূল্যায়ন আর আজকের অনেক কৌশলের জন্ম। কোনো AI পণ্য “সব সম্ভাবনা যাচাই করে” বললে b আর d কত, প্রশ্ন করুন।

মূল উপলব্ধি: অন্ধ search ছোট কাজে সরাসরি, বড় কাজে কাঠামো হিসেবে টিকে আছে, আর তার ঘাতের হিসাবই বুঝিয়ে দেয় কেন অনুমান আর শেখা মূল্যায়ন দরকার।

অনুশীলন

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

  1. হাতে trace: একটি দুই-দিকের graph-এর রাস্তা আর খরচ: S–A 1, S–B 4, A–B 2, A–C 5, B–C 1, B–G 7, C–G 3. শুরু S, goal G; প্রতিবেশীর ক্রম বর্ণানুক্রমে, সমান খরচে নামের বর্ণানুক্রম। (ক) BFS (তৈরির সময় goal test, explored set সহ) কোন node-গুলো expand করে, আর কোন পথ ও খরচ দেয়? (খ) UCS (expand-এর সময় goal test) প্রতিটি ধাপে কোন (g, node) বের করে, আর কোন পথ দেয়?

    (ক) BFS: S expand → A, B তৈরি। A expand → C তৈরি (B আগেই দেখা)। B expand → C দেখা, G তৈরি হতেই goal. expand হলো S, A, B (৩টি)। পথ S → B → G, ২ hop, খরচ ৪ + ৭ = ১১.

    (খ) UCS: heap [(0, S)]. বের হয় (0, S) → (1, A), (4, B). বের হয় (1, A) → B-তে 1 + 2 = 3, সস্তা, (3, B) ঢোকে; C-তে 6. বের হয় (3, B) → C-তে 3 + 1 = 4, সস্তা; G-তে 3 + 7 = 10. heap-এ তখন (4, B) পুরোনো, (4, C), (6, C) পুরোনো, (10, G). সমান খরচ ৪-এ নামের ক্রমে আগে (4, B) বের হয়, কিন্তু B আগেই explored, তাই বাদ। তারপর (4, C) → G-তে 4 + 3 = 7, সস্তা। শেষে (7, G) বের হয়: goal. পথ S → A → B → C → G, ৪ hop, খরচ ৭. BFS-এর ১১-এর চেয়ে কম।

    লেখকের যন্ত্রে এই পর্বের কোডের মতো BFS আর UCS দিয়ে মেলানো (Python 3.14): BFS ['S', 'B', 'G'] 11, UCS ['S', 'A', 'B', 'C', 'G'] 7, UCS-এর expand ক্রম S, A, B, C, G.

  2. আউটপুট অনুমান করুন: চালানোর আগে প্রতিটি print-এর আউটপুট লিখুন, তারপর যেকোনো Python-এ মিলিয়ে দেখুন।
    from collections import deque
    import heapq
    q = deque(["ক", "খ"]); s = ["ক", "খ"]
    q.append("গ"); s.append("গ")
    print(q.popleft(), s.pop())
    print(list(q), s)
    h = [(5, "Mirpur"), (2, "Gulshan"), (2, "Badda"), (9, "Uttara")]
    heapq.heapify(h)
    print(heapq.heappop(h), heapq.heappop(h))
    print(h[0], len(h))
    ক গ
    ['খ', 'গ'] ['ক', 'খ']
    (2, 'Badda') (2, 'Gulshan')
    (5, 'Mirpur') 2

    deque-এর popleft সবচেয়ে পুরোনোটি (ক) দেয়, BFS-এর queue; list-এর pop সবচেয়ে নতুনটি (গ), DFS-এর stack. heap-এ খরচ 2 দুটি; tuple-এর দ্বিতীয় মান তুলনা হয়, আর "Badda" < "Gulshan", তাই Badda আগে। দুটি বের হওয়ার পর সবচেয়ে ছোট (5, 'Mirpur') ওপরে, বাকি ২টি। লেখকের যন্ত্রে Python 3.14 দিয়ে চালিয়ে মেলানো।

  3. সংখ্যার হিসাব: একটি সমস্যায় b = 3 আর সবচেয়ে অগভীর goal গভীরতা d = 6-এ। (ক) শুরুর node বাদ দিয়ে সবচেয়ে খারাপ অবস্থায় BFS (তৈরির সময় goal test) কতগুলো node তৈরি করে? (খ) IDDFS কতগুলো? অনুপাত কত, আর b/(b − 1)-এর সঙ্গে তুলনা করুন। (গ) প্রতি node 1,000 byte ধরে BFS-এর শেষ স্তর আর IDDFS-এর O(bd)-এর memory মোটামুটি কত?

    (ক) 3 + 9 + 27 + 81 + 243 + 729 = 1,092.

    (খ) 6 × 3 + 5 × 9 + 4 × 27 + 3 × 81 + 2 × 243 + 1 × 729 = 18 + 45 + 108 + 243 + 486 + 729 = 1,629. অনুপাত 1,629 ÷ 1,092 ≈ 1.49. b/(b − 1) = 1.5, মানে b ছোট হলে পুনরাবৃত্তির দাম প্রায় ৫০%; b = 10-এর ১১%-এর চেয়ে অনেক বেশি, তবু দ্বিগুণের কম।

    (গ) BFS-এর শেষ স্তরেই ৭২৯টি node একসঙ্গে frontier-এ, প্রায় 729 KB (আর reached set-এ আগের সব মিলিয়ে প্রায় 1.09 MB). IDDFS-এর O(bd) = 3 × 6 = প্রায় ১৮টি node, প্রায় 18 KB; শুধু পথ রাখলে আরও কম। (লেখকের যন্ত্রে Python দিয়ে যোগফল দুটি মেলানো: 1092 আর 1629.)

  4. নকশা ও সমালোচনা: একজন সহকর্মী (বা একটি AI সহকারী) বাজারঘরের ডেলিভারির জন্য “সবচেয়ে কম km-এর পথ” বের করতে নিচের কোড দিয়েছেন। অন্তত পাঁচটি সমস্যা চিহ্নিত করে একটি ভালো নকশা বলুন।
    def shortest(roads, start, goal):
        frontier = [(start, [start], 0)]
        while frontier:
            node, path, km = frontier.pop(0)
            for nxt, d in roads[node]:
                if nxt == goal:
                    return path + [nxt], km + d
                frontier.append((nxt, path + [nxt], km + d))
    • এটি BFS, UCS নয়: frontier FIFO, খরচ ধরে সাজানো নয়; তাই সবচেয়ে কম hop-এর পথ দেয়, কম km-এর নয় (এই পর্বের মানচিত্রে ২২ km).
    • তৈরির সময় goal test: খরচ ধরে সাজালেও এভাবে থামলে প্রথম দেখা দামি পথ ফেরত আসবে; UCS-এ test হবে বের করার সময়।
    • চক্র ঠেকানো নেই: কোনো explored বা best-cost হিসাব নেই, তাই একই এলাকা বারবার ঢোকে, frontier ঘাতের হারে ফুলে ওঠে; goal পৌঁছানো না গেলে loop কখনো শেষ হয় না।
    • list.pop(0) প্রতিবার O(n); queue-র জন্য deque, UCS-এর জন্য heapq.
    • প্রতিটি entry-তে পুরো path-এর কপি: memory অনেক বাড়ায়; একটি parent dict রেখে শেষে পথ বানানো ভালো।
    • start == goal বা পথ না থাকার ক্ষেত্র সামলানো নেই, আর কোনো test বা node-গণনা নেই।

    ভালো নকশা: heapq-এ (g, counter, node), best আর parent dict, বের করার সময় পুরোনো entry বাদ আর goal test, পথ না থাকলে None ফেরত, আর হাতে trace করা একটি ছোট graph (অনুশীলন ১) দিয়ে test.

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

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

ক্রমheapq দিয়ে uniform-cost search-এর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 🏁 শুরুর node-কে g = 0 দিয়ে heap-এ রাখা
  2. ⬆️ heap থেকে সবচেয়ে কম g-এর entry বের করা
  3. 🗑️ entry-টি পুরোনো (আরও সস্তা পথ আগেই জানা) হলে বাদ দেওয়া
  4. 🎯 node-টি goal হলে থামা আর parent ধরে পথ বানানো
  5. 🔁 প্রতিটি প্রতিবেশীর g + খরচ আগের চেয়ে কম হলে best আর parent হালনাগাদ করে push

একটি সমস্যায় branching factor b = 10 আর সবচেয়ে অগভীর goal গভীরতা ৪-এ, সেই স্তরের একেবারে শেষ node. BFS node তৈরির সময় goal test করলে শুরুর node-সহ মোট কতগুলো node তৈরি হয়?

  • 10,000
  • 11,111
  • 1,11,101
  • 40

দুই দিকে চলাচল করা যায় এমন রাস্তার একটি মানচিত্রে explored set বা পথের ওপর কোনো যাচাই ছাড়া (tree search) DFS চালানো হলো। কোনটি ঘটতে পারে?

  • সবসময় সবচেয়ে ছোট পথ পাবে, শুধু একটু ধীরে
  • memory কখনো বাড়বে না, তাই কোনো সমস্যা নেই
  • দুটি পাশাপাশি এলাকার মধ্যে চিরকাল ঘুরতে পারে
  • স্বয়ংক্রিয়ভাবে BFS-এর মতো স্তর ধরে চলবে

b = 10 আর goal-এর গভীরতা d = 5 হলে, শুরুর node বাদ দিয়ে, iterative deepening মোট প্রায় কতগুলো node তৈরি করে, আর তা BFS-এর তুলনায় কত?

  • 1,23,450; BFS-এর চেয়ে প্রায় ১১% বেশি
  • 2,22,220; BFS-এর দ্বিগুণ
  • 1,00,000; BFS-এর চেয়ে কম
  • 5,55,550; BFS-এর পাঁচ গুণ

একটি graph-এর রাস্তা আর খরচ: S–A 1, S–B 5, A–B 1, A–G 6, B–G 1. শুরু S, goal G. Uniform-cost search কোন পথ আর খরচ দেবে?

  • S → A → G, খরচ 7
  • S → B → G, খরচ 6
  • S → G, খরচ 1
  • S → A → B → G, খরচ 3

Uniform-cost search-এ goal test node তৈরির সময় না করে, priority queue থেকে বের করার (expand-এর) সময় কেন করা হয়?

  • এতে কম node expand হয়, তাই দ্রুত
  • প্রথম যে পথে goal তৈরি হয়, তা সবচেয়ে সস্তা নাও হতে পারে; পরে সস্তা পথ আসতে পারে
  • heapq তৈরির সময় goal চিনতে পারে না
  • BFS-ও সবসময় একই নিয়ম মানে, এটি শুধু প্রথা

b = 10 আর goal-এর গভীরতা d = 6. দুই দিক থেকে BFS চালালে (bidirectional) মোটামুটি কতগুলো node লাগে, এক দিকের BFS-এর তুলনায়?

  • 5 লাখ বনাম 10 লাখ
  • 20 লাখ বনাম 10 লাখ
  • প্রায় 2,000 বনাম প্রায় 10 লাখ
  • 60 বনাম 10 লাখ

শব্দকোষ

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

মনে রাখুন

  1. অন্ধ search goal-এর দূরত্বের অনুমান জানে না; পদ্ধতিগুলোর পার্থক্য শুধু frontier-এর নিয়মে: FIFO (BFS), LIFO (DFS), সবচেয়ে কম g (UCS). তুলনা চার মাপে: complete, optimal, time, space.
  2. BFS সবচেয়ে কম ধাপের পথ দেয়, সমান খরচে optimal; time আর space O(bd), আর memory আগে ফুরায়। BFS-এ তৈরির সময় goal test চলে আর একটি পুরো স্তর বাঁচায়।
  3. DFS-এর space O(bm), কিন্তু optimal নয়, আর চক্র বা অসীম শাখায় complete নয়। Iterative deepening সীমা ০, ১, ২, … করে বাড়িয়ে BFS-এর পথ দেয় O(bd) memory-তে; b = 10-এ বাড়তি কাজ প্রায় ১১%.
  4. UCS heapq-এ g ধরে চলে, goal test expand-এর সময়, খরচ ঋণাত্মক না হলে optimal; এটি Dijkstra-র দরকার-মতো রূপ। মানচিত্রে BFS ২ hop / ২২ km, UCS ৩ hop / ১৬ km.
  5. Bidirectional প্রায় 2·bd/2-এ নামায়, যদি goal জানা আর পেছনে হাঁটা যায়। কোনটি ভালো, তা দাবি নয়, node গুনে দেখুন; bd সাধ্যের বাইরে হলে অনুমান লাগবে।

পরের পর্বে: Heuristic আর A*. UCS-এর সঙ্গে “goal আর কত দূরে” একটি অনুমান যোগ করলে কী হয়: একই মানচিত্রে অনেক কম node খুলে সবচেয়ে সস্তা পথ, আর কখন সেই অনুমান বিশ্বাস করা যায়।

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

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