A konténerobjektumok alapvető megkülönböztető jellemzői, hogy számít-e a bennük foglalt elemek sorrendje és az, hogy megengedett-e, hogy ugyanazon elem többször előforduljon. Ez a két feltétel négy konténerkategóriát határoz meg. Ezt mutatja az ábra, ahol a Python releváns beépített alaptípusai vannak besorolva az egyes kategóriákba.

Láthatjuk, hogy abban a konténerkategóriában, amelynél az elemek sorrendje nem jellemző, viszont ugyanazon elem többször is előfordulhat nem szerepel beépített konténertípus. Ezt a kategóriát multihalmaznak (multiset) nevezik a matematikában. Ez az elnevezés lényegében arra utal arra, hogy az ilyen konténerobjektum sok tekintetben hasonlít a halmazokra, de eltér abban, hogy lehet benne elemismétlődés.
A multihalmaz tehát abban különbözik a halmaztól (set), hogy megenged azonos elemeket is, azaz egy adott elem nem csak legfeljebb 1-szer fordulhat elő, mint egy normál halmazban, hanem többször is.
Multihalmazzal számos olyan valós helyzet és feladat modellezhető, amelyek halmazzal vagy sorozattal nem, vagy csak nehézkesen. Ilyen például egy dobozban levő tárgyak sokasága, amelyek között van egyforma (pl. a kombinatorikában szinte mindig megemlített valamilyen darabszámú piros és fehér golyók); egy bevásárló kosár tartalma, ahol ugyanazon árucikkből több darabot is vehetünk. Vagy akár a molekulák, hiszen azok atomok olyan sokasága, amelyek között ugyanaz többször szerepelhet egy molekulában.
A multihalmazzal való munkához szükséges annak alapjellemzőit megismerni. Ezek röviden összefoglalva a következők.
- Egy multihalmaz akkor adott, ha ismertek az elemei és, hogy azok hányszor fordulnak elő.
- A multihalmaz egy elemének multiplicitása (multiplicity) az a pozitív egész szám, ahányszor az adott elem előfordul, ismétlődik. A multihalmazok között végzett műveletek nulla vagy negatív multiplicitást is eredményezhetnek. Azonban egy objektum csak akkor eleme a multihalmaznak, ha multiplicitása pozitív.
- A multihalmaz elemszáma vagy számossága (cardinality) az elemei multiplicitásának összege. Ha a számosság nulla, akkor üres multihalmazról van szó.
- Egy A multihalmaz részmultihalmaza (submultiset) egy olyan multihalmaz, amelynek minden eleme A-nak is eleme és multiplicitásuk legfeljebb akkora, mint az A-ban szereplő multiplicitás.
- Egy A multihalmaz valódi részmultihalmaza (proper submultiset) egy olyan multihalmaz, amelynek minden eleme A-nak is eleme és multiplicitásuk kisebb, mint az A-ban szereplő multiplicitás.
A multihalmazok között a normál halmazokra vonatkozó alapműveletek (unió, metszet, különbség, szimmetrikus különbség) szintén értelmezettek, de általánosított módon a következő definíciók szerint:
Az A és B multihalmaz
– uniója (union) az a legkisebb számosságú multihalmaz, amelynek mind A, mind B részhalmaza. Ebből következik, hogy az unióban szereplő bármely x elem multiplicitása az A és B multihalmazokban szereplő x elemek multiplicitásai közül a nagyobb.
– metszete (intersection) az a legnagyobb számosságú multihalmaz, amely mind az A, mind a B multihalmaznak részhalmaza. Ebből következik, hogy a metszetben szereplő bármely x elem multiplicitása az A és B multihalmazokban szereplő x elemek multiplicitásai közül a kisebb.
– különbsége (difference) egy olyan multihalmaz, amelyben egy adott elem multiplicitása az elem A-beli és B-beli multiplicitásának különbsége, ha az pozitív, és nulla egyébként.
– szimmetrikus különbsége (symmetric difference) az a multihalmaz, amely A és B uniójának és metszetének különbsége.
Eltérően a normál halmazoktól, a multihalmazok között értelmezett még az összegzés (sum) művelete is, amely egy olyan multihalmazt eredményez, amelyben egy elem multiplicitása az adott elem A-beli és B-beli multiplicitásainak összege.
Ezen alapműveleteket szemlélteti a következő ábra a szokásos matematikai műveleti jelekkel.

Ha multihalmazokkal szeretnénk a programunkban dolgozni, akkor kell egy erre alkalmas konténertípus. Ahogy fentebb említettük, sztenderd beépített típus erre nincs a Pythonban, de a kategorizálást mutató ábrán jeleztük, hogy a collections modulban található Counter osztály – bár a neve nem utal rá – alkalmazható lehet erre. Mindazonáltal a Counter osztálynak ebből a szempontból megvannak a maga korlátai. Például nem kínálja fel a set és frozetset típusoknál elérhető halmazműveleteket (unió, metszet, különbség, szimmetrikus különbség) megvalósító metódusokat; ezek a műveletek csak operátorral valósíthatók meg, kivéve a szimmetrikus különbséget, mert a ^ operátor, illetve a __xor__ metódus nincs értelmezve. Ráadásul ezek az operátorok is csak Counter típusok között működnek, holott lenne értelme halmazműveleteknek multihalmaz és halmaz között is, hiszen egy normál halmaz felfogható úgy, mint egy olyan multihalmaz, amelyben az elemek multiplicitása 1.
Mindezen okból érdemes lehet elkészíteni egy Multiset nevű osztályt, amely rendelkezik egy multihalmaztól elvárható képességekkel. Azért is lehet hasznos egy ilyen osztály felépítésén gondolkodni és dolgozni, mert ennek során nem csak a multihalmazokkal van alkalmunk mélyrehatóbban megismerkedni, hanem Python tudásunkat is jelentősen fejleszthetjük, mert az osztály létrehozásához jónéhány nyelvi elem és konstrukció ismerete szükséges.
Mivel a Counter osztály számos, multihalmazra jellemző attribútummal és képességgel rendelkezik, célszerű ezt kihasználni. Ezért a Multiset oszályunk a Counter osztályt örökli. A Multiset osztály egy lehetséges osztálydefinícióját alább láthatjuk. A kommentek segítik a megértést. /A működéshez Python 3.10+ szükséges/
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 |
# modul: multiset.py from __future__ import annotations from collections import Counter from typing import Optional, Sequence, Mapping, TypeAlias, Type, Any class Multiset(Counter): def __init__(self, mset_arg: MsetArgType): arg = Counter(mset_arg) # Mivel multihalmaznál a multiplicitás nemnegatív egész lehet csak, ezt ellenőrizzük. if not all((isinstance(mult, int) and mult >= 0 for mult in arg.values())): raise ValueError('Multiplicitás csak nemnegatív egész lehet.') super().__init__(arg) def __str__(self): return '{}{}{}'.format(chr(0x2774), str(dict(self)).strip('{}'), chr(0x2775)) # Alapvető halmazműveletek. def union(self, other: MsetArgType) -> Multiset: """Két multihalmaz uniója. Egy olyan új multihalmazzal tér vissza, amely mindkét halmaz elemeit tartalmazza úgy, hogy egy elem multiplicitása a két multihalmazban szereplő multiplicitások közül a nagyobb. """ return type(self)(self | other) def intersection(self, other: MsetArgType) -> Multiset: """Két multihalmaz metszete. Egy olyan új multihalmazzal tér vissza, amely mindkét halmaz elemeit tartalmazza úgy, hogy egy elem multiplicitása a két multihalmazban szereplő multiplicitások közül a kisebb. """ return type(self)(self & other) def difference(self, other: MsetArgType) -> Multiset: """Két multihalmaz különbsége. Egy olyan új multihalmazzal tér vissza, amelyben egy adott elem multiplicitása a két multihalmazban szereplő multiplicitások különbsége, ha az pozitív, és nulla egyébként. """ return type(self)((self - type(self)(other))) def symmetric_difference(self, other: MsetArgType) -> Multiset: """Két multihalmaz szimmetrikus különbsége. Egy olyan új multihalmazzal tér vissza, amely két multihalmaz uniójának és metszetének különbsége. """ return type(self)(self.union(other) - self.intersection(other)) def sum(self, other: MsetArgType) -> Multiset: """Két multihalmaz összege. Egy olyan új multihalmazzal tér vissza, amelyben egy adott elem multiplicitása a két multihalmazban szereplő multiplicitások összege. """ return type(self)(self + type(self)(other)) def isdisjoint(self, other: MsetArgType) -> bool: """A visszatérési értéke True, ha a két multihalmaznak nincs közös eleme.""" return len(self.intersection(other)) == 0 def issubset(self, other: MsetArgType) -> bool: """A visszatérési értéke True, ha a példány részhalmaza az argumentumnak.""" return self <= type(self)(other) def issuperset(self, other: MsetArgType) -> bool: """A visszatérési értéke True, ha az argumentum részhalmaza a példánynak.""" return self >= type(self)(other) # A multihalmaz elemeinek multiplicitását, a multihalmaz számosságát, valamint az eltérő elem számát visszaadó metódusok. def multiplicity(self, element) -> int | None: """Visszaadja az argumentum multiplicitását vagy None-t, ha az elemként nem található.""" return self.get(element) def number_of_distinct_elements(self) -> int: """Visszaadja a megkülönböztethető elemek számát.""" return len(self.keys()) # Elemek hozzáadását és eltávolítását végző metódusok. def add(self, element, multiplicity=1): """Egy elem felvétele a multihalmazba adott multiplicitással.""" super().update({element: multiplicity}) def discard(self, element, multiplicity: Optional[int] = None): """Az argumentumként megadott elem összes példányát eltávolítja a multihalmazból, ha a második argumentum nincs megadva vagy None. Ha a második argumentum is adott, akkor ez az érték lesz levonva az elem multiplicitásából. Ha az argumentum nem eleme a multihalmaznak a metódusnak nincs hatása. """ if multiplicity is not None: self[element] -= multiplicity else: del self[element] def remove(self, element, multiplicity: Optional[int] = None): """Az argumentumként megadott elem összes példányát eltávolítja a multihalmazból, ha a második argumentum nincs megadva vagy None. Ha a második argumentum is adott, akkor ez az érték lesz levonva az elem multiplicitásából. Ha az argumentum nem eleme a multihalmaznak, akkor egy KeyError kivétel keletkezik. """ if element in self: self.discard(element, multiplicity) else: raise KeyError(f'{element}') def pop(self): """Egy tetszőleges elemet eltávolít és ezt visszatérési értékként szolgáltatja.""" element = next(iter(self)) self.remove(element) return element def copy(self) -> Multiset: """A multihalmaz sekély másolatát adja vissza""" return type(self)(self.copy()) # Értékadással kiterjesztett halmazműveletek, amelynek során a példány tartalma módosul. def union_update(self, other: MsetArgType) -> None: """A két multihalmaz unióját fogja a példány tartalmazni.""" arg = type(self)(other) resulted_mset = self.union(arg) self.clear() for element, multiplicity in resulted_mset.items(): self.add(element, multiplicity) def intersection_update(self, other: MsetArgType) -> None: """A két multihalmaz metszetét fogja a példány tartalmazni.""" arg = type(self)(other) resulted_mset = self.intersection(arg) self.clear() for element, multiplicity in resulted_mset.items(): self.add(element, multiplicity) def difference_update(self, other: MsetArgType) -> None: """A példány és other különbségét fogja a példány tartalmazni.""" arg = type(self)(other) resulted_mset = self.difference(arg) self.clear() for element, multiplicity in resulted_mset.items(): self.add(element, multiplicity) def symmetric_difference_update(self, other: MsetArgType) -> None: """A két multihalmaz szimmetrikus különbségét fogja a példány tartalmazni.""" arg = type(self)(other) resulted_mset = self.symmetric_difference(arg) self.clear() for element, multiplicity in resulted_mset.items(): self.add(element, multiplicity) # Függvények és operátorok működéséhez szükséges speciális metódusok. def __len__(self): """Visszaadja a multihalmaz összes elemének számát, azaz a multihalmaz számosságát.""" return self.total() def __or__(self, other: MsetArgType) -> Multiset: return type(self)(super().__or__(type(self)(other))) def __ror__(self, other: MsetArgType) -> Multiset: return self | type(self)(other) def __and__(self, other: MsetArgType) -> Multiset: return type(self)(super().__and__(type(self)(other))) def __rand__(self, other: MsetArgType) -> Multiset: return self & type(self)(other) def __sub__(self, other: MsetArgType) -> Multiset: return type(self)(super().__sub__(type(self)(other))) def __rsub__(self, other: MsetArgType): return (self ^ type(self)(other)).difference(self) def __xor__(self, other: MsetArgType) -> Multiset: union = self | type(self)(other) isect = self & type(self)(other) return type(self)(union - isect) def __rxor__(self, other: MsetArgType) -> Multiset: return self ^ type(self)(other) def __add__(self, other: MsetArgType) -> Multiset: return type(self)(super().__add__(type(self)(other))) def __radd__(self, other: MsetArgType) -> Multiset: return self + type(self)(other) def __ior__(self, other: MsetArgType) -> Multiset: self.union_update(other) return self def __iand__(self, other: MsetArgType) -> Multiset: self.intersection_update(other) return self def __isub__(self, other: MsetArgType) -> Multiset: self.difference_update(other) return self def __ixor__(self, other: MsetArgType) -> Multiset: self.symmetric_difference_update(other) return self # A metódusok által elfogadható típusokat képviselő típushelyettesítő (type alias). MsetArgType: TypeAlias = set | frozenset | Counter | Multiset | Sequence | Mapping |
A multihalmazok alkalmazására, a közöttük végrehajtható műveletekre (unió, metszet, különbség és összegzés) mutatunk pár példát a következőkben az elkészített Multiset típus használatával:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
from multiset import Multiset # 1. Feladat. # Vendéget várunk, és tudjuk, hogy két ételt nagyon szeret és fel szeretnénk készülni, hogy választása szerint # bármelyiket azonnal helyben el tudjuk készíteni. Elmegyünk bevásárolni a hozzávalókat. # Az egyik ételhez kell 3 fej hagyma, 4 tojás, és 2 pohár tejföl. A másikhoz 2 fej hagyma, 3 tojás, és 1 szál répa. # A két étel alapanyagai között tehát vannak átfedések. # Ahhoz, hogy a két étel közül bármelyiket biztosan el lehessen készíteni, mennyit kell vásárolni egy-egy alapanyagból # úgy, hogy a minimálisan szükségesnél ne vegyünk többet (legkisebb legyen a költség)? # MEGOLDÁS: a két ételhez szükséges alapanyakokat egy-egy multihalmazzal modellezzük. Ezek uniójaként előálló # multihalmaz adja meg a legkisebb összköltségű beszerzés tételeit és mennyiségeit. ingredients1, ingredients2 = Multiset(dict(hagyma=3, tojás=4, tejföl=2)), Multiset(dict(hagyma=2, tojás=3, répa=1)) ingredients_to_be_purchased = ingredients1 | ingredients2 print('A beszerzendő árucikkek és mennyiségeik', ingredients_to_be_purchased) # A beszerzendő árucikkek és mennyiségeik ❴'hagyma': 3, 'tojás': 4, 'tejföl': 2, 'répa': 1❵ # 2. Feladat. # Városnéző túrára megy egy csoport. Két látnivalót mindenképpen meg szeretnének tekinteni. Azonban mindkét helyen # valamiért korlátozva van, hogy egyszerre legfeljebb hány nő, hány férfi és hány gyerek tartózkodhat ott. # Az egyik helyet 5 férfi, 5 nő és 10 gyerek látogathatja egyidőben, a másikat 9 férfi, 3 nő és 12 gyerek. # Mekkora lehet a csoport maximális létszáma, hogy mindkét helyet minden résztvevő meg tudja nézni, és # hány férfi, hány nő és hány gyerek lesz ebben a csoportban? # MEGOLDÁS: a két helyszín maximális kapacitásviszonyait egy-egy multihalmazzal modellezzük. Ezek metszeteként előálló # multihalmaz adja meg a látogatócsoport összetételét és maximális létszámát. site_capacity1, site_capacity2 = Multiset(dict(férfi=5, nő=5, gyerek=10)), Multiset(dict(férfi=9, nő=3, gyerek=12)) visitor_group = site_capacity1.intersection(site_capacity2) print('A csoport maximális létszáma {}, összetétele: {}'.format(len(visitor_group), visitor_group)) # A csoport maximális létszáma 18, összetétele: ⎨'férfi': 5, 'nő': 3, 'gyerek': 10⎬ # 3. Feladat. # Egy egyetemen a félév végén egy-egy tantárgyból vizsgázók száma a következő: # matematikából 20 fő, fizikából 35 fő, informatikából 30 fő. A sikertelen vizsgák számának alakulása: # matematika 5 fő, fizika 8 fő, informatika 10 fő. Mennyi lesz az egyes tárgyakból a sikeres vizsgák száma? # MEGOLDÁS: az egyes tantárgyakból vizsgára jelentkezők számát és a sikertelen vizsgák számát egy-egy multihalmazban # gyűjtjük össze. A tantárgyankénti sikres vizsgákat jelentkezők és a sikertelen vizsgák halmazának különbsége adja. exams = Multiset(dict(matematika=20, fizika=35, informatika=30)) unsuccessful_exams = Multiset(dict(matematika=5, fizika=8, informatika=10)) successful_exams = exams - unsuccessful_exams print('Az egyes tárgyakból tett sikeres vizsgák:', successful_exams) # Az egyes tárgyakból tett sikeres vizsgák: ⎨'matematika': 15, 'fizika': 27, 'informatika': 20⎬ # 4. Feladat. # Két hadsereg állomásozik két különböző helyen. Az egyikben 100 gyalogos, 5 tank és 3 felderítő van, a másikban # 3 tüzér, 1 híradó, 1 felderítő és 10 gyalogos. A két csapat parancsot kap, hogy egy bizonyos célpontot # egyesített erővel támadjanak. Milyen lesz az összetétele az egyesített haderőnek? # MEGOLDÁS: a két hadsereget egységeik szerint egy-egy multihalmazzal modellezzük. Az egyesített haderő összetétele és # az egységek száma a két multihalmaz összegzéseként kapható meg. army1 = Multiset(dict(gyalogos=100, tank=5, felderítő=3)) army2 = Multiset(dict(tüzér=3, híradó=1, felderítő=1, gyalogos=10)) united_army = army1 + army2 print('Az egyesített hadsereg összetétel:', united_army) # Az egyesített hadsereg összetétel: ⎨'gyalogos': 110, 'tank': 5, 'felderítő': 4, 'tüzér': 3, 'híradó': 1⎬ |
E bejegyzésben szereplő főbb nyelvi elemekről a Python tudásépítés lépésről lépésre című e-könyv következő részeiben olvashatunk részleteiben: „Beépített típusok nyilvános metódusai” fejezet „Halmazműveletek” alfejezet. Az „Osztály vigyázz! – típuslétrehozás osztályokkal”, az „Öröklődés” és a „Mágikus metódusok egyedi igényre szabott osztályokban” című fejezetek. Továbbá a „Készétel fogyasztás – a szabványos könyvtár moduljainak használata” fejezet „Speciális konténer típusok” alfejezet „Sorozatelemek összeszámolása – Counter” címe.