Any fool can make things bigger, more complex, and more violent. It takes a touch of genius and a lot of courage to move in the opposite direction. Albert Einstein

o Ruby

Projekt Eulera - wyzwanie dla programistów

29 komentarzy | Kategorie: Java, Programowanie, Ruby, Techblog | trackback
Tagi:
Leonhard Euler

Muszę przyznać, że od zawsze uwielbiałem zadania, które wymagają myślenia, główkowania i kombinowania. W okresie mojej podstawówki dotyczyło to głównie rozwiązywania zadań z matematyki, potem (gdy tylko zetknąłem się z informatyką w LO) przeszło to na zadania algorytmiczne. To były zresztą moje początki z programowaniem i do dzisiaj mam ogromny sentyment do tej dziedziny informatyki.

Ostatnio na forum rubyonrails.pl zaproponowano, by rozwiązywać zadania ze strony o nazwie "Project Euler". Jak się okazało zadania są na pograniczu matematyki, logiki, kombinatoryki, algorytmiki i większość ma tę własność, że bez pomocy komputera nie da się ich rozwiązać.

To co mnie urzekło od samego początku to fakt, że nie zawsze chodzi o znalezienie najbardziej wydajnego rozwiązania. Nie ma bowiem narzuconego z góry żadnego limitu (na stronie musimy podać otrzymane rozwiązanie i sprawdzana jest jego poprawność). Często naiwne rozwiązanie wystarcza, co nie znaczy, że napisanie programu jest banalne! Barierą może być sam język, w którym piszemy. Sporo zadań wymaga obliczeń na dużych liczbach lub operowania danymi tekstowymi, co może okazać się bardzo niewygodne (nie mylić z "niemożliwe") w takich językach jak C/C++ czy Java. Z kolei te same zadania okazują się przysłowiową "bułką z masłem" jeśli użyjemy wysokopoziomowych języków, np. Ruby lub Python (jeśli oczywiści dana funkcjonalność występuje).

Weźmy dla przykładu zadanie 13ste, w którym chodzi o zsumowanie stu 50-cio cyfrowych liczb. Do reprezentacji takich liczb potrzeba około 167 bitów, a więc taki typ (znany np. z C, Javy) jak long po prostu odpada. Nawet jak przebrniemy przez ten problem (możemy napisać własną obsługę dużych liczb lub użyć gotowej biblioteki, np. gmp) to musimy poradzić sobie z przetworzeniem tekstu zawierającego te liczby. I znowu okaże się, że w przypadku języka C (i nie tylko) takie operacje są dosyć uciążliwe.

Spójrzmy na rozwiązania tego samego problemu w językach Java i Ruby:

~ E013.java
import java.io.IOException;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.File;
import java.math.BigInteger;
import java.util.ArrayList;

public class E013 {
  public static BigInteger sumOf(Iterable<BigInteger> numbers) {
    BigInteger sum = new BigInteger("0");

    for (BigInteger number: numbers) {
      sum = sum.add(number);
    }
    return sum;
  }

  public static void main(String[] args) throws IOException {
    BufferedReader reader = new BufferedReader(new FileReader(new File("013.txt")));
    String line;

    ArrayList<BigInteger> numbers = new ArrayList<BigInteger>();
    while ((line = reader.readLine()) != null) {
      numbers.add(new BigInteger(line));
    }
    BigInteger sum = sumOf(numbers);
    System.out.println(sum.toString().substring(0, 10));
  }
}
~ 013.rb
puts File.read("013.txt").split("\n").inject(0) {|sum, n| sum + n.to_i }.to_s[0, 10]

Bo czasem jabłko jest lepsze od gruszki

Tak, to są równoważne programy! Oba robią to samo: wczytują kolejne linie z pliku 013.txt, konwertują je na liczby i sumują. Być może dla programistów niezaznajomionych z Ruby, wersja w tym języku może wydawać się trudna w odczytaniu, ale zapewniam Was, że tak nie jest. Wystarczy szybkie przeskanowanie tej jednej linijki by przekonać się, że program składa się z pięciu kroków: wczytaj z pliku (File.read("013.txt")), skonwertuj do tablicy dzieląc wedle znaku nowej linii (split("\n")), zsumuj skonwertowane do liczby kolejne linie (inject(0) {|sum, n| sum + n.to_i }), skonwertuj do łańcucha (to_s) i pobierz pierwsze 10 znaków ([0, 10]).

Wspomniany inject, który w tym wypadku sumuje liczby (arr.inject(0){|sum, item| sum + item }, jest już klasycznym idiomem tego języka.

A czasem i nawet gruszka lepsza od jabłka

Z kolei weźmy zadanie 5te, w którym musimy znaleźć najmniejszą liczbę naturalną, która będzie podzielna bez reszty przez wszystkie liczby całkowite z zakresu 1..20. Pozostańmy przy naiwnym rozwiązaniu, które wynika wprost z zadania. Sprawdzając kolejne liczby 2, 3, 4, 5 będziemy testować czy dzieli się bez reszty przez liczby z zakresu 2..20 (1 omijamy bo wiemy, że każda liczba całkowita jest podzielna przez nią bez reszty). Zacznijmy tym razem od wersji w Rubym.

~ 005_1.rb
puts 1.upto((2..20).inject(:*)).each {|n| break n if (2..20).all? {|e| n % e == 0} } 

Odpalając ten program na ruby1.9 dostaję odpowiedź po ponad 3 minutach. Na ruby1.8 nawet nie sprawdzam - będzie na pewno jeszcze dłużej. Spójrzmy zatem na rozwiązanie w javie.

~ E005.java
public class E005 {
  public static long product(int from, int to) {
    long result = 1;
    for (int i = from; i < to; i++) {
      result *= i;
    }
    return result;
  }

  public static boolean isDivisibleBy(long number, int[] divisors) {
    for (int divisor: divisors) {
      if ((number % divisor) != 0) {
        return false;
      }
    }
    return true;
  }

  public static void main(String[] args) {
    long min = 1;
    long max = product(2, 20);
    int[] divisors = {2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20};

    for (long i = min; i <= max; i++) {
      if (isDivisibleBy(i, divisors)) {
        System.out.println(i);
        break;
      }
    }
  }
}

Tym razem dostaję odpowiedź po około 10 sekundach. Oczywiście znowu wymagało to dłuższego kodu, ale zdecydowanie wolę czekać 10 sekund niż 3 minuty...

Nie myślcie, że te dwa przykłady mają prowadzić do wielkiej konkluzji. Raczej chodzi o pokazanie, co tracimy/zyskujemy decydując się na dany język. Java i Ruby są tu tylko przykładem. Mógłbym równie dobrze pokazać rozwiązania w Pascalu, Php, Pythonie czy też Erlangu (myślę, że to mogłoby być bardzo ciekawe doświadczenie). Zachęcam Was spróbowania własnych sił. Spróbujcie rozwiązywać te same zadania w kilku językach i wyciągajcie własne wnioski.

Ucz się sztuki programowania od innych

Ktoś kiedyś powiedział, że najważniejszym źródłem wiedzy jesteśmy my. Możemy uczyć się na własną rękę, ale i tak najwięcej nauczymy się od innych. Ta prawda sprawdza się idealnie jeśli chodzi o programowanie (czy też wiedzę związaną z komputerami). Niezależnie od użytego języka, praktycznie każdy problem można rozwiązać na mnóstwo sposobów, które będą mniej lub bardziej dobre. Czasem to kwestia znajomości standardowej lub zewnętrznej biblioteki, dobrze wykorzystanego API czy też zastosowanego algorytmu.

Dzięki tym zadaniom i rozwiązaniom innych mogłem przekonać się o potędze biblioteki prime dostępnej od wersji 1.9 Rubiego. Zadania, które wymagają operowania na liczbach pierwszych, czynnikach pierwszych lub podzielnikach liczb okazują się banalnie proste, jeśli mamy pod ręką taką bibliotekę (a z Ruby 1.9 mamy!).

Przykładowe zadanie i wykorzystanie tej biblioteki:

http://projecteuler.net/index.php?section=problems&id=3

The prime factors of 13195 are 5, 7, 13 and 29.

What is the largest prime factor of the number 600851475143 ?
~ 003_2.rb
require "prime"

puts 600851475143.prime_division.max.first

Nawet jeśli takie przykłady można skwitować stwierdzeniem "to tylko kwestia biblioteki" to dla mnie bardzo ważny jest fakt, że na poziomie języka i jego standardowej biblioteki mogę zrobić

2**1000 + 15

aniżeli

System.out.println(new BigInteger("2").pow(1000).add(new BigInteger("15")));

Zapraszam Was do wspólnego rozwiązywania zadań i umieszczania ich w przygotowanym dziale na forum.rubyonrails.pl. Część moich rozwiązań znajdziecie w repozytorium gita na githubie.

Jeśli spodobał Ci się wpis to może umieścisz ten blog w swoim czytniku RSS?

Komentarze

1. avatar icon teamon napisał(a) 06 Kwi 2009 o godz. 01:25:

Jak już tak porównujesz to może dorzucę jedno z zadań z matury z informatyki: http://gist.github.com/90276
Trzeba być masochistą żeby przetwarzać tekst w C(++) i mu podobnych… (Co do eulera, mam rozwiązane jakies 58 ;])

2. avatar icon darkjames napisał(a) 06 Kwi 2009 o godz. 02:55:

teamon, ’87? Kojarzę to zadanie…
(ztcp jeden wiersz miał małą literę ;D)

3. avatar icon jam łasica napisał(a) 06 Kwi 2009 o godz. 05:34:

W E013 sumOf i ArrayList nie są chyba potrzebne :)

4. avatar icon teamon napisał(a) 06 Kwi 2009 o godz. 07:36:

@darkjames: Chyba maj 2006. Taaa, byla mala literka ;]

5. avatar icon GiM napisał(a) 06 Kwi 2009 o godz. 12:11:

no pacz pan, też miałem napisać o projecteuler, bo w wolnych chwilach próbuję różne rozwiązywać, a tu ktoś mnie wyręczył :)

ostatnio jeszcze bawię się w uzupełnianie rosetty, raczej mało kształcące, ale może przyczyni się do rozpowszechniania języków.
http://rosettacode.org/wiki/Tasks_not_implemented_in_Ruby

6. avatar icon Hoppke napisał(a) 06 Kwi 2009 o godz. 15:23:

Jako wieczny maruda zauważę tylko, że „projekt eulera” jest specyficzny i z produkowaniem kodu użytkowego ma niewiele wspólnego — np. oceniając języki pod kodem przydatności do ‘eulera’ dużo punktów daję za obecność konsoli interaktywnej, a wydajność, wielowątkowość czy łatwość utrzymywania kodu są mało nieistotne.

PS. Bardzo lubię takie zadania :) Chyba najfajniejsze jest właśnie rozwiązywanie tego samego problemu różnymi językami, albo jednym językiem lecz w różny sposób.

PPS. Faktycznie w E013 sumOf i lista tylko niepotrzebnie wydłużają kod. Aha, a jakby rozpisać 005 w jakiś inny sposób (może bez injecta? innymi konstrukcjami?), to czy wpłynęłoby to na wydajność?

7. avatar icon Stanisław 'dozzie' Klekot napisał(a) 06 Kwi 2009 o godz. 20:11:

Nawet jak przebrniemy przez ten problem (możemy napisać własną obsługę dużych liczb lub użyć gotowej biblioteki, np. gmp) to musimy poradzić sobie z przetworzeniem tekstu zawierającego te liczby.

Pfff. A GMP niby od czego ma funkcje przetwarzające tekst na liczby?

Poza tym ja bym to zrobił po ludzku, w shellu.

(printf %s+ `cat liczby.txt`; echo 0) | bc

8. avatar icon Radarek napisał(a) 06 Kwi 2009 o godz. 20:26:

Faktycznie w rozwiązaniu w javie zad. nr 13, metoda licząca sumę nie jest potrzebna.

@Hopke, myślę, że nie miałoby to najmniejszego wpływu na wydajność.

@Stanisław ‘dozzie’ Klekot, moje przeoczenie. Chociaż mimo wszystko wydaje mi się, że fajnie jest jak mamy możliwość operowania na bardzo dużych liczbach bez sięgania do zewnętrznych bibliotek (niech będzie i choćby w stdlib tak jak w javie).

9. avatar icon Chris Trynkiewicz napisał(a) 06 Kwi 2009 o godz. 21:47:

No tak, jak sie w jednym jezyku pisze celowo skrotowo, a w drugim sie robi tysiac zbednych brow, to faktycznie moze wyjsc stosunek 100:1.

Dobrze zrozumialem, ze te zadania to proba zaprzegniecia tych jezykow do tego, do czego nie sa stworzone? Genialna idea, pogratulowac pomyslu.

10. avatar icon teamon napisał(a) 06 Kwi 2009 o godz. 21:48:

Co, jednak Java/C nie jest do wszystkiego?

11. avatar icon Hoppke napisał(a) 06 Kwi 2009 o godz. 21:51:

A ktoś twierdzi, że w ogóle jakiś język jest „do wszystkiego”?

12. avatar icon teamon napisał(a) 06 Kwi 2009 o godz. 21:52:

no Javo/C-owcy przewaznie (przewaznie != zawsze, zeby sie nikt nie rzucal) tak mysla.

13. avatar icon Radarek napisał(a) 06 Kwi 2009 o godz. 22:23:

@Chris Trynkiewicz, zdecydowanie źle odebrałeś wpis. Przede wszystkim nie było moim celem wykazanie, że „ruby jest super, a java/c złe”. Myślę, że najlepiej będzie, jak sam rozwiążesz kilka zadań w różnych językach (polecam podzielić języki na grupy java/c/c++, ruby/python i hybrydy np. scala), a sam wyciągniesz pewnie wnioski.

Dobrze zrozumialem, ze te zadania to proba zaprzegniecia tych jezykow do tego, do czego nie sa stworzone? Genialna idea, pogratulowac pomyslu.

Zatem jakie wg. Ciebie języki nadają się do tego typu zadań?

14. avatar icon Zal napisał(a) 07 Kwi 2009 o godz. 10:41:

Interesująca sprawa – zadania przypominają mi nieco te ze spojowego Open Contestu z 2006 roku ;] Chociaż tam zdecydowanie łatwiej można było załatwić czystym C/C++.

15. avatar icon marines napisał(a) 07 Kwi 2009 o godz. 19:32:

niezłe, nie słyszałem o tym wcześniej. wykorzystam to do nauki rubiego :]

16. avatar icon Ravicious napisał(a) 07 Kwi 2009 o godz. 19:38:

Z ciekawości odpaliłem 005_1.rb na 1.8.6. Na mojej konfiguracji (Windows XP, Athlon 1,8GHz, 1 GB… SDRAM) trwało to trochę ponad… 70 minut.

17. avatar icon D4rky napisał(a) 07 Kwi 2009 o godz. 22:35:

Bash ftw

18. avatar icon Radarek napisał(a) 08 Kwi 2009 o godz. 21:01:

@Ravicious, u mnie na ruby1.9 zajmuje to ~3min. Biorąc pod uwagę, że ruby pod windowsem jest jakieś 2x wolniejsze niż pod linuksem oraz to jak bardzo przyspieszono pewne konstrukcje (np. wywołanie bloku itp.) w ruby1.9 to nie dziwi mnie to.

19. avatar icon neq napisał(a) 09 Kwi 2009 o godz. 11:13:

Jeśli lubicie takie problemy to polecam http://codegolf.com – trzeba rozwiazac przedstawione problemy przy uzyciu jak najmniejsze ilosci znakow. Pisac mozna do wyboru w Ruby, Perl’u, Pythonie i PHP. Przedstawiane problemy wydaja sie proste, ale zakodowanie dwumianu Newtona w mniej niz 40 znakow to jest prawdziwe wyzwanie :)

20. avatar icon hosiawak napisał(a) 14 Kwi 2009 o godz. 15:54:

Dobry wpis, Radarek, chociaż ostatnio trochę przycichłeś z rozwiązywaniem tych zadań z forum (rozumiem, że chcesz dać szansę innym ? ;)

Pozdrawiam

21. avatar icon teamon napisał(a) 14 Kwi 2009 o godz. 16:09:

@hosiawak – nie, wszystkim sie juz znudzilo ;p

22. avatar icon hosiawak napisał(a) 14 Kwi 2009 o godz. 16:41:

@teamon – nie rozumiem dlaczego miałoby się już znudzić, problemy robią się coraz ciekawsze (przez ciekawsze rozumiem te, których nie rozwiążesz w 1 linijce pisząc na przemian map i inject – wiem, że te 2 metody masz już opanowane ;p)

23. avatar icon teamon napisał(a) 14 Kwi 2009 o godz. 16:42:

No może. Ale po zrobieniu prawie 50 pod rząd trochę się odechciewa :P

24. avatar icon hosiawak napisał(a) 15 Kwi 2009 o godz. 21:39:

Jeszcze wracając do tematu – przykład z Javą jest sugestywny ale chyba na siłę rozwlekły, rozwiązuję te problemy też w Javie i nie jest aż tak strasznie w sumie:

public class Problem5 {
    public static void main(String[] args) {
        long n = 2432902008176640000L; // == (2..20).inject(:*)
        outer:
        for (long i = 1; i <= n; i++) {
            for (int j = 2; j <= 20; j++)
                if (i % j != 0) continue outer;
            System.out.println(i);
            break outer;
        }
    }
}

Poza tym czy nie fajnie byłoby gdyby istniał język który łączy prostotę i elegancję zapisu z szybkością Javy ?

Okazuje się, że taki język istnieje

Problem 5 w Haskell’u:

foldr1 lcm [1..20]

Rozwiązanie zwracane jest od razu. Jasne, że funkcja lcm jest zaimplemetowana wewnętrznie i korzysta z „przyspieszającego” twierdzenia ale sam fakt że jest dostępna w bibliotece standardowej języka bardzo dobrze o nim świadczy.

Zachęcam wszystkich do zapoznania się z Haskell’em :)

25. avatar icon hosiawak napisał(a) 17 Kwi 2009 o godz. 14:19:

No coż, okazuje się, że Ruby 1.9 też ma funkcję lcm :D

(1..20).inject(:lcm)
26. avatar icon aniou@smutek.pl napisał(a) 23 Lis 2009 o godz. 21:24:

Wiem, że komentuję odrobinę zleżały wpis, ale akurat nie mam siły na nic powazniejszego. Jeśli chodzi o wspomnianego Erlanga, to mam takie rozwiązanie (też z kwietnia 2009 ;) ):

% wywolanie  check_div(20, lists:seq(11,20), lists:seq(11,20)).
%
check_div(Value, _Table, []) ->
    Value;                  % wynik

check_div(Value,  Table, [H|T]) ->
    if
        Value rem H == 0  -> check_div(Value, Table, T);
        true              -> check_div(Value + 20, Table, Table)
    end.

W tej postaci program potrzebuje, na moim sprzęcie, od 2.5 do 3 sekund na znalezienie wyniku. Upiększenie kodu (wyrzucenie kopii tablicy co wymusza jej odtwarzanie przy każdej badanej liczbie) da kod, który potrzebuje od 12 do 14 sekund i wygląda tak:

% wywolanie check_div(20, lists:seq(11,20)).
%
check_div(Value, []) ->
    Value;                  % wynik

check_div(Value, [H|T]) ->
    if
        Value rem H == 0  -> check_div(Value, T);
        true              -> check_div(Value + 20, lists:seq(11,20))
    end.
27. avatar icon aniou@smutek.pl napisał(a) 23 Lis 2009 o godz. 21:55:

Tak po namyśle. Można jeszcze krócej i równie szybko:

% wywołanie check_div().
%
check_div()             -> check_div(20).
check_div(Value)        -> check_div(Value, Value rem 11,    11).
%
check_div(Value, _, 21) -> Value;
check_div(Value, 0, H)  -> check_div(Value, Value rem (H+1), H+1);
check_div(Value, _, _)  -> check_div(Value+20).
28. avatar icon aniou@smutek.pl napisał(a) 23 Lis 2009 o godz. 23:50:

Mycie naczyń jednak doskonale robi na pracę mózgu: można jeszcze krócej i jeszcze wolniej (jakieś 15 sekund):

% wywolanie: check_div(20, [0])
%
check_div(Value, []) -> Value-20;
check_div(Value, _)  -> check_div(Value+20, [ Y || Y <- [11,12,13,14,15,16,17,18,19], Value rem Y > 0 ]).

Przy okazji zastąpienie w drugim przykładzie dynamicznego tworzenia tablicy:

lists:seq(11,20)   % zwraca: [11,12,13,14,15,16,17,18,19,20]

statyczną tablicą, skróconą o liczbę 20:

[11,12,13,14,15,16,17,18,19]

zamienia go w najładniejszą i najszybszą (poniżej 3 sekund) z wersji.

29. avatar icon qertoip napisał(a) 11 Wrz 2010 o godz. 12:38:

A tutaj rozwiązania Eulerów w Clojure: http://clojure-euler.wikispaces.com

; 013

(defn char-int [c]
  (- (int c) 48))

(defn digits [n]
  (map char-int (str n)))

(take 10 (digits (reduce + *test-numbers*)))

;005

(defn gcd
  "greatest common divisor of two positive integers"
  [a b]
  (cond
   (= a b) a
   (> a b) (recur (- a b) b)
   :else   (recur (- b a) a)))

(defn lcm
  "least common multiple of two positive integers"
  [a b]
  (/ (* a b) (gcd a b)))

(reduce lcm (range 2 21))

Dodaj coś od siebie

Możesz korzystać ze składni Textile.

Pola oznaczone * są wymagane.

Proszę o dodawanie komentarzy związanych z tematem postu, sprawy osobiste proszę załatwiać przez maila bądź gg.

Zastrzegam sobie prawo do moderacji komentarzy (edycja, usuwanie).