Przejdź do głównej zawartości

Liczby pseudolosowe


Wymaga znajomości: 1. Pierwszy program - 7. Funkcje

Motywacja

W różnych dziedzinach informatyki, np. w kryptografii, cyberbezpieczeństwie czy przy tworzeniu gier komputerowych, potrzebna jest możliwośc generowania losowych liczb. Przykładowo:

  • 🔑 tworzenie hasła z losowych znaków
  • 💥 losowe zdarzenia w świecie gry
  • 🎲 szansa na zadanie obrażeń oparta na rzucie kostką

Dlaczego "pseudolosowe"

Jak możesz zauważyć, artykuł nazywa się "liczby pseudolosowe". Komputer nie jest w stanie samodzielnie wygenerować prawdziwie losowych liczb jednak może, za pomocą pewnych sztuczek, dać nam złudzenie losowości, która jest jak najbardziej wystarczająca.

Generowanie liczb

Uwaga

W tym artykule skupimy się na bardzo prymitywnym, ale jednocześnie łatwym sposobie. W dalszej części kursu poznasz dużo potężniejsze narzędzia, pochodzące z biblioteki <random>. Wtedy z std::rand należy zrezygnować.

W tym artykule będziemy korzystać z poniższych nagłówków:

Potrzebne nagłówki
#include <cstdlib>
#include <ctime>

Zobaczmy na przykładzie jak z tego się korzysta:

Losowanie 5 liczb
#include <iostream>
#include <cstdlib>
#include <ctime>

int main() {
std::srand( std::time(0) );

std::cout << "Generuję 5 losowych liczb:\n";
for (int i = 0; i < 5; ++i)
std::cout << std::rand() << '\n';
}
Przykładowy wynik
Generuję 5 losowych liczb:
570368048
1028036926
1798519773
2028832115
1034913436

Uzyskiwanie kolejnych liczb

Kluczową funkcją jest tutaj

std::rand()

(od ang. random), która generuje i zwraca kolejną pseudolosową liczbę z sekwencji. Taka sekwencja jest bardzo nieprzewidywalna, co daje złudzenie losowości.

Ustawianie ziarna

To z jakich liczb będzie ta sekwencja się składać zależy od tzw. ziarna (ang.: seed), które również jest pewną liczbą. Do ustawiania ziarna służy funkcja:

Ustawianie ziarna
std::srand( <ziarno> )

(od ang. seed random)

Uwaga

Jeśli pozostawimy domyślne ziarno, generowana sekwencja będzie zawsze taka sama.

Dobrym pomysłem jest ustawianie go raz, na początku programu, tak jak w przykładzie. Za ziarno posłużył nam aktualny czas, w formie liczby, która zwiększa się z każdą sekundą, dlatego za każdym uruchomieniem programu dostaniemy inny efekt. Uzyskaliśmy to za pomocą wywołania funkcji:

Aktualny czas (w sekundach)
std::time(0)

Wady std::rand

Funkcja std::rand() jest prosta w użyciu i właściwie na tym jej zalety się kończą. Problemem jest m.in. to, że zakres zwracanych przez nią liczb nie jest ściśle określony i różni się w zależności np. od użytego kompilatora czy systemu operacyjnego.

Możemy być jedynie pewni tego, że zwrócona liczba jest zawsze z zakresu [0; RAND_MAX], przy czym to RAND_MAX to pewna stała, zależna od systemu czy kompilatora (nie mniejsza niż 32767).

Zakres od 0 do RAND_MAX

To jaką wartość ma RAND_MAX można banalnie sprawdzić:

Sprawdzanie maks. możliwej liczby do uzyskania z rand
#include <iostream>
#include <cstdlib>

int main() {
std::cout << "RAND_MAX: " << RAND_MAX;
}

Możliwy wynik:

Wersja z Visual Studio 2022 Preview.

RAND_MAX: 32767

Powyższe wyniki jasno ukazują ten problem. Na Windowsie uzyskaliśmy wynik 215 - 1, a na Linuxie 231 - 1

Ograniczanie zakresu liczb

Wiedząc, że std::rand() daje nam liczby z zakresu [0; RAND_MAX], możemy się tym trochę "pobawić".

Liczby rzeczywiste od 0 do 1

Zakres od 0 do 1

Wystarczy podzielić uzyskaną liczbę, przez RAND_MAX, by uzyskać wartość z zakresu od 0 do 1.

Liczba od 0 do 1
float randomFloat() {
return float( std::rand() ) / RAND_MAX;
}
Rzutowanie na float

Zwróć uwagę, że musimy skonwertować co najmniej jedną z tych liczb na typ float. Obie z tych rzeczy - RAND_MAX oraz rand() są liczbami całkowitymi, w związku z tym operacje na nich dają również liczbę całkowitą. W uproszczeniu:

int / int = int

Po konwersji będzie to wyglądać tak:

float / int = float

Jeśli się zastanawiasz dlaczego to tak działa, zobacz tą prostą analizę:

  • dla liczby 0 dostaniemy 0 / RAND_MAX, czyli nadal 0
  • dla RAND_MAX (czyli maks. liczby) dostaniemy RAND_MAX / RAND_MAX, czyli 1
  • dla wszystkich wartości pośrednich uzyskamy liczbę większą od 0 i mniejszą od 1

Liczby rzeczywiste od A do B

Zakres od A do B

Korzystając z poprzedniej funkcji randomFloat(), możemy zdefiniować podobną funkcje, która wygeneruje liczbę rzeczywistą w zakresie od A do B.

Co musimy zrobić:

  • obliczyć nowego zakresu, float Length = B - A
  • pomnożyć liczbę [0; 1] przez tą długość, by uzyskać zakres [0; Length]
  • przesunąć cały zakres o A, by uzyskać: [A; Length + A], czyli [A; B]
Liczba rzeczywista w zakresie
// Możemy użyć tej samej nazwy, bo mamy
// inne parametry (przeładowanie funkcji)
float randomFloat(float from, float to)
{
float length = to - from;

return randomFloat()*length + from;
}

lub upraszczając:

Liczba rzeczywista w zakresie (uproszczone)
float randomFloat(float from, float to)
{
return randomFloat()*(to - from) + from;
}

Liczby całkowite od A do B

W dokładnie ten sam sposób co powyżej, możemy utworzyć funkcję randomInt:

🔹Liczba całkowita w zakresie
int randomInt(int from, int to)
{
return int( randomFloat()*(to - from) ) + from;
}

Przykłady użycia

Funkcje z tego artykułu

Korzystanie z wyżej utworzonych funkcji jest bardzo proste i wygodne:

#include <iostream>
#include <cstdlib>
#include <ctime>
#include <iomanip>

// Deklaracja funkcji
float randomFloat(); // od 0 do 1
float randomFloat(float from, float to); // od "from" do "to"
int randomInt(int from, int to); // od "from" do "to" (int)

int main()
{
// Ustawienie ziarna
std::srand( std::time(0) );

// Ustawiam precyzję wyświetlania float-ów
std::cout << std::fixed;
std::cout.precision(2);

std::cout << "Losuję 5 liczb od 0 do 1:\n";
for (int i = 0; i < 5; ++i)
std::cout << randomFloat() << ' ';

std::cout << "\n\nLosuję 5 float-ów od 10 do 30:\n";
for (int i = 0; i < 5; ++i)
std::cout << randomFloat(10, 30) << ' ';

std::cout << "\n\nLosuję 5 int-ów od 0 do 100:\n";
for (int i = 0; i < 5; ++i)
std::cout << randomInt(0, 100) << ' ';

std::cout << std::endl;
}


// Definicja funkcji do losowania
/////////////////////////////////////
float randomFloat()
{
return float( std::rand() ) / RAND_MAX;
}

/////////////////////////////////////
float randomFloat(float from, float to)
{
return randomFloat()*(to - from) + from;
}

/////////////////////////////////////
int randomInt(int from, int to)
{
return int( randomFloat()*(to - from) ) + from;
}

Losowa szansa na zdarzenie

Jeśli chcemy, by pewne zdarzenie miało np. 30% szans na wystąpienie, możemy wylosować liczbę całkowitą z zakresu [1; 100] i sprawdzić, czy liczba <= 30:

🎲 Losowa szansa (30%)
int randomChance = randomInt(1, 100);

if (randomChance <= 30)
{
std::cout << "Wygrana :)";
}
else
{
std::cout << "Przegrana :(";
}

Możesz też zrezygnować z procentów i użyć zwykłych ułamków:

🎲 Losowa szansa (30%)
float randomChance = randomFloat();

if (randomChance <= 0.30f)
{
std::cout << "Wygrana :)";
}
else
{
std::cout << "Przegrana :(";
}

Liczby pseudolosowe


Wymaga znajomości: 1. Pierwszy program - 7. Funkcje

Motywacja

W różnych dziedzinach informatyki, np. w kryptografii, cyberbezpieczeństwie czy przy tworzeniu gier komputerowych, potrzebna jest możliwośc generowania losowych liczb. Przykładowo:

  • 🔑 tworzenie hasła z losowych znaków
  • 💥 losowe zdarzenia w świecie gry
  • 🎲 szansa na zadanie obrażeń oparta na rzucie kostką

Dlaczego "pseudolosowe"

Jak możesz zauważyć, artykuł nazywa się "liczby pseudolosowe". Komputer nie jest w stanie samodzielnie wygenerować prawdziwie losowych liczb jednak może, za pomocą pewnych sztuczek, dać nam złudzenie losowości, która jest jak najbardziej wystarczająca.

Generowanie liczb

Uwaga

W tym artykule skupimy się na bardzo prymitywnym, ale jednocześnie łatwym sposobie. W dalszej części kursu poznasz dużo potężniejsze narzędzia, pochodzące z biblioteki <random>. Wtedy z std::rand należy zrezygnować.

W tym artykule będziemy korzystać z poniższych nagłówków:

Potrzebne nagłówki
#include <cstdlib>
#include <ctime>

Zobaczmy na przykładzie jak z tego się korzysta:

Losowanie 5 liczb
#include <iostream>
#include <cstdlib>
#include <ctime>

int main() {
std::srand( std::time(0) );

std::cout << "Generuję 5 losowych liczb:\n";
for (int i = 0; i < 5; ++i)
std::cout << std::rand() << '\n';
}
Przykładowy wynik
Generuję 5 losowych liczb:
570368048
1028036926
1798519773
2028832115
1034913436

Uzyskiwanie kolejnych liczb

Kluczową funkcją jest tutaj

std::rand()

(od ang. random), która generuje i zwraca kolejną pseudolosową liczbę z sekwencji. Taka sekwencja jest bardzo nieprzewidywalna, co daje złudzenie losowości.

Ustawianie ziarna

To z jakich liczb będzie ta sekwencja się składać zależy od tzw. ziarna (ang.: seed), które również jest pewną liczbą. Do ustawiania ziarna służy funkcja:

Ustawianie ziarna
std::srand( <ziarno> )

(od ang. seed random)

Uwaga

Jeśli pozostawimy domyślne ziarno, generowana sekwencja będzie zawsze taka sama.

Dobrym pomysłem jest ustawianie go raz, na początku programu, tak jak w przykładzie. Za ziarno posłużył nam aktualny czas, w formie liczby, która zwiększa się z każdą sekundą, dlatego za każdym uruchomieniem programu dostaniemy inny efekt. Uzyskaliśmy to za pomocą wywołania funkcji:

Aktualny czas (w sekundach)
std::time(0)

Wady std::rand

Funkcja std::rand() jest prosta w użyciu i właściwie na tym jej zalety się kończą. Problemem jest m.in. to, że zakres zwracanych przez nią liczb nie jest ściśle określony i różni się w zależności np. od użytego kompilatora czy systemu operacyjnego.

Możemy być jedynie pewni tego, że zwrócona liczba jest zawsze z zakresu [0; RAND_MAX], przy czym to RAND_MAX to pewna stała, zależna od systemu czy kompilatora (nie mniejsza niż 32767).

Zakres od 0 do RAND_MAX

To jaką wartość ma RAND_MAX można banalnie sprawdzić:

Sprawdzanie maks. możliwej liczby do uzyskania z rand
#include <iostream>
#include <cstdlib>

int main() {
std::cout << "RAND_MAX: " << RAND_MAX;
}

Możliwy wynik:

Wersja z Visual Studio 2022 Preview.

RAND_MAX: 32767

Powyższe wyniki jasno ukazują ten problem. Na Windowsie uzyskaliśmy wynik 215 - 1, a na Linuxie 231 - 1

Ograniczanie zakresu liczb

Wiedząc, że std::rand() daje nam liczby z zakresu [0; RAND_MAX], możemy się tym trochę "pobawić".

Liczby rzeczywiste od 0 do 1

Zakres od 0 do 1

Wystarczy podzielić uzyskaną liczbę, przez RAND_MAX, by uzyskać wartość z zakresu od 0 do 1.

Liczba od 0 do 1
float randomFloat() {
return float( std::rand() ) / RAND_MAX;
}
Rzutowanie na float

Zwróć uwagę, że musimy skonwertować co najmniej jedną z tych liczb na typ float. Obie z tych rzeczy - RAND_MAX oraz rand() są liczbami całkowitymi, w związku z tym operacje na nich dają również liczbę całkowitą. W uproszczeniu:

int / int = int

Po konwersji będzie to wyglądać tak:

float / int = float

Jeśli się zastanawiasz dlaczego to tak działa, zobacz tą prostą analizę:

  • dla liczby 0 dostaniemy 0 / RAND_MAX, czyli nadal 0
  • dla RAND_MAX (czyli maks. liczby) dostaniemy RAND_MAX / RAND_MAX, czyli 1
  • dla wszystkich wartości pośrednich uzyskamy liczbę większą od 0 i mniejszą od 1

Liczby rzeczywiste od A do B

Zakres od A do B

Korzystając z poprzedniej funkcji randomFloat(), możemy zdefiniować podobną funkcje, która wygeneruje liczbę rzeczywistą w zakresie od A do B.

Co musimy zrobić:

  • obliczyć nowego zakresu, float Length = B - A
  • pomnożyć liczbę [0; 1] przez tą długość, by uzyskać zakres [0; Length]
  • przesunąć cały zakres o A, by uzyskać: [A; Length + A], czyli [A; B]
Liczba rzeczywista w zakresie
// Możemy użyć tej samej nazwy, bo mamy
// inne parametry (przeładowanie funkcji)
float randomFloat(float from, float to)
{
float length = to - from;

return randomFloat()*length + from;
}

lub upraszczając:

Liczba rzeczywista w zakresie (uproszczone)
float randomFloat(float from, float to)
{
return randomFloat()*(to - from) + from;
}

Liczby całkowite od A do B

W dokładnie ten sam sposób co powyżej, możemy utworzyć funkcję randomInt:

🔹Liczba całkowita w zakresie
int randomInt(int from, int to)
{
return int( randomFloat()*(to - from) ) + from;
}

Przykłady użycia

Funkcje z tego artykułu

Korzystanie z wyżej utworzonych funkcji jest bardzo proste i wygodne:

#include <iostream>
#include <cstdlib>
#include <ctime>
#include <iomanip>

// Deklaracja funkcji
float randomFloat(); // od 0 do 1
float randomFloat(float from, float to); // od "from" do "to"
int randomInt(int from, int to); // od "from" do "to" (int)

int main()
{
// Ustawienie ziarna
std::srand( std::time(0) );

// Ustawiam precyzję wyświetlania float-ów
std::cout << std::fixed;
std::cout.precision(2);

std::cout << "Losuję 5 liczb od 0 do 1:\n";
for (int i = 0; i < 5; ++i)
std::cout << randomFloat() << ' ';

std::cout << "\n\nLosuję 5 float-ów od 10 do 30:\n";
for (int i = 0; i < 5; ++i)
std::cout << randomFloat(10, 30) << ' ';

std::cout << "\n\nLosuję 5 int-ów od 0 do 100:\n";
for (int i = 0; i < 5; ++i)
std::cout << randomInt(0, 100) << ' ';

std::cout << std::endl;
}


// Definicja funkcji do losowania
/////////////////////////////////////
float randomFloat()
{
return float( std::rand() ) / RAND_MAX;
}

/////////////////////////////////////
float randomFloat(float from, float to)
{
return randomFloat()*(to - from) + from;
}

/////////////////////////////////////
int randomInt(int from, int to)
{
return int( randomFloat()*(to - from) ) + from;
}

Losowa szansa na zdarzenie

Jeśli chcemy, by pewne zdarzenie miało np. 30% szans na wystąpienie, możemy wylosować liczbę całkowitą z zakresu [1; 100] i sprawdzić, czy liczba <= 30:

🎲 Losowa szansa (30%)
int randomChance = randomInt(1, 100);

if (randomChance <= 30)
{
std::cout << "Wygrana :)";
}
else
{
std::cout << "Przegrana :(";
}

Możesz też zrezygnować z procentów i użyć zwykłych ułamków:

🎲 Losowa szansa (30%)
float randomChance = randomFloat();

if (randomChance <= 0.30f)
{
std::cout << "Wygrana :)";
}
else
{
std::cout << "Przegrana :(";
}