Dwa różne - zadanie ze sparingu
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.