Word Break
Otrzymujesz ciąg znaków s i listę słów wordDict. Zwróć true, jeśli możesz podzielić s na części tak, aby każda część była słowem z wordDict, a w przeciwnym razie zwróć false.
Części zachowują swoją kolejność i łącznie wykorzystują każdą literę z s dokładnie raz. Słowo może być użyte dowolną liczbę razy, a nie musisz używać każdego słowa.
Funkcja
- sstring
- ciąg znaków do podzielenia na słowa
- wordDictstring-array
- słowa, których możesz używać, tak często, jak chcesz
- Zwracaboolean
- prawda, jeśli s można podzielić na słowa ze słownika, w przeciwnym razie fałsz
Ograniczenia
1 ≤ s.length ≤ 3001 ≤ wordDict.length ≤ 10001 ≤ wordDict[i].length ≤ 20si każde słowo zawiera wyłącznie małe litery alfabetu angielskiego.- Słowa w
wordDictsą różne.
Przykłady
- Wejście
- s = "sunflowerseed"wordDict = ["sun", "flow", "flower", "seed"]
- Wyjście
- true
- Wyjaśnienie
- Podziel to na
sun,flower,seed. Wybranieflowposundonikąd nie prowadzi, ponieważ żadne słowo nie zaczyna się od pozostałegoer, więc pierwsze pasujące słowo nie zawsze jest właściwe.
- Wejście
- s = "bananaban"wordDict = ["ban", "ana"]
- Wyjście
- true
- Wyjaśnienie
ban+ana+banpokrywa cały ciąg znaków i używabandwa razy, co jest dozwolone.
- Wejście
- s = "pineappletart"wordDict = ["pine", "apple", "pineapple", "tar"]
- Wyjście
- false
- Wyjaśnienie
- Ciąg znaków zaczyna się od
pine+applealbo odpineapple, a w obu przypadkach pozostajetart. Jedynym słowem, które tam pasuje, jesttar, po którym zostaje samotnet, więc żadne cięcie nie działa.
+21 ukrytych testów przy wysłaniu
Pytanie dodatkowe
Zwróć najmniejszą liczbę słów, których można użyć w poprawnym podziale, lub -1, jeśli ciągu s nie da się podzielić. Co zmienia się w tabeli i czy zmienia się czas działania?
Podpowiedzi
Otwieraj je po kolei. Każda zdradza trochę więcej.
Pierwszym elementem dowolnego podziału jest słowo zaczynające się na
s. Gdy je wybierzesz, jakie pytanie pozostaje?To, czy litery od pewnego indeksu do końca można usunąć, zależy wyłącznie od tego indeksu. Jest tylko
n + 1takich pytań, więc zapamiętaj każdą odpowiedź, przede wszystkim tefalse.Niech
canEnd[i]określa, czy można podzielić pierwszeiliter, przy czymcanEnd[0] = true. WtedycanEnd[end]ma wartość true, gdy dla pewnegocanEnd[start]wartość jest true, a litery odstartdoendtworzą słowo. Przechowuj słowa w zbiorze haszującym i sprawdzaj tylko fragmenty nie dłuższe niż najdłuższe słowo.
Rozwiązanie
Łapczywe cięcie zawodzi w obu kierunkach: wybranie najkrótszego słowa jako pierwszego dzieli sunflowerseed na sun + flow, a wybranie najdłuższego jako pierwszego dzieli carpetal na carpet i pozostawia al. Musisz więc wypróbować różne możliwości, a łańcuch znaków można podzielić na wykładniczo wiele sposobów. Rozwiązaniem jest to, że możliwość podzielenia pozostałej części łańcucha zależy tylko od miejsca, w którym ta część się zaczyna, więc istnieje tylko n + 1 różnych pytań. Poniżej n oznacza długość s, m liczbę słów, a L długość najdłuższego słowa.
Wypróbuj każde słowo na każdej pozycji
Poprawne, ale nie kończy się na największych testach
Intuicja
Czytaj s od lewej. Pierwszy fragment musi być słowem, od którego zaczyna się s. Wypróbuj każde takie słowo i dla każdego z nich zadaj to samo pytanie o pozostałe litery. Jeśli którekolwiek słowo prowadzi do pełnego podziału, odpowiedź brzmi true. Jeśli żadne — false. Gdy nic nie pozostaje, udało Ci się podzielić wszystkie litery, więc to oznacza sukces.
Ta metoda sprawdza każde możliwe pierwsze słowo, następnie każde możliwe drugie słowo i tak dalej, więc nie może przeoczyć poprawnego podziału, a każdy zwrócony przez nią wynik true oznacza, że istnieje rzeczywisty podział.
Jest powolna, ponieważ wielokrotnie sprawdza te same pozostałe fragmenty. Weź 299 kopii a, po których następuje jedno b, a jako słowa przyjmij a, aa i tak dalej, aż do dziesięciu liter a. Każdy sposób podziału liter a na bloki o długości co najwyżej dziesięciu dociera do b i tam kończy się niepowodzeniem, a takich sposobów jest ponad 10^89. Rekurencja musi wypróbować je wszystkie, zanim będzie mogła zwrócić odpowiedź false.
Algorytm
- Napisz funkcję pomocniczą
canSplit(start), która określa, czy litery od indeksustartdo końca można podzielić na słowa. - Jeśli
startjest równe długościs, zwróćtrue. - Dla każdego słowa sprawdź, czy
szawiera je, zaczynając od indeksustart. - Jeśli tak i
canSplit(start + length of the word)ma wartośćtrue, zwróćtrue. - Jeśli żadne słowo nie pasuje, zwróć
false. Odpowiedzią jestcanSplit(0).
def wordBreak(s, wordDict):
def can_split(start):
# Can s[start:] be cut into dictionary words?
if start == len(s):
return True # nothing left to cut
for word in wordDict:
if s.startswith(word, start) and can_split(start + len(word)):
return True
return False
return can_split(0)Rekurencja z zapamiętywaniem wyników
Intuicja
Wynik dla pozostałej części zależy tylko od miejsca, w którym się zaczyna, a start przyjmuje tylko n + 1 wartości. W przykładzie z literą a pozostała część zaczynająca się na indeksie 20 zostaje osiągnięta po dwóch blokach po dziesięć, po dwudziestu pojedynczych literach a i na wiele innych sposobów, a jej wynik za każdym razem to false. Zapisz wynik dla każdego początku, gdy obliczysz go po raz pierwszy, a potem odczytuj go ponownie.
Komórka pamięci podręcznej potrzebuje trzech stanów: jeszcze nieobliczona, true i false. To wyniki false mają znaczenie. Wynik true natychmiast kończy całe wyszukiwanie, więc praca powtarzana przez zwykłą rekurencję odbywa się wyłącznie w gałęziach, które kończą się niepowodzeniem.
Każdy początek jest obliczany raz i sprawdza każde słowo, porównując do L liter, więc czas działania wynosi O(n × m × L): w tym przypadku najwyżej 300 × 1000 × 20 = 6 × 10^6 porównań liter. Pamięć podręczna i stos wywołań zajmują O(n) miejsca, a wywołania zagnieżdżają się na głębokość najwyżej 300.
Algorytm
- Utwórz memo z jednym miejscem dla każdego indeksu, oznaczając każde z nich jako jeszcze nierozwiązane.
- W
canSplit(start)zwróćtruena końcu ciągu, a jeśli miejsce dlastartzawiera odpowiedź, zwróć zapisaną odpowiedź. - W przeciwnym razie wypróbuj każde słowo zaczynające się na
start, tak jak w zwykłej rekurencji, i zatrzymaj się przy pierwszym, po którym pozostałą część da się podzielić. - Zapisz wynik w tym miejscu, również
false, i zwróć go. - Zwróć
canSplit(0).
def wordBreak(s, wordDict):
memo = [None] * len(s) # memo[start]: answer for s[start:], None until worked out
def can_split(start):
if start == len(s):
return True
if memo[start] is not None:
return memo[start]
result = False
for word in wordDict:
if s.startswith(word, start) and can_split(start + len(word)):
result = True
break
memo[start] = result
return result
return can_split(0)Oddolnie, po prefiksach, z użyciem zbioru haszującego
Intuicja
Odwróć podejście i zajmij się prefiksami. Niech canEnd[i] określa, czy pierwsze i liter można podzielić na słowa. Pusty prefiks nie wymaga żadnych słów, więc canEnd[0] ma wartość true. Pierwsze end liter można podzielić dokładnie wtedy, gdy ich ostatni fragment — litery od start do end — jest słowem, a litery przed nim można podzielić, czyli canEnd[start] ma wartość true. Wypełniaj tablicę od lewej do prawej, a każde potrzebne canEnd[start] będzie już znane.
Zamiast porównywać wszystkie m słów w każdej pozycji, umieść słowa w zbiorze haszującym i sprawdzaj możliwe ostatnie fragmenty. Żadne słowo nie jest dłuższe niż L, więc pasować może tylko L fragmentów kończących się na end. Dla sunflowerseed wartość canEnd staje się prawdziwa w pozycjach 0, 3 (sun), 7 (flow), 9 (flower) i 13 (seed po pozycji 9), więc odpowiedź to true. Pozycja 7 donikąd nie prowadzi, ponieważ żadne słowo nie zaczyna się od er, ale tablica nie musi się tym przejmować.
Jest n pozycji, dla każdej sprawdzanych jest najwyżej L fragmentów, a utworzenie i zahaszowanie fragmentu kosztuje do L kroków. Daje to O(n × L²), czyli najwyżej 300 × 20 × 20 = 1.2 × 10^5 kroków przetwarzania liter, niezależnie od wielkości słownika. Zbudowanie zbioru wymaga jednokrotnego odczytania każdego słowa, czyli O(m × L), zatem łączna złożoność wynosi O(m × L + n × L²). Zbiór przechowuje słowa, czyli O(m × L) liter, a tablica — n + 1 wartości logicznych. Nie ma rekurencji.
Algorytm
- Umieść każde słowo w zbiorze haszującym i zanotuj długość
Lnajdłuższego słowa. - Utwórz
canEndzn + 1elementami, wszystkie ustaw nafalse, a następnie ustawcanEnd[0]natrue. - Dla każdego
endod 1 donwypróbuj każdąlengthod 1 domin(L, end). - Jeśli
canEnd[end-length]ma wartośćtrue, a fragment o tej długości, kończący się naend, znajduje się w zbiorze, ustawcanEnd[end]natruei przestań wypróbowywać długości. - Zwróć
canEnd[n].
def wordBreak(s, wordDict):
words = set(wordDict)
longest = max(len(word) for word in wordDict)
n = len(s)
# can_end[i]: the first i letters split into dictionary words
can_end = [False] * (n + 1)
can_end[0] = True # the empty prefix needs no words
for end in range(1, n + 1):
# The last word is s[end-length:end], and no word is longer than longest.
for length in range(1, min(longest, end) + 1):
if can_end[end - length] and s[end - length:end] in words:
can_end[end] = True
break
return can_end[n]
Pułapki i przypadki brzegowe
Większość błędnych odpowiedzi wynika ze zbyt wczesnego wyboru jednego podziału albo z wyszukiwania, które nie pamięta wcześniejszych porażek.
- Chciwe dzielenie. Wybór najdłuższego słowa w pierwszej kolejności dzieli
carpetalnacarpeti pozostawiaal, chociażcar+petaldziała. Wybór najkrótszego słowa w pierwszej kolejności nie sprawdza się w przypadkusunflowerseed. - Sprawdzanie tylko, czy każda litera
swystępuje w jakimś słowie. W przypadku słówaaaaiaakażda część ma parzystą długość, więc ciąguaaaaaaazłożonego z siedmiu liter nie da się podzielić. - Zapisywanie w pamięci podręcznej tylko odpowiedzi
true. Odpowiedźtruei tak kończy wyszukiwanie. Powtarzana praca odbywa się w gałęziachfalse, więc pamięć podręczna bez nich nie zapobiega wykładniczemu wzrostowi złożoności. - Utworzenie tablicy o jeden element za krótkiej.
canEnd[i]dotyczy pierwszychiliter, a zarówno 0, jak insą prawidłowe, więc tablica potrzebujen + 1elementów. - Porównywanie znaków poza końcem
s, gdy słowo jest dłuższe niż pozostały fragment, na przykład słowaabczab. Przed porównaniem liter sprawdź długości. - W Lua i R indeksy ciągu znaków zaczynają się od 1: fragment o długości
k, kończący się na literzee, zaczyna się od literye-k+1.
Najczęstsze pytania4
Jaka jest złożoność czasowa problemu Word Break?
Tablica budowana od dołu z użyciem zbioru haszującego działa w czasie O(m × L + n × L²), gdzie n to długość s, m to liczba słów, a L to długość najdłuższego słowa. Budowanie zbioru odczytuje każde słowo raz, a dla każdej z n pozycji sprawdzanych jest najwyżej L fragmentów o długości do L liter. Jeśli zamiast tego porównasz każde słowo na każdej pozycji, złożoność wynosi O(n × m × L). Zwykła rekurencja bez zapamiętywania wyników ma złożoność wykładniczą.
Dlaczego podejście zachłanne nie sprawdza się w problemie Word Break?
Chciwa reguła zatwierdza jedno słowo i nigdy nie wraca do wcześniejszego wyboru. Zasada najdłuższego dopasowania dzieli carpetal na carpet i al, podczas gdy car + petal działa. Zasada najkrótszego dopasowania dzieli sunflowerseed na sun + flow i zatrzymuje się na erseed. Programowanie dynamiczne zachowuje każdą pozycję, do której może dotrzeć jakieś podzielenie, więc nigdy nie gubi właściwej.
Czy Word Break to problem programowania dynamicznego czy problem grafowy?
Oba ujęcia działają. W programowaniu dynamicznym canEnd[i] określa, czy pierwsze i liter można podzielić, na podstawie krótszych prefiksów. W ujęciu grafowym każdy indeks jest węzłem, a krawędź prowadzi z i do j, gdy litery od i do j tworzą słowo; sprawdzasz wtedy, czy węzeł n jest osiągalny z węzła 0. Wyszukiwanie wszerz ze zbiorem odwiedzonych węzłów wykonuje tę samą pracę co tablica.
Jak wyświetlić każde zdanie zamiast zwracać wartość true lub false?
Użyj przeszukiwania z nawrotami: na każdym indeksie wypróbuj każde pasujące słowo i rekurencyjnie przetwórz resztę, budując w ten sposób zdanie. Pamiętaj listę zdań dla każdego indeksu, aby ten sam fragment rozwiązać tylko raz. Najpierw utwórz tabelę wartości true lub false, aby pominąć wyszukiwanie dla ciągu, którego nie da się podzielić. Liczba zdań może rosnąć wykładniczo, więc rozmiar wyniku określa czas działania.
Podobne zadania
Zadania oparte na tych samych pomysłach. Rozwiązanie dwóch lub trzech utrwala schemat.
Python
def wordBreak(s, wordDict):
# Wpisz tutaj kodPrzypadek 1
Przypadek 2
Przypadek 3
Wejście
s = "sunflowerseed" wordDict = ["sun", "flow", "flower", "seed"]
Oczekiwane
true