Projekt Eulera - wyzwanie dla programistów
29 komentarzy | Kategorie: Java, Programowanie, Ruby, Techblog | trackbackTagi: java projekt eulera ruby
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.javaimport 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.rbputs 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.javapublic 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.
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 ;])
teamon, ’87? Kojarzę to zadanie…
(ztcp jeden wiersz miał małą literę ;D)
W E013 sumOf i ArrayList nie są chyba potrzebne :)
@darkjames: Chyba maj 2006. Taaa, byla mala literka ;]
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
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ść?
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
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).
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.
Co, jednak Java/C nie jest do wszystkiego?
A ktoś twierdzi, że w ogóle jakiś język jest „do wszystkiego”?
no Javo/C-owcy przewaznie (przewaznie != zawsze, zeby sie nikt nie rzucal) tak mysla.
@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.
Zatem jakie wg. Ciebie języki nadają się do tego typu zadań?
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++.
niezłe, nie słyszałem o tym wcześniej. wykorzystam to do nauki rubiego :]
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.
Bash ftw
@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.
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 :)
Dobry wpis, Radarek, chociaż ostatnio trochę przycichłeś z rozwiązywaniem tych zadań z forum (rozumiem, że chcesz dać szansę innym ? ;)
Pozdrawiam
@hosiawak – nie, wszystkim sie juz znudzilo ;p
@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)
No może. Ale po zrobieniu prawie 50 pod rząd trochę się odechciewa :P
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:
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 :)
No coż, okazuje się, że Ruby 1.9 też ma funkcję lcm :D
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 ;) ):
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:
Tak po namyśle. Można jeszcze krócej i równie szybko:
Mycie naczyń jednak doskonale robi na pracę mózgu: można jeszcze krócej i jeszcze wolniej (jakieś 15 sekund):
Przy okazji zastąpienie w drugim przykładzie dynamicznego tworzenia tablicy:
statyczną tablicą, skróconą o liczbę 20:
zamienia go w najładniejszą i najszybszą (poniżej 3 sekund) z wersji.
A tutaj rozwiązania Eulerów w Clojure: http://clojure-euler.wikispaces.com
; 013
;005