Verkrijgen van een Power Set van een Set in Java

1. Inleiding

In deze tutorial bestuderen we het proces van het genereren van een vermogensset van een bepaalde set in Java.

Ter herinnering, voor elke maat n, er is een krachtige set van grootte 2n. We zullen leren hoe we het kunnen krijgen met behulp van verschillende technieken.

2. Definitie van een Power Set

De vermogensset van een bepaalde set S is de verzameling van alle subsets van S, inclusief S zichzelf en de lege set.

Bijvoorbeeld voor een bepaalde set:

{"APPLE", "ORANGE", "MANGO"}

de krachtset is:

{{}, {"APPLE"}, {"ORANGE"}, {"APPLE", "ORANGE"}, {"MANGO"}, {"APPLE", "MANGO"}, {"ORANGE", "MANGO" }, {"APPLE", "ORANJE", "MANGO"}}

Omdat het ook een set van subsets is, is de volgorde van de interne subsets niet belangrijk en kunnen ze in elke volgorde verschijnen:

{{}, {"MANGO"}, {"ORANGE"}, {"ORANGE", "MANGO"}, {"APPLE"}, {"APPLE", "MANGO"}, {"APPLE", "ORANGE" }, {"APPLE", "ORANGE", "MANGO"}}

3. Guava-bibliotheek

De Google Guava-bibliotheek heeft een aantal handige Set hulpprogramma's, zoals de power set. We kunnen het dus gemakkelijk gebruiken om ook de vermogensset van de gegeven set te krijgen:

@Test openbare ongeldig gegevenSet_WhenGuavaLibraryGeneratePowerSet_ThenItContainsAllSubsets () {ImmutableSet set = ImmutableSet.of ("APPLE", "ORANGE", "MANGO"); Set powerSet = Sets.powerSet (set); Assertions.assertEquals ((1 << set.size ()), powerSet.size ()); MatcherAssert.assertThat (powerSet, Matchers.containsInAnyOrder (ImmutableSet.of (), ImmutableSet.of ("APPLE"), ImmutableSet.of ("ORANGE"), ImmutableSet.of ("APPLE", "ORANGE"), ImmutableSet.of ("MANGO"), ImmutableSet.of ("APPLE", "MANGO"), ImmutableSet.of ("ORANGE", "MANGO"), ImmutableSet.of ("APPLE", "ORANGE", "MANGO"))) ; }

Guave powerSet werkt intern over de Iterator interface in de manier waarop de volgende subset wordt aangevraagd, wordt de subset berekend en geretourneerd. De complexiteit van de ruimte wordt dus gereduceerd tot Aan) in plaats van O (2n).

Maar hoe bereikt Guava dit?

4. Aanpak voor het genereren van stroomtoestellen

4.1. Algoritme

Laten we nu de mogelijke stappen bespreken voor het maken van een algoritme voor deze bewerking.

De power set van een lege set is {{}} waarin het slechts één lege set bevat, dus dat is ons eenvoudigste geval.

Voor elke set S anders dan de lege set, extraheren we eerst een element en noemen het - element. Dan, voor de rest van de elementen van een set subsetWithoutElement, berekenen we hun power set recursief - en noemen het zoiets als powerSetSubsetWithoutElement. Vervolgens door het geëxtraheerde element voor alle sets in powerSetSubsetWithoutElement, we krijgen powerSetSubsetWithElement.

Nu is het vermogen ingesteld S is de vereniging van een powerSetSubsetWithoutElement en een powerSetSubsetWithElement:

Laten we een voorbeeld bekijken van de recursieve power set-stack voor de gegeven set {"APPLE", "ORANJE", "MANGO"}.

Om de leesbaarheid van de afbeelding te verbeteren, gebruiken we korte namen: P. betekent power set functie en "A", "O", "M" zijn korte vormen van "APPLE", "ORANJE", en "MANGO", respectievelijk:

4.2. Implementatie

Laten we dus eerst de Java-code schrijven voor het extraheren van één element en de resterende subsets ophalen:

T-element = set.iterator (). Next (); Set subsetWithoutElement = nieuwe HashSet (); for (T s: set) {if (! s.equals (element)) {subsetWithoutElement.add (s); }}

We willen dan de powerset van subsetWithoutElement:

Set powersetSubSetWithoutElement = recursivePowerSet (subsetWithoutElement);

Vervolgens moeten we die powerset weer toevoegen aan het origineel:

Set powersetSubSetWithElement = nieuwe HashSet (); for (Set subsetWithoutElement: powerSetSubSetWithoutElement) {Set subsetWithElement = nieuwe HashSet (subsetWithoutElement); subsetWithElement.add (element); powerSetSubSetWithElement.add (subsetWithElement); }

Eindelijk de vereniging van powerSetSubSetWithoutElement en powerSetSubSetWithElement is de power set van de gegeven input set:

Set powerSet = nieuwe HashSet (); powerSet.addAll (powerSetSubSetWithoutElement); powerSet.addAll (powerSetSubSetWithElement);

Als we al onze codefragmenten samenvoegen, kunnen we ons eindproduct zien:

openbare set recursivePowerSet (Set set) {if (set.isEmpty ()) {Set ret = nieuwe HashSet (); ret.add (set); retourneren; } T-element = set.iterator (). Next (); Set subSetWithoutElement = getSubSetWithoutElement (set, element); Set powerSetSubSetWithoutElement = recursivePowerSet (subSetWithoutElement); Set powerSetSubSetWithElement = addElementToAll (powerSetSubSetWithoutElement, element); Set powerSet = nieuwe HashSet (); powerSet.addAll (powerSetSubSetWithoutElement); powerSet.addAll (powerSetSubSetWithElement); terugkeer powerSet; } 

4.3. Opmerkingen voor unit-tests

Laten we nu testen. We hebben hier een aantal criteria om te bevestigen:

  • Eerst controleren we de grootte van de vermogensset en dat moet zo zijn 2n voor een set maat n.
  • Dan komt elk element slechts één keer voor in een subset en 2n-1 verschillende subsets.
  • Ten slotte moet elke subset één keer voorkomen.

Als aan al deze voorwaarden is voldaan, kunnen we er zeker van zijn dat onze functie werkt. Nu, sinds we hebben gebruikt Set, we weten al dat er geen herhaling is. In dat geval hoeven we alleen de grootte van de vermogensset te controleren en het aantal keren dat elk element in de subsets voorkomt.

Om de grootte van de krachtset te controleren, kunnen we gebruiken:

MatcherAssert.assertThat (powerSet, IsCollectionWithSize.hasSize ((1 << set.size ())));

En om het aantal keren dat elk element voorkomt te controleren:

Kaartteller = nieuwe HashMap (); for (Set subset: powerSet) {for (String name: subset) {int num = counter.getOrDefault (name, 0); counter.put (naam, num + 1); }} counter.forEach ((k, v) -> Assertions.assertEquals ((1 << (set.size () - 1)), v.intValue ()));

Eindelijk, als we alles samen in één eenheidstest kunnen samenbrengen:

@Test openbare ongeldige gegevenSet_WhenPowerSetIsCalculated_ThenItContainsAllSubsets () {Set set = RandomSetOfStringGenerator.generateRandomSet (); Set powerSet = nieuwe PowerSet (). recursivePowerSet (set); MatcherAssert.assertThat (powerSet, IsCollectionWithSize.hasSize ((1 << set.size ()))); Kaartteller = nieuwe HashMap (); for (Set subset: powerSet) {for (String name: subset) {int num = counter.getOrDefault (name, 0); counter.put (naam, num + 1); }} counter.forEach ((k, v) -> Assertions.assertEquals ((1 << (set.size () - 1)), v.intValue ())); }

5. Optimalisatie

In deze sectie proberen we de ruimte te minimaliseren en het aantal interne bewerkingen te verminderen om het ingestelde vermogen op een optimale manier te berekenen.

5.1. Data structuur

Zoals we in de gegeven benadering kunnen zien, hebben we veel aftrekkingen nodig in de recursieve aanroep, die veel tijd en geheugen in beslag neemt.

In plaats daarvan kunnen we elke set of subset toewijzen aan een aantal andere begrippen om het aantal bewerkingen te verminderen.

Eerst moeten we een oplopend nummer toewijzen, beginnend met 0, aan elk object in de gegeven set S wat betekent dat we werken met een geordende lijst met nummers.

Bijvoorbeeld voor de gegeven set {"APPLE", "ORANJE", "MANGO"} we krijgen:

"APPLE" -> 0

"ORANJE" ->

"MANGO" -> 2

Dus vanaf nu, in plaats van subsets van S, genereren we ze voor de geordende lijst van [0, 1, 2], en zoals deze is geordend, kunnen we aftrekkingen simuleren door een startindex.

Als de startindex bijvoorbeeld 1 is, betekent dit dat we de vermogensset [1,2] genereren.

Om de toegewezen id van het object op te halen en vice versa, slaan we beide zijden van de toewijzing op. In ons voorbeeld slaan we beide op ("MANGO" -> 2) en (2 -> "MANGO"). Omdat het in kaart brengen van getallen vanaf nul begon, kunnen we voor de omgekeerde kaart daar een eenvoudige array gebruiken om het respectieve object op te halen.

Een van de mogelijke implementaties van deze functie zou zijn:

private Map map = nieuwe HashMap (); private List reverseMap = nieuwe ArrayList (); private void initializeMap (collectie collectie) {int mapId = 0; voor (T c: collectie) {map.put (c, mapId ++); reverseMap.add (c); }}

Om deelverzamelingen weer te geven, zijn er twee bekende ideeën:

  1. Index weergave
  2. Binaire weergave

5.2. Indexweergave

Elke subset wordt vertegenwoordigd door de index van zijn waarden. Bijvoorbeeld de indextoewijzing van de gegeven set {"APPLE", "ORANJE", "MANGO"} zou zijn:

{{} -> {} [0] -> {"APPLE"} [1] -> {"ORANGE"} [0,1] -> {"APPLE", "ORANGE"} [2] -> {" MANGO "} [0,2] -> {" APPLE "," MANGO "} [1,2] -> {" ORANGE "," MANGO "} [0,1,2] -> {" APPLE "," ORANJE "," MANGO "}}

We kunnen dus de respectieve set ophalen uit een subset van indices met de gegeven mapping:

privé Set unMapIndex (Set sets) {Set ret = nieuwe HashSet (); for (Set s: sets) {HashSet subset = new HashSet (); voor (Geheel getal i: s) {subset.add (reverseMap.get (i)); } ret.add (subset); } retourneren; }

5.3. Binaire weergave

Of we kunnen elke subset binair weergeven. Als een element van de feitelijke set in deze subset bestaat, is zijn respectieve waarde 1; anders is het 0.

Voor ons fruitvoorbeeld zou de krachtset zijn:

{[0,0,0] -> {} [1,0,0] -> {"APPLE"} [0,1,0] -> {"ORANJE"} [1,1,0] -> { "APPLE", "ORANGE"} [0,0,1] -> {"MANGO"} [1,0,1] -> {"APPLE", "MANGO"} [0,1,1] -> { "ORANGE", "MANGO"} [1,1,1] -> {"APPLE", "ORANGE", "MANGO"}}

We kunnen dus de respectieve set ophalen uit een binaire subset met de gegeven mapping:

privé Set unMapBinary (verzameling sets) {Set ret = nieuwe HashSet (); for (List s: sets) {HashSet subset = new HashSet (); voor (int i = 0; i <s.size (); i ++) {if (s.get (i)) {subset.add (reverseMap.get (i)); }} ret.add (subset); } retourneren; }

5.4. Implementatie van recursieve algoritmen

In deze stap proberen we de vorige code te implementeren met behulp van beide datastructuren.

Voordat we een van deze functies aanroepen, moeten we de initializeMap methode om de geordende lijst te krijgen. Nadat we onze gegevensstructuur hebben gemaakt, moeten we ook de respectieve unMap functie om de daadwerkelijke objecten op te halen:

openbare set recursivePowerSetIndexRepresentation (verzameling set) {initializeMap (set); Set powerSetIndices = recursivePowerSetIndexRepresentation (0, set.size ()); retourneer unMapIndex (powerSetIndices); }

Laten we dus eens kijken naar de indexweergave:

privé Set recursivePowerSetIndexRepresentation (int idx, int n) {if (idx == n) {Set empty = nieuwe HashSet (); empty.add (nieuwe HashSet ()); terugkeer leeg; } Instellen powerSetSubset = recursivePowerSetIndexRepresentation (idx + 1, n); Set powerSet = nieuwe HashSet (powerSetSubset); for (Set s: powerSetSubset) {HashSet subSetIdxInclusive = nieuwe HashSet (s); subSetIdxInclusive.add (idx); powerSet.add (subSetIdxInclusive); } terugkeer powerSet; }

Laten we nu eens kijken naar de binaire benadering:

privé Set recursivePowerSetBinaryRepresentation (int idx, int n) {if (idx == n) {Set powerSetOfEmptySet = nieuwe HashSet (); powerSetOfEmptySet.add (Arrays.asList (nieuwe Boolean [n])); retourneer powerSetOfEmptySet; } Instellen powerSetSubset = recursivePowerSetBinaryRepresentation (idx + 1, n); Set powerSet = nieuwe HashSet (); for (List s: powerSetSubset) {List subSetIdxExclusive = nieuwe ArrayList (s); subSetIdxExclusive.set (idx, false); powerSet.add (subSetIdxExclusive); List subSetIdxInclusive = nieuwe ArrayList (s); subSetIdxInclusive.set (idx, true); powerSet.add (subSetIdxInclusive); } terugkeer powerSet; }

5.5. Herhaal door [0, 2n)

Nu is er een mooie optimalisatie die we kunnen doen met de binaire weergave. Als we ernaar kijken, kunnen we zien dat elke rij equivalent is aan het binaire formaat van een getal in [0, 2n).

Dus als we getallen van 0 naar 2nkunnen we die index naar binair converteren en deze gebruiken om een ​​booleaanse weergave van elke subset te maken:

privélijst iterativePowerSetByLoopOverNumbers (int n) {List powerSet = nieuwe ArrayList (); for (int i = 0; i <(1 << n); i ++) {List subset = new ArrayList (n); for (int j = 0; j <n; j ++) subset.add (((1 <0); powerSet.add (subset);} return powerSet;}

5.6. Minimale wijziging van subsets door grijze code

Als we nu een bijectieve functie definiëren uit de binaire weergave van lengte n naar een nummer in [0, 2n)kunnen we subsets genereren in elke gewenste volgorde.

Gray Code is een bekende functie die wordt gebruikt om binaire representaties van getallen te genereren, zodat de binaire representatie van opeenvolgende getallen slechts één bit verschilt (zelfs het verschil tussen het laatste en het eerste getal is één).

We kunnen dit dus nog wat verder optimaliseren:

privélijst iterativePowerSetByLoopOverNumbersWithGrayCodeOrder (int n) {List powerSet = nieuwe ArrayList (); for (int i = 0; i <(1 << n); i ++) {List subset = new ArrayList (n); voor (int j = 0; j > 1); subset.add (((1 <0);} powerSet.add (subset);} return powerSet;}

6. Lui laden

Om het ruimtegebruik van de stroomset te minimaliseren, namelijk O (2n), kunnen we de Iterator interface om elke subset op te halen, en ook elk element in elke subset lui.

6.1. ListIterator

Ten eerste om te kunnen herhalen van 0 naar 2n, we zouden een special moeten hebben Iterator dat loopt over dit bereik, maar verbruikt niet het hele bereik van tevoren.

Om dit probleem op te lossen, gebruiken we twee variabelen; een voor de maat, namelijk 2n, en een andere voor de huidige subsetindex. Onze hasNext () functie zal dat controleren positie is minder dan grootte:

abstracte klasse ListIterator implementeert Iterator {protected int position = 0; privé int grootte; openbare ListIterator (int size) {this.size = size; } @Override public boolean hasNext () {return position <size; }}

En onze De volgende() functie retourneert de subset voor de huidige positie en verhoogt de waarde van positie bij een:

@Override public Set next () {return new Subset (map, reverseMap, position ++); }

6.2. Subgroep

Om een ​​luie lading te hebben Subgroep, definiëren we een klasse die zich uitbreidt AbstractSet, en we overschrijven enkele van zijn functies.

Door alle bits die zijn 1 in de ontvangst masker (of positie) van de Subgroep, kunnen we het Iterator en andere methoden in AbstractSet.

Bijvoorbeeld de grootte() is het aantal 1s in de ontvangende masker:

@Override public int size () {return Integer.bitCount (masker); }

En de bevat () functie is alleen of de respectieve bit in de masker is 1 of niet:

@Override openbare boolean bevat (@Nullable Object o) {Integer index = map.get (o); return index! = null && (mask & (1 << index))! = 0; }

We gebruiken een andere variabele - resterende SetBits - om het te wijzigen wanneer we het respectieve element ervan ophalen in de subset waarnaar we dat bit wijzigen 0. Dan de hasNext () controleert of resterende SetBits is niet nul (dat wil zeggen, het heeft ten minste één bit met de waarde 1):

@Override openbare boolean hasNext () {return resterendeSetBits! = 0; }

En de De volgende() functie gebruikt de meest rechtse 1 in de resterende SetBits, en converteert het vervolgens naar 0, en retourneert ook het respectieve element:

@Override public E next () {int index = Integer.numberOfTrailingZeros (resterendeSetBits); if (index == 32) {gooi nieuwe NoSuchElementException (); } resterendeSetBits & = ~ (1 << index); return reverseMap.get (index); }

6.3. PowerSet

Om een ​​luie lading te hebben PowerSet klasse, we hebben een klasse nodig die zich uitbreidt AbstractSet.

De grootte() functie is gewoon 2 tot de kracht van de grootte van de set:

@Override public int size () {return (1 << this.set.size ()); }

Omdat de power set alle mogelijke subsets van de input set zal bevatten, dus bevat (Object o) functie controleert of alle elementen van het object o bestaan ​​in de reverseMap (of in de invoerset):

@Override openbare boolean bevat (@Nullable Object obj) {if (obj instanceof Set) {Set set = (Set) obj; return reverseMap.containsAll (set); } return false; }

Om gelijkheid van een gegeven te controleren Voorwerp met deze klasse kunnen we alleen controleren of de input set is gelijk aan het gegeven Voorwerp:

@Override public boolean is gelijk aan (@Nullable Object obj) {if (obj instanceof PowerSet) {PowerSet that = (PowerSet) obj; retourneer set.equals (that.set); } retourneer super.equals (obj); }

De iterator () functie retourneert een instantie van ListIterator die we al hebben gedefinieerd:

@Override openbare Iterator iterator () {retourneer nieuwe ListIterator(this.size ()) {@Override public Set next () {return new Subset (map, reverseMap, position ++); }}; }

De Guava-bibliotheek gebruikt dit lazy-load-idee en deze PowerSet en Subgroep zijn de equivalente implementaties van de Guava-bibliotheek.

Raadpleeg hun broncode en documentatie voor meer informatie.

Bovendien, als we parallelle werking willen uitvoeren via subsets in PowerSet, we kunnen bellen Subgroep voor verschillende waarden in a ThreadPool.

7. Samenvatting

Om samen te vatten, hebben we eerst bestudeerd wat een vermogensset is. Vervolgens hebben we het gegenereerd met behulp van de Guava-bibliotheek. Daarna hebben we de aanpak bestudeerd en hoe we deze moeten implementeren, en ook hoe we er een unit-test voor kunnen schrijven.

Ten slotte hebben we de Iterator interface om de ruimte voor het genereren van subsets en ook hun interne elementen te optimaliseren.

Zoals altijd is de broncode beschikbaar op GitHub.