Longest Increasing Subsequence
Otrzymujesz listę liczb całkowitych nums. Podciąg zachowuje niektóre elementy w ich pierwotnej kolejności, a pozostałe pomija; zachowane elementy nie muszą znajdować się obok siebie. Zwróć długość najdłuższego podciągu, którego wartości ściśle rosną od lewej do prawej. Dwie równe wartości występujące kolejno nie oznaczają wzrostu.
Funkcja
- numsinteger-array
- lista liczb całkowitych, z których można wybierać
- Zwracainteger
- długość najdłuższego ściśle rosnącego podciągu
Ograniczenia
1 ≤ nums.length ≤ 2500-104 ≤ nums[i] ≤ 104
Przykłady
- Wejście
- nums = [3, 1, 8, 2, 5, 9, 4, 7]
- Wyjście
- 4
- Wyjaśnienie
- Pozostawienie 1, 2, 5, 9 daje rosnący podciąg o długości 4, podobnie jak 1, 2, 5, 7 oraz 1, 2, 4, 7. Nie ma wyboru pięciu wartości, który zachowywałby rosnącą kolejność, więc odpowiedź wynosi 4.
- Wejście
- nums = [7, 7, 7, 7]
- Wyjście
- 1
- Wyjaśnienie
- Wartości muszą ściśle rosnąć, więc żadne dwie liczby 7 nie mogą znaleźć się w tym samym podciągu. Pojedynczy element również się liczy, dlatego odpowiedź to 1.
- Wejście
- nums = [12, -4, 0, 25, -10, 3, 16, 5]
- Wyjście
- 4
- Wyjaśnienie
- -4, 0, 3, 16 ma długość 4 (-4, 0, 3, 5 też). Zaczynając od pierwszego elementu, 12, otrzymujesz tylko dwie wartości, na przykład 12, 25: najlepsza podsekwencja nie musi zaczynać się od początku.
+20 ukrytych testów przy wysłaniu
Pytanie dodatkowe
Czy potrafisz zwrócić jedną najdłuższą rosnącą podsekwencję, a nie tylko jej długość, i nadal działać w czasie O(n log n)?
Podpowiedzi
Otwieraj je po kolei. Każda zdradza trochę więcej.
Najlepszą podsekwencję z całej listy trudno opisać bezpośrednio. Zadaj bardziej szczegółowe pytanie dla każdego indeksu
i: jaka jest najdłuższa rosnąca podsekwencja, która kończy się dokładnie nanums[i]?Podciąg kończący się na
nums[i]składa się albo wyłącznie znums[i], albo jest kontynuacją najlepszego podciągu kończącego się na wcześniejszymnums[j] < nums[i]. Wybierz najlepsze takieji dodaj jeden. Odpowiedzią jest największa z tych wartości, niezależnie od miejsca zakończenia podciągu.Aby zejść poniżej
O(n²), zachowuj dla każdej długości tylko najmniejszą wartość, którą może kończyć się podciąg o tej długości. Wartości te pozostają posortowane, więc wyszukiwanie binarne podpowie Ci, czy nowa liczba wydłuża najdłuższy podciąg, czy zastępuje jego ostatnią wartość.
Rozwiązanie
Podciąg może pomijać dowolne elementy, więc lista n liczb ma 2^n podciągów — zdecydowanie za dużo, by je sprawdzić. Rozwiązanie z programowaniem dynamicznym polega na zadaniu węższego pytania dla każdego indeksu: jak długa jest najdłuższy rosnący podciąg, który kończy się dokładnie tutaj? Daje to tablicę o rozmiarze O(n²). Najszybsza wersja przechowuje jedną liczbę dla każdej długości — najmniejszą wartość, jaką może mieć ostatni element podciągu o tej długości — i wstawia każdy nowy element za pomocą wyszukiwania binarnego.
Wybierz lub pomiń każdy element
Poprawne, ale nie kończy się na największych testach
Intuicja
Przejdź przez listę i podejmij jedną decyzję dla każdego elementu: zachować go czy pominąć. Możesz zachować nums[i] tylko wtedy, gdy jest większy od ostatniej zachowanej wartości. Funkcja rekurencyjna longest(i, prev) odpowiada na pytanie: jeśli ostatni zachowany element znajduje się pod indeksem prev (lub -1, gdy nie zachowano jeszcze żadnego elementu), ile kolejnych elementów możesz dodać, zaczynając od indeksu i?
Pominięcie daje longest(i+1, prev). Zachowanie elementu, gdy jest dozwolone, daje 1 + longest(i+1, i). Odpowiedzią jest większa z tych dwóch wartości. Po dojściu do końca listy nie można już nic dodać, więc wynik wynosi 0. Każdy rosnący podciąg odpowiada jednej ścieżce decyzji o zachowaniu lub pominięciu elementów, więc wyszukiwanie nie może przeoczyć najlepszego rozwiązania.
To rozwiązanie jest powolne, ponieważ obie gałęzie pozostają otwarte, gdy wartości rosną. Na liście takiej jak 1, 2, 3, ..., n liczba wywołań podwaja się z każdym elementem: 2 do potęgi 40 to już około 10^12 wywołań, a duże testy zawierają 2500 elementów. Jednak longest(i, prev) zależy tylko od pary (i, prev), więc istnieje co najwyżej n² różnych pytań. Następnym podejściem jest zadanie każdego z nich tylko raz.
Algorytm
- Napisz
longest(i, prev), gdzieprevjest indeksem ostatniego zachowanego elementu lub-1. - Jeśli
iwykracza poza koniec, zwróć 0. - Pomiń
nums[i]:best = longest(i+1, prev). - Jeśli
prevwynosi-1lubnums[i] > nums[prev], zachowaj element:best = max(best, 1 + longest(i+1, i)). - Zwróć
best. Odpowiedzią jestlongest(0, -1).
def lengthOfLIS(nums):
def longest(i, prev):
# The longest increasing subsequence of nums[i:] whose values all exceed nums[prev].
# prev is -1 while nothing has been taken.
if i == len(nums):
return 0
best = longest(i + 1, prev) # skip nums[i]
if prev == -1 or nums[i] > nums[prev]:
best = max(best, 1 + longest(i + 1, i)) # take nums[i]
return best
return longest(0, -1)Najdłuższy podciąg kończący się na każdym indeksie
Intuicja
Stan. Niech ending[i] oznacza długość najdłuższego rosnącego podciągu, którego ostatnim elementem jest nums[i]. Ustalenie ostatniego elementu sprawia, że problem można łatwo podzielić: gdy wiesz, gdzie kończy się podciąg, wiesz, jakie późniejsze wartości mogą po nim wystąpić.
Rekurencja. Jeśli podciąg kończący się na nums[i] ma więcej niż jeden element, elementem poprzedzającym nums[i] jest pewne nums[j], dla którego j < i i nums[j] < nums[i], a podciąg kończący się na tym elemencie powinien być jak najdłuższy. Zatem ending[i] = 1 + max(ending[j]) dla takich j. Przypadek bazowy: każdy element sam w sobie jest podciągiem, więc początkowo ending[i] ma wartość 1. Kolejność: ending[i] odczytuje tylko mniejsze indeksy, więc wypełniaj tablicę od lewej do prawej.
Dla [3, 1, 8, 2, 5, 9, 4, 7] tablica ma postać [1, 1, 2, 2, 3, 4, 3, 4]. Na przykład przed 5 może wystąpić 3, 1 lub 2, a najlepszy z tych podciągów kończy się na 2 i ma ending = 2, więc ending[4] = 3. Odpowiedzią jest największa wartość w tablicy, czyli 4, a nie jej ostatni element: najlepszy podciąg może kończyć się w dowolnym miejscu.
Każdy indeks sprawdza po kolei wszystkie wcześniejsze indeksy, więc łączna liczba porównań wynosi n(n-1)/2, czyli około 3.1 × 10^6 dla n = 2500.
Algorytm
- Utwórz
ending, ustawiając każdą wartość na 1. - Dla każdego
i, od lewej do prawej, sprawdź każdej < i. - Jeśli
nums[j] < nums[i], ustawending[i]naending[j] + 1, jeśli ta wartość jest większa. - Zwróć największą wartość w
ending.
def lengthOfLIS(nums):
n = len(nums)
# ending[i]: the longest increasing subsequence that ends with nums[i]
ending = [1] * n
for i in range(1, n):
for j in range(i):
if nums[j] < nums[i] and ending[j] + 1 > ending[i]:
ending[i] = ending[j] + 1
return max(ending)Najmniejsze ogony z wyszukiwaniem binarnym
Intuicja
Powyższa tabela zapamiętuje jedną długość dla każdego indeksu. Możesz zapamiętywać mniej: dla każdej długości tylko najmniejszą wartość, którą może kończyć się rosnący podciąg o tej długości. Oznaczmy ją jako tails[k] dla długości k+1. Mniejsze zakończenie jest zawsze co najmniej równie dobre, ponieważ każda wartość, która może wystąpić po podciągu kończącym się na 9, może też wystąpić po podciągu kończącym się na 5.
tails jest zawsze posortowane ściśle rosnąco: podciąg o długości k+2 kończący się na t zawiera podciąg o długości k+1, który kończy się wartością mniejszą niż t. Dlatego dla każdej nowej wartości x wyszukaj binarnie pierwszy ogon, który jest ≥ x. Jeśli go nie ma, x jest większe od każdego ogona i wydłuża najdłuższy podciąg, więc dopisz je na końcu. W przeciwnym razie zastąp ten ogon przez x: podciąg krótszy o jeden element kończy się wartością mniejszą niż x, więc dodanie x daje tę samą długość z mniejszą wartością końcową.
Dla [3, 1, 8, 2, 5, 9, 4, 7] tails przyjmuje kolejno wartości [3], [1], [1, 8], [1, 2], [1, 2, 5], [1, 2, 5, 9], [1, 2, 4, 9], [1, 2, 4, 7], a jego długość 4 jest odpowiedzią. Na etapie [1, 2, 4, 9] wartość 4 pojawiła się w danych wejściowych po 9, więc tails samo w sobie nie jest podciągiem; znaczenie ma tylko jego długość. Metoda ta jest również nazywana sortowaniem cierpliwościowym, od gry karcianej, w której każdy ogon jest wierzchnią kartą stosu.
Każdy element wymaga jednego wyszukiwania binarnego wśród co najwyżej n ogonów: około 2500 × 12 = 30 000 kroków dla największego wejścia.
Algorytm
- Zacznij od pustej listy
tails. - Dla każdego
xwnumswyszukaj binarnie pierwszy indeksk, dla któregotails[k] ≥ x. - Jeśli żaden element
tailsnie jest≥ x, dołączx. - W przeciwnym razie ustaw
tails[k] = x. - Zwróć długość
tails.
from bisect import bisect_left
def lengthOfLIS(nums):
# tails[k]: the smallest last value of any increasing subsequence of length k + 1
tails = []
for x in nums:
k = bisect_left(tails, x) # the first tail that is >= x
if k == len(tails):
tails.append(x) # x extends the longest subsequence so far
else:
tails[k] = x # x is a smaller ending for length k + 1
return len(tails)
Pułapki i przypadki brzegowe
Większość błędnych odpowiedzi wynika z pomylenia tego, co przechowuje tablica, albo z traktowania równych wartości jako rosnących.
- Zwracanie
ending[n-1]zamiast największego elementu. Dla[1, 2, 3, 0]ostatni element to 1, ale odpowiedź wynosi 3. - Porównywanie za pomocą
≤zamiast<.[7, 7, 7, 7]musi zwrócić 1, a nie 4. - W wersji z ogonami wyszukiwanie pierwszego ogona
> xzamiast≥ x. Przy powtórzeniach dopisuje to drugą 7 po pierwszej i zlicza równe wartości jako dłuższy podciąg. - Traktowanie
tailsjako samego podciągu. Jego wartości mogą pochodzić z różnych podciągów, więc wypisuj go tylko wtedy, gdy osobno śledzisz poprzedniki. - Przypadkowe rozwiązywanie wersji ciągłej. W
[3, 1, 8, 2, 5, 9, 4, 7]najdłuższy rosnący ciąg sąsiadujących elementów to 2, 5, 9 (długość 3), a odpowiedź wynosi 4. - W Lua i R tablice zaczynają się od 1, więc znacznik
prev = -1oparty na indeksowaniu od 0 zmienia się na 0, a wyszukiwanie binarne przebiega po indeksach od 1 do bieżącego rozmiaru.
Najczęstsze pytania4
Jaka jest złożoność czasowa problemu najdłuższego rosnącego podciągu?
Metoda tails działa w czasie O(n log n) i wykorzystuje O(n) pamięci: jedno wyszukiwanie binarne na każdy element. Tabela programowania dynamicznego obejmująca każdą parę indeksów wymaga O(n²) czasu, a sprawdzenie każdego podciągu wymaga O(2ⁿ). Dla n = 2500 daje to około 30 000, 3 milionów i astronomiczną liczbę kroków.
Dlaczego metoda sortowania cierpliwości daje prawidłową długość?
Po każdym elemencie tails[k] przechowuje najmniejszą wartość, którą może kończyć się dowolny podciąg rosnący o długości k+1 napotkany do tej pory. Dodanie następuje tylko wtedy, gdy x jest większe od każdego elementu tails, co oznacza, że istnieje teraz podciąg o jeden element dłuższy niż wszystkie dotychczasowe. Zastąpienie nigdy nie zmienia długości — jedynie zmniejsza wartość końcową, więc długość listy zawsze jest równa długości najdłuższego podciągu rosnącego.
Jak uzyskać właściwy najdłuższy rosnący podciąg, a nie tylko jego długość?
Zapisz poprzednika dla każdego elementu. W tabeli O(n²) poprzednikiem i jest j, które nadało ending[i] jego wartość. W metodzie z ogonami zapisuj indeks elementu znajdującego się za każdym ogonem i ustawiaj poprzednikiem elementu indeks zapisany o jedną pozycję w lewo, gdy element zostanie umieszczony. Następnie przejdź wstecz po poprzednikach od końca najdłuższego podciągu i odwróć wynik.
Jak zamiast tego znaleźć najdłuższy niemalejący podciąg?
Zezwalaj na równych sąsiadów. W tabeli użyj nums[j] ≤ nums[i]. W metodzie ogonów wyszukuj pierwszy ogon, który jest ściśle większy od x, a nie większy lub równy, aby równa wartość wydłużała listę zamiast zastępować ogon. [7, 7, 7, 7] zwraca wtedy 4.
Podobne zadania
Zadania oparte na tych samych pomysłach. Rozwiązanie dwóch lub trzech utrwala schemat.
Python
def lengthOfLIS(nums):
# Wpisz kod tutajPrzypadek 1
Przypadek 2
Przypadek 3
Wejście
nums = [3, 1, 8, 2, 5, 9, 4, 7]
Oczekiwane
4