Wersja dla języka C++

System oceniania na OIJ jest całkowicie zautomatyzowany. Z użyciem takiego systemu wiążą się niewątpliwe zalety: szybkość, zaufanie, jednolitość kryteriów i wiele innych, jak również kilka wad. Jedną z nich jest bardzo “brutalna” ocena rozwiązań zawierających choćby drobne pomyłki. Intencją tego poradnika jest wytłumaczyć Ci w jaki sposób przygotować swoje rozwiązania, aby uzyskana punktacja odzwierciedlała poprawność pomysłu i umiejętności implementacyjne. Poradnik przestrzega przed niektórymi popularnymi pułapkami i problemami, rekomendujemy jego przeczytanie w całości.

Rozwiążemy przykładowe zadanie Liczby Fibonacciego. Treść zadania można zobaczyć tutaj. Zadanie, choć niezbyt ciekawe, ma wiele poprawnych i niepoprawnych rozwiązań. Jeszcze raz zachęcamy wszystkich Czytelników do przejrzenia dokumentu w całości, nawet jeżeli potrafią z łatwością rozwiązać to zadanie.

Po zrozumieniu tego samouczka, rekomendujemy przygotowanie własnego rozwiązania i wysłanie go do systemu SIO2. W konkursie testowym znajduje się jeszcze kilka innych, bardzo prostych zadań. Rozwiązanie zadań w konkursie testowym nie jest wymagane, nie przynosi też żadnych punktów do kwalifikacji do kolejnych etapów zawodów, ale pozwala upewnić się, że reguły zawodów i sposobu oceny są jasne.

Wyjaśnienie zadania

Przykładowe zadanie polega na wczytaniu liczby naturalnej określającej numer elementu ciągu Fibonacciego, obliczeniu tego elementu i jego wypisaniu.

Dane należy wczytywać ze standardowego wejścia (można założyć, że z klawiatury), a odpowiedź wypisać na standardowe wyjście (można założyć, że na ekran). Innymi słowy można użyć na przykład strumieni (std::cin oraz std::cout). Można też używać funkcji z biblioteki cstdio (scanf oraz printf), albo jeszcze innych: na~przykład gets/puts. Tak naprawdę, system sprawdzający na standardowe wejście podaje plik, a odpowiedź programu zapisywana jest do pliku, ale cały proces jest dla programu niezauważalny, więc zawodnik nie musi się tym przejmować.

Program powinien zatem oczekiwać na wczytanie jednej liczby i jeśli otrzyma na przykład 4, oznacza to, że powinien obliczyć czwartą liczbę Fibonacciego. Liczby Fibonacciego numer 0 i 1 to jedynki, a każda kolejna jest sumą dwóch poprzednich, więc na przykład druga liczba Fibonacciego to 1+1 = 2, trzecia to 2+1 = 3, a czwarta to 3+2 = 5. Program powinien więc dla danej 4 wprowadzonej z klawiatury (pytanie o czwartą liczbę Fibonacciego) wypisać na ekran wynik 5 (ile ona wynosi).

W sekcji Wejście napisano, że liczba będzie naturalna z zakresu od 0 do 80 włącznie. To znaczy, że program ma działać nie tylko dla danej 4, ale również 0, 1, 17, 35, 80 i wielu innych. Przykładowo, jeśli do programu wprowadzimy 0, powinien on wypisać 1, a jeśli wprowadzimy 80 powinien wypisać 37889062373143906. W każdym zadaniu olimpijskim można założyć, że dane będą zgodne ze specyfikacją. W przypadku tego zadania nikt zatem nie będzie wprowadzał do Twojego programu danej 1.5, ani alamakota, nie ma więc konieczności sprawdzania tego. Nie jest też ważne, jak zachowałby się program w takiej sytuacji.

Możliwe błędy

Przeanalizujemy najpierw kilka nieprawidłowych rozwiązań zadania.

Nieprawidłowy algorytm

Spróbujmy napisać pierwszy program:

#include <bits/stdc++.h>
using namespace std;

int main() {
  int n;

  cin >> n;  // test przykładowy: 4

  cout << n + 1 << "\n";   // odpowiedź poprawna: 5

  return 0;
}

Program działa prawidłowo dla danych przykładowych z treści zadania, tzn. po wprowadzeniu do programu liczby 4 uzyskamy 5, czyli dokładnie tak jak powinno być.

Po wysłaniu zgłoszenia do SIO2, uzyskamy następujący raport z oceny rozwiązania: SIO2 - wstępna ocena Jest to jedynie raport z oceny na testach przykładowych - w tym przypadku jedynie na teście 4 z treści zadania. Skoro program prawidłowo wypisuje dla tych danych wynik 5, otrzymuje status OK.

Nie oznacza to jednak, że zadanie zostało rozwiązane prawidłowo. Po kliknięciu przycisku Odsłoń wynik, naszym oczom ukazuje się: SIO2 - odsłoniony wynik oceny A zatem zgłoszenie zostało ocenione na 0 punktów.

Po zawodach, gdy wysyłanie zgłoszeń nie będzie już możliwe, zawodnik uzyskuje dostęp do pełnego raportu oceny rozwiązania: SIO2 - pełny raport Dla większości testów został ustalony werdykt Błędna odpowiedź.

Uwaga: Na zawodach właściwych (tzn. wszystkich turach wszystkich etapów) zawodów OIJ tak szczegółowy raport udostępniany jest zawodnikom dopiero po zawodach. W trakcie zawodów widoczny jest jedynie wynik sprawdzenia na testach przykładowych, a wynik punktowy zgłoszenia nie jest jawny. Jedynie podczas tury “otwartej” pierwszego etapu możliwe jest odsłonięcie wyniku niektórych nadesłanych zgłoszeń (należy pamiętać, że wynik zgłoszeń na danym zadaniu można odsłonić tylko ustaloną liczbę razy, zgodnie z Zasadami organizacji zawodów). Odsłonięcie wyniku ujawnia jedynie liczbę punktów, a pełny, szczególowy raport z oceny nadal jest tajny, nawet podczas tury “otwartej” pierwszego etapu.

Program jest niepoprawny, bo nie jest prawdą, że N-ta liczba Fibonacciego jest zawsze równa N+1. Na przykład, dla danej 5, program wypisze 6, a powinien wypisać 8.

Zadaniem każdego zawodnika na Olimpiadzie jest zawsze sprawdzić działanie programu na różnych, również złośliwych danych testowych. Jurorzy dokładają wszelkich starań, żeby możliwie najpełniej testować poprawność i wydajność programów zawodników, można więc śmiało założyć, że w zbiorze danych testowych zawarto różne testy, także inne niż 4.

Można zapytać, dlaczego program uzyskał 0 punktów, chociaż nawet na SIO2 zaliczone zostały dwa testy: jeden przykładowy i jeden punktowany (program zadziałał poprawnie jeszcze dla N = 0)? Czy nie powinniśmy uzyskać chociaż kilku punktów, skoro program czasami działa, a czasami nie? Wszystkiemu winne jest grupowanie testów: każdej grupie testów (składającej się z jednego lub wielu różnych możliwych pojedynczych testów, czyli danych do programu) przypisana jest pewna liczba punktów. Jeśli ocenimy każdy test z osobna, wynik za całą grupę obliczany jest jako minimum z całej grupy testów: jeśli więc program nie zaliczy choćby jednego testu z danej grupy, otrzyma za nią 0 punktów. Testy z tej samej grupy mają ten sam numer, ale mają dopisaną literkę (np. testy 2a i 2b należą do tej samej grupy testów, a test 3 stanowi sam osobną grupę).

Każde zadanie na Olimpiadzie można spróbować rozwiązać wiele razy (limit podany jest w Zasadach organizacji zawodów). Podczas zawodów I stopnia wynikiem za zadanie uznaje się maksimum pośród wszystkich wyników z wysłanych zgłoszeń dla danego zadania.

Błąd kompilacji

Zmieniamy więc pomysł i implementujemy następujące rozwiązanie:

#include <bits/stdc++.h>
using namespace std;

int main() {
  int n;
  long long fib_poprz = 1, fib_akt = 1;

  cin >> n;
  for (int i = 2; i <= n; i++) {
    long long fib_nast = fib_poprz + fib_akt   // zapomniany średnik
    fib_poprz = fib_akt;
    fib_akt = fib_nast;
  }

  cout << fib_akt << "\n";

  return 0;
}

Po wysłaniu rozwiązania do systemu SIO2 uzyskamy werdykt Błąd kompilacji. Rzeczywiście, takiego programu nie można uruchomić, bo się nie skompiluje z powodu zapomnianego średnika. Niestety, pomimo tego, że od rozwiązania poprawnego dzieli nas tylko jeden znak, nadal ten kod wart jest 0 punktów. Co więcej, skoro programu nie udało się skompilować, nie został nawet uruchomiony na testach. SIO2 wyświetla jedynie błąd kompilacji, który pomaga zidentyfikować i naprawić problem.

SIO2 - wynik zgłoszenia (błąd kompilacji)

Wysłanie programu wykonywalnego zamiast kodu źródłowego

Powiedzmy, że naprawiliśmy program i umieśliliśmy go w pliku program.cpp, ale podczas wysyłki do SIO2, wyślemy plik wykonywalny program.exe. Wówczas SIO2 nie przyjmie takiego rozwiązania do oceny. Z kilku powodów:

  • do oceny uwzględniane są kody źródłowe, a nie programy wykonywalne,
  • program musi być napisany w jednym z dostępnych języków programowania i kompilować się z flagami podanymi w Zasadach organizacji zawodów i Ustaleniach technicznych,
  • jest limit długości wysyłanego kodu (a bardzo możliwe, że program wykonywalny ten limit przekracza).

SIO2 - błąd wysłania zgłoszenia

Dodatkowe komunikaty

No dobrze, postanawiamy w końcu wysłać właściwy program, ale zanim to nastąpi, przyszło nam do głowy “upiększyć” go o dodatkowe komunikaty pomagające zrozumieć co robi program i co należy do niego wprowadzić.

#include <bits/stdc++.h>
using namespace std;

int main() {
  int n;
  long long fib_poprz = 1, fib_akt = 1;

  cout << "Podaj n: ";   // błąd: ładne komentarze spowodują błędną odpowiedź
  cin >> n;

  cout << "Obliczam...\n";
  for (int i = 2; i <= n; i++) {
    long long fib_nast = fib_poprz + fib_akt;
    fib_poprz = fib_akt;
    fib_akt = fib_nast;
  }

  cout << "Wynik to: " << fib_akt << "\n";

  system("PAUSE");   // błąd: wywołanie system jest niedozwolone

  return 0;
}

Taki program wysłany na SIO2 otrzyma na wszystkich testach werdykt Błędna odpowiedź, Błąd wykonania lub Naruszenie bezpieczeństwa.

Po pierwsze: wywołanie funkcji system potencjalnie mogłoby uruchomić inny program, co mogłoby być niebezpieczne dla systemu sprawdzającego: np. system("rm -Rf /");.

Po drugie: program powinien wypisywać informacje dokładnie w takim formacie jak podano w sekcji Wyjście. System sprawdzający nie jest człowiekiem, nie rozumie ludzkich komunikatów i jedynie stwierdza, że zamiast wypisać odpowiedź 5 dla testu przykładowego, program wypisuje Podaj n: i serię dodatkowych komunikatów. Należy zawsze pamiętać, żeby dane były wypisane w formacie zgodnym z sekcją Wyjście.

Zły typ danych

Dobrze, wycofujemy się z pomysłu upiększania programu i wysyłamy na SIO2 następujący program:

#include <bits/stdc++.h>
using namespace std;

int main() {
  int n, fib_poprz = 1, fib_akt = 1;  // błąd: 80-ta liczba Fibonacciego jest za duża na typ int

  cin >> n;
  for (int i = 2; i <= n; i++) {
    int fib_nast = fib_poprz + fib_akt;
    fib_poprz = fib_akt;
    fib_akt = fib_nast;
  }

  cout << fib_akt << "\n";

  return 0;
}

Program ten w końcu otrzymuje punkty! Ale niestety, nie uzyskuje maksimum. Zmienna typu int pozwala przechowywać jedynie liczby całkowite z określonego zakresu (od około minus dwóch miliardów do około plus dwóch miliardów). 80-ta liczba Fibonacciego znacznie przekracza dwa miliardy. Po przekroczeniu zakresu działanie programu jest nieokreślone (zwykle zmienna osiąga ujemne wartości).

Błąd można wykryć samemu: wystarczy wprowadzić do programu daną 46. Najprawdopodobniej uzyskamy -1323752223, co z pewnością nie jest poprawną odpowiedzią. Aby to naprawić, należy skorzystać z typu danych, który pozwala przechowywać liczby większe, na przykład long long. Zakres zmiennej tego typu również jest ograniczony, jednak jest większy (zmienne long long zwykle pozwalają przechowywać liczby do 18 cyfr włącznie).

SIO2 - wynik zgłoszenia (70 punktów)

Program uzyskał 70 punktów, dlatego że jeśli N podane na wejściu nie przekracza 45, program działa prawidłowo. W treści zadania zaś w sekcji Ocenianie napisane było, że testy warte 70% punktów mają N właśnie do 45. W zadaniach trudniejszych, często możliwe jest na przykład wymyślenie algorytmu poprawnego, ale działającego zbyt wolno na dużych danych. Z sekcji Ocenianie często możliwe jest wywnioskowanie ile punktów otrzyma takie rozwiązanie. Zazwyczaj poprawne, ale za wolne rozwiązania otrzymują pewną, dodatnią liczbę punktów. Intencją Jury jest natomiast, żeby rozwiązania algorytmicznie błędne otrzymywały bardzo mało punktów (często nawet 0).

Za dużo zużytej pamięci

Przyszedł nam do głowy dowcip. Do poprawnego rozwiązania, dla hecy dodajemy ogromną tablicę miliarda zmiennych typu int:

#include <bits/stdc++.h>
using namespace std;

int main() {
  int n;
  long long fib_poprz = 1, fib_akt = 1;

  int tabliczka[1000000000];  // haha, tablica dla szpanu!

  cin >> n;
  for (int i = 2; i <= n; i++) {
    long long fib_nast = fib_poprz + fib_akt;
    fib_poprz = fib_akt;
    fib_akt = fib_nast;
  }

  cout << fib_akt << "\n";

  return 0;
}

Taki program wysłany na SIO2 otrzyma 0 punktów i werdykt Błąd wykonania lub Przekroczenie limitu pamięci. W każdym zadaniu olimpijskim podany jest limit pamięci. Jeśli program przekroczy ów limit, jego wykonanie na systemie sprawdzającym jest przerywane. Nie zawsze jest oczywiste, czy program został przerwany z powodu przekroczenia limitu pamięci czy z powodu innego błędu (na przykład naruszenia bezpieczeństwa, złego dostępu do pamięci czy dzielenia przez zero), dlatego czasami przekroczenie limitu pamięci może być sygnalizowane również jako Błąd wykonania. Niezależnie od kwalifikacji błędu, program, który nie uzyskuje statusu OK otrzymuje 0 punktów za dany test. Ponieważ tablica jest tworzona niezależnie od danych wejściowych, program przekracza pamięć na każdym teście, więc nie zdobywa żadnych punktów.

Może się również zdarzyć, że program otrzyma status Błąd kompilacji (kompilator na etapie kompilacji może zaraportować błąd, że tablica jest zbyt duża).

Tablica stworzona w programie zajmowałaby 4 GB pamięci (jeden int zajmuje zwykle 4 bajty pamięci, tworzonych było miliard takich zmiennych).

W tym przypadku tworzona tablica nie miała żadnego sensu. Czasami jednak, gdy nasz pomysł zużywa zbyt wiele pamięci w najgorszym przypadku, dobrze jest zrobić mniejszą tablicę, być może za małą dla niektórych danych, ale tak, aby program był w stanie chociaż dla małych danych zadziałać poprawnie i mieszcząc się w limitach. W ten sposób ma szansę uzyskać częściowe punkty za działanie programu dla mniejszych danych zgodnie z sekcją Ocenianie z treści zadania.

Niedeterministyczny program

A gdyby napisać taki (prawie poprawny) program?

#include <bits/stdc++.h>
using namespace std;

int main() {
  int n;
  long long fib_poprz = 1, fib_akt = 1;

  cin >> n;
  for (int i; i <= n-2; i++) {  // początkowa wartość zmiennej i jest nieustalona
    long long fib_nast = fib_poprz + fib_akt;
    fib_poprz = fib_akt;
    fib_akt = fib_nast;
  }

  cout << fib_akt << "\n";

  return 0;
}

Trudno przewidzieć jaki wynik zwróci program. Jeśli wartość zmiennej i po utworzeniu będzie równa 0, program zadziała prawidłowo. Jeśli jednak będzie inaczej pętla może wykonać się zbyt wiele razy lub zbyt mało razy, prowadząc do uzyskania werdyktu Błędna odpowiedź lub Limit czasu przekroczony. Działanie programu jest nieustalone, bowiem standard języka C++ nie precyzuje jaką wartość przyjmie niezainicjalizowana zmienna. Może się okazać, że na Twoim komputerze program będzie działał prawidłowo (bo akurat tak się złoży, że zmienna otrzyma prawidłową wartość), a na SIO2 nie (bo zmienna uzyska tam inną wartość początkową). Wiążący dla końcowego wyniku zawodnika jest oczywiście wynik oceny na SIO2. Należy pisać programy w taki sposób, żeby działały poprawnie zawsze, niezależnie od tego, na jakim komputerze zostaną uruchomione. W przeciwnym razie zawodnik naraża się na niebezpieczeństwo uzyskania niskiej punktacji (czasami, lub wręcz często) 0 punktów.

Powyższy przykład pokazuje również, że u mnie działa nie jest argumentem, że program jest poprawny. Dobrze napisany program działa zawsze, a źle napisany być może działa czasami. Na Olimpiadzie wysoką punktację uzyskują programy poprawne i szybkie.

Uruchomienie próbne

Z pomocą na przypadki podobne do tego wyżej przychodzi funkcja Uruchomienie próbne. Pozwala ona uruchomić program w środowisku SIO2 dla własnych danych i przejrzenie wyniku jaki zwróci program oraz sprawdzenie jego czasu wykonania (zgodnie z narzędziem ojejq).

Uruchomienie próbne NIE jest tym samym co wysłanie zgłoszenia:

  • dla uruchomień próbnych jest zupełnie osobny limit programów, które można nadesłać niż limit zgłoszeń,
  • program uruchamiany jest na dokładnie takich danych jak wysłane przez zawodnika, a nie na danych przygotowanych przez organizatorów,
  • możliwe jest pobranie odpowiedzi, którą zwrócił program na wyjście,
  • NIE jest sprawdzana ani poprawność nadesłanych danych wejściowych do programu, ani NIE jest sprawdzana poprawność odpowiedzi programu dla tych danych,
  • sprawdzane jest natomiast czy program się kompiluje, czy zmieścił się w limicie czasu, limicie pamięci, czy nie nastąpił błąd wykonania podczas działania, czy nie nastąpiło naruszenie reguł uruchomienia.

Celem uruchomienia próbnego jest umożliwić zawodnikowi sprawdzenie czy program działa w warunkach SIO2 tak samo jak na maszynie zawodnika. Jeśli zawodnik zna prawidłową odpowiedź dla nadesłanych przez siebie danych, może porównać czy program zadziałał zgodnie z oczekiwaniami. Funkcja jest również szczególnie przydatna, gdy zawodnik martwi się o limit zgłoszeń, gdyż nadesłane uruchomienia próbne nie są wliczane do tego limitu. Należy jednak pamiętać, aby nie pomylić tej funkcji z właściwym wysłaniem rozwiązania do oceny. Aby zgłoszenie było brane pod uwagę przy obliczaniu wyniku zawodnika, należy je zgłosić używając opcji Wyślij w SIO2.

Drobny błąd w przypadku brzegowym

Poprawiamy program i nieco go zmieniamy:

#include <bits/stdc++.h>
using namespace std;

int main() {
  int n;
  long long fib_poprz = 1, fib_akt = 1;

  cin >> n;
  int i = 1;
  do {
    long long fib_nast = fib_poprz + fib_akt;
    fib_poprz = fib_akt;
    fib_akt = fib_nast;   // błąd! wykona się zawsze, nawet gdy n jest 0 lub 1
    i++;
  } while (i < n);

  cout << fib_akt << "\n";

  return 0;
}

Program nie zalicza jedynie dwóch testów: tych, w których N jest równe 0 lub N jest równe 1. Spowodowane jest to błędem implementacyjnym. Pętla wykona się zawsze co najmniej raz, a zatem zwrócony wynik jest co najmniej 2. Należy pamiętać o testowaniu programu na różnych danych: również tych minimalnego rozmiaru.

Rozwiązanie rekurencyjne: zbyt wolne

Zanim poprawiliśmy błąd, wpadł nam do głowy pomysł użycia rekurencji: skoro liczby Fibonacciego zdefiniowane są rekurencyjnie, może warto napisać program bezpośrednio z definicji?

#include <bits/stdc++.h>
using namespace std;

long long fib(int n) {
  if (n == 0)
    return 1;
  if (n == 1)
    return 1;
  return fib(n-1) + fib(n-2);  // będzie wolne
}

int main() {
  int n;

  cin >> n;
  cout << fib(n) << "\n";

  return 0;
}

Program otrzymuje jedynie 50 punktów i na niektórych testach uzyskuje werdykt Limit czasu przekroczony. Oznacza to, że jest zbyt wolny (lub się zapętla).

Rzeczywiście, program uruchomiony dla danej 10 zwraca wynik bardzo szybko, jednak już na przykład dla danej 40 obliczenie wyniku trwa kilka sekund, a dla danych większych może to trwać kilka godzin, dni lub jeszcze dłużej.

Limit czasu w większości zadań olimpijskich waha się w przedziale od pół sekundy do kilku(nastu) sekund. W tym zadaniu limit wynosi pół sekundy, chociaż nawet jeśli byłby nieco większy, program i tak nie miałby żadnych szans na ukończenie działania dla wejścia 80.

Powodem wolnego działania programu jest fakt, że funkcja fib obliczana jest wielokrotnie dla tych samych parametrów. W szczególności, ponieważ program zwraca prawidłowy wynik, dla dowolnej danej N, program uruchamia funkcję fib z parametrem 0 lub 1 dokładnie tyle razy, ile wynosi N-ta liczba Fibonacciego. W końcu prawidłowy wynik ostatecznie bierze się z posumowania jedynek na końcu drzewa rekursji. A ponieważ zauważyliśmy już wcześniej, że wynik może być bardzo duży, wiemy dlaczego powyższy program jest bardzo wolny.

Program radzi sobie dobrze dla danych, w których wynik jest nieduży (rzędu kilku milionów). Dla N nie przekraczających 30, czas wykonania jest jeszcze niezauważalny. Dlatego, zgodnie z sekcją Ocenianie, program uzyskał połowę maksymalnej liczby punktów.

Rozwiązanie poprawne

Zapominamy o powyższym rozwiązaniu i w końcu wysyłamy do systemu poprawiony kod źródłowy jednej z pierwszych wersji programu:

#include <bits/stdc++.h>
using namespace std;

int main() {
  int n;
  long long fib_poprz = 1, fib_akt = 1;

  cin >> n;
  for (int i = 2; i <= n; i++) {
    long long fib_nast = fib_poprz + fib_akt;
    fib_poprz = fib_akt;
    fib_akt = fib_nast;
  }

  cout << fib_akt << "\n";

  return 0;
}

i otrzymujemy upragnione 100 punktów.

Uwaga! Jedynie podczas tury “otwartej” pierwszego etapu OIJ zawodnicy mogą odsłonić pełny wynik niektórych swoich zgłoszeń. W turze “ukrytej” pierwszego etapu oraz w drugim i trzecim etapie zawodów, zawodnik po nadesłaniu widzi jedynie wynik testów przykładowych podanych w treści zadania (oraz być może kilku dodatkowych testów) wartych 0 punktów.

Pełny wynik oceny zgłoszenia będzie znany dopiero po zawodach, gdy nie będzie już możliwości zmieniania programu. Dlatego tak ważne jest testowanie programu samodzielnie na różnych (również bardzo złośliwych) danych. Inaczej może się okazać, jak w przypadku niektórych wcześniej pokazanych programów, że SIO2 pokaże werdykt OK na testach przykładowych, ale właściwy wynik na punktowanych testach niejawnych będzie mizerny.

Inne rozwiązanie poprawne: z użyciem tablicy

A co jeśli przyszłoby nam do głowy jednak użyć tablicy?

// również poprawne rozwiązanie z tablicą (nie za dużą)

#include <bits/stdc++.h>
using namespace std;

long long fib[90];

int main() {
  int n;

  cin >> n;

  fib[0] = 1;
  fib[1] = 1;
  for (int i = 2; i <= n; i++)
    fib[i] = fib[i-1] + fib[i-2];

  cout << fib[n] << "\n";

  return 0;
}

Okazuje się, że takie rozwiązanie również zostanie zaakceptowane pomimo nieco większego zużycia pamięci niż rozwiązanie poprzednie. Tak długo jak program mieści się w limitach podanych w treści zadania na wszystkich danych, tak długo otrzymuje maksymalną punktację. Nie jest zatem potrzebne przesadne optymalizowanie programu. Zwykle kluczem do zaakceptowania zadania nie jest przeoptymalizowana implementacja, lecz poprawny algorytm, bez błędów implementacyjnych, który ma optymalną (lub bliską optymalnej) złożoność obliczeniową i pamięciową. Więcej na ten temat można przeczytać na przykład tutaj.

Inne rozwiązanie poprawne: bardzo skrócony kod

Z tych samych powodów poniższy program:

// krótki i brzydki, ale poprawny kod
#include <iostream>
typedef long long L;int main(){int n;L f,g,h;f=g=1;std::cin>>n;for(int i=1;i<n;i++){h=f+g;f=g;g=h;}std::cout<<g<<"\n";return 0;}

również uzyska 100 punktów, ani mniej, ani więcej. Miarą oceny programu na olimpiadzie jest poprawność, szybkość działania i zużycie pamięci, a nie długość kodu źródłowego. A zatem nie ma żadnego bonusa za krótsze kody programów. Nie jest też prawdą, że program krótszy jest zawsze szybszy niż dłuższy, więc przesadne skracanie kodu mija się z celem. Jedyne, o czym należy pamiętać to limit długości kodu źródłowego, ale ten jest na tyle długi, że w praktyce niemożliwym jest jego przekroczenie. Wyjątkiem byłaby sytuacja, gdy kod programu miałby być generowany automatycznie, zamiast być pisany ręcznie.

Przykład powyżej również pokazuje, że ocenie nie podlega styl implementacji i przestrzeganie dobrych reguł pisania kodu. Automatyczny system sprawdzający ignoruje treść kodu, od razu przechodząc do jego kompilacji i uruchomienia na zbiorze wcześniej przygotowanych danych. Mimo to jednak, polecamy pisać programy rozsądnie. Z reguły programy napisane brzydko, z rażącym naruszeniem dobrych praktyk, są również niepoprawne lub wolne.

Inne rozwiązanie poprawne: rozwiązanie rekurencyjne ze spamiętywaniem

Możliwe jest również poprawienie rozwiązania rekurencyjnego, które wcześniej było za wolne, poprzez dodanie spamiętywania:

#include <bits/stdc++.h>
using namespace std;

long long obliczone[90];  // jeśli 0 to znaczy, że jeszcze nieobliczone

long long fib(int n) {
  if (n == 0)
    return 1;
  if (n == 1)
    return 1;
  if (obliczone[n] == 0)   // obliczamy tylko jeśli wcześniej nie obliczyliśmy
    obliczone[n] = fib(n-1) + fib(n-2);   // spamiętaj wynik w tablicy
  return obliczone[n];
}

int main() {
  int n;

  cin >> n;
  cout << fib(n) << "\n";

  return 0;
}

Funkcja fib najpierw sprawdza czy nie została wcześniej uruchomiona dla tej danej. Jeśli tak, korzysta z wcześniej obliczonego i zapamiętanego wyniku. Dzięki temu, każdy wynik obliczany jest jedynie raz, za pierwszym razem gdy jest potrzebny. Program działa tylko nieznacznie wolniej od poprzednich i również uzyskuje maksymalną punktację.

Inne rozwiązanie poprawne: rozwiązanie z arytmetyką dużych liczb

Typ long long choć ma zakres większy niż int, również ma swoje ograniczenia. Na przykład, próbując wprowadzić do programu 92, również prawdopodobnie uzyskamy nieprawidłowy, ujemny wynik. Istnieje możliwość obejścia tych ograniczeń poprzez implementację własnej arytmetyki dużych liczb - algorytmu dodawania pisemnego:

#include <bits/stdc++.h>
using namespace std;

string Dodaj(string a, string b) {
  // funkcja dodaje dwie liczby zapisane w systemie dziesiątkowym jako napisy
  // i zwraca wynik jako napis

  reverse(a.begin(), a.end());  // odwróć cyfry liczb
  reverse(b.begin(), b.end());

  while (a.length() < b.length())
    a.push_back('0');   // dopełnij zerami
  a.push_back('0');  // dodaj jedno zero (na ewentualne przepełnienie)
  b.push_back('0');

  for (int i = 0; i < a.length()-1; i++) {
    a[i] += b[i] - '0';
    if (a[i] > '9') {
      a[i + 1]++;
      a[i] -= 10;
    }
  }

  if (a.back() == '0')  // jeśli nie było przepełnienia, pozbądź się zera
    a.pop_back();

  reverse(a.begin(), a.end());  // odwróć zapis do normalnej kolejności cyfr

  return a;
}

int main() {
  int n;
  string fib_poprz("1"), fib_akt("1");

  cin >> n;
  for (int i = 2; i <= n; i++) {
    string fib_nast = Dodaj(fib_poprz, fib_akt);
    fib_poprz = fib_akt;
    fib_akt = fib_nast;
  }

  cout << fib_akt << "\n";

  return 0;
}

Rozwiązanie również uzyskuje maksymalną punktację i działa nawet dla danych znacznie przekraczających ograniczenia podane w treści zadania. Limity są jednak zwykle dobierane, aby ta sztuczka nie była konieczna (choć nie jest to regułą). Tak było i w przypadku tego zadania.

Inne rozwiązanie poprawne: szybkie potęgowanie macierzy

Ten fragment radzimy traktować jako dodatkowy, jedynie dla zainteresowanych Czytelników.

Z użyciem zaawansowanych metod algebry liniowej, możliwe jest rozwiązanie zadania istotnie szybciej:

// zaawansowane rozwiązanie z potęgowaniem macierzy

#include <bits/stdc++.h>
using namespace std;

struct Macierz22 {
  long long a, b, c, d;

  Macierz22(long long a, long long b, long long c, long long d):
    a(a), b(b), c(c), d(d) {}

  Macierz22 operator*(const Macierz22& r) const {
    long long ra = a * r.a + b * r.c;
    long long rb = a * r.b + b * r.d;
    long long rc = c * r.a + d * r.c;
    long long rd = c * r.b + d * r.d;
    return Macierz22(ra, rb, rc, rd);
  }

  Macierz22 potega(int n) {  // działa w czasie O(log n)
    if (n == 0)
      return Macierz22(1, 0, 0, 1);

    Macierz22 h = potega(n / 2);

    if (n % 2 == 0)
      return h * h;
    return (*this) * h * h;
  }
};

int main() {
  int n;

  cin >> n;

  // potęgujemy macierz
  // (1 1)
  // (1 0)
  // do potęgi n
  Macierz22 w = Macierz22(1, 1, 1, 0).potega(n);

  // i odczytujemy lewy górny róg macierzy
  cout << w.a << "\n";

  return 0;
}

Rozwiązanie jest istotnie lepsze, bo wykonuje jedynie O(log N) iteracji, zamiast O(N) we wszystkich wcześniejszych zaakceptowanych rozwiązaniach. W naszym zadaniu implementacja takiego rozwiązania nie ma żadnego sensu, bo zanim rozwiązanie pokaże swoją wyższość nad innymi, N będzie musiało być ogromne, a zyski z implementacji szybszego rozwiązania, skonsumowane będą przez konieczność implementacji bardziej złożonych operacji na dużych liczbach (między innymi mnożenia).

Czasami jednak takie rozwiązanie miałoby sens - na przykład, gdyby zapytać o ostatnich dziewięć cyfr N-tej liczby Fibonacciego w zapisie dziesiętnym. Każde obliczenie można wtedy wykonać modulo miliard (w C++ używamy do tego operatora %), co pozwala uniknąć konieczności implementowania arytmetyki dużych liczb. Wtedy program można uruchomić dla N nawet rzędu tryliona, a program błyskawicznie obliczy odpowiedź, podczas gdy pozostałe programy nie zakończyłyby pracy w ciągu kilku dni.

Więcej informacji o tym sposobie rozwiązania można uzyskać tutaj. Warto podkreślić, że ta tematyka znacznie wykracza poza program OIJ. Stosowanie jej nie jest zakazane, jednak organizatorzy dobierając zadania będą starali się, aby stosowanie tej sztuczki nie było konieczne.

Inne rozwiązanie poprawne: spamiętane liczby Fibonacciego w kodzie programu

Do rozwiązania można podejść jeszcze inaczej. Skoro N jest liczbą naturalną między 0 a 80, to istnieje zaledwie 81 możliwych przypadków do rozpatrzenia. Ciąg Fibonacciego jest bardzo znany, więc znalezienie go w encyklopedii ciągów liczbowych nie jest zbyt trudne. Równie łatwe jest odszukanie tabeli pierwszych dwóch tysięcy elementów ciągu Fibonacciego. Możliwe jest więc napisanie następującego programu:

#include <bits/stdc++.h>
using namespace std;

// ciąg znaleziony na https://oeis.org/A000045/b000045.txt
const long long WYNIKI[] = {
  1,1,2,3,5,8,13,21,34,55,89,144,233,377,610,987,1597,2584,4181,6765,10946,
  17711,28657,46368,75025,121393,196418,317811,514229,832040,1346269,2178309,
  3524578,5702887,9227465,14930352,24157817,39088169,63245986,102334155,
  165580141,267914296,433494437,701408733,1134903170,1836311903,2971215073,
  4807526976,7778742049,12586269025,20365011074,32951280099,53316291173,
  86267571272,139583862445,225851433717,365435296162,591286729879,956722026041,
  1548008755920,2504730781961,4052739537881,6557470319842,10610209857723,
  17167680177565,27777890035288,44945570212853,72723460248141,117669030460994,
  190392490709135,308061521170129,498454011879264,806515533049393,
  1304969544928657,2111485077978050,3416454622906707,5527939700884757,
  8944394323791464,14472334024676221,23416728348467685,37889062373143906 };

int main() {
  int n;
  cin >> n;
  cout << WYNIKI[n] << "\n";

  return 0;
}

Program ma zapisane wyniki dla wszystkich możliwych testów, jakie są dozwolone w zadaniu (patrz sekcja Wejście). Tak długo, jak długość kodu nie przekracza limitu długości kodu podanego w Zasadach Organizacji Zawodów, tak długo jest to rozwiązanie dopuszczalne.

Umieszczenie komentarza, skąd pochodzi ciąg jest wskazane. Wyobraźmy sobie, że więcej niż jeden zawodnik wpadł na ten pomysł. Organizatorzy weryfikują na różne sposoby, czy rozwiązania zawodników są samodzielne. Jeśli dwa kody są podobne, może (choć nie musi) to oznaczać, że zawodnicy współpracowali. Dlatego, zgodnie z Regulaminem zawodów, użycie kawałków programów dostępnych w internecie, książkach lub innych publicznych źródłach jest dozwolone, pod warunkiem podania źródła. Należy pamiętać jednocześnie, że niesamodzielna praca, a w szczególności (choć nie tylko) wymienianie się programami, może skutkować dyskwalifikacją zaangażowanych zawodników.

O ocenie rozwiązań

Zainteresowany Czytelnik zauważył być może pewien problem ze zaproponowanym sposobem automatycznej oceny rozwiązań. Czy poniższy program jest poprawny?

#include <bits/stdc++.h>
using namespace std;

int main() {
  int n;
  long long fib_poprz = 1, fib_akt = 1;

  cin >> n;

  if (n == 67) {
    // dla zabawy, ciekawe czy testy to wykryją...
    cout << "HAHAHA!\n";
    return 0;
  }

  for (int i = 2; i <= n; i++) {
    long long fib_nast = fib_poprz + fib_akt;
    fib_poprz = fib_akt;
    fib_akt = fib_nast;
  }

  cout << fib_akt << "\n";

  return 0;
}

Rzecz jasna, jeśli wprowadzić do programu 67, program wypisze na wyjście napis HAHAHA! zamiast 67-tej liczby Fibonacciego, co oczywiście nie jest poprawnym wynikiem. Jeśli jednak 67 nie zostało umieszczone w zbiorze danych testowych, błąd nie zostanie wykryty. Oznacza to, że jest możliwe, że takie rozwiązanie uzyska 100 punktów i werdykt OK na wszystkich testach. Mimo wszelkich starań, pokrycie testami wszystkich możliwych przypadków jest często niewykonalne (zbyt duża liczba przypadków do sprawdzenia). Smutnym faktem jest również to, że ułomność tego systemu oceny nie wynika z ograniczeń organizatorów, lecz ograniczeń samej informatyki.

System sprawdzający dba o to, żeby odpowiedź wypisana przez program zawodnika była prawidłowa. Tak długo, jak program zawodnika wypisuje prawidłową odpowiedź mieszcząc się w ustalonych limitach, otrzymuje maksymalny wynik - również jeżeli program nie wczytywałby wejścia lub odpowiedź była wylosowana albo zapisana na stałe do kodu programu.