Partition Equal Subset Sum
Otrzymujesz tablicę nums dodatnich liczb całkowitych. Zdecyduj, czy możesz podzielić wartości na dwie grupy o równych sumach. Każda wartość trafia dokładnie do jednej grupy, a grupa może zawierać wartości z dowolnych pozycji. Zwróć true, jeśli taki podział istnieje, a w przeciwnym razie false.
Funkcja
- numsinteger-array
- wartości dodatnie do podziału na dwie grupy
- Zwracaboolean
- true, gdy wartości można podzielić na dwie grupy o równych sumach, w przeciwnym razie false
Ograniczenia
1 ≤ nums.length ≤ 2001 ≤ nums[i] ≤ 100
Przykłady
- Wejście
- nums = [6, 1, 4, 9, 2]
- Wyjście
- true
- Wyjaśnienie
- Suma wynosi 22, więc każda grupa potrzebuje 11. Grupy 9 + 2 oraz 6 + 1 + 4 dają po 11, więc odpowiedź to
true.
- Wejście
- nums = [4, 7, 2, 9, 6]
- Wyjście
- false
- Wyjaśnienie
- Suma wynosi 28, więc każda grupa potrzebuje 14. Grupie, która ma 9, brakuje 5, a żadna kombinacja 4, 7, 2 i 6 nie daje 5, więc odpowiedź to
false, mimo że suma jest parzysta.
- Wejście
- nums = [1, 2, 3, 5]
- Wyjście
- false
- Wyjaśnienie
- Suma wynosi 11. Dwie równe liczby całkowite zawsze dają w sumie liczbę parzystą, więc nieparzystej sumy nigdy nie da się podzielić i odpowiedź to
false.
+18 ukrytych testów przy wysłaniu
Pytanie dodatkowe
Gdy nie da się podzielić na równe grupy, czy możesz zwrócić najmniejszą możliwą różnicę między sumami obu grup?
Podpowiedzi
Otwieraj je po kolei. Każda zdradza trochę więcej.
Jeśli sumy obu grup są równe, ile musi wynosić każda z nich w zależności od sumy wszystkich elementów
nums? A co od razu mówi ci nieparzysta suma?Wystarczy znaleźć jedną grupę, której suma wynosi połowę całości; pozostałe wartości tworzą drugą grupę. Zastanów się, jakie sumy mogą uzyskać pierwsze wartości i jak kolejna wartość zmienia ten zbiór.
Utrzymuj tablicę logiczną
reach[0..target], w której tylkoreach[0]ma wartość true. Dla każdej wartościnumprzechodź posodtargetw dół donumi oznaczajreach[s], gdyreach[s-num]jest oznaczone. Przechodzenie w dół zapobiega użyciu każdej wartości dwa razy.
Rozwiązanie
Każda grupa musi zawierać dokładnie połowę całości, więc prawdziwe pytanie brzmi, czy suma pewnego podzbioru nums jest równa target = total / 2. Sprawdzenie wszystkich podzbiorów wymaga 2^n operacji, co jest beznadziejne dla 200 wartości. Same sumy są jednak niewielkie: target wynosi co najwyżej 200 × 100 / 2 = 10^4. Zapisywanie osiągalnych sum, po jednej wartości naraz, zamienia wyszukiwanie w tabelę plecakową 0/1, która wypełnia się w O(n × sum) krokach.
Wypróbuj każdy podzbiór za pomocą rekurencji
Poprawne, ale nie kończy się na największych testach
Intuicja
Zacznij od obliczenia sumy. Jeśli jest nieparzysta, podział nie jest możliwy, ponieważ suma dwóch równych liczb całkowitych jest parzysta. W przeciwnym razie każda grupa musi mieć sumę równą dokładnie target = total / 2. Gdy znajdziesz wartości, które dają target, niewybrane wartości same utworzą drugą połowę. Wystarczy więc odpowiedzieć na jedno pytanie: czy istnieje podzbiór, którego suma wynosi target?
Przejdź po kolei przez wartości i dla każdej podejmij jedną decyzję: dodaj ją do pierwszej grupy albo zostaw ją dla drugiej. Funkcja pomocnicza reach(i, remaining) odpowiada na pytanie, czy wartości od indeksu i wzwyż mogą dać sumę remaining. Zwraca true, gdy remaining osiąga 0, false, gdy kończą się wartości lub suma spada poniżej 0, a w przeciwnym razie sprawdza obie możliwości dla nums[i].
Każdy podzbiór odpowiada jednej ścieżce wyborów, więc wyszukiwanie nie może przeoczyć podziału i daje poprawną odpowiedź. Jest powolne, ponieważ istnieje 2^n ścieżek, a dane wejściowe, dla których podział nie istnieje, zmuszają je do sprawdzenia niemal wszystkich. Weź 199 kopii liczby 100 i jedną liczbę 98: suma wynosi 19998, suma docelowa 9999 nigdy nie zostaje osiągnięta, a wyszukiwanie sprawdza każdy sposób wybrania najwyżej 99 setek — około 4 × 10^59 ścieżek. Nawet 40 wartości daje 2^40, czyli około 10^12 ścieżek.
Algorytm
- Dodaj do siebie elementy
nums. Jeśli suma jest nieparzysta, zwróćfalse. - Ustaw
targetna połowę sumy. - Napisz
reach(i, remaining): zwróć true, gdyremainingwynosi 0, i false, gdyiwykracza poza ostatnią wartość lubremainingjest mniejsze od 0. - W przeciwnym razie zwróć
reach(i+1, remaining-nums[i])lubreach(i+1, remaining): weź tę wartość albo ją pomiń. - Zwróć
reach(0, target).
def canPartition(nums):
total = sum(nums)
if total % 2 == 1:
return False
# Can some of the values from index i on add up to exactly remaining?
def reach(i, remaining):
if remaining == 0:
return True
if i == len(nums) or remaining < 0:
return False
# Put nums[i] in the first group, or leave it for the second one
return reach(i + 1, remaining - nums[i]) or reach(i + 1, remaining)
return reach(0, total // 2)Wypełnij tabelę według wartości i sumy
Intuicja
Rekurencja zadaje wciąż to samo pytanie. reach(i, remaining) zależy tylko od dwóch liczb: i od 0 do n oraz remaining od 0 do target. Oznacza to co najwyżej (n+1) × (target+1) różnych pytań, czyli około 201 × 10001 ≈ 2 × 10^6 przy wartościach granicznych — wystarczająco mało, by odpowiedzieć na każde z nich tylko raz.
Zbuduj odpowiedzi w tabeli, zaczynając od początku. can[i][s] informuje, czy pewne wartości spośród pierwszych i sumują się do s. Bez żadnych wartości możliwa jest tylko suma 0, więc wiersz 0 zawiera false, z wyjątkiem can[0][0]. Wartość num = nums[i-1] daje dwa sposoby uzyskania s: pominąć num, czyli wcześniejsze wartości już sumują się do s, albo ją dodać, czyli wcześniejsze wartości sumują się do s-num. To cała reguła: can[i][s] = can[i-1][s] or can[i-1][s-num], przy czym druga część ma znaczenie tylko wtedy, gdy s ≥ num. Każdy wiersz odczytuje dane tylko z wiersza powyżej, więc każda wartość jest używana co najwyżej raz.
Dla [6, 1, 4, 9, 2] i celu 11 osiągalne sumy zwiększają się od {0} do {0, 6}, następnie do {0, 1, 6, 7}, a potem do {0, 1, 4, 5, 6, 7, 10, 11}. Suma 11 pojawia się po dodaniu 4 (6 + 1 + 4), a kolejne wiersze ją zachowują. Odpowiedzią jest can[n][target]. Każda komórka wymaga stałej liczby operacji, więc zarówno czas, jak i pamięć mają złożoność O(n × target).
Algorytm
- Zwróć
falsedla nieparzystej sumy i ustawtargetna jej połowę. - Utwórz tabelę z n+1 wierszami i target+1 kolumnami, wypełnioną wartościami false, i ustaw
can[0][0]na true. - Dla każdego wiersza
iod 1 do n przyjmijnum = nums[i-1]. - Dla każdej sumy
sod 0 dotargetustawcan[i][s]nacan[i-1][s]lub, gdys ≥ num, nacan[i-1][s-num]. - Zwróć
can[n][target].
def canPartition(nums):
total = sum(nums)
if total % 2 == 1:
return False
target = total // 2
n = len(nums)
# can[i][s] is True when some of the first i values add up to s
can = [[False] * (target + 1) for _ in range(n + 1)]
can[0][0] = True
for i in range(1, n + 1):
num = nums[i - 1]
for s in range(target + 1):
# Leave num out, or put it in and reach s - num with the values before it
can[i][s] = can[i - 1][s] or (s >= num and can[i - 1][s - num])
return can[n][target]Jeden wiersz sum, wypełniany od góry do dołu
Intuicja
Każdy wiersz tabeli odczytuje tylko wiersz powyżej, więc wystarczy jeden wiersz, jeśli aktualizujesz go w miejscu: reach[s] informuje, czy niektóre z dotychczasowych wartości sumują się do s. Niebezpieczna jest kolejność aktualizacji. Jeśli przechodzisz po s rosnąco, reach[s-num] mogło już zostać ustawione przez tę samą wartość num. Dla [3, 9] i celu 6 wartość 3 oznacza reach[3], a następnie odczytuje tę wartość, by oznaczyć reach[6], tak jakby dostępne były dwie trójki, i zwracasz true dla podziału, który nie istnieje.
Przechodź po s malejąco, od target do num. Wtedy s-num jest mniejszym indeksem, którego ta wartość jeszcze nie zmieniła, więc reach[s-num] nadal zawiera odpowiedź sprzed pojawienia się num. To dokładnie can[i-1][s-num] z tabeli, a pojedynczy wiersz wykonuje pracę całej tabeli.
Możesz też zatrzymać się, gdy tylko reach[target] stanie się true, ponieważ kolejne wartości tylko dodają osiągalne sumy i nigdy żadnej nie usuwają. W najgorszym przypadku nadal potrzeba O(n × target) kroków, czyli około 2 × 10^6, a ilość pamięci spada do target + 1 wartości logicznych.
Algorytm
- Zwróć
falsedla nieparzystej sumy i ustawtargetna jej połowę. - Utwórz
reachztarget + 1elementami, wszystkie ustaw nafalse, z wyjątkiemreach[0]. - Dla każdej wartości
numprzechodź przezsodtargetw dół donumi ustawreach[s]natrue, gdyreach[s-num]ma wartośćtrue. - Po każdej wartości zwróć
true, jeślireach[target]ma wartośćtrue. - Jeśli pętla się zakończy, zwróć
reach[target], czylifalse.
def canPartition(nums):
total = sum(nums)
if total % 2 == 1:
return False
target = total // 2
# reach[s] is True when some of the values seen so far add up to s
reach = [False] * (target + 1)
reach[0] = True
for num in nums:
# Walk the sums downward so num is used at most once
for s in range(target, num - 1, -1):
if reach[s - num]:
reach[s] = True
if reach[target]:
return True
return reach[target]
Pułapki i przypadki brzegowe
Błędne odpowiedzi wynikają z ufania zachłannej regule, pomijania sprawdzenia parzystości i ponownego używania wartości w tabeli z jednym wierszem.
- Przechodzenie po sumach w górę w wersji z jednym wierszem powoduje użycie tej samej wartości więcej niż raz. Dla
[3, 9]celem jest 6; wartość 3 oznacza sumę 3, a potem 6, więc odpowiadasz true. - Pomijanie sprawdzenia parzystości: dla
[1, 2]suma 3 jest zaokrąglana w dół do celu 1, wartość 1 pozwala go osiągnąć, więc odpowiadasz true, mimo że taki podział nie jest możliwy. - Zachłanne wypełnianie, na przykład przez sortowanie i zawsze dodawanie do lżejszej grupy, zawodzi dla
[3, 3, 2, 2, 2]: kończy się wynikiem 7 wobec 5, chociaż 3 + 3 = 2 + 2 + 2. - Wartość większa od celu, jak w
[2, 2, 2, 10]. Pętla malejąca odtargetdonumwykona się wtedy zero razy, co jest poprawne, ale zakres taki jak(num+1):(target+1)w R przechodzi wstecz i psuje tabelę. Pomijaj takie wartości. - Parzysta suma nie wystarczy: suma
[4, 7, 2, 9, 6]wynosi 28, a mimo to nie da się jej podzielić. - W Lua i R tablice zaczynają się od indeksu 1, więc wpis dla sumy
sznajduje się pod indeksems + 1.
Najczęstsze pytania4
Dlaczego problem podziału na równe sumy podzbiorów jest problemem plecakowym 0/1?
Masz plecak o pojemności target = total / 2 i musisz wypełnić go dokładnie, używając każdej wartości co najwyżej raz. Wybór, czy wziąć wartość, czy ją pominąć, to wybór 0/1, a rozmiar wartości jest równy samej wartości. Tablica plecaka zawierająca osiągalne sumy pozwala rozwiązać ten problem w czasie O(n × target).
Jaka jest złożoność czasowa problemu podziału na podzbiory o równych sumach?
Podejście tabelaryczne zajmuje O(n × target) czasu, gdzie target to połowa sumy wszystkich wartości, i O(target) pamięci przy użyciu jednego wiersza. Dla 200 wartości nie większych niż 100 oznacza to około 2 × 10^6 kroków. Ograniczenie rośnie wraz z wielkością wartości, a nie tylko ich liczbą, dlatego nazywa się je pseudowielomianowym: przy wartościach bliskich 10^9 żadna tabela by się nie zmieściła, a ogólny problem jest NP-zupełny.
Dlaczego pętla wewnętrzna biegnie od wartości docelowej w dół do wartości?
Przechodzenie w dół oznacza, że odczytujemy reach[s-num], zanim ta wartość będzie mogła się zmienić, więc nadal opisuje wartości sprzed num. Przechodzenie w górę pozwoliłoby rozszerzyć sumę utworzoną z użyciem num ponownie o num, przez co jedna wartość byłaby liczona wiele razy. Pętla w górę jest właściwa dla nieograniczonej liczby kopii, jak w problemie Coin Change, ale tutaj jest niewłaściwa.
Czy problem podziału na równe sumy podzbiorów można rozwiązać za pomocą bitsetu?
Tak. Zapisz osiągalne sumy jako bity jednej dużej liczby, zaczynając od ustawionego tylko bitu 0. Dla każdej wartości bits |= bits << num dodaje tę wartość do każdej osiągalnej sumy naraz, a odpowiedzią jest informacja, czy bit target jest ustawiony. To ta sama tabela, ale każde słowo maszynowe obsługuje jednocześnie 64 sumy, więc w praktyce działa znacznie szybciej.
Podobne zadania
Zadania oparte na tych samych pomysłach. Rozwiązanie dwóch lub trzech utrwala schemat.
Python
def canPartition(nums):
# Wpisz kod tutajPrzypadek 1
Przypadek 2
Przypadek 3
Wejście
nums = [6, 1, 4, 9, 2]
Oczekiwane
true