Word Search
Bạn nhận được một bảng chữ cái board, một danh sách chuỗi, trong đó board[r][c] là chữ cái ở hàng r, cột c, và một chuỗi word.
Trả về true nếu bạn có thể lần theo word: bắt đầu từ bất kỳ ô nào rồi mỗi lần di chuyển đến ô phía trên, phía dưới, bên trái hoặc bên phải, sao cho các ô bạn đi qua ghép thành word theo đúng thứ tự và không sử dụng ô nào quá một lần. Nếu không, trả về false. Phân biệt chữ hoa và chữ thường.
Hàm
- boardstring-array
- lưới, mỗi hàng là một chuỗi chữ cái
- wordstring
- từ cần lần theo
- Trả vềboolean
- liệu từ đó có thể được lần theo qua các ô đặt cạnh nhau hay không, mỗi ô chỉ được dùng tối đa một lần
Ràng buộc
1 ≤ board.length ≤ 61 ≤ board[i].length ≤ 6, và mọi hàng đều có cùng độ dài.1 ≤ word.length ≤ 20boardvàwordchỉ chứa các chữ cái tiếng Anh, cả chữ hoa lẫn chữ thường.
Ví dụ
- Đầu vào
- board = ["STAR", "POOL", "ENDS"]word = "STOOLS"
- Đầu ra
- true
- Giải thích
- Bắt đầu ở
Strên cùng bên trái, đi sang phải đếnT, đi xuống đếnO, đi sang phải đếnOvàL, rồi đi xuống đếnSbên dướiL. Không ô nào bị lặp lại.
- Đầu vào
- board = ["STAR", "POOL", "ENDS"]word = "POP"
- Đầu ra
- false
- Giải thích
- Trên bảng có một
P. SauPvàO, bạn cần thêm mộtP, nhưng ô duy nhất có ký tự này là ô bắt đầu, không thể dùng hai lần.
- Đầu vào
- board = ["STAR", "POOL", "ENDS"]word = "SAND"
- Đầu ra
- false
- Giải thích
- Mọi chữ cái của
SANDđều có trên bảng, nhưng chữAduy nhất, ở hàng 0, cột 2, không tiếp xúc với chữS.
Câu hỏi mở rộng
Thay vì trả lời có hoặc không, bạn có thể đếm xem bảng có bao nhiêu cách lần theo word khác nhau không?
Gợi ý
Mở từng gợi ý một. Mỗi gợi ý tiết lộ thêm một chút.
Hãy thử từng ô làm vị trí bắt đầu của từ. Khi một ô khớp với chữ cái hiện tại, những ô nào có thể chứa chữ cái tiếp theo?
Đây là quá trình tìm kiếm theo các đường đi: ở mỗi chữ cái, bạn chọn một trong tối đa bốn ô lân cận, nếu chọn sai, bạn quay lại và thử ô khác. Vì một đường đi không thể dùng lại một ô, hãy đánh dấu ô khi ô đó nằm trên đường đi hiện tại và bỏ đánh dấu khi bạn quay lui khỏi ô đó.
Viết
dfs(r, c, i): trả về thất bại nếu(r, c)nằm ngoài lưới, đã có trên đường đi hoặc không phảiword[i]; trả về thành công nếuilà chỉ số cuối cùng; nếu không, đánh dấu ô, thử bốn ô lân cận vớii+1, bỏ đánh dấu ô rồi cho biết liệu có ô lân cận nào thành công hay không. Trước khi tìm kiếm, hãy kiểm tra xem bảng có đủ số lượng của từng chữ cái hay không, rồi bắt đầu từ đầu có chữ cái hiếm hơn của từ.
Lời giải
Bạn cần tìm các đường đi qua ô lưới. Thuật toán quay lui mở rộng từng đường đi thêm một chữ cái mỗi lần, đánh dấu từng ô khi đường đi đi qua và bỏ đánh dấu khi quay lui, nhờ đó không đường đi nào dùng lại một ô. Tìm kiếm có độ phức tạp theo hàm mũ theo độ dài của từ, điều mà bảng 6 × 6 cho phép.
Quay lui với lưới đã đánh dấu
Ý tưởng
Đường đi khớp với i chữ cái đầu tiên sẽ tiếp tục đến một ô lân cận chứa word[i] và chưa nằm trên đường đi. Nếu không có ô lân cận nào như vậy thì đó là đường cụt, và bạn quay lui.
Việc bỏ đánh dấu giúp cách này hoạt động chính xác: ô mà một nhánh cụt đã đi qua phải được để trống cho lần thử tiếp theo. Với AA / AB và AAA, cả hai lần thử bắt đầu từ ô trên cùng bên trái đều bị mắc kẹt. Nếu các ô đó vẫn được đánh dấu, bạn sẽ bỏ lỡ đáp án: ô dưới cùng bên trái, rồi ô trên cùng bên trái, rồi ô trên cùng bên phải.
Sau bước đi đầu tiên, mỗi bước có tối đa ba hướng mới, nên quá trình tìm kiếm có thể khám phá đến m·n·3^L đường đi. Trên bảng 5 × 5 gồm các chữ A, với 8 chữ A rồi đến một chữ B, thuật toán kiểm tra khoảng 53.000 ô trước khi trả lời false.
Thuật toán
- Tạo một lưới
visited, ban đầu tất cả đều là false. dfs(r, c, i): trả về false nếu(r, c)nằm ngoài lưới, đã được thăm hoặc không phảiword[i]; trả về true nếuilà chỉ số cuối cùng.- Nếu không, đánh dấu
(r, c), thử bốn ô lân cận vớii+1, bỏ đánh dấu ô đó rồi trả về liệu có ô nào thành công hay không. - Trả về true nếu
dfs(r, c, 0)thành công từ bất kỳ ô nào.
def exist(board, word):
rows, cols = len(board), len(board[0])
visited = [[False] * cols for _ in range(rows)]
def dfs(r, c, i):
# Can word[i:] be traced starting at cell (r, c)?
if r < 0 or r >= rows or c < 0 or c >= cols:
return False
if visited[r][c] or board[r][c] != word[i]:
return False
if i == len(word) - 1:
return True
visited[r][c] = True # mark: the current path owns this cell
found = (dfs(r + 1, c, i + 1) or dfs(r - 1, c, i + 1)
or dfs(r, c + 1, i + 1) or dfs(r, c - 1, i + 1))
visited[r][c] = False # restore: other paths may use it
return found
for r in range(rows):
for c in range(cols):
if dfs(r, c, 0):
return True
return FalseQuay lui với đánh dấu tại chỗ và cắt tỉa
Ý tưởng
Giữ nguyên cách tìm kiếm, nhưng thay đổi hai điểm. Đánh dấu các ô trên bản sao của bảng bằng cách ghi #, rồi khôi phục chữ cái khi quay lui. # không bao giờ khớp với chữ cái trong từ, nên bước kiểm tra chữ cái cũng loại bỏ các ô nằm trên đường đi.
Sau đó, cắt tỉa. Nếu từ cần nhiều hơn số lượng một chữ cái nào đó mà bảng có, hãy trả về false: như vậy ta xử lý được bảng toàn chữ A mà không cần tìm kiếm. Đọc đường đi theo chiều ngược lại sẽ tạo thành từ đảo ngược trên cùng các ô, nên tìm từ đảo ngược cũng cho cùng kết quả. Bắt đầu từ đầu có chữ cái hiếm hơn trên bảng: có ít ô hơn để bắt đầu, và các điểm bắt đầu sai sẽ bị loại ngay.
Khi chữ B duy nhất nằm ở một góc giữa hai chữ C, tìm kiếm xuôi kiểm tra khoảng 35.000 ô; tìm kiếm ngược chỉ kiểm tra 27 ô. Trường hợp xấu nhất vẫn là O(m·n·3^L).
Thuật toán
- Nếu từ cần nhiều ký tự hơn số ký tự trên bảng, hãy trả về false.
- Nếu bảng có nhiều bản sao của
word[0]hơn chữ cái cuối cùng, hãy đảo ngượcword. - Sao chép bảng vào một lưới mà bạn có thể thay đổi.
dfs(r, c, i): thất bại nếu ô không phải làword[i]; thành công ở chỉ số cuối cùng; nếu không, đặt nó thành#, thử từng ô lân cận nằm trong giới hạn vớii+1, khôi phục ô đó rồi trả về kết quả cho biết có bước thử nào thành công hay không.- Trả về true nếu
dfsthành công từ bất kỳ ô nào.
from collections import Counter
def exist(board, word):
rows, cols = len(board), len(board[0])
# Pruning 1: the board must hold every letter as many times as the word uses it.
have = Counter("".join(board))
for letter, need in Counter(word).items():
if have[letter] < need:
return False
# Pruning 2: a path read backwards is the same path, so start from the
# end whose letter is rarer on the board: fewer cells begin a search.
if have[word[0]] > have[word[-1]]:
word = word[::-1]
grid = [list(row) for row in board]
def dfs(r, c, i):
# Can word[i:] be traced starting at cell (r, c)?
if grid[r][c] != word[i]:
return False
if i == len(word) - 1:
return True
grid[r][c] = "#" # mark: "#" matches no letter, so this path cannot reuse the cell
found = False
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 dfs(nr, nc, i + 1):
found = True
break
grid[r][c] = word[i] # restore the letter for other paths
return found
for r in range(rows):
for c in range(cols):
if dfs(r, c, 0):
return True
return False
Lỗi thường gặp và trường hợp biên
- Không bỏ đánh dấu một ô sau khi nhánh thất bại:
AAAtrênAA/ABcho kết quả false. - Không đánh dấu ô nào: đường đi quay lại ô cuối cùng, và
POPtrên bảng ví dụ trả về true. - Đọc ô trước khi kiểm tra giới hạn: trong Python,
board[-1]là hàng cuối cùng, nên đường đi vòng quanh lưới. - Chỉ kiểm tra thành công sau khi di chuyển:
["A"]vớiAphải trả về true. - Đánh dấu bằng cách đổi chữ hoa, chữ thường của một ô: trên
["aA"], chữađược đánh dấu sẽ được đọc thành chữAthứ hai, nênaAAcho kết quả true.
Câu hỏi thường gặp3
Độ phức tạp thời gian của Word Search là gì?
O(m·n·3^L) cho bảng m × n và một từ có độ dài L: mỗi ô có thể bắt đầu một đường đi, và sau bước đầu tiên, mỗi ô có tối đa ba ô lân cận chưa được thăm. Bộ nhớ phụ cần O(L) cho đệ quy, cộng thêm O(m·n) để tạo bản sao của bảng.
Việc cắt tỉa giúp Word Search chạy nhanh hơn như thế nào?
Trước tiên, hãy đếm số chữ cái và trả về false nếu bảng có quá ít một chữ cái nào đó. Sau đó, bắt đầu tìm từ cuối bằng chữ cái hiếm hơn, vì có ít ô hơn có thể bắt đầu bằng chữ cái đó. Cả hai bước kiểm tra đều không làm thay đổi trường hợp xấu nhất.
Sự khác biệt giữa Word Search và Word Search II là gì?
Word Search II yêu cầu tìm những từ nào trong danh sách xuất hiện trên bảng. Tìm kiếm riêng từng từ sẽ lặp lại công việc, vì vậy lời giải thường đưa các từ vào một trie rồi duyệt bảng một lần, dừng một đường đi khi không có từ nào bắt đầu bằng các ký tự trê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 exist(board, word):
# Viết code ở đâyKiểm thử #1
Kiểm thử #2
Kiểm thử #3
Đối số
board = ["STAR", "POOL", "ENDS"] word = "STOOLS"
Đầu ra mong đợi
true