Rat in a Maze
Một con chuột bắt đầu ở ô trên cùng bên trái của n × n maze, trong đó 1 là ô trống và 0 là tường, và phải đến ô dưới cùng bên phải. Mỗi lần di chuyển, chuột đi một ô xuống dưới (D), sang trái (L), sang phải (R) hoặc lên trên (U) vào một ô trống. Đường đi không bao giờ đi qua cùng một ô hai lần và kết thúc ở lối ra.
Trả về mọi đường đi dưới dạng chuỗi các bước di chuyển, theo thứ tự bảng chữ cái, hoặc một mảng rỗng nếu không có đường đi nào.
Hàm
- mazeinteger-2d-array
- lưới n × n, với 1 là ô trống và 0 là tường
- Trả vềstring-array
- mọi đường đi từ góc trên bên trái đến góc dưới bên phải dưới dạng chuỗi các bước di chuyển, theo thứ tự bảng chữ cái
Ràng buộc
2 ≤ n ≤ 5maze[i][j]là0hoặc1.- Mỗi mê cung trong test có nhiều nhất 2.000 đường đi. Mê cung 5 × 5 không có vật cản có 8.512 đường đi, nên không nằm trong các test.
Ví dụ
- Đầu vào
- maze = [[1, 1, 0, 0], [0, 1, 1, 0], [1, 1, 1, 0], [1, 0, 1, 1]]
- Đầu ra
- ["RDDRDR", "RDRDDR"]
- Giải thích
- Lúc đầu, chỉ ô bên phải được mở, rồi từ đó chỉ ô bên dưới được mở, nên cả hai đường đi đều bắt đầu bằng
RD.RDDRDRđược xếp trước vì nước đi thứ ba,D, đứng trướcR.
- Đầu vào
- maze = [[1, 1, 1], [1, 0, 1], [1, 1, 0]]
- Đầu ra
- []
- Giải thích
- Lối ra, ô ở góc dưới bên phải, là một bức tường, nên không có đường đi nào có thể kết thúc ở đó và đáp án là rỗng, dù mọi ô khác đều thông thoáng.
Câu hỏi mở rộng
Với cả bốn nước đi đều được phép, bạn có thể đếm số đường đi mà không cần liệt kê chúng không? n có thể lớn đến mức nào trước khi không thể đếm nổi?
Gợi ý
Mở từng gợi ý một. Mỗi gợi ý tiết lộ thêm một chút.
Đường đi là một chuỗi các bước di chuyển từ ô bắt đầu, vì vậy hãy tạo đường đi từng bước một. Từ mỗi ô, có tối đa bốn bước di chuyển: những bước nào được phép?
Tiến về phía trước bằng một hàm đệ quy, rồi đánh dấu ô hiện tại là đã dùng trước khi rời đi để đường đi không thể quay lại ô đó. Khi lời gọi hàm trả về, hãy bỏ đánh dấu ô đó: đường đi khác vẫn có thể đi qua ô này.
Thử các nước đi theo thứ tự
D,L,R,U, tức là theo thứ tự bảng chữ cái. Hai đường đi khác nhau đầu tiên ở nước đi tại điểm chúng tách ra, và nhánh có chữ cái nhỏ hơn được khám phá trước, nên các đường đi đã được sắp xếp sẵn.
Lời giải
Bạn phải liệt kê mọi đường đi, không lặp lại và không bỏ sót. Thuật toán quay lui làm được điều đó: mở rộng đường đi từng bước một, đánh dấu các ô mà đường đi sử dụng, rồi bỏ đánh dấu từng ô khi bạn quay lui khỏi ô đó. Thử các nước đi theo thứ tự bảng chữ cái sẽ giúp sắp xếp các đường đi theo thứ tự.
Mở rộng mọi đường đi theo chiều rộng trước, rồi sắp xếp
Ý tưởng
Giữ một hàng đợi các đường đi chưa hoàn chỉnh, mỗi đường đi gồm ô hiện tại, các ô đã đi qua và các nước đi. Lấy một đường đi ra rồi thêm lại mọi đường đi mở rộng thêm một nước đi đến ô còn mở và chưa đi qua, hoặc lưu đường đi đó nếu nó đến lối ra. Mỗi đường đi được tìm thấy đúng một lần. Với tối đa 25 ô, tập hợp các ô đã đi qua vừa trong một số nguyên: bit r * n + c đại diện cho ô (r, c).
Hàng đợi đưa ra các đường đi ngắn nhất trước, nhưng thứ tự đó không phải thứ tự sắp xếp. Trong mê cung 5 × 5 thông thoáng, đường đi 16 nước DDDDRRRULLURRRDD được sắp xếp trước đường đi 8 nước DDDRDRRR. Vì vậy, bạn sắp xếp ở cuối, còn hàng đợi lưu cả một lớp đường đi chưa hoàn chỉnh trong bộ nhớ.
Thuật toán
- Nếu điểm bắt đầu hoặc lối ra là tường, hãy trả về một mảng rỗng.
- Đưa điểm bắt đầu vào hàng đợi: ô
(0, 0), tập hợp ô đã đi qua được đánh dấu bằng bit 0, không có bước di chuyển nào. - Lấy mục tiếp theo. Nếu mục đó nằm ở lối ra, hãy lưu các bước di chuyển của nó rồi tiếp tục.
- Nếu không, với mỗi bước di chuyển đến một ô trống, chưa đi qua bên trong mê cung, hãy thêm một mục gồm ô đó, tập hợp ô đã đi qua có thêm bit của ô đó và bước di chuyển được nối vào.
- Khi hàng đợi trống, hãy sắp xếp các đường đi đã lưu rồi trả về chúng.
from collections import deque
def findPaths(maze):
n = len(maze)
if maze[0][0] == 0 or maze[n - 1][n - 1] == 0:
return []
moves = (("D", 1, 0), ("L", 0, -1), ("R", 0, 1), ("U", -1, 0))
result = []
# Each entry is a path in progress: its cell, the cells it has used
# (bit r * n + c), and its moves so far.
queue = deque([(0, 0, 1, "")])
while queue:
r, c, used, path = queue.popleft()
if r == n - 1 and c == n - 1:
result.append(path)
continue
for letter, dr, dc in moves:
nr, nc = r + dr, c + dc
if 0 <= nr < n and 0 <= nc < n and maze[nr][nc] == 1:
bit = 1 << (nr * n + nc)
if not used & bit:
queue.append((nr, nc, used | bit, path + letter))
result.sort() # shorter paths were found first, so sort by text
return resultQuay lui theo thứ tự bảng chữ cái
Ý tưởng
Một hàm đệ quy đứng trên một ô. Khi đến lối ra, hàm lưu các bước đi. Nếu không, hàm đặt ô thành 0 để đường đi không thể quay lại, thử từng bước đi sang một ô trống rồi đặt ô trở lại thành 1, vì một đường đi sau đó có thể đi qua ô này. Bộ nhớ lưu đường đi hiện tại và độ sâu đệ quy tối đa là n².
Hai đường đi có chung các bước cho đến bước đầu tiên chúng khác nhau; tại đó, quá trình tìm kiếm duyệt nhánh có ký tự nhỏ hơn trước. Đường đi cũng dừng ở lối ra, vì vậy không có đường đi nào là phần đầu của đường đi khác. Với thứ tự thử các bước đi là D, L, R, U, các đường đi được lưu theo thứ tự đã sắp xếp.
Thuật toán
- Nếu điểm bắt đầu hoặc lối ra là tường, hãy trả về một mảng rỗng.
- Viết
backtrack(r, c). Nếu(r, c)là lối ra, hãy lưu các bước di chuyển hiện tại rồi trả về. - Đặt
maze[r][c]thành 0. - Với mỗi bước di chuyển theo thứ tự
D,L,R,Udẫn đến một ô trống bên trong mê cung, hãy thêm chữ cái tương ứng, gọi đệ quy rồi xóa chữ cái đó. - Đặt
maze[r][c]trở lại thành 1. Gọibacktrack(0, 0)rồi trả về các đường đi đã lưu.
def findPaths(maze):
n = len(maze)
result = []
if maze[0][0] == 0 or maze[n - 1][n - 1] == 0:
return result
# The moves in alphabetical order, so the paths come out sorted.
moves = (("D", 1, 0), ("L", 0, -1), ("R", 0, 1), ("U", -1, 0))
path = []
def backtrack(r, c):
if r == n - 1 and c == n - 1:
result.append("".join(path))
return
maze[r][c] = 0 # block the cell while this path uses it
for letter, dr, dc in moves:
nr, nc = r + dr, c + dc
if 0 <= nr < n and 0 <= nc < n and maze[nr][nc] == 1:
path.append(letter)
backtrack(nr, nc)
path.pop()
maze[r][c] = 1 # open it again for the other paths
backtrack(0, 0)
return result
Lỗi thường gặp và trường hợp biên
- Không bỏ đánh dấu ô khi bạn quay lại. Ô vẫn bị chặn, nên không nhánh nào sau đó có thể đi qua. Trong mê cung mở 3 × 3, cách này trả về
DDRRvàDDRURD, tức 2 trong số 12 đường đi. - Quên rằng ô bắt đầu hoặc ô thoát có thể là tường. Nếu ô thoát có tường, như trong ví dụ thứ hai, kết quả phải là danh sách rỗng, không phải đường đi kết thúc ở ô bên cạnh.
- Thử các bước đi theo thứ tự xuống, lên, trái, phải. Tập hợp các đường đi là đúng, nhưng thứ tự của chúng thì không;
D,L,R,Ulà thứ tự theo bảng chữ cái.
Câu hỏi thường gặp3
Độ phức tạp thời gian của Rat in a Maze là bao nhiêu?
Sau bước đi đầu tiên, tại mỗi ô, một đường đi có nhiều nhất ba lựa chọn và đi qua nhiều nhất n² ô, nên có O(3^(n²)) đường đi từng phần, mỗi đường cần O(n²) thời gian để ghi ra. Tường và các ô đã đi qua làm giảm đáng kể số này.
Vì sao phải xóa dấu đã ghé thăm khi quay lui?
Dấu đánh dấu phải có nghĩa là “trên đường đi hiện tại”, không phải “đã đi qua”. Hai đường đi khác nhau có thể cùng đi qua một ô. Nếu ô vẫn được đánh dấu sau khi quá trình tìm kiếm quay lui khỏi ô đó, mọi đường đi qua ô đó về sau đều sẽ bị bỏ lỡ.
Nếu chú chuột chỉ có thể di chuyển sang phải và xuống dưới thì sao?
Khi đó, không đường đi nào có thể quay lại một ô, nên bạn không cần đánh dấu các ô đã đi qua, và mỗi đường đi đều có chính xác 2n-2 bước. Việc đếm số đường đi trở thành bài toán quy hoạch động: số cách đến một ô bằng tổng số cách đến ô trống ngay phía trên và ô trống ngay bên trá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 findPaths(maze):
# Viết code tại đâyKiểm thử #1
Kiểm thử #2
Đối số
maze = [[1, 1, 0, 0], [0, 1, 1, 0], [1, 1, 1, 0], [1, 0, 1, 1]]
Đầu ra mong đợi
["RDDRDR", "RDRDDR"]