Subsets
Du erhältst eine Liste nums mit verschiedenen ganzen Zahlen. Gib jede Teilmenge davon zurück, einschließlich der leeren Menge und der vollständigen Liste, sodass n Werte 2^n Teilmengen ergeben. Schreibe die Werte jeder Teilmenge in aufsteigender Reihenfolge und liste die Teilmengen in lexikografischer Reihenfolge auf: Vergleiche zwei Teilmengen Wert für Wert; der erste Unterschied entscheidet, und eine Teilmenge, die den Anfang einer anderen bildet, kommt davor. Für [1, 2] lautet die Antwort [[], [1], [1, 2], [2]].
Funktion
- numsinteger-array
- die Werte, alle unterschiedlich, in beliebiger Reihenfolge
- Gibt zurückinteger-2d-array
- jede Teilmenge, jeweils aufsteigend sortiert und in lexikografischer Reihenfolge aufgeführt
Einschränkungen
1 ≤ nums.length ≤ 10-10 ≤ nums[i] ≤ 10- Alle Werte in
numssind unterschiedlich. numskann in beliebiger Reihenfolge vorkommen.
Beispiele
- Eingabe
- nums = [3, 1, 2]
- Ausgabe
- [[], [1], [1, 2], [1, 2, 3], [1, 3], [2], [2, 3], [3]]
- Erklärung
- Sortiert lauten die Werte 1, 2, 3, und drei Werte ergeben 2^3 = 8 Teilmengen.
[1, 2]kommt vor[1, 2, 3], weil es dessen Anfang ist, und[1, 2, 3]kommt vor[1, 3], weil 2 an der zweiten Position kleiner als 3 ist.
- Eingabe
- nums = [0]
- Ausgabe
- [[], [0]]
- Erklärung
- Ein Wert hat zwei Teilmengen: Lass ihn weg und erhalte
[], oder nimm ihn und erhalte[0]. Die leere Teilmenge kommt immer zuerst.
- Eingabe
- nums = [5, -2]
- Ausgabe
- [[], [-2], [-2, 5], [5]]
- Erklärung
- Die Werte werden zu -2 und 5 sortiert, daher wird
[-2, 5]in dieser Reihenfolge geschrieben. Jede Teilmenge, die -2 enthält, kommt vor[5], weil -2 kleiner als 5 ist.
+13 versteckte Tests beim Einreichen
Weiterführende Frage
Kannst du dieselbe Liste ohne Rekursion erstellen, indem du jede Teilmenge direkt aus der vorherigen bildest?
Tipps
Öffne sie nacheinander. Jeder verrät ein bisschen mehr.
Jeder Wert hat in einer Teilmenge zwei Möglichkeiten: drin oder draußen. Wie viele Teilmengen hat eine Liste mit
nWerten, und wie könntest du jede davon aus einer kleineren Teilmenge bilden?Sortiere zuerst die Werte. Wenn du immer nur einen Wert hinzufügst, der rechts vom zuletzt hinzugefügten Wert steht, wird jede Teilmenge in aufsteigender Reihenfolge erstellt und keine Teilmenge wird zweimal erstellt.
Schreibe eine rekursive Hilfsfunktion, die einen Startindex erhält. Sie speichert den aktuellen Pfad als Teilmenge und fügt dann für jeden Index vom Start bis zum Ende den entsprechenden Wert hinzu, ruft sich ab dem nächsten Index erneut auf und entfernt den Wert anschließend wieder. Wenn du den Pfad beim Betreten, also vor der Schleife, speicherst, werden die Teilmengen ohne Sortierung in lexikografischer Reihenfolge ausgegeben.
Lösung
Es gibt 2^n Teilmengen, daher benötigt keine Methode weniger als O(2^n) Arbeit. Die eigentliche Frage ist, wie man jede Teilmenge genau einmal und in der erforderlichen Reihenfolge erzeugt, ohne anschließend 1024 Listen zu sortieren. Backtracking über die sortierten Werte und das Aufzeichnen jedes Knotens des Entscheidungsbaums beim Betreten durchläuft die Teilmengen genau in lexikografischer Reihenfolge.
Bitmasken, dann sortieren
Idee
Ordne die sortierten Werte den Positionen 0 bis n-1 zu. Eine Teilmenge gibt für jede Position an, ob sie enthalten ist oder nicht, und genau das leisten die n Bits einer Zahl. Die Zahlen von 0 bis 2^n-1 stehen also für die Teilmengen: Für [1, 2, 3] ist die Maske 5 binär 101, die Bits 0 und 2 sind gesetzt und stehen für [1, 3]. Maske 0 ist die leere Teilmenge und Maske 7 ist die vollständige Liste.
Verschiedene Masken ergeben verschiedene Teilmengen und jede Teilmenge hat eine Maske, also erzeugt die Schleife alle 2^n Teilmengen genau einmal. Wenn man die Bits von Position 0 aufwärts anhand der sortierten Werte ausliest, wird jede Teilmenge in aufsteigender Reihenfolge geschrieben.
Die Masken kommen nicht in der vom Problem verlangten Reihenfolge heraus. Maske 1 ist [1], Maske 2 ist [2] und Maske 3 ist [1, 2], also würde [2] vor [1, 2] landen. Das behebst du mit einer Sortierung, deren Vergleichsfunktion die Werte der Reihe nach vergleicht und ein Präfix zuerst einordnet. Die Sortierung kostet mehr als die Erzeugung: Für 2^n Teilmengen braucht man ungefähr n × 2^n Vergleiche, und jeder Vergleich liest bis zu n Werte. Für n = 10 sind das etwa 10^5 Lesezugriffe, immer noch schnell, aber ein Aufwand, den der nächste Ansatz gar nicht erst hat.
Algorithmus
- Sortiere
nums, sodass jede Teilmenge in aufsteigender Reihenfolge gelesen wird. - Sammle für jede Maske von 0 bis 2^n-1 die Werte an den Positionen, deren Bit gesetzt ist.
- Sortiere die Liste der Teilmengen: An der ersten Position, an der sich zwei unterscheiden, gewinnt der kleinere Wert; wenn eine zuerst endet, kommt sie zuerst.
- Gib die sortierte Liste zurück.
def subsets(nums):
values = sorted(nums)
n = len(values)
result = []
for mask in range(1 << n):
# Bit i of mask says whether values[i] is in this subset.
result.append([values[i] for i in range(n) if (mask >> i) & 1])
# Python compares lists position by position, and a prefix comes first.
result.sort()
return resultBacktracking: auswählen, erkunden, rückgängig machen
Idee
Stell dir die Teilmengen als Baum vor. Die Wurzel ist die leere Teilmenge. Unterhalb eines Knotens kannst du jeden Wert hinzufügen, der größer ist als der zuletzt hinzugefügte. Für sortierte Werte [1, 2, 3] hat die Wurzel die Kinder [1], [2] und [3]; [1] hat die Kinder [1, 2] und [1, 3]; [1, 2] hat das Kind [1, 2, 3]. Jede Teilmenge kommt in diesem Baum genau einmal vor, denn es gibt nur eine Möglichkeit, sie in aufsteigender Reihenfolge zu schreiben, und jeder Knoten ist eine Lösung, nicht nur die Blätter.
Beim Backtracking wird der Baum mit einer gemeinsamen Liste path durchlaufen. Um zu einem Kind hinabzusteigen, wählst du: Füge den Wert hinzu. Du erkundest den Zweig: Rufe die Funktion rekursiv auf, und der Helfer speichert eine Kopie von path, sobald er dort ankommt. Dann machst du die Wahl rückgängig: Entferne den Wert, sodass path wieder beim Elternknoten ist und das nächste Geschwisterelement ausprobiert werden kann. Da jeder Knoten beim Betreten aufgezeichnet wird, wird ein Elternknoten immer vor seinen Kindern ausgegeben.
Deshalb ist die Ausgabe ohne Sortierung in lexikografischer Reihenfolge. Die Kinder eines Knotens werden vom kleinsten Wert an ausprobiert, und der Durchlauf beendet einen ganzen Zweig, bevor er mit dem nächsten beginnt. Für [1, 2, 3] werden [], [1], [1, 2], [1, 2, 3], [1, 3], [2], [2, 3], [3] aufgezeichnet: die Reihenfolge eines Wörterbuchs, wobei ein Präfix vor seinen Erweiterungen steht.
Der Baum hat 2^n Knoten, und das Kopieren eines Pfads kostet bis zu n, daher beträgt die Laufzeit O(n × 2^n), also die Größe der Antwort selbst. Zusätzlich zur Ausgabe benötigst du einen Pfad und einen Aufrufstapel, die beide höchstens n tief sind.
Algorithmus
- Sortiere die Werte.
- Schreibe
explore(start). Zuerst fügt die Funktion eine Kopie vonpathzum Ergebnis hinzu. - Dann für jeden Index
ivonstartbis zum Ende: Fügevalues[i]zupathhinzu (wähle aus), rufeexplore(i+1)auf (erkunde), und entferne den letzten Wert (mache die Auswahl rückgängig). - Rufe
explore(0)mit einem leeren Pfad auf und gib das Ergebnis zurück.
def subsets(nums):
values = sorted(nums)
result = []
path = []
def explore(start):
# Every node of the decision tree is a subset: record it on the way in.
result.append(path[:])
for i in range(start, len(values)):
path.append(values[i]) # choose
explore(i + 1) # explore: only larger values may follow
path.pop() # un-choose
explore(0)
return result
Stolperfallen und Grenzfälle
Die meisten falschen Antworten entstehen hier durch die Reihenfolge oder dadurch, dass dieselbe Liste mehrfach verwendet wird.
pathselbst statt einer Kopie anzuhängen. Dann verweist jeder Eintrag auf dieselbe Liste, die am Ende des Durchlaufs leer ist. Daher gibst du 2^n Kopien von[]zurück.- Zu vergessen,
numszu sortieren. Bei[3, 1, 2]wird der Baum mit[3, 1]aufgebaut, was nicht aufsteigend ist, und der Durchlauf ist nicht mehr in lexikografischer Reihenfolge. - Nur an den Blättern aufzuzeichnen, wie du es bei Permutationen tun würdest. Jeder Knoten dieses Baums ist eine Teilmenge; wenn du nur Pfade aufzeichnest, die das Ende erreichen, erhältst du zu wenige Teilmengen.
- Mit
start+1statt miti+1rekursiv aufzurufen. Dann kann auf einen größeren Wert ein kleinerer folgen oder sogar derselbe Wert erneut auftreten. So erhältst du Listen wie[3, 2]und[3, 3], die keine aufsteigend geordneten Teilmengen sind. - Den Baum mit Ein- oder Ausschluss zu verwenden (entscheide für Wert 0, dann für Wert 1 und so weiter) und die Blätter aufzuzeichnen. Damit findest du alle 2^n Teilmengen, aber wenn du zuerst den Einschluss ausprobierst, steht die vollständige Liste an erster Stelle; probierst du zuerst den Ausschluss aus, steht
[3]vor[2]. Beides ist nicht lexikografisch. - Ein Komparator, der zuerst nach Länge sortiert, ergibt
[],[1],[2],[3],[1, 2]– eine andere Reihenfolge.
Häufige Fragen4
Wie viele Teilmengen hat eine Menge mit n Elementen?
2^n. Jedes Element ist entweder enthalten oder nicht enthalten, unabhängig von den anderen, daher multiplizieren sich die Möglichkeiten: zwei für das erste Element, zwei für das zweite und so weiter. Drei Werte ergeben 8 Teilmengen und zehn ergeben 1024, einschließlich der leeren Teilmenge und der vollständigen Menge.
Wie hoch ist die Zeitkomplexität des Subsets-Problems?
O(n × 2^n). Es gibt 2^n Teilmengen, und das Ausgeben einer Teilmenge benötigt bis zu n Schritte. Daher kostet bereits die Rückgabe des Ergebnisses so viel. Backtracking erreicht diese Schranke und benötigt nur O(n) zusätzlichen Speicherplatz. Die Generierung mit Bitmasken ist genauso schnell, aber das anschließende Sortieren des Ergebnisses fügt einen weiteren Faktor n hinzu.
Sollte ich für Teilmengen Backtracking oder Bitmasken verwenden?
Bitmasken sind kurz, benötigen keine Rekursion und machen die Entscheidung für oder gegen ein Element als Bits sichtbar. Backtracking liefert die Teilmengen von selbst in lexikografischer Reihenfolge und lässt sich an gängige Varianten anpassen: wiederholte Werte überspringen, nur Teilmengen der Größe k oder nur Teilmengen ausgeben, deren Summe einen Zielwert erreicht; dabei kannst du die Erkundung eines Zweigs vorzeitig abbrechen.
Wie gehst du mit doppelten Werten in Teilmengen um?
Sortiere die Werte und überspringe dann in der Schleife der Backtracking-Hilfsfunktion einen Wert, der auf derselben Ebene dem vorherigen entspricht: i > start und values[i] == values[i-1]. Die erste Kopie erkundet bereits alle Teilmengen, die sie enthalten. Ein Geschwisterzweig, der mit der zweiten Kopie beginnt, würde daher nur dieselben Teilmengen erneut erstellen.
Ähnliche Aufgaben
Aufgaben mit denselben Ideen. Wer zwei oder drei davon löst, behält das Muster.
Python
def subsets(nums):
# Schreibe hier den CodeFall 1
Fall 2
Fall 3
Eingabe
nums = [3, 1, 2]
Erwartet
[[], [1], [1, 2], [1, 2, 3], [1, 3], [2], [2, 3], [3]]