Dwa różne - zadanie ze sparingu

13 Feb 2014

Podczas sparingu 31 stycznia mieliście do rozwiązania zadanie "Dwa różne" (jest ono dostępne na themisie). Zanim będziecie czytać dalej przypomnijcie sobie treść zadania.

Zadanie można próbować rozwiązać w następujący sposób: sprawdźmy każdą parę liczb i znajdźmy takie dwie różne liczby, które są najdalej od siebie w ciągu. Jak zapisać takie rozwiązanie?

Załóżmy, że mamy wczytany ciąg do tablicy A i jest w nim n elementów. Schemat rozwiązania wygląda następująco:

int najlepsze = 0;

for(int i = 0; i < n; i++) {
  for(int j = i+1; j < n; j++) {
    if(A[i] != A[j] && j-i > najlepsze) {
      najlepsze = j-i;
    }
  }
}

cout << najlepsze << endl;

Sprawdźcie sami, że faktycznie j-i to odległość między elementami i-tym i j-tym (taka, jak jest opisana w treści zadania). Pytanie kontrolne: dlaczego wewnętrzną pętlę zaczynam od i+1 zamiast od 0?

Przedstawiony rozwiązanie jest poprawne - sprawdzamy wszystkie możliwości, więc musimy trafić na wynik optymalny. Jest jednak zbyt wolne! Ile operacji porównania (!=) wykonuje powyższy program przy ciągu długości n?

Popatrzmy na zewnętrzną pętlę. W pierwszym obiegu wykona ona n-1 obrotów, ponieważ wewnętrzna pętla zacznie się wykonywać od j = 1. W drugim obiegu pętli zewnętrznej (i = 1) pętla w środku wykona n-2 obrotów. I tak dalej.

Zatem liczba wykonanych operacji to (n-1) + (n-2) + (n-3) + ... + 1 =~ n^2. Ostatni znak oznacza, że wynikiem tej sumy jest mniej więcej n^2 (w jakim sensie mniej więcej dowiecie się na zajęciach).

Patrząc na limity zadania n może być równe nawet 500000, co daje około 250000000000, czyli 25*10^10 operacji. Wykonanie tylu operacji będzie trwało wiele godzin, więc na themisie dostaniemy TLE (time limit exceeded - przekroczono limit czasu).

Rozwiązanie optymalne

Na szczęście możemy wymyślić rozwiązanie szybsze, wykonujące mniej więcej n operacji. Rozważmy jakiś prosty przypadek.

Co by się stało, gdyby pierwszy i ostatni element ciągu były różne (tj. A[0] != A[n-1])? Wtedy znamy odpowiedź: nie ma bardziej odległych od siebie elementów niż elementy skrajne w ciągu. Odległość między nimi wynosi (n-1)-1 = n-2.

Co gdy skrajne elementy ciągu są sobie równe? Wtedy musimy wyszukać dwa elementy w ciągu: najwcześniejszy element różny od A[n-1] oraz najpóźniejszy element różny od A[0]. Nazwijmy je odpowiednio A[l] i A[u].

Zobaczmy to na przykładzie: jeżeli na wejściu dostaliśmy poniższy ciąg, to l = 3 i u = 6.

|3|3|3|4|5|3|7|3|3|3|
       l     u

Teraz zauważmy, że największą odległością między różnymi elementami ciągu jest odległość A[l] od A[n-1] lub A[0] od A[u]. Dlaczego?

Na pewno są to odległości optymalne jeżeli założymy, że jedną z liczb w rozwiązaniu jest A[0] albo A[n-1]. Jeżeli pokażemy, że każde rozwiązanie optymalne musi zawierać jeden z krańców ciągu, to udowodnimy poprawność algorytmu.

Załóżmy, że istnieje rozwiązanie optymalne, tj. para A[i] A[j] taka, że A[i] != A[j] i odległość między nimi jest największa możliwa. Dodatkowo załóżmy, że ani i, ani j nie są końcami ciągu (nie są równe 0 lub n-1).

Co by było, gdyby A[i-1] != A[j]? Mielibyśmy lepsze rozwiązanie, ale przecież najlepszym jest A[i] A[j]! Zatem wiemy, że A[i-1] = A[j]. Powtarzając rozumowanie otrzymujemy, że A[i-2] = A[j], A[i-3] = A[j], ..., A[0] = A[j].

Zauważmy, że analogicznie możemy zapytać, czy A[j+1] != A[i]? Tak samo jak poprzednio wiemy, że nie może tak być. Z tego wynika, że A[j+1] = A[i], A[j+2] = A[i], ..., A[n-1] = A[i].

Co nam to daje? Popatrzmy: (A[0] = A[j]) != (A[i] = A[n-1]). Wyszło, że elementy skrajne są różne! Otrzymujemy sprzeczność z tym, że A[i] A[j] było optymalnym rozwiązaniem. Podsumujmy to na diagramie:

|5|5|5|5|5|4|......|5|4|4|4|4|4|
           i        j

Gdybyśmy którąkolwiek z piątek z przodu (lub czwórek z tyłu) zamienili na inną liczbę, otrzymalibyśmy lepsze rozwiązanie (a założyliśmy, że lepszego nie ma).

Implementacja

Żeby zapisać rozwiązanie w C++ musimy potrafić znaleźć liczby A[l] i A[u] z powyższego opisu. Można to zrobić prostą pętlą.

Poza tym trzeba uważać na następujące przypadki:

  • wszystkie liczby na wejściu są takie same,
  • na wejściu jest tylko jedna liczba.