Swim in Rising Water
Bạn có một lưới n × n gồm các độ cao, chứa mọi số từ 0 đến n²-1 đúng một lần. Ở thời điểm t, nước dâng đến độ cao t ở mọi nơi, vì vậy mọi ô có độ cao bằng hoặc thấp hơn t đều chìm dưới nước. Bạn bắt đầu ở ô trên cùng bên trái. Bạn có thể bơi, không mất thời gian, đến một ô chung cạnh với ô hiện tại khi cả hai ô đều chìm dưới nước. Hãy trả về thời điểm sớm nhất bạn có thể đến ô dưới cùng bên phải.
Hàm
- gridinteger-2d-array
- các chiều cao, dưới dạng danh sách gồm n hàng, mỗi hàng có n số
- Trả vềinteger
- thời điểm sớm nhất bạn có thể đến ô dưới cùng bên phải
Ràng buộc
n == grid.length == grid[i].length1 ≤ n ≤ 1000 ≤ grid[i][j] ≤ n²-1- Mỗi giá trị từ 0 đến
n²-1xuất hiện đúng một lần.
Ví dụ
- Đầu vào
- grid = [[0, 2], [3, 1]]
- Đầu ra
- 2
- Giải thích
- Qua ô trên cùng bên phải, đường đi là 0, 2, 1, với ô cao nhất là 2. Qua ô dưới cùng bên trái, đường đi là 0, 3, 1, với ô cao nhất là 3. Ở thời điểm 2, đường đi đầu tiên bị ngập nước.
- Đầu vào
- grid = [[0, 1, 2, 3, 4], [24, 23, 22, 21, 5], [12, 13, 14, 15, 16], [11, 17, 18, 19, 20], [10, 9, 8, 7, 6]]
- Đầu ra
- 16
- Giải thích
- Ở thời điểm 15, bạn chỉ đến được hàng trên cùng và ô 5 bên dưới cuối hàng đó; mọi đường thoát đều đi qua 16 trở lên. Đi vòng từ 16 qua 15, 14, 13, 12, 11 rồi đến hàng dưới cùng, bạn không bao giờ vượt quá 16.
- Đầu vào
- grid = [[3, 0], [1, 2]]
- Đầu ra
- 3
- Giải thích
- Ô xuất phát có độ cao 3, nên bạn không thể ở trong ô đó và cũng không thể rời khỏi ô đó trước thời điểm 3. Đến lúc đó, toàn bộ lưới đã chìm dưới nước.
Câu hỏi mở rộng
Nếu chiều cao có thể lặp lại và đạt đến 10^9, cách tiếp cận nào của bạn vẫn hoạt động mà không cần thay đổi, và bạn sẽ tìm kiếm nhị phân trên những gì?
Gợi ý
Mở từng gợi ý một. Mỗi gợi ý tiết lộ thêm một chút.
Giả sử bạn biết mực nước
t. Bạn có thể cho biết có lối đi nào xuyên qua hay không? Câu trả lời thay đổi thế nào khittăng lên?Một tuyến đường cần nước phủ kín mọi ô trên tuyến đó, nên thời gian cần để đi hết tuyến đường bằng thời gian của ô lâu nhất. Bạn muốn tìm tuyến đường giữa hai góc có ô lâu nhất với thời gian ngắn nhất có thể.
Hoặc tìm kiếm nhị phân trên
tvới phép tô vùng làm phép kiểm tra, hoặc chạy thuật toán Dijkstra với một đống tối thiểu, trong đó thời gian tại một ô là giá trị lớn hơn giữa thời điểm bạn đến ô đó và độ cao của chính ô đó. Dừng lại khi ô dưới cùng bên phải được lấy ra khỏi đống.
Lời giải
Một đường đi cần mực nước cao bằng ô cao nhất trên đường, vì nước phải phủ mọi ô bạn đi qua. Vì vậy, hãy tìm đường đi giữa hai góc sao cho ô cao nhất trên đường là thấp nhất: một đường đi ngắn nhất có chi phí là giá trị lớn nhất, không phải tổng các giá trị. Bạn có thể nâng mực nước từng bước, tìm kiếm nhị phân (binary search) mức nước hoặc chạy thuật toán Dijkstra với chi phí đó.
Nâng mực nước lên từng nấc một
Đúng, nhưng không chạy xong với các test lớn nhất
Ý tưởng
Cố định mực nước t. Thực hiện một lần đổ đầy từ góc trên bên trái, đi qua các ô có độ cao không vượt quá t, để xác định xem có thể đến được góc dưới bên phải hay không. Bắt đầu t ở độ cao của góc cao hơn, max(grid[0][0], grid[n-1][n-1]), vì cả hai góc đều phải nằm dưới nước, rồi tăng thêm 1 cho đến khi dòng nước đến được đích. Nước dâng chỉ mở thêm các ô, nên mực nước đầu tiên thỏa mãn là đáp án.
Mỗi lần đổ đầy tốn O(n²), và mực nước có thể phải dâng gần n² lần: trên lưới 100 × 100, tối đa 10^4 mực nước × 10^4 ô, tương đương khoảng 10^8 lượt thăm ô.
Thuật toán
- Đặt
tbằng chiều cao lớn hơn trong hai góc. - Tô loang từ góc trên bên trái qua các ô có chiều cao không vượt quá
t, dùng ngăn xếp tường minh và đánh dấu đã thăm cho từng ô. - Nếu vùng tô chạm đến góc dưới bên phải, trả về
t. - Nếu không, cộng 1 vào
trồi tô lại.
def canReach(grid, t):
# True when you can swim from the top left to the bottom right with the water at height t.
n = len(grid)
seen = [[False] * n for _ in range(n)]
seen[0][0] = True
stack = [(0, 0)]
while stack:
r, c = stack.pop()
if r == n - 1 and c == n - 1:
return True
for nr, nc in ((r + 1, c), (r - 1, c), (r, c + 1), (r, c - 1)):
if 0 <= nr < n and 0 <= nc < n and not seen[nr][nc] and grid[nr][nc] <= t:
seen[nr][nc] = True
stack.append((nr, nc))
return False
def swimInWater(grid):
n = len(grid)
# You cannot finish before the water covers both corners.
t = max(grid[0][0], grid[n - 1][n - 1])
# Raise the water one step at a time until a way through opens.
while not canReach(grid, t):
t += 1
return tTìm kiếm nhị phân theo mực nước
Ý tưởng
Phép thử tô vùng (flood fill) thất bại ở mọi mức thấp hơn đáp án và thành công ở mọi mức từ đáp án trở lên. Tìm kiếm nhị phân tìm ra mức chuyển đổi đó, nằm giữa góc cao hơn và n²-1, nơi toàn bộ lưới bị ngập nước.
Trong ví dụ 5 × 5, lo = 6 và hi = 24. Mức 15 thất bại, nên lo = 16. Các mức 20, 18, 17 và 16 đều thành công, và phép tìm kiếm kết thúc ở mức 16 sau năm lần tô.
Lưới 100 × 100 có 10^4 mức, nên cần khoảng 14 lần tô để xác định kết quả: khoảng 1.4 × 10^5 lượt duyệt ô thay vì 10^8.
Thuật toán
- Đặt
lobằng độ cao của góc cao hơn vàhibằngn²-1. - Trong khi
lo < hi, lấymid = (lo + hi) / 2, làm tròn xuống. - Thực hiện tô vùng tại mức
mid. Nếu vùng đó đến được góc dưới bên phải, đặthi = mid; nếu không, đặtlo = mid + 1. - Trả về
lo.
def canReach(grid, t):
# True when you can swim from the top left to the bottom right with the water at height t.
n = len(grid)
seen = [[False] * n for _ in range(n)]
seen[0][0] = True
stack = [(0, 0)]
while stack:
r, c = stack.pop()
if r == n - 1 and c == n - 1:
return True
for nr, nc in ((r + 1, c), (r - 1, c), (r, c + 1), (r, c - 1)):
if 0 <= nr < n and 0 <= nc < n and not seen[nr][nc] and grid[nr][nc] <= t:
seen[nr][nc] = True
stack.append((nr, nc))
return False
def swimInWater(grid):
n = len(grid)
# The answer lies between the higher corner and the highest cell.
lo = max(grid[0][0], grid[n - 1][n - 1])
hi = n * n - 1
# canReach is false below the answer and true from it on: find the first true.
while lo < hi:
mid = (lo + hi) // 2
if canReach(grid, mid):
hi = mid
else:
lo = mid + 1
return loDijkstra trên ô cao nhất của tuyến đường
Ý tưởng
Gán cho mỗi đường đi một chi phí: ô có độ cao lớn nhất trên đường đó. Thuật toán Dijkstra vẫn hoạt động, vì khi nối dài đường đi đến một ô có độ cao h, chi phí là max(old cost, h), không bao giờ thấp hơn chi phí cũ. Dùng một hàng đợi ưu tiên nhỏ nhất chứa các ô, sắp theo thời gian đó, bắt đầu từ ô trên cùng bên trái tại grid[0][0]. Khi ô dưới cùng bên phải rời khỏi hàng đợi, thời gian của nó là đáp án.
Bạn có thể đánh dấu một ô là đã thấy ngay lần đầu tiên đưa ô đó vào hàng đợi. Các ô rời hàng đợi theo thứ tự thời gian, nên ô đầu tiên đến một ô lân cận sẽ cho ô đó thời gian nhỏ nhất mà bất kỳ ô nào có thể đạt được, và mỗi ô chỉ vào hàng đợi một lần.
Hàng đợi chứa rìa của vùng bạn có thể đến, và mỗi lần lấy một ô ra thì mực nước tăng vừa đủ để bạn bước đến đó. Trong ví dụ 5 × 5, các lần lấy ô ra có thời gian 0, 1, 2, 3, 4, 5, rồi đến cổng ở mức 16, và ô dưới cùng bên phải rời hàng đợi với thời gian 16.
Thuật toán
- Đánh dấu ô trên cùng bên trái là đã thăm và đưa ô đó vào hàng đợi với thời gian
grid[0][0]. - Lấy ra ô có thời gian nhỏ nhất
t. Nếu đó là ô dưới cùng bên phải, trả vềt. - Với mỗi ô láng giềng chưa được thăm, đánh dấu ô đó và đưa vào hàng đợi với thời gian
max(t, its height). - Lặp lại từ bước 2.
import heapq
def swimInWater(grid):
n = len(grid)
seen = [[False] * n for _ in range(n)]
seen[0][0] = True
# (time, row, col): the time is the highest cell on the best path found to that cell.
heap = [(grid[0][0], 0, 0)]
while True:
t, r, c = heapq.heappop(heap)
# Cells leave the heap in order of time, so this is the earliest you can be here.
if r == n - 1 and c == n - 1:
return t
for nr, nc in ((r + 1, c), (r - 1, c), (r, c + 1), (r, c - 1)):
if 0 <= nr < n and 0 <= nc < n and not seen[nr][nc]:
# Reached for the first time from the cell with the smallest time:
# no later route can arrive earlier, so mark it now.
seen[nr][nc] = True
heapq.heappush(heap, (max(t, grid[nr][nc]), nr, nc))
Lỗi thường gặp và trường hợp biên
- Bỏ qua độ cao của chính ô góc. Đáp án ít nhất là
grid[0][0]vàgrid[n-1][n-1]: với[[3, 0], [1, 2]], đáp án là 3. - Đi tham lam đến ô lân cận thấp nhất. Đường đi tốt nhất có thể phải leo lên một cổng rồi đi đường vòng xa hơn, như trong ví dụ 5 × 5.
- Cộng độ cao dọc đường đi như trong bài toán tìm đường đi ngắn nhất thông thường. Thời gian mới là
max(t, height), không phảit + height. - Dùng đệ quy để lấp đầy vùng. Một đường đi quanh co có thể dài hàng nghìn ô, vượt quá giới hạn 1.000 lời gọi lồng nhau của Python.
Câu hỏi thường gặp3
Độ phức tạp thời gian của Swim in Rising Water là bao nhiêu?
O(n² log n) với thuật toán Dijkstra: mỗi ô trong n² ô được đưa vào và lấy ra khỏi heap nhiều nhất một lần. Tìm kiếm nhị phân trên mực nước có cùng độ phức tạp, khoảng log2(n²) lần lấp đầy với độ phức tạp O(n²) mỗi lần. Cả hai đều dùng O(n²) bộ nhớ.
Liệu có thể giải bài toán Can Swim in Rising Water bằng tìm kiếm nhị phân không?
Đúng vậy. Khả năng bạn băng qua được ở mức t chỉ chuyển một lần, từ sai sang đúng, tại đáp án. Dùng tìm kiếm nhị phân trên t, với thuật toán tô vùng (flood fill) làm phép kiểm tra, cần khoảng log2(n²) lần tô vùng, tức 14 lần với lưới 100 × 100.
Union-find có thể giải bài toán Swim in Rising Water không?
Có. Mở các ô theo thứ tự chiều cao, nối mỗi ô mới với các ô lân cận đã mở, rồi dừng ngay khi hai góc nằm trong cùng một tập hợp. Chiều cao của ô bạn mở cuối cùng là đáp án. Vì mỗi giá trị từ 0 đến n²-1 chỉ xuất hiện một lần, một bảng ánh xạ từ chiều cao đến ô sẽ cho biết thứ tự mà không cần sắp xếp.
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 swimInWater(grid):
# Viết code tại đâyKiểm thử #1
Kiểm thử #2
Kiểm thử #3
Đối số
grid = [[0, 2], [3, 1]]
Đầu ra mong đợi
2