Przeglądając różne informacje w Internecie trafiłem na stronę skierowanego w stronę programistów konkursu „Potyczki Algorytmiczne”, edycja 2011. Jednym z organizatorów jest wydział Matematyki, Informatyki i Mechaniki Uniwersytetu Warszawskiego [MIMUW], co gwarantuje dobry poziom zawodów. Jako, że lubię czasem sprawdzić swoje umiejętności programistyczne, zarejestrowałem się w serwisie. Zapraszam do lektury krótkiego komentarza i analizy rozwiązania pierwszego, testowego zadania. Uwaga – zawiera spoiler! ;]
Zadanie próbne „Tulipany”.
Uwaga: Czytelników Planety PHP proszę o wybaczenie – nie jest on w żaden sposób związany z tematyką tworzenia stron internetowych, aczkolwiek pozwoliłem sobie otagować go jako „internet”, żeby zainteresowani także mogli się z nim zapoznać. Kolejne wpisy o tym konkursie nie będą zawierały już problematycznych tagów – chyba, że będzie duży odzew poparcia, wtedy rozważę taką możliwość. ;]
Pierwsze zadanie w konkursie nosi tytuł „Tulipany”. Jego treść można ściągnąć jako plik PDF tutaj. Ze względu na to, że nie jest długa, pozwolę sobie ją przytoczyć:
Na świecie znanych jest 15 tysięcy odmian tulipanów (tak naprawdę ta liczba jest pewnym przybliżeniem, ale dla uproszczenia założymy, że jest ich dokładnie tyle). Bajtazar przez wiele lat tworzył swój prywatny spis tulipanów, w którym każdej z odmian tych wspaniałych kwiatów przyporządkował numer katalogowy między 1 a 15 000.
Niedawno katalogiem Bajtazara zainteresowała się dyrekcja bajtockiego ogrodu botanicznego. W ogrodzie tym rośnie mnóstwo przeróżnych tulipanów. Kierownictwo uznało jednak, że gdyby udało im się zebrać wszystkie znane odmiany tulipanów, to na pewno stanowiłoby to świetną reklamę ich placówki. Dlatego poproszono Bajtazara o sprawdzenie, ilu odmian tulipanów brakuje w ogrodzie.
Bajtazar zidentyfikował już odmiany tulipanów rosnących w kolejnych grządkach ogrodu, jednak wyznaczenie liczby brakujących odmian sprawiło mu pewien problem. Czy pomógłbyś mu w tym?
Dane wejściowe:
W pierwszy wierszu standardowego wejścia znajduje się jedna liczba całkowita n (1 <= n <= 20 000), oznaczająca liczbę grządek tulipanów w ogrodzie. Wdrugim wierszu znajduje się n liczb całkowitych z przedziału [1, 15 000], oznaczających odmiany tulipanów rosnących w poszczególnych grządkach ogrodu.
Wyjście:
Twój program powinien wypisać na standardowe wyjście jeden wiersz zawierający jedną liczbę całkowitą,
oznaczającą liczbę odmian tulipanów, które ogród musi jeszcze dokupić, aby móc się poszczycić kolekcją
wszystkich znanych odmian tulipanów.
Przykład:
Dla danych wejściowych:
8
3 6 2 2 4 6 3 7
poprawnym wynikiem jest:
14995
Rozwiązanie:
Zadanie sprowadza się do wyznaczenia ilości unikalnych gatunków tulipanów zasadzonych we wszystkich podanych w danych wejściowych grządkach. W PHP moglibyśmy sobie pomóc funkcją array_unique(), tutaj trzeba było chwilę pogłówkować.
Mój trywialny algorytm polegał na tym, żeby kolejne liczby konwertować na typ int [można było zastosować unsigned int - założenia zadania], a następnie sprawdzać wszystkie już wczytane wartości w poszukiwaniu duplikatu. Na początku zgodnie z treścią zakładałem że maksymalna ilość unikatów jest równa 15000. Każdy kolejny znaleziony unikat zmniejszał tą wartość.
Ze względu na to, że informację o konkursie znalazłem nieco po godzinie 17, a termin oddania zadania był ustawiony sztywno na godzinę 18, szybko włączyłem Eclipse i nie zważając na to, że piszę kod C++ w IDE do PHP naskrobałem coś takiego [main.cpp]:
#include <cstdlib>
#include <cstdio>
int main(int argc, char **argv)
{
int rows = 0;
scanf("%i", &rows);
int result = 15000;
bool unique = false;
int *nums = (int *)calloc(rows, sizeof(int));
for(int i = 0; i < rows; i++)
{
scanf("%i", &nums[i]);
unique = true;
for(int j = 0; j < i; j++)
{
if(nums[j] == nums[i])
{
unique = false;
}
}
if(true == unique)
{
result--;
}
}
printf("%i", result);
return 0;
}
Plik z danymi testowymi wyglądał następująco [data.txt]:
8 3 6 2 2 4 6 3 7
Polecenie kompilujące i uruchamiające program z danymi testowymi:
g++ -O2 -static main.cpp -lm | ./a.exe < data.txt
Na początku źle zrozumiałem sformułowanie mówiące, że dane pojawią się na standardowym wejściu i zacząłem je czytać z parametrów wywołania [tablica argv]. Nie muszę mówić, że szybki segfault w automatycznym sprawdzeniu naprawił mój błąd myślowy i pozwolił na oddanie powyższego listingu.
W tym momencie widzę już jeden „karygodny” błąd – nie umieściłem w kodzie instrukcji break po znalezieniu duplikatu [linia 19] – myślę, że zmniejszyłoby to istotnie ilość porównań. Z drugiej strony najbardziej wymagające testy wykonały się na maszynie konkursowej w <0,1s dla limitu 1s, i <0,4s dla limitu 4s, także nie jest tragicznie. Dawno już nic nie robiłem w językach C / C++, także dobrze będzie odświeżyć sobie trochę informacji z tej dziedziny.
Oczywiście rzeczony program, nawet z powyższą poprawką nie jest optymalny – na pewno możnaby było w nim wiele rzeczy usprawnić, np. zamiast sprawdzać całą tablicę do indeksu zrobić mapę istniejących wartości i z nich odczytywać odpowiednią pozycję. Można to osiągnąć prosto stosując kontener STL std::map, który gwarantuje odczyt w O(log n). Oszczędzilibyśmy wtedy też nieco pamięci, ponieważ w najgorszym przypadku program zaalokuje 20.000 4 bajtowych intów, a więc 80kB pamięci.
Zachęcam do rejestracji w konkursie i rozwiązywania kolejnych zadań. Pierwsze, „prawdziwe” zadanie pojawi się już jutro. Oczywiście proszę także o komentarze do mojego rozwiązania, oraz przesyłania własnych. W końcu i tak celem jest to, żebyśmy byli coraz lepszymi programistami, prawda?
Warto przeczytać.
Trwa ładowanie…
Dane wejściowe są bardzo małe, więc każde rozwiązanie zwracając poprawny wynik będzie zaakceptowane, nawet Twoje kwadratowe O(n^2) ;) (w konkursie to już nie przejdzie, n log n też nie). Zadanie można zrobić w czasie liniowym O(n+m): wystarczy zauważyć że zakres wartości jest bardzo mały 1 -15000, więc robimy wersje „sortowania przez zliczanie”, tablica 15 tys elementów bool oznaczająca czy dana liczba już była czas O(n), a potem zliczamy ile mamy falsów O(m) i gotowe ;).
W międzyczasie, na spokojnie, naskrobałem bardziej optymalną wersję:
#include <cstdio> #include <map> int main(int argc, char **argv) { int rows = 0; int num = 0; scanf("%i", &rows); std::map<int, int> values = std::map<int, int>(); for(int i = 0; i < rows; i++) { scanf("%i", &num); values[num]++; } printf("%i", 15000 - values.size()); return 0; }Wydaje mi się, że to będzie O(n) – bo zliczanie to już O(1) – std::map zapisuje sobie ilość elementów w „międzyczasie”. Jeśli możesz – zamieścisz swój kod? Możesz użyć znaczników „code” w nawiasach kwadratowych – jak coś, to ja poprawię. ;]
Potyczki Algorytmiczne 2011- Zadanie próbne „Tulipany”
W przypadku drugiego rozwiązania nie będzie to O(n), tylko O(nlgn). operator[] na mapie sięga w czasie logarytmicznym. Bardziej intuicyjny wydaje się tu kontener set (również O(nlgn), które dla małych danych powinno być szybsze od O(n)). Rozwiązanie z tablicą:
int count = 0; bool tab[15001]; for { scanf(%d, d); tab[d] = true; } for { count += tab[i] ? 0 : 1; }Ahhh… prawie zapomniałbym o potyczkach – mail informacyjny trafił do spamu na skrzynce.
Od razu coś dla Java’owców – bo co roku mamy ten sam problem – sposób na szybkie wczytanie danych do programu:
private static int readInt(InputStream in) throws IOException { int ret = 0; boolean dig = false; for (int c = 0; (c = in.read()) != -1;) { if (c >= '0' && c <= '9') { dig = true; ret = ret * 10 + c - '0'; } else if (dig) { break; } } return ret; } BufferedInputStream bis = new BufferedInputStream(System.in); int n = readInt(bis);Korzystanie ze Scannera i podobnych może i wygodniejsze, ale samo wczytanie danych przekroczy dopuszczalny limit czasowy.
Kodu nie mam bo nie startuję. Twój kod jest liniowy, tyle że mapa z której korzystasz jest logarytmiczna. Zastąp mapę zwykła tablicą ;) i nie potrzebujesz informacji ile danych liczb było, tylko czy były – czyli typ bool ;)
@eee: Masz rację – napisałem to we wpisie, a tutaj się pomyliłem. Pozwoliłem sobie nieco przeformatować i pokolorować Twój kod – mam nadzieję, że się nie obrazisz. ;]
Potyczki Algorytmiczne 2011- Zadanie próbne „Tulipany”
@eN: Wiesz co – zastąpiłem mapę tablicą i nie zauważyłem wcale różnicy. Tzn. nie zauważyłem stabilnej różnicy, bo to „lepsze” rozwiązanie dla testu 10a waha się u mnie między 0,06s a 0,11s. Spróbuję jeszcze z boolem, ale to chyba tylko optymalizacja pamięciowa. ;]
Potyczki Algorytmiczne 2011- Zadanie próbne „Tulipany”
Dzięki za komentarz – na pewno przyda się ludziom od Javy. Pozwoliłem sobie nieco podkolorować ten kod, powinno być bardziej czytelne.
Potyczki Algorytmiczne 2011- Zadanie próbne „Tulipany”
Przy kolorowaniu coś się HTML-owskie znaki popsuły. ;)
PS. Przydałaby się jakaś informacja koło pola na treść komentarza o sposobie formatowania i umieszczania kodu.
Używam wtyczki SyntaxHighlighter Evolved, a więc formatowanie odbywa się przez swojego rodzaju „BBCode” – znacznik „code” z parametrem „lang” i odpowiednią nazwą języka – java, php, cpp. Np.: {code lang=”php”}{/code} – oczywiście w nawiasach kwadratowych.
Aczkolwiek masz rację – powinna być taka informacja i w wolnej chwili postaram się coś takiego zrobić.
Potyczki Algorytmiczne 2011- Zadanie próbne „Tulipany”