Tìm kiếm theo chiều rộng (BFS)
Cập nhật lần cuối
Tìm kiếm theo chiều rộng (breadth-first search) duyệt đồ thị theo từng tầng. Bắt đầu từ một đỉnh nguồn, thuật toán thăm tất cả các đỉnh kề trực tiếp trước, rồi đến tất cả các đỉnh kề chưa thăm của chúng, cứ thế lan rộng ra theo từng vòng có khoảng cách tăng dần. Nhấn Phát ở trên để xem thuật toán tỏa ra từ đỉnh bắt đầu, mỗi lần một tầng.
BFS dùng hàng đợi vào trước ra trước (FIFO), chính điều này bảo đảm thứ tự duyệt theo từng tầng. Vì đến các đỉnh theo thứ tự số bước nhảy, BFS tìm được đường đi ngắn nhất (ít cạnh nhất) trên đồ thị không trọng số. Thuật toán thăm mỗi đỉnh và mỗi cạnh một lần, nên chạy trong thời gian O(V + E).
Độ phức tạp thời gian và không gian
| Tiêu chí | Độ phức tạp | Ghi chú |
|---|---|---|
| Thời gian | O(V + E) | Mỗi đỉnh và mỗi cạnh được thăm một lần |
| Không gian | O(V) | Hàng đợi + tập đã thăm, xấu nhất chứa mọi đỉnh |
| Cách duyệt | Theo từng tầng | Đỉnh gần nhất trước, theo từng vòng |
| Đường đi ngắn nhất | Có (không trọng số) | Đến mỗi đỉnh bằng ít cạnh nhất |
Từng bước thực hiện
| Bước | Điều gì xảy ra |
|---|---|
| 1 | Đưa đỉnh nguồn vào hàng đợi. |
| 2 | Lấy đỉnh ở đầu hàng đợi ra và đánh dấu đã thăm. |
| 3 | Xem từng đỉnh kề của nó. |
| 4 | Đưa vào hàng đợi mọi đỉnh kề chưa được thăm hoặc chưa nằm trong hàng đợi. |
| 5 | Lặp lại cho đến khi hàng đợi rỗng. |
Ví dụ minh họa
Duyệt đồ thị này từ đỉnh 0, với các cạnh 0-1, 0-2, 1-3, 2-3, 2-4:
| Bước | Đã thăm | Hàng đợi (biên) |
|---|---|---|
| Bắt đầu | {} | [0] |
Lấy ra 0 | {0} | [1, 2] |
Lấy ra 1 | {0, 1} | [2, 3] |
Lấy ra 2 | {0, 1, 2} | [3, 4] |
Lấy ra 3 | {0, 1, 2, 3} | [4] |
Lấy ra 4 | {0, 1, 2, 3, 4} | [] (xong) |
Khi nào nên dùng BFS
| Nên dùng khi | Tránh dùng khi |
|---|---|
| Bạn cần đường đi ngắn nhất trên đồ thị không trọng số | Các cạnh có trọng số: hãy dùng Dijkstra |
| Đích nhiều khả năng ở gần đỉnh nguồn | Đồ thị rất rộng: hàng đợi có thể phải chứa một biên khổng lồ |
| Bạn muốn duyệt đồ thị theo thứ tự từng tầng | Bạn chỉ cần đến được một đỉnh bất kỳ, khi đó DFS tốn ít bộ nhớ hơn |
| Bạn đang tìm các thành phần liên thông hoặc tuyến đường ít bước nhất | Bạn cần sắp xếp topo hoặc phát hiện chu trình: DFS phù hợp hơn |
So sánh BFS và DFS
Cả hai đều duyệt mọi đỉnh trong O(V + E), nhưng thứ tự và cấu trúc dữ liệu khác nhau. Xem mô phỏng tìm kiếm theo chiều sâu để so sánh hai thuật toán cạnh nhau.
| Khía cạnh | BFS | DFS |
|---|---|---|
| Cấu trúc dữ liệu | Hàng đợi (FIFO) | Ngăn xếp / đệ quy |
| Thứ tự | Theo từng tầng (gần nhất trước) | Đi sâu theo một nhánh, rồi quay lui |
| Đường đi ngắn nhất (không trọng số) | Có, ít cạnh nhất | Không, không được bảo đảm |
| Bộ nhớ trên đồ thị rộng | Cao, biên có thể rất lớn | Thấp, mỗi lần một đường đi |
| Phù hợp nhất cho | Ít bước nhảy nhất, thành phần liên thông | Phát hiện chu trình, sắp xếp topo, quay lui |
Code Breadth-First Search
Một cách cài đặt Breadth-First Search gọn gàng, chạy được ngay bằng Python, JavaScript, Java, C++, C. Chọn một ngôn ngữ, sao chép code hoặc mở sẵn trong Coddy Playground.
Code Breadth-First Search bằng Python
1from collections import deque2
3
4def bfs(graph, start):5 visited = {start}6 queue = deque([start])7 order = []8 while queue:9 node = queue.popleft()10 order.append(node)11 for neighbor in graph[node]:12 if neighbor not in visited:13 visited.add(neighbor) # mark on enqueue, not dequeue14 queue.append(neighbor)15 return order16
17
18graph = {19 "A": ["B", "C"],20 "B": ["D", "E"],21 "C": ["F"],22 "D": [],23 "E": ["F"],24 "F": [],25}26
27print("BFS order:", " -> ".join(bfs(graph, "A")))Code Breadth-First Search bằng JavaScript
1const graph = {2 A: ["B", "C"],3 B: ["D", "E"],4 C: ["F"],5 D: [],6 E: ["F"],7 F: [],8};9
10function bfs(start) {11 const visited = new Set([start]);12 const queue = [start];13 const order = [];14 while (queue.length > 0) {15 const node = queue.shift(); // Dequeue the oldest node16 order.push(node);17 for (const next of graph[node]) {18 if (!visited.has(next)) {19 visited.add(next);20 queue.push(next);21 }22 }23 }24 return order;25}26
27console.log("BFS from A:", bfs("A").join(" -> "));Code Breadth-First Search bằng Java
1import java.util.ArrayDeque;2import java.util.List;3import java.util.Queue;4
5public class Main {6 public static void main(String[] args) {7 List<List<Integer>> adj = List.of(8 List.of(1, 2), // neighbors of 09 List.of(0, 3, 4), // neighbors of 110 List.of(0, 5),11 List.of(1),12 List.of(1, 5),13 List.of(2, 4)14 );15 boolean[] visited = new boolean[adj.size()];16 Queue<Integer> queue = new ArrayDeque<>();17 visited[0] = true;18 queue.add(0);19
20 StringBuilder order = new StringBuilder("BFS from 0:");21 while (!queue.isEmpty()) {22 int node = queue.poll();23 order.append(" ").append(node);24 // Mark neighbors when enqueued so nothing enters twice25 for (int next : adj.get(node)) {26 if (!visited[next]) {27 visited[next] = true;28 queue.add(next);29 }30 }31 }32 System.out.println(order);33 }34}Code Breadth-First Search bằng C++
1#include <iostream>2#include <queue>3#include <vector>4
5void bfs(int start, const std::vector<std::vector<int>>& adj) {6 std::vector<bool> visited(adj.size(), false);7 std::queue<int> frontier;8 visited[start] = true;9 frontier.push(start);10 // Visit nodes level by level11 while (!frontier.empty()) {12 int node = frontier.front();13 frontier.pop();14 std::cout << node << " ";15 for (int next : adj[node]) {16 if (!visited[next]) {17 visited[next] = true;18 frontier.push(next);19 }20 }21 }22}23
24int main() {25 // 6-node undirected graph as an adjacency list26 std::vector<std::vector<int>> adj = {27 {1, 2}, // 028 {0, 3, 4}, // 129 {0, 5}, // 230 {1}, // 331 {1, 5}, // 432 {2, 4}, // 533 };34 std::cout << "BFS from node 0: ";35 bfs(0, adj);36 std::cout << "\n";37 return 0;38}Code Breadth-First Search bằng C
1#include <stdbool.h>2#include <stdio.h>3
4#define N 65
6void bfs(int start, const int adj[N][N]) {7 bool visited[N] = {false};8 int queue[N]; // fixed-size queue: head chases tail9 int head = 0, tail = 0;10 visited[start] = true;11 queue[tail++] = start;12 // Visit nodes level by level13 while (head < tail) {14 int node = queue[head++];15 printf("%d ", node);16 for (int next = 0; next < N; next++) {17 if (adj[node][next] && !visited[next]) {18 visited[next] = true;19 queue[tail++] = next;20 }21 }22 }23}24
25int main(void) {26 // 6-node undirected graph as an adjacency matrix27 int adj[N][N] = {28 {0, 1, 1, 0, 0, 0},29 {1, 0, 0, 1, 1, 0},30 {1, 0, 0, 0, 0, 1},31 {0, 1, 0, 0, 0, 0},32 {0, 1, 0, 0, 0, 1},33 {0, 0, 1, 0, 1, 0},34 };35 printf("BFS from node 0: ");36 bfs(0, adj);37 printf("\n");38 return 0;39}Câu hỏi thường gặp về tìm kiếm theo chiều rộng
Độ phức tạp thời gian của BFS là bao nhiêu?
O(V + E), với V là số đỉnh và E là số cạnh, vì thuật toán thăm mỗi đỉnh một lần và xét mỗi cạnh một lần. BFS dùng O(V) bộ nhớ cho hàng đợi và tập đã thăm.BFS có tìm được đường đi ngắn nhất không?
BFS và DFS khác nhau như thế nào?
Khi nào nên dùng BFS thay cho thuật toán Dijkstra?
O(V + E) mà không cần hàng đợi ưu tiên. Thuật toán Dijkstra cần thiết khi các cạnh có trọng số khác nhau; chạy BFS thông thường trên đồ thị có trọng số chỉ cho đường đi ít bước nhất, không phải đường đi có chi phí thấp nhất.Vì sao BFS cần tập đã thăm?
Đồ thị có thể chứa chu trình, nên nếu không có tập đã thăm, BFS sẽ đưa cùng một đỉnh vào hàng đợi nhiều lần và lặp vô hạn. Đánh dấu một đỉnh ngay khi đưa nó vào hàng đợi lần đầu (không phải khi lấy ra) còn giúp tránh việc cùng một đỉnh bị các đỉnh kề khác nhau thêm vào hàng đợi hai lần.
Nên đánh dấu đã thăm khi đưa đỉnh vào hay khi lấy đỉnh ra khỏi hàng đợi?
Đánh dấu khi đưa vào hàng đợi. Nếu đợi đến lúc lấy ra, một đỉnh có thể bị các đỉnh kề khác nhau thêm vào hàng đợi nhiều lần trước khi được xử lý, gây lãng phí bộ nhớ và thời gian. Đánh dấu khi đưa vào bảo đảm mỗi đỉnh vào hàng đợi đúng một lần.