STL + BFS + Grid Traversal

🚍 Queue – Hàng đợi FIFO

Queue xử lý phần tử theo nguyên tắc vào trước, ra trước. Từ mô phỏng hàng chờ, Queue trở thành cấu trúc trung tâm của BFS, tìm đường đi ngắn nhất trên đồ thị không trọng số, thuật toán loang và BFS đa nguồn.

FIFOFirst In, First Out
push/popO(1) mỗi thao tác
BFSDuyệt theo từng lớp
Shortest pathĐồ thị không trọng số

🎯 Mục tiêu bài học

Cần hiểu

  • Nguyên tắc FIFO.
  • Các thao tác push, front, pop, empty, size.
  • Vì sao BFS phải đánh dấu khi đưa vào Queue.
  • Vì sao BFS tìm đường ngắn nhất trong đồ thị không trọng số.
  • Cách mở rộng sang lưới và BFS đa nguồn.

Dấu hiệu nhận biết

  • Xử lý theo thứ tự đến trước.
  • Duyệt theo tầng hoặc theo khoảng cách.
  • Lan truyền từng bước/phút.
  • Đường đi ngắn nhất khi mọi cạnh có cùng trọng số.
  • Nhiều nguồn lan đồng thời.

1. Đặt vấn đề

Bài toán mở đầu 1 – hàng chờ.
Có q người lần lượt đến quầy. Mỗi thao tác là:
  • ADD x: người x vào cuối hàng;
  • SERVE: phục vụ người ở đầu hàng;
  • FRONT: xem ai đang đứng đầu.

Cấu trúc cần bảo đảm người đến trước được phục vụ trước. Stack không phù hợp vì Stack lấy người đến sau nhất ra trước.

Queue: push ở cuối, pop ở đầu → FIFO
Bài toán mở đầu 2 – tìm đường.
Cho đồ thị không trọng số. Hãy tìm số cạnh ít nhất từ đỉnh S đến mọi đỉnh khác.

Nếu duyệt theo thứ tự tùy ý, ta có thể tới một đỉnh bằng đường dài trước. Ta cần mở rộng các đỉnh theo từng lớp khoảng cách:

  • Lớp 0: đỉnh S.
  • Lớp 1: các đỉnh kề S.
  • Lớp 2: các đỉnh chưa thăm kề lớp 1.
Quan sát: Queue giữ đúng thứ tự các lớp: đỉnh được phát hiện trước sẽ được mở rộng trước.

2. Queue cơ bản trong C++

Thao tác chính

  • q.push(x): thêm cuối.
  • q.front(): xem đầu.
  • q.back(): xem cuối.
  • q.pop(): xóa đầu.
  • q.empty(): kiểm tra rỗng.
  • q.size(): số phần tử.

Độ phức tạp

  • push: O(1).
  • front/back: O(1).
  • pop: O(1).
  • Không hỗ trợ truy cập q[i].
  • Muốn duyệt toàn bộ thường phải pop hoặc sao chép Queue.
Lưu ý: q.pop() không trả về phần tử bị xóa. Muốn lấy giá trị, phải đọc q.front() trước rồi mới q.pop().

3. Mô phỏng Queue và highlight code

queue q;
q.push(x);
if (!q.empty()) cout << q.front();
if (!q.empty()) q.pop();
cout << q.size();

4. BFS trên đồ thị

Queue BFS

4.1. Quy trình chuẩn

  1. Đánh dấu nguồn đã thăm.
  2. Đưa nguồn vào Queue.
  3. Lặp: lấy đầu Queue, duyệt các đỉnh kề chưa thăm.
  4. Đánh dấu đỉnh kề ngay khi đưa vào Queue.
Không đánh dấu khi pop. Nếu đợi đến lúc lấy ra mới đánh dấu, một đỉnh có thể bị nhiều đỉnh cha đưa vào Queue nhiều lần.

5. BFS trên lưới – thuật toán loang

Mỗi ô là một đỉnh; hai ô kề nhau tạo thành cạnh nếu được phép di chuyển. Mọi bước đi có cùng chi phí 1 nên BFS tìm được số bước ít nhất.

Queue ô

6. BFS đa nguồn

Bài toán. Nhiều ô bắt đầu có virus. Mỗi phút, virus lan sang bốn ô kề. Tính thời điểm mỗi ô bị nhiễm.

Đưa tất cả nguồn vào Queue ngay từ đầu với khoảng cách 0. BFS sẽ mô phỏng các nguồn lan đồng thời.

Queue ban đầu = tất cả nguồn, không phải chỉ một nguồn

Virus/lửa

Tính thời gian lan đến mọi ô.

Khoảng cách gần nhất

Khoảng cách từ mỗi ô đến nguồn gần nhất.

Nhiều S – nhiều T

Tìm khoảng cách ngắn nhất từ bất kỳ S đến một T.

7. Phân tích thuật toán

Bài toánThời gianBộ nhớGiải thích
q thao tác QueueO(q)O(q)Mỗi thao tác O(1).
BFS đồ thịO(V+E)O(V)Mỗi đỉnh và cạnh được xử lý hữu hạn lần.
BFS lưới n×mO(nm)O(nm)Mỗi ô vào Queue tối đa một lần.
Multi-source BFSO(V+E)O(V)Chỉ khác bước khởi tạo nhiều nguồn.
Giới hạn của BFS: BFS chỉ trực tiếp tìm đường ngắn nhất khi mọi cạnh có cùng trọng số. Cạnh 0/1 dùng 0–1 BFS; trọng số không âm bất kỳ dùng Dijkstra.

8. Code mẫu

#include 
using namespace std;

int main() {
    queue q;

    q.push(10);
    q.push(20);
    q.push(30);

    cout << q.front() << '
'; cout << q.back() << '
'; if (!q.empty()) { int value = q.front(); q.pop(); cout << "Removed: " << value << '
'; } cout << q.size(); }
#include 
using namespace std;

int main() {
    int n, m, source;
    cin >> n >> m >> source;

    vector> graph(n + 1);

    for (int i = 0; i < m; i++) {
        int u, v;
        cin >> u >> v;

        graph[u].push_back(v);
        graph[v].push_back(u);
    }

    vector distance(n + 1, -1);
    vector parent(n + 1, -1);
    queue q;

    distance[source] = 0;
    q.push(source);

    while (!q.empty()) {
        int u = q.front();
        q.pop();

        for (int v : graph[u]) {
            if (distance[v] != -1) continue;

            distance[v] = distance[u] + 1;
            parent[v] = u;
            q.push(v);
        }
    }
}
#include 
using namespace std;

int dx[4] = {-1, 1, 0, 0};
int dy[4] = {0, 0, -1, 1};

int main() {
    int n, m;
    cin >> n >> m;

    vector grid(n);
    for (string &row : grid) cin >> row;

    vector> distance(
        n,
        vector(m, -1)
    );

    queue> q;

    if (grid[0][0] != '#') {
        distance[0][0] = 0;
        q.push({0, 0});
    }

    while (!q.empty()) {
        auto [x, y] = q.front();
        q.pop();

        for (int direction = 0; direction < 4; direction++) {
            int nx = x + dx[direction];
            int ny = y + dy[direction];

            if (nx < 0 || nx >= n || ny < 0 || ny >= m) {
                continue;
            }

            if (grid[nx][ny] == '#') continue;
            if (distance[nx][ny] != -1) continue;

            distance[nx][ny] = distance[x][y] + 1;
            q.push({nx, ny});
        }
    }

    cout << distance[n - 1][m - 1];
}
queue> q;
vector> distance(n, vector(m, -1));

for (int i = 0; i < n; i++) {
    for (int j = 0; j < m; j++) {
        if (grid[i][j] == 'S') {
            distance[i][j] = 0;
            q.push({i, j});
        }
    }
}

while (!q.empty()) {
    auto [x, y] = q.front();
    q.pop();

    for (int direction = 0; direction < 4; direction++) {
        int nx = x + dx[direction];
        int ny = y + dy[direction];

        if (!inside(nx, ny)) continue;
        if (grid[nx][ny] == '#') continue;
        if (distance[nx][ny] != -1) continue;

        distance[nx][ny] = distance[x][y] + 1;
        q.push({nx, ny});
    }
}
from collections import deque

def bfs(graph, source):
    distance = [-1] * len(graph)
    queue = deque([source])

    distance[source] = 0

    while queue:
        vertex = queue.popleft()

        for neighbor in graph[vertex]:
            if distance[neighbor] != -1:
                continue

            distance[neighbor] = distance[vertex] + 1
            queue.append(neighbor)

    return distance

9. Giải thích code

9.1. Vì sao phải đọc front trước pop?

pop() chỉ xóa phần tử, không trả về giá trị. Vì vậy mẫu chuẩn là:

u = q.front(); q.pop();

9.2. Vì sao đánh dấu ngay khi push?

Ngay khi một đỉnh được phát hiện, ta đánh dấu để không đỉnh nào khác đưa nó vào Queue lần nữa.

9.3. Vì sao BFS cho khoảng cách ngắn nhất?

Queue xử lý các đỉnh theo khoảng cách tăng dần: lớp 0 trước lớp 1, lớp 1 trước lớp 2. Do đó lần đầu một đỉnh được phát hiện chính là bằng số cạnh ít nhất.

9.4. Vì sao khoảng cách ban đầu là -1?

-1 vừa biểu diễn chưa thăm, vừa tránh cần một mảng visited riêng.

9.5. Vì sao BFS lưới vẫn là BFS đồ thị?

Mỗi ô là một đỉnh; các phép di chuyển hợp lệ tạo thành cạnh. Ta chỉ không xây danh sách cạnh tường minh mà sinh hàng xóm trực tiếp bằng mảng hướng.

9.6. Multi-source BFS khác gì BFS thường?

Chỉ khác bước khởi tạo: đưa tất cả nguồn vào Queue với khoảng cách 0. Phần vòng lặp BFS giữ nguyên.

10. Lỗi thường gặp

  • Gọi front() hoặc pop() khi Queue rỗng.
  • Tưởng pop() trả về phần tử.
  • Đánh dấu visited khi pop thay vì khi push.
  • Quên gán khoảng cách cho nguồn bằng 0.
  • Quên kiểm tra biên khi BFS trên lưới.
  • Không kiểm tra tường hoặc ô đã thăm.
  • Dùng BFS thường cho đồ thị có trọng số khác nhau.
  • Multi-source BFS nhưng chạy BFS riêng từ từng nguồn, gây chậm.
  • Muốn in đường đi nhưng không lưu mảng parent.

11. Quiz và bài tập

Câu 1. Queue tuân theo nguyên tắc nào?
Câu 2. Khi nào nên đánh dấu đỉnh đã thăm trong BFS?
Câu 3. BFS thường tìm đường ngắn nhất cho loại đồ thị nào?

📘 Cơ bản

  1. Mô phỏng q thao tác Queue.
  2. In phần tử đầu sau mỗi thao tác.
  3. Tự cài Queue bằng mảng vòng.

📗 Trung bình

  1. BFS thứ tự duyệt.
  2. Khoảng cách từ một nguồn.
  3. Khôi phục đường đi bằng parent.

📙 Nâng cao

  1. BFS mê cung.
  2. Multi-source BFS.
  3. BFS trạng thái.

🐉 HSG

  1. 0–1 BFS.
  2. Bidirectional BFS.
  3. BFS trên không gian trạng thái nén.
Bài thực hành 1 – Hàng chờ

Xử lý các lệnh E x, D, F. Với thao tác không hợp lệ, in EMPTY.

Bài thực hành 2 – Mê cung

Tìm số bước ít nhất từ S đến T trên lưới có tường.

Bài thực hành 3 – Virus

Nhiều nguồn virus lan đồng thời. Tìm thời gian phủ toàn bộ ô có thể đi.