3Sum
Otrzymujesz listę liczb całkowitych nums. Znajdź każdą trójkę [a, b, c] wartości pobranych z trzech różnych pozycji w nums, dla których a + b + c = 0. Zapisz każdą trójkę w kolejności niemalejącej (a ≤ b ≤ c) i uwzględnij każdą różną trójkę tylko raz, nawet jeśli powstaje ona przy kilku wyborach pozycji. Zwróć trójki posortowane według pierwszej wartości, a następnie drugiej.
Funkcja
- numsinteger-array
- lista liczb całkowitych zawierająca co najmniej trzy elementy
- Zwracainteger-2d-array
- każda odrębna trójka, której suma wynosi 0, każda uporządkowana niemalejąco, a lista posortowana
Ograniczenia
3 ≤ nums.length ≤ 3000-105 ≤ nums[i] ≤ 105- Co najmniej jedna trójka sumuje się do 0.
- Dwie trójki są takie same, gdy zawierają te same trzy wartości.
Przykłady
- Wejście
- nums = [-2, 0, 1, 1, -1, 2]
- Wyjście
- [[-2, 0, 2], [-2, 1, 1], [-1, 0, 1]]
- Wyjaśnienie
- -2 + 0 + 2, -2 + 1 + 1 oraz -1 + 0 + 1 dają 0.
[-2, 1, 1]może używać wartości 1 dwa razy, ponieważ 1 występuje na dwóch pozycjach, podczas gdy[-1, 0, 1]można utworzyć z użyciem dowolnej z dwóch jedynek, ale pojawia się tylko raz.
- Wejście
- nums = [0, 0, 0, 0]
- Wyjście
- [[0, 0, 0]]
- Wyjaśnienie
- Dowolne trzy z czterech zer dają w sumie 0. To cztery możliwe wybory pozycji, ale wszystkie dają tę samą trójkę, więc odpowiedź zawiera
[0, 0, 0]tylko raz.
+15 ukrytych testów przy wysłaniu
Pytanie dodatkowe
Ten sam wzorzec rozwiązuje problem 4Sum: ustal dwie wartości i użyj dwóch wskaźników dla pozostałych. Czy potrafisz napisać to w O(n³) i poprawnie obsłużyć duplikaty na każdym poziomie?
Podpowiedzi
Otwieraj je po kolei. Każda zdradza trochę więcej.
Najpierw posortuj listę. Posortowana lista pomaga na dwa sposoby: każda trójka pojawia się w kolejności, a równe wartości znajdują się obok siebie, więc powtórzenie zawsze występuje bezpośrednio po wartości, którą powtarza.
Ustal najmniejszą wartość z trójki,
nums[i]. Dwie pozostałe muszą sumować się do-nums[i]i pochodzą z posortowanych wartości po prawej stroniei. To pytanie o sumę pary w posortowanej liście.Dla tej pary ustaw jeden wskaźnik tuż za
i, a drugi na ostatnim indeksie. Jeśli suma tych trzech wartości jest mniejsza niż 0, przesuń lewy wskaźnik w prawo; jeśli jest większa, przesuń prawy wskaźnik w lewo. Po znalezieniu dopasowania przesuń oba wskaźniki i przesuń lewy wskaźnik za kolejne kopie jego wartości. Pomijaj każdei, którego wartość jest równa wartości poprzedzającej je.
Rozwiązanie
Dwie rzeczy sprawiają, że 3Sum jest trudniejsze, niż się wydaje. Sprawdzenie każdej trójki wymaga O(n³), a odpowiedź musi zawierać każdą trójkę tylko raz, nawet gdy wartości się powtarzają. Sortowanie rozwiązuje oba problemy: jednakowe wartości trafiają obok siebie, więc można pomijać powtórzenia, porównując sąsiadów, a gdy najmniejsza wartość jest już ustalona, pozostałe dwie tworzą problem sumy pary w posortowanej liście, który można rozwiązać jednym przebiegiem za pomocą dwóch wskaźników.
Wypróbuj każdą trójkę
Poprawne, ale nie kończy się na największych testach
Intuicja
Najpierw posortuj listę. Wtedy dowolne trzy pozycje i < j < k dają wartości, które są już uporządkowane, nums[i] ≤ nums[j] ≤ nums[k], więc trójka jest zapisana poprawnie w chwili, gdy ją znajdziesz. Trzy zagnieżdżone pętle odwiedzają każdy wybór pozycji, więc żadna trójka nie zostanie pominięta.
Teraz powtórzenia. Pierwszy posortowany przykład to [-2, -1, 0, 1, 1, 2], a [-1, 0, 1] może pobrać swoją jedynkę z indeksu 3 albo indeksu 4. Każda pętla pomija więc pozycję, której wartość jest równa wartości wypróbowanej wcześniej przez tę samą pętlę. Każda pętla wypróbowuje wtedy każdą odrębną wartość tylko raz, a każda odrębna trójka pojawia się raz, już w posortowanej kolejności. Pomijanie porównuje tylko z poprzednią pozycją wewnątrz tej samej pętli, więc [-2, 1, 1] nadal wykorzystuje obie jedynki.
Problemem jest koszt. Istnieje około n³/6 trójek: dla 3000 liczb daje to 4.5 × 10^9 sum, czyli znacznie więcej, niż pozwala jakikolwiek limit czasu.
Algorytm
- Posortuj
nums. - Iteruj
ipo pozycjach i pomijaji, gdynums[i]jest równenums[i-1]. - Wewnątrz tej pętli iteruj
jodi+1i pomijajj, gdyj > i+1inums[j]jest równenums[j-1]. - Wewnątrz tej pętli iteruj
kodj+1, stosując tę samą regułę pomijania, i zapisuj[nums[i], nums[j], nums[k]], gdy suma tych trzech wartości wynosi 0. - Zwróć trójki w kolejności, w jakiej je znaleziono. Są już posortowane.
def threeSum(nums):
nums = sorted(nums)
n = len(nums)
triplets = []
for i in range(n - 2):
if i > 0 and nums[i] == nums[i - 1]:
continue # same first value as the round before
for j in range(i + 1, n - 1):
if j > i + 1 and nums[j] == nums[j - 1]:
continue # same second value as the round before
for k in range(j + 1, n):
if k > j + 1 and nums[k] == nums[k - 1]:
continue # same third value as the round before
if nums[i] + nums[j] + nums[k] == 0:
triplets.append([nums[i], nums[j], nums[k]])
return tripletsNapraw jedną wartość, znajdź parę za pomocą zbioru haszującego
Intuicja
Gdy pierwsza wartość nums[i] jest ustalona, potrzebujesz dwóch kolejnych wartości, których suma wynosi -nums[i]. To problem Two Sum. Przesuwaj j w prawo od i i przechowuj w zbiorze wartości, które minąłeś. Dla każdego j brakująca wartość to need = -nums[i] - nums[j]. Jeśli need znajduje się w zbiorze, [nums[i], need, nums[j]] daje w sumie 0. Wyszukiwanie w zbiorze ma średni koszt O(1), więc dla jednego i koszt wynosi O(n), a dla całego wyszukiwania O(n²).
Sortowanie nadal pomaga zarządzać przetwarzaniem. Pomiń i, jeśli jego wartość jest taka sama jak poprzedniej. Po znalezieniu dopasowania przesuń j za wszystkie wystąpienia nums[j]: gdy pierwsza i trzecia wartość są ustalone, środkowa również jest ustalona, więc kolejna taka sama wartość mogłaby jedynie powtórzyć tę samą trójkę. Ponieważ need pochodzi z wcześniejszej pozycji na posortowanej liście, need ≤ nums[j], a wartości w trójce są uporządkowane. Możesz też przerwać, gdy tylko nums[i] > 0: dwie kolejne wartości są co najmniej tak duże, więc suma nie może wynieść 0.
Jeden szczegół: gdy j przesuwa się w prawo, nums[j] rośnie, a need maleje, więc dla jednego i trójki pojawiają się w kolejności malejących wartości środkowych. Dla [-2, -1, 0, 1, 1, 2] przy i = 0 znajdujesz [-2, 1, 1] przy drugim 1, a następnie [-2, 0, 2] przy 2. Odwróć każdą grupę, zanim dodasz ją do odpowiedzi. Wersje C i R oznaczają widziane wartości w tablicy indeksowanej wartością zamiast w zbiorze haszującym, co działa, ponieważ każda wartość mieści się w zakresie ±10^5.
Algorytm
- Posortuj
nums. - Dla każdego
iprzerwij, gdynums[i] > 0, a pomińi, gdynums[i]jest równenums[i-1]. - Utwórz pusty zbiór. Dla każdego
jzaczynając odi+1, obliczneed = -nums[i] - nums[j]. Jeślineedznajduje się w zbiorze, zapisz[nums[i], need, nums[j]]i przesuńjza kopienums[j]. - Dodaj
nums[j]do zbioru i przejdź do następnegoj. - Odwróć kolejność trójek znalezionych dla tego
ii dołącz je do odpowiedzi.
def threeSum(nums):
nums = sorted(nums)
n = len(nums)
triplets = []
for i in range(n - 2):
if nums[i] > 0:
break # the two values after it are at least as large
if i > 0 and nums[i] == nums[i - 1]:
continue # this first value was already handled
group = []
seen = set() # values between position i and position j
j = i + 1
while j < n:
need = -nums[i] - nums[j]
if need in seen:
group.append([nums[i], need, nums[j]])
while j + 1 < n and nums[j + 1] == nums[j]:
j += 1
seen.add(nums[j])
j += 1
# need shrinks as nums[j] grows, so this group came out backwards
group.reverse()
triplets.extend(group)
return tripletsSortuj i użyj dwóch wskaźników
Intuicja
Posortowana kolejność może zastąpić zbiór. Ustal nums[i], ustaw lo na i+1, a hi na ostatnim indeksie, i sprawdź nums[i] + nums[lo] + nums[hi]. Jeśli suma jest mniejsza od 0, potrzebujesz większej wartości, więc lo przesuwa się w prawo. Jeśli jest większa od 0, potrzebujesz mniejszej wartości, więc hi przesuwa się w lewo. Gdy suma wynosi dokładnie 0, zapisz trójkę i przesuń oba wskaźniki.
Żadna trójka nie zostanie pominięta. Gdy suma jest mniejsza od 0, nums[lo] jest za małe nawet w połączeniu z największą pozostałą wartością, nums[hi], więc nie może utworzyć pary z żadną wartością, która nadal mieści się w zakresie, a pominięcie go niczego nie tracisz. Suma większa od 0 to sytuacja odwrotna: nums[hi] jest za duże nawet w połączeniu z najmniejszą pozostałą wartością. W każdym kroku jedna wartość zostaje ostatecznie pominięta, więc dla jednego i potrzeba najwyżej n kroków, a całe wyszukiwanie ma złożoność O(n²) i nie wymaga pamięci poza sortowaniem i wynikiem.
Weź posortowaną tablicę [-2, -1, 0, 1, 1, 2]. Dla i = 0 (wartość -2) lo zaczyna od -1, a hi od 2: suma wynosi -1, więc lo przesuwa się na 0. Teraz -2 + 0 + 2 = 0, więc zapisujesz [-2, 0, 2], a oba wskaźniki trafiają na dwie jedynki, które dają [-2, 1, 1]. Dla i = 1 (wartość -1) wartości 0 i 2 dają sumę 1, więc hi przesuwa się na drugą jedynkę, a -1 + 0 + 1 = 0 pozwala zapisać [-1, 0, 1]. Wartość 0 przy i = 2 nie daje żadnego wyniku, a przy i = 3 wartość jest dodatnia, więc wyszukiwanie się kończy.
Powtarzające się wartości wymagają dwóch reguł. Pomiń i, którego wartość jest równa poprzedniej. Po znalezieniu wyniku przesuń lo za kolejne wystąpienia użytej wartości. Dla hi nie jest potrzebna osobna reguła: gdy lo wskazuje większą wartość, kopia poprzedniego nums[hi] daje teraz sumę większą od 0 i sama powoduje przesunięcie wskaźnika. Ponieważ i odwiedza różne wartości w kolejności rosnącej, a lo przesuwa się tylko w prawo, trójki są zwracane w posortowanej kolejności.
Algorytm
- Posortuj
nums. - Dla każdego
iprzerwij, gdynums[i] > 0, i pomińi, gdynums[i]jest równenums[i-1]. - Ustaw
lo = i+1ihi = n-1. Gdylo < hi, dodaj do siebienums[i],nums[lo]inums[hi]. - Jeśli suma jest mniejsza od 0, przesuń
low prawo. Jeśli jest większa od 0, przesuńhiw lewo. - Jeśli wynosi 0, zapisz trójkę, przesuń oba wskaźniki, a następnie przesuń
loza kopie użytej wartości. - Zwróć trójki. Są już posortowane.
def threeSum(nums):
nums = sorted(nums)
n = len(nums)
triplets = []
for i in range(n - 2):
if nums[i] > 0:
break # the two values after it are at least as large
if i > 0 and nums[i] == nums[i - 1]:
continue # this first value was already handled
lo, hi = i + 1, n - 1
while lo < hi:
total = nums[i] + nums[lo] + nums[hi]
if total < 0:
lo += 1
elif total > 0:
hi -= 1
else:
triplets.append([nums[i], nums[lo], nums[hi]])
lo += 1
hi -= 1
while lo < hi and nums[lo] == nums[lo - 1]:
lo += 1
return triplets
Pułapki i przypadki brzegowe
Większość błędnych odpowiedzi wynika z powtarzających się wartości, dlatego testuj rozwiązanie na danych wejściowych, które je zawierają.
- Pominięcie
i, gdynums[i]jest równenums[i+1], sprawia, że ostatnia kopia każdej wartości staje się pierwszym elementem, a wcześniejsze kopie znikają. W[-1, -1, 2]prowadzi to do utraty[-1, -1, 2]. Porównuj z poprzednią pozycją:nums[i-1]. - Zatrzymanie działania, gdy
nums[i] ≥ 0, zamiast gdynums[i] > 0, pomija[0, 0, 0]. - Usuwanie powtórzeń na końcu zamiast ich pomijania. Przy 3000 zerach pętla z dwoma wskaźnikami zapisuje miliony kopii
[0, 0, 0]przed jakimkolwiek czyszczeniem, a w kilku językach zbiór list porównuje listy według tożsamości, więc kopie i tak pozostają. - Dwukrotne użycie tej samej pozycji. Wersja z tablicą mieszającą, która zapełnia zbiór całą listą z góry, zmienia
[-2, 1, 3]w[-2, 1, 1], używając jedynej jedynki dwukrotnie. Wyszukuj wartości tylko na pozycjach, które zostały już przetworzone. - Zwracanie trójek w niewłaściwej kolejności. Porównanie jest dokładne, więc wersja z tablicą mieszającą musi odwrócić każdą grupę, a rozwiązanie, które zbiera trójki w zbiorze, musi je na końcu posortować.
Najczęstsze pytania4
Jaka jest złożoność czasowa problemu 3Sum?
Rozwiązanie wykorzystujące sortowanie i dwa wskaźniki działa w czasie O(n²). Sortowanie kosztuje O(n log n), a każda z n możliwości wyboru pierwszej wartości wymaga jednego przejścia w czasie O(n). Potrzebuje O(1) dodatkowej pamięci poza sortowaniem i wynikiem. Sprawdzanie każdej trójki wymaga natomiast O(n³).
Jak 3Sum unika zduplikowanych trójek?
Sortuje listę, więc równe wartości znajdują się obok siebie. Następnie pomija pierwszą wartość, która jest równa poprzedniej, a po każdym dopasowaniu przesuwa lewy wskaźnik za kopie użytej wartości. Każda trójka jest znajdowana raz, na podstawie pierwszych kopii jej wartości, więc nie trzeba tworzyć zbioru wyników.
Czy w przypadku problemu 3Sum użyć dwóch wskaźników czy zbioru mieszającego?
Oba działają w czasie O(n²). Metoda dwóch wskaźników nie wymaga dodatkowej pamięci, a posortowane dane zapewniają, że trójki są już uporządkowane. Zbiór haszujący wymaga O(n) pamięci i trzeba zadbać o to, by pozycje były różne, a wynik posortowany. Pomysł ze zbiorem haszującym jest przydatny, gdy nie możesz sortować, jak w przypadku Two Sum, gdzie zwracasz oryginalne indeksy.
Czy problem 3Sum można rozwiązać szybciej niż w czasie O(n²)?
Niewiele. Najlepsze znane algorytmy przewyższają n² tylko o kilka czynników logarytmicznych, a wiele wyników dotyczących trudności obliczeniowej w geometrii obliczeniowej zakłada, że żaden algorytm nie osiąga potęgi n mniejszej niż 2. Te szybsze algorytmy są wynikami badań, więc O(n²) to odpowiedź, której oczekuje się na rozmowach kwalifikacyjnych.
Podobne zadania
Zadania oparte na tych samych pomysłach. Rozwiązanie dwóch lub trzech utrwala schemat.
Python
def threeSum(nums):
# Napisz tutaj kodPrzypadek 1
Przypadek 2
Wejście
nums = [-2, 0, 1, 1, -1, 2]
Oczekiwane
[[-2, 0, 2], [-2, 1, 1], [-1, 0, 1]]