Minimum Window Substring
Otrzymujesz dwa ciągi znaków: s i t. Znajdź najkrótszy podciąg ciągu s, czyli ciąg kolejnych znaków, który zawiera każdy znak z t, uwzględniając powtórzenia: jeśli t zawiera daną literę dwa razy, podciąg musi zawierać ją co najmniej dwa razy. Kolejność nie ma znaczenia, a podciąg może też zawierać inne znaki.
Jeśli kilka podciągów ma tę samą najkrótszą długość, zwróć ten najbardziej z lewej. Jeśli żaden podciąg ciągu s nie zawiera wszystkich znaków z t, zwróć pusty ciąg znaków.
Funkcja
- sstring
- ciąg znaków, którego szukasz
- tstring
- znaki, które musi zawierać okno, wraz z powtórzeniami
- Zwracastring
- najkrótszy, a w przypadku remisu najbardziej lewy podciąg s, który zawiera wszystkie znaki z t, albo pusty ciąg
Ograniczenia
1 ≤ s.length ≤ 5 × 1041 ≤ t.length ≤ 104sitzawierają wyłącznie angielskie litery. Wielkie i małe litery to różne znaki.- Jeśli kilka podciągów ma najkrótszą długość, odpowiedzią jest ten najbardziej z lewej; jeśli nie ma żadnego, jest nią
"".
Przykłady
- Wejście
- s = "mappingtheplan"t = "nap"
- Wyjście
- "plan"
- Wyjaśnienie
- Licząc od lewej, pierwsze okno zawierające
n,aiptoappin, które ma pięć znaków.planna końcu zawiera wszystkie trzy w czterech znakach, a żaden ciąg trzech znaków ich nie zawiera.
- Wejście
- s = "banana"t = "aan"
- Wyjście
- "ana"
- Wyjaśnienie
twymaga dwóch kopiiai jednegon.anana indeksie 1 zawiera dokładnie tyle. Drugieanazaczyna się na indeksie 3, a wygrywa to najbardziej z lewej.
- Wejście
- s = "Coddy"t = "cd"
- Wyjście
- ""
- Wyjaśnienie
- Jedyna wielka litera C w
Coddyto wielkie C, a wielkie i małe litery są różnymi znakami. Żaden podciąg nie zawiera małej literyc, więc odpowiedzią jest pusty ciąg.
+17 ukrytych testów przy wysłaniu
Pytanie dodatkowe
Gdy t składa się tylko z kilku liter, a s jest długie, większość s nigdy nie ma znaczenia. Czy możesz sprawić, by okno przeskakiwało tylko między pozycjami zawierającymi literę z t?
Podpowiedzi
Otwieraj je po kolei. Każda zdradza trochę więcej.
Okno, które zawiera całe
t, nadal je zawiera, gdy je wydłużysz, a okno, któremu czegoś brakuje, nadal nie będzie tego zawierać, gdy je skrócisz. Wykorzystaj to, aby uniknąć sprawdzania każdego początku z każdym końcem.Przesuwaj prawą krawędź do przodu, aż okno obejmie
t. Następnie przesuwaj lewą krawędź do przodu tak długo, jak okno nadal obejmujet, za każdym razem ją zapisując. Żadna z krawędzi nie musi się cofać.Prowadź tabelę określającą, ilu kopii każdego znaku brakuje w oknie, oraz jedną liczbę,
missing, określającą łączną liczbę brakujących kopii. Znak, który wchodzi do okna, zmniejsza wartośćmissingtylko wtedy, gdy nadal był potrzebny, a znak, który je opuszcza, zwiększa tę wartość tylko wtedy, gdy w oknie zaczyna go brakować. Okno zawiera wszystkie znaki ztdokładnie wtedy, gdymissingwynosi 0.
Rozwiązanie
Odpowiedź zależy od tego, ile znaków każdego rodzaju zawiera okno, a nie od ich kolejności, i najlepsze okno może zaczynać się w dowolnym miejscu. Sprawdzenie każdego początku z każdym końcem oznacza O(n²) okien. Rozwiązaniem jest okno, którego krawędzie przesuwają się tylko do przodu: prawa krawędź powiększa je, aż obejmie t, lewa krawędź zmniejsza je, dopóki nadal obejmuje t, a jeden licznik brakujących znaków informuje w pojedynczym kroku, czy okno obejmuje t.
Rozwijaj okno od każdego początku
Poprawne, ale nie kończy się na największych testach
Intuicja
Ustal miejsce początku podciągu. Następnie powiększaj go po jednym znaku, zliczając wystąpienia każdego znaku wewnątrz, a po każdym kroku sprawdzaj, czy obejmuje t: dla każdej z u różnych liter użytych w t okno musi zawierać co najmniej tyle ich wystąpień, ile zawiera ich t. Pierwszy koniec, który spełnia ten warunek, wyznacza najkrótsze okno obejmujące t dla tego początku, ponieważ każde krótsze okno o tym samym początku zostało wcześniej sprawdzone i nie spełniło warunku. Zatrzymaj się w tym miejscu.
Powtórz to dla każdego początku i zachowaj najkrótsze okno. Początki są sprawdzane od lewej do prawej, a okno zastępuje dotychczas najlepsze tylko wtedy, gdy jest ściśle krótsze, więc spośród okien o tej samej długości pozostaje to najbardziej z lewej.
To powolne, gdy okna są długie lub nie istnieją. Jeśli jedyne Z w s znajduje się na samym końcu, a t wymaga jednego, każdy początek wymaga odczytania aż do końca: około n²/2 kroków, czyli 1.25 × 10^9 dla n = 5 × 10^4, z których każdy obejmuje sprawdzenie maksymalnie 52 liter. To samo dzieje się, gdy żadne okno nie istnieje.
Algorytm
- Policz, ile kopii każdego znaku wymaga
t, i wypisz użyte w nim litery. - Dla każdego
startwyczyść tabelę zliczeń i przesuwajendodstartdo końcas, dodającs[end]do tabeli. - Po każdym dodaniu sprawdź każdą literę z
t. Jeśli okno zawiera wystarczającą liczbę każdej z nich, porównaj jego długość z dotychczasową najlepszą, zachowaj je, jeśli jest ściśle krótsze, i przestań je powiększać. - Po sprawdzeniu wszystkich początków zwróć najlepsze okno albo
"", jeśli żadne nie zawierało wszystkich znaków zt.
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]Przesuwane okno, które sprawdza każdą literę
Intuicja
Dwa fakty pozwalają uniknąć ponownego rozpoczynania. Dodawanie znaków do okna, które zawiera t, sprawia, że nadal je zawiera, a usuwanie znaków z okna, w którym czegoś brakuje, sprawia, że nadal tego brakuje. Dlatego gdy początek przesuwa się w prawo, koniec najkrótszego okna zawierającego t może pozostać na miejscu lub przesunąć się w prawo. Oba końce mogą przesuwać się naprzód razem i żaden z nich nigdy się nie cofa.
Przesuwaj right po s, dodając każdy znak do tabeli zliczeń. Gdy tylko okno zawiera t, jest kandydatem: zapisz je, jeśli jest krótsze od najlepszego, a następnie usuń s[left], przesuń left do przodu i sprawdź ponownie. Powtarzaj, aż okno przestanie zawierać t, a potem wróć do powiększania go z prawej strony.
Żadne okno nie zostanie pominięte. Weź najlepsze okno, od L do R. Gdyby left minęło L, zanim right dotarło do R, jakieś okno zaczynające się w L i kończące przed R zawierałoby t, a byłoby krótsze od najlepszego. Zatem gdy right dociera do R, pętla zmniejszająca przesuwa left do L i zapisuje najlepsze okno. Każdy koniec przesuwa się najwyżej n razy, ale każde sprawdzenie odczytuje do u zliczeń, po jednym dla każdej litery użytej w t, mimo że od poprzedniego sprawdzenia zmieniło się tylko jedno zliczenie.
Algorytm
- Policz kopie, o które prosi
t, i wypisz jego litery; zacznij od pustego okna,left = 0i najlepszej długości równejn+1. - Przesuwaj
rightpo każdym indeksie i dodawajs[right]do liczników w oknie. - Gdy okno zawiera wystarczającą liczbę kopii każdej litery z
t, zapisz okno, jeśli jest ściśle krótsze od najlepszego, usuńs[left]z liczników i przesuńleftdo przodu. - Zwróć najlepsze okno lub
"", jeśli najlepsza długość nadal wynosin+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]Okno przesuwne z licznikiem brakujących elementów
Intuicja
Pozostaw to samo okno i zastąp sprawdzanie jedną liczbą. Niech need[c] oznacza liczbę wystąpień c, o które prosi t, pomniejszoną o liczbę wystąpień w oknie. Wartość dodatnia oznacza, że w oknie wciąż czegoś brakuje, a ujemna — że są w nim nadmiarowe kopie. Niech missing oznacza łączną liczbę kopii, których brakuje w oknie; początkowo jest równa długości t. Okno zawiera dokładnie t, gdy missing wynosi 0.
Aktualizacja kosztuje jeden krok. Gdy s[right] trafia do okna, a jego wartość need jest większa od 0, uzupełnia brak, więc missing zmniejsza się o jeden; tak czy inaczej need zmniejsza się o jeden i może spaść poniżej 0, oznaczając nadmiarową kopię. Gdy s[left] opuszcza okno, need zwiększa się o jeden, a jeśli teraz jest większa od 0, okno oddało kopię potrzebną w t, więc missing zwiększa się o jeden. Nadmiarowe kopie pojawiają się i znikają bez wpływu na missing.
Prześledźmy s = banana, t = aan: początkowo need wynosi 2 dla a i 1 dla n, a missing wynosi 3. b nie jest potrzebne. Pierwsze a zmniejsza missing do 2, n do 1, a drugie a do 0, więc bana zawiera t. Zmniejszanie okna usuwa nadmiarowe b i pozostawia ana — trzy znaki, czyli nowe najlepsze okno. Usunięcie tego a przywraca missing do 1. Ostatnie a ponownie daje komplet, tym razem w nana, które zmniejsza się do drugiego ana. Nie jest ono krótsze, więc pozostaje pierwsze, najbardziej z lewej strony ana.
Każdy znak s wchodzi do okna raz i opuszcza je najwyżej raz, a każdy ruch wymaga stałej ilości pracy. Tworzenie need wymaga jednokrotnego odczytania t. Całe działanie ma złożoność O(n + m), a jedyną dodatkową pamięcią jest tablica 128 liczników.
Algorytm
- Wypełnij
needliczebnościami znaków wti ustawmissingna długośćt,left = 0, a najlepszą długość nan+1. - Dla każdej wartości
right: jeślineed[s[right]]jest większe od 0, zmniejszmissing; następnie zmniejszneed[s[right]]. - Dopóki
missingwynosi 0, zapisz okno, jeśli jest ściśle krótsze od najlepszego. Następnie zwiększneed[s[left]]; jeśli teraz jest większe od 0, zwiększmissing. Przesuńleftdo przodu. - Zwróć najlepsze okno albo
"", jeśli najlepsza długość nadal wynosin+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]
Pułapki i przypadki brzegowe
Większość błędnych odpowiedzi wynika z liczenia niewłaściwych rzeczy albo zapisywania okna w niewłaściwym momencie.
- Liczenie liter zamiast ich kopii.
t = aanwymaga dwóch litera, więcbango nie pokrywa. - Zmniejszanie
missingdla każdego dodanego znaku. Trzecieajest nadmiarowe; jeśli zmniejszymissing, licznik osiągnie 0, mimo że w oknie nadal brakujen. Zmniejszaj go tylko wtedy, gdyneedbyło większe od 0. - Zwiększanie
missingdla każdego usuwanego znaku. Usunięcie nadmiarowego znaku nie sprawia, że okno przestaje pokrywaćt; zwiększaj je tylko wtedy, gdyneedwzrośnie powyżej 0. - Zapisywanie okna po pętli zmniejszającej. Wtedy okno już nie pokrywa
t. Zapisz je wewnątrz pętli, zanim usunieszs[left]. - Zastępowanie najlepszego okna, gdy nowe ma taką samą długość. Zwraca to najbardziej prawe spośród najkrótszych okien; porównuj za pomocą ścisłego operatora mniejszości.
- Używanie
njako długości oznaczającej „nie znaleziono”. Gdy odpowiedzią jest całes, jego długość również wynosin. Zacznij odn+1, aby te dwa przypadki się różniły. - Tablica 26 elementów indeksowana przez
c - 'a'. Wielkie litery wykraczają poza jej zakres. Użyj jednego elementu dla każdego kodu znaku.
Najczęstsze pytania4
Jaka jest złożoność czasowa problemu „Minimum Window Substring”?
Przesuwane okno z licznikiem brakujących znaków działa w czasie O(n + m), gdzie n i m to długości s i t. Budowanie tabeli odczytuje t raz, a każdy znak s wchodzi do okna i je opuszcza co najwyżej raz, przy stałym koszcie każdego przesunięcia. Dodatkowa pamięć to tabela z jednym licznikiem dla każdego kodu znaku, która nie rośnie wraz z rozmiarem danych wejściowych.
Dlaczego lewa krawędź nigdy nie przesuwa się z powrotem?
Lewa krawędź przesuwa się za daną pozycję dopiero wtedy, gdy okno zaczynające się w tym miejscu objęło t, a było to najkrótsze okno obejmujące t spośród tych zaczynających się w tym miejscu. Każde okno zaczynające się w tym miejscu i kończące się później jest dłuższe, więc cofnięcie się nigdy nie pozwoliłoby znaleźć lepszej odpowiedzi. Dlatego obie krawędzie przesuwają się tylko do przodu, a złożoność czasowa pozostaje liniowa.
Czego nie uwzględnia brakujący licznik?
To liczba kopii znaków, o które prosi t, których jeszcze nie ma w oknie — suma dodatnich wartości w need. Zaczyna się od długości t i wynosi 0 dokładnie wtedy, gdy okno obejmuje t. Nadmiarowe kopie nie zmieniają tej wartości, dzięki czemu jedno porównanie może zastąpić sprawdzanie każdej litery.
Czym różni się problem Minimum Window Substring od wyszukiwania anagramu w ciągu znaków?
Anagram zawiera dokładnie te same litery co t i żadnych innych, więc okno ma stałą długość m i przesuwa się za każdym razem o jedną pozycję. Tutaj okno może zawierać dodatkowe znaki, więc jego długość jest częścią odpowiedzi: powiększa się z prawej strony, aż obejmie t, a następnie zmniejsza się z lewej strony, dopóki nadal je obejmuje.
Podobne zadania
Zadania oparte na tych samych pomysłach. Rozwiązanie dwóch lub trzech utrwala schemat.
Python
def minWindow(s, t):
# Napisz kod tutajPrzypadek 1
Przypadek 2
Przypadek 3
Wejście
s = "mappingtheplan" t = "nap"
Oczekiwane
"plan"