Breadth-First Search. Thăm hết mọi đỉnh cách 1 bước, rồi mới tới mọi đỉnh cách 2 bước, rồi 3 bước. Lan ra như vết dầu trên mặt nước.
1. Ý tưởng
Nhỏ một giọt mực vào chậu nước. Vết mực không lao theo một hướng; nó lan đều ra mọi phía cùng
một lúc. Sau một giây nó phủ hết vùng cách tâm 1cm, sau hai giây phủ hết vùng cách 2cm.
BFS làm đúng như vậy trên đồ thị. Từ đỉnh 1, nó thăm hết các đỉnh kề trực tiếp — đó là tầng 1.
Rồi thăm hết các đỉnh kề của tầng 1 mà chưa thăm — tầng 2. Và tiếp tục.
BFS sau khi xử lý xong tầng 1: các đỉnh 1, 2, 3 đã được thăm; các đỉnh 4, 5, 6 đang xếp hàng chờ và đều thuộc tầng 2. Các dải ngang chính là các tầng — hãy để ý hình vẽ được xếp đúng theo tầng.
2. Tự bấm từng bước
Chú ý con số d= dưới mỗi đỉnh: đó là khoảng cách tới đỉnh xuất phát, và nó được gán đúng một
lần, vào lúc đỉnh được đưa vào hàng đợi.
BFS trên đồ thị mẫu — bấm từng bước
Hàng đợi (queue)
1
← đầu (lấy ra)đuôi (đưa vào) ←
Đã thăm (1)
1
Thứ tự thăm
chưa thăm đỉnh nào
khởi tạo
Bắt đầu: đưa đỉnh 1 vào hàng đợi và đánh dấu đã thăm. Khoảng cách d = 0.
Thứ tự thăm của BFS trên đồ thị mẫu là 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8. (Ở đồ thị này nó tình
cờ là 1..8 theo đúng thứ tự — nhờ cách mình đánh số các đỉnh, để các em dễ nhớ.)
Bảng mô phỏng đầy đủ — 26 bước
#
Phép toán
Hàng đợi (queue)
Đã thăm
Diễn biến
1
khởi tạo 1
1
1
Bắt đầu: đưa đỉnh 1 vào hàng đợi và đánh dấu đã thăm. Khoảng cách d = 0.
2
lấy ra 1
—
1
Lấy đỉnh 1 ra khỏi đầu hàng đợi và xử lý. Đây là đỉnh thứ 1 được thăm, ở tầng 0.
3
đưa vào 2
2
1 2
Xét đỉnh 2 kề với đỉnh 1: chưa thăm → đưa vào cuối hàng đợi và đánh dấu đã thăm ngay. d = 1.
4
đưa vào 3
2 3
1 2 3
Xét đỉnh 3 kề với đỉnh 1: chưa thăm → đưa vào cuối hàng đợi và đánh dấu đã thăm ngay. d = 1.
5
lấy ra 2
3
1 2 3
Lấy đỉnh 2 ra khỏi đầu hàng đợi và xử lý. Đây là đỉnh thứ 2 được thăm, ở tầng 1.
6
bỏ qua 1
3
1 2 3
Xét đỉnh 1 kề với đỉnh 2: đã thăm rồi, bỏ qua.
7
đưa vào 4
3 4
1 2 3 4
Xét đỉnh 4 kề với đỉnh 2: chưa thăm → đưa vào cuối hàng đợi và đánh dấu đã thăm ngay. d = 2.
8
đưa vào 5
3 4 5
1 2 3 4 5
Xét đỉnh 5 kề với đỉnh 2: chưa thăm → đưa vào cuối hàng đợi và đánh dấu đã thăm ngay. d = 2.
9
lấy ra 3
4 5
1 2 3 4 5
Lấy đỉnh 3 ra khỏi đầu hàng đợi và xử lý. Đây là đỉnh thứ 3 được thăm, ở tầng 1.
10
bỏ qua 1
4 5
1 2 3 4 5
Xét đỉnh 1 kề với đỉnh 3: đã thăm rồi, bỏ qua.
11
đưa vào 6
4 5 6
1 2 3 4 5 6
Xét đỉnh 6 kề với đỉnh 3: chưa thăm → đưa vào cuối hàng đợi và đánh dấu đã thăm ngay. d = 2.
12
lấy ra 4
5 6
1 2 3 4 5 6
Lấy đỉnh 4 ra khỏi đầu hàng đợi và xử lý. Đây là đỉnh thứ 4 được thăm, ở tầng 2.
13
bỏ qua 2
5 6
1 2 3 4 5 6
Xét đỉnh 2 kề với đỉnh 4: đã thăm rồi, bỏ qua.
14
đưa vào 7
5 6 7
1 2 3 4 5 6 7
Xét đỉnh 7 kề với đỉnh 4: chưa thăm → đưa vào cuối hàng đợi và đánh dấu đã thăm ngay. d = 3.
15
lấy ra 5
6 7
1 2 3 4 5 6 7
Lấy đỉnh 5 ra khỏi đầu hàng đợi và xử lý. Đây là đỉnh thứ 5 được thăm, ở tầng 2.
16
bỏ qua 2
6 7
1 2 3 4 5 6 7
Xét đỉnh 2 kề với đỉnh 5: đã thăm rồi, bỏ qua.
17
bỏ qua 7
6 7
1 2 3 4 5 6 7
Xét đỉnh 7 kề với đỉnh 5: đã thăm rồi, bỏ qua.
18
lấy ra 6
7
1 2 3 4 5 6 7
Lấy đỉnh 6 ra khỏi đầu hàng đợi và xử lý. Đây là đỉnh thứ 6 được thăm, ở tầng 2.
19
bỏ qua 3
7
1 2 3 4 5 6 7
Xét đỉnh 3 kề với đỉnh 6: đã thăm rồi, bỏ qua.
20
đưa vào 8
7 8
1 2 3 4 5 6 7 8
Xét đỉnh 8 kề với đỉnh 6: chưa thăm → đưa vào cuối hàng đợi và đánh dấu đã thăm ngay. d = 3.
21
lấy ra 7
8
1 2 3 4 5 6 7 8
Lấy đỉnh 7 ra khỏi đầu hàng đợi và xử lý. Đây là đỉnh thứ 7 được thăm, ở tầng 3.
22
bỏ qua 4
8
1 2 3 4 5 6 7 8
Xét đỉnh 4 kề với đỉnh 7: đã thăm rồi, bỏ qua.
23
bỏ qua 5
8
1 2 3 4 5 6 7 8
Xét đỉnh 5 kề với đỉnh 7: đã thăm rồi, bỏ qua.
24
lấy ra 8
—
1 2 3 4 5 6 7 8
Lấy đỉnh 8 ra khỏi đầu hàng đợi và xử lý. Đây là đỉnh thứ 8 được thăm, ở tầng 3.
25
bỏ qua 6
—
1 2 3 4 5 6 7 8
Xét đỉnh 6 kề với đỉnh 8: đã thăm rồi, bỏ qua.
26
xong
—
1 2 3 4 5 6 7 8
Hàng chờ rỗng — BFS kết thúc. Thứ tự thăm: 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8.
3. Cài đặt
BFS cơ bản, kèm mảng khoảng cách
vector<vector<int>> ke;vector<bool> daTham;vector<int> d; // d[v] = khoảng cách ngắn nhất từ đỉnh xuất phátvoid bfs(int batDau) { queue<int> q; q.push(batDau); daTham[batDau] = true; // đánh dấu KHI ĐƯA VÀO d[batDau] = 0; while (!q.empty()) { int u = q.front(); // lấy đỉnh Ở ĐẦU hàng đợi q.pop(); cout << u << ' '; // xử lý đỉnh u for (int v : ke[u]) if (!daTham[v]) { daTham[v] = true; // đánh dấu NGAY, không đợi tới lúc lấy ra d[v] = d[u] + 1; q.push(v); } }}
from collections import dequedef bfs(bat_dau: int) -> None: q = deque([bat_dau]) daTham[bat_dau] = True # đánh dấu KHI ĐƯA VÀO d[bat_dau] = 0 while q: u = q.popleft() # lấy đỉnh Ở ĐẦU hàng đợi print(u, end=' ') # xử lý đỉnh u for v in ke[u]: if not daTham[v]: daTham[v] = True # đánh dấu NGAY, không đợi tới lúc lấy ra d[v] = d[u] + 1 q.append(v)
So sánh với DFS đệ quy ở bài trước, chỉ có hai thay đổi thật sự: dùng queue lấy từ đầu thay vì
ngăn xếp, và đánh dấu daTham[v] = true ở chỗ đưa vào chứ không phải chỗ lấy ra.
4. Vì sao BFS cho đường đi ngắn nhất
Đây là tính chất quan trọng nhất của BFS, và nó chỉ đúng trên đồ thị không trọng số (mọi cạnh
coi như dài 1).
Lập luận gọn: BFS xử lý xong toàn bộ tầng k trước khi chạm tới bất kỳ đỉnh nào của tầng k+1.
Cho nên lần đầu tiên BFS chạm tới đỉnh v, số tầng hiện tại chính là khoảng cách ngắn nhất
tới v. Nếu tồn tại đường ngắn hơn, thì v đã bị chạm ở một tầng sớm hơn rồi — trái với việc
“đây là lần đầu”.
Đó cũng là lý do dòng d[v] = d[u] + 1 chỉ chạy đúng một lần cho mỗi đỉnh và không bao giờ cần
sửa lại. Hãy so với DFS: DFS tới đỉnh 5 qua đường dài 4 cạnh, BFS tới đỉnh 5 qua đường 2 cạnh.
Mê cung 5×5, ô đen là tường. BFS từ (0,0) tới (4,4) trả về khoảng cách 8 — và đường tô đậm là một đường đi đạt đúng 8 bước. DFS cũng tới được đích, nhưng không đảm bảo con số nào.
BFS tìm đường trong mê cung — số trong ô là khoảng cách
Hàng đợi (queue)
(0,0)
← đầu (lấy ra)đuôi (đưa vào) ←
Đã thăm (1)
(0,0)
Thứ tự thăm
chưa thăm đỉnh nào
khởi tạo
Bắt đầu: đưa ô (0,0) vào hàng đợi và đánh dấu đã thăm. Khoảng cách d = 0.
Muốn in ra cả đường đi, không chỉ độ dài, hãy lưu thêm “đỉnh cha”:
Truy vết đường đi ngắn nhất
vector<int> cha(n + 1, 0); // cha[v] = đỉnh đã đưa v vào hàng đợi// ... trong vòng BFS, lúc đưa v vào:cha[v] = u;// truy lại đường đi từ dinhDich về batDau, rồi đảo ngượcvector<int> duongDi;for (int v = dinhDich; v != 0; v = cha[v]) duongDi.push_back(v);reverse(duongDi.begin(), duongDi.end());
cha = [0] * (n + 1) # cha[v] = đỉnh đã đưa v vào hàng đợi# ... trong vòng BFS, lúc đưa v vào:cha[v] = u# truy lại đường đi từ dinh_dich về bat_dau, rồi đảo ngượcduong_di = []v = dinh_dichwhile v != 0: duong_di.append(v) v = cha[v]duong_di.reverse()
5. Mẹo chốt tầng
Rất nhiều bài cần biết “đỉnh này thuộc tầng mấy” hoặc cần xử lý theo từng tầng một. Mẹo chuẩn
là chốt sz = q.size() ở đầu mỗi lượt:
Xử lý BFS theo từng tầng
int tang = 0;while (!q.empty()) { int sz = q.size(); // CHỐT: đúng bằng số đỉnh của tầng này for (int i = 0; i < sz; i++) { int u = q.front(); q.pop(); // ... xử lý u, biết chắc u thuộc tầng "tang" ... for (int v : ke[u]) if (!daTham[v]) { daTham[v] = true; q.push(v); } } tang++; // xong một tầng}
tang = 0while q: for _ in range(len(q)): # CHỐT: đúng bằng số đỉnh của tầng này u = q.popleft() # ... xử lý u, biết chắc u thuộc tầng "tang" ... for v in ke[u]: if not daTham[v]: daTham[v] = True q.append(v) tang += 1 # xong một tầng
Ở đầu mỗi lượt, hàng đợi chứa đúng và chỉ các đỉnh của một tầng. Lấy ra đúng sz đỉnh thì ta
xử lý gọn một tầng; các đỉnh vừa được đưa vào thuộc tầng sau và sẽ đợi tới lượt kế tiếp.
Thiếu sz, vòng lặp trong sẽ ăn luôn cả các đỉnh mới — các tầng trộn lẫn vào nhau. Đây là lỗi
thường gặp nhất ở bài LeetCode 102.
Mẹo này mở ra ba loại bài: in cây theo tầng (bài 102), đếm số bước ít nhất (bài 1091), và đếm
thời gian lan truyền (bài 994).
6. Độ phức tạp
Chi phí
Vì sao
Thời gian
O(V + E)
Giống DFS: mỗi đỉnh vào hàng đợi đúng một lần, mỗi cạnh xét hai lần.
Bộ nhớ
O(V)
Hàng đợi lớn nhất bằng tầng rộng nhất của đồ thị.
Trong thực tế BFS thường tốn bộ nhớ hơn DFS: một tầng của lưới 1000×1000 có thể chứa hàng nghìn ô
cùng lúc, trong khi ngăn xếp của DFS chỉ sâu bằng độ dài một đường đi.
7. Lỗi thường gặp
8. Ba bài LeetCode
Ba bài này lần lượt dạy ba ý: tầng là gì, tầng là khoảng cách, và tầng có thể là thời gian.
In các nút của cây theo từng tầng, mỗi tầng một dòng. Đây là BFS ở dạng thuần khiết nhất — và là bài để học mẹo "chốt kích thước hàng đợi" để biết một tầng kết thúc ở đâu.
Xem đáp án
Vì sao cây không cần mảng daTham
Cây là đồ thị không có chu trình và mỗi nút chỉ có một đường đi tới từ gốc. Không bao giờ
có chuyện đi vòng về một nút cũ, nên phần “đánh dấu đã thăm” biến mất. Đây là lý do bài này là
bài BFS dễ nhất: chỉ còn đúng phần cốt lõi — hàng đợi.
Trên đồ thị thường thì mảng daTham bắt buộc phải có. Đừng mang thói quen từ bài này sang đó.
Mẹo chốt kích thước hàng đợi
Ở đầu mỗi lượt, hàng đợi chứa đúng và chỉ các nút của một tầng. Ta ghi lại số lượng đó vào
sz, rồi chỉ lấy ra đúng sz nút. Những nút con được đưa vào trong lúc đó thuộc tầng sau, và
sẽ được xử lý ở lượt tiếp theo.
Không có sz, vòng lặp trong sẽ ăn luôn cả các nút mới vừa đưa vào, và kết quả là một danh
sách phẳng chứ không phải danh sách theo tầng.
Cùng một mẹo, ba bài khác
Mẹo sz = q.size() dùng lại được ở rất nhiều bài:
199. Right Side View (lấy nút cuối
mỗi tầng), 104. Maximum Depth
(đếm số tầng), và bài 994 ngay dưới đây — ở đó một “tầng” là một phút.
Cài đặt
class Solution {public: vector<vector<int>> levelOrder(TreeNode* root) { vector<vector<int>> kq; if (root == nullptr) return kq; // cây rỗng: đừng quên! queue<TreeNode*> q; q.push(root); while (!q.empty()) { int sz = q.size(); // CHỐT số nút của tầng này vector<int> tang; for (int i = 0; i < sz; i++) { // lấy đúng sz nút, không hơn TreeNode* u = q.front(); q.pop(); tang.push_back(u->val); if (u->left) q.push(u->left); if (u->right) q.push(u->right); } kq.push_back(tang); } return kq; }};
class Solution: def levelOrder(self, root: Optional[TreeNode]) -> List[List[int]]: if root is None: # cây rỗng: đừng quên! return [] kq = [] q = deque([root]) while q: sz = len(q) # CHỐT số nút của tầng này tang = [] for _ in range(sz): # lấy đúng sz nút, không hơn u = q.popleft() tang.append(u.val) if u.left: q.append(u.left) if u.right: q.append(u.right) kq.append(tang) return kq
Độ phức tạp
O(n) thời gian, O(n) bộ nhớ — hàng đợi có thể chứa cả tầng rộng nhất của cây.
Chỗ dễ sai
Dùng while (!q.empty()) mà không chốt sz = q.size() trước vòng lặp trong. Hàng đợi lớn dần trong lúc đang lặp, các tầng trộn vào nhau.
Đi từ góc trên bên trái xuống góc dưới bên phải, được đi cả 8 hướng, tránh các ô bị chặn. Đây là bài chứng minh vì sao BFS mới tìm được đường ngắn nhất — DFS sẽ cho một đường đi, nhưng gần như chắc chắn không ngắn nhất.
Xem đáp án
Vì sao BFS cho đường ngắn nhất
BFS lan ra theo từng tầng: thăm hết mọi ô cách xuất phát 1 bước, rồi mọi ô cách 2 bước, rồi
3… Lần đầu tiên BFS chạm tới một ô, số tầng hiện tại chính là khoảng cách ngắn nhất tới
ô đó. Không đường nào có thể ngắn hơn, vì nếu có thì ô đã bị chạm ở tầng sớm hơn rồi.
DFS thì đi sâu một nhánh tới cùng. Nó cũng tới được đích, nhưng bằng con đường nó gặp đầu tiên
— thường là đường rất dài. Muốn có đường ngắn nhất bằng DFS thì phải thử mọi đường, tức là
quay lui, và chi phí tăng theo hàm mũ.
Tám hướng thay vì bốn
Hai vòng dr, dc từ -1 đến 1 cho ra 9 cặp; bỏ cặp (0,0) còn đúng 8 hướng. Viết như vậy gọn
hơn khai báo hai mảng 8 phần tử, và không sợ gõ sai một dấu.
Đánh dấu khi đưa vào, không phải khi lấy ra
Đây là lỗi kinh điển. Hãy tưởng tượng ô X có 5 ô kề đều nằm cùng một tầng. Nếu chỉ đánh dấu
lúc lấy ra, cả 5 ô đó đều thấy X “chưa thăm” và đẩy X vào hàng đợi: X nằm 5 lần trong
hàng đợi. Với lưới 100×100 số bản sao bùng nổ và chương trình quá thời gian.
Đánh dấu ngay khi đưa vào thì mỗi ô vào hàng đợi đúng một lần. Trong khi đó, DFS bằng ngăn xếp
cố tình đánh dấu khi lấy ra — hai thuật toán khác nhau ở đúng chỗ này, và đó không phải sự
tuỳ tiện.
Cài đặt
class Solution {public: int shortestPathBinaryMatrix(vector<vector<int>>& grid) { int n = grid.size(); if (grid[0][0] == 1 || grid[n - 1][n - 1] == 1) return -1; queue<pair<int, int>> q; q.push({0, 0}); grid[0][0] = 1; // đánh dấu KHI ĐƯA VÀO int d = 1; // độ dài tính theo số ô while (!q.empty()) { int sz = q.size(); // một lượt = một tầng khoảng cách for (int i = 0; i < sz; i++) { auto [r, c] = q.front(); q.pop(); if (r == n - 1 && c == n - 1) return d; // tới đích: d là ngắn nhất for (int dr = -1; dr <= 1; dr++) for (int dc = -1; dc <= 1; dc++) { if (dr == 0 && dc == 0) continue; int nr = r + dr, nc = c + dc; if (nr < 0 || nc < 0 || nr >= n || nc >= n) continue; if (grid[nr][nc] != 0) continue; // tường hoặc đã thăm grid[nr][nc] = 1; // đánh dấu NGAY q.push({nr, nc}); } } d++; } return -1; // hàng đợi cạn mà chưa tới đích }};
class Solution: def shortestPathBinaryMatrix(self, grid: List[List[int]]) -> int: n = len(grid) if grid[0][0] == 1 or grid[n - 1][n - 1] == 1: return -1 q = deque([(0, 0)]) grid[0][0] = 1 # đánh dấu KHI ĐƯA VÀO d = 1 # độ dài tính theo số ô while q: for _ in range(len(q)): # một lượt = một tầng khoảng cách r, c = q.popleft() if r == n - 1 and c == n - 1: return d # tới đích: d là ngắn nhất for dr in (-1, 0, 1): for dc in (-1, 0, 1): if dr == 0 and dc == 0: continue nr, nc = r + dr, c + dc if not (0 <= nr < n and 0 <= nc < n): continue if grid[nr][nc] != 0: # tường hoặc đã thăm continue grid[nr][nc] = 1 # đánh dấu NGAY q.append((nr, nc)) d += 1 return -1 # hàng đợi cạn mà chưa tới đích
Độ phức tạp
O(n²) thời gian, O(n²) bộ nhớ cho hàng đợi.
Chỗ dễ sai
Đánh dấu ô khi lấy ra thay vì khi đưa vào. Một ô có thể được nhiều ô kề đẩy vào hàng đợi trước khi được lấy ra, hàng đợi phình lên và khoảng cách sai.
Mỗi phút, cam thối làm thối bốn quả cam kề nó. Hỏi sau bao nhiêu phút thì không còn cam tươi. Bài này dạy hai ý mới: BFS có thể bắt đầu từ nhiều đỉnh cùng lúc, và một "tầng" có thể mang nghĩa thời gian.
Xem đáp án
BFS đa nguồn (multi-source)
Từ trước tới giờ ta luôn bắt đầu BFS từ một đỉnh. Ở bài này có thể có nhiều quả cam thối
ngay từ phút 0, và chúng lan ra đồng thời.
Cách xử lý đẹp đến mức đáng ngạc nhiên: đưa tất cả các nguồn vào hàng đợi trước khi vào
vòng lặp, coi chúng đều ở tầng 0. Phần còn lại của thuật toán không thay đổi một dòng nào.
Vì sao đúng? Vì BFS vẫn xử lý hết tầng 0 trước tầng 1. Một quả cam tươi sẽ bị quả cam thối
gần nhất làm thối trước tiên — đúng như mô tả của đề.
Hãy hình dung: thay vì n nguồn, ta tưởng tượng có một “đỉnh ảo” nối tới cả n quả cam thối.
BFS từ đỉnh ảo đó, bỏ qua bước đầu, chính là BFS đa nguồn.
Tầng mang nghĩa thời gian
Trong bài 1091, một tầng là một bước đi. Ở đây, một tầng là một phút. Cấu trúc code giống
hệt nhau — vẫn sz = q.size() rồi phut++ sau mỗi lượt. Cái thay đổi chỉ là cách ta đọc
con số đó.
Đây là lúc để nhận ra: BFS không đo “khoảng cách” theo nghĩa hình học. Nó đo số lần lan
truyền. Số đó là gì thì tuỳ bài: bước đi, phút, số lần đổi chữ trong một từ…
Hai điều kiện biên phải nhớ
while (!q.empty() && tuoi > 0): dừng ngay khi hết cam tươi, nếu không sẽ đếm thừa một phút
cho lượt cuối chẳng làm thối được gì.
Kiểm tra tuoi == 0 lúc trả về: cam nằm trong ô bị vây kín không bao giờ thối, đáp án là -1.
Lưới không có quả cam tươi nào ngay từ đầu: tuoi == 0, vòng while không chạy, trả về 0. Đúng.
Cài đặt
class Solution {public: int orangesRotting(vector<vector<int>>& grid) { int n = grid.size(), m = grid[0].size(), tuoi = 0; queue<pair<int, int>> q; // ĐA NGUỒN: đưa MỌI quả cam thối ban đầu vào hàng đợi cùng lúc for (int i = 0; i < n; i++) for (int j = 0; j < m; j++) { if (grid[i][j] == 2) q.push({i, j}); else if (grid[i][j] == 1) tuoi++; } int dr[] = {-1, 0, 1, 0}, dc[] = {0, 1, 0, -1}; int phut = 0; while (!q.empty() && tuoi > 0) { int sz = q.size(); // một tầng = một PHÚT for (int i = 0; i < sz; i++) { auto [r, c] = q.front(); q.pop(); for (int k = 0; k < 4; k++) { int nr = r + dr[k], nc = c + dc[k]; if (nr < 0 || nc < 0 || nr >= n || nc >= m) continue; if (grid[nr][nc] != 1) continue; // ô trống hoặc đã thối grid[nr][nc] = 2; // thối ngay, khi ĐƯA VÀO tuoi--; q.push({nr, nc}); } } phut++; } return tuoi == 0 ? phut : -1; // còn cam tươi → không bao giờ thối hết }};
class Solution: def orangesRotting(self, grid: List[List[int]]) -> int: n, m = len(grid), len(grid[0]) q = deque() tuoi = 0 # ĐA NGUỒN: đưa MỌI quả cam thối ban đầu vào hàng đợi cùng lúc for i in range(n): for j in range(m): if grid[i][j] == 2: q.append((i, j)) elif grid[i][j] == 1: tuoi += 1 phut = 0 while q and tuoi > 0: for _ in range(len(q)): # một tầng = một PHÚT r, c = q.popleft() for dr, dc in ((-1, 0), (0, 1), (1, 0), (0, -1)): nr, nc = r + dr, c + dc if not (0 <= nr < n and 0 <= nc < m): continue if grid[nr][nc] != 1: # ô trống hoặc đã thối continue grid[nr][nc] = 2 # thối ngay, khi ĐƯA VÀO tuoi -= 1 q.append((nr, nc)) phut += 1 return phut if tuoi == 0 else -1 # còn cam tươi → không bao giờ thối hết
Độ phức tạp
O(n·m) thời gian, O(n·m) bộ nhớ.
Chỗ dễ sai
Trả về phut mà không kiểm tra còn cam tươi sót lại — cam bị tường nước vây kín thì đáp án phải là -1. Và nếu ngay từ đầu không có cam tươi thì đáp án là 0, không phải -1.