Podział na równe sumy podzbiorów
Przekształcać problem podziału w problem plecakowy 0/1 z celem równym połowie sumy wszystkich elementów i sprawdzać możliwość rozwiązania za pomocą boolowskiej tablicy DP
Podział na równe sumy podzbiorów to bezpłatna lekcja Coding Interview Prep na CoddyKit. To lekcja 3 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej Coding Interview Prep, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs Coding Interview Prep zawiera 4 lekcji w sumie.
Treść problemu
Mając niepustą tablicę dodatnich liczb całkowitych nums, należy ustalić, czy można podzielić ją na dwa podzbiory o równych sumach. Na przykład [1, 5, 11, 5] można podzielić na [1, 5, 5] i [11], przy czym suma obu podzbiorów wynosi 11. Jeśli suma całkowita jest nieparzysta, odpowiedź od razu wynosi False. W przeciwnym razie musimy znaleźć podzbiór o sumie równej total_sum // 2 — jest to klasyczny problem sumy podzbioru.
Redukcja do problemu Subset Sum
Kluczowa redukcja: jeśli łączna suma S jest parzysta, a suma pewnego podzbioru wynosi S//2, to pozostałe elementy również automatycznie sumują się do S//2. Zatem problem Partition Equal Subset Sum sprowadza się do pytania: czy istnieje podzbiór nums o sumie S//2? Jest to klasyczny NP-zupełny problem Subset Sum, który rozwiązujemy za pomocą programowania dynamicznego dla problemu plecakowego 0/1 w czasie O(n × S).
def canPartition(nums):
total = sum(nums)
if total % 2 != 0:
return False # odd sum: impossible
target = total // 2
# Now: does any subset of nums sum to target?Tablica wartości logicznych DP
Należy zdefiniować tablicę wartości logicznych dp[c], gdzie dp[c] = True oznacza, że istnieje podzbiór o sumie dokładnie c. Należy zainicjalizować dp[0] = True (suma pustego podzbioru wynosi 0), a wszystkie pozostałe wartości ustawić na False. Dla każdej liczby num należy iterować po pojemności od target w dół do num (iteracja wsteczna jak w problemie plecakowym 0/1) i ustawić dp[c] = dp[c] or dp[c - num].
def canPartition(nums):
total = sum(nums)
if total % 2 != 0:
return False
target = total // 2
dp = [False] * (target + 1)
dp[0] = True
for num in nums:
for c in range(target, num - 1, -1): # backward: 0/1 knapsack
dp[c] = dp[c] or dp[c - num]
return dp[target]
print(canPartition([1, 5, 11, 5])) # True
print(canPartition([1, 2, 3, 5])) # FalsePrześledzenie przykładu
Dla [1, 5, 11, 5], total=22, target=11. Początkowo dp[0]=True. Po num=1: dp[1]=True. Po num=5: dp[5]=True, dp[6]=True. Po num=11: dp[11]=True (z wykorzystaniem samej liczby 11). Wartość dp[11]=True została już znaleziona, ale nadal przetwarzamy wszystkie liczby. Wynik końcowy: dp[11]=True, więc podział jest możliwy.
Optymalizacja przez wcześniejsze zakończenie
Można dodać wcześniejsze zakończenie: jeśli w dowolnym momencie dp[target] stanie się równe True, należy natychmiast zwrócić True. Może to znacznie przyspieszyć działanie w najlepszym przypadku. Jeśli pojedynczy element jest równy target, również można od razu zwrócić True. Jeśli pojedynczy element przekracza target, nie może należeć do żadnego podzbioru o sumie równej target, ale nadal trzeba sprawdzić pozostałe elementy.
def canPartition_fast(nums):
total = sum(nums)
if total % 2 != 0:
return False
target = total // 2
if max(nums) > target: # any element > target makes it impossible
return False
dp = [False] * (target + 1)
dp[0] = True
for num in nums:
for c in range(target, num - 1, -1):
dp[c] = dp[c] or dp[c - num]
if dp[target]:
return True # early exit
return dp[target]
print(canPartition_fast([1, 5, 11, 5])) # TrueUżycie zbioru w języku Python zamiast tablicy DP
Alternatywnym rozwiązaniem jest utrzymywanie zbioru osiągalnych sum. Należy rozpocząć od {0}. Dla każdej liczby należy dodać ją do każdej sumy znajdującej się w bieżącym zbiorze: reachable = reachable | {s + num for s in reachable}. Następnie należy odfiltrować sumy, które przekraczają target. Na końcu sprawdzamy, czy target znajduje się w zbiorze. To podejście jest intuicyjne, ale może wymagać więcej pamięci i w praktyce działać wolniej.
def canPartition_set(nums):
total = sum(nums)
if total % 2 != 0:
return False
target = total // 2
reachable = {0}
for num in nums:
reachable = {s + num for s in reachable if s + num <= target} | reachable
return target in reachable
print(canPartition_set([1, 5, 11, 5])) # TrueAnaliza złożoności
Podejście DP działa w czasie O(n × S), gdzie S = sum(nums), i wykorzystuje O(S) pamięci na tablicę wartości logicznych. Dla ograniczeń z LeetCode (n ≤ 200, sum ≤ 20,000) oznacza to najwyżej 4 000 000 operacji — bardzo szybko. Podejście ze zbiorem ma taką samą złożoność asymptotyczną, ale w praktyce może działać wolniej ze względu na koszt tworzenia zbiorów.
Uogólnienie: zliczanie podzbiorów o danej sumie
Pokrewny problem polega na zliczeniu podzbiorów, których suma jest równa target. Należy zmienić typ DP z wartości logicznych na liczby całkowite: dp[c] = number of ways to reach sum c. Zamiast operatora OR należy użyć dodawania: dp[c] += dp[c - num]. Należy zainicjalizować dp[0] = 1. Iteracja wsteczna pozostaje taka sama. To uogólnienie pokazuje, jak szablon problemu plecakowego można dostosować do różnych pytań dotyczących podzbiorów.
def count_subsets(nums, target):
dp = [0] * (target + 1)
dp[0] = 1
for num in nums:
for c in range(target, num - 1, -1):
dp[c] += dp[c - num]
return dp[target]
print(count_subsets([1, 1, 1, 1, 1], 3)) # 10 (C(5,3))Typowe pytania uzupełniające podczas rozmowy
Należy spodziewać się pytań uzupełniających: (1) Co zrobić, jeśli trzeba zwrócić rzeczywisty podział? — wymaga to użycia DP 2D do odtworzenia rozwiązania. (2) Co zrobić, jeśli elementy mogą być ujemne? — należy przesunąć target albo użyć słownika zamiast tablicy. (3) Jaka jest złożoność czasowa? — O(n × sum). (4) Czy można poprawić rozwiązanie, jeśli wiele liczb jest takich samych? — tak, można użyć zliczania częstotliwości, aby zmniejszyć liczbę iteracji zewnętrznych. Należy samodzielnie wspomnieć o tych kompromisach.
Powiązanie z problemem plecakowym 0/1
Partition Equal Subset Sum jest bezpośrednim zastosowaniem problemu plecakowego 0/1: elementami są liczby, ich wagi są równe wartościom, a pojemność plecaka jest równa target. Pytamy, czy maksymalna wartość jest równa target (sprawdzamy wykonalność), a nie jaka jest sama maksymalna wartość. Iteracja wsteczna pozostaje taka sama — zmienia się jedynie operacja z max na logiczne or. Rozpoznanie tego powiązania podczas rozmowy rekrutacyjnej świadczy o dobrej umiejętności rozpoznawania wzorców.
Przypadki brzegowe
Należy obsłużyć następujące przypadki brzegowe: (1) tablica długości 1 — pojedynczego elementu nie można podzielić, więc wynik to zawsze False; (2) wszystkie elementy są identyczne, a ich liczba jest parzysta — rozwiązanie może istnieć lub nie, zależnie od wartości pojedynczych elementów; (3) bardzo duże sumy — przed zaalokowaniem tablicy DP należy sprawdzić ograniczenia; (4) elementy większe od target — można je pominąć, ponieważ nigdy nie mogą należeć do podzbioru o sumie równej target. Sprawdzenie największego elementu jako warunek wcześniejszego zakończenia skutecznie obsługuje przypadek (4).
Szybkie sprawdzenie
Sprawdź swoje rozumienie zagadnień z zakresu Data Structures & Algorithms — Coding Interview Prep przedstawionych w tej lekcji.
Podsumowanie lekcji
W tej lekcji poznali Państwo następujące zagadnienia: Partition Equal Subset Sum sprowadza się do problemu sumy podzbioru z target = total//2, jednowymiarowe DP z wartościami logicznymi dp[c] wykorzystuje iterację wsteczną identyczną jak w problemie plecakowym 0/1 oraz podejście można uogólnić na zliczanie podzbiorów, zastępując logiczne OR dodawaniem liczb całkowitych. Następnie zajmiemy się problemem Target Sum, przekształcając przypisywanie znaków w problem plecakowy oparty na różnicy sum podzbiorów.
Często zadawane pytania
Czy lekcja „Podział na równe sumy podzbiorów” jest bezpłatna?
Tak — pełny tekst „Podział na równe sumy podzbiorów” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu Coding Interview Prep, przejdź na CoddyKit PRO. Kurs Coding Interview Prep zawiera 4 lekcji w sumie.
Co nauczysz się w „Podział na równe sumy podzbiorów”?
Przekształcać problem podziału w problem plecakowy 0/1 z celem równym połowie sumy wszystkich elementów i sprawdzać możliwość rozwiązania za pomocą boolowskiej tablicy DP Ćwiczysz Coding Interview Prep z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.
Czy potrzebuję doświadczenia, aby zacząć Coding Interview Prep?
Nie wymagamy żadnego doświadczenia. Coding Interview Prep w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 3 z 4.
Ile czasu zajmuje lekcja „Podział na równe sumy podzbiorów”?
Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.
Czy mogę pisać i uruchamiać kod w tej lekcji Coding Interview Prep?
Tak. Każda lekcja Coding Interview Prep zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.
Wszystkie lekcje w tym kursie
- Plecak 0/1 i optymalizacja pamięci
- Plecak bez ograniczeń i Coin Change II
- Podział na równe sumy podzbiorów
- Suma docelowa ze znakami dodatnimi i ujemnymi