Alien Dictionary
Một danh sách các từ được sắp xếp theo một bảng chữ cái mà bạn không biết: 26 chữ cái tiếng Anh viết thường theo một thứ tự bí mật. Các từ được so sánh như bình thường. Vị trí đầu tiên mà hai từ khác nhau sẽ quyết định thứ tự, và nếu một từ là phần đầu của từ kia thì từ ngắn hơn đứng trước.
Trả về các chữ cái xuất hiện trong những từ đó, dưới dạng một chuỗi theo thứ tự bảng chữ cái. Nếu có nhiều thứ tự phù hợp, hãy trả về thứ tự đứng trước theo thứ tự từ điển thông thường. Nếu không có thứ tự nào phù hợp, hãy trả về "invalid".
Hàm
- wordsstring-array
- các từ được sắp xếp theo thứ tự bảng chữ cái chưa biết
- Trả vềstring
- các chữ cái theo thứ tự nhỏ nhất có thể, hoặc "invalid"
Ràng buộc
1 ≤ words.length ≤ 50001 ≤ words[i].length ≤ 10- Mỗi từ chỉ chứa các chữ cái tiếng Anh viết thường.
- Cùng một từ có thể xuất hiện nhiều lần.
Ví dụ
- Đầu vào
- words = ["tea", "ten", "ate", "act", "cat"]
- Đầu ra
- "etacn"
- Giải thích
teađứng trướctennên a đứng trước n. Các cặp còn lại cho biết t đứng trước a, t đứng trước c và a đứng trước c. Không có quy tắc nào nhắc đến e, nên thứ tự nhỏ nhất bắt đầu bằng e: e, t, a, c, n.
- Đầu vào
- words = ["bat", "tab", "tub", "bus"]
- Đầu ra
- "invalid"
- Giải thích
batđứng trướctabnghĩa là b đứng trước t, còntubđứng trướcbusnghĩa là t đứng trước b. Cả hai điều này không thể cùng đúng, nên không có thứ tự nào phù hợp.
- Đầu vào
- words = ["cooking", "cook"]
- Đầu ra
- "invalid"
- Giải thích
cooklà phần đầu củacooking, nên trong mọi bảng chữ cái, nó đứng trước. Danh sách lại đặt nó ở vị trí thứ hai, điều mà không thứ tự chữ cái nào có thể giải thích.
Câu hỏi mở rộng
Làm thế nào để biết thứ tự lắp ghép đó có phải là thứ tự duy nhất 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 xem hai từ đứng cạnh nhau, chẳng hạn như
teavàten. Chúng cho bạn biết điều gì về bảng chữ cái, và còn để ngỏ điều gì?Mỗi cặp từ liền kề tạo ra tối đa một quy tắc: tại vị trí đầu tiên mà hai từ khác nhau, chữ cái trong từ thứ nhất đứng trước chữ cái trong từ thứ hai. Các quy tắc là những cạnh trong đồ thị gồm các chữ cái, và đáp án là thứ tự tuân theo mọi cạnh. Hãy chú ý đến cặp từ không có vị trí khác nhau nào nhưng từ thứ nhất lại dài hơn.
Dùng thuật toán Kahn: đặt một chữ cái không có quy tắc nào trỏ đến nó, xóa các quy tắc của chữ cái đó rồi lặp lại. Giữ các chữ cái sẵn sàng trong một đống tối thiểu và luôn đặt chữ cái nhỏ nhất. Nếu có chữ cái không bao giờ được đặt, các quy tắc có chứa chu trình.
Lời giải
Mỗi cặp từ đứng cạnh nhau tạo ra nhiều nhất một quy tắc, tại vị trí đầu tiên mà chúng khác nhau: chữ cái x đứng trước chữ cái y. Một thứ tự phù hợp là thứ tự tô pô của các quy tắc đó. Một chu trình, hoặc một từ được đặt trước tiền tố của chính nó, khiến danh sách không hợp lệ. Một đống tối thiểu luôn chọn chữ cái nhỏ nhất đã sẵn sàng sẽ tạo ra thứ tự nhỏ nhất.
Thử mọi cách sắp xếp các chữ cái
Đúng, nhưng không chạy xong với các test lớn nhất
Ý tưởng
Kiểm tra một cách sắp xếp các chữ cái phân biệt bằng cách xét từng cặp liền kề. Ở vị trí đầu tiên khác nhau, chữ cái trong từ thứ nhất phải đứng trước chữ cái trong từ thứ hai; nếu không có điểm khác nhau, từ thứ nhất không được dài hơn. Duyệt các cách sắp xếp theo thứ tự tăng dần, bắt đầu từ thứ tự bảng chữ cái, bằng thuật toán hoán vị kế tiếp. Cách đầu tiên thỏa mãn là thứ tự nhỏ nhất phù hợp.
k chữ cái có k! cách sắp xếp: 10 chữ cái có 3,628,800 cách và 26 chữ cái có khoảng 4 × 10^26 cách, trong khi mỗi lần kiểm tra đọc tối đa 5 × 10^4 ký tự. Khi không có cách nào phù hợp, quá trình tìm kiếm thử tất cả các cách.
Thuật toán
- Thu thập các chữ cái khác nhau rồi sắp xếp theo thứ tự bảng chữ cái.
- Ghi lại thứ hạng của mỗi chữ cái trong cách sắp xếp hiện tại.
- Kiểm tra từng cặp từ liền kề: ở vị trí khác nhau đầu tiên, chữ cái của từ thứ nhất cần có thứ hạng nhỏ hơn; nếu không có điểm khác nhau, từ thứ nhất không được dài hơn.
- Nếu mọi cặp đều đạt yêu cầu, trả về cách sắp xếp đó; nếu không, chuyển sang cách sắp xếp lớn hơn tiếp theo.
- Khi không còn cách sắp xếp tiếp theo, trả về
"invalid".
from itertools import permutations
def fits(words, rank):
# The list is sorted under rank when every neighbouring pair is in order.
for first, second in zip(words, words[1:]):
for x, y in zip(first, second):
if x != y:
if rank[x] > rank[y]:
return False
break
else:
# One word starts the other: the shorter must come first.
if len(first) > len(second):
return False
return True
def alienOrder(words):
letters = sorted(set("".join(words)))
# permutations() of a sorted list yields the orders from smallest to largest,
# so the first order that fits is the answer.
for order in permutations(letters):
rank = {ch: i for i, ch in enumerate(order)}
if fits(words, rank):
return "".join(order)
return "invalid"Thuật toán của Kahn với hàng đợi ưu tiên tối thiểu
Ý tưởng
Trong một cặp từ đứng cạnh nhau, chỉ khác biệt đầu tiên mới được tính: act đứng trước cat cho biết a đứng trước c, còn các chữ cái sau đó không bao giờ được so sánh. Vì vậy, mỗi cặp tạo ra tối đa một cạnh.
Thuật toán Kahn (Kahn's algorithm) chuyển các cạnh thành một thứ tự: đếm số cạnh đi vào của mỗi chữ cái, đưa vào thứ tự một chữ cái có số đếm bằng 0, xóa các cạnh đi ra của nó rồi lặp lại. Một chữ cái nằm trên chu trình luôn giữ một cạnh đi vào từ chữ cái đứng trước nó trên chu trình, nên chữ cái đó không bao giờ được đưa vào thứ tự. Nếu số chữ cái được đưa vào thứ tự ít hơn số chữ cái xuất hiện, đáp án là "invalid".
Giữ các chữ cái sẵn sàng trong một đống tối thiểu (min-heap) và đưa chữ cái nhỏ nhất vào thứ tự. Mọi thứ tự phù hợp đều bắt đầu bằng một chữ cái sẵn sàng, nên đây là chữ cái đầu tiên nhỏ nhất có thể; việc đưa nó vào thứ tự chỉ xóa các cạnh, nên không cản trở chữ cái nào khác. Trong ví dụ đầu tiên, e và t sẵn sàng ngay từ đầu, và e được đưa vào trước. Hàng đợi thông thường cho ra một thứ tự hợp lệ, nhưng không phải lúc nào cũng là thứ tự nhỏ nhất.
Thuật toán
- Đánh dấu mọi chữ cái xuất hiện trong các từ.
- Với mỗi cặp từ liền kề, tìm vị trí khác nhau đầu tiên rồi thêm cạnh giữa hai chữ cái tương ứng, chỉ thêm một lần. Nếu không có vị trí nào khác nhau và từ đầu tiên dài hơn, trả về
"invalid". - Đếm số cạnh đi vào và đưa mọi chữ cái đã đánh dấu có số cạnh đi vào bằng 0 vào hàng đợi ưu tiên tối thiểu.
- Lấy chữ cái nhỏ nhất ra rồi thêm vào kết quả. Giảm số cạnh đi vào của mỗi chữ cái mà nó trỏ đến, rồi đưa vào hàng đợi mọi chữ cái có số cạnh đi vào giảm xuống 0.
- Trả về các chữ cái đã xếp nếu đã xếp tất cả chữ cái được đánh dấu, nếu không thì trả về
"invalid".
import heapq
def alienOrder(words):
# Letters are numbered 0 for 'a' up to 25 for 'z'.
present = [False] * 26
for word in words:
for ch in word:
present[ord(ch) - 97] = True
# before[a][b] is True once you know letter a comes before letter b.
before = [[False] * 26 for _ in range(26)]
indegree = [0] * 26
for first, second in zip(words, words[1:]):
for x, y in zip(first, second):
if x != y:
# Only the first difference tells you anything.
a, b = ord(x) - 97, ord(y) - 97
if not before[a][b]:
before[a][b] = True
indegree[b] += 1
break
else:
# No difference: one word starts the other, so the shorter must come first.
if len(first) > len(second):
return "invalid"
# A min-heap of the letters with nothing left before them.
heap = [c for c in range(26) if present[c] and indegree[c] == 0]
heapq.heapify(heap)
order = []
while heap:
c = heapq.heappop(heap)
order.append(chr(c + 97))
for nxt in range(26):
if before[c][nxt]:
indegree[nxt] -= 1
if indegree[nxt] == 0:
heapq.heappush(heap, nxt)
# A letter on a cycle never gets down to indegree 0, so it is never placed.
if len(order) < sum(present):
return "invalid"
return "".join(order)
Lỗi thường gặp và trường hợp biên
- Lấy nhiều hơn một quy tắc từ một cặp. Đọc vị trí thứ hai của
actvàcatsẽ thêm c trước a, khiến ví dụ đầu tiên trở thành một chu trình giả. - Bỏ sót bẫy tiền tố.
cookingđứng trướccooknhưng không có ký tự nào khác nhau, vì vậy vòng lặp chỉ xử lý các điểm khác biệt sẽ trả về một thứ tự. - Bỏ sót các chữ cái không xuất hiện trong quy tắc nào. Trong ví dụ đầu tiên, không có quy tắc nào nhắc đến e, nhưng chữ cái này vẫn phải có trong đáp án.
- Tính một quy tắc lặp lại hai lần vào bậc vào (indegree) nhưng chỉ lưu một lần. Bậc của chữ cái đó không bao giờ về 0, và chương trình báo một danh sách hợp lệ là chu trình.
- Coi hai từ giống nhau đứng cạnh nhau là bẫy tiền tố; từ lặp lại vẫn đúng thứ tự.
Câu hỏi thường gặp3
Độ phức tạp thời gian của Alien Dictionary là bao nhiêu?
O(C + k²), trong đó C là tổng số ký tự và k ≤ 26 là số chữ cái khác nhau. Một lượt duyệt danh sách tìm ra mọi điểm khác biệt đầu tiên, còn thuật toán Kahn duyệt qua tối đa k² cạnh. Bảng cạnh cần O(k²) không gian.
Tại sao chỉ so sánh các từ liền kề?
Đã được sắp xếp có tính bắc cầu: nếu mỗi từ không lớn hơn từ tiếp theo, thì cả danh sách đã được sắp xếp. Vì vậy, mọi quy tắc bạn có thể suy ra từ hai từ cách xa nhau đều đã được thể hiện qua các cặp từ liền kề ở giữa chúng.
Tại sao một từ đứng trước tiền tố của chính nó lại không hợp lệ?
Trong mọi bảng chữ cái, một từ đứng sau tiền tố của chính nó, vì phép so sánh hết chữ cái ở từ ngắn hơn trước khi tìm thấy điểm khác biệt. Đây là trường hợp duy nhất khiến một danh sách không thể sắp xếp dù các quy tắc của nó không có chu trình nào.
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 alienOrder(words):
# Viết code ở đâyKiểm thử #1
Kiểm thử #2
Kiểm thử #3
Đối số
words = ["tea", "ten", "ate", "act", "cat"]
Đầu ra mong đợi
"etacn"