পাঠ ৩০ · ৫৭-এর মধ্যে · মডিউল ৭

BFS/DFS কমপ্লেক্সিটি ও সঠিকতা

BFS/DFS complexity and correctness
১০ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • এই কোর্স জুড়ে ব্যবহৃত গ্রাফ রিপ্রেজেন্টেশন কনভেনশন (dict-of-lists adjacency list)
  • BFS/DFS-এর $\Theta(V+E)$ কমপ্লেক্সিটির সম্পূর্ণ ডেরিভেশন
  • BFS-এর "নন-ডিক্রিজিং দূরত্ব" সঠিকতা প্রোপার্টির একটি সংক্ষিপ্ত ইনডাক্টিভ প্রমাণ
  • একটি সত্যিকারের, স্বাধীনভাবে লেখা ব্রুট-ফোর্স যাচাই যা BFS-এর গণনা করা দূরত্বকে ক্রস-চেক করে

১ · গ্রাফ রিপ্রেজেন্টেশন ও কমপ্লেক্সিটি ডেরিভেশন

এই মডিউল জুড়ে আমরা একটি গ্রাফ $G=(V,E)$-কে একটি adjacency list হিসেবে প্রতিনিধিত্ব করব — Python-এ এটি একটি dict, যেখানে প্রতিটি key একটি ভার্টেক্স এবং value হলো তার প্রতিবেশী ভার্টেক্সগুলোর একটি লিস্ট। BFS ও DFS উভয়ই এই রিপ্রেজেন্টেশনে একই মৌলিক কাঠামো অনুসরণ করে: প্রতিটি ভার্টেক্স ঠিক একবার "আবিষ্কৃত" (discovered) হয় এবং তারপর তার সম্পূর্ণ প্রতিবেশী লিস্ট ঠিক একবার স্ক্যান করা হয়।

যেহেতু প্রতিটি ভার্টেক্স $O(1)$ কাজে প্রসেস হয় (queue/stack push-pop) এবং প্রতিটি এজ ঠিক একবার (ডাইরেক্টেড গ্রাফে) স্ক্যান করা হয় (adjacency list-এ প্রতিটি এজ ঠিক একটি ভার্টেক্সের লিস্টে থাকে), মোট কাজ:

$$T(V,E) = \underbrace{O(V)}_{\text{প্রতিটি ভার্টেক্স আবিষ্কার + প্রসেস}} + \underbrace{O(E)}_{\text{প্রতিটি এজ স্ক্যান}} = \Theta(V+E)$$

লক্ষ্য করুন এটি adjacency matrix রিপ্রেজেন্টেশনের $\Theta(V^2)$ কমপ্লেক্সিটির চেয়ে ভিন্ন — স্পার্স গ্রাফে ($E \ll V^2$, যেমন বাস্তব-জীবনের নেটওয়ার্ক), adjacency list উল্লেখযোগ্যভাবে দ্রুত, কারণ আমরা অস্তিত্বহীন এজের জন্য সময় ব্যয় করি না। এই কোর্সে M7-এর সব অ্যালগরিদম adjacency list ব্যবহার করবে (কোনো তৃতীয়-পক্ষের গ্রাফ লাইব্রেরি নয় — শুধু plain dict/list এবং প্রয়োজনে heapq)।

BFS বনাম DFS — একই কমপ্লেক্সিটি, ভিন্ন তথ্য

BFS ও DFS উভয়েরই কমপ্লেক্সিটি $\Theta(V+E)$ — পার্থক্য হলো ডেটা স্ট্রাকচারে: BFS একটি queue (FIFO) ব্যবহার করে, DFS একটি stack (LIFO, প্রায়শই রিকার্সন কল স্ট্যাক হিসেবে) ব্যবহার করে। এই একক পার্থক্যই তাদের সম্পূর্ণ ভিন্ন তথ্য দেয় — BFS দূরত্ব-ভিত্তিক লেয়ার তৈরি করে (এই পাঠের বিষয়), আর DFS "ফিনিশ টাইম" তৈরি করে যা L31 (টপোলজিক্যাল সর্ট) ও L32 (স্ট্রংলি কানেক্টেড কম্পোনেন্ট)-এ সরাসরি ব্যবহৃত হবে।

২ · BFS-এর সঠিকতা প্রোপার্টি — নন-ডিক্রিজিং দূরত্বের ক্রম

ধরি $G=(V,E)$ একটি আনওয়েটেড গ্রাফ (ডাইরেক্টেড বা আনডাইরেক্টেড, যেকোনোটি হতে পারে) এবং $d(s,v)$ হলো সোর্স $s$ থেকে $v$-এ প্রকৃত শর্টেস্ট-পাথ দূরত্ব (এজ সংখ্যায়)। BFS-এর মূল সঠিকতা দাবি:

$$\textbf{দাবি: } \text{BFS ভার্টেক্সগুলোকে dequeue করে এমন ক্রমে } v_1, v_2, \ldots \text{ যেখানে } d(s,v_1) \le d(s,v_2) \le \cdots$$ এবং প্রতিটি $v$-এর জন্য BFS-এর নির্ণয় করা dist[v] ঠিক $d(s,v)$-এর সমান।

s dist=0 a dist=1 b dist=1 c dist=2 d dist=3 লেয়ার ০ লেয়ার ১ লেয়ার ২ লেয়ার ৩
BFS queue-তে ভার্টেক্সগুলো লেয়ার অনুযায়ী FIFO ক্রমে জমা হয় — লেয়ার $i$-এর সব ভার্টেক্স dequeue হওয়ার আগেই আবিষ্কৃত হয়ে যায়, এবং লেয়ার $i{+}1$-এর কোনো ভার্টেক্স dequeue হওয়ার আগে লেয়ার $i$-এর সব ভার্টেক্স dequeue হয়ে যায়।

প্রমাণ (ইনডাকশন অন লেয়ার সংখ্যা $i$): ধরি $L_i = \{v \in V : d(s,v) = i\}$।

Base case ($i=0$): $L_0 = \{s\}$। BFS শুরুতেই dist[s] = 0 নির্ধারণ করে এবং $s$-কে queue-তে রাখে — সঠিক।

Inductive hypothesis: ধরি নিলাম $L_0, L_1, \ldots, L_i$-এর প্রতিটি ভার্টেক্স সঠিক দূরত্বসহ আবিষ্কৃত হয়েছে এবং queue-তে ঠিক এই ক্রমে (লেয়ার অনুযায়ী, প্রতি লেয়ারের ভিতরে যেকোনো ক্রমে) সাজানো আছে — অর্থাৎ $L_i$-এর সব ভার্টেক্স $L_{i+1}$-এর যেকোনো ভার্টেক্সের আগে queue-তে ছিল।

Inductive step: যখন BFS $L_i$-এর কোনো ভার্টেক্স $u$-কে dequeue করে তার প্রতিবেশী $v$ পরীক্ষা করে, দুটো সম্ভাবনা: (ক) $v$ ইতিমধ্যে আবিষ্কৃত (visited) — তাহলে কিছু করার নেই। (খ) $v$ নতুন — যেহেতু গ্রাফের একটি এজ দূরত্বকে সর্বোচ্চ ১ পরিবর্তন করতে পারে ($|d(s,u) - d(s,v)| \le 1$ যেকোনো এজ $(u,v)$-এর জন্য), এবং $v$ যদি $L_0,\ldots,L_i$-এর কোনোটাতে থাকত তবে ইনডাক্টিভ হাইপোথিসিস অনুযায়ী এটি ইতিমধ্যে আবিষ্কৃত হয়ে যেত (কারণ $L_i$ পর্যন্ত সব ভার্টেক্স ইতিমধ্যে queue-তে ঢুকে গেছে $u$ dequeue হওয়ার আগেই) — তাই $v$ অবশ্যই $L_{i+1}$-এ, অর্থাৎ $d(s,v) = i+1$। BFS ঠিক এই মুহূর্তেই dist[v] = dist[u] + 1 = i+1 নির্ধারণ করে এবং $v$-কে queue-তে রাখে — এবং যেহেতু queue FIFO, $L_i$-এর সব ভার্টেক্স $L_{i+1}$-এর সব ভার্টেক্সের আগে enqueue হয়েছিল, তাই তারা আগেই dequeue হবে। এভাবে ইনডাকশন সম্পূর্ণ হয়। $\blacksquare$

এই প্রমাণের মূল অন্তর্দৃষ্টি হলো — queue-এর FIFO ধর্ম এবং "একটি এজ দূরত্ব ১-এর বেশি পরিবর্তন করতে পারে না" এই দুটো তথ্য একসাথে গ্যারান্টি দেয় যে dequeue ক্রম কখনো দূরত্বে "পিছিয়ে" যেতে পারে না।

৩ · একটি সত্যিকারের যাচাই — BFS বনাম স্বাধীন ব্রুট-ফোর্স

উপরের প্রমাণটি বিশ্বাসযোগ্য হলেও, আমরা এটি একটি সম্পূর্ণ স্বাধীন পদ্ধতিতে কম্পিউটেশনালিও যাচাই করব — অর্থাৎ এমন একটি ফাংশন লিখব যা BFS ব্যবহার করে না, বরং সোর্স থেকে প্রতিটি ভার্টেক্সে যাওয়ার সব সিম্পল পাথ (কোনো ভার্টেক্স পুনরাবৃত্তি ছাড়া) রিকার্সিভভাবে এক্সপ্লোর করে সবচেয়ে ছোট পাথের দৈর্ঘ্য বের করে। যেহেতু একটি আনওয়েটেড গ্রাফে শর্টেস্ট পাথ কখনোই কোনো ভার্টেক্স পুনরাবৃত্তি করে না (তা করলে সেই অংশ কেটে ফেলে আরও ছোট পাথ পাওয়া যেত), তাই এই ব্রুট-ফোর্স সার্চ থেকে পাওয়া মিনিমাম দৈর্ঘ্যই প্রকৃত $d(s,v)$-এর সমান হতে বাধ্য — এটি BFS-এর নিজস্ব লজিকের উপর নির্ভর করে না, তাই একটি জেনুইন, স্বাধীন ক্রস-চেক।

Python
from collections import deque

def bfs_distances(graph, source):
    # প্রমিত BFS: queue (FIFO) ব্যবহার করে দূরত্ব লেয়ার-বাই-লেয়ার নির্ণয়
    dist = {source: 0}
    queue = deque([source])
    dequeue_order = []
    while queue:
        u = queue.popleft()
        dequeue_order.append(u)
        for v in graph.get(u, []):
            if v not in dist:
                dist[v] = dist[u] + 1
                queue.append(v)
    return dist, dequeue_order

def brute_force_distances(graph, source):
    # সম্পূর্ণ স্বাধীন পদ্ধতি: BFS ব্যবহার না করে, সোর্স থেকে প্রতিটি ভার্টেক্সে
    # যাওয়ার সব সিম্পল পাথ রিকার্সিভভাবে এক্সপ্লোর করে সবচেয়ে ছোট দৈর্ঘ্য বের করা
    best = {source: 0}
    def explore(u, visited, depth):
        for v in graph.get(u, []):
            if v not in visited:
                if v not in best or depth + 1 < best[v]:
                    best[v] = depth + 1
                visited.add(v)
                explore(v, visited, depth + 1)
                visited.remove(v)
    explore(source, {source}, 0)
    return best

# টেস্ট গ্রাফ (আনডাইরেক্টেড, adjacency list হিসেবে দুই দিকেই এজ রাখা হয়েছে)
graph = {
    0: [1, 2],
    1: [0, 3],
    2: [0, 3, 4],
    3: [1, 2, 5],
    4: [2, 5],
    5: [3, 4, 6],
    6: [5],
}

source = 0
dist_bfs, order = bfs_distances(graph, source)
dist_brute = brute_force_distances(graph, source)

print(f"{'vertex':>6} | {'BFS dist':>8} | {'brute-force dist':>17} | match")
all_match = True
for v in sorted(graph):
    match = dist_bfs.get(v) == dist_brute.get(v)
    all_match = all_match and match
    print(f"{v:>6} | {dist_bfs.get(v):>8} | {dist_brute.get(v):>17} | {match}")

print("\nBFS dequeue order:      ", order)
print("দূরত্ব dequeue ক্রমে:      ", [dist_bfs[v] for v in order])
is_nondecreasing = all(
    dist_bfs[order[i]] <= dist_bfs[order[i + 1]] for i in range(len(order) - 1)
)
print("dequeue ক্রম নন-ডিক্রিজিং:  ", is_nondecreasing)

print("\nসব ভার্টেক্সে BFS ও ব্রুট-ফোর্স দূরত্ব একমত:", all_match)
assert all_match, "BFS distances did not match the independent brute-force check!"
assert is_nondecreasing, "BFS dequeue order was not non-decreasing in distance!"
print("যাচাই সম্পন্ন — প্রমাণিত সঠিকতা প্রোপার্টি কোডেও নিশ্চিত হলো।")

    
brute_force_distances ফাংশনটি BFS-এর মতো queue ব্যবহার করে না — এটি DFS-স্টাইলে সব সিম্পল পাথ এক্সপ্লোর করে, যা এক্সপোনেনশিয়াল সময় নিতে পারে (তাই শুধু ছোট গ্রাফে ব্যবহারযোগ্য) কিন্তু ঠিক সেই কারণেই এটি একটি বিশ্বাসযোগ্য, স্বাধীন গ্রাউন্ড-ট্রুথ — এটি BFS-এর কোনো ধারণা (queue, লেয়ার) ব্যবহার করে না, শুধু সংজ্ঞা অনুযায়ী "সবচেয়ে ছোট পাথ" সরাসরি খুঁজে বের করে।
মূল কথা · Key takeaway

BFS-এর "দূরত্বের নন-ডিক্রিজিং ক্রম" প্রোপার্টিটাই ওজনবিহীন শর্টেস্ট-পাথ সমস্যায় BFS-কে সঠিক করে তোলে — এবং এই একই প্রোপার্টি M7-এর পরবর্তী পাঠগুলোতে বারবার ফিরে আসবে (ওয়েটেড গ্রাফে Dijkstra-র জন্য L33-এ একটি অনুরূপ কিন্তু কঠোরতর দাবি লাগবে, কারণ ওজনযুক্ত এজে "distance" আর শুধু hop-count থাকে না)।

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

প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।

প্র ০১ প্রমাণে বলা হয়েছে "একটি এজ দূরত্বকে সর্বোচ্চ ১ পরিবর্তন করতে পারে" — এটি কেন সত্য?

যদি $(u,v)$ একটি এজ হয়, তাহলে $s$ থেকে $u$-এ যাওয়ার শর্টেস্ট পাথের শেষে $v$-তে যাওয়ার এই এজটি যোগ করলে $s$ থেকে $v$-এ একটি (হয়তো অ-শর্টেস্ট) পাথ পাওয়া যায় যার দৈর্ঘ্য $d(s,u)+1$ — তাই $d(s,v) \le d(s,u)+1$। একইভাবে বিপরীত দিকেও (যদি এজটি আনডাইরেক্টেড বা $v \to u$ এজও থাকে) $d(s,u) \le d(s,v)+1$। এই দুটো একসাথে দেয় $|d(s,u)-d(s,v)| \le 1$।

প্র ০২ DFS দিয়ে BFS-এর মতো শর্টেস্ট-পাথ দূরত্ব বের করা যায় না কেন?

DFS একটি stack (বা রিকার্সন) ব্যবহার করে, যা "সবচেয়ে গভীরে যাও, তারপর ব্যাকট্র্যাক করো" — এটি লেয়ার-বাই-লেয়ার এক্সপ্লোর করে না। ফলে DFS একটি ভার্টেক্সে পৌঁছানোর প্রথম পাথ খুঁজে পায়, কিন্তু সেটি শর্টেস্ট পাথ হওয়ার কোনো গ্যারান্টি নেই — DFS অনেক দূরের একটি শাখায় গভীরে ঢুকে যেতে পারে, একটি কাছের ভার্টেক্স আবিষ্কার করার অনেক আগেই।

প্র ০৩ উপরের টেস্ট গ্রাফে যদি ভার্টেক্স ৬-এর সাথে সরাসরি ভার্টেক্স ০-এর একটি এজ যোগ করা হতো, তাহলে dist[6] কী হতো?

dist[6] হয়ে যেত ১ (সরাসরি এজ, তাই দূরত্ব ১), বর্তমান ৪-এর বদলে। এটি লক্ষণীয় যে BFS/শর্টেস্ট-পাথ দূরত্ব গ্রাফের গঠনের উপর সম্পূর্ণ নির্ভরশীল — একটি নতুন "শর্টকাট" এজ পুরো দূরত্ব বিন্যাস পরিবর্তন করে দিতে পারে, এবং BFS/ব্রুট-ফোর্স উভয়েই (আবার রান করলে) এই নতুন মান একমত হয়ে দেখাবে।

অনুশীলন

  1. চিন্তা করুন: উপরের টেস্ট গ্রাফে সোর্স ভার্টেক্স ০-এর বদলে ৬ ব্যবহার করলে dist[0] কী হবে বলে আপনার ধারণা?

    গ্রাফটি আনডাইরেক্টেড (প্রতিটি এজ দুই দিকেই যোগ করা আছে), তাই শর্টেস্ট পাথের দৈর্ঘ্য দিক-নিরপেক্ষ — source=6 দিয়ে চালালে dist[0] একই মান ৪ দেখাবে যা source=0 দিয়ে চালানো অবস্থায় dist[6]-এর মান ছিল।

  2. পরীক্ষা করুন: কোড সেলে source = 6 সেট করে Run চেপে আপনার অনুমান যাচাই করুন, এবং লক্ষ্য করুন all_match ও is_nondecreasing উভয়েই এখনও True থাকে কি না।

    হ্যাঁ — উভয় assert-ই পাস করবে, কারণ সঠিকতার প্রমাণটি কোনো নির্দিষ্ট সোর্স ভার্টেক্সের উপর নির্ভর করে না; এটি যেকোনো সোর্স থেকে শুরু করা BFS-এর জন্য সমানভাবে প্রযোজ্য।

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

  • কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৭টি পাঠ টপোলজিক্যাল সর্ট, SCC, শর্টেস্ট পাথ, ম্যাক্স ফ্লো, ব্যাকট্র্যাকিং, স্ট্রিং অ্যালগরিদম, অ্যামর্টাইজড অ্যানালাইসিস, NP-কমপ্লিটনেস ও অ্যাপ্রক্সিমেশন — বাকি পাঠগুলো শীঘ্রই যুক্ত হবে।
  • Data Structures & Algorithms কোর্স সহোদর কোর্স BFS/DFS-এর মৌলিক ইমপ্লিমেন্টেশন ও ব্যবহারিক প্রয়োগ (গ্রাফ ট্রাভার্সাল, কানেক্টিভিটি চেক) সেই কোর্সে বিস্তারিত কভার করা হয়েছে।
  • সব Courses দেখুন ABCL TECH C, C++, Python, Java, JavaScript, DSA, DBMS, Discrete Mathematics, System Design, Cybersecurity, Cloud Computing & DevOps, Computer Networks, Operating Systems, Computer Architecture, Programming Languages & Compiler Design, Software Engineering & Git, Theory of Computation, Engineering Economics, Full-Stack Web Frameworks, Mobile App Development, Ethics in Computing & AI Safety, Software Testing & Quality Assurance ও Design and Analysis of Algorithms — সব এক জায়গায়।
পূর্ববর্তী পাঠ
এডিট ডিস্ট্যান্স ও অপটিমাল সাবস্ট্রাকচার