Flood Fill
Hình ảnh là một lưới gồm các số nguyên, trong đó mỗi số biểu thị màu của một pixel. Bạn nhận được hình ảnh dưới dạng danh sách các hàng, một pixel bắt đầu ở hàng sr và cột sc, cùng một color mới. Tô lại vùng chứa pixel bắt đầu: gồm mọi pixel có cùng màu với pixel bắt đầu mà bạn có thể đi đến từ pixel đó bằng cách di chuyển lên, xuống, sang trái hoặc sang phải qua các pixel cùng màu. Trả về hình ảnh sau khi tô lại.
Hàm
- imageinteger-2d-array
- hình ảnh dưới dạng danh sách các hàng, mỗi điểm ảnh ứng với một số
- srinteger
- hàng của pixel bắt đầu, được đếm từ 0
- scinteger
- cột của điểm ảnh bắt đầu, được đếm từ 0
- colorinteger
- màu mới cho vùng
- Trả vềinteger-2d-array
- hình ảnh sau khi vùng đó được vẽ lại
Ràng buộc
1 ≤ image.length ≤ 801 ≤ image[i].length ≤ 80- Mỗi hàng có cùng độ dài.
0 ≤ image[i][j], color ≤ 655350 ≤ sr < image.lengthvà0 ≤ sc < image[0].length
Ví dụ
- Đầu vào
- image = [[1, 1, 0], [1, 0, 1], [1, 1, 1]]sr = 0sc = 0color = 5
- Đầu ra
- [[5, 5, 0], [5, 0, 5], [5, 5, 5]]
- Giải thích
- Ô bắt đầu có màu 1. Cả bảy ô 1 đều nối với ô đó qua các ô 1 chung cạnh, dọc theo cột bên trái, theo hàng dưới cùng và lên cạnh phải, nên tất cả đều chuyển thành 5. Hai ô 0 giữ nguyên màu.
- Đầu vào
- image = [[3, 3, 3], [3, 7, 3], [3, 3, 3]]sr = 1sc = 1color = 7
- Đầu ra
- [[3, 3, 3], [3, 7, 3], [3, 3, 3]]
- Giải thích
- Ô bắt đầu đã có màu 7, nên tô vùng của nó bằng màu 7 không làm thay đổi gì, hình ảnh trở lại như ban đầu.
- Đầu vào
- image = [[2, 2, 4, 4], [4, 2, 2, 4], [4, 4, 2, 2]]sr = 2sc = 3color = 9
- Đầu ra
- [[9, 9, 4, 4], [4, 9, 9, 4], [4, 4, 9, 9]]
- Giải thích
- Các số 2 tạo thành một cầu thang từ góc dưới bên phải lên góc trên bên trái, mỗi bậc có chung một cạnh với bậc tiếp theo, nên cả sáu số đều biến thành 9. Các số 4 tạo thành hai vùng riêng biệt và giữ nguyên màu sắc.
Câu hỏi mở rộng
Nếu các pixel chỉ chạm nhau ở góc cũng được tính là liền kề, lời giải của bạn sẽ thay đổi 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.
Những điểm ảnh nào có thể thay đổi? Chỉ những điểm ảnh có cùng màu với điểm ảnh ban đầu và được nối với nó bằng một đường đi có màu đó.
Xem mỗi pixel là một nút và nối hai pixel với nhau khi chúng chung cạnh và đều có màu ban đầu. Vùng này gồm tất cả những pixel bạn có thể đi đến từ điểm bắt đầu, vì vậy có thể dùng bất kỳ thuật toán tìm kiếm đồ thị nào để tìm vùng đó.
Giữ một ngăn xếp các điểm ảnh cần xem xét. Tô màu một điểm ảnh ngay khi bạn đưa nó vào ngăn xếp, để điểm ảnh đã tô không còn khớp và không bị đưa vào ngăn xếp lần nữa. Trước tiên, hãy kiểm tra xem màu mới có bằng màu cũ không.
Lời giải
Xem mỗi pixel như một nút và nối hai pixel có màu ban đầu khi chúng chung một cạnh. Vùng đó là phần liên thông chứa điểm bắt đầu, và mọi cách tìm kiếm bắt đầu từ đó, chỉ đi qua các pixel cùng màu, đều sẽ tìm thấy vùng này. Hãy chú ý nếu màu mới trùng với màu cũ, và nếu vùng dài, quanh co khiến đệ quy bị tràn.
Tìm kiếm theo chiều sâu đệ quy
Đúng, nhưng không chạy xong với các test lớn nhất
Ý tưởng
paint(r, c) tô một pixel bên trong ảnh nếu pixel đó vẫn còn màu cũ, sau đó tự gọi hàm với bốn pixel lân cận. Mọi pixel trong vùng đều nối với điểm bắt đầu bằng một đường đi gồm các pixel có màu cũ, và các lần gọi hàm lần theo đường đi đó. Tô màu trước khi gọi hàm sẽ ngăn quá trình lan màu quay ngược lại, vì pixel đã được tô không còn khớp màu nữa. Cách này chỉ hiệu quả khi hai màu khác nhau, vì vậy hãy trả về ngay nếu chúng giống nhau.
Đệ quy đi sâu đến bằng độ dài đường đi mà nó lần theo. Một đường ngoằn ngoèo rộng một pixel trong ảnh 80 × 80 dài khoảng 3.200 pixel, nên các lần gọi lồng nhau sâu khoảng 3.200 cấp, còn ảnh lớn hơn sẽ làm tràn mọi ngăn xếp lời gọi.
Thuật toán
- Đọc
old = image[sr][sc]. Nếuoldbằngcolor, trả về ảnh. - Định nghĩa
paint(r, c): kết thúc nếu(r, c)nằm ngoài ảnh hoặc màu của nó không phải làold. - Nếu không, đặt
image[r][c] = colorrồi gọipaintcho các điểm ảnh phía trên, phía dưới, bên trái và bên phải. - Gọi
paint(sr, sc)rồi trả về ảnh.
def floodFill(image, sr, sc, color):
old = image[sr][sc]
if old == color:
return image
rows, cols = len(image), len(image[0])
def paint(r, c):
# Stop outside the image and at any pixel that is not the old color.
if r < 0 or r >= rows or c < 0 or c >= cols or image[r][c] != old:
return
image[r][c] = color
paint(r + 1, c)
paint(r - 1, c)
paint(r, c + 1)
paint(r, c - 1)
paint(sr, sc)
return imageTìm kiếm theo chiều sâu với ngăn xếp tường minh
Ý tưởng
Thực hiện cùng cách duyệt bằng một ngăn xếp do bạn tự quản lý thay vì ngăn xếp lời gọi. Tô màu cho một điểm ảnh khi bạn đẩy nó vào ngăn xếp, không phải khi lấy nó ra. Khi đó, việc kiểm tra màu cũng đồng thời kiểm tra điểm ảnh đã được thăm hay chưa: không điểm ảnh nào vào ngăn xếp hai lần, và bạn không cần một lưới đánh dấu riêng.
Mỗi điểm ảnh trong vùng được đẩy vào ngăn xếp một lần và kiểm tra bốn điểm ảnh lân cận, nên thời gian là O(m × n). Ngăn xếp nằm trong bộ nhớ thông thường, vì vậy đường ngoằn ngoèo dài 3,200 điểm ảnh không thành vấn đề.
Thuật toán
- Đọc
old = image[sr][sc]. Nếuoldbằngcolor, trả về hình ảnh. - Tô màu
(sr, sc)rồi đẩy nó vào ngăn xếp. - Lấy một điểm ảnh ra khỏi ngăn xếp và xem xét bốn điểm lân cận.
- Với mỗi điểm lân cận nằm trong hình ảnh có màu
old, hãy tô màu cho nó rồi đẩy nó vào ngăn xếp. - Khi ngăn xếp trống, trả về hình ảnh.
def floodFill(image, sr, sc, color):
old = image[sr][sc]
# Painting a region its own color changes nothing. Returning here also
# stops the search from pushing the same cells forever.
if old == color:
return image
rows, cols = len(image), len(image[0])
image[sr][sc] = color
stack = [(sr, sc)]
while stack:
r, c = stack.pop()
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 image[nr][nc] == old:
# Paint on push: a painted cell no longer matches old,
# so it can never be pushed twice.
image[nr][nc] = color
stack.append((nr, nc))
return image
Lỗi thường gặp và trường hợp biên
- Đọc
image[sr][sc]sau khi tô màu cho điểm ảnh (pixel). Hãy lưu màu cũ trước, nếu không, bạn sẽ so sánh mọi điểm ảnh lân cận với màu mới. - Đếm các điểm ảnh lân cận theo đường chéo. Các điểm ảnh chỉ chạm nhau ở góc không được xem là liền kề.
- Kiểm tra màu của điểm ảnh lân cận trước khi kiểm tra xem điểm ảnh đó có nằm trong ảnh hay không. Trước tiên, hãy kiểm tra
0 ≤ row < rowsvà0 ≤ col < cols. - Dùng đệ quy trong Python, ngôn ngữ này có giới hạn mặc định là 1.000 lần gọi, khiến chương trình dừng ở hình con rắn phía trên.
- Tô màu mọi điểm ảnh trong ảnh có màu cũ. Những điểm ảnh có màu đó nhưng bị ngăn cách với điểm bắt đầu vẫn giữ nguyên màu.
Câu hỏi thường gặp3
Độ phức tạp thời gian của Flood Fill là gì?
O(m × n) cho ảnh có m hàng và n cột. Mỗi pixel trong vùng được đưa vào ngăn xếp một lần và kiểm tra bốn pixel lân cận, còn các pixel bên ngoài vùng chỉ được xét như pixel lân cận. Ngăn xếp chứa tối đa m × n pixel khi toàn bộ ảnh là một vùng.
Bạn nên dùng BFS hay DFS cho Flood Fill?
Cả hai đều chạy trong thời gian O(m × n). Vùng được tô giống nhau bất kể thứ tự duyệt, vì vậy hàng đợi (queue) khi duyệt theo chiều rộng (breadth first) và ngăn xếp (stack) khi duyệt theo chiều sâu (depth first) đều tô cùng một tập điểm ảnh. Hãy chọn cách nào ngắn hơn để viết và tránh dùng đệ quy (recursion) với ảnh lớn.
Vì sao Flood Fill lặp vô hạn khi màu mới giống màu cũ?
Giải pháp thông thường xem “vẫn còn màu cũ” là “chưa được thăm”. Khi màu mới trùng với màu cũ, việc tô màu không làm thay đổi gì, nên các ô lân cận liên tục đẩy nhau trở lại ngăn xếp. Trong trường hợp đó, hãy trả về ảnh không thay đổi; đó đã là đáp án đúng.
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 floodFill(image, sr, sc, color):
# Viết code tại đâyKiểm thử #1
Kiểm thử #2
Kiểm thử #3
Đối số
image = [[1, 1, 0], [1, 0, 1], [1, 1, 1]] sr = 0 sc = 0 color = 5
Đầu ra mong đợi
[[5, 5, 0], [5, 0, 5], [5, 5, 5]]