Twitter: @grzegg
Kategoria: java, Tagi: - - - - .

Java [18] – Obliczenia: liczby pseudolosowe, przydatne biblioteki i triki

Tym razem poznamy kilka bibliotek, metod a także trików przydatnych w obliczeniach.

Ta strona jest częścią materiałów do kursu “Programowanie w Javie z elementami bioinformatyki dla poczatkujących”. Pozostałe materiały znajdziesz tutaj

Przypadek

Symulowanie przypadkowych zdarzeń jest bardzo często wykorzystywane w programowaniu, nie tylko do gier ale także przy poważnych obliczeniach naukowych. Ponieważ w programie nic tak naprawdę nie dzieje się przypadkowo, stosujemy w tym celu algorytmy, które generują liczby w taki sposób, aby ich rozrzut możliwie najbardziej przypominał efekt przypadkowego losowania. Takie liczby nazywamy pseudolosowymi. Na szczęście nie musimy sami tworzyć takich algorytmów (choć z pewnością jest to ciekawe zagadnienie), możemy skorzystać z już istniejących rozwiązań.

java.util.Random

Klasa java.util.Random pozwala na stworzenie obiektu, który udostępnia szereg metod generujących liczby pseudolosowe. Jak zwykle, warto zerknąć do oficjalnej dokumentacji gdzie znajduje się wyczerpująca lista metod wraz z ich krótkim opisem. Tutaj pokażę kilka z nich w działaniu.

Ponieważ mamy tu do czynienia z liczbami pseudolosowymi, przy każdym uruchomieniu programu otrzymane wartości powinny być inne. Warto jednak przy tej okazji wspomnieć o ważny aspekcie generowania liczb pseudolosowych. Jak wspomniałem, są one tworzone z użyciem algorytmów, które wyliczają liczby w oparciu o wzory. Obliczenia rozpoczynają się od pewnej liczby, nazywanej ziarnem (ang. seed) którą komputer może pobrać sam (np. może ona mieć związek z bieżącym czasem) albo możemy ją wprowadzić. Jeśli algorytm rozpoczyna obliczenia od rożnych ziaren seria wygenerowanych liczb powinna być za każdym razem inna, ale jeśli rozpocznie od tej samej, to za każdym razem otrzymamy ten sam ciąg.

Ziarno można wprowadzić w obiekcie klasy java.util.Random przy użyciu metody setSeed(long ziarno), która przyjmuje jako argument liczbę typu long. Aby się o tym przekonać umieść w programie taki fragment kodu:


// wprowadzanie wartości ziarna
randomGenerator.setSeed(100);

System.out.println("liczby: " +randomGenerator.nextInt()+
     ", "+ randomGenerator.nextDouble()+
     ", "+ randomGenerator.nextLong());

Za każdym razem gdy uruchomisz program, otrzymasz takie same liczby.

Dla biologów (i nie tylko) może się także okazać pożyteczna metoda nextGaussian() zwracająca liczbę ze zbioru liczb o rozkładzie normalnym (Gaussa) o średniej = 0.0 i odchyleniu standardowym = 1.0.

Math.random()

Zanim poznamy inne możliwości oferowane przez klasę Math zacznijmy od generowania liczb pseudolosowych. Metoda Match.random(), którą wywołujemy bezpośrednio bez tworzenia obiektu, zwraca liczbę double z zakresu [0;1) czyli od 0 (włącznie) do 1 (z wyłączeniem 1).


System.out.println("Math.random(): "+Math.random());

Parę uwag o generowaniu liczb pseudolosowych

Jak widać na powyższych przykładach metody generujące liczby zmiennoprzecinkowe zwracają liczby z przedziału [0;1) czyli włącznie z 0 ale bez 1. Może się wydawać, że brak jedynki jest problemem jeśli chcielibyśmy otrzymać losową liczbę z zakresu od 0 do 1 włącznie. Problem jednak nie jest tak istotny ponieważ szansa na wylosowanie którejś z liczb krańcowych przedziału i tak jest bardzo mała. Jeśli nie wierzysz, napisz kod, który będzie losował liczby typu double aż wylosuje 0, uruchom go i… uzbrój się w cierpliwość ;-).

Znacznie ważniejszym problemem, który zapewne przyszedł Ci do głowy, jest taki, że często potrzebujemy liczby pseudolosowej z zakresu innego niż dostarczany przez dostępne metody. Na przykład potrzebujemy liczby całkowitej z przedziału od –2 do 2 albo zmiennoprzecinkowej mieszczącej się między 1.0 a 6.0.

Najprostsza sytuacja jest wtedy gdy zakres żądanych liczb jest przesunięty o jakąś wartość, wtedy po prostu dodajemy ją do liczby zwracanej przez odpowiednią metodę. Na przykład jeśli chcielibyśmy otrzymać liczbę double z zakresu [1;2) można to osiągnąć w ten sposób:


Math.random() + 1

Dla uzyskania liczb całkowitych z przedziału 10 – 20 możemy użyć takiego kodu:


randomGenerator.nextInt(11) + 10

Metoda nextInt(int max) obecna w drugim przykładzie pozwala także określać zakres losowanych liczb, nie jest to jednak takie proste w przypadku metod zwracających liczby zmiennoprzecinkowe, ponieważ zwracają one domyślnie liczby w przedziale 0 a 1 i nie można tego bezpośrednio zmienić. Można natomiast zwracane liczby przekształcić tak aby otrzymać w rzeczywistości większy zakres. Jeśli chcemy otrzymać zakres od 0 do jakiejś innej niż jeden liczby to po prostu mnożymy zwracaną przez metodę liczbę przez górną granicę zakresu (pamiętając, że będzie ona wyłączona z zakresu). Na przykład jeśli wykonamy kod:


Math.random()*5

Uzyskamy liczbę pseudolosową z zakresu [0;5)

Jeśli natomiast chcemy uzyskać liczbę pseudolosową z dowolnego zakresu (oczywiście biorąc pod uwagę typ generowanej liczby) to można użyć takiego wzoru:


Math.random() * (liczbaMax - liczbaMin) + liczbaMin

Przykładowo, aby uzyskać liczbę z zakresu 2 – 6 zastosujemy kod:


double liczba = Math.random() * (6 - 2) + 2

Zwróć uwagę, że wyrażenie * (liczbaMax - liczbaMin) zmienia rozmiar przedziału liczb, a + liczbaMin przesuwa początek przedziału.

Inne możliwości klasy Math

Poza generowaniem liczb pseudolosowych, klasa Math dostarcza zestawu metod pozwalających na przeprowadzanie takich operacji jak wyciąganie pierwiastków kwadratowych, podnoszenie liczb do potęgi czy obliczanie funkcji trygonometrycznych. Oczywiście można samemu napisać odpowiedni kod bazując na zdobytej dotychczas wiedzy i ewentualnie podręczniku matematyki, jest to nawet niezły sposób na trening umiejętności programistycznych ale z pewnością łatwiej wykorzystać to, co już ktoś zrobił.

Poniżej, w kodzie pokazuję w działaniu parę metod klasy Math, moim zdaniem najbardziej przydatnych, ale jak zwykle zachęcam do przejrzenia ich pełnego wykazu w dokumentacji.

Większość powyższych metod jest samoobjaśniająca, bliższego wyjaśnienia zapewne wymaga różnica między dwoma metodami zaokrąglającymi liczby: round i rint. Z pobieżnego spojrzenia na wynik (nie mówiąc o czytaniu dokumentacji) wynika, że pierwsza z nich zwraca liczbę int (lub long) a druga double. Druga różnica jest dużo bardziej subtelna i ujawnia się wtedy, gdy część ułamkowa znajduje się dokładnie między dwoma najbliższymi liczbami całkowitymi, czyli wynosi .5. W takiej sytuacji round zaokrągla w górę a rint do najbliższej liczby parzystej, co widać wynikach generowanych przez powyższy kod.

Liczby long

Z wpisu poświęconego typom danych dowiedziałeś się, że liczby int przechowują wartości od –2 147 483 648 do 2 147 483 647 natomiast long pozwalają na zapamiętanie liczb w zakresie –9 223 372 036 854 775 808 do 9 223 372 036 854 775 807. Jeśli więc chciałbyś zapisać w zmiennej przybliżoną liczbę ludzi na Ziemi w 2012 roku zapewne użyłbyś zmiennej typu long w ten sposób:


long ludzkosc = 7000000000;

Niestety, NetBeans zgłasza błąd: integer number too large: 7000000000. Zapewne to brzmi dziwnie, przecież zapisałeś, że zmienna jest typu long. Problem w tym, że najpierw jest wykonywana prawa część wyrażenia a domyślnie wszystkie liczby całkowite są traktowane jako typ int (pisałem, że komputery nie są zbyt domyślne?). Trzeba więc jasno określić, że liczba powinna być traktowana jako long. Robi się to dodając na końcu liczby literę L lub l:


long ludzkosc = 7000000000L;

Nie jest to konieczne jeśli liczba oraz wynik wyrażenia nie przekracza zakresu int. Ale zwróć uwagę na to drugie zastrzeżenie. Poszczególne liczby mogą mieścić się w zakresie int ale wynik wyrażenia już nie. W tym przypadku NetBeans ani kompilator nie zgłoszą błędu, ale wynik będzie nonsensowny. Porównaj wynik dwu wyrażeń:


long ludzkosc1 = 1000000000 * 7;
long ludzkosc2 = 1000000000 * 7L;
System.out.println("ludzkosc1: " + ludzkosc1);
System.out.println("ludzkosc2: " + ludzkosc2);

Otrzymujemy:


ludzkosc1: -1589934592
ludzkosc2: 7000000000

Ten pierwszy wynik to rezultat tzw. „przekręcenia licznika”

Przekręcenie licznika

Wykonaj kod:


int licznik = 2147483647;
System.out.println("licznik: " + licznik);
licznik++;
System.out.println("licznik: " + licznik);

Wynik jest taki:


licznik: 2147483647
licznik: -2147483648

Liczba 2147483647 jest najwyższą jaką może zapamiętać zmienna typu int jeśli zwiększymy ją o 1, zachodzi podobny efekt jak w przypadku licznika kilometrów w samochodzie kiedy przekroczy najwyższą możliwą liczbę (np. 9999999) – wtedy pokazuje najmniejszą możliwą liczbę (0000000). W przypadku zmiennych liczbowych po przekroczeniu o 1 zmienna przyjmuje najniższą możliwą wartość, dla int jest to –2147483648.

Jest to potencjalna przyczyna błędów w programie.

Precyzja obliczeń

Wykonując obliczenia na liczbach zmiennoprzecinkowych możesz czasem zobaczyć coś dziwnego. Spróbuj wykonać kod:

System.out.println(1.0 - 1.1)

Spodziewałeś się zobaczyć wynik -0.1? Otóż nie, wynik wygląda tak:

-0.10000000000000009

Przyczyną jest to, że niektóre ułamki dziesiętne nie mogą być reprezentowane dokładnie za pomocą typu double i float, w tej sytuacji zostaje użyta najbliższa możliwa liczba i w efekcie otrzymujemy wynik jak wyżej. Należy o tym pamiętać posługując się liczbami zmiennoprzecinkowymi. Na przykład nie należy ich używać w warunkach stosowanych w pętlach. Następny wniosek jest taki, że jeśli zależy nam na bardzo dokładnych obliczeniach z użyciem ułamków dziesiętnych to o typach double i float należy zapomnieć. Cóż więc w takiej sytuacji zrobić?

W pewnych przypadkach można radzić sobie przekształcając ułamki w liczby całkowite, podnosząc je o odpowiednią liczbę rzędów wielkości. Innym rozwiązaniem jest użycie klasy BigDecimal. Dodatkowym bonusem używania tej klasy jest to, że pozwala ona przechowywać większe liczby niż double. Siostrą (albo bratem) BigDecimal jest BigInteger, który jak sama nazwa wskazuje jest stworzony do obsługi baaaardzo dużych liczb całkowitych.

Zalety obu klas nieco przyćmiewa mało intuicyjny, szczególnie dla początkującego programisty, sposób ich użycia. Po pierwsze dla każdej liczby należy stworzyć obiekt, któremu przypisujemy wartość, która już się nie zmienia. Po drugie w obliczeniach nie posługujemy się operatorami działań arytmetycznych ale na obiektach wywołujemy metody, które wykonują odpowiednie działania.

Nie będziemy tu omawiać szczegółowo tych klas, poniżej w kodzie zawarłem parę podstawowych operacji na przykładzie BigDecimal. Więcej dowiesz się z pewnością z dokumentacji dla BigDecimal i BigInteger

Dzielenie może jednak skończyć się błędem:

Exception in thread "main" java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.

Tak dzieje się, gdy rozwinięcie dziesiętne wyniku jest nieskończone. Gdy stosujemy BigDecimal domyślnie program stara się wyliczyć dokładny wynik, a to nie jest możliwe takich przypadkach jak na przykład dzielnie 10 przez 3. Aby tego uniknąć należy ustawić dokładność obliczeń i sposób zaokrąglania. Na przykład poniższy kod przeprowadzi dzielenie z dokładnością do 12 miejsc po przecinku, przy zaokrąglaniu od .5 w górę:


System.out.println("Dzielenie BigDecimal: " + liczba1.divide(liczba2, 12, RoundingMode.HALF_UP));

Stałe

Spróbuj wpisac taki kod:


final double PI = 3.146;
PI = 4;

W drugiej linijce jest sygnalizowany błąd. W czym tkwi problem?
W pierwszej linii została utworzona stała i przypisana jej została wartość. Linia wygląda prawie tak jak byśmy tworzyli zmienną, ale są dwie istotne różnice. Słowo final decyduje o tym, że jest to stała a nie zmienna oraz nazwa jest piana wielkimi literami, co jest przyjętą konwencją ułatwiającą rozróżnienie zmiennych od stałych na pierwszy rzut oka.
Stałe, jak sama nazwa wskazuje, w przeciwieństwie do zmiennych nie mogą się zmieniać.
Stosujemy je wtedy gdy mamy do czynienia z wartościami które nie powinny ulegać modyfikacjom np. stałe matematyczne (jak np. Pi, e). Można oczywiście też używać zmiennych, ale stosowanie stałych daje gwarancję, że wartość pozostanie niezmienna.

Zadanie

Napisz program symulujący zmiany częstości alleli w zależności od mutacji. Zasady i założenia teoretyczne:

  • W populacji dany gen posiada dwa allele, tradycyjnie nazwijmy je: A i a.
  • Allel A mutuje w a z pewnym prawdopodobieństwem, oznaczmy je u.
  • Allel a mutuje w A z innym prawdopodobieństwem (v).
  • W populacji znajduje się początkowo określona liczba jednych i drugich alleli, np. po 100
  • Prawdopodobieństwa mutacji w obie strony podaje użytkownik
  • Program ma symulować określoną liczbę pokoleń, w każdym pokoleniu każdy allel A może zmutować w a i odwrotnie.
  • Proporcja alleli w każdym pokoleniu mają być przedstawione liczbowo i graficznie, np. w taki sposób, gdzie # to allel A, a * to allel a:

----------- Pokolenie: 0 -----------
########################################
########################################
####################********************
****************************************
****************************************

A: 100 a: 100

----------- Pokolenie: 1 -----------
########################################
########################################
###################*********************
****************************************
****************************************

A: 99 a: 101
itd.


Ta strona jest częścią materiałów do kursu “Programowanie w Javie z elementami bioinformatyki dla poczatkujących”. Pozostałe materiały znajdziesz tutaj

7 komentarzy Java [18] – Obliczenia: liczby pseudolosowe, przydatne biblioteki i triki

  • Maciek

    W jaki sposób można pogrupować poszczególne znaki (# i *)?? Wg mojej wersji kodu wyświetla się np. tak:

    1
    #****#*#**#*#*#*****#**#*#***#*#*#*#*
    #**#***#*#**#****#**#*#*#*#*#********
    #****#**#****#**#*#****#****#*#***#**
    #**#*#**#*#*#********#*#***#*#**#***#
    **#******#*****#**#*#*#*#**#*****#***
    **#****#****#****#****#**

    • Grzegorz

      Jak widzę, prawdopodobnie przechowujesz w tablicy allele, które mutują a następnie drukujesz zawartość tablicy. Ale tak naprawdę wystarczy jeśli wiesz ile jest alleli A i a. Drukujesz tyle razy znak # ile masz allelu A i tyle razy znak * ile masz alleli a.

  • MM

    Witam,

    mogę prosić o króciutką notkę, czego nie rozumiem w tym kodzie ? :)

    // liczba całkowita z zakresu 0-10 (do liczby w nawiasie -1)
    System.out.println(„int [0-19]: „+randomGenerator.nextInt(11));

    dlaczego int [0-19]?

    pozdrawiam

    MM

  • Mateusz

    Zrobiłem to w ten sposób i działa chociaż się różnie od Pana sposobu troszkę ale raczej tylko ustawieniem. No i przez zaokrąglanie może wychodzić trochę inny wynik.

    import java.util.Arrays;
    import java.util.Scanner;

    public class test {
    public static void main(String[] args) {
    Scanner sc = new Scanner(System.in);
    double allelA = 100;
    double allela = 100;
    System.out.println(„Podaj prawdopodobieństwo mutacji Allel a do A”);

    double v = sc.nextInt(); // szansa mutacji Allel a do A
    System.out.println(„Podaj prawdopodobieństwo mutacji Allel A do a”);
    double u = sc.nextInt(); // szansa mutacji Allel A do a
    System.out.println(„Podaj ilość pokoleń która ma być przewertowana”);
    int iloscPokolen = sc.nextInt();

    v= v/100;
    u = u/100;
    double zmienna;
    double zmienna2;

    //
    // for (int i = 0; i < allelA.length; i++) {
    // allelA[i] = 0;
    // allela[i] = 1;

    for (int i = 0; i < iloscPokolen; i++) {
    zmienna = allela * v;
    zmienna2 = allelA * u;

    allela = allela – zmienna + zmienna2;
    allelA = allelA – zmienna2 + zmienna;

    for (int j = 0; j<allela+allelA;j++) {
    if (j<allela)
    System.out.print("*");
    else System.out.print("#");

    if((j+1)%40 ==0 && j !=0 ) System.out.println("");

    }
    System.out.println("");
    System.out.println("Allel a: " + Math.round(allela) + " , Allel A: " + Math.round(allelA));
    }
    }
    }

  • Karolina

    Uparłam się, że zrobię inaczej ;)

    import java.util.Arrays;
    import java.util.Random;
    import java.util.Scanner;

    public class MutacjeAlleli {

    public static void main(String[] args) {

    Scanner skaner = new Scanner(System.in);
    Random random = new Random();

    System.out.println(„Podaj częstość występowania alleli A: „);
    int A = skaner.nextInt();

    int[] tabA = new int[A];

    System.out.println(„Podaj częstość występowania alleli a: „);
    int a = skaner.nextInt();

    int[] taba = new int[a];

    System.out.println(„Podaj prawdopodobieństwo mutacji allelu A: „);
    double u = skaner.nextDouble();

    System.out.println(„Podaj prawdopodobieństwo mutacji allelu a: „);
    double v = skaner.nextDouble();

    System.out.println(„Podaj liczbe pokoleń, dla których ma zostać przeprowadzona symulacja: „);
    int pokolenia = skaner.nextInt();

    int zmA, zma, zmA1, zma1, sumaA, sumaa;

    System.out.println(„W pokoleniu 0 suma alleli A wynosi: ” + A + „, a suma alleli a wynosi: ” + a);

    for (int j = 1; j < pokolenia; j++) {
    zmA = 0;
    for (int i = 0; i < A; i++) {

    if (random.nextDouble() < u) {
    tabA[i] = 0;
    } else tabA[i] = 1;

    zmA += tabA[i];
    }

    Arrays.sort(tabA);
    zma = A – zmA;

    zma1 = 0;
    for (int i = 0; i < a; i++) {

    if (random.nextDouble() < v) {
    taba[i] = 0;
    } else taba[i] = 1;

    zma1 += taba[i];
    }

    Arrays.sort(taba);

    zmA1 = a – zma1;
    sumaA = zmA + zmA1;
    sumaa = zma + zma1;

    System.out.println("W pokoleniu " + j + " suma alleli A wynosi: " + sumaA + ", a suma alleli a " + " wynosi:" + sumaa);

    for (int x : tabA) {
    if (x == 0) {
    System.out.print("#");
    } else System.out.print("*");
    }

    System.out.println("");

    for (int x : taba) {
    if (x == 0) {
    System.out.print("*");
    } else System.out.print("#");

    }
    System.out.println("");

    }
    }
    }

    Z tym kodem mam tylko jeden problem. Mianowicie nie mam pojęcia jak przedzielić grafikę, żeby mi co 40 robiło nowy println. Jest na to jakiś sposób? Z góry dziękuję :)

    • Grzegorz

      Do pętli można dodać ,,licznik”, który zwiększa się o 1 przy każdym obrocie pętli. Następnie umieszczamy ,,if” w którym, jeśli licznik jest podzielny przez 40, wykonuje się ,,println”.

Leave a Reply to Mateusz Cancel reply