Partition Labels
Otrzymujesz ciąg s składający się z małych liter. Podziel go na jak najwięcej kolejnych części, tak aby każda litera występowała tylko w jednej części: jeśli litera pojawia się w jakiejś części, wszystkie jej wystąpienia znajdują się w tej części. Zwróć długości części od lewej do prawej.
Funkcja
- sstring
- ciąg znaków do wycięcia, tylko małe litery
- Zwracainteger-array
- długość każdej części, od lewej do prawej
Ograniczenia
1 ≤ s.length ≤ 5 × 104szawiera wyłącznie małe litery alfabetu angielskiego.- Te części zachowują swoją kolejność i razem składają się na całe
s, więc ich długości sumują się dos.length.
Przykłady
- Wejście
- s = "abacdcefe"
- Wyjście
- [3, 3, 3]
- Wyjaśnienie
- Litery a znajdują się na pozycjach 0 i 2, litery c na pozycjach 3 i 5, a litery e na pozycjach 6 i 8, więc cięcia wypadają po
abai pocdc. Żadnej części nie można już przeciąć, ponieważ każda zaczyna się i kończy tą samą literą.
- Wejście
- s = "codingisfun"
- Wyjście
- [1, 1, 1, 8]
- Wyjaśnienie
- Litery c, o i d występują po jednym razie, więc każda z nich stanowi osobną część. Litera i o indeksie 3 ma kopię na pozycji 6, a litera n na pozycji 4 ma kopię na pozycji 10, na końcu ciągu, więc wszystko od indeksu 3 wzwyż stanowi jedną część składającą się z 8 liter.
- Wejście
- s = "zebraz"
- Wyjście
- [6]
- Wyjaśnienie
- Pierwsza litera, z, wraca jako ostatnia litera, więc cały ciąg musi pozostać w jednej części.
+14 ukrytych testów przy wysłaniu
Podpowiedzi
Otwieraj je po kolei. Każda zdradza trochę więcej.
Pierwsza część musi zawierać
s[0]. Jak daleko w prawo musi sięgać przynajmniej?Część zawierająca literę musi dotrzeć do jej ostatniego wystąpienia, a każda napotkana po drodze litera może przesunąć ją dalej. Najpierw zapisz ostatnią pozycję każdej litery, aby każde wyszukiwanie kosztowało
O(1).Czytaj od lewej do prawej i zachowuj
end, największą ostatnią pozycję spośród liter bieżącej części. Gdy Twoja pozycja jest równaend, żadna litera tej części nie pojawia się później: odetnij w tym miejscu, zapisz długość i zacznij nową część.
Rozwiązanie
Cięcie jest dozwolone tylko tam, gdzie po obu jego stronach nie występuje ta sama litera, a najlepsza odpowiedź zakłada cięcie w każdym takim miejscu. Sprawdzanie każdego miejsca przez ponowne skanowanie ciągu znaków ma złożoność kwadratową. Najpierw zapisz ostatnią pozycję każdej litery, a następnie jedno przejście od lewej do prawej pozwoli znaleźć wszystkie miejsca cięcia, ponieważ fragment musi sięgać do ostatniego wystąpienia każdej litery w jego obrębie.
Przetestuj każdą lukę
Poprawne, ale nie kończy się na największych testach
Intuicja
Między sąsiednimi literami jest n-1 przerw. Cięcie w przerwie jest dozwolone tylko wtedy, gdy żadna litera nie występuje po obu jej stronach, ponieważ litera rozdzielona cięciem znalazłaby się w dwóch częściach. Wykonanie wszystkich dozwolonych cięć daje najwięcej części. Weźmy fragment między dwoma sąsiednimi dozwolonymi cięciami: żadna z jego liter nie występuje na lewo od lewego cięcia ani na prawo od prawego cięcia, więc wszystkie ich wystąpienia znajdują się wewnątrz fragmentu i jest on prawidłową częścią. Każda prawidłowa odpowiedź może zaś zawierać cięcia tylko w dozwolonych przerwach, więc żadna odpowiedź nie ma więcej części.
Sprawdź więc każdą przerwę: zbierz litery po jej lewej i prawej stronie i wykonaj cięcie, jeśli zbiory nie mają wspólnych elementów. W abacdcefe po aba po lewej stronie przerwy są a i b, a po prawej c, d, e i f. Nie ma wspólnych liter, więc wykonujesz cięcie. Po ab po obu stronach przerwy występuje a, więc nie wykonujesz cięcia.
Każde sprawdzenie wymaga odczytania całego ciągu, a przerw jest n-1, więc trzeba odczytać około n² liter. Przy 50 000 liter oznacza to 2.5 × 10^9 odczytów, zdecydowanie za dużo, by największe testy działały wystarczająco szybko.
Algorytm
- Ustaw
start = 0, gdzie zaczyna się bieżąca część. - Dla każdej przerwy
cutod 1 don-1(przerwy tuż przeds[cut]) zaznacz litery zs[0..cut-1]i litery zs[cut..n-1]. - Jeśli żadna litera nie jest zaznaczona po obu stronach, dodaj
cut-startdo wyniku i ustawstart = cut. - Po pętli dodaj ostatnią część,
n-start.
def partitionLabels(s):
n = len(s)
sizes = []
start = 0 # where the current part begins
for cut in range(1, n): # the gap just before s[cut]
left = set(s[:cut])
right = set(s[cut:])
if not (left & right): # no letter on both sides: cut here
sizes.append(cut - start)
start = cut
sizes.append(n - start) # the last part has no gap after it
return sizesPołącz zakresy każdej litery
Intuicja
Potraktuj każdą literę jako przedział od jej pierwszej do ostatniej pozycji. Część zawierająca daną literę musi obejmować cały ten przedział. Zatem dwie litery, których przedziały się nakładają, muszą znaleźć się w tej samej części, a nakładanie się rozprzestrzenia: jeśli przedział a nakłada się na b, a przedział b na c, wszystkie trzy znajdą się w jednej części.
To problem scalania przedziałów. W jednym przebiegu zanotuj pierwszą i ostatnią pozycję każdej litery. Następnie weź przedziały w kolejności ich początków i scal te, które się nakładają. Każdy scalony blok stanowi jedną część, a przerwy między blokami to dokładnie dozwolone miejsca podziału. Otrzymasz przedziały w kolejności początków bez sortowania: przejdź ponownie po ciągu i pobierz przedział litery, gdy znajdziesz się na jej pierwszej pozycji.
W ciągu codingisfun przedziały w kolejności to c [0, 0], o [1, 1], d [2, 2], i [3, 6], n [4, 10], g [5, 5], s [7, 7], f [8, 8] i u [9, 9]. Pierwsze trzy tworzą osobne części. Począwszy od i, każdy przedział zaczyna się na pozycji 10 lub wcześniej, a na tej pozycji kończy się przedział n, więc łączą się w [3, 10], czyli część złożoną z 8 liter.
Ciąg zawiera najwyżej 26 różnych liter, więc przedziałów jest najwyżej 26, a tablice pierwszych i ostatnich pozycji mają stały rozmiar.
Algorytm
- Podczas jednego przejścia po
szapiszfirstilast, czyli pierwszą i ostatnią pozycję każdej litery. - Przejdź ponownie po
s. Gdy pozycjaijest pierwszą pozycją danej litery, przedział tej litery[i, last]jest następnym przedziałem w kolejności początków. - Jeśli przedział zaczyna się po bieżącym bloku
end, zamknij blok o długościend-start+1i rozpocznij nowy blok wi. - W obu przypadkach ustaw
end = max(end, last). - Zamknij ostatni blok i zwróć długości.
def partitionLabels(s):
first, last = {}, {}
for i, c in enumerate(s):
first.setdefault(c, i)
last[c] = i
sizes = []
start = end = 0 # the block of merged spans being built
for i, c in enumerate(s):
if first[c] != i:
continue # take each letter's span once, at its first position
if i > end: # this span starts after the block: close the block
sizes.append(end - start + 1)
start = i
end = max(end, last[c])
sizes.append(end - start + 1)
return sizesRozwiń każdą część do jej ostatniej litery
Intuicja
Pierwsze pozycje wcale nie są potrzebne. Czytaj ciąg znaków od lewej do prawej i przechowuj w end najbardziej odległą ostatnią pozycję dowolnej litery w bieżącej części. Gdy odczytasz literę na pozycji i, jej ostatnie wystąpienie również musi znajdować się w tej części, więc zwiększ end do last[s[i]], jeśli ta wartość jest większa.
Gdy i osiągnie end, ostatnie wystąpienie każdej odczytanej w tej części litery znajduje się na pozycji i lub wcześniej. Żadna litera nie przekracza przerwy za i, więc można tam zrobić cięcie. Zamknij część o długości end-start+1 i rozpocznij następną od i+1.
Dlaczego cięcie przy pierwszej okazji jest właściwym wyborem zachłannym? Zanim i osiągnie end, jakaś litera z tej części nadal ma wystąpienie dalej w ciągu, więc nie można wykonać wcześniejszego cięcia. Przejście nie pomija też żadnej dozwolonej przerwy: jeśli żadna litera nie przekracza przerwy za i, ostatnie wystąpienie każdej litery z tej części znajduje się na pozycji i lub wcześniej, więc end jest wtedy równe i. Przejście wykonuje cięcia dokładnie w dozwolonych miejscach, co daje najwięcej części.
W ciągu abacdcefe ostatnie pozycje to: a — 2, b — 1, c — 5, d — 4, e — 8 i f — 7. Odczytanie a ustawia end na 2, b pozostawia tę wartość bez zmian, a na i = 2 część zostaje zamknięta i ma długość 3. Litera c ustawia end na 5 i część zostaje zamknięta na pozycji 5 — ponownie ma długość 3. Część z e zostaje zamknięta na pozycji 8.
Algorytm
- W jednym przebiegu zapisz
last[c], czyli ostatnią pozycję każdej literyc, w tablicy o rozmiarze 26. - Ustaw
start = 0iend = 0. - Dla każdej pozycji
iustawend = max(end, last[s[i]]). - Jeśli
i == end, dodajend-start+1do wyniku i ustawstart = i+1. - Zwróć długości.
def partitionLabels(s):
last = {c: i for i, c in enumerate(s)} # last position of each letter
sizes = []
start = end = 0
for i, c in enumerate(s):
end = max(end, last[c]) # the part must reach c's last copy
if i == end: # no letter of this part appears later
sizes.append(end - start + 1)
start = i + 1
return sizes
Pułapki i przypadki brzegowe
Przebieg zachłanny jest krótki, więc błędy kryją się w tym, którą pozycję porównujesz, oraz w długościach części.
- Cięcie po dojściu do ostatniego wystąpienia bieżącej litery zamiast do
endczęści. Wabcbalitera c na indeksie 2 jest swoim ostatnim wystąpieniem, ale litery a występują aż do indeksu 4, więc cięcie w tym miejscu rozdzieliłoby zarówno a, jak i b. - Błąd o jeden w długości. Część od
startdoend, z uwzględnieniem obu końców, maend-start+1liter. - Zwracanie pozycji cięć zamiast długości. Dla
abacdcefeodpowiedzią jest[3, 3, 3], a nie[2, 5, 8]. - Zapominanie o ostatniej części przy cięciu w przerwach. Za ostatnią częścią nie ma już przerwy, więc po zakończeniu pętli dodaj
n-start. - Oczekiwanie jednej części na każdą odrębną literę.
zebrazma pięć różnych liter i jedną część, ponieważ litery z łączą wszystko, co znajduje się między nimi.
Najczęstsze pytania4
Jaka jest złożoność czasowa Partition Labels?
Jedno przejście zapisuje ostatnią pozycję każdej litery, a drugie umieszcza miejsca podziału, więc czas wynosi O(n). Tablica ostatnich pozycji ma 26 wpisów niezależnie od długości ciągu znaków, więc dodatkowa przestrzeń wynosi O(1), nie licząc wyniku.
Dlaczego podejście zachłanne działa w przypadku podziału na etykiety?
Obecna część musi sięgać do ostatniego wystąpienia każdej zawartej w niej litery, więc nie wolno jej kończyć przed end. Po end żadna litera z tej części już się nie pojawia, więc można ją tam zakończyć, a takie cięcie nigdy nie szkodzi pozostałej części ciągu. Zatem przejście tnie przy każdej dozwolonej przerwie i nigdzie indziej, dzięki czemu odpowiedź może zawierać najwięcej części.
Czy Partition Labels to problem scalania przedziałów?
Tak, w przebraniu. Każda litera obejmuje przedział od swojej pierwszej kopii do ostatniej, nachodzące na siebie przedziały muszą mieć wspólną część, a ich scalenie daje dokładnie te części. Przebieg zachłanny to to samo scalanie wykonywane na bieżąco: end to prawa krawędź dotychczas scalonego bloku.
Ile części może zwrócić Partition Labels?
Od 1 do 26. Żadna litera nie może występować w dwóch częściach, więc każda część zawiera co najmniej jedną własną literę, a małych liter jest tylko 26. Ciąg znaków zawierający każdą literę dokładnie raz daje 26 części o długości 1, a ciąg znaków, który zaczyna się i kończy tą samą literą, daje jedną część.
Podobne zadania
Zadania oparte na tych samych pomysłach. Rozwiązanie dwóch lub trzech utrwala schemat.
Python
def partitionLabels(s):
# Napisz kod tutajPrzypadek 1
Przypadek 2
Przypadek 3
Wejście
s = "abacdcefe"
Oczekiwane
[3, 3, 3]