• Nie Znaleziono Wyników

Kodowanie permutacji Konkurs:

N/A
N/A
Protected

Academic year: 2021

Share "Kodowanie permutacji Konkurs:"

Copied!
11
0
0

Pełen tekst

(1)

Kodowanie permutacji

Konkurs: II Olimpiada Informatyczna Autor zadania: Krzysztof Diks Pamięć: 32 MB

http://main.edu.pl/pl/archive/oi/2/kod

Każdą permutację A = (a1, …, an) liczb 1, …, n można zakodować za pomocą ciągu B = (b1, …, bn), w którym bi jest równe liczbie wszystkich aj takich, że j < i oraz aj> ai, dla każdego i = 1, …, n.

Przykład

Kodem permutacji A = (1, 5, 2, 6, 4, 7, 3) jest ciąg B = (0, 0, 1, 0, 2, 0, 4).

Zadanie

Napisz program, który:

→ wczytuje z wejścia długość n i kolejne wyrazy ciągu liczb B,

→ sprawdza, czy jest on kodem jakiejś permutacji liczb 1, …, n,

→ jeżeli tak, to znajduje tę permutację i zapisuje ją na wyjściu, w przeciwnym przypadku zapisuje na wyjściu jedno słowo NIE.

Wejście

W  pierwszym wierszu wejścia jest zapisana dodatnia liczba całkowita n ⩽ 30 000. Jest to liczba wyrazów ciągu B. W każdym z kolejnych n wierszy jest zapisana jedna liczba całkowita nieujemna nie większa niż 30 000. Jest to kolejny wyraz ciągu B.

Wyjście

Na wyjściu należy zapisać:

→ w każdym z kolejnych n wierszy jeden wyraz permutacji A, której kodem jest dany ciąg B, zapisany na wejściu, albo

→ jedno słowo NIE, jeśli ciąg B nie jest kodem żadnej permutacji.

sztOF dIKs / KOdOWANIE pErMutACJI

(2)

Przykłady

Dla danych wejściowych:

7 0 0 1 0 2 0 4

poprawną odpowiedzią jest:

1 5 2 6 4 7 3

natomiast dla danych:

4 0 2 0 0

poprawnym wynikiem jest:

NIE

sztOF dIKs / KOdOWANIE pErMutACJI

(3)

Rozwiązanie

Pragnę przedstawić dwa zadania mojego autorstwa, pochodzące z zawodów Olimpiady Informatycznej. Oba zadania tylko z pozoru są bardzo łatwe. Naj- prostsze rozwiązanie poda natychmiast nawet początkujący algorytmik. Takie rozwiązania ad hoc okazują się jednak zbyt wolne, a prawdziwym wyzwaniem jest zaproponowanie rozwiązań znacząco szybszych.

Oba zadania dobrze ilustrują moją filozofię układania zadań dla młodych adeptów algorytmiki. Proponując zadania dla uczestników zawodów infor- matycznych, nie tyle myślę o tym, żeby były one oryginalne, ale żeby niosły ze sobą jakiś przekaz, np. przedstawiały techniki projektowania algorytmów, ukazywały rolę struktur danych w efektywnej implementacji algorytmów, do- starczały szablonów do rozwiązywania podobnych problemów itp. Tego typu zadaniem jest Kodowanie permutacji, które można uznać za standardowe w roku 2012, ale ma ono walory dydaktyczne, które były kluczowe w uczeniu algoryt- miki w roku 1994 (brak podręczników, serwisów z zadaniami), a i dzisiaj jest ono bardzo pouczające. Podobne walory ma zadanie Wieże. Oprócz wszystkich dobrych cech zadania Kodowanie permutacji, zadanie Wieże posiada jeszcze jedną zaletę. Pozwala ono na eksperymenty (zabawę) z prawdziwą szachownicą lub z kartką papieru.

Rok 1994 był dla mnie szczególnie ważny. To rok, w którym rozpocząłem przygodę z popularyzowaniem algorytmiki wśród polskiej młodzieży – rok po- czątku mojej pracy w Olimpiadzie Informatycznej. Praca z młodzieżą uzdolnioną informatycznie przyniosła mi, i nadal przynosi, dużo satysfakcji i wspaniałych przeżyć związanych z sukcesami młodych polskich informatyków. Miałem szczęście pracować ze zwycięzcami Międzynarodowej Olimpiady Informatycz- nej oraz mistrzami świata w programowaniu zespołowym. Olbrzymią radość sprawia mi obserwowanie, jak byli uczestnicy konkursów stają się znakomitymi naukowcami lub czołowymi pracownikami najlepszych firm informatycznych na świecie. A wszystko może zacząć się od próby rozwiązania zadań takich, jak te poniżej.

Spojrzenie abstrakcyjne

Początkujący algorytmicy często popełniają błąd polegający na myśleniu o al- gorytmie od razu w kategoriach implementacyjnych, a nie abstrakcyjnych.

Takie podejście niejednokrotnie wyklucza całą gamę rozwiązań. Dużo lepiej jest myśleć o możliwych rozwiązaniach algorytmicznych, operując abstrakcyjnymi

sztOF dIKs / KOdOWANIE pErMutACJI

(4)

obiektami, takimi jak na przykład zbiór i ciąg, oraz operacjami na tych obiektach (np. „dodaj element do zbioru”, „sprawdź, czy zbiór zawiera zadany element”,

„podaj i-ty element ciągu”). Dopiero gdy opiszemy algorytm w postaci abstrak- cyjnej, mamy wolną rękę w doborze takiej implementacji, która jest najlepsza w danym kontekście. Tutaj „najlepsza implementacja” oznacza implementację o możliwie najkrótszym czasie działania.

Spróbujmy rozwiązać zadanie Kodowanie permutacji w sposób zapropono- wany powyżej. Na początek zauważmy, że dla każdej n-elementowej permutacji A elementy jej kodu B spełniają następujący warunek: 0 ⩽ bi< i dla każdego i = 1, 2, …, n. Wynika stąd, że różnych możliwych kodów jest co najwyżej n!, ponieważ bi może przyjąć co najwyżej i różnych wartości. Z drugiej strony nietrudno pokazać, że dowolne dwie różne n-elementowe permutacje A i A′′

mają różne kody. W tym celu wystarczy rozważyć największy taki indeks j, że aj jest różne od a′′j. Bez straty ogólności przyjmijmy, że aj < a′′j. Wówczas bj

musi być większe od b′′j, ponieważ na lewo od aj w permutacji A znajdują się wszystkie elementy, które są większe od a′′j i leżą na lewo od a′′j w permutacji A′′, a ponadto znajduje się tam a′′j. Ponieważ permutacji jest n!, to różnych kodów jest też n!. Tak więc każdy ciąg B spełniający warunek

0 ⩽ bi< i dla każdego i = 1, 2, …, n (∗) jest (jednoznacznym) kodem pewnej permutacji.

Nietrudno w czasie liniowym sprawdzić, czy dany ciąg B spełnia warunek (∗), czyli, czy jest kodem pewnej permutacji. Zatem w dalszej części opisu za- kładamy, że B jest kodem pewnej permutacji i naszym celem jest „odkodowanie”

właśnie tej permutacji.

Rozpoczniemy od prostego spostrzeżenia, że bn jednoznacznie wskazuje na element w zbiorze kandydatów {n, n − 1, …, 1}, który należy umieścić na ostatniej pozycji w permutacji A. Element bn mówi, że na lewo od an w permutacji A jest dokładnie bn elementów większych od niego. Ponieważ an jest ostatnim ele- mentem w permutacji, to jest to element (bn+ 1)-szy co do wielkości w zbiorze {n, n − 1, …, 1}, licząc od największego. Jeśli teraz ze zbioru kandydatów usu- niemy an, to an 1 będzie równe elementowi (bn 1 + 1)-szemu co do wielkości w uaktualnionym zbiorze kandydatów {n, n − 1, …, 1} \ {an}. Element an 2 jest teraz elementem (bn 2 + 1)-szym co do wielkości w {n, n − 1, …, 1} \ {an,an 1} itd.

sztOF dIKs / KOdOWANIE pErMutACJI

(5)

Oto pełny, abstrakcyjny opis powyższego algorytmu:

Algorithm OdkodujPermutację((b1, …, bn)) Kandydaci := {n, n − 1, …, 1}

for i := n downto 1 do k := bi+ 1

e := element k-ty co do wielkości w Kandydaci ai := e

Kandydaci := Kandydaci\ {e}

return (a1, …, an)

Mając w ręku taki abstrakcyjny opis algorytmu, możemy się zastanawiać nad jego implementacją. Zarówno kod B, jak i obliczaną permutację A będziemy re- prezentowali przez n-elementowe tablice, odpowiednio B[1..n] i A[1..n]. Jednak najważniejszym elementem rozwiązania naszego zadania jest dobra implemen- tacja zbioru Kandydaci. Co wiemy o tym zbiorze? Na początku zbiór Kandydaci zawiera wszystkie liczby 1, 2, …, n. W każdym obrocie pętli znajdujemy w zbiorze Kandydaci element e, który jest w nim k-tym co do wielkości, licząc od najwięk- szego, dla pewnego k pomiędzy 1 a aktualną liczbą elementów w tym zbiorze.

Następnie (wskazany) element e usuwamy ze zbioru Kandydaci. W naszych rozwiązaniach połączymy te dwie operacje w jedną operację ZnajdźUsuń(k).

Widać, że złożoność algorytmu odkodowywania permutacji zależy od sposobu reprezentacji zbioru Kandydaci i implementacji operacji ZnajdźUsuń.

Implementacja algorytmu

Sposób 1 – tablica. W tej reprezentacji przyjmujemy, że zbiór Kandydaci jest reprezentowany przez n-elementową tablicę Kand[1..n] przyjmującą tylko war- tości zero lub jeden, przy czym Kand[e] = 1 tylko wtedy, gdy e jest w zbiorze Kandydaci. Gdy e nie ma w zbiorze, Kand[e] = 0. Przy takiej reprezentacji zbioru Kandydaci najprostsza implementacja funkcji ZnajdźUsuń polega na przegląda- niu tablicy Kand od strony prawej (od indeksu n) do lewej (do indeksu 1) w po- szukiwaniu k-tej jedynki od prawej strony. Indeks pozycji, na której znajduje się ta jedynka, to poszukiwany element w zbiorze Kandydaci. Usunięcie elementu polega na zapisaniu 0 w tablicy Kand na odpowiadającej mu pozycji.

sztOF dIKs / KOdOWANIE pErMutACJI

(6)

Function ZnajdźUsuń(k) licz := 0; e := n + 1 while licz < k do

e := e − 1

if Kand[e] = 1 then licz := licz + 1 Kand[e] := 0

return e

Nietrudno zauważyć, że znalezienie wartości e, która jest wynikiem działa- nia ZnajdźUsuń, wymaga przejrzenia n − e + 1 pozycji w tablicy Kand. Ponieważ każdy element zbioru Kandydaci musi w końcu zostać znaleziony, to koszt odko- dowywania permutacji wymaga przejrzenia łącznie ne =1(n − e + 1) = n(n + 1)/2 pozycji w tablicy Kand. Niestety odkodowywanie permutacji tym sposobem zawsze zabiera czas rzędu n2, niezależnie od postaci kodu permutacji B.

Sposób 2 – lista. Powodem tego, że poprzednie rozwiązanie zawsze daje czas kwadratowy, jest to, że przy poszukiwaniu k-tego elementu w zbiorze Kandydaci oglądamy zarówno elementy, które do tego zbioru aktualnie należą (1 w tablicy Kand), jak i te, które już kandydatami nie są (0 w tablicy Kand).

Żeby temu zapobiec, możemy zbiór kandydatów reprezentować jako uporząd- kowaną malejąco listę jego elementów. W tym przypadku znalezienie k-tego elementu w zbiorze polega na wyznaczeniu po prostu k-tego elementu listy, a do tego wystarczy „obejrzeć” tylko k elementów zbioru Kandydaci – k pierw- szych elementów listy. Usunięcie znalezionego elementu można już wykonać w czasie stałym. Jeśli przyjmiemy, że listę mamy zadaną przez pola nast[e], które wskazują na następny po e element listy (jeśli e jest ostatnim elementem listy, to nast[e] = 0), oraz że dostęp do listy jest dany przez wskaźnik Kand do dodatkowego elementu s (tzw. strażnika), którego pole nast[s] wskazuje na pierwszy element listy (kandydatów), to operację ZnajdźUsuń można zapisać następująco:

Function ZnajdźUsuń(k) licz := 1; f := Kand while licz < k do

f := nast[f]

licz := licz + 1

e := nast[f]; nast[f] := nast[e]

return e

sztOF dIKs / KOdOWANIE pErMutACJI

(7)

W tej implementacji koszt odkodowywania zależy od kodu B. Nietrudno zauważyć, że łączna liczba przejść po elementach zbioru Kandydaci wynosi

ni =1 (bi+ 1), która to suma przyjmuje wartość n (najmniejszą możliwą), gdy wszystkie bi są równe 0, oraz wartość n(n + 1)/2 (największą możliwą), gdy B = (0, 1, 2, …, n − 1). Zatem przy reprezentacji listowej czas odkodowywania waha się od liniowego do kwadratowego. Jak nasza implementacja zachowu- je się w średnim przypadku? To oczywiście zależy od rozkładu danych. Jeśli przyjmiemy, że B jest kodem losowej permutacji, a każda permutacja jest jed- nakowo prawdopodobna (czyli pojawia się z prawdopodobieństwem 1/n!), to można pokazać, że średnia łączna liczba przejść po elementach zmieniającej się listy Kand wynosi n(n + 2)/4.

Czy można nasze zadanie rozwiązać szybciej?

Sposób 3 – wyszukiwanie binarne i drzewo. Wróćmy do implementacji, w której zbiór Kandydaci jest reprezentowany przez tablicę zero-jedynkową Kan- d[1..n], i załóżmy, że mamy do dyspozycji „magiczną” funkcję IleJedynek(l, p), której wartością jest liczba jedynek w podtablicy Kand[l..p], czyli na pozy- cjach od l do p. Teraz do znalezienia k-tej jedynki od prawej strony można użyć wyszukiwania binarnego. Jeśli chcemy znaleźć k-tą jedynkę od prawej w Kand[l..p], dzielimy Kand[l..p] na dwie równe, z dokładnością do jednego elementu, podtablice Kand[l..s] i Kand[s + 1..p], gdzie s = ⌊(l + p) / 2⌋. Jeśli Ile- Jedynek(s + 1, p) ⩾ k, to kontynuujemy wyszukiwanie k-tej jedynki od prawej w podtablicy Kand[s + 1..p]. Jeśli natomiast IleJedynek(s + 1, p) < k, to szukamy jedynki na pozycji k − IleJedynek(s + 1, p) od prawej strony w Kand[l..s], ponie- waż pomijamy wszystkie jedynki z podtablicy Kand[s+1..p]. Nasze rozważania skonkretyzujemy z pomocą rekurencyjnej funkcji ZnajdźUsuń(k, l, p), która znaj- duje i usuwa k-tą jedynkę od prawej z podtablicy Kand[l..p]. W celu znalezienia i usunięcia k-tej jedynki z całej tablicy wywołujemy ZnajdźUsuń(k, 1, n).

Function ZnajdźUsuń(k, l, p) if l = p then

Kand[l] := 0 return l else

s := ⌊(l + p)/2⌋

if IleJedynek(s + 1,p) ⩾ k then return ZnajdźUsuń(k, s + 1,p)

else

return ZnajdźUsuń(k − IleJedynek(s + 1,p), l, s)

sztOF dIKs / KOdOWANIE pErMutACJI

(8)

Gdybyśmy funkcję IleJedynek potrafili obliczać w czasie stałym, to koszt wykonania ZnajdźUsuń(k, 1, n) wyniósłby O(log n), ponieważ za każdym razem przedział, do którego ograniczamy poszukiwanie, jest o połowę krótszy (z do- kładnością do jednego elementu) od przedziału, w którym znajdujemy się aktual- nie. Ale skąd wziąć odpowiednie wartości funkcji IleJedynek? Potrzebne warto- ści funkcji IleJedynek można stablicować. Załóżmy, że mamy tablicę IleJed[1..n]

taką, że dla l < p, które mogą być parametrami funkcji ZnajdźUsuń, IleJed[s]

= IleJedynek(s + 1, p), gdzie s = ⌊(l + p)/2⌋. Wówczas w funkcji ZnajdźUsuń wy- starczy zastąpić wywołanie IleJedynek(s + 1, p) przez IleJed[s]. A co z usuwa- niem elementu e ze zbioru Kandydaci? To też jest proste. Ilekroć poszukiwanie kontynuujemy w prawym podprzedziale Kand[s + 1..p], oznacza to, że z tego podprzedziału zostanie usunięta jedynka. Zatem IleJed[s] należy zmniejszyć o 1. Opisane pomysły konkretyzujemy w poniższej funkcji:

Function ZnajdźUsuń(k, l, p) if l = p then

Kand[l] := 0 return l else

s := ⌊(l + p)/2⌋

if IleJed[s] ⩾ k then IleJed[s] := IleJed[s] − 1 return ZnajdźUsuń(k, s + 1, p)

else

return ZnajdźUsuń(k − IleJed[s], l, s)

Pozostaje pokazać, w jaki sposób zainicjować tablicę IleJed. To nie jest trudne, gdyż na początku w tablicy Kand są same jedynki. Oto rekurencyjna, naturalna inicjalizacja tablicy IleJed:

Function IniIleJed(l, p) if l < p then

s := ⌊(l + p)/2⌋

IleJed[s] := p − s IniIleJed(l, s)

IniIleJed(s + 1, p)

sztOF dIKs / KOdOWANIE pErMutACJI

(9)

Pozostawiamy Czytelnikowi dowód, że koszt wykonania IniIleJed(1, n) jest liniowy. Ponadto, dociekliwy Czytelnik powinien zauważyć, że w ostatnim roz- wiązaniu tablica Kand jest nam niepotrzebna, jeśli nie liczyć jasności prezentacji.

Przedstawione zadanie jest jednym z najłatwiejszych w tej książce. Niejeden czytelnik zauważy, że mamy tu do czynienia z tzw. wektorami inwersji, które opisują trudność algorytmu sortowania przez wstawianie. Inwersją w ciągu liczbowym nazywamy każdą nieuporządkowaną parę elementów. Złożoność algorytmu sortowania przez wstawianie jest proporcjonalna do sumy długości sortowanego ciągu i liczby zawartych w nim inwersji.

Mimo że zadanie jest proste, pozwala ono przedstawić pewne sposoby atakowania problemów algorytmicznych w celu uzyskania jak najlepszego (w terminach złożoności czasowej) rozwiązania. Zaczynamy od rozwiązania abstrakcyjnego, a następnie poszukujemy jak najlepszych sposobów implemen- tacji abstrakcyjnych obiektów. To zadanie ma jeszcze jedną zaletę. Może ono posłużyć do omówienia różnorodnych struktur danych (tablic, list i drzew), metod przeszukiwania (liniowej i binarnej), rekurencji, pojęć czasowej złożo- ności obliczeniowej (pesymistycznej i oczekiwanej) itd.

Odwracamy problem

Na koniec tego opisu zastanówmy się, jak szybko można rozwiązać zadanie

„odwrotne” – z permutacji A otrzymać jej kod B. Naturalne rozwiązanie polega na przejrzeniu permutacji A od lewej do prawej i policzeniu dla każdego elemen- tu aj liczby elementów w A większych od aj i położonych na lewo od aj. Prosta implementacja tego algorytmu działa w czasie O(n2). Można go jednak zaimple- mentować z pomocą zrównoważonych drzew wyszukiwań binarnych, otrzy- mując złożoność O(n log n). Tutaj jednak przedstawimy inny sposób obliczania kodu B w czasie O(n log n), w którym nie korzystamy z żadnych złożonych struktur danych. Pomysł tego rozwiązania pochodzi od prof. Wojciecha Ryttera.

Czasami zdarza się, że łatwiej jest rozwiązać problem ogólniejszy od pro- blemu wyjściowego, a także udowodnić poprawność takiego rozwiązania. Roz- ważmy następujące, ogólniejsze zadanie.

Zadanie. Dana jest tablica A[1..n] liczb całkowitych z przedziału od 0 do n−1, niekoniecznie różnych. Należy obliczyć tablicę B[1..n] taką, że B[j] jest liczbą elementów większych od A[j] i położonych na lewo od A[j], czyli

B[j] = |{1 ⩽ k < j : A[k] > A[j]}|.

sztOF dIKs / KOdOWANIE pErMutACJI

(10)

Zauważmy, że jeżeli A jest permutacją liczb {1, …, n}, to po odjęciu jedynki od każdego A[j] i policzeniu tablicy B dostaniemy kod permutacji A.

Nasze rozwiązanie wykorzystuje następującą prostą obserwację:

Obserwacja. Dla dowolnych nieujemnych, różnych liczb całkowitych a < b za- chodzi ⌊a/2⌋ = ⌊b/2⌋ wtedy i tylko wtedy, gdy a = 2k i b = 2k + 1, dla pewnego całkowitego k ⩾ 0.

Co daje nam ta obserwacja? Jeśli podzielimy każdy element A[j] całkowicie przez 2, to liczba par (A[l], A[p]) takich, że A[l] > A[p] i l < p, pozostanie taka sama, nie licząc par, dla których przed podzieleniem zachodziło A[l] = 2k + 1 i A[p] = 2k, dla pewnego całkowitego k ⩾ 0. Ta obserwacja umożliwia zapro- ponowanie następującego algorytmu.

Dopóki w tablicy A jest co najmniej jeden element większy od zera, dla każ- dego elementu parzystego A[j] oblicz, ile jest elementów (nieparzystych) o jeden większych od niego i położonych z lewej strony. Tak wyznaczoną liczbę dodaj do ogólnej liczby elementów większych od elementu na pozycji j-tej w wyjściowej tablicy A i położonych na lewo od niego. Następnie każdy element w A podziel całkowicie przez 2 i ponów powyższe obliczenia.

Poniższy program konkretyzuje nasze pomysły. W tablicy Nieparzyste[0..n]

zliczamy wystąpienia poszczególnych liczb nieparzystych pojawiających przy przeglądaniu (zmieniającej) się tablicy A od strony lewej do prawej.

sztOF dIKs / KOdOWANIE pErMutACJI

(11)

Algorithm ZakodujPermutację(A) for i := 1 to n do

B[i] := 0 ile_zer := 0

while ile_zer < n do for i := 0 to n do Nieparzyste[i] := 0

ile_zer := 0 for i := 1 to n do if 2 ∤ A[i] then

Nieparzyste[A[i]] := Nieparzyste[A[i]] + 1 else

B[i] := B[i] + Nieparzyste[A[i] + 1]

A[i] := A[i] div 2 if A[i] = 0 then

ile_zer := ile_zer + 1 return B

Każda iteracja pętli while wykonuje się w czasie O(n). Iteracji jest co najwyżej

⌈log(n + 1)⌉. Zatem cały algorytm działa w czasie O(n log n).

sztOF dIKs / KOdOWANIE pErMutACJI

Cytaty

Powiązane dokumenty

Dla dodatniej liczby naturalnej n znaleźć wzór na największą potęgę liczby pierwszej p dzielącą n!4. Rozłożyć na czynniki pierwsze

Pokazać, że każdy operator śladowy jest iloczynem dwu operatorów

Pokazać, że dla podzbioru A w przestrzeni Hilberta, A ⊥⊥ jest najmniejszą domkniętą podprze- strzenią zawierającą

Pokazać, że iloczyn skalarny na przestrzeni z iloczynem skalarnym jest ograniczoną formą pół- toraliniową.. 2.. ), dla ustalonego ograniczonego ciągu

Gdy odległość pomiędzy pociągami wynosi 1 km, pszczoła zaczyna latać tam i z powrotem pomiędzy pociągami z prędkością 60 km na godzinę.. Wyrazić od- ległość jaką

Zbadać, w jakim kole jest zbieżny szereg MacLaurina funkcji tgh z.. Znaleźć kilka pierwszych

Zbiór funkcji nieparzystych oznaczymy literą N, natomiast zbiór funkcji parzystych - literą P..

Obieramy dowolny punkt X na symetralnej AB, wpisujemy okr ag , w trójk at ABX oraz dopisujemy doń okr , ag styczny do odcinka AB.. Pokazać, że iloczyn rR