Rotting Oranges
Bạn nhận được một lưới dưới dạng danh sách các hàng có độ dài bằng nhau. Mỗi ô là 0 (trống), 1 (cam tươi) hoặc 2 (cam thối). Mỗi phút, mỗi quả cam tươi nằm cạnh một quả cam thối ở phía trên, phía dưới, bên trái hoặc bên phải sẽ chuyển thành cam thối. Hãy trả về số phút cho đến khi không còn quả cam tươi nào, hoặc -1 nếu có quả cam tươi không bao giờ bị thối. Nếu ban đầu lưới không có quả cam tươi nào thì cần 0 phút.
Hàm
- gridinteger-2d-array
- lưới, một danh sách gồm 0, 1 và 2 cho mỗi hàng
- Trả vềinteger
- số phút cho đến khi không còn quả cam nào tươi, hoặc -1 nếu điều đó không bao giờ xảy ra
Ràng buộc
1 ≤ grid.length ≤ 1501 ≤ grid[i].length ≤ 150- Mỗi hàng có cùng độ dài.
- Mỗi
grid[i][j]là0,1hoặc2.
Ví dụ
- Đầu vào
- grid = [[2, 1, 1, 0], [0, 1, 0, 1], [1, 1, 1, 1]]
- Đầu ra
- 6
- Giải thích
- Các ô có tọa độ (hàng, cột). Quá trình thối lan theo con đường duy nhất từ (0,0) và đến (2,3) ở phút thứ 5. Quả cam ở (1,3) chỉ tiếp xúc với (2,3), nên nó thối cuối cùng, ở phút thứ 6.
- Đầu vào
- grid = [[2, 1, 0], [0, 0, 1]]
- Đầu ra
- -1
- Giải thích
- Quả cam ở vị trí (1,2) có các ô trống phía trên và bên trái, còn phía dưới và bên phải là cạnh của lưới. Không thể có quả thối nào lan đến đó, nên đáp án là -1.
- Đầu vào
- grid = [[0, 2, 0, 2]]
- Đầu ra
- 0
- Giải thích
- Ban đầu không có quả cam tươi nào, nên không cần chờ thời gian trôi qua và đáp án là 0.
Câu hỏi mở rộng
Giả sử mỗi quả cam tươi cần một số phút riêng để bị thối sau khi quả cam bên cạnh bị thối. Vậy bạn sẽ tìm thời điểm tất cả cam bị thối như thế nào?
Gợi ý
Mở từng gợi ý một. Mỗi gợi ý tiết lộ thêm một chút.
Hãy hình dung sự thối rữa lan ra theo từng đợt. Những quả cam nào có thể bị thối vào phút thứ 3? Chỉ những quả cam tươi nằm cạnh một quả cam đã bị thối vào phút thứ 2.
Bắt đầu một lượt tìm kiếm theo chiều rộng (breadth-first search) đồng thời từ mỗi quả cam bị thối: đưa tất cả chúng vào hàng đợi trước khi bắt đầu tìm kiếm. Khi đó, hàng đợi luôn chứa ranh giới vùng bị thối.
Xử lý hàng đợi từng tầng một: đọc kích thước của hàng đợi, lấy số ô tương ứng rồi tính một phút cho mỗi tầng. Đếm số quả cam tươi từ đầu và giảm số lượng khi chúng bị thối, để bạn có thể dừng ngay khi số lượng bằng 0, đồng thời trả về -1 nếu hàng đợi cạn trước đó.
Lời giải
Quá trình thối lan đồng thời từ mỗi quả cam thối, mỗi phút một ô, nên đáp án là khoảng cách: quả cam tươi xa nhất cách quả cam thối gần nhất bao xa. Tìm kiếm theo chiều rộng đo khoảng cách đó khi mọi quả cam thối đều bắt đầu trong hàng đợi và mỗi tầng của hàng đợi tương ứng với một phút.
Mô phỏng từng phút
Đúng, nhưng không chạy xong với các test lớn nhất
Ý tưởng
Mỗi phút, quét toàn bộ lưới và liệt kê mọi quả cam tươi tiếp xúc với quả cam thối. Sau đó làm thối tất cả chúng và tăng đồng hồ thêm một đơn vị. Dừng lại khi lần quét không tìm thấy quả nào cần làm thối; nếu còn quả cam tươi, trả về -1.
Liệt kê trước, làm thối sau. Nếu bạn làm thối một quả cam giữa lần quét, một ô được quét sau đó trong cùng lần quét sẽ thấy quả cam đó đã bị thối, khiến sự thối lan qua nhiều ô chỉ trong một phút.
Mỗi phút, quét tất cả các ô trong rows × cols hàng và cột, số phút có thể gần bằng số ô: một đường đi ngoằn ngoèo trên lưới 150 × 150 cần 11,324 phút, tương đương khoảng 2.5 × 10^8 lần kiểm tra ô.
Thuật toán
- Đặt số phút bằng 0.
- Duyệt qua lưới và liệt kê từng quả cam tươi có quả cam thối ở cạnh.
- Nếu danh sách trống, hãy dừng lại. Nếu không, chuyển tất cả quả cam trong danh sách thành cam thối, cộng thêm 1 vào số phút rồi duyệt lại.
- Trả về -1 nếu còn quả cam tươi, nếu không thì trả về số phút.
def orangesRotting(grid):
rows, cols = len(grid), len(grid[0])
minutes = 0
while True:
# Find every fresh orange that touches a rotten one right now.
to_rot = []
for r in range(rows):
for c in range(cols):
if grid[r][c] != 1:
continue
for nr, nc in ((r + 1, c), (r - 1, c), (r, c + 1), (r, c - 1)):
if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] == 2:
to_rot.append((r, c))
break
if not to_rot:
break
# Rot them only after the scan, so the rot moves one step a minute.
for r, c in to_rot:
grid[r][c] = 2
minutes += 1
for row in grid:
if 1 in row:
return -1
return minutesBFS đa nguồn theo từng cấp
Ý tưởng
Những quả cam duy nhất có thể bị thối vào phút t+1 là các quả cam tươi nằm cạnh những quả đã bị thối vào phút t, vì vậy hãy chỉ đưa đúng những quả đó vào hàng đợi. Khởi tạo hàng đợi với mọi quả cam đã bị thối ở phút 0. Một quả cam tươi bị thối vào phút bằng khoảng cách từ nó đến quả cam thối gần nhất, và thuật toán tìm kiếm theo chiều rộng được khởi tạo với tất cả các nguồn sẽ đến mỗi ô trước tiên từ nguồn gần nhất.
Xử lý theo từng lớp. Khi bắt đầu một phút, hàng đợi chứa những quả cam đã bị thối ở phút trước; lấy đúng số quả đó từ đầu hàng đợi, làm thối các quả cam tươi nằm cạnh chúng rồi thêm các quả đó vào cuối hàng đợi. Phần còn lại là lớp tiếp theo.
Đếm số quả cam tươi trước và dừng khi số lượng giảm xuống 0, hoặc khi lớp cuối cùng khiến thời gian tăng thêm một phút mà không có quả nào bị thối. Mỗi ô chỉ vào hàng đợi tối đa một lần: O(rows × cols).
Thuật toán
- Đưa mọi quả cam bị thối vào hàng đợi và đếm số cam tươi.
- Đặt số phút bằng 0. Khi hàng đợi chưa rỗng và vẫn còn cam tươi, tăng số phút thêm 1 và ghi lại kích thước k của hàng đợi.
- Lấy k quả cam từ đầu hàng đợi. Với mỗi quả cam tươi lân cận nằm trong lưới, đánh dấu quả đó là bị thối, giảm số cam tươi đi 1 và thêm quả đó vào cuối hàng đợi.
- Khi vòng lặp kết thúc, trả về số phút nếu số cam tươi bằng 0, nếu không thì trả về -1.
from collections import deque
def orangesRotting(grid):
rows, cols = len(grid), len(grid[0])
queue = deque()
fresh = 0
# Every orange that is rotten at minute 0 starts in the queue.
for r in range(rows):
for c in range(cols):
if grid[r][c] == 2:
queue.append((r, c))
elif grid[r][c] == 1:
fresh += 1
minutes = 0
while queue and fresh > 0:
minutes += 1
# The queue holds exactly the oranges that went rotten last minute.
# Rot their fresh neighbours; those become the next minute's queue.
for _ in range(len(queue)):
r, c = queue.popleft()
for nr, nc in ((r + 1, c), (r - 1, c), (r, c + 1), (r, c - 1)):
if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] == 1:
grid[nr][nc] = 2
fresh -= 1
queue.append((nr, nc))
return minutes if fresh == 0 else -1
Lỗi thường gặp và trường hợp biên
- Tính thêm một phút cho cấp độ cuối cùng. Nếu vòng lặp chạy đến khi hàng đợi trống, lượt cuối không làm thối quả cam nào nhưng vẫn cộng thêm 1. Dừng ngay khi không còn quả cam tươi.
- Tìm kiếm lần lượt từ mỗi quả cam đã thối. Lần tìm kiếm đầu tiên đánh dấu mọi quả cam mà nó tiếp cận bằng đồng hồ riêng:
[[2, 1, 1, 1, 1, 1, 1, 2]]rồi cho kết quả 6 phút, không phải 3. - Trả về -1 vì không có quả cam nào đã thối. Khi cũng không có quả cam tươi, không cần làm gì cả:
[[0]]trả về 0. - Chỉ đánh dấu quả cam bị thối khi nó rời hàng đợi, thay vì khi nó vào hàng đợi. Khi một quả cam nằm cạnh hai quả đã thối, nó sẽ được thêm vào hàng đợi hai lần và số quả cam tươi sẽ giảm xuống dưới 0.
Câu hỏi thường gặp3
Độ phức tạp thời gian của bài toán Cam thối là bao nhiêu?
O(rows × cols) với tìm kiếm theo chiều rộng: lần quét đầu tiên đọc mỗi ô một lần, mỗi ô màu cam được thêm vào hàng đợi nhiều nhất một lần và kiểm tra bốn ô lân cận. Trong trường hợp xấu nhất, hàng đợi chiếm O(rows × cols) bộ nhớ.
Tại sao dùng BFS thay vì DFS cho bài toán Cam thối?
Tìm kiếm theo chiều rộng duyệt các ô theo khoảng cách từ nguồn, và ở đây khoảng cách được tính bằng thời gian: mức k là tập hợp các quả cam bị thối vào phút k. Tìm kiếm theo chiều sâu có thể đến một ô bằng đường vòng dài trước, rồi phải duyệt lại ô đó mỗi khi tìm thấy một đường ngắn hơn.
Bạn có thể giải bài toán Cam bị thối mà không thay đổi lưới không?
Đúng vậy. Hãy dùng một mảng đã thăm riêng và kiểm tra mảng đó thay vì ghi số 2 vào lưới. Cách này tốn thêm O(rows × cols) bộ nhớ, vốn có thể cần cho hàng đợi, đồng thời giữ nguyên lưới của bên gọi.
Bài tập tương tự
Các bài tập dùng cùng ý tưởng. Giải hai hoặc ba bài là bạn sẽ nhớ được mẫu này.
def orangesRotting(grid):
# Viết code ở đâyKiểm thử #1
Kiểm thử #2
Kiểm thử #3
Đối số
grid = [[2, 1, 1, 0], [0, 1, 0, 1], [1, 1, 1, 1]]
Đầu ra mong đợi
6