Minimum Window Substring
Bạn nhận được hai chuỗi, s và t. Hãy tìm chuỗi con ngắn nhất của s chứa mọi ký tự của t, tính cả ký tự lặp lại: nếu t có một chữ cái hai lần, chuỗi con phải có chữ cái đó ít nhất hai lần. Thứ tự không quan trọng, và chuỗi con cũng có thể chứa các ký tự khác.
Nếu có nhiều chuỗi con ngắn nhất, hãy trả về chuỗi con nằm bên trái nhất. Nếu không có chuỗi con nào chứa tất cả ký tự của t, hãy trả về chuỗi rỗng.
Hàm
- sstring
- chuỗi cần tìm kiếm trong đó
- tstring
- các ký tự mà cửa sổ phải chứa, kể cả ký tự lặp lại
- Trả vềstring
- chuỗi con ngắn nhất của s chứa tất cả t, nếu có nhiều chuỗi thì chọn chuỗi nằm ngoài cùng bên trái, hoặc chuỗi rỗng
Ràng buộc
1 ≤ s.length ≤ 5 × 1041 ≤ t.length ≤ 104svàtchỉ chứa các chữ cái tiếng Anh. Chữ hoa và chữ thường là các ký tự khác nhau.- Khi có nhiều chuỗi con cùng ngắn nhất, đáp án là chuỗi nằm ngoài cùng bên trái; nếu không có chuỗi nào, đáp án là
"".
Ví dụ
- Đầu vào
- s = "mappingtheplan"t = "nap"
- Đầu ra
- "plan"
- Giải thích
- Cửa sổ đầu tiên từ bên trái chứa
n,avàplàappin, gồm năm ký tự.planchứa cả ba trong bốn ký tự, và không có đoạn liên tiếp nào gồm ba ký tự chứa đủ cả ba.
- Đầu vào
- s = "banana"t = "aan"
- Đầu ra
- "ana"
- Giải thích
tcần hai bản sao củaavà mộtn.anaở chỉ số 1 có đúng như vậy. Mộtanathứ hai bắt đầu ở chỉ số 3, vàanaở vị trí ngoài cùng bên trái được chọn.
- Đầu vào
- s = "Coddy"t = "cd"
- Đầu ra
- ""
- Giải thích
- Chữ
CtrongCoddyđược viết hoa, khác với ký tực. Không có chuỗi con nào chứa chữcviết thường, vì vậy đáp án là chuỗi rỗng.
Câu hỏi mở rộng
Khi t có ít ký tự còn s dài, phần lớn s không bao giờ cần thiết. Bạn có thể giúp cửa sổ chỉ nhảy giữa các vị trí chứa ký tự của 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.
Một cửa sổ chứa toàn bộ
tvẫn chứa toàn bộtkhi bạn kéo dài nó, còn cửa sổ thiếu một phần nào đó vẫn thiếu phần đó khi bạn rút ngắn nó. Hãy tận dụng điều này để khỏi phải thử mọi điểm bắt đầu với mọi điểm kết thúc.Di chuyển cạnh phải về phía trước cho đến khi cửa sổ bao gồm
t. Sau đó, di chuyển cạnh trái về phía trước chừng nào cửa sổ vẫn bao gồmt, đồng thời ghi nhận cửa sổ mỗi lần. Không cạnh nào cần phải lùi lại.Lưu số bản sao còn thiếu của từng ký tự trong cửa sổ vào một bảng, cùng với một số,
missing, cho biết tổng số bản sao còn thiếu. Một ký tự đi vào chỉ làm giảmmissingnếu cửa sổ vẫn cần ký tự đó, còn một ký tự rời đi chỉ làm tăngmissingnếu cửa sổ đang thiếu ký tự đó. Cửa sổ chứa đủtkhimissingbằng 0.
Lời giải
Chỉ số lượng ký tự trong cửa sổ mới quan trọng, không phải thứ tự của chúng. Di chuyển cạnh phải cho đến khi cửa sổ bao phủ t, rồi di chuyển cạnh trái khi cửa sổ vẫn còn bao phủ t; cả hai cạnh đều không lùi lại. Một bộ đếm số ký tự còn thiếu cho bạn biết chỉ trong một bước liệu cửa sổ có bao phủ t hay không.
Mở rộng một cửa sổ từ mọi điểm bắt đầu
Đúng, nhưng không chạy xong với các test lớn nhất
Ý tưởng
Cố định vị trí bắt đầu và mở rộng cửa sổ từng ký tự một, đồng thời đếm số lần xuất hiện của từng ký tự. Sau mỗi bước, kiểm tra từng chữ cái trong u mà t sử dụng. Vị trí kết thúc đầu tiên thỏa mãn điều kiện sẽ cho cửa sổ ngắn nhất bao phủ các ký tự cần tìm từ vị trí bắt đầu này, vì vậy hãy dừng tại đó. Duyệt các vị trí bắt đầu từ trái sang phải và chỉ thay thế kết quả tốt nhất nếu cửa sổ mới ngắn hơn hẳn, do đó nếu có các cửa sổ bằng nhau thì cửa sổ ở vị trí bên trái nhất sẽ được giữ lại.
Nếu không có cửa sổ nào thỏa mãn, mỗi vị trí bắt đầu sẽ đọc đến cuối s: khoảng n²/2 bước, tức 1.25 × 10^9 với n = 5 × 10^4.
Thuật toán
- Đếm số lần xuất hiện của từng ký tự mà
tyêu cầu. - Với mỗi
start, xóa bảng đếm rồi dịchendsang phải, thêms[end]. - Khi cửa sổ chứa đủ từng ký tự, giữ lại nếu nó ngắn hơn nghiêm ngặt so với cửa sổ tốt nhất, rồi chuyển sang
starttiếp theo. - Trả về cửa sổ tốt nhất hoặc
""nếu không có cửa sổ nào chứa đủt.
def minWindow(s, t):
need = [0] * 128 # copies of each character code that t asks for
for ch in t:
need[ord(ch)] += 1
letters = [c for c in range(128) if need[c] > 0]
best_start, best_len = 0, len(s) + 1
for start in range(len(s)):
have = [0] * 128 # counts inside s[start..end]
for end in range(start, len(s)):
have[ord(s[end])] += 1
if all(have[c] >= need[c] for c in letters):
# The first end that covers t gives the shortest window from this start.
if end - start + 1 < best_len:
best_start, best_len = start, end - start + 1
break
if best_len > len(s):
return ""
return s[best_start:best_start + best_len]Cửa sổ trượt kiểm tra từng chữ cái
Ý tưởng
Thêm ký tự vào một cửa sổ đang bao phủ vẫn giữ cho nó bao phủ, còn xóa ký tự khỏi một cửa sổ đang thiếu thứ gì đó vẫn khiến nó thiếu. Vì vậy, khi điểm bắt đầu dịch sang phải, điểm kết thúc của cửa sổ bao phủ ngắn nhất không bao giờ dịch sang trái.
Không cửa sổ nào bị bỏ sót. Xét cửa sổ tốt nhất, từ L đến R. Để left vượt qua L trước khi right chạm đến R, một cửa sổ ngắn hơn bắt đầu từ L phải bao phủ t. Vì vậy, khi right chạm đến R, quá trình thu hẹp sẽ đưa left đến L và ghi nhận cửa sổ đó.
Mỗi đầu mút dịch chuyển nhiều nhất n lần, nhưng mỗi lần kiểm tra đọc tối đa u số lượng: O(n·u).
Thuật toán
- Đếm số lần xuất hiện của các ký tự trong
t; đặtleft = 0và độ dài tốt nhất làn+1. - Với mỗi
right, thêms[right]vào số lần xuất hiện của các ký tự trong cửa sổ. - Trong khi cửa sổ có đủ số lần xuất hiện của mọi ký tự trong
t, ghi nhận cửa sổ nếu nó ngắn hơn hẳn, xóas[left]rồi tăngleft. - Trả về cửa sổ tốt nhất, hoặc
""nếu độ dài tốt nhất vẫn làn+1.
def minWindow(s, t):
need = [0] * 128 # copies of each character code that t asks for
for ch in t:
need[ord(ch)] += 1
letters = [c for c in range(128) if need[c] > 0]
have = [0] * 128 # counts inside s[left..right]
def covers():
for c in letters:
if have[c] < need[c]:
return False
return True
left = 0
best_start, best_len = 0, len(s) + 1
for right in range(len(s)):
have[ord(s[right])] += 1 # expand on the right
while covers(): # shrink from the left while the window still covers t
if right - left + 1 < best_len:
best_start, best_len = left, right - left + 1
have[ord(s[left])] -= 1
left += 1
if best_len > len(s):
return ""
return s[best_start:best_start + best_len]Cửa sổ trượt với bộ đếm số phần tử còn thiếu
Ý tưởng
Thay thao tác quét bằng một con số. Gọi need[c] là số lần xuất hiện của c mà t cần trừ đi số lần xuất hiện trong cửa sổ; giá trị âm có nghĩa là dư. Gọi missing là số ký tự mà cửa sổ còn thiếu, bắt đầu bằng độ dài của t. Cửa sổ chứa đủ chính xác các ký tự của t khi missing bằng 0.
Khi một ký tự đi vào và need lớn hơn 0, ký tự đó lấp đầy một chỗ thiếu, nên missing giảm. Khi việc loại bỏ một ký tự khiến need tăng lên lớn hơn 0, một chỗ thiếu xuất hiện, nên missing tăng. Các ký tự dư không ảnh hưởng đến giá trị này. Với banana và t = aan, missing lần đầu bằng 0 tại bana, và khi bỏ ký tự b dư ra, cửa sổ ana vẫn chứa đủ các ký tự cần thiết.
Mỗi ký tự đi vào và rời khỏi cửa sổ đúng một lần, với chi phí cố định: thời gian O(n + m), cùng một bảng gồm 128 số đếm.
Thuật toán
- Điền số lần xuất hiện của
tvàoneed; đặtmissingbằng độ dài củat,left = 0và độ dài tốt nhất bằngn+1. - Với mỗi
right: nếuneed[s[right]]lớn hơn 0, giảmmissing; sau đó giảmneed[s[right]]. - Khi
missingbằng 0, ghi lại cửa sổ nếu cửa sổ đó ngắn hơn hẳn. Tăngneed[s[left]]; nếu giá trị này giờ lớn hơn 0, tăngmissing. Tăngleft. - Trả về cửa sổ tốt nhất, hoặc
""nếu độ dài tốt nhất vẫn làn+1.
def minWindow(s, t):
# need[c]: copies of c that t asks for minus copies inside the window.
# Positive means the window still lacks c; negative means it holds spares.
need = [0] * 128
for ch in t:
need[ord(ch)] += 1
missing = len(t) # characters of t the window does not cover yet
left = 0
best_start, best_len = 0, len(s) + 1
for right in range(len(s)):
c = ord(s[right])
if need[c] > 0: # this copy fills a gap
missing -= 1
need[c] -= 1
while missing == 0: # the window covers t: record it, then shrink
if right - left + 1 < best_len:
best_start, best_len = left, right - left + 1
c = ord(s[left])
need[c] += 1
if need[c] > 0: # gave away a copy t needs
missing += 1
left += 1
if best_len > len(s):
return ""
return s[best_start:best_start + best_len]
Lỗi thường gặp và trường hợp biên
- Đếm số chữ cái thay vì số lần xuất hiện.
t = aancần haia, nênbankhông đủ. - Thay đổi
missingmỗi khi một ký tự đi vào hoặc rời khỏi cửa sổ. Khi đó, mộtathừa vẫn được tính, vàmissingcó thể bằng 0 dù cửa sổ thiếun. - Thay thế cửa sổ tốt nhất khi độ dài bằng nhau. Trong ví dụ thứ hai, cách này trả về
anatại chỉ số 3. - Dùng
nlàm độ dài cho trường hợp “không tìm thấy”. Khi đáp án là toàn bộs, độ dài của nó cũng làn; hãy bắt đầu từn+1. - Dùng bảng gồm 26 ô, đánh chỉ số bằng
c - 'a'. Các chữ cái viết hoa, như trong ví dụ thứ ba, nằm ngoài phạm vi của bảng.
Câu hỏi thường gặp3
Độ phức tạp thời gian của bài toán chuỗi con nhỏ nhất chứa đủ ký tự là bao nhiêu?
Cửa sổ đếm ký tự còn thiếu chạy trong thời gian O(n + m), trong đó n và m là độ dài của s và t. Việc xây dựng bảng đọc t một lần, và mỗi ký tự của s đi vào rồi rời khỏi cửa sổ nhiều nhất một lần. Bộ nhớ bổ sung gồm một bộ đếm cho mỗi mã ký tự.
Tại sao cạnh trái không bao giờ dịch chuyển trở lại?
Cạnh trái chỉ đi qua một vị trí sau khi cửa sổ bắt đầu tại đó đã bao phủ t, và đó là cửa sổ ngắn nhất bao phủ được t từ vị trí bắt đầu ấy. Mọi cửa sổ bắt đầu tại đó nhưng kết thúc muộn hơn đều dài hơn, vì vậy quay lại sẽ không bao giờ tìm được đáp án tốt hơn.
Minimum Window Substring khác với việc tìm một từ đảo chữ trong chuỗi như thế nào?
Chuỗi đảo chữ chứa chính xác các ký tự của t, vì vậy cửa sổ của nó có độ dài cố định m và trượt từng bước một. Ở đây, bạn có thể có thêm ký tự, vì vậy độ dài cũng là một phần của đáp án: cửa sổ mở rộng cho đến khi bao phủ t và thu hẹp lại khi vẫn còn bao phủ t.
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 minWindow(s, t):
# Viết code ở đâyKiểm thử #1
Kiểm thử #2
Kiểm thử #3
Đối số
s = "mappingtheplan" t = "nap"
Đầu ra mong đợi
"plan"