Kombinációk generálása Java-ban
1. Bemutatkozás
Ebben az oktatóanyagban megbeszéljük a k-kombinációk problémájának megoldását a Java-ban.
Először a rekurzív és az iteratív algoritmusokat is megvitatjuk és megvalósítjuk az adott méretű összes kombináció előállításához. Ezután áttekintjük a megoldásokat a közös Java könyvtárak használatával.
2. Kombinációk áttekintése
Egyszerűen fogalmazva, a kombináció egy adott halmaz elemeinek részhalmaza.
A permutációkkal ellentétben az egyes elemek kiválasztásának sorrendje nem számít. Ehelyett csak az érdekel, hogy egy adott elem szerepel-e a kiválasztásban.
Például egy kártyajátékban 5 kártyát kell osztanunk az 52 kártyából álló csomagból. Nem érdekel az 5 kártya kiválasztásának sorrendje. Inkább csak az érdekel, hogy melyik kártya van jelen a kézben.
Néhány probléma megköveteli, hogy értékeljük az összes lehetséges kombinációt. Ennek érdekében felsoroljuk a különféle kombinációkat.
Az „n” elemek halmazából az „r” elemek kiválasztásának különféle módjai matematikailag kifejezhetők a következő képlettel:

Ezért az elemek kiválasztásának módjai a legrosszabb esetben exponenciálisan növekedhetnek. Ennélfogva nagy populációk esetében nem biztos, hogy fel lehet sorolni a különböző szelekciókat.
Ilyen esetekben véletlenszerűen választhatunk ki néhány reprezentatív választást. A folyamatot ún mintavétel.
Ezután áttekintjük a különböző algoritmusokat a kombinációk felsorolásához.
3. Rekurzív algoritmusok kombinációk előállításához
A rekurzív algoritmusok általában úgy működnek, hogy egy problémát hasonló kisebb problémákra osztanak fel. Ez a folyamat addig folytatódik, amíg el nem érjük a befejező feltételt, ami szintén alapeset. Ezután közvetlenül megoldjuk az alapesetet.
Két elemet fogunk megvitatni, hogy fel lehessen osztani az elemeket egy készletből. Az első megközelítés a problémát a halmaz elemeire osztja. A második megközelítés csak a kiválasztott elemek követésével osztja meg a problémát.
3.1. Felosztás a teljes készlet elemei szerint
Osszuk meg a „r ” elemekn ” tételeket úgy, hogy egyesével megvizsgálja az elemeket. A készlet minden eleméhez vagy felvehetjük a kiválasztásba, vagy kizárhatjuk.
Ha beletesszük az első tételt, akkor ki kell választanunk az „r – 1″ elemek a fennmaradón - 1 ″ elemeket. Másrészről, ha elvetjük az első tételt, akkor ki kell választanunkr ” elemek a fennmaradón - 1 ″ elemeket.
Ez matematikailag kifejezhető:

Most vizsgáljuk meg ennek a megközelítésnek a rekurzív megvalósítását:
private void helper (Listakombinációk, int adatok [], int kezdet, int vég, int index) {if (index == adat.hossz) {int [] kombináció = adatok.klón (); kombinációk.add (kombináció); } else if (kezdet <= vég) {data [index] = kezdet; segítő (kombinációk, adatok, kezdet + 1, vég, index + 1); segítő (kombinációk, adatok, kezdet + 1, vég, index); }}
A segítő metódus két rekurzív hívást kezdeményez magának. Az első hívás az aktuális elemet tartalmazza. A második hívás elveti az aktuális elemet.
Ezután írjuk meg a kombinációs generátort ezzel segítő módszer:
public List generator (int n, int r) {List kombinációk = new ArrayList (); segítő (kombinációk, új int [r], 0, n-1, 0); visszatérési kombinációk; }
A fenti kódban az generál módszer beállítja az első hívást a segítő módszerrel, és átadja a megfelelő paramétereket.
Ezután hívjuk meg ezt a módszert kombinációk létrehozására:
Listakombinációk = generál (N, R); for (int [] kombináció: kombinációk) {System.out.println (Tömbök.String (kombináció)); } System.out.printf ("% d elem% d kombinációjának generálása% d-ből% d-ből", kombinációk.size (), R, N);
A program futtatásakor a következő kimenetet kapjuk:
[0, 1] [0, 2] [0, 3] [0, 4] [1, 2] [1, 3] [1, 4] [2, 3] [2, 4] [3, 4] generált 2 kombináció 10 elemet az 5-ből
Végül írjuk meg a tesztesetet:
@Test public void givenSetAndSelectionSize_whenCalculatedUsingSetRecursiveAlgorithm_thenExpectedCount () {SetRecursiveCombinationGenerator generator = new SetRecursiveCombinationGenerator (); Listaválasztás = generátor.generate (N, R); assertEquals (nCr, selection.size ()); }
Könnyű megfigyelni, hogy a halom szükséges mérete a készletben lévő elemek száma. Ha a halmazban lévő elemek száma nagy, mondjuk nagyobb, mint a hívásverem maximális mélysége, akkor túlcsordítjuk a köteget, és kapunk egy StackOverflowError.
Ezért ez a megközelítés nem működik, ha a bemeneti halmaz nagy.
3.2. Felosztás a kombináció elemei szerint
Ahelyett, hogy nyomon követné a bemeneti halmaz elemeit, felosztjuk a feladatot a kijelölt elemek nyomon követésével.
Először rendeljük meg a bemeneti halmaz elemeit az „1” és „n ”. Most kiválaszthatjuk az első tételt az elsőn-r + 1 " elemeket.
Tegyük fel, hogy a kth tétel. Aztán ki kell választanunkr - 1 " elemek a fennmaradón - k ” indexelt tételekk + 1 " nak nek "n ”.
Ezt a folyamatot matematikailag fejezzük ki:

Következő, írjuk meg a rekurzív módszert ennek a megközelítésnek a megvalósításához:
private void helper (Listakombinációk, int adatok [], int kezdet, int vég, int index) {if (index == adat.hossz) {int [] kombináció = adatok.klón (); kombinációk.add (kombináció); } else {int max = Math.min (vég, vég + 1 - adat.hossz + index); for (int i = start; i <= max; i ++) {adatok [index] = i; segítő (kombinációk, adatok, i + 1, vég, index + 1); }}}
A fenti kódban az mert hurok választja a következő elemet, majd hívja a segítő() módszer rekurzív módon a többi elem kiválasztásához. Megállunk, amikor a szükséges számú elemet kiválasztottuk.
Ezután használjuk a segítő módszer a kiválasztások előállítására:
public List generator (int n, int r) {List kombinációk = new ArrayList (); segítő (kombinációk, új int [r], 0, n - 1, 0); visszatérési kombinációk; }
Végül írjunk egy tesztesetet:
@Test public void givenSetAndSelectionSize_whenCalculatedUsingSelectionRecursiveAlgorithm_thenExpectedCount () {SelectionRecursiveCombinationGenerator generator = new SelectionRecursiveCombinationGenerator (); Listaválasztás = generátor.generate (N, R); assertEquals (nCr, selection.size ()); }
Az ezzel a megközelítéssel használt hívásverem-méret megegyezik a kijelölt elemek számával. Ebből kifolyólag, ez a megközelítés akkor működik nagy bemeneteknél, ha a kiválasztandó elemek száma kisebb, mint a hívásverem maximális mélysége.
Ha a választandó elemek száma is nagy, akkor ez a módszer nem fog működni.
4. Iteratív algoritmus
Az iteratív megközelítésben egy kezdeti kombinációval indulunk. Azután, addig generáljuk a következő kombinációt az aktuálisból, amíg nem állítjuk elő az összes kombinációt.
Generáljuk a kombinációkat lexikográfiai sorrendben. A legalacsonyabb lexikográfiai kombinációval kezdjük.
Annak érdekében, hogy a következő kombinációt az aktuálisból kapjuk, megtaláljuk az aktuális kombinációban a jobb szélső helyet, amely növelhető. Ezután növeljük a helyet, és a lehető legalacsonyabb lexikográfiai kombinációt generáljuk a helytől jobbra.
Írjuk meg a kódot, amely ezt a megközelítést követi:
public List generator (int n, int r) {List kombinációk = new ArrayList (); int [] kombináció = új int [r]; // inicializálás a legalacsonyabb lexikográfiai kombinációval a (int i = 0; i <r; i ++) {kombinációhoz [i] = i; } while (kombináció [r - 1] <n) {kombinációk.add (kombináció.klón ()); // következő kombináció generálása lexikográfiai sorrendben int t = r - 1; míg (t! = 0 && kombináció [t] == n - r + t) {t--; } kombináció [t] ++; for (int i = t + 1; i <r; i ++) {kombináció [i] = kombináció [i - 1] + 1; }} visszatérési kombinációk; }
Ezután írjuk meg a tesztesetet:
@Test public void givenSetAndSelectionSize_whenCalculatedUsingIterativeAlgorithm_thenExpectedCount () {IterativeCombinationGenerator generator = new IterativeCombinationGenerator (); Listaválasztás = generátor.generate (N, R); assertEquals (nCr, selection.size ()); }
Most használjunk néhány Java-könyvtárat a probléma megoldására.
5. Java könyvtárak kombinációk megvalósítása
Amennyire lehetséges, a meglévő könyvtári megvalósításokat újra fel kell használnunk a sajátunk bevezetése helyett. Ebben a szakaszban a következő Java könyvtárakat tárjuk fel, amelyek kombinációkat valósítanak meg:
- Apache Commons
- Gujávafa
- KombinatorikaLib
5.1. Apache Commons
A CombinatoricsUtils Az Apache Commons osztály számos kombinált segédfunkciót kínál. Különösen a kombinációkIterátor A method egy iterátort ad vissza, amely lexikográfiai sorrendben generál kombinációkat.
Először tegyük hozzá a Maven-függőséget commons-math3 a projekthez:
org.apache.commons commons-math3 3.6.1
Következő, használjuk a kombinációkIterátor módszer a kombinációk kinyomtatására:
public static void generáló (int n, int r) {Iterator iterator = CombinatoricsUtils.combinationsIterator (n, r); while (iterator.hasNext ()) {final int [] kombináció = iterator.next (); System.out.println (Tömbök.String (kombináció)); }}
5.2. Google Guava
A Készletek osztály a guavai könyvtárból nyújt hasznos módszereket a halmazhoz kapcsolódó műveletekhez. A kombinációk metódus egy adott méretű összes részhalmazot ad vissza.
Először tegyük fel a projektbe a guava könyvtár függőleges függőségét:
com.google.guava guava 27.0.1-jre
Következő, használjuk a kombinációk módszer kombinációk előállítására:
Készlet kombinációk = Készletek.com kombinációk (ImmutableSet.of (0, 1, 2, 3, 4, 5), 3);
Itt használjuk a ImmutableSet.of módszer a megadott számokból halmaz létrehozására.
5.3. KombinatorikaLib
A CombinatoricsLib egy kicsi és egyszerű Java könyvtár permutációkhoz, kombinációkhoz, részhalmazokhoz, egész partíciókhoz és derékszögű termékhez.
A projektben való felhasználáshoz adjuk hozzá a combinatoricslib3 Maven-függőség:
com.github.dpaukov combinatoricslib3 3.3.0
Következő, használjuk a könyvtárat a kombinációk kinyomtatásához:
Generator.combináció (0, 1, 2, 3, 4, 5) .egyszerű (3) .stream () .forEach (System.out :: println);
Ez a következő kimenetet eredményezi végrehajtáskor:
[0, 1, 2] [0, 1, 3] [0, 1, 4] [0, 1, 5] [0, 2, 3] [0, 2, 4] [0, 2, 5] [0, 3, 4] [0, 3, 5] [0, 4, 5] [1, 2, 3] [1, 2, 4] [1, 2, 5] [1, 3, 4] [1, 3, 5] [1, 4, 5] [2, 3, 4] [2, 3, 5] [2, 4, 5] [3, 4, 5]
További példák a következő címen érhetők el: combinatoricslib3-példa.
6. Következtetés
Ebben a cikkben néhány algoritmust valósítottunk meg kombinációk előállítására.
Néhány könyvtári megvalósítást is áttekintettünk. Általában ezeket használnánk, ahelyett, hogy a sajátunkat görgetnénk.
Szokás szerint a teljes forráskód megtalálható a GitHubon.