5. hét: Öröklés, random számok

Dobra Gábor · 2019.02.27.

Heti kiegészítő feladatok

Ezen a héten az öröklést gyakoroljuk.

Felkészülés

1. Áttekintés

A példafeladat: oldjuk meg, hogy különböző típusú véletlenszám-generátorokat tudjunk egységesen kezelni!

Az álvéletlenszámokat a számítógép egy matematikai műveletsor segítségével állítja elő, aminek a kimenete nem tűnik szabályosnak. A generátornak van belső állapota (seed), ami minden új szám generálásánál megváltozik, így lesz többször futtatva különböző a kimenet.

Azonos seedről indulva, többszöri futtatásra viszont mindig ugyanaz a számsorozat lesz a kimenet. Ezért generátorok seedjét be kell tudni állítani, amit tipikusan a program elején, valamilyen, futásonként változó adattal szoktunk megadni.

A randomgenerátor interfésze kiinduláskor a következő:

class RandomGenerator {
public:
    virtual unsigned next() = 0;
    virtual void setSeed(unsigned newSeed) = 0;
    virtual ~RandomGenerator() {}
};

Töltsd le a kiinduló Code::Blocks projektet innen, és nyisd meg a projektfájlt. Ezeket a fájlokat találod benne:

  • random.hpp: a RandomGenerator interfészt és két osztály elődeklarációját tartalmazza. Az elődeklarációkhoz ne nyúlj, sajnos kellenek az automatikus tesztek miatt.
  • random.cpp: egyelőre üres, itt kell majd megvalósítanod néhány függvényt
  • random_test.cpp: általunk előkészített tesztek, ami automatikusan ellenőrzi az elkészült feladatokat

A projektet lefordítva és lefuttatva azt kell látnod, hogy minden teszteset FAILED. Ahogy haladsz a feladatok megoldásával, ezek egyesével, a legfelsővel kezdve zöldre fognak váltani, feltéve, hogy jól oldottad meg a feladatot.

2. Generálás C-s rand()-dal

A legegyszerűbb, Prog1-en is megismert randomgenerátor a C-ből ismerős rand() függvény, aminek a seed-jét az srand függvénnyel állíthatjuk be.

Írj egy CRand osztályt, ami a RandomGenerator leszármazottja, és a rand() - srand() függvényeket használja a tisztán virtuális függvények implementálásához!

Sajnos ez az osztály nem fog minden elvárásnak megfelelően működni, mert a rand() állapotát nem tudjuk példányonként külön tárolni. Így ha a programunkban máshol is használjuk a rand()-ot, vagy két példányunk van a CRand-ból, furcsa dolgok történhetnek. Erre nincs platformfüggetlen megoldás, Linuxon a rand_r(), Windows-on pedig a rand_s() segítségével lehetne javítani.

Megoldás
class CRand : public RandomGenerator {
public:
    virtual unsigned next() {
        return rand();
    }
    virtual void setSeed(unsigned newSeed) {
        srand(newSeed);
    }
};

3. Windows-féle rand()

Néha érdemes magunknak implementálni egy ilyen generátort. Például ha szeretnénk egy játék viselkedését utánozni, akkor a játékéval megegyező randomgenerátort kell használnunk. A Windows XP például az alábbi módon generálja a számokat:

seed = seed * 0x343fd + 0x269EC3;
return (seed >> 0x10) & 0x7FFF;

Írj egy MSRand osztályt, ami RandomGenerator, és a fenti implementációt használja! A seed kezdeti értéke legyen 0!

Tipp

A next() továbbra is paraméter nélküli, így a seed csak adattag lehet: ez lesz az MSRand objektumok belső állapota. A konstruktor 0-ra állítja, a setSeed()-del módosíthatjuk, és a next() használja a fenti módon.

Megoldás
class MSRand : public RandomGenerator {
    int seed;
public:
    MSRand() : seed(0) {}

    virtual unsigned next() {
        seed = seed * 0x343fd + 0x269EC3;
        return (seed >> 0x10) & 0x7FFF;
    }
    virtual void setSeed(unsigned newSeed) {
        seed = newSeed;
    }
};

4. int-ek generálása adott intervallumon

Írj globális getIntDistribution függvényt, ami egy [min, max) balról zárt, jobbról nyitott intervallumon lévő számot ad vissza!

  • Az első paramétere bármilyen RandomGenerator lehessen,
  • második és harmadik paramétere pedig a [min, max) intervallum határai!

Az intervallum skálázását így érdemes megoldani:

INTERVALLUM

Feltételezheted, hogy a maximális érték mindig nagyobb az intervallum hosszánál. Ez a módszer nem produkál egyenletes eloszlást, de ezzel most ne foglalkozzunk.

Megoldás
int getIntDistribution(RandomGenerator& rng, int a, int b) {
    return (rng.next() % (b-a)) + a;
}

5. std::string keverése

Írj shuffleString nevű globális függvényt, ami Fisher-Yates algoritmussal összekever egy std::string-et!

  • Az első paramétere bármilyen RandomGenerator lehessen,
  • a második paramétere pedig az std::string, amit helyben összekever (megváltoztat)!

A Fisher-Yates algoritmus elve:

  • a sztringen hátulról megy végig,
  • az utolsó kivételével minden karakterét megcseréli
  • az előtte lévő karakterek valamelyikével, akár önmagával.

Az std::string két, számunkra fontos művelete:

  • ugyanúgy indexelhető, mint egy tömb,
  • a mérete lekérdezhető a .size() tagfüggvénnyel.
Megoldás
void shuffleString(RandomGenerator& rng, std::string& str) {
    for (int i = str.size() - 1; i > 0; --i) {
        int r = getIntDistribution(rng, 0, i+1);
        char temp = str[r];
        str[r] = str[i];
        str[i] = temp;
    }
}

6. nextDouble

A RandomGenerator osztályhoz adj egy nextDouble() tagfüggvényt, ami a next() felhasználásával [0, 1) közötti valós számot generál! Vigyázz, a next() által visszaadott legnagyobb szám is változik az egyes generátoroknál!

Tipp

A legnagyobb számnak is tisztán virtuális függvénynek kell lennie (max()), ezt implementálnod kell a leszármazottakban. A CRand esetében RAND_MAX, az MSRand esetében pedig ki tudod olvasni az implementációból a bitmask-ot.

A nextDouble() függvény a RandomGenerator osztályban is implementálható. Vigyázz, a max() által visszaadott értéket még visszaadhatja a next() is, neked viszont mindenképp 1.0-nál kisebb számot kell generálnod!

Megoldás
class RandomGenerator {
public:
    // ...

    double nextDouble() {
        return next() / ((double) max() + 1.0);
    }
};

7. double-ök generálása adott intervallumon

Írj a getIntDistribution-höz hasonló getDoubleDistribution függvényt, ami valós számot generál a valósakkal megadott intervallumon! Használd a nextDouble() függvényt, az intervallum skálázásához segít az ábra:

INTERVALLUM

Megoldás
double getDoubleDistribution(RandomGenerator& rng, double a, double b) {
    return rng.nextDouble() * (b-a) + a;
}

8. Haladó (IMSc) feladat: C++11 random

A C++11 sokkal fejlettebb eszközöket szabványosított a random számok generálásához. Az egyik ilyen osztály a <random>-beli std::mt19937, ami egy 32 bites Mersenne twistert valósít meg.

  • A következő random számot a függvényhívás operátorával tudjuk tőle kérni,
  • van seed() nevű tagfüggvénye, amivel beállíthatjuk a seed-et,
  • és van statikus max() függvénye, ami visszaadja a legnagyobb generálható számot.

Írj egy Cpp11Random osztályt, ami egy ilyen Mersenne twistert használ!

Megoldás

9. Haladó (IMSc) feladat: set_random_seed()

A seed "véletlenszerű" beállítása is különbözhet az egyes generátorokban:

  • CRand és MSRand esetén nincs jobb lehetőségünk a Prog1-en megismert time(NULL) függvénynél,
  • a Mersenne twistert azonban a szintén C++11-es std::random_device használatával szokás inicializálni. Olvass utána, hogyan kell használni!

Írj set_random_seed() függvényt, ami beállítja a seedet, és a hívónak vissza is tér az új seeddel! Ez arra lehet jó, hogy el lehessen menteni, hogy később újra lehessen játszani ugyanazt a játékot.