Longest Valid Parentheses
Otrzymujesz ciąg znaków s składający się wyłącznie ze znaków ( i ). Znajdź najdłuższy podciąg (ciąg kolejnych znaków), który jest poprawnie uformowany: każdy znak ( jest w nim zamykany przez późniejszy znak ), a pary są prawidłowo zagnieżdżone, jak w (()()). Zwróć długość tego podciągu lub 0, jeśli nie występuje nawet ().
Funkcja
- sstring
- ciąg znaków zawierający znaki ( i )
- Zwracainteger
- długość najdłuższego poprawnie sformowanego podciągu lub 0, jeśli taki nie istnieje
Ograniczenia
1 ≤ s.length ≤ 6 × 104- Każdy znak w
sto(albo).
Przykłady
- Wejście
- s = "()(())"
- Wyjście
- 6
- Wyjaśnienie
- Cały ciąg jest poprawnie utworzony:
(), po którym następuje(()). Dwa poprawnie utworzone fragmenty obok siebie tworzą jeden poprawnie utworzony fragment, więc odpowiedzią jest cały ciąg 6 znaków.
- Wejście
- s = "())((())"
- Wyjście
- 4
- Wyjaśnienie
- Znak
)o indeksie 2 nie ma pary, więc żadna odpowiedź nie może go przekroczyć, a znak(o indeksie 3 nigdy nie zostaje zamknięty. Najdłuższy fragment to(())od indeksu 4 do 7, o długości 4, który jest dłuższy niż()na początku.
- Wejście
- s = "))(("
- Wyjście
- 0
- Wyjaśnienie
- Oba
)występują przed oboma(, więc żadne(nigdy nie zostaje zamknięte. Żaden podciąg nie jest poprawnie sformowany, a odpowiedź wynosi 0.
+21 ukrytych testów przy wysłaniu
Pytanie dodatkowe
Czy możesz też podać, gdzie zaczyna się najdłuższy poprawnie sformowany podciąg, wybierając ten najbardziej z lewej, jeśli kilka ma taką samą długość?
Podpowiedzi
Otwieraj je po kolei. Każda zdradza trochę więcej.
Odczytuj podciąg od lewej do prawej i śledź bilans: +1 za
(, -1 za). Co dzieje się z bilansem w poprawnie sformowanym podciągu i co mówi o każdym podciągu, który przez niego przechodzi, znak), który obniża bilans poniżej zera?Przechowuj na stosie indeksy znaków
(, które nadal są otwarte. Gdy znak)zamyka ten znajdujący się na szczycie, poprawny ciąg kończący się tutaj zaczyna się tuż za indeksem, który jest teraz na szczycie. Co powinno znajdować się na stosie, gdy nic nie jest otwarte?Zacznij stos od -1, czyli indeksu bezpośrednio przed ciągiem znaków. Wstaw na stos indeks każdego
(. Przy)zdejmij element ze stosu; jeśli stos jest teraz pusty, tego)nie da się dopasować, więc wstaw jego indeks jako nową podstawę; w przeciwnym razie bieżący ciąg ma długośćiminus indeks na szczycie stosu. Zachowaj największą zmierzoną długość.
Rozwiązanie
Dwie rzeczy utrudniają to zadanie w porównaniu ze sprawdzeniem jednego ciągu znaków. Poprawne fragmenty łączą się, gdy stykają się ze sobą, więc sąsiadujące () i (()) liczą się jako jeden ciąg o długości 6. Pojedynczy zbłąkany znak, taki jak ) w ())(()), przecina ciąg, więc żadna odpowiedź nie może obejmować obu jego stron. Sprawdzanie każdego początku wymaga O(n²). Rozwiązaniem jest zapamiętanie, gdzie rozpoczął się bieżący ciąg: stos indeksów z bazowym znacznikiem na dole pozwala zrobić to w jednym przebiegu, a dwa przebiegi z prostymi licznikami — bez użycia stosu.
Rozwijaj podciąg od każdego początku
Poprawne, ale nie kończy się na największych testach
Intuicja
Czytaj podciąg od lewej do prawej, śledząc bilans, który zwiększa się o 1 dla ( i zmniejsza o 1 dla ). Podciąg jest poprawny dokładnie wtedy, gdy bilans nigdy nie spada poniżej 0 i na końcu wynosi 0. Wartość poniżej 0 oznacza, że pojawił się znak ), który nie miał żadnego otwartego nawiasu do zamknięcia.
Ustal więc początek i przechodź w prawo, aktualizując bilans po jednym znaku. Za każdym razem, gdy bilans wraca do 0, fragment od początku do tego miejsca jest poprawny, więc zapisujesz jego długość. Gdy tylko bilans spadnie poniżej 0, zatrzymaj się: ten znak ) pozostanie niedopasowany w każdym dłuższym fragmencie zaczynającym się w tym miejscu. Każdy poprawny podciąg ma jakiś początek, a dla każdego z nich sprawdzasz wszystkie możliwe końce, więc niczego nie pomijasz.
Problemem jest koszt. W ciągu złożonym z 59998 znaków (, po których następuje (), bilans nigdy nie spada poniżej 0, więc dla każdego początku przechodzisz do końca: około n²/2 = 1.8 × 10^9 kroków dla n = 6 × 10^4. Duże testy są skonstruowane właśnie w ten sposób. (Sprawdzanie każdego podciągu od początku zamiast jego rozbudowy byłoby jeszcze wolniejsze: O(n³).)
Algorytm
- Ustaw
bestna 0. - Dla każdego początku ustaw
balancena 0 i przechodź od początku do ostatniego znaku. - Dodaj 1 dla
(i odejmij 1 dla). - Jeśli
balancejest mniejsze od 0, przerwij dla tego początku. Jeśli wynosi 0, zaktualizujbest, używając długości fragmentuend - start + 1. - Zwróć
best.
def longestValidParentheses(s):
best = 0
for start in range(len(s)):
balance = 0
for end in range(start, len(s)):
balance += 1 if s[end] == "(" else -1
if balance < 0:
# A ')' without a partner: no longer run starts here
break
if balance == 0:
best = max(best, end - start + 1)
return bestStos indeksów ze znacznikiem podstawy
Intuicja
Dopasowywanie nawiasów za pomocą stosu jest dobrze znane: dodawaj każdy ( na stos, a przy każdym ) zdejmuj jeden element. Tutaj potrzebujesz także długości, więc dodawaj indeksy i zachowaj na dole stosu jeden dodatkowy indeks: bazę, czyli pozycję tuż przed ciągiem, w którym się znajdujesz. Na początku nic nie zostało odczytane, więc baza wynosi -1.
Przy ( dodaj jego indeks na stos. Przy ) zdejmij element ze stosu. Mogą się zdarzyć dwie rzeczy. Jeśli stos jest teraz pusty, zdjęty został indeks bazy, więc ten ) nie miał pasującego nawiasu otwierającego. Żaden poprawnie sformowany podciąg nie może go zawierać i staje się on nową bazą: dodaj jego indeks na stos. W przeciwnym razie indeks pozostały na wierzchu to ostatni znak przed ciągiem kończącym się na i: albo wciąż niedomknięty (, albo baza. Wszystko po nim aż do i jest dopasowane, a ciąg nie może sięgać dalej w lewo, więc jego długość wynosi i - top.
Oto ())((()):
i = 0,(: dodaj 0 na stos. Stos[-1, 0].i = 1,): zdejmij 0. Na wierzchu jest -1, więc ciąg ma długość1 - (-1) = 2.i = 2,): zdejmij -1 i stos jest pusty. Ten)nie ma pary, więc dodaj 2 jako nową bazę. Stos[2].i = 3, 4, 5, trzy(: dodaj je na stos. Stos[2, 3, 4, 5].i = 6,): zdejmij 5. Na wierzchu jest 4, więc ciąg ma długość6 - 4 = 2.i = 7,): zdejmij 4. Na wierzchu jest 3, więc ciąg ma długość7 - 3 = 4, czyli jest to odpowiedź.
Baza sprawia, że stykające się fragmenty łączą się. Dla ()(()) pierwsza para ma długość 1 - (-1) = 2, a ostatni ) zdejmuje indeks 2 i ponownie znajduje -1 na wierzchu, więc oblicza 5 - (-1) = 6. Liczenie od pasującego ( dałoby zamiast tego 4 i pominęłoby poprzedzające (). Każdy indeks jest dodawany na stos i zdejmowany z niego najwyżej raz, więc przejście ma złożoność O(n), a stos może przechowywać do n+1 indeksów.
Algorytm
- Rozpocznij stos od wartości -1 i ustaw
bestna 0. - Dla każdego indeksu
idodajina stos, jeślis[i]jest równe(. - Jeśli jest równe
), zdejmij jeden element ze stosu. - Jeśli stos jest teraz pusty, dodaj
ina stos jako nową podstawę. W przeciwnym razie zaktualizujbestwartościąi - top. - Zwróć
best.
def longestValidParentheses(s):
# The bottom of the stack is the index just before the current run
stack = [-1]
best = 0
for i, ch in enumerate(s):
if ch == "(":
stack.append(i)
else:
stack.pop()
if not stack:
# This ')' has no partner: it becomes the new base
stack.append(i)
else:
best = max(best, i - stack[-1])
return bestPolicz otwarcia i zamknięcia w dwóch przebiegach
Intuicja
Stos mówi tylko, gdzie zaczęła się bieżąca seria. Dwa liczniki też mogą to ustalić. Przejdź od lewej do prawej, zliczając opens i closes od ostatniego resetu. Gdy są równe, wszystko od resetu jest poprawnie sformowane, a jego długość wynosi 2 × closes. Gdy closes wysunie się na prowadzenie, nawias ) nie ma pary — w tej samej chwili stos stracił swoją podstawę — więc zresetuj oba liczniki do 0.
Jedno przejście nie wystarczy. Nawias (, który nigdy się nie zamyka, sprawia, że opens na zawsze wyprzedza closes, a liczniki już się nie zrównają. Dla (() przejście w lewo kończy się wynikiem 2 otwarcia i 1 zamknięcie i niczego nie zgłasza, choć () znajduje się tuż obok. Przejdź więc drugi raz, od prawej do lewej, zamieniając role: resetuj, gdy opens wysunie się na prowadzenie. Czytając od tyłu, dla (() najpierw napotykasz nawias zamykający, potem otwierający (liczniki są równe: długość 2), a następnie otwierający, który powoduje reset. Odpowiedzią jest większy wynik z tych dwóch przejść.
Dlaczego dwa przejścia znajdują każdą serię: najdłuższa seria jest ograniczona znakami, których nigdy nie można dopasować, albo końcami ciągu. Jeśli jej lewą granicą jest zbędny nawias ) lub początek ciągu, lewe przejście resetuje liczniki dokładnie tam, gdzie zaczyna się seria, i zauważa ich zrównanie w miejscu, gdzie się kończy. Jeśli jej lewą granicą jest zbędny nawias (, jej prawa granica nie może być nawiasem ), bo ten nawias ) zamknąłby zbędny nawias (, a seria byłaby dłuższa. Zatem prawą granicą jest zbędny nawias ( lub koniec ciągu, a prawe przejście znajduje serię w ten sam sposób. Każde przejście odczytuje ciąg raz, używając dwóch liczb całkowitych, więc czas działania wynosi O(n), a dodatkowa pamięć O(1).
Algorytm
- Ustaw
bestna 0, aopensiclosesna 0. - Idź od lewej do prawej, licząc każdy znak. Gdy liczniki są równe, zaktualizuj
best, używając2 × closes. Gdyclosesjest większe, wyzeruj oba liczniki. - Wyzeruj oba liczniki, a następnie przejdź od prawej do lewej w ten sam sposób, z wyjątkiem tego, że zerujesz liczniki, gdy
opensjest większe. - Zwróć
best.
def longestValidParentheses(s):
best = 0
# Left to right: more ')' than '(' ends every run that started earlier
opens = closes = 0
for ch in s:
if ch == "(":
opens += 1
else:
closes += 1
if opens == closes:
best = max(best, 2 * closes)
elif closes > opens:
opens = closes = 0
# Right to left: catches the runs that an unmatched '(' hid from the first pass
opens = closes = 0
for ch in reversed(s):
if ch == "(":
opens += 1
else:
closes += 1
if opens == closes:
best = max(best, 2 * opens)
elif opens > closes:
opens = closes = 0
return best
Pułapki i przypadki brzegowe
Większość błędnych odpowiedzi zlicza właściwe pary w niewłaściwych miejscach albo pomija początek ciągu.
- Zliczanie dopasowanych par w całym ciągu.
())((())zawiera 3 pary, ale nie wszystkie stykają się ze sobą, a wynik to 4, nie 6. - Mierzenie ciągu od pasującego
(. W()(())ostatni)pasuje do indeksu 2, co daje 4 i pomija znajdujące się wcześniej(). Mierz od indeksu pozostawionego na stosie po zdjęciu elementu. - Rozpoczynanie od pustego stosu. Pierwszy
)w())nie ma wtedy niczego, z czym można go porównać, a niedopasowane)zdejmuje element z pustego stosu. Wartość bazowa -1 rozwiązuje oba problemy. - Uruchamianie liczników tylko w jednym kierunku.
(()zwraca 0 przy przejściu od lewej do prawej, a())zwraca 0 przy przejściu od prawej do lewej; w obu przypadkach wynikiem jest 2. - Resetowanie liczników, gdy są równe. Równe liczniki oznaczają, że ciąg może się jeszcze wydłużyć, jak w przypadku
()(); resetuj je tylko wtedy, gdy jedna ze stron uzyska przewagę. - W Lua i R pozycje zaczynają się od 1, więc pierwsza wartość bazowa to 0, a nie -1.
Najczęstsze pytania4
Czaka złożoność czasowa problemu Najdłuższy poprawny nawias?
Zarówno rozwiązanie ze stosem, jak i rozwiązanie z licznikami w dwóch przebiegach odczytują każdy znak stałą liczbę razy, więc działają w czasie O(n). W najgorszym przypadku stos wymaga O(n) pamięci, na przykład dla ciągu złożonego wyłącznie z (, podczas gdy liczniki wymagają O(1). Sprawdzanie każdego możliwego początku ma złożoność O(n²).
Dlaczego stos zaczyna się od -1?
Długość serii to bieżący indeks minus indeks bezpośrednio przed serią. W przypadku serii zaczynającej się od indeksu 0 wcześniejszy indeks to -1, czyli pozycja o jeden krok przed ciągiem. Wstawienie najpierw -1 oznacza, że stos nigdy nie jest pusty podczas obliczania długości dopasowanego ), a gdy niedopasowany ) zdejmuje element ze stosu, to właśnie to ) staje się nową podstawą.
Czy istnieje rozwiązanie problemu najdłuższego poprawnego ciągu nawiasów za pomocą programowania dynamicznego?
Tak. Niech end[i] oznacza długość najdłuższego poprawnie sformowanego podciągu, który kończy się na indeksie i; wynosi ona 0, gdy s[i] to (. Jeśli s[i-1] to (, wtedy end[i] = end[i-2] + 2. Jeśli to ), sprawdź j = i - end[i-1] - 1, czyli znak przed ciągiem kończącym się na i-1: gdy s[j] to (, obejmuje on ten ciąg, a end[i] = end[i-1] + 2 + end[j-1], gdzie ostatni składnik łączy się z ciągiem, który styka się z nim z lewej strony. Odpowiedzią jest największa wartość end[i]; algorytm działa w czasie i wymaga pamięci O(n).
Dlaczego jedno przejście z licznikami nie wystarczy?
Przebieg od lewej do prawej resetuje się tylko wtedy, gdy ) występuje więcej razy niż (. Dodatkowy (, który nigdy nie zostaje zamknięty, sprawia, że liczniki pozostają różne do końca ciągu, więc podczas przebiegu nigdy się nie zrównują. W (() kończy się z 2 nawiasami otwierającymi i 1 zamykającym i niczego nie znajduje. Czytanie od prawej do lewej traktuje zbędny ( tak, jak pierwszy przebieg traktuje zbędny ), więc oba przebiegi razem obejmują każdy fragment.
Podobne zadania
Zadania oparte na tych samych pomysłach. Rozwiązanie dwóch lub trzech utrwala schemat.
Python
def longestValidParentheses(s):
# Wpisz kod tutajPrzypadek 1
Przypadek 2
Przypadek 3
Wejście
s = "()(())"
Oczekiwane
6