Combination Sum
Otrzymujesz listę candidates zawierającą różne dodatnie liczby całkowite oraz dodatnią liczbę całkowitą target. Znajdź wszystkie kombinacje elementów listy candidates, których wartości sumują się dokładnie do target, przy czym każdy element można wykorzystać dowolną liczbę razy. Dwie kombinacje są takie same, jeśli zawierają te same wartości tyle samo razy, więc [2, 3, 3] i [3, 2, 3] liczą się jako jedna kombinacja.
Zwróć każdą kombinację z wartościami w kolejności rosnącej, a kombinacje w porządku leksykograficznym: porównaj wartości dwóch kombinacji kolejno od lewej, a ta, która przy pierwszej różnicy ma mniejszą wartość, powinna znaleźć się wcześniej.
Funkcja
- candidatesinteger-array
- różne wartości, których możesz używać, w dowolnej kolejności i dowolną liczbę razy
- targetinteger
- suma każdej kombinacji musi wynosić dokładnie
- Zwracainteger-2d-array
- każda kombinacja, której suma jest równa wartości docelowej, każda w kolejności rosnącej, uporządkowana leksykograficznie
Ograniczenia
1 ≤ candidates.length ≤ 502 ≤ candidates[i] ≤ 5002 ≤ target ≤ 500- Wszystkie wartości w
candidatessą różne i nie są uporządkowane. - Co najmniej jedna kombinacja osiąga
target, a najwyżej 150 kombinacji to robi.
Przykłady
- Wejście
- candidates = [6, 2, 3]target = 8
- Wyjście
- [[2, 2, 2, 2], [2, 3, 3], [2, 6]]
- Wyjaśnienie
- Cztery dwójki dają 8, podobnie jak 2 + 3 + 3 oraz 2 + 6. Wszystkie trzy zaczynają się od 2, więc o kolejności decyduje druga wartość: 2, potem 3, a następnie 6. Bez 2 zostają tylko 3 i 6, a każda ich kombinacja jest wielokrotnością 3, a 8 nią nie jest.
- Wejście
- candidates = [5, 3, 4]target = 11
- Wyjście
- [[3, 3, 5], [3, 4, 4]]
- Wyjaśnienie
- 3 + 3 + 5 i 3 + 4 + 4 dają 11. Są takie same przy pierwszej wartości, a przy drugiej 3 jest mniejsze niż 4, więc
[3, 3, 5]jest pierwsze. Żadna kombinacja samych czwórek i piątek nie daje 11.
- Wejście
- candidates = [4, 9]target = 9
- Wyjście
- [[9]]
- Wyjaśnienie
- 9 samo w sobie jest kombinacją. 4 daje po drodze tylko 4, 8 i 12, a 4 + 9 to już 13, więc
[9]jest jedyną odpowiedzią.
+12 ukrytych testów przy wysłaniu
Pytanie dodatkowe
Każdy kandydat może być teraz użyty najwyżej raz, a candidates może zawierać powtarzające się wartości. Jak zmienić wyszukiwanie, aby żadna kombinacja nie pojawiła się dwa razy?
Podpowiedzi
Otwieraj je po kolei. Każda zdradza trochę więcej.
[2, 3, 3]i[3, 2, 3]to ta sama kombinacja. Jeśli zawsze tworzysz kombinację z wartościami w kolejności rosnącej, na ile sposobów można utworzyć każdą z nich?Sortuj kandydatów i twórz kombinację, dodając po jednej wartości naraz. Po dodaniu
nums[i]następną wartością może być ponownienums[i]albo dowolna późniejsza wartość, ale nigdy wcześniejsza.Napisz
backtrack(start, remaining). Gdyremainingwynosi 0, zapisz kopię bieżących wartości. W przeciwnym razie wykonuj pętlę odstart: dodaj wartość, wywołaj rekurencyjnie funkcję z tym samym indeksem i mniejszą pozostałą wartością, a następnie usuń tę wartość. Zakończ pętlę przy pierwszej wartości większej niżremaining.
Rozwiązanie
Każda odpowiedź jest multizbiorem kandydatów, a pułapka polega na budowaniu tego samego multizbioru więcej niż raz: wybierając 2, potem 3, a następnie 3, oraz wybierając 3, potem 2, a następnie 3, otrzymujemy tę samą kombinację. Rozwiązaniem jest budowanie każdej kombinacji w kolejności rosnącej, dzięki czemu można ją zbudować tylko na jeden sposób, oraz posortowanie kandydatów, aby gałąź kończyła się w chwili, gdy następna wartość jest większa niż to, co pozostało. To samo przechodzenie w kolejności rosnącej daje kombinacje w porządku leksykograficznym, bez końcowego sortowania.
Wypróbuj każdą liczbę dla każdego kandydata
Poprawne, ale nie kończy się na największych testach
Intuicja
Kombinacja jest w pełni opisana przez liczbę kopii każdego kandydata, których używa. Dla [6, 2, 3] i wartości docelowej 8 odpowiedź [2, 3, 3] oznacza jedną 2, dwie 3 i żadnej 6. Jednym ze sposobów znalezienia wszystkich odpowiedzi jest więc wypróbowanie każdej możliwej liczby kopii każdego kandydata i zachowanie tych wyborów, których suma jest dokładnie równa target. Kandydat c może wystąpić najwyżej target / c razy, więc jego liczba kopii mieści się w zakresie od 0 do tej granicy.
Wyobraź sobie drzewo decyzyjne z jednym poziomem na kandydata, po ich posortowaniu. Na poziomie i decydujesz, ile kopii i-tej wartości wziąć, a każdy liść na dole odpowiada jednemu pełnemu zestawowi wybranych liczb kopii. Każdy multizbiór ma dokładnie jedną listę liczb kopii, więc żadna kombinacja nie zostanie znaleziona dwa razy. Próbowanie największej liczby kopii w pierwszej kolejności zapewnia też wymaganą kolejność: gdy dwie odpowiedzi po raz pierwszy różnią się liczbą kopii jakiejś wartości, ta, która ma ich więcej, nadal zawiera tę małą wartość, podczas gdy druga ma już większą, więc pojawia się jako pierwsza.
Problemem jest rozmiar drzewa. Liczba liści to iloczyn target / c + 1 dla wszystkich kandydatów: dla posortowanej listy [2, 3, 6] i wartości docelowej 8 daje to 5 × 3 × 2 = 30 liści dla 3 odpowiedzi. Każdy kandydat większy niż target / 2 podwaja liczbę liści, mimo że może wystąpić najwyżej raz, więc już 40 takich kandydatów oznacza 2^40, czyli około 10^12 liści. Duże testy są skonstruowane właśnie w ten sposób i to podejście nie jest w stanie ich ukończyć.
Algorytm
- Posortuj kandydatów i utwórz tablicę liczności, po jednej dla każdej wartości.
- Napisz
choose(i, total), która ustala liczność wartości o indeksiei. - Dla
kodtarget / nums[i]do 0 ustaw liczność naki wywołajchoose(i + 1, total + k × nums[i]). - Gdy każda wartość ma już ustaloną liczność, zachowaj kombinację, jeśli
totaljest równetarget, wypisując każdą wartość tyle razy, ile wynosi jej liczność. - Wywołaj
choose(0, 0). Zachowane kombinacje są już w porządku leksykograficznym.
def combinationSum(candidates, target):
nums = sorted(candidates)
counts = [0] * len(nums)
result = []
def choose(i, total):
if i == len(nums):
if total == target:
combo = []
for value, k in zip(nums, counts):
combo.extend([value] * k)
result.append(combo)
return
# Most copies first, so the combinations come out in lexicographic order.
for k in range(target // nums[i], -1, -1):
counts[i] = k
choose(i + 1, total + k * nums[i])
counts[i] = 0
choose(0, 0)
return resultCofaj się w kolejności rosnącej i odrzucaj gałęzie
Intuicja
Buduj każdą kombinację po jednej wartości, tak jakbyś ją zapisywał: w kolejności rosnącej. Indeks początkowy wymusza tę kolejność. Po dodaniu nums[i] kolejną wartością może być ponownie nums[i], ponieważ kandydat może się powtarzać, albo dowolna późniejsza wartość, ale nigdy wcześniejsza. Dlatego wywołanie, które dodało indeks i, wykonuje pętlę tylko od i wzwyż. Każda kombinacja ma dokładnie jedną kolejność rosnącą, więc odpowiada jej dokładnie jedna ścieżka w drzewie, a duplikat taki jak [3, 2, 3] nigdy nie powstaje.
Oto całe drzewo dla posortowanej tablicy [2, 3, 6] i celu 8. Korzeń ma 8 do wykorzystania i próbuje wartości 2, 3 oraz 6. Po wybraniu 2 zostaje 6. Po wybraniu 2, 2 zostają 4, a po 2, 2, 2 zostają 2; dodanie jeszcze jednej 2 daje odpowiedź [2, 2, 2, 2]. Po 2, 2, 3 zostaje 1 i ścieżka kończy się niepowodzeniem. Po 2, 3 zostają 3 i można spróbować tylko wartości 3 i 6; wartość 3 daje [2, 3, 3]. Po 2, 6 nic nie zostaje: [2, 6]. Po wybraniu 3 można spróbować tylko wartości 3 i 6, a po 3, 3 zostają 2, których nie da się uzupełnić. Po wybraniu 6 zostają 2 i można spróbować tylko 6. Łącznie dwanaście wywołań, w porównaniu z 30 liśćmi w pierwszym podejściu.
Sortowanie zmienia ślepą uliczkę we wczesne zakończenie. Gdy nums[i] jest większe niż pozostała wartość, każda późniejsza wartość również jest większa, więc kończysz pętlę za pomocą break, zamiast sprawdzać pozostałe wartości. W powyższym drzewie węzeł 2, 2, 3 z pozostałą wartością 1 sprawdza 3, widzi, że ta wartość się nie mieści, i nie sprawdza już 6. Wyszukiwanie odwiedza tylko te prefiksy, których suma nadal nie przekracza target; dlatego duże testy, które spowalniają pierwsze podejście, wymagają tutaj tylko kilku tysięcy wywołań.
Kolejność wyników wynika z tego samego przejścia. Na każdym poziomie pętla najpierw próbuje mniejsze wartości, a każda kombinacja jest zapisywana w kolejności rosnącej. Dwie odpowiedzi po raz pierwszy różnią się na poziomie, na którym rozdzielają się ich ścieżki; ścieżka z mniejszą wartością na tym poziomie jest przeszukiwana jako pierwsza, więc odpowiedzi pojawiają się w porządku leksykograficznym. Jedna kombinacja nigdy nie może być prefiksem innej, ponieważ wartości są dodatnie i obie kombinacje osiągają tę samą sumę.
Algorytm
- Posortuj kandydatów w porządku rosnącym.
- Napisz funkcję
backtrack(start, remaining), która współdzieli jedną listępath. Jeśliremainingwynosi 0, zapisz kopiępath. - W przeciwnym razie wykonaj pętlę po
iodstartdo końca. Jeślinums[i] > remaining, przerwij: każda późniejsza wartość jest większa. - Dodaj
nums[i], wywołajbacktrack(i, remaining-nums[i])zi, a nie zi + 1, aby wartość mogła się powtarzać, a następnie ją usuń. - Wywołaj
backtrack(0, target)i zwróć zapisane kombinacje, już w porządku leksykograficznym.
def combinationSum(candidates, target):
nums = sorted(candidates)
result = []
path = []
def backtrack(start, remaining):
if remaining == 0:
result.append(path[:])
return
for i in range(start, len(nums)):
if nums[i] > remaining:
break # sorted, so every later value is too big as well
path.append(nums[i])
backtrack(i, remaining - nums[i]) # i, not i + 1: nums[i] may repeat
path.pop()
backtrack(0, target)
return result
Pułapki i przypadki brzegowe
Większość błędnych odpowiedzi wynika ze sposobu uporządkowania wyszukiwania, a nie z obliczeń.
- Przechodzenie w pętli przez każdego kandydata na każdym poziomie, zamiast zaczynać od bieżącego indeksu, tworzy
[2, 3, 3],[3, 2, 3]i[3, 3, 2]jako trzy odpowiedzi. Posortowanie każdej odpowiedzi i usunięcie duplikatów na końcu daje właściwą listę, ale wymaga wykładniczo więcej pracy. - Rekurencja z
i + 1zamiastipozwala użyć każdej wartości tylko raz, więc brakuje[2, 2, 2, 2]. - Zapisanie samego
pathzamiast jego kopii: każda zapisana odpowiedź jest wtedy tą samą listą, którą cofanie się wyczyściło do końca. - Używanie
breakdla kandydatów, których nie posortowano. Przy[6, 2, 3]i pozostałej wartości 2 pętla zatrzymuje się na 6 i nigdy nie sprawdza 2. - Zwracanie kombinacji w kolejności sugerowanej przez nieposortowane dane wejściowe. Oczekiwana lista jest uporządkowana leksykograficznie, co zapewnia wyszukiwanie na posortowanych danych, bez dodatkowego sortowania.
- W Lua i R tablice zaczynają się od 1, więc pierwsze wywołanie rozpoczyna się od indeksu 1, a pętla działa aż do długości tablicy.
Najczęstsze pytania4
Czy złożoność czasowa problemu sumy kombinacji wynosi?
Wyszukiwanie z nawrotami ma złożoność wykładniczą. Przy n kandydatach, wartości docelowej t i najmniejszym kandydacie m kombinacja zawiera najwyżej t/m wartości, a na każdym kroku mamy najwyżej n możliwości, co ogranicza liczbę operacji do O(n^(t/m)). Przycinanie na podstawie posortowanych kandydatów sprawia, że rzeczywista liczba wywołań jest znacznie mniejsza, ponieważ wyszukiwanie odwiedza tylko te prefiksy, których suma nadal wynosi co najwyżej t. Dodatkowe miejsce zajmuje O(t/m) na bieżącą ścieżkę i stos wywołań, a także miejsce na wynik.
Dlaczego w Combination Sum wywołujesz rekurencję z i, a nie z i + 1?
Rekurencja z i pozwala, by następną wartością ponownie był ten sam kandydat, dzięki czemu wartość może zostać użyta więcej niż raz. Rekurencja z i + 1 przechodzi dalej, co zmienia problem w wariant, w którym każdego kandydata można użyć co najwyżej raz. Druga część reguły jest równie ważna: nigdy nie cofając się do indeksu wcześniejszego niż i, zachowujemy każdą kombinację w kolejności rosnącej i zapobiegamy duplikatom.
Jak uniknąć powtarzających się kombinacji bez użycia zbioru?
Generuj każdą kombinację w ustalonej kolejności rosnącej. Indeks początkowy to zapewnia: po umieszczeniu nums[i] wyszukiwanie sprawdza tylko nums[i] i późniejsze wartości. Każda kombinacja ma wtedy dokładnie jedną ścieżkę w drzewie wyszukiwania, więc jest tworzona tylko raz i nie jest potrzebny zbiór ani końcowe usuwanie duplikatów.
Czy problem Combination Sum można rozwiązać za pomocą programowania dynamicznego?
Tak. Dla każdej sumy od 0 do wartości docelowej przechowuj listę kombinacji, które ją osiągają, i dodawaj po jednym kandydacie, tak aby wartości w każdej liście pozostawały w kolejności rosnącej — to ten sam pomysł co zliczanie sposobów na wydanie reszty. Algorytm nigdy nie eksploruje dwa razy ślepej uliczki, ale przechowuje każdą częściową kombinację dla każdej sumy, co wymaga znacznie więcej pamięci niż przeszukiwanie z nawrotami, a końcowa lista może wymagać sortowania. Ponieważ sam wynik może mieć rozmiar wykładniczy, zwykle stosuje się przeszukiwanie z nawrotami.
Podobne zadania
Zadania oparte na tych samych pomysłach. Rozwiązanie dwóch lub trzech utrwala schemat.
Python
def combinationSum(candidates, target):
# Wpisz kod tutajPrzypadek 1
Przypadek 2
Przypadek 3
Wejście
candidates = [6, 2, 3] target = 8
Oczekiwane
[[2, 2, 2, 2], [2, 3, 3], [2, 6]]