Partition Equal Subset Sum
Du erhältst ein Array nums positiver Ganzzahlen. Entscheide, ob du die Werte in zwei Gruppen aufteilen kannst, deren Summen gleich sind. Jeder Wert kommt genau in eine Gruppe, und eine Gruppe kann Werte von beliebigen Positionen enthalten. Gib true zurück, wenn eine solche Aufteilung möglich ist, andernfalls false.
Funktion
- numsinteger-array
- die positiven Werte in zwei Gruppen aufteilen
- Gibt zurückboolean
- wahr, wenn die Werte zwei Gruppen mit gleichen Summen bilden können, andernfalls falsch
Einschränkungen
1 ≤ nums.length ≤ 2001 ≤ nums[i] ≤ 100
Beispiele
- Eingabe
- nums = [6, 1, 4, 9, 2]
- Ausgabe
- true
- Erklärung
- Die Summe beträgt 22, also braucht jede Gruppe 11. Die Gruppen 9 + 2 und 6 + 1 + 4 ergeben beide 11, also lautet die Antwort
true.
- Eingabe
- nums = [4, 7, 2, 9, 6]
- Ausgabe
- false
- Erklärung
- Die Summe beträgt 28, also braucht jede Gruppe 14. Der Gruppe, die 9 enthält, fehlen noch 5, und keine Kombination aus 4, 7, 2 und 6 ergibt 5. Daher lautet die Antwort
false, obwohl die Summe gerade ist.
- Eingabe
- nums = [1, 2, 3, 5]
- Ausgabe
- false
- Erklärung
- Die Summe beträgt 11. Zwei gleiche ganze Zahlen ergeben immer eine gerade Zahl, daher kann eine ungerade Summe niemals aufgeteilt werden und die Antwort lautet
false.
+18 versteckte Tests beim Einreichen
Weiterführende Frage
Wenn keine gleichmäßige Aufteilung möglich ist, kannst du die kleinstmögliche Differenz zwischen den Summen der beiden Gruppen zurückgeben?
Tipps
Öffne sie nacheinander. Jeder verrät ein bisschen mehr.
Wenn die beiden Gruppen gleiche Summen haben, wie groß muss dann jede Summe in Bezug auf die Gesamtsumme von
numssein? Und was sagt dir eine ungerade Gesamtsumme sofort?Du musst nur eine Gruppe finden, deren Summe die Hälfte der Gesamtsumme ergibt; die übrigen Werte bilden die andere Gruppe. Überlege, welche Summen sich mit den ersten paar Werten erreichen lassen und wie ein weiterer Wert diese Menge verändert.
Behalte ein boolesches Array
reach[0..target], in dem nurreach[0]true ist. Gehe für jeden Wertnummitsvontargetabwärts bisnumund markierereach[s], wennreach[s-num]markiert ist. Durch das Abwärtsgehen wird verhindert, dass jeder Wert zweimal verwendet wird.
Lösung
Jede Gruppe muss genau die Hälfte der Gesamtsumme enthalten, daher lautet die eigentliche Frage, ob eine Teilmenge von nums die Summe target = total / 2 ergibt. Alle Teilmengen auszuprobieren kostet 2^n, was bei 200 Werten aussichtslos ist. Die Summen selbst sind jedoch klein: target beträgt höchstens 200 × 100 / 2 = 10^4. Wenn man für jeden Wert einzeln festhält, welche Summen erreichbar sind, wird aus der Suche eine 0/1-Rucksacktabelle, die in O(n × sum) Schritten gefüllt wird.
Probiere jede Teilmenge mit Rekursion aus
Korrekt, wird aber bei den größten Tests nicht fertig
Idee
Beginne mit der Gesamtsumme. Ist sie ungerade, gibt es keine Aufteilung, denn zwei gleiche ganze Zahlen ergeben zusammen eine gerade Zahl. Andernfalls muss jede Gruppe genau target = total / 2 ergeben. Sobald du Werte findest, die target ergeben, bilden die nicht ausgewählten Werte von selbst die andere Hälfte. Es genügt also, eine Frage zu beantworten: Gibt es eine Teilmenge, deren Summe target ergibt?
Gehe die Werte der Reihe nach durch und triff für jeden eine Entscheidung: Lege ihn in die erste Gruppe oder lasse ihn für die zweite. Eine Hilfsfunktion reach(i, remaining) gibt an, ob die Werte ab Index i zusammen remaining ergeben können. Sie gibt true zurück, wenn remaining 0 erreicht, false, wenn keine Werte mehr übrig sind oder der Wert unter 0 fällt, und versucht andernfalls beide Möglichkeiten für nums[i].
Jede Teilmenge entspricht einem Entscheidungspfad. Daher kann die Suche keine Aufteilung übersehen, und die Antwort ist korrekt. Sie ist langsam, weil es 2^n Pfade gibt und eine Eingabe ohne mögliche Aufteilung sie dazu zwingt, fast alle davon auszuprobieren. Nimm 199 Kopien von 100 und eine 98: Die Gesamtsumme ist 19998, das Ziel 9999 wird nie erreicht, und die Suche probiert jede Möglichkeit aus, höchstens 99 der Hunderter auszuwählen – etwa 4 × 10^59 Pfade. Schon 40 Werte ergeben 2^40, also etwa 10^12 Pfade.
Algorithmus
- Addiere die Werte in
nums. Wenn die Summe ungerade ist, gibfalsezurück. - Setze
targetauf die Hälfte der Summe. - Schreibe
reach(i, remaining): Gib true zurück, wennremaining0 ist, und false, wennihinter dem letzten Wert liegt oderremainingkleiner als 0 ist. - Gib andernfalls
reach(i+1, remaining-nums[i])oderreach(i+1, remaining)zurück: Nimm den Wert oder lass ihn weg. - Gib
reach(0, target)zurück.
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)Fülle eine Tabelle mit Werten und ihrer Summe
Idee
Die Rekursion stellt immer wieder dieselbe Frage. reach(i, remaining) hängt nur von zwei Zahlen ab: i von 0 bis n und remaining von 0 bis target. Dadurch gibt es höchstens (n+1) × (target+1) verschiedene Fragen, an den Grenzen etwa 201 × 10001 ≈ 2 × 10^6 – wenige genug, um jede einzelne davon einmal zu beantworten.
Erstelle die Antworten in einer Tabelle von vorne nach hinten. can[i][s] gibt an, ob sich mit einigen der ersten i Werte die Summe s bilden lässt. Ohne Werte ist nur die Summe 0 möglich, daher ist Zeile 0 überall false außer bei can[0][0]. Für den Wert num = nums[i-1] gibt es zwei Möglichkeiten, s zu erreichen: num nicht verwenden, sodass die vorherigen Werte bereits s ergeben, oder ihn verwenden, sodass die vorherigen Werte s-num ergeben. Das ist die ganze Regel: can[i][s] = can[i-1][s] or can[i-1][s-num], wobei der zweite Teil nur zählt, wenn s ≥ num. Jede Zeile liest nur die darüberliegende Zeile, daher wird jeder Wert höchstens einmal verwendet.
Bei [6, 1, 4, 9, 2] mit dem Zielwert 11 wachsen die erreichbaren Summen von {0} auf {0, 6}, dann auf {0, 1, 6, 7} und anschließend auf {0, 1, 4, 5, 6, 7, 10, 11}. Die Summe 11 erscheint nach der 4 (6 + 1 + 4), und die folgenden Zeilen behalten sie bei. Die Antwort ist can[n][target]. Jede Zelle erfordert konstanten Aufwand, daher betragen Zeit- und Speicherbedarf jeweils O(n × target).
Algorithmus
- Gib
falsezurück, wenn die Gesamtsumme ungerade ist, und setzetargetauf die Hälfte davon. - Erstelle eine Tabelle mit n+1 Zeilen und target+1 Spalten, die alle auf false gesetzt sind, und setze
can[0][0]auf true. - Gehe für jede Zeile
ivon 1 bis n vonnum = nums[i-1]aus. - Setze für jede Summe
svon 0 bistargetcan[i][s]aufcan[i-1][s]oder, fallss ≥ num, aufcan[i-1][s-num]. - Gib
can[n][target]zurück.
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]Eine Summenzeile, die von oben nach unten ausgefüllt wird
Idee
Jede Zeile der Tabelle liest nur die Zeile darüber. Daher reicht eine Zeile aus, wenn du sie direkt aktualisierst: reach[s] gibt an, ob sich einige der bisher gesehenen Werte zu s summieren. Die Gefahr liegt in der Reihenfolge der Aktualisierungen. Wenn du s aufsteigend durchläufst, wurde reach[s-num] möglicherweise bereits durch dieselbe Zahl num aktiviert. Bei [3, 9] und dem Zielwert 6 setzt die 3 reach[3] auf wahr und liest diesen Wert anschließend, um reach[6] zu aktivieren, als hättest du zwei Dreien, und du gibst für eine nicht existierende Aufteilung „wahr“ zurück.
Durchlaufe s absteigend, von target bis num. Dann ist s-num ein kleinerer Index, den dieser Wert noch nicht verändert hat, sodass reach[s-num] weiterhin das Ergebnis von vor dem Hinzukommen von num enthält. Das entspricht genau can[i-1][s-num] aus der Tabelle, und die einzelne Zeile übernimmt die Arbeit der gesamten Tabelle.
Du kannst auch sofort aufhören, sobald reach[target] wahr wird, denn spätere Werte fügen nur erreichbare Summen hinzu und entfernen niemals eine. Im schlimmsten Fall sind es weiterhin O(n × target) Schritte, etwa 2 × 10^6, und der Speicherbedarf sinkt auf target + 1 boolesche Werte.
Algorithmus
- Gib für eine ungerade Gesamtsumme
falsezurück und setzetargetauf die Hälfte davon. - Erstelle
reachmittarget + 1Einträgen, die allefalsesind, außerreach[0]. - Gehe für jeden Wert
numsvontargetabwärts bisnumdurch und setzereach[s]auf true, wennreach[s-num]true ist. - Gib nach jedem Wert
truezurück, wennreach[target]true ist. - Wenn die Schleife endet, gib
reach[target]zurück, dasfalseist.
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]
Stolperfallen und Grenzfälle
Falsche Antworten entstehen hier dadurch, dass man einer gierigen Regel vertraut, die Prüfung auf eine ungerade Summe überspringt und in der Tabelle mit einer Zeile einen Wert wiederverwendet.
- Wenn man in der Version mit einer Zeile die Summen aufwärts durchgeht, wird ein Wert mehrmals verwendet. Bei
[3, 9]ist das Ziel 6; die 3 markiert zuerst die Summe 3 und dann die Summe 6, und du antwortest mit „wahr“. - Die Prüfung auf eine ungerade Summe zu überspringen: Bei
[1, 2]wird die Gesamtsumme 3 auf ein Ziel von 1 abgerundet, der Wert 1 erreicht es, und du antwortest mit „wahr“ für eine Aufteilung, die nicht existieren kann. - Gieriges Auffüllen, etwa indem man sortiert und immer zur leichteren Gruppe addiert, scheitert bei
[3, 3, 2, 2, 2]: Es endet bei 7 gegenüber 5, obwohl 3 + 3 = 2 + 2 + 2 gilt. - Ein Wert, der größer als das Ziel ist, wie bei
[2, 2, 2, 10]. Eine absteigende Schleife vontargetbisnumwird dann nullmal ausgeführt, was richtig ist; ein Bereich wie(num+1):(target+1)in R zählt jedoch rückwärts und bringt die Tabelle durcheinander. Überspringe solche Werte. - Eine gerade Gesamtsumme reicht nicht aus:
[4, 7, 2, 9, 6]ergibt die Summe 28 und lässt sich trotzdem nicht aufteilen. - In Lua und R beginnen Arrays bei 1, daher steht der Eintrag für die Summe
sam Indexs + 1.
Häufige Fragen4
Warum ist Partition Equal Subset Sum ein 0/1-Rucksackproblem?
Du hast einen Rucksack mit der Größe target = total / 2 und musst ihn genau füllen, wobei du jeden Wert höchstens einmal verwendest. Einen Wert zu nehmen oder ihn wegzulassen, ist die 0/1-Entscheidung, und die Größe eines Werts entspricht dem Wert selbst. Die Rucksacktabelle der erreichbaren Summen löst das Problem in O(n × target) Zeit.
Wie hoch ist die Zeitkomplexität von „Partition Equal Subset Sum“?
Der Tabellenansatz benötigt O(n × target) Zeit, wobei target die Hälfte der Gesamtsumme ist, und mit einer Zeile O(target) Speicher. Bei 200 Werten von höchstens 100 sind das etwa 2 × 10^6 Schritte. Die Schranke wächst mit der Größe der Werte, nicht nur mit ihrer Anzahl, daher wird sie pseudo-polynomiell genannt: Bei Werten nahe 10^9 wäre keine Tabelle groß genug, und das allgemeine Problem ist NP-vollständig.
Warum läuft die innere Schleife vom Zielwert abwärts bis zum Wert?
Beim Herunterzählen wird reach[s-num] gelesen, bevor dieser Wert geändert werden kann, sodass er weiterhin die Werte vor num beschreibt. Beim Hochzählen könnte eine mit num gebildete Summe erneut um num erweitert werden, wodurch ein Wert mehrfach gezählt wird. Die aufsteigende Schleife ist für unbegrenzt viele Wiederholungen richtig, wie beim Coin Change, hier jedoch falsch.
Kann Partition Equal Subset Sum mit einer Bitmenge gelöst werden?
Ja. Speichere die erreichbaren Summen als Bits einer großen Zahl, wobei zunächst nur Bit 0 gesetzt ist. Für jeden Wert addiert bits |= bits << num diesen Wert gleichzeitig zu jeder erreichbaren Summe hinzu. Die Antwort ist, ob Bit target gesetzt ist. Es ist dieselbe Tabelle, aber jedes Maschinenwort verarbeitet 64 Summen auf einmal, sodass es in der Praxis viel schneller läuft.
Ähnliche Aufgaben
Aufgaben mit denselben Ideen. Wer zwei oder drei davon löst, behält das Muster.
Python
def canPartition(nums):
# Schreibe hier den CodeFall 1
Fall 2
Fall 3
Eingabe
nums = [6, 1, 4, 9, 2]
Erwartet
true