Sliding Window Maximum
Otrzymujesz tablicę liczb całkowitych nums oraz rozmiar okna k. Okno obejmuje k kolejnych wartości. Zaczyna się na lewym końcu tablicy i za każdym razem przesuwa się o jedną pozycję w prawo, aż jego prawa krawędź znajdzie się przy ostatniej wartości.
Zwróć tablicę zawierającą największą wartość w oknie na każdej z jego pozycji, od lewej do prawej. Tablica o długości n ma n-k+1 okien, więc wynik zawiera n-k+1 wartości.
Funkcja
- numsinteger-array
- tablica, po której przesuwa się okno
- kinteger
- liczba wartości w każdym oknie
- Zwracainteger-array
- największa wartość w każdym oknie, od okna najbardziej po lewej do okna najbardziej po prawej
Ograniczenia
1 ≤ k ≤ nums.length ≤ 2 × 104-104 ≤ nums[i] ≤ 104- Wynik zawiera
nums.length-k+1wartości, po jednej dla każdego okna, w kolejności od lewej do prawej.
Przykłady
- Wejście
- nums = [4, 2, 12, 3, 8, 5, 1]k = 3
- Wyjście
- [12, 12, 12, 8, 8]
- Wyjaśnienie
- 12 znajduje się w pierwszych trzech oknach:
[4, 2, 12],[2, 12, 3]i[12, 3, 8]. Gdy wypada z okna, w oknach[3, 8, 5]i[8, 5, 1]największą wartością jest 8.
- Wejście
- nums = [-3, -1, -7, -2]k = 2
- Wyjście
- [-1, -1, -2]
- Wyjaśnienie
- Przedziały to
[-3, -1],[-1, -7]i[-7, -2]. Większa z dwóch liczb ujemnych to ta bliższa zeru, co daje -1, -1 i -2.
- Wejście
- nums = [6, 6, 1]k = 3
- Wyjście
- [6]
- Wyjaśnienie
- Gdy
kjest równe długości tablicy, istnieje jedno okno — cała tablica. Jej największa wartość to 6, a druga kopia 6 nie daje drugiej odpowiedzi.
+15 ukrytych testów przy wysłaniu
Pytanie dodatkowe
Czy potrafisz zbudować kolejkę, która obsługuje dodawanie wartości na końcu, usuwanie wartości z początku i odczytywanie jej bieżącej wartości maksymalnej, każdą z tych operacji w zamortyzowanym czasie O(1)?
Podpowiedzi
Otwieraj je po kolei. Każda zdradza trochę więcej.
Przeskanowanie każdego okna w poszukiwaniu jego największej wartości kosztuje
kkroków na okno. Porównaj dwa sąsiednie okna: mająk-1wspólnych wartości, ponieważ jedna wartość opuszcza okno z lewej strony, a inna wchodzi do niego z prawej.Gdy pojawia się nowa wartość, żadna starsza wartość w oknie, która jest od niej mniejsza lub równa, nie może już nigdy być maksimum. Nowa wartość pozostaje w każdym późniejszym oknie, w którym nadal znajduje się starsza wartość, i jest od niej co najmniej tak duża. Możesz na stałe odrzucić te starsze wartości.
Przechowuj indeksy wartości, które pozostają w kolejce dwustronnej, tak aby ich wartości ściśle malały od początku do końca. Dla każdego nowego indeksu usuwaj z końca mniejsze lub równe wartości, dodaj indeks, usuń pierwszy element, jeśli wypadł już z okna, i odczytaj maksimum z okna na początku kolejki.
Rozwiązanie
Sąsiadujące okna mają wspólne k-1 wartości, więc obliczanie każdego maksimum od zera powtarza niemal całą pracę. Trudność polega na tym, że maksimum nie da się cofnąć: gdy największa wartość wysuwa się z lewej strony, potrzebujesz następnej największej, nie odczytując ponownie całego okna. Monotoniczna kolejka dwustronna przechowuje w odpowiedniej kolejności dokładnie te wartości, które wciąż mogą stać się maksimum, więc odpowiedź zawsze znajduje się na jej początku, a każdy indeks jest do niej dodawany i usuwany tylko raz.
Skanuj każde okno
Poprawne, ale nie kończy się na największych testach
Intuicja
Najbardziej bezpośredni pomysł wynika z treści zadania. Okno zaczynające się pod indeksem start obejmuje elementy od start do start+k-1. Odczytaj te k wartości, zachowaj największą i przesuń początek o jeden krok w prawo. Istnieje n-k+1 możliwych początków, od 0 do n-k.
To rozwiązanie jest poprawne z definicji: każde okno jest odczytywane w całości, więc jego największa wartość nie może zostać pominięta. Dodatkowa pamięć to jedna zmienna przechowująca bieżące maksimum, poza wynikiem.
To rozwiązanie jest powolne. Odczytanie każdego z n-k+1 okien wymaga k odczytów, a ich iloczyn jest największy, gdy k wynosi mniej więcej połowę n. Dla n = 2 × 10^4 i k = 10^4 oznacza to 10^4 okien po 10^4 wartości, czyli 10^8 odczytów. Co gorsza, dwa sąsiednie okna współdzielą k-1 wartości, więc niemal każdy odczyt powtarza jeden z tych, które już wykonano.
Algorytm
- Utwórz pustą listę wyników.
- Wykonuj pętlę dla
startod 0 don-k. - Ustaw
bestnanums[start], a następnie porównaj tę wartość z każdą kolejną aż donums[start+k-1]i zachowaj większą. - Dodaj
bestdo wyniku. - Zwróć wynik.
def maxSlidingWindow(nums, k):
result = []
for start in range(len(nums) - k + 1):
# Read all k values of this window again
result.append(max(nums[i] for i in range(start, start + k)))
return resultBloki z maksimami z każdej strony
Intuicja
Podziel tablicę na bloki o długości k: indeksy od 0 do k-1, następnie od k do 2k-1 i tak dalej; ostatni blok będzie krótszy, jeśli n nie jest wielokrotnością k. Okno ma dokładnie k elementów, więc albo pokrywa się z jednym blokiem, albo obejmuje koniec jednego bloku i początek następnego. Nigdy nie obejmuje trzech bloków.
To sugeruje użycie dwóch tablic. fromStart[i] to największa wartość od początku bloku zawierającego i do i; tablicę tę wypełnia się od lewej do prawej i resetuje na początku każdego bloku. toEnd[i] to największa wartość od i do końca jego bloku; tablicę tę wypełnia się od prawej do lewej i resetuje na końcu każdego bloku. Okno zaczynające się na i kończy się na i+k-1. Jego lewą część obejmuje toEnd[i], a prawą fromStart[i+k-1], więc maksimum okna to większa z tych dwóch wartości. Gdy okno obejmuje cały blok, obie części mają maksimum tego bloku, więc wynik nadal jest poprawny.
Dla nums = [4, 2, 12, 3, 8, 5, 1] i k = 3 bloki to [4, 2, 12], [3, 8, 5] i [1]. fromStart wynosi [4, 4, 12, 3, 8, 8, 1], a toEnd wynosi [12, 12, 12, 8, 8, 5, 1]. Okno [2, 12, 3] zaczyna się na indeksie 1: toEnd[1] = 12 obejmuje 2 i 12, fromStart[3] = 3 obejmuje 3, a wynikiem jest 12.
Algorytm działa w czasie O(n) i wymaga trzech przejść przez tablicę. Kosztem są dwie pomocnicze tablice o długości n; ponadto przed obliczeniem wyniku dla pierwszego okna potrzebna jest cała tablica.
Algorytm
- Wypełniaj
fromStartod lewej do prawej: kopiujnums[i], gdyijest wielokrotnościąk, w przeciwnym razie wybierz większą z wartościfromStart[i-1]inums[i]. - Wypełniaj
toEndod prawej do lewej: kopiujnums[i], gdyijest ostatnim indeksem lubi+1jest wielokrotnościąk, w przeciwnym razie wybierz większą z wartościtoEnd[i+1]inums[i]. - Dla każdego początku
iod 0 don-kdołącz większą z wartościtoEnd[i]ifromStart[i+k-1]. - Zwróć wynik.
def maxSlidingWindow(nums, k):
n = len(nums)
# Cut nums into blocks of k: indices 0..k-1, k..2k-1, and so on.
from_start = [0] * n # max from the start of i's block up to i
to_end = [0] * n # max from i up to the end of i's block
for i in range(n):
if i % k == 0:
from_start[i] = nums[i]
else:
from_start[i] = max(from_start[i - 1], nums[i])
for i in range(n - 1, -1, -1):
if i == n - 1 or (i + 1) % k == 0:
to_end[i] = nums[i]
else:
to_end[i] = max(to_end[i + 1], nums[i])
# A window [i, i+k-1] is the tail of one block plus the head of the next.
return [max(to_end[i], from_start[i + k - 1]) for i in range(n - k + 1)]Monotoniczna kolejka indeksów
Intuicja
Zacznij od jednej obserwacji. Załóżmy, że indeks j występuje przed indeksem i i nums[j] ≤ nums[i]. Każde późniejsze okno, w którym nadal znajduje się j, zawiera też i, ponieważ i leży dalej na prawo i opuszcza okno później. We wszystkich tych oknach nums[i] jest co najmniej tak duże, więc j nie może już być maksimum. Gdy pojawia się i, j staje się nieprzydatne i możesz o nim zapomnieć.
Przechowuj w kolejce dwustronnej indeksy, o których jeszcze nie zapomniano. Gdy pojawia się i, usuwaj indeksy z końca kolejki, dopóki ich wartości są nie większe niż nums[i], a następnie dodaj i. Pozostałe wartości będą wtedy ściśle malejące od początku do końca, ponieważ każda starsza wartość, która nie była większa, zostałaby usunięta. Zatem na początku kolejki znajduje się największa wartość w oknie. Deque przechowuje indeksy, a nie wartości, ponieważ indeks z początku kolejki również musi zostać usunięty, gdy okno go minie: okno kończące się na i zaczyna się na i-k+1, więc indeks i-k to ten, który właśnie wypadł z okna; jeśli znajduje się na początku kolejki, usuń go.
Prześledź nums = [4, 2, 12, 3, 8, 5, 1] dla k = 3, wypisując wartości w kolejce dwustronnej. Pojawia się 4: [4]. 2 jest mniejsze, więc czeka za nim: [4, 2]. 12 usuwa obie wartości: [12], a odpowiedzią dla pierwszego okna jest 12. 3 czeka: [12, 3], odpowiedź 12. 8 usuwa 3: [12, 8], odpowiedź 12. 5 czeka: [12, 8, 5], ale 12 znajduje się na indeksie 2, a okno kończące się na indeksie 5 zaczyna się na indeksie 3, więc 12 wypadło z okna: [8, 5], odpowiedź 8. 1 czeka: [8, 5, 1], odpowiedź 8.
Dlaczego złożoność wynosi O(n): pętla wewnętrzna może usunąć kilka indeksów w jednym kroku, ale każdy indeks jest dodawany raz i usuwany najwyżej raz — z końca, gdy pokona go większa wartość, albo z początku, gdy wypadnie z okna. Łączna liczba usunięć w całym przebiegu wynosi najwyżej n, więc łączna praca to najwyżej 2n operacji na kolejce dwustronnej. Każdy indeks w kolejce należy do bieżącego okna, więc kolejka nigdy nie zawiera ich więcej niż k.
Algorytm
- Utwórz pustą dwustronną kolejkę indeksów i pustą listę wyników.
- Dla każdego indeksu
iusuwaj indeksy z końca kolejki, dopóki kolejka nie jest pusta, a wartość na jej końcu jest mniejsza lub równanums[i]. - Dodaj
ina końcu. - Jeśli indeks na początku jest równy
i-k, opuścił okno: usuń go z początku. - Gdy
i ≥ k-1, pełne okno kończy się nai: dodaj do wyniku wartość pod indeksem z początku kolejki. - Zwróć wynik.
from collections import deque
def maxSlidingWindow(nums, k):
window = deque() # indices; their values strictly decrease from front to back
result = []
for i, x in enumerate(nums):
# A value at the back that is not bigger than x can never be a maximum again.
while window and nums[window[-1]] <= x:
window.pop()
window.append(i)
# The front index has slid out of the window on the left.
if window[0] == i - k:
window.popleft()
# From index k-1 on, every step completes a window; its maximum sits at the front.
if i >= k - 1:
result.append(nums[window[0]])
return result
Pułapki i przypadki brzegowe
Większość błędów wynika z granic okna lub z tego, co przechowuje kolejka dwustronna.
- Przechowywanie wartości zamiast indeksów. Wtedy usuwasz pierwszy element, gdy jest równy
nums[i-k], a duplikaty powodują błędy. Dla[3, 1, 3]ik = 2druga 3 usuwa pierwszą, a potem sama zostaje usunięta, ponieważ jest równa wartości, która opuściła okno. Przechowuj indeksy i porównuj pierwszy indeks zi-k. - Podawanie odpowiedzi za wcześnie lub za późno. Pierwsze pełne okno kończy się na indeksie
k-1, a niek, a wynik musi zawierać dokładnien-k+1wartości. - Usuwanie niewłaściwego indeksu. Okno kończące się na
izaczyna się nai-k+1, więci-kto indeks elementu, który opuszcza okno. Usunięciei-k+1usuwa wartość, która nadal znajduje się w oknie. - Odczytywanie ostatniego lub pierwszego elementu pustej kolejki dwustronnej. Zanim porównasz element z ostatnim, sprawdź, czy kolejka coś zawiera.
- Traktowanie kolejki dwustronnej jak kopii okna. Zawiera tylko kandydatów — od 1 do
kindeksów — więc jej rozmiar nic nie mówi o oknie. - W podejściu blokowym łatwo zapomnieć, że ostatni blok może być krótszy niż
k. Przejście od prawej do lewej musi zaczynać się również od ostatniego indeksu, a także od końca każdego bloku.
Najczęstsze pytania4
Jaka jest złożoność czasowa problemu maksimum w przesuwanym oknie?
Rozwiązanie z użyciem monotonicznej kolejki dwustronnej działa w czasie O(n). Każdy indeks jest dodawany raz i usuwany co najwyżej raz, więc wewnętrzna pętla wykonuje łącznie najwyżej n operacji usuwania, nawet jeśli w pojedynczym kroku może usunąć kilka elementów. Kolejka zawiera najwyżej k indeksów, więc dodatkowe zużycie pamięci wynosi O(k) poza wynikiem.
Czy problem maksimum w oknie przesuwnym można rozwiązać za pomocą kopca?
Tak. Wstawiaj pary wartości i indeksu do kopca maksymalnego. Przed odczytaniem elementu ze szczytu usuwaj go, dopóki jego indeks znajduje się poza oknem, ponieważ nieaktualne wpisy są usuwane dopiero wtedy, gdy dotrą na szczyt. Działa to w czasie O(n log n) i może przechowywać do n wpisów. Kolejka dwustronna jest szybsza i zajmuje mniej miejsca, ponieważ usuwa nieprzydatne wartości, gdy tylko pojawi się większa.
Dlaczego deque przechowuje indeksy, a nie wartości?
Przód musi opuścić kolejkę, gdy okno przesunie się poza niego, a tylko jego indeks ci o tym powie. Na podstawie samych wartości trzeba byłoby zgadywać, korzystając z nums[i-k], co nie działa, gdy ta sama wartość pojawia się więcej niż raz. Indeks pozwala też uzyskać wartość bez dodatkowego kosztu, jako nums[index].
Jaka jest różnica między monotoniczną kolejką dwustronną a monotonicznym stosem?
Koniec kolejki dwustronnej działa jak stos monotoniczny: zanim dodasz wartość, usuwasz te, które przez nią stają się bezużyteczne. Kolejka dwustronna dodaje drugi koniec z przodu, aby usuwać wartości, które są już za stare. Problem bez terminu ważności, taki jak znajdowanie następnego większego elementu, wymaga tylko stosu; okno przesuwne wymaga obu końców. Odwróć warunek porównania, a ten sam kod zwróci minimum w każdym oknie.
Podobne zadania
Zadania oparte na tych samych pomysłach. Rozwiązanie dwóch lub trzech utrwala schemat.
Python
def maxSlidingWindow(nums, k):
# Wpisz kod tutajPrzypadek 1
Przypadek 2
Przypadek 3
Wejście
nums = [4, 2, 12, 3, 8, 5, 1] k = 3
Oczekiwane
[12, 12, 12, 8, 8]